diff --git a/administrator/components/com_actionlogs/services/provider.php b/administrator/components/com_actionlogs/services/provider.php index 0cad8c5826d5d..3d81708bd9fe0 100644 --- a/administrator/components/com_actionlogs/services/provider.php +++ b/administrator/components/com_actionlogs/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Actionlogs')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Actionlogs')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Actionlogs')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Actionlogs')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_actionlogs/src/Controller/ActionlogsController.php b/administrator/components/com_actionlogs/src/Controller/ActionlogsController.php index 6b429f54278e3..0c28230097e8b 100644 --- a/administrator/components/com_actionlogs/src/Controller/ActionlogsController.php +++ b/administrator/components/com_actionlogs/src/Controller/ActionlogsController.php @@ -1,4 +1,5 @@ registerTask('exportSelectedLogs', 'exportLogs'); - } - - /** - * Method to export logs - * - * @return void - * - * @since 3.9.0 - * - * @throws Exception - */ - public function exportLogs() - { - // Check for request forgeries. - $this->checkToken(); - - $task = $this->getTask(); - - $pks = array(); - - if ($task == 'exportSelectedLogs') - { - // Get selected logs - $pks = ArrayHelper::toInteger(explode(',', $this->input->post->getString('cids'))); - } - - /** @var ActionlogsModel $model */ - $model = $this->getModel(); - - // Get the logs data - $data = $model->getLogDataAsIterator($pks); - - if (\count($data)) - { - try - { - $rows = ActionlogsHelper::getCsvData($data); - } - catch (InvalidArgumentException $exception) - { - $this->setMessage(Text::_('COM_ACTIONLOGS_ERROR_COULD_NOT_EXPORT_DATA'), 'error'); - $this->setRedirect(Route::_('index.php?option=com_actionlogs&view=actionlogs', false)); - - return; - } - - // Destroy the iterator now - unset($data); - - $date = new Date('now', new DateTimeZone('UTC')); - $filename = 'logs_' . $date->format('Y-m-d_His_T'); - - $csvDelimiter = ComponentHelper::getComponent('com_actionlogs')->getParams()->get('csv_delimiter', ','); - - $this->app->setHeader('Content-Type', 'application/csv', true) - ->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '.csv"', true) - ->setHeader('Cache-Control', 'must-revalidate', true) - ->sendHeaders(); - - $output = fopen("php://output", "w"); - - foreach ($rows as $row) - { - fputcsv($output, $row, $csvDelimiter); - } - - fclose($output); - $this->app->triggerEvent('onAfterLogExport', array()); - $this->app->close(); - } - else - { - $this->setMessage(Text::_('COM_ACTIONLOGS_NO_LOGS_TO_EXPORT')); - $this->setRedirect(Route::_('index.php?option=com_actionlogs&view=actionlogs', false)); - } - } - - /** - * 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 object The model. - * - * @since 3.9.0 - */ - public function getModel($name = 'Actionlogs', $prefix = 'Administrator', $config = ['ignore_request' => true]) - { - // Return the model - return parent::getModel($name, $prefix, $config); - } - - /** - * Clean out the logs - * - * @return void - * - * @since 3.9.0 - */ - public function purge() - { - // Check for request forgeries. - $this->checkToken(); - - $model = $this->getModel(); - - if ($model->purge()) - { - $message = Text::_('COM_ACTIONLOGS_PURGE_SUCCESS'); - } - else - { - $message = Text::_('COM_ACTIONLOGS_PURGE_FAIL'); - } - - $this->setRedirect(Route::_('index.php?option=com_actionlogs&view=actionlogs', false), $message); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * 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 Application for the dispatcher + * @param Input $input Input + * + * @since 3.9.0 + * + * @throws Exception + */ + public function __construct($config = [], MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('exportSelectedLogs', 'exportLogs'); + } + + /** + * Method to export logs + * + * @return void + * + * @since 3.9.0 + * + * @throws Exception + */ + public function exportLogs() + { + // Check for request forgeries. + $this->checkToken(); + + $task = $this->getTask(); + + $pks = array(); + + if ($task == 'exportSelectedLogs') { + // Get selected logs + $pks = ArrayHelper::toInteger(explode(',', $this->input->post->getString('cids'))); + } + + /** @var ActionlogsModel $model */ + $model = $this->getModel(); + + // Get the logs data + $data = $model->getLogDataAsIterator($pks); + + if (\count($data)) { + try { + $rows = ActionlogsHelper::getCsvData($data); + } catch (InvalidArgumentException $exception) { + $this->setMessage(Text::_('COM_ACTIONLOGS_ERROR_COULD_NOT_EXPORT_DATA'), 'error'); + $this->setRedirect(Route::_('index.php?option=com_actionlogs&view=actionlogs', false)); + + return; + } + + // Destroy the iterator now + unset($data); + + $date = new Date('now', new DateTimeZone('UTC')); + $filename = 'logs_' . $date->format('Y-m-d_His_T'); + + $csvDelimiter = ComponentHelper::getComponent('com_actionlogs')->getParams()->get('csv_delimiter', ','); + + $this->app->setHeader('Content-Type', 'application/csv', true) + ->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '.csv"', true) + ->setHeader('Cache-Control', 'must-revalidate', true) + ->sendHeaders(); + + $output = fopen("php://output", "w"); + + foreach ($rows as $row) { + fputcsv($output, $row, $csvDelimiter); + } + + fclose($output); + $this->app->triggerEvent('onAfterLogExport', array()); + $this->app->close(); + } else { + $this->setMessage(Text::_('COM_ACTIONLOGS_NO_LOGS_TO_EXPORT')); + $this->setRedirect(Route::_('index.php?option=com_actionlogs&view=actionlogs', false)); + } + } + + /** + * 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 object The model. + * + * @since 3.9.0 + */ + public function getModel($name = 'Actionlogs', $prefix = 'Administrator', $config = ['ignore_request' => true]) + { + // Return the model + return parent::getModel($name, $prefix, $config); + } + + /** + * Clean out the logs + * + * @return void + * + * @since 3.9.0 + */ + public function purge() + { + // Check for request forgeries. + $this->checkToken(); + + $model = $this->getModel(); + + if ($model->purge()) { + $message = Text::_('COM_ACTIONLOGS_PURGE_SUCCESS'); + } else { + $message = Text::_('COM_ACTIONLOGS_PURGE_FAIL'); + } + + $this->setRedirect(Route::_('index.php?option=com_actionlogs&view=actionlogs', false), $message); + } } diff --git a/administrator/components/com_actionlogs/src/Controller/DisplayController.php b/administrator/components/com_actionlogs/src/Controller/DisplayController.php index 4c51bfe7ddbe5..d1c05eabe6901 100644 --- a/administrator/components/com_actionlogs/src/Controller/DisplayController.php +++ b/administrator/components/com_actionlogs/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('extension')) - ->from($db->quoteName('#__action_logs')) - ->order($db->quoteName('extension')); + /** + * Method to get the options to populate list + * + * @return array The field option objects. + * + * @since 3.9.0 + */ + public function getOptions() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('extension')) + ->from($db->quoteName('#__action_logs')) + ->order($db->quoteName('extension')); - $db->setQuery($query); - $context = $db->loadColumn(); + $db->setQuery($query); + $context = $db->loadColumn(); - $options = array(); + $options = array(); - if (\count($context) > 0) - { - foreach ($context as $item) - { - $extensions[] = strtok($item, '.'); - } + if (\count($context) > 0) { + foreach ($context as $item) { + $extensions[] = strtok($item, '.'); + } - $extensions = array_unique($extensions); + $extensions = array_unique($extensions); - foreach ($extensions as $extension) - { - ActionlogsHelper::loadTranslationFiles($extension); - $options[] = HTMLHelper::_('select.option', $extension, Text::_($extension)); - } - } + foreach ($extensions as $extension) { + ActionlogsHelper::loadTranslationFiles($extension); + $options[] = HTMLHelper::_('select.option', $extension, Text::_($extension)); + } + } - return array_merge(parent::getOptions(), $options); - } + return array_merge(parent::getOptions(), $options); + } } diff --git a/administrator/components/com_actionlogs/src/Field/LogcreatorField.php b/administrator/components/com_actionlogs/src/Field/LogcreatorField.php index 0db00673751ca..3a85d96270cb1 100644 --- a/administrator/components/com_actionlogs/src/Field/LogcreatorField.php +++ b/administrator/components/com_actionlogs/src/Field/LogcreatorField.php @@ -1,4 +1,5 @@ element); + /** + * Method to get the options to populate list + * + * @return array The field option objects. + * + * @since 3.9.0 + */ + protected function getOptions() + { + // Accepted modifiers + $hash = md5($this->element); - if (!isset(static::$options[$hash])) - { - static::$options[$hash] = parent::getOptions(); + if (!isset(static::$options[$hash])) { + static::$options[$hash] = parent::getOptions(); - $db = $this->getDatabase(); + $db = $this->getDatabase(); - // Construct the query - $query = $db->getQuery(true) - ->select($db->quoteName('u.id', 'value')) - ->select($db->quoteName('u.username', 'text')) - ->from($db->quoteName('#__users', 'u')) - ->join('INNER', $db->quoteName('#__action_logs', 'c') . ' ON ' . $db->quoteName('c.user_id') . ' = ' . $db->quoteName('u.id')) - ->group($db->quoteName('u.id')) - ->group($db->quoteName('u.username')) - ->order($db->quoteName('u.username')); + // Construct the query + $query = $db->getQuery(true) + ->select($db->quoteName('u.id', 'value')) + ->select($db->quoteName('u.username', 'text')) + ->from($db->quoteName('#__users', 'u')) + ->join('INNER', $db->quoteName('#__action_logs', 'c') . ' ON ' . $db->quoteName('c.user_id') . ' = ' . $db->quoteName('u.id')) + ->group($db->quoteName('u.id')) + ->group($db->quoteName('u.username')) + ->order($db->quoteName('u.username')); - // Setup the query - $db->setQuery($query); + // Setup the query + $db->setQuery($query); - // Return the result - if ($options = $db->loadObjectList()) - { - static::$options[$hash] = array_merge(static::$options[$hash], $options); - } - } + // Return the result + if ($options = $db->loadObjectList()) { + static::$options[$hash] = array_merge(static::$options[$hash], $options); + } + } - return static::$options[$hash]; - } + return static::$options[$hash]; + } } diff --git a/administrator/components/com_actionlogs/src/Field/LogsdaterangeField.php b/administrator/components/com_actionlogs/src/Field/LogsdaterangeField.php index 89f94ac6f2d61..9a4891fbc5d78 100644 --- a/administrator/components/com_actionlogs/src/Field/LogsdaterangeField.php +++ b/administrator/components/com_actionlogs/src/Field/LogsdaterangeField.php @@ -1,4 +1,5 @@ 'COM_ACTIONLOGS_OPTION_RANGE_TODAY', - 'past_week' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_WEEK', - 'past_1month' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_1MONTH', - 'past_3month' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_3MONTH', - 'past_6month' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_6MONTH', - 'past_year' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_YEAR', - ); + /** + * Available options + * + * @var array + * @since 3.9.0 + */ + protected $predefinedOptions = array( + 'today' => 'COM_ACTIONLOGS_OPTION_RANGE_TODAY', + 'past_week' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_WEEK', + 'past_1month' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_1MONTH', + 'past_3month' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_3MONTH', + 'past_6month' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_6MONTH', + 'past_year' => 'COM_ACTIONLOGS_OPTION_RANGE_PAST_YEAR', + ); - /** - * Method to instantiate the form field object. - * - * @param Form $form The form to attach to the form field object. - * - * @since 3.9.0 - */ - public function __construct($form = null) - { - parent::__construct($form); + /** + * Method to instantiate the form field object. + * + * @param Form $form The form to attach to the form field object. + * + * @since 3.9.0 + */ + public function __construct($form = null) + { + parent::__construct($form); - // Load the required language - $lang = Factory::getLanguage(); - $lang->load('com_actionlogs', JPATH_ADMINISTRATOR); - } + // Load the required language + $lang = Factory::getLanguage(); + $lang->load('com_actionlogs', JPATH_ADMINISTRATOR); + } } diff --git a/administrator/components/com_actionlogs/src/Field/LogtypeField.php b/administrator/components/com_actionlogs/src/Field/LogtypeField.php index 6bffb3de8267b..e63b6b93d90cc 100644 --- a/administrator/components/com_actionlogs/src/Field/LogtypeField.php +++ b/administrator/components/com_actionlogs/src/Field/LogtypeField.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('extension')) - ->from($db->quoteName('#__action_logs_extensions')); - - $extensions = $db->setQuery($query)->loadColumn(); - - $options = []; - - foreach ($extensions as $extension) - { - ActionlogsHelper::loadTranslationFiles($extension); - $extensionName = Text::_($extension); - $options[ApplicationHelper::stringURLSafe($extensionName) . '_' . $extension] = HTMLHelper::_('select.option', $extension, $extensionName); - } - - ksort($options); - - return array_merge(parent::getOptions(), array_values($options)); - } + /** + * The form field type. + * + * @var string + * @since 3.9.0 + */ + protected $type = 'LogType'; + + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.9.0 + */ + public function getOptions() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('extension')) + ->from($db->quoteName('#__action_logs_extensions')); + + $extensions = $db->setQuery($query)->loadColumn(); + + $options = []; + + foreach ($extensions as $extension) { + ActionlogsHelper::loadTranslationFiles($extension); + $extensionName = Text::_($extension); + $options[ApplicationHelper::stringURLSafe($extensionName) . '_' . $extension] = HTMLHelper::_('select.option', $extension, $extensionName); + } + + ksort($options); + + return array_merge(parent::getOptions(), array_values($options)); + } } diff --git a/administrator/components/com_actionlogs/src/Field/PlugininfoField.php b/administrator/components/com_actionlogs/src/Field/PlugininfoField.php index 6b225cee279e7..cd281af35425c 100644 --- a/administrator/components/com_actionlogs/src/Field/PlugininfoField.php +++ b/administrator/components/com_actionlogs/src/Field/PlugininfoField.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('actionlog')) - ->where($db->quoteName('element') . ' = ' . $db->quote('joomla')); - $db->setQuery($query); + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 3.9.2 + */ + protected function getInput() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('actionlog')) + ->where($db->quoteName('element') . ' = ' . $db->quote('joomla')); + $db->setQuery($query); - $result = (int) $db->loadResult(); + $result = (int) $db->loadResult(); - $link = HTMLHelper::_( - 'link', - Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . $result), - Text::_('PLG_SYSTEM_ACTIONLOGS_JOOMLA_ACTIONLOG_DISABLED'), - array('class' => 'alert-link') - ); + $link = HTMLHelper::_( + 'link', + Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . $result), + Text::_('PLG_SYSTEM_ACTIONLOGS_JOOMLA_ACTIONLOG_DISABLED'), + array('class' => 'alert-link') + ); - return '
' - . '' - . Text::_('INFO') - . '' - . Text::sprintf('PLG_SYSTEM_ACTIONLOGS_JOOMLA_ACTIONLOG_DISABLED_REDIRECT', $link) - . '
'; - } + return '
' + . '' + . Text::_('INFO') + . '' + . Text::sprintf('PLG_SYSTEM_ACTIONLOGS_JOOMLA_ACTIONLOG_DISABLED_REDIRECT', $link) + . '
'; + } } diff --git a/administrator/components/com_actionlogs/src/Helper/ActionlogsHelper.php b/administrator/components/com_actionlogs/src/Helper/ActionlogsHelper.php index 309ed9d5dcc11..26bb763c612bf 100644 --- a/administrator/components/com_actionlogs/src/Helper/ActionlogsHelper.php +++ b/administrator/components/com_actionlogs/src/Helper/ActionlogsHelper.php @@ -1,4 +1,5 @@ extension, '.'); - - static::loadTranslationFiles($extension); - - yield array( - 'id' => $log->id, - 'message' => self::escapeCsvFormula(strip_tags(static::getHumanReadableLogMessage($log, false))), - 'extension' => self::escapeCsvFormula(Text::_($extension)), - 'date' => (new Date($log->log_date, new \DateTimeZone('UTC')))->format('Y-m-d H:i:s T'), - 'name' => self::escapeCsvFormula($log->name), - 'ip_address' => self::escapeCsvFormula($log->ip_address === 'COM_ACTIONLOGS_DISABLED' ? $disabledText : $log->ip_address) - ); - } - } - - /** - * Load the translation files for an extension - * - * @param string $extension Extension name - * - * @return void - * - * @since 3.9.0 - */ - public static function loadTranslationFiles($extension) - { - static $cache = array(); - $extension = strtolower($extension); - - if (isset($cache[$extension])) - { - return; - } - - $lang = Factory::getLanguage(); - $source = ''; - - switch (substr($extension, 0, 3)) - { - case 'com': - default: - $source = JPATH_ADMINISTRATOR . '/components/' . $extension; - break; - - case 'lib': - $source = JPATH_LIBRARIES . '/' . substr($extension, 4); - break; - - case 'mod': - $source = JPATH_SITE . '/modules/' . $extension; - break; - - case 'plg': - $parts = explode('_', $extension, 3); - - if (\count($parts) > 2) - { - $source = JPATH_PLUGINS . '/' . $parts[1] . '/' . $parts[2]; - } - break; - - case 'pkg': - $source = JPATH_SITE; - break; - - case 'tpl': - $source = JPATH_BASE . '/templates/' . substr($extension, 4); - break; - - } - - $lang->load($extension, JPATH_ADMINISTRATOR) - || $lang->load($extension, $source); - - if (!$lang->hasKey(strtoupper($extension))) - { - $lang->load($extension . '.sys', JPATH_ADMINISTRATOR) - || $lang->load($extension . '.sys', $source); - } - - $cache[$extension] = true; - } - - /** - * Get parameters to be - * - * @param string $context The context of the content - * - * @return mixed An object contains content type parameters, or null if not found - * - * @since 3.9.0 - * - * @deprecated 5.0 Use the action log config model instead - */ - public static function getLogContentTypeParams($context) - { - return Factory::getApplication()->bootComponent('actionlogs')->getMVCFactory() - ->createModel('ActionlogConfig', 'Administrator')->getLogContentTypeParams($context); - } - - /** - * Get human readable log message for a User Action Log - * - * @param \stdClass $log A User Action log message record - * @param boolean $generateLinks Flag to disable link generation when creating a message - * - * @return string - * - * @since 3.9.0 - */ - public static function getHumanReadableLogMessage($log, $generateLinks = true) - { - static $links = array(); - - $message = Text::_($log->message_language_key); - $messageData = json_decode($log->message, true); - - // Special handling for translation extension name - if (isset($messageData['extension_name'])) - { - static::loadTranslationFiles($messageData['extension_name']); - $messageData['extension_name'] = Text::_($messageData['extension_name']); - } - - // Translating application - if (isset($messageData['app'])) - { - $messageData['app'] = Text::_($messageData['app']); - } - - // Translating type - if (isset($messageData['type'])) - { - $messageData['type'] = Text::_($messageData['type']); - } - - $linkMode = Factory::getApplication()->get('force_ssl', 0) >= 1 ? Route::TLS_FORCE : Route::TLS_IGNORE; - - foreach ($messageData as $key => $value) - { - // Escape any markup in the values to prevent XSS attacks - $value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); - - // Convert relative url to absolute url so that it is clickable in action logs notification email - if ($generateLinks && StringHelper::strpos($value, 'index.php?') === 0) - { - if (!isset($links[$value])) - { - $links[$value] = Route::link('administrator', $value, false, $linkMode, true); - } - - $value = $links[$value]; - } - - $message = str_replace('{' . $key . '}', $value, $message); - } - - return $message; - } - - /** - * Get link to an item of given content type - * - * @param string $component - * @param string $contentType - * @param integer $id - * @param string $urlVar - * @param CMSObject $object - * - * @return string Link to the content item - * - * @since 3.9.0 - */ - public static function getContentTypeLink($component, $contentType, $id, $urlVar = 'id', $object = null) - { - // Try to find the component helper. - $eName = str_replace('com_', '', $component); - $file = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/helpers/' . $eName . '.php'); - - if (file_exists($file)) - { - $prefix = ucfirst(str_replace('com_', '', $component)); - $cName = $prefix . 'Helper'; - - \JLoader::register($cName, $file); - - if (class_exists($cName) && \is_callable(array($cName, 'getContentTypeLink'))) - { - return $cName::getContentTypeLink($contentType, $id, $object); - } - } - - if (empty($urlVar)) - { - $urlVar = 'id'; - } - - // Return default link to avoid having to implement getContentTypeLink in most of our components - return 'index.php?option=' . $component . '&task=' . $contentType . '.edit&' . $urlVar . '=' . $id; - } - - /** - * Load both enabled and disabled actionlog plugins language file. - * - * It is used to make sure actions log is displayed properly instead of only language items displayed when a plugin is disabled. - * - * @return void - * - * @since 3.9.0 - */ - public static function loadActionLogPluginsLanguage() - { - $lang = Factory::getLanguage(); - $db = Factory::getDbo(); - - // Get all (both enabled and disabled) actionlog plugins - $query = $db->getQuery(true) - ->select( - $db->quoteName( - array( - 'folder', - 'element', - 'params', - 'extension_id' - ), - array( - 'type', - 'name', - 'params', - 'id' - ) - ) - ) - ->from('#__extensions') - ->where('type = ' . $db->quote('plugin')) - ->where('folder = ' . $db->quote('actionlog')) - ->where('state IN (0,1)') - ->order('ordering'); - $db->setQuery($query); - - try - { - $rows = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $rows = array(); - } - - if (empty($rows)) - { - return; - } - - foreach ($rows as $row) - { - $name = $row->name; - $type = $row->type; - $extension = 'Plg_' . $type . '_' . $name; - $extension = strtolower($extension); - - // If language already loaded, don't load it again. - if ($lang->getPaths($extension)) - { - continue; - } - - $lang->load($extension, JPATH_ADMINISTRATOR) - || $lang->load($extension, JPATH_PLUGINS . '/' . $type . '/' . $name); - } - - // Load plg_system_actionlogs too - $lang->load('plg_system_actionlogs', JPATH_ADMINISTRATOR); - - // Load com_privacy too. - $lang->load('com_privacy', JPATH_ADMINISTRATOR); - } - - /** - * Escapes potential characters that start a formula in a CSV value to prevent injection attacks - * - * @param mixed $value csv field value - * - * @return mixed - * - * @since 3.9.7 - */ - protected static function escapeCsvFormula($value) - { - if ($value == '') - { - return $value; - } - - if (\in_array($value[0], self::$characters, true)) - { - $value = ' ' . $value; - } - - return $value; - } + /** + * Array of characters starting a formula + * + * @var array + * + * @since 3.9.7 + */ + private static $characters = array('=', '+', '-', '@'); + + /** + * Method to convert logs objects array to an iterable type for use with a CSV export + * + * @param array|\Traversable $data The logs data objects to be exported + * + * @return Generator + * + * @since 3.9.0 + * + * @throws \InvalidArgumentException + */ + public static function getCsvData($data): Generator + { + if (!is_iterable($data)) { + throw new \InvalidArgumentException( + sprintf( + '%s() requires an array or object implementing the Traversable interface, a %s was given.', + __METHOD__, + \gettype($data) === 'object' ? \get_class($data) : \gettype($data) + ) + ); + } + + $disabledText = Text::_('COM_ACTIONLOGS_DISABLED'); + + // Header row + yield ['Id', 'Action', 'Extension', 'Date', 'Name', 'IP Address']; + + foreach ($data as $log) { + $extension = strtok($log->extension, '.'); + + static::loadTranslationFiles($extension); + + yield array( + 'id' => $log->id, + 'message' => self::escapeCsvFormula(strip_tags(static::getHumanReadableLogMessage($log, false))), + 'extension' => self::escapeCsvFormula(Text::_($extension)), + 'date' => (new Date($log->log_date, new \DateTimeZone('UTC')))->format('Y-m-d H:i:s T'), + 'name' => self::escapeCsvFormula($log->name), + 'ip_address' => self::escapeCsvFormula($log->ip_address === 'COM_ACTIONLOGS_DISABLED' ? $disabledText : $log->ip_address) + ); + } + } + + /** + * Load the translation files for an extension + * + * @param string $extension Extension name + * + * @return void + * + * @since 3.9.0 + */ + public static function loadTranslationFiles($extension) + { + static $cache = array(); + $extension = strtolower($extension); + + if (isset($cache[$extension])) { + return; + } + + $lang = Factory::getLanguage(); + $source = ''; + + switch (substr($extension, 0, 3)) { + case 'com': + default: + $source = JPATH_ADMINISTRATOR . '/components/' . $extension; + break; + + case 'lib': + $source = JPATH_LIBRARIES . '/' . substr($extension, 4); + break; + + case 'mod': + $source = JPATH_SITE . '/modules/' . $extension; + break; + + case 'plg': + $parts = explode('_', $extension, 3); + + if (\count($parts) > 2) { + $source = JPATH_PLUGINS . '/' . $parts[1] . '/' . $parts[2]; + } + break; + + case 'pkg': + $source = JPATH_SITE; + break; + + case 'tpl': + $source = JPATH_BASE . '/templates/' . substr($extension, 4); + break; + } + + $lang->load($extension, JPATH_ADMINISTRATOR) + || $lang->load($extension, $source); + + if (!$lang->hasKey(strtoupper($extension))) { + $lang->load($extension . '.sys', JPATH_ADMINISTRATOR) + || $lang->load($extension . '.sys', $source); + } + + $cache[$extension] = true; + } + + /** + * Get parameters to be + * + * @param string $context The context of the content + * + * @return mixed An object contains content type parameters, or null if not found + * + * @since 3.9.0 + * + * @deprecated 5.0 Use the action log config model instead + */ + public static function getLogContentTypeParams($context) + { + return Factory::getApplication()->bootComponent('actionlogs')->getMVCFactory() + ->createModel('ActionlogConfig', 'Administrator')->getLogContentTypeParams($context); + } + + /** + * Get human readable log message for a User Action Log + * + * @param \stdClass $log A User Action log message record + * @param boolean $generateLinks Flag to disable link generation when creating a message + * + * @return string + * + * @since 3.9.0 + */ + public static function getHumanReadableLogMessage($log, $generateLinks = true) + { + static $links = array(); + + $message = Text::_($log->message_language_key); + $messageData = json_decode($log->message, true); + + // Special handling for translation extension name + if (isset($messageData['extension_name'])) { + static::loadTranslationFiles($messageData['extension_name']); + $messageData['extension_name'] = Text::_($messageData['extension_name']); + } + + // Translating application + if (isset($messageData['app'])) { + $messageData['app'] = Text::_($messageData['app']); + } + + // Translating type + if (isset($messageData['type'])) { + $messageData['type'] = Text::_($messageData['type']); + } + + $linkMode = Factory::getApplication()->get('force_ssl', 0) >= 1 ? Route::TLS_FORCE : Route::TLS_IGNORE; + + foreach ($messageData as $key => $value) { + // Escape any markup in the values to prevent XSS attacks + $value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); + + // Convert relative url to absolute url so that it is clickable in action logs notification email + if ($generateLinks && StringHelper::strpos($value, 'index.php?') === 0) { + if (!isset($links[$value])) { + $links[$value] = Route::link('administrator', $value, false, $linkMode, true); + } + + $value = $links[$value]; + } + + $message = str_replace('{' . $key . '}', $value, $message); + } + + return $message; + } + + /** + * Get link to an item of given content type + * + * @param string $component + * @param string $contentType + * @param integer $id + * @param string $urlVar + * @param CMSObject $object + * + * @return string Link to the content item + * + * @since 3.9.0 + */ + public static function getContentTypeLink($component, $contentType, $id, $urlVar = 'id', $object = null) + { + // Try to find the component helper. + $eName = str_replace('com_', '', $component); + $file = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/helpers/' . $eName . '.php'); + + if (file_exists($file)) { + $prefix = ucfirst(str_replace('com_', '', $component)); + $cName = $prefix . 'Helper'; + + \JLoader::register($cName, $file); + + if (class_exists($cName) && \is_callable(array($cName, 'getContentTypeLink'))) { + return $cName::getContentTypeLink($contentType, $id, $object); + } + } + + if (empty($urlVar)) { + $urlVar = 'id'; + } + + // Return default link to avoid having to implement getContentTypeLink in most of our components + return 'index.php?option=' . $component . '&task=' . $contentType . '.edit&' . $urlVar . '=' . $id; + } + + /** + * Load both enabled and disabled actionlog plugins language file. + * + * It is used to make sure actions log is displayed properly instead of only language items displayed when a plugin is disabled. + * + * @return void + * + * @since 3.9.0 + */ + public static function loadActionLogPluginsLanguage() + { + $lang = Factory::getLanguage(); + $db = Factory::getDbo(); + + // Get all (both enabled and disabled) actionlog plugins + $query = $db->getQuery(true) + ->select( + $db->quoteName( + array( + 'folder', + 'element', + 'params', + 'extension_id' + ), + array( + 'type', + 'name', + 'params', + 'id' + ) + ) + ) + ->from('#__extensions') + ->where('type = ' . $db->quote('plugin')) + ->where('folder = ' . $db->quote('actionlog')) + ->where('state IN (0,1)') + ->order('ordering'); + $db->setQuery($query); + + try { + $rows = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $rows = array(); + } + + if (empty($rows)) { + return; + } + + foreach ($rows as $row) { + $name = $row->name; + $type = $row->type; + $extension = 'Plg_' . $type . '_' . $name; + $extension = strtolower($extension); + + // If language already loaded, don't load it again. + if ($lang->getPaths($extension)) { + continue; + } + + $lang->load($extension, JPATH_ADMINISTRATOR) + || $lang->load($extension, JPATH_PLUGINS . '/' . $type . '/' . $name); + } + + // Load plg_system_actionlogs too + $lang->load('plg_system_actionlogs', JPATH_ADMINISTRATOR); + + // Load com_privacy too. + $lang->load('com_privacy', JPATH_ADMINISTRATOR); + } + + /** + * Escapes potential characters that start a formula in a CSV value to prevent injection attacks + * + * @param mixed $value csv field value + * + * @return mixed + * + * @since 3.9.7 + */ + protected static function escapeCsvFormula($value) + { + if ($value == '') { + return $value; + } + + if (\in_array($value[0], self::$characters, true)) { + $value = ' ' . $value; + } + + return $value; + } } diff --git a/administrator/components/com_actionlogs/src/Model/ActionlogConfigModel.php b/administrator/components/com_actionlogs/src/Model/ActionlogConfigModel.php index ec6149c81d666..95b76768fc792 100644 --- a/administrator/components/com_actionlogs/src/Model/ActionlogConfigModel.php +++ b/administrator/components/com_actionlogs/src/Model/ActionlogConfigModel.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select('a.*') - ->from($db->quoteName('#__action_log_config', 'a')) - ->where($db->quoteName('a.type_alias') . ' = :context') - ->bind(':context', $context); + /** + * Returns the action logs config for the given context. + * + * @param string $context The context of the content + * + * @return stdClass|null An object contains content type parameters, or null if not found + * + * @since 4.2.0 + */ + public function getLogContentTypeParams(string $context): ?stdClass + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('a.*') + ->from($db->quoteName('#__action_log_config', 'a')) + ->where($db->quoteName('a.type_alias') . ' = :context') + ->bind(':context', $context); - $db->setQuery($query); + $db->setQuery($query); - return $db->loadObject(); - } + return $db->loadObject(); + } } diff --git a/administrator/components/com_actionlogs/src/Model/ActionlogModel.php b/administrator/components/com_actionlogs/src/Model/ActionlogModel.php index 97fc399b36ca8..7bf5524ef8161 100644 --- a/administrator/components/com_actionlogs/src/Model/ActionlogModel.php +++ b/administrator/components/com_actionlogs/src/Model/ActionlogModel.php @@ -1,4 +1,5 @@ getDatabase(); - $date = Factory::getDate(); - $params = ComponentHelper::getComponent('com_actionlogs')->getParams(); - - if ($params->get('ip_logging', 0)) - { - $ip = IpHelper::getIp(); - - if (!filter_var($ip, FILTER_VALIDATE_IP)) - { - $ip = 'COM_ACTIONLOGS_IP_INVALID'; - } - } - else - { - $ip = 'COM_ACTIONLOGS_DISABLED'; - } - - $loggedMessages = array(); - - foreach ($messages as $message) - { - $logMessage = new \stdClass; - $logMessage->message_language_key = $messageLanguageKey; - $logMessage->message = json_encode($message); - $logMessage->log_date = (string) $date; - $logMessage->extension = $context; - $logMessage->user_id = $user->id; - $logMessage->ip_address = $ip; - $logMessage->item_id = isset($message['id']) ? (int) $message['id'] : 0; - - try - { - $db->insertObject('#__action_logs', $logMessage); - $loggedMessages[] = $logMessage; - } - catch (\RuntimeException $e) - { - // Ignore it - } - } - - try - { - // Send notification email to users who choose to be notified about the action logs - $this->sendNotificationEmails($loggedMessages, $user->name, $context); - } - catch (MailDisabledException | phpMailerException $e) - { - // Ignore it - } - } - - /** - * Send notification emails about the action log - * - * @param array $messages The logged messages - * @param string $username The username - * @param string $context The Context - * - * @return void - * - * @since 3.9.0 - * - * @throws MailDisabledException if mail is disabled - * @throws phpmailerException if sending mail failed - */ - protected function sendNotificationEmails($messages, $username, $context) - { - $app = Factory::getApplication(); - $lang = $app->getLanguage(); - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query - ->select($db->quoteName(array('u.email', 'l.extensions'))) - ->from($db->quoteName('#__users', 'u')) - ->where($db->quoteName('u.block') . ' = 0') - ->join( - 'INNER', - $db->quoteName('#__action_logs_users', 'l') . ' ON ( ' . $db->quoteName('l.notify') . ' = 1 AND ' - . $db->quoteName('l.user_id') . ' = ' . $db->quoteName('u.id') . ')' - ); - - $db->setQuery($query); - - $users = $db->loadObjectList(); - - $recipients = array(); - - foreach ($users as $user) - { - $extensions = json_decode($user->extensions, true); - - if ($extensions && \in_array(strtok($context, '.'), $extensions)) - { - $recipients[] = $user->email; - } - } - - if (empty($recipients)) - { - return; - } - - $extension = strtok($context, '.'); - $lang->load('com_actionlogs', JPATH_ADMINISTRATOR); - ActionlogsHelper::loadTranslationFiles($extension); - $temp = []; - - foreach ($messages as $message) - { - $m = []; - $m['extension'] = Text::_($extension); - $m['message'] = ActionlogsHelper::getHumanReadableLogMessage($message); - $m['date'] = HTMLHelper::_('date', $message->log_date, 'Y-m-d H:i:s T', 'UTC'); - $m['username'] = $username; - $temp[] = $m; - } - - $templateData = [ - 'messages' => $temp - ]; - - $mailer = new MailTemplate('com_actionlogs.notification', $app->getLanguage()->getTag()); - $mailer->addTemplateData($templateData); - - foreach ($recipients as $recipient) - { - $mailer->addRecipient($recipient); - } - - $mailer->send(); - } + /** + * Function to add logs to the database + * This method adds a record to #__action_logs contains (message_language_key, message, date, context, user) + * + * @param array $messages The contents of the messages to be logged + * @param string $messageLanguageKey The language key of the message + * @param string $context The context of the content passed to the plugin + * @param integer $userId ID of user perform the action, usually ID of current logged in user + * + * @return void + * + * @since 3.9.0 + */ + public function addLog($messages, $messageLanguageKey, $context, $userId = null) + { + $user = Factory::getUser($userId); + $db = $this->getDatabase(); + $date = Factory::getDate(); + $params = ComponentHelper::getComponent('com_actionlogs')->getParams(); + + if ($params->get('ip_logging', 0)) { + $ip = IpHelper::getIp(); + + if (!filter_var($ip, FILTER_VALIDATE_IP)) { + $ip = 'COM_ACTIONLOGS_IP_INVALID'; + } + } else { + $ip = 'COM_ACTIONLOGS_DISABLED'; + } + + $loggedMessages = array(); + + foreach ($messages as $message) { + $logMessage = new \stdClass(); + $logMessage->message_language_key = $messageLanguageKey; + $logMessage->message = json_encode($message); + $logMessage->log_date = (string) $date; + $logMessage->extension = $context; + $logMessage->user_id = $user->id; + $logMessage->ip_address = $ip; + $logMessage->item_id = isset($message['id']) ? (int) $message['id'] : 0; + + try { + $db->insertObject('#__action_logs', $logMessage); + $loggedMessages[] = $logMessage; + } catch (\RuntimeException $e) { + // Ignore it + } + } + + try { + // Send notification email to users who choose to be notified about the action logs + $this->sendNotificationEmails($loggedMessages, $user->name, $context); + } catch (MailDisabledException | phpMailerException $e) { + // Ignore it + } + } + + /** + * Send notification emails about the action log + * + * @param array $messages The logged messages + * @param string $username The username + * @param string $context The Context + * + * @return void + * + * @since 3.9.0 + * + * @throws MailDisabledException if mail is disabled + * @throws phpmailerException if sending mail failed + */ + protected function sendNotificationEmails($messages, $username, $context) + { + $app = Factory::getApplication(); + $lang = $app->getLanguage(); + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query + ->select($db->quoteName(array('u.email', 'l.extensions'))) + ->from($db->quoteName('#__users', 'u')) + ->where($db->quoteName('u.block') . ' = 0') + ->join( + 'INNER', + $db->quoteName('#__action_logs_users', 'l') . ' ON ( ' . $db->quoteName('l.notify') . ' = 1 AND ' + . $db->quoteName('l.user_id') . ' = ' . $db->quoteName('u.id') . ')' + ); + + $db->setQuery($query); + + $users = $db->loadObjectList(); + + $recipients = array(); + + foreach ($users as $user) { + $extensions = json_decode($user->extensions, true); + + if ($extensions && \in_array(strtok($context, '.'), $extensions)) { + $recipients[] = $user->email; + } + } + + if (empty($recipients)) { + return; + } + + $extension = strtok($context, '.'); + $lang->load('com_actionlogs', JPATH_ADMINISTRATOR); + ActionlogsHelper::loadTranslationFiles($extension); + $temp = []; + + foreach ($messages as $message) { + $m = []; + $m['extension'] = Text::_($extension); + $m['message'] = ActionlogsHelper::getHumanReadableLogMessage($message); + $m['date'] = HTMLHelper::_('date', $message->log_date, 'Y-m-d H:i:s T', 'UTC'); + $m['username'] = $username; + $temp[] = $m; + } + + $templateData = [ + 'messages' => $temp + ]; + + $mailer = new MailTemplate('com_actionlogs.notification', $app->getLanguage()->getTag()); + $mailer->addTemplateData($templateData); + + foreach ($recipients as $recipient) { + $mailer->addRecipient($recipient); + } + + $mailer->send(); + } } diff --git a/administrator/components/com_actionlogs/src/Model/ActionlogsModel.php b/administrator/components/com_actionlogs/src/Model/ActionlogsModel.php index aff8e7da27922..d108c1cc0e72c 100644 --- a/administrator/components/com_actionlogs/src/Model/ActionlogsModel.php +++ b/administrator/components/com_actionlogs/src/Model/ActionlogsModel.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select('a.*') - ->select($db->quoteName('u.name')) - ->from($db->quoteName('#__action_logs', 'a')) - ->join('LEFT', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('a.user_id') . ' = ' . $db->quoteName('u.id')); - - // Get ordering - $fullorderCol = $this->state->get('list.fullordering', 'a.id DESC'); - - // Apply ordering - if (!empty($fullorderCol)) - { - $query->order($db->escape($fullorderCol)); - } - - // Get filter by user - $user = $this->getState('filter.user'); - - // Apply filter by user - if (!empty($user)) - { - $user = (int) $user; - $query->where($db->quoteName('a.user_id') . ' = :userid') - ->bind(':userid', $user, ParameterType::INTEGER); - } - - // Get filter by extension - $extension = $this->getState('filter.extension'); - - // Apply filter by extension - if (!empty($extension)) - { - $extension = $extension . '%'; - $query->where($db->quoteName('a.extension') . ' LIKE :extension') - ->bind(':extension', $extension); - } - - // Get filter by date range - $dateRange = $this->getState('filter.dateRange'); - - // Apply filter by date range - if (!empty($dateRange)) - { - $date = $this->buildDateRange($dateRange); - - // If the chosen range is not more than a year ago - if ($date['dNow'] !== false && $date['dStart'] !== false) - { - $dStart = $date['dStart']->format('Y-m-d H:i:s'); - $dNow = $date['dNow']->format('Y-m-d H:i:s'); - $query->where( - $db->quoteName('a.log_date') . ' BETWEEN :dstart AND :dnow' - ); - $query->bind(':dstart', $dStart); - $query->bind(':dnow', $dNow); - } - } - - // Filter the items over the search string if set. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $ids, ParameterType::INTEGER); - } - elseif (stripos($search, 'item_id:') === 0) - { - $ids = (int) substr($search, 8); - $query->where($db->quoteName('a.item_id') . ' = :itemid') - ->bind(':itemid', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . $search . '%'; - $query->where($db->quoteName('a.message') . ' LIKE :message') - ->bind(':message', $search); - } - } - - return $query; - } - - /** - * Construct the date range to filter on. - * - * @param string $range The textual range to construct the filter for. - * - * @return array The date range to filter on. - * - * @since 3.9.0 - * - * @throws Exception - */ - private function buildDateRange($range) - { - // Get UTC for now. - $dNow = new Date; - $dStart = clone $dNow; - - switch ($range) - { - case 'past_week': - $dStart->modify('-7 day'); - break; - - case 'past_1month': - $dStart->modify('-1 month'); - break; - - case 'past_3month': - $dStart->modify('-3 month'); - break; - - case 'past_6month': - $dStart->modify('-6 month'); - break; - - case 'past_year': - $dStart->modify('-1 year'); - break; - - case 'today': - // Ranges that need to align with local 'days' need special treatment. - $offset = Factory::getApplication()->get('offset'); - - // Reset the start time to be the beginning of today, local time. - $dStart = new Date('now', $offset); - $dStart->setTime(0, 0, 0); - - // Now change the timezone back to UTC. - $tz = new DateTimeZone('GMT'); - $dStart->setTimezone($tz); - break; - } - - return array('dNow' => $dNow, 'dStart' => $dStart); - } - - /** - * Get all log entries for an item - * - * @param string $extension The extension the item belongs to - * @param integer $itemId The item ID - * - * @return array - * - * @since 3.9.0 - */ - public function getLogsForItem($extension, $itemId) - { - $itemId = (int) $itemId; - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('a.*') - ->select($db->quoteName('u.name')) - ->from($db->quoteName('#__action_logs', 'a')) - ->join('INNER', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('a.user_id') . ' = ' . $db->quoteName('u.id')) - ->where($db->quoteName('a.extension') . ' = :extension') - ->where($db->quoteName('a.item_id') . ' = :itemid') - ->bind(':extension', $extension) - ->bind(':itemid', $itemId, ParameterType::INTEGER); - - // Get ordering - $fullorderCol = $this->getState('list.fullordering', 'a.id DESC'); - - // Apply ordering - if (!empty($fullorderCol)) - { - $query->order($db->escape($fullorderCol)); - } - - $db->setQuery($query); - - return $db->loadObjectList(); - } - - /** - * Get logs data into Table object - * - * @param integer[]|null $pks An optional array of log record IDs to load - * - * @return array All logs in the table - * - * @since 3.9.0 - */ - public function getLogsData($pks = null) - { - $db = $this->getDatabase(); - $query = $this->getLogDataQuery($pks); - - $db->setQuery($query); - - return $db->loadObjectList(); - } - - /** - * Get logs data as a database iterator - * - * @param integer[]|null $pks An optional array of log record IDs to load - * - * @return DatabaseIterator - * - * @since 3.9.0 - */ - public function getLogDataAsIterator($pks = null) - { - $db = $this->getDatabase(); - $query = $this->getLogDataQuery($pks); - - $db->setQuery($query); - - return $db->getIterator(); - } - - /** - * Get the query for loading logs data - * - * @param integer[]|null $pks An optional array of log record IDs to load - * - * @return DatabaseQuery - * - * @since 3.9.0 - */ - private function getLogDataQuery($pks = null) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('a.*') - ->select($db->quoteName('u.name')) - ->from($db->quoteName('#__action_logs', 'a')) - ->join('INNER', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('a.user_id') . ' = ' . $db->quoteName('u.id')); - - if (\is_array($pks) && \count($pks) > 0) - { - $pks = ArrayHelper::toInteger($pks); - $query->whereIn($db->quoteName('a.id'), $pks); - } - - return $query; - } - - /** - * Delete logs - * - * @param array $pks Primary keys of logs - * - * @return boolean - * - * @since 3.9.0 - */ - public function delete(&$pks) - { - $keys = ArrayHelper::toInteger($pks); - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__action_logs')) - ->whereIn($db->quoteName('id'), $keys); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - Factory::getApplication()->triggerEvent('onAfterLogPurge', array()); - - return true; - } - - /** - * Removes all of logs from the table. - * - * @return boolean result of operation - * - * @since 3.9.0 - */ - public function purge() - { - try - { - $this->getDatabase()->truncateTable('#__action_logs'); - } - catch (Exception $e) - { - return false; - } - - Factory::getApplication()->triggerEvent('onAfterLogPurge', array()); - - return true; - } - - /** - * Get the filter form - * - * @param array $data data - * @param boolean $loadData load current data - * - * @return Form|boolean The Form object or false on error - * - * @since 3.9.0 - */ - public function getFilterForm($data = array(), $loadData = true) - { - $form = parent::getFilterForm($data, $loadData); - $params = ComponentHelper::getParams('com_actionlogs'); - $ipLogging = (bool) $params->get('ip_logging', 0); - - // Add ip sort options to sort dropdown - if ($form && $ipLogging) - { - /* @var \Joomla\CMS\Form\Field\ListField $field */ - $field = $form->getField('fullordering', 'list'); - $field->addOption(Text::_('COM_ACTIONLOGS_IP_ADDRESS_ASC'), array('value' => 'a.ip_address ASC')); - $field->addOption(Text::_('COM_ACTIONLOGS_IP_ADDRESS_DESC'), array('value' => 'a.ip_address DESC')); - } - - return $form; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 3.9.0 + * + * @throws Exception + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'a.id', 'id', + 'a.extension', 'extension', + 'a.user_id', 'user', + 'a.message', 'message', + 'a.log_date', 'log_date', + 'a.ip_address', 'ip_address', + 'dateRange', + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.9.0 + * + * @throws Exception + */ + protected function populateState($ordering = 'a.id', $direction = 'desc') + { + parent::populateState($ordering, $direction); + } + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery + * + * @since 3.9.0 + * + * @throws Exception + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('a.*') + ->select($db->quoteName('u.name')) + ->from($db->quoteName('#__action_logs', 'a')) + ->join('LEFT', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('a.user_id') . ' = ' . $db->quoteName('u.id')); + + // Get ordering + $fullorderCol = $this->state->get('list.fullordering', 'a.id DESC'); + + // Apply ordering + if (!empty($fullorderCol)) { + $query->order($db->escape($fullorderCol)); + } + + // Get filter by user + $user = $this->getState('filter.user'); + + // Apply filter by user + if (!empty($user)) { + $user = (int) $user; + $query->where($db->quoteName('a.user_id') . ' = :userid') + ->bind(':userid', $user, ParameterType::INTEGER); + } + + // Get filter by extension + $extension = $this->getState('filter.extension'); + + // Apply filter by extension + if (!empty($extension)) { + $extension = $extension . '%'; + $query->where($db->quoteName('a.extension') . ' LIKE :extension') + ->bind(':extension', $extension); + } + + // Get filter by date range + $dateRange = $this->getState('filter.dateRange'); + + // Apply filter by date range + if (!empty($dateRange)) { + $date = $this->buildDateRange($dateRange); + + // If the chosen range is not more than a year ago + if ($date['dNow'] !== false && $date['dStart'] !== false) { + $dStart = $date['dStart']->format('Y-m-d H:i:s'); + $dNow = $date['dNow']->format('Y-m-d H:i:s'); + $query->where( + $db->quoteName('a.log_date') . ' BETWEEN :dstart AND :dnow' + ); + $query->bind(':dstart', $dStart); + $query->bind(':dnow', $dNow); + } + } + + // Filter the items over the search string if set. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $ids, ParameterType::INTEGER); + } elseif (stripos($search, 'item_id:') === 0) { + $ids = (int) substr($search, 8); + $query->where($db->quoteName('a.item_id') . ' = :itemid') + ->bind(':itemid', $ids, ParameterType::INTEGER); + } else { + $search = '%' . $search . '%'; + $query->where($db->quoteName('a.message') . ' LIKE :message') + ->bind(':message', $search); + } + } + + return $query; + } + + /** + * Construct the date range to filter on. + * + * @param string $range The textual range to construct the filter for. + * + * @return array The date range to filter on. + * + * @since 3.9.0 + * + * @throws Exception + */ + private function buildDateRange($range) + { + // Get UTC for now. + $dNow = new Date(); + $dStart = clone $dNow; + + switch ($range) { + case 'past_week': + $dStart->modify('-7 day'); + break; + + case 'past_1month': + $dStart->modify('-1 month'); + break; + + case 'past_3month': + $dStart->modify('-3 month'); + break; + + case 'past_6month': + $dStart->modify('-6 month'); + break; + + case 'past_year': + $dStart->modify('-1 year'); + break; + + case 'today': + // Ranges that need to align with local 'days' need special treatment. + $offset = Factory::getApplication()->get('offset'); + + // Reset the start time to be the beginning of today, local time. + $dStart = new Date('now', $offset); + $dStart->setTime(0, 0, 0); + + // Now change the timezone back to UTC. + $tz = new DateTimeZone('GMT'); + $dStart->setTimezone($tz); + break; + } + + return array('dNow' => $dNow, 'dStart' => $dStart); + } + + /** + * Get all log entries for an item + * + * @param string $extension The extension the item belongs to + * @param integer $itemId The item ID + * + * @return array + * + * @since 3.9.0 + */ + public function getLogsForItem($extension, $itemId) + { + $itemId = (int) $itemId; + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('a.*') + ->select($db->quoteName('u.name')) + ->from($db->quoteName('#__action_logs', 'a')) + ->join('INNER', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('a.user_id') . ' = ' . $db->quoteName('u.id')) + ->where($db->quoteName('a.extension') . ' = :extension') + ->where($db->quoteName('a.item_id') . ' = :itemid') + ->bind(':extension', $extension) + ->bind(':itemid', $itemId, ParameterType::INTEGER); + + // Get ordering + $fullorderCol = $this->getState('list.fullordering', 'a.id DESC'); + + // Apply ordering + if (!empty($fullorderCol)) { + $query->order($db->escape($fullorderCol)); + } + + $db->setQuery($query); + + return $db->loadObjectList(); + } + + /** + * Get logs data into Table object + * + * @param integer[]|null $pks An optional array of log record IDs to load + * + * @return array All logs in the table + * + * @since 3.9.0 + */ + public function getLogsData($pks = null) + { + $db = $this->getDatabase(); + $query = $this->getLogDataQuery($pks); + + $db->setQuery($query); + + return $db->loadObjectList(); + } + + /** + * Get logs data as a database iterator + * + * @param integer[]|null $pks An optional array of log record IDs to load + * + * @return DatabaseIterator + * + * @since 3.9.0 + */ + public function getLogDataAsIterator($pks = null) + { + $db = $this->getDatabase(); + $query = $this->getLogDataQuery($pks); + + $db->setQuery($query); + + return $db->getIterator(); + } + + /** + * Get the query for loading logs data + * + * @param integer[]|null $pks An optional array of log record IDs to load + * + * @return DatabaseQuery + * + * @since 3.9.0 + */ + private function getLogDataQuery($pks = null) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('a.*') + ->select($db->quoteName('u.name')) + ->from($db->quoteName('#__action_logs', 'a')) + ->join('INNER', $db->quoteName('#__users', 'u') . ' ON ' . $db->quoteName('a.user_id') . ' = ' . $db->quoteName('u.id')); + + if (\is_array($pks) && \count($pks) > 0) { + $pks = ArrayHelper::toInteger($pks); + $query->whereIn($db->quoteName('a.id'), $pks); + } + + return $query; + } + + /** + * Delete logs + * + * @param array $pks Primary keys of logs + * + * @return boolean + * + * @since 3.9.0 + */ + public function delete(&$pks) + { + $keys = ArrayHelper::toInteger($pks); + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__action_logs')) + ->whereIn($db->quoteName('id'), $keys); + $db->setQuery($query); + + try { + $db->execute(); + } catch (RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + Factory::getApplication()->triggerEvent('onAfterLogPurge', array()); + + return true; + } + + /** + * Removes all of logs from the table. + * + * @return boolean result of operation + * + * @since 3.9.0 + */ + public function purge() + { + try { + $this->getDatabase()->truncateTable('#__action_logs'); + } catch (Exception $e) { + return false; + } + + Factory::getApplication()->triggerEvent('onAfterLogPurge', array()); + + return true; + } + + /** + * Get the filter form + * + * @param array $data data + * @param boolean $loadData load current data + * + * @return Form|boolean The Form object or false on error + * + * @since 3.9.0 + */ + public function getFilterForm($data = array(), $loadData = true) + { + $form = parent::getFilterForm($data, $loadData); + $params = ComponentHelper::getParams('com_actionlogs'); + $ipLogging = (bool) $params->get('ip_logging', 0); + + // Add ip sort options to sort dropdown + if ($form && $ipLogging) { + /* @var \Joomla\CMS\Form\Field\ListField $field */ + $field = $form->getField('fullordering', 'list'); + $field->addOption(Text::_('COM_ACTIONLOGS_IP_ADDRESS_ASC'), array('value' => 'a.ip_address ASC')); + $field->addOption(Text::_('COM_ACTIONLOGS_IP_ADDRESS_DESC'), array('value' => 'a.ip_address DESC')); + } + + return $form; + } } diff --git a/administrator/components/com_actionlogs/src/Plugin/ActionLogPlugin.php b/administrator/components/com_actionlogs/src/Plugin/ActionLogPlugin.php index c6520f1360810..ca2b4cc6e984d 100644 --- a/administrator/components/com_actionlogs/src/Plugin/ActionLogPlugin.php +++ b/administrator/components/com_actionlogs/src/Plugin/ActionLogPlugin.php @@ -1,4 +1,5 @@ $message) - { - if (!\array_key_exists('userid', $message)) - { - $message['userid'] = $user->id; - } + foreach ($messages as $index => $message) { + if (!\array_key_exists('userid', $message)) { + $message['userid'] = $user->id; + } - if (!\array_key_exists('username', $message)) - { - $message['username'] = $user->username; - } + if (!\array_key_exists('username', $message)) { + $message['username'] = $user->username; + } - if (!\array_key_exists('accountlink', $message)) - { - $message['accountlink'] = 'index.php?option=com_users&task=user.edit&id=' . $user->id; - } + if (!\array_key_exists('accountlink', $message)) { + $message['accountlink'] = 'index.php?option=com_users&task=user.edit&id=' . $user->id; + } - if (\array_key_exists('type', $message)) - { - $message['type'] = strtoupper($message['type']); - } + if (\array_key_exists('type', $message)) { + $message['type'] = strtoupper($message['type']); + } - if (\array_key_exists('app', $message)) - { - $message['app'] = strtoupper($message['app']); - } + if (\array_key_exists('app', $message)) { + $message['app'] = strtoupper($message['app']); + } - $messages[$index] = $message; - } + $messages[$index] = $message; + } - /** @var \Joomla\Component\Actionlogs\Administrator\Model\ActionlogModel $model */ - $model = $this->app->bootComponent('com_actionlogs') - ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); + /** @var \Joomla\Component\Actionlogs\Administrator\Model\ActionlogModel $model */ + $model = $this->app->bootComponent('com_actionlogs') + ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); - $model->addLog($messages, strtoupper($messageLanguageKey), $context, $userId); - } + $model->addLog($messages, strtoupper($messageLanguageKey), $context, $userId); + } } diff --git a/administrator/components/com_admin/postinstall/addnosniff.php b/administrator/components/com_admin/postinstall/addnosniff.php index b8dcab690900b..620d898cedcf1 100644 --- a/administrator/components/com_admin/postinstall/addnosniff.php +++ b/administrator/components/com_admin/postinstall/addnosniff.php @@ -1,4 +1,5 @@ get('behind_loadbalancer', '0')) - { - return false; - } + if ($app->get('behind_loadbalancer', '0')) { + return false; + } - if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER) && !empty($_SERVER['HTTP_X_FORWARDED_FOR'])) - { - return true; - } + if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER) && !empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { + return true; + } - if (array_key_exists('HTTP_CLIENT_IP', $_SERVER) && !empty($_SERVER['HTTP_CLIENT_IP'])) - { - return true; - } + if (array_key_exists('HTTP_CLIENT_IP', $_SERVER) && !empty($_SERVER['HTTP_CLIENT_IP'])) { + return true; + } - return false; + return false; } @@ -55,35 +51,32 @@ function admin_postinstall_behindproxy_condition() */ function behindproxy_postinstall_action() { - $prev = ArrayHelper::fromObject(new JConfig); - $data = array_merge($prev, array('behind_loadbalancer' => '1')); + $prev = ArrayHelper::fromObject(new JConfig()); + $data = array_merge($prev, array('behind_loadbalancer' => '1')); - $config = new Registry($data); + $config = new Registry($data); - // Set the configuration file path. - $file = JPATH_CONFIGURATION . '/configuration.php'; + // Set the configuration file path. + $file = JPATH_CONFIGURATION . '/configuration.php'; - // Attempt to make the file writeable - if (Path::isOwner($file) && !Path::setPermissions($file, '0644')) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTWRITABLE'), 'error'); + // Attempt to make the file writeable + if (Path::isOwner($file) && !Path::setPermissions($file, '0644')) { + Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTWRITABLE'), 'error'); - return; - } + return; + } - // Attempt to write the configuration file as a PHP class named JConfig. - $configuration = $config->toString('PHP', array('class' => 'JConfig', 'closingtag' => false)); + // Attempt to write the configuration file as a PHP class named JConfig. + $configuration = $config->toString('PHP', array('class' => 'JConfig', 'closingtag' => false)); - if (!File::write($file, $configuration)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_WRITE_FAILED'), 'error'); + if (!File::write($file, $configuration)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_WRITE_FAILED'), 'error'); - return; - } + return; + } - // Attempt to make the file unwriteable - if (Path::isOwner($file) && !Path::setPermissions($file, '0444')) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTUNWRITABLE'), 'error'); - } + // Attempt to make the file unwriteable + if (Path::isOwner($file) && !Path::setPermissions($file, '0444')) { + Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTUNWRITABLE'), 'error'); + } } diff --git a/administrator/components/com_admin/postinstall/htaccesssvg.php b/administrator/components/com_admin/postinstall/htaccesssvg.php index 91645751f1d39..bfe292da5ab4b 100644 --- a/administrator/components/com_admin/postinstall/htaccesssvg.php +++ b/administrator/components/com_admin/postinstall/htaccesssvg.php @@ -1,4 +1,5 @@ getQuery(true) - ->select($db->quoteName('access')) - ->from($db->quoteName('#__languages')) - ->where($db->quoteName('access') . ' = ' . $db->quote('0')); - $db->setQuery($query); - $db->execute(); - $numRows = $db->getNumRows(); + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('access')) + ->from($db->quoteName('#__languages')) + ->where($db->quoteName('access') . ' = ' . $db->quote('0')); + $db->setQuery($query); + $db->execute(); + $numRows = $db->getNumRows(); - if (isset($numRows) && $numRows != 0) - { - // We have rows here so we have at minimum one row with access set to 0 - return true; - } + if (isset($numRows) && $numRows != 0) { + // We have rows here so we have at minimum one row with access set to 0 + return true; + } - // All good the query return nothing. - return false; + // All good the query return nothing. + return false; } diff --git a/administrator/components/com_admin/postinstall/statscollection.php b/administrator/components/com_admin/postinstall/statscollection.php index c1cf1a3a9886a..ead154f4548cd 100644 --- a/administrator/components/com_admin/postinstall/statscollection.php +++ b/administrator/components/com_admin/postinstall/statscollection.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt - */ -defined('_JEXEC') or die; + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + */ use Joomla\CMS\Extension\ExtensionHelper; use Joomla\CMS\Factory; @@ -27,8795 +28,8650 @@ */ class JoomlaInstallerScript { - /** - * The Joomla Version we are updating from - * - * @var string - * @since 3.7 - */ - protected $fromVersion = null; - - /** - * Function to act prior to installation process begins - * - * @param string $action Which action is happening (install|uninstall|discover_install|update) - * @param Installer $installer The class calling this method - * - * @return boolean True on success - * - * @since 3.7.0 - */ - public function preflight($action, $installer) - { - if ($action === 'update') - { - // Get the version we are updating from - if (!empty($installer->extension->manifest_cache)) - { - $manifestValues = json_decode($installer->extension->manifest_cache, true); - - if (array_key_exists('version', $manifestValues)) - { - $this->fromVersion = $manifestValues['version']; - - // Ensure templates are moved to the correct mode - $this->fixTemplateMode(); - - return true; - } - } - - return false; - } - - return true; - } - - /** - * Method to update Joomla! - * - * @param Installer $installer The class calling this method - * - * @return void - */ - public function update($installer) - { - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'joomla_update.php'; - - Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); - - try - { - Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_DELETE_FILES'), Log::INFO, 'Update'); - } - catch (RuntimeException $exception) - { - // Informational log only - } - - // Uninstall plugins before removing their files and folders - $this->uninstallRepeatableFieldsPlugin(); - $this->uninstallEosPlugin(); - - // This needs to stay for 2.5 update compatibility - $this->deleteUnexistingFiles(); - $this->updateManifestCaches(); - $this->updateDatabase(); - $this->updateAssets($installer); - $this->clearStatsCache(); - $this->convertTablesToUtf8mb4(true); - $this->addUserAuthProviderColumn(); - $this->cleanJoomlaCache(); - } - - /** - * Method to clear our stats plugin cache to ensure we get fresh data on Joomla Update - * - * @return void - * - * @since 3.5 - */ - protected function clearStatsCache() - { - $db = Factory::getDbo(); - - try - { - // Get the params for the stats plugin - $params = $db->setQuery( - $db->getQuery(true) - ->select($db->quoteName('params')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) - ->where($db->quoteName('element') . ' = ' . $db->quote('stats')) - )->loadResult(); - } - catch (Exception $e) - { - echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; - - return; - } - - $params = json_decode($params, true); - - // Reset the last run parameter - if (isset($params['lastrun'])) - { - $params['lastrun'] = ''; - } - - $params = json_encode($params); - - $query = $db->getQuery(true) - ->update($db->quoteName('#__extensions')) - ->set($db->quoteName('params') . ' = ' . $db->quote($params)) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) - ->where($db->quoteName('element') . ' = ' . $db->quote('stats')); - - try - { - $db->setQuery($query)->execute(); - } - catch (Exception $e) - { - echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; - - return; - } - } - - /** - * Method to update Database - * - * @return void - */ - protected function updateDatabase() - { - if (Factory::getDbo()->getServerType() === 'mysql') - { - $this->updateDatabaseMysql(); - } - } - - /** - * Method to update MySQL Database - * - * @return void - */ - protected function updateDatabaseMysql() - { - $db = Factory::getDbo(); - - $db->setQuery('SHOW ENGINES'); - - try - { - $results = $db->loadObjectList(); - } - catch (Exception $e) - { - echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; - - return; - } - - foreach ($results as $result) - { - if ($result->Support != 'DEFAULT') - { - continue; - } - - $db->setQuery('ALTER TABLE #__update_sites_extensions ENGINE = ' . $result->Engine); - - try - { - $db->execute(); - } - catch (Exception $e) - { - echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; - - return; - } - - break; - } - } - - /** - * Uninstalls the plg_fields_repeatable plugin and transforms its custom field instances - * to instances of the plg_fields_subfields plugin. - * - * @return void - * - * @since 4.0.0 - */ - protected function uninstallRepeatableFieldsPlugin() - { - $app = Factory::getApplication(); - $db = Factory::getDbo(); - - // Check if the plg_fields_repeatable plugin is present - $extensionId = $db->setQuery( - $db->getQuery(true) - ->select('extension_id') - ->from('#__extensions') - ->where('name = ' . $db->quote('plg_fields_repeatable')) - )->loadResult(); - - // Skip uninstalling when it doesn't exist - if (!$extensionId) - { - return; - } - - // Ensure the FieldsHelper class is loaded for the Repeatable fields plugin we're about to remove - \JLoader::register('FieldsHelper', JPATH_ADMINISTRATOR . '/components/com_fields/helpers/fields.php'); - - try - { - $db->transactionStart(); - - // Get the FieldsModelField, we need it in a sec - $fieldModel = $app->bootComponent('com_fields')->getMVCFactory()->createModel('Field', 'Administrator', ['ignore_request' => true]); - /** @var FieldModel $fieldModel */ - - // Now get a list of all `repeatable` custom field instances - $db->setQuery( - $db->getQuery(true) - ->select('*') - ->from('#__fields') - ->where($db->quoteName('type') . ' = ' . $db->quote('repeatable')) - ); - - // Execute the query and iterate over the `repeatable` instances - foreach ($db->loadObjectList() as $row) - { - // Skip broken rows - just a security measure, should not happen - if (!isset($row->fieldparams) || !($oldFieldparams = json_decode($row->fieldparams)) || !is_object($oldFieldparams)) - { - continue; - } - - /** - * We basically want to transform this `repeatable` type into a `subfields` type. While $oldFieldparams - * holds the `fieldparams` of the `repeatable` type, $newFieldparams shall hold the `fieldparams` - * of the `subfields` type. - */ - $newFieldparams = [ - 'repeat' => '1', - 'options' => [], - ]; - - /** - * This array is used to store the mapping between the name of form fields from Repeatable field - * with ID of the child-fields. It will then be used to migrate data later - */ - $mapping = []; - - /** - * Store name of media fields which we need to convert data from old format (string) to new - * format (json) during the migration - */ - $mediaFields = []; - - // If this repeatable fields actually had child-fields (normally this is always the case) - if (isset($oldFieldparams->fields) && is_object($oldFieldparams->fields)) - { - // Small counter for the child-fields (aka sub fields) - $newFieldCount = 0; - - // Iterate over the sub fields - foreach (get_object_vars($oldFieldparams->fields) as $oldField) - { - // Used for field name collision prevention - $fieldname_prefix = ''; - $fieldname_suffix = 0; - - // Try to save the new sub field in a loop because of field name collisions - while (true) - { - /** - * We basically want to create a completely new custom fields instance for every sub field - * of the `repeatable` instance. This is what we use $data for, we create a new custom field - * for each of the sub fields of the `repeatable` instance. - */ - $data = [ - 'context' => $row->context, - 'group_id' => $row->group_id, - 'title' => $oldField->fieldname, - 'name' => ( - $fieldname_prefix - . $oldField->fieldname - . ($fieldname_suffix > 0 ? ('_' . $fieldname_suffix) : '') - ), - 'label' => $oldField->fieldname, - 'default_value' => $row->default_value, - 'type' => $oldField->fieldtype, - 'description' => $row->description, - 'state' => '1', - 'params' => $row->params, - 'language' => '*', - 'assigned_cat_ids' => [-1], - 'only_use_in_subform' => 1, - ]; - - // `number` is not a valid custom field type, so use `text` instead. - if ($data['type'] == 'number') - { - $data['type'] = 'text'; - } - - if ($data['type'] == 'media') - { - $mediaFields[] = $oldField->fieldname; - } - - // Reset the state because else \Joomla\CMS\MVC\Model\AdminModel will take an already - // existing value (e.g. from previous save) and do an UPDATE instead of INSERT. - $fieldModel->setState('field.id', 0); - - // If an error occurred when trying to save this. - if (!$fieldModel->save($data)) - { - // If the error is, that the name collided, increase the collision prevention - $error = $fieldModel->getError(); - - if ($error == 'COM_FIELDS_ERROR_UNIQUE_NAME') - { - // If this is the first time this error occurs, set only the prefix - if ($fieldname_prefix == '') - { - $fieldname_prefix = ($row->name . '_'); - } - else - { - // Else increase the suffix - $fieldname_suffix++; - } - - // And start again with the while loop. - continue 1; - } - - // Else bail out with the error. Something is totally wrong. - throw new \Exception($error); - } - - // Break out of the while loop, saving was successful. - break 1; - } - - // Get the newly created id - $subfield_id = $fieldModel->getState('field.id'); - - // Really check that it is valid - if (!is_numeric($subfield_id) || $subfield_id < 1) - { - throw new \Exception('Something went wrong.'); - } - - // And tell our new `subfields` field about his child - $newFieldparams['options'][('option' . $newFieldCount)] = [ - 'customfield' => $subfield_id, - 'render_values' => '1', - ]; - - $newFieldCount++; - - $mapping[$oldField->fieldname] = 'field' . $subfield_id; - } - } - - // Write back the changed stuff to the database - $db->setQuery( - $db->getQuery(true) - ->update('#__fields') - ->set($db->quoteName('type') . ' = ' . $db->quote('subform')) - ->set($db->quoteName('fieldparams') . ' = ' . $db->quote(json_encode($newFieldparams))) - ->where($db->quoteName('id') . ' = ' . $db->quote($row->id)) - )->execute(); - - // Migrate data for this field - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__fields_values')) - ->where($db->quoteName('field_id') . ' = ' . $row->id); - $db->setQuery($query); - - foreach ($db->loadObjectList() as $rowFieldValue) - { - // Do not do the version if no data is entered for the custom field this item - if (!$rowFieldValue->value) - { - continue; - } - - /** - * Here we will have to update the stored value of the field to new format - * The key for each row changes from repeatable to row, for example repeatable0 to row0, and so on - * The key for each sub-field change from name of field to field + ID of the new sub-field - * Example data format stored in J3: {"repeatable0":{"id":"1","username":"admin"}} - * Example data format stored in J4: {"row0":{"field1":"1","field2":"admin"}} - */ - $newFieldValue = []; - - // Convert to array to change key - $fieldValue = json_decode($rowFieldValue->value, true); - - // If data could not be decoded for some reason, ignore - if (!$fieldValue) - { - continue; - } - - $rowIndex = 0; - - foreach ($fieldValue as $rowKey => $rowValue) - { - $rowKey = 'row' . ($rowIndex++); - $newFieldValue[$rowKey] = []; - - foreach ($rowValue as $subFieldName => $subFieldValue) - { - // This is a media field, so we need to convert data to new format required in Joomla! 4 - if (in_array($subFieldName, $mediaFields)) - { - $subFieldValue = ['imagefile' => $subFieldValue, 'alt_text' => '']; - } - - if (isset($mapping[$subFieldName])) - { - $newFieldValue[$rowKey][$mapping[$subFieldName]] = $subFieldValue; - } - else - { - // Not found, use the old key to avoid data lost - $newFieldValue[$subFieldName] = $subFieldValue; - } - } - } - - $query->clear() - ->update($db->quoteName('#__fields_values')) - ->set($db->quoteName('value') . ' = ' . $db->quote(json_encode($newFieldValue))) - ->where($db->quoteName('field_id') . ' = ' . $rowFieldValue->field_id) - ->where($db->quoteName('item_id') . ' =' . $rowFieldValue->item_id); - $db->setQuery($query) - ->execute(); - } - } - - // Now, unprotect the plugin so we can uninstall it - $db->setQuery( - $db->getQuery(true) - ->update('#__extensions') - ->set('protected = 0') - ->where($db->quoteName('extension_id') . ' = ' . $extensionId) - )->execute(); - - // And now uninstall the plugin - $installer = new Installer; - $installer->setDatabase($db); - $installer->uninstall('plugin', $extensionId); - - $db->transactionCommit(); - } - catch (\Exception $e) - { - $db->transactionRollback(); - throw $e; - } - } - - /** - * Uninstall the 3.10 EOS plugin - * - * @return void - * - * @since 4.0.0 - */ - protected function uninstallEosPlugin() - { - $db = Factory::getDbo(); - - // Check if the plg_quickicon_eos310 plugin is present - $extensionId = $db->setQuery( - $db->getQuery(true) - ->select('extension_id') - ->from('#__extensions') - ->where('name = ' . $db->quote('plg_quickicon_eos310')) - )->loadResult(); - - // Skip uninstalling if it doesn't exist - if (!$extensionId) - { - return; - } - - try - { - $db->transactionStart(); - - // Unprotect the plugin so we can uninstall it - $db->setQuery( - $db->getQuery(true) - ->update('#__extensions') - ->set('protected = 0') - ->where($db->quoteName('extension_id') . ' = ' . $extensionId) - )->execute(); - - // Uninstall the plugin - $installer = new Installer; - $installer->setDatabase($db); - $installer->uninstall('plugin', $extensionId); - - $db->transactionCommit(); - } - catch (\Exception $e) - { - $db->transactionRollback(); - throw $e; - } - } - - /** - * Update the manifest caches - * - * @return void - */ - protected function updateManifestCaches() - { - $extensions = ExtensionHelper::getCoreExtensions(); - - // If we have the search package around, it may not have a manifest cache entry after upgrades from 3.x, so add it to the list - if (File::exists(JPATH_ROOT . '/administrator/manifests/packages/pkg_search.xml')) - { - $extensions[] = array('package', 'pkg_search', '', 0); - } - - // Attempt to refresh manifest caches - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('*') - ->from('#__extensions'); - - foreach ($extensions as $extension) - { - $query->where( - 'type=' . $db->quote($extension[0]) - . ' AND element=' . $db->quote($extension[1]) - . ' AND folder=' . $db->quote($extension[2]) - . ' AND client_id=' . $extension[3], 'OR' - ); - } - - $db->setQuery($query); - - try - { - $extensions = $db->loadObjectList(); - } - catch (Exception $e) - { - echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; - - return; - } - - $installer = new Installer; - $installer->setDatabase($db); - - foreach ($extensions as $extension) - { - if (!$installer->refreshManifestCache($extension->extension_id)) - { - echo Text::sprintf('FILES_JOOMLA_ERROR_MANIFEST', $extension->type, $extension->element, $extension->name, $extension->client_id) . '
'; - } - } - } - - /** - * Delete files that should not exist - * - * @param bool $dryRun If set to true, will not actually delete files, but just report their status for use in CLI - * @param bool $suppressOutput Set to true to suppress echoing any errors, and just return the $status array - * - * @return array - */ - public function deleteUnexistingFiles($dryRun = false, $suppressOutput = false) - { - $status = [ - 'files_exist' => [], - 'folders_exist' => [], - 'files_deleted' => [], - 'folders_deleted' => [], - 'files_errors' => [], - 'folders_errors' => [], - 'folders_checked' => [], - 'files_checked' => [], - ]; - - $files = array( - // From 3.10 to 4.1 - '/administrator/components/com_actionlogs/actionlogs.php', - '/administrator/components/com_actionlogs/controller.php', - '/administrator/components/com_actionlogs/controllers/actionlogs.php', - '/administrator/components/com_actionlogs/helpers/actionlogs.php', - '/administrator/components/com_actionlogs/helpers/actionlogsphp55.php', - '/administrator/components/com_actionlogs/layouts/logstable.php', - '/administrator/components/com_actionlogs/libraries/actionlogplugin.php', - '/administrator/components/com_actionlogs/models/actionlog.php', - '/administrator/components/com_actionlogs/models/actionlogs.php', - '/administrator/components/com_actionlogs/models/fields/extension.php', - '/administrator/components/com_actionlogs/models/fields/logcreator.php', - '/administrator/components/com_actionlogs/models/fields/logsdaterange.php', - '/administrator/components/com_actionlogs/models/fields/logtype.php', - '/administrator/components/com_actionlogs/models/fields/plugininfo.php', - '/administrator/components/com_actionlogs/models/forms/filter_actionlogs.xml', - '/administrator/components/com_actionlogs/views/actionlogs/tmpl/default.php', - '/administrator/components/com_actionlogs/views/actionlogs/tmpl/default.xml', - '/administrator/components/com_actionlogs/views/actionlogs/view.html.php', - '/administrator/components/com_admin/admin.php', - '/administrator/components/com_admin/controller.php', - '/administrator/components/com_admin/controllers/profile.php', - '/administrator/components/com_admin/helpers/html/directory.php', - '/administrator/components/com_admin/helpers/html/phpsetting.php', - '/administrator/components/com_admin/helpers/html/system.php', - '/administrator/components/com_admin/models/forms/profile.xml', - '/administrator/components/com_admin/models/help.php', - '/administrator/components/com_admin/models/profile.php', - '/administrator/components/com_admin/models/sysinfo.php', - '/administrator/components/com_admin/postinstall/eaccelerator.php', - '/administrator/components/com_admin/postinstall/htaccess.php', - '/administrator/components/com_admin/postinstall/joomla40checks.php', - '/administrator/components/com_admin/postinstall/updatedefaultsettings.php', - '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion-01.sql', - '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion-02.sql', - '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-06.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-21-1.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-21-2.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-22.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-23.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-24.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2012-01-10.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2012-01-14.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.1-2012-01-26.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.2-2012-03-05.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.3-2012-03-13.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.4-2012-03-18.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.4-2012-03-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.5.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.6.sql', - '/administrator/components/com_admin/sql/updates/mysql/2.5.7.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.0.0.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.0.1.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.0.2.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.0.3.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.1.0.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.1.1.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.1.2.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.1.3.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.1.4.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.1.5.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.10.0-2020-08-10.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.10.0-2021-05-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.10.7-2022-02-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.10.7-2022-03-18.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.0.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.1.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2013-12-22.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2013-12-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-08.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-15.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-18.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-23.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.2.3-2014-02-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.3.0-2014-02-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.3.0-2014-04-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.3.4-2014-08-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.3.6-2014-09-30.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-08-24.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-09-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-09-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-10-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-12-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2015-01-21.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2015-02-26.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-07-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-10-13.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-10-26.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-10-30.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-11-04.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-11-05.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2016-02-26.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2016-03-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.1-2016-03-25.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.5.1-2016-03-29.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-06.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-08.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-09.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-05-06.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-06-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-06-05.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.3-2016-08-15.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.6.3-2016-08-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-08-06.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-08-22.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-08-29.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-09-29.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-10-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-10-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-04.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-21.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-24.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-27.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-08.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-09.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-15.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-17.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-31.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-02-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-02-15.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-02-17.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-03-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-03-09.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-03-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-04-10.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-04-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.3-2017-06-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.7.4-2017-07-05.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.8.0-2017-07-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.8.0-2017-07-31.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.8.2-2017-10-14.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.8.4-2018-01-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.8.6-2018-02-14.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.8.8-2018-05-18.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.8.9-2018-06-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-05.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-24.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-27.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-12.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-13.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-14.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-17.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-07-09.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-07-10.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-07-11.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-08-12.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-08-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-08-29.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-09-04.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-10-15.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-10-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-10-21.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.10-2019-07-09.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.16-2020-02-15.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.16-2020-03-04.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.19-2020-05-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.19-2020-06-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.21-2020-08-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.22-2020-09-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.26-2021-04-07.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.27-2021-04-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.3-2019-01-12.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.3-2019-02-07.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.7-2019-04-23.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.7-2019-04-26.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.7-2019-05-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.8-2019-06-11.sql', - '/administrator/components/com_admin/sql/updates/mysql/3.9.8-2019-06-15.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.0.0.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.0.1.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.0.2.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.0.3.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.1.0.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.1.1.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.1.2.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.1.3.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.1.4.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.1.5.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.10.0-2020-08-10.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.10.0-2021-05-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.10.7-2022-02-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.10.7-2022-02-20.sql.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.10.7-2022-03-18.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.0.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.1.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2013-12-22.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2013-12-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-08.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-15.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-18.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-23.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.2.3-2014-02-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.3.0-2013-12-21.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.3.0-2014-02-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.3.0-2014-04-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.3.4-2014-08-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.3.6-2014-09-30.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-08-24.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-09-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-09-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-10-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-12-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2015-01-21.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2015-02-26.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.4.4-2015-07-11.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-10-13.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-10-26.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-10-30.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-11-04.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-11-05.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2016-03-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-04-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-04-08.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-04-09.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-05-06.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-06-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-06-05.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.3-2016-08-15.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.3-2016-08-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.6.3-2016-10-04.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-08-06.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-08-22.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-08-29.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-09-29.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-10-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-10-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-04.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-19.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-21.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-24.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-08.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-09.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-15.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-17.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-31.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-02-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-02-15.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-02-17.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-03-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-03-09.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-04-10.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-04-19.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.7.4-2017-07-05.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.8.0-2017-07-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.8.0-2017-07-31.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.8.2-2017-10-14.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.8.4-2018-01-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.8.6-2018-02-14.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.8.8-2018-05-18.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.8.9-2018-06-19.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-05.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-19.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-24.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-27.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-12.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-13.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-14.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-17.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-07-09.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-07-10.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-07-11.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-08-12.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-08-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-08-29.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-09-04.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-10-15.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-10-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-10-21.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.10-2019-07-09.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.15-2020-01-08.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.16-2020-02-15.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.16-2020-03-04.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.19-2020-06-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.21-2020-08-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.22-2020-09-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.26-2021-04-07.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.27-2021-04-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.3-2019-01-12.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.3-2019-02-07.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.7-2019-04-23.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.7-2019-04-26.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.7-2019-05-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.8-2019-06-11.sql', - '/administrator/components/com_admin/sql/updates/postgresql/3.9.8-2019-06-15.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/2.5.2-2012-03-05.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/2.5.3-2012-03-13.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/2.5.4-2012-03-18.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/2.5.4-2012-03-19.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/2.5.5.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/2.5.6.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/2.5.7.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.0.0.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.0.1.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.0.2.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.0.3.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.1.0.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.1.1.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.1.2.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.1.3.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.1.4.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.1.5.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.10.0-2021-05-28.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.10.1-2021-08-17.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.10.7-2022-02-20.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.10.7-2022-02-20.sql.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.10.7-2022-03-18.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.0.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.1.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2013-12-22.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2013-12-28.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-08.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-15.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-18.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-23.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.2.3-2014-02-20.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.3.0-2014-02-16.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.3.0-2014-04-02.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.3.4-2014-08-03.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.3.6-2014-09-30.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-08-24.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-09-01.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-09-16.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-10-20.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-12-03.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2015-01-21.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2015-02-26.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.4.4-2015-07-11.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-10-13.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-10-26.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-10-30.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-11-04.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-11-05.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2016-03-01.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-01.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-06.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-08.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-09.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-05-06.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-06-01.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-06-05.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.3-2016-08-15.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.6.3-2016-08-16.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-08-06.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-08-22.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-08-29.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-09-29.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-10-01.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-10-02.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-11-04.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-11-19.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-11-24.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-08.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-09.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-15.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-17.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-31.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-02.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-15.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-16.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-17.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-03-03.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-03-09.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-04-10.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-04-19.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.7.4-2017-07-05.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.8.0-2017-07-28.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.8.0-2017-07-31.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.8.2-2017-10-14.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.8.4-2018-01-16.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.8.6-2018-02-14.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.8.8-2018-05-18.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.8.9-2018-06-19.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-02.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-03.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-05.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-19.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-20.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-24.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-27.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-02.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-12.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-13.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-14.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-17.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-07-09.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-07-10.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-07-11.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-08-12.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-08-28.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-08-29.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-09-04.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-10-15.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-10-20.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-10-21.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.10-2019-07-09.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.16-2020-03-04.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.19-2020-06-01.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.21-2020-08-02.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.22-2020-09-16.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.26-2021-04-07.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.27-2021-04-20.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.3-2019-01-12.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.3-2019-02-07.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.4-2019-03-06.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.7-2019-04-23.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.7-2019-04-26.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.7-2019-05-16.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.8-2019-06-11.sql', - '/administrator/components/com_admin/sql/updates/sqlazure/3.9.8-2019-06-15.sql', - '/administrator/components/com_admin/views/help/tmpl/default.php', - '/administrator/components/com_admin/views/help/tmpl/default.xml', - '/administrator/components/com_admin/views/help/tmpl/langforum.php', - '/administrator/components/com_admin/views/help/view.html.php', - '/administrator/components/com_admin/views/profile/tmpl/edit.php', - '/administrator/components/com_admin/views/profile/view.html.php', - '/administrator/components/com_admin/views/sysinfo/tmpl/default.php', - '/administrator/components/com_admin/views/sysinfo/tmpl/default.xml', - '/administrator/components/com_admin/views/sysinfo/tmpl/default_config.php', - '/administrator/components/com_admin/views/sysinfo/tmpl/default_directory.php', - '/administrator/components/com_admin/views/sysinfo/tmpl/default_phpinfo.php', - '/administrator/components/com_admin/views/sysinfo/tmpl/default_phpsettings.php', - '/administrator/components/com_admin/views/sysinfo/tmpl/default_system.php', - '/administrator/components/com_admin/views/sysinfo/view.html.php', - '/administrator/components/com_admin/views/sysinfo/view.json.php', - '/administrator/components/com_admin/views/sysinfo/view.text.php', - '/administrator/components/com_associations/associations.php', - '/administrator/components/com_associations/controller.php', - '/administrator/components/com_associations/controllers/association.php', - '/administrator/components/com_associations/controllers/associations.php', - '/administrator/components/com_associations/helpers/associations.php', - '/administrator/components/com_associations/layouts/joomla/searchtools/default/bar.php', - '/administrator/components/com_associations/models/association.php', - '/administrator/components/com_associations/models/associations.php', - '/administrator/components/com_associations/models/fields/itemlanguage.php', - '/administrator/components/com_associations/models/fields/itemtype.php', - '/administrator/components/com_associations/models/fields/modalassociation.php', - '/administrator/components/com_associations/models/forms/association.xml', - '/administrator/components/com_associations/models/forms/filter_associations.xml', - '/administrator/components/com_associations/views/association/tmpl/edit.php', - '/administrator/components/com_associations/views/association/view.html.php', - '/administrator/components/com_associations/views/associations/tmpl/default.php', - '/administrator/components/com_associations/views/associations/tmpl/default.xml', - '/administrator/components/com_associations/views/associations/tmpl/modal.php', - '/administrator/components/com_associations/views/associations/view.html.php', - '/administrator/components/com_banners/banners.php', - '/administrator/components/com_banners/controller.php', - '/administrator/components/com_banners/controllers/banner.php', - '/administrator/components/com_banners/controllers/banners.php', - '/administrator/components/com_banners/controllers/client.php', - '/administrator/components/com_banners/controllers/clients.php', - '/administrator/components/com_banners/controllers/tracks.php', - '/administrator/components/com_banners/controllers/tracks.raw.php', - '/administrator/components/com_banners/helpers/html/banner.php', - '/administrator/components/com_banners/models/banner.php', - '/administrator/components/com_banners/models/banners.php', - '/administrator/components/com_banners/models/client.php', - '/administrator/components/com_banners/models/clients.php', - '/administrator/components/com_banners/models/download.php', - '/administrator/components/com_banners/models/fields/bannerclient.php', - '/administrator/components/com_banners/models/fields/clicks.php', - '/administrator/components/com_banners/models/fields/impmade.php', - '/administrator/components/com_banners/models/fields/imptotal.php', - '/administrator/components/com_banners/models/forms/banner.xml', - '/administrator/components/com_banners/models/forms/client.xml', - '/administrator/components/com_banners/models/forms/download.xml', - '/administrator/components/com_banners/models/forms/filter_banners.xml', - '/administrator/components/com_banners/models/forms/filter_clients.xml', - '/administrator/components/com_banners/models/forms/filter_tracks.xml', - '/administrator/components/com_banners/models/tracks.php', - '/administrator/components/com_banners/tables/banner.php', - '/administrator/components/com_banners/tables/client.php', - '/administrator/components/com_banners/views/banner/tmpl/edit.php', - '/administrator/components/com_banners/views/banner/view.html.php', - '/administrator/components/com_banners/views/banners/tmpl/default.php', - '/administrator/components/com_banners/views/banners/tmpl/default_batch_body.php', - '/administrator/components/com_banners/views/banners/tmpl/default_batch_footer.php', - '/administrator/components/com_banners/views/banners/view.html.php', - '/administrator/components/com_banners/views/client/tmpl/edit.php', - '/administrator/components/com_banners/views/client/view.html.php', - '/administrator/components/com_banners/views/clients/tmpl/default.php', - '/administrator/components/com_banners/views/clients/view.html.php', - '/administrator/components/com_banners/views/download/tmpl/default.php', - '/administrator/components/com_banners/views/download/view.html.php', - '/administrator/components/com_banners/views/tracks/tmpl/default.php', - '/administrator/components/com_banners/views/tracks/view.html.php', - '/administrator/components/com_banners/views/tracks/view.raw.php', - '/administrator/components/com_cache/cache.php', - '/administrator/components/com_cache/controller.php', - '/administrator/components/com_cache/helpers/cache.php', - '/administrator/components/com_cache/models/cache.php', - '/administrator/components/com_cache/models/forms/filter_cache.xml', - '/administrator/components/com_cache/views/cache/tmpl/default.php', - '/administrator/components/com_cache/views/cache/tmpl/default.xml', - '/administrator/components/com_cache/views/cache/view.html.php', - '/administrator/components/com_cache/views/purge/tmpl/default.php', - '/administrator/components/com_cache/views/purge/tmpl/default.xml', - '/administrator/components/com_cache/views/purge/view.html.php', - '/administrator/components/com_categories/categories.php', - '/administrator/components/com_categories/controller.php', - '/administrator/components/com_categories/controllers/ajax.json.php', - '/administrator/components/com_categories/controllers/categories.php', - '/administrator/components/com_categories/controllers/category.php', - '/administrator/components/com_categories/helpers/association.php', - '/administrator/components/com_categories/helpers/html/categoriesadministrator.php', - '/administrator/components/com_categories/models/categories.php', - '/administrator/components/com_categories/models/category.php', - '/administrator/components/com_categories/models/fields/categoryedit.php', - '/administrator/components/com_categories/models/fields/categoryparent.php', - '/administrator/components/com_categories/models/fields/modal/category.php', - '/administrator/components/com_categories/models/forms/category.xml', - '/administrator/components/com_categories/models/forms/filter_categories.xml', - '/administrator/components/com_categories/tables/category.php', - '/administrator/components/com_categories/views/categories/tmpl/default.php', - '/administrator/components/com_categories/views/categories/tmpl/default.xml', - '/administrator/components/com_categories/views/categories/tmpl/default_batch_body.php', - '/administrator/components/com_categories/views/categories/tmpl/default_batch_footer.php', - '/administrator/components/com_categories/views/categories/tmpl/modal.php', - '/administrator/components/com_categories/views/categories/view.html.php', - '/administrator/components/com_categories/views/category/tmpl/edit.php', - '/administrator/components/com_categories/views/category/tmpl/edit.xml', - '/administrator/components/com_categories/views/category/tmpl/edit_associations.php', - '/administrator/components/com_categories/views/category/tmpl/edit_metadata.php', - '/administrator/components/com_categories/views/category/tmpl/modal.php', - '/administrator/components/com_categories/views/category/tmpl/modal_associations.php', - '/administrator/components/com_categories/views/category/tmpl/modal_extrafields.php', - '/administrator/components/com_categories/views/category/tmpl/modal_metadata.php', - '/administrator/components/com_categories/views/category/tmpl/modal_options.php', - '/administrator/components/com_categories/views/category/view.html.php', - '/administrator/components/com_checkin/checkin.php', - '/administrator/components/com_checkin/controller.php', - '/administrator/components/com_checkin/models/checkin.php', - '/administrator/components/com_checkin/models/forms/filter_checkin.xml', - '/administrator/components/com_checkin/views/checkin/tmpl/default.php', - '/administrator/components/com_checkin/views/checkin/tmpl/default.xml', - '/administrator/components/com_checkin/views/checkin/view.html.php', - '/administrator/components/com_config/config.php', - '/administrator/components/com_config/controller.php', - '/administrator/components/com_config/controller/application/cancel.php', - '/administrator/components/com_config/controller/application/display.php', - '/administrator/components/com_config/controller/application/removeroot.php', - '/administrator/components/com_config/controller/application/save.php', - '/administrator/components/com_config/controller/application/sendtestmail.php', - '/administrator/components/com_config/controller/application/store.php', - '/administrator/components/com_config/controller/component/cancel.php', - '/administrator/components/com_config/controller/component/display.php', - '/administrator/components/com_config/controller/component/save.php', - '/administrator/components/com_config/controllers/application.php', - '/administrator/components/com_config/controllers/component.php', - '/administrator/components/com_config/helper/config.php', - '/administrator/components/com_config/model/application.php', - '/administrator/components/com_config/model/component.php', - '/administrator/components/com_config/model/field/configcomponents.php', - '/administrator/components/com_config/model/field/filters.php', - '/administrator/components/com_config/model/form/application.xml', - '/administrator/components/com_config/models/application.php', - '/administrator/components/com_config/models/component.php', - '/administrator/components/com_config/view/application/html.php', - '/administrator/components/com_config/view/application/json.php', - '/administrator/components/com_config/view/application/tmpl/default.php', - '/administrator/components/com_config/view/application/tmpl/default.xml', - '/administrator/components/com_config/view/application/tmpl/default_cache.php', - '/administrator/components/com_config/view/application/tmpl/default_cookie.php', - '/administrator/components/com_config/view/application/tmpl/default_database.php', - '/administrator/components/com_config/view/application/tmpl/default_debug.php', - '/administrator/components/com_config/view/application/tmpl/default_filters.php', - '/administrator/components/com_config/view/application/tmpl/default_ftp.php', - '/administrator/components/com_config/view/application/tmpl/default_ftplogin.php', - '/administrator/components/com_config/view/application/tmpl/default_locale.php', - '/administrator/components/com_config/view/application/tmpl/default_mail.php', - '/administrator/components/com_config/view/application/tmpl/default_metadata.php', - '/administrator/components/com_config/view/application/tmpl/default_navigation.php', - '/administrator/components/com_config/view/application/tmpl/default_permissions.php', - '/administrator/components/com_config/view/application/tmpl/default_proxy.php', - '/administrator/components/com_config/view/application/tmpl/default_seo.php', - '/administrator/components/com_config/view/application/tmpl/default_server.php', - '/administrator/components/com_config/view/application/tmpl/default_session.php', - '/administrator/components/com_config/view/application/tmpl/default_site.php', - '/administrator/components/com_config/view/application/tmpl/default_system.php', - '/administrator/components/com_config/view/component/html.php', - '/administrator/components/com_config/view/component/tmpl/default.php', - '/administrator/components/com_config/view/component/tmpl/default.xml', - '/administrator/components/com_config/view/component/tmpl/default_navigation.php', - '/administrator/components/com_contact/contact.php', - '/administrator/components/com_contact/controller.php', - '/administrator/components/com_contact/controllers/ajax.json.php', - '/administrator/components/com_contact/controllers/contact.php', - '/administrator/components/com_contact/controllers/contacts.php', - '/administrator/components/com_contact/helpers/associations.php', - '/administrator/components/com_contact/helpers/html/contact.php', - '/administrator/components/com_contact/models/contact.php', - '/administrator/components/com_contact/models/contacts.php', - '/administrator/components/com_contact/models/fields/modal/contact.php', - '/administrator/components/com_contact/models/forms/contact.xml', - '/administrator/components/com_contact/models/forms/fields/mail.xml', - '/administrator/components/com_contact/models/forms/filter_contacts.xml', - '/administrator/components/com_contact/tables/contact.php', - '/administrator/components/com_contact/views/contact/tmpl/edit.php', - '/administrator/components/com_contact/views/contact/tmpl/edit_associations.php', - '/administrator/components/com_contact/views/contact/tmpl/edit_metadata.php', - '/administrator/components/com_contact/views/contact/tmpl/edit_params.php', - '/administrator/components/com_contact/views/contact/tmpl/modal.php', - '/administrator/components/com_contact/views/contact/tmpl/modal_associations.php', - '/administrator/components/com_contact/views/contact/tmpl/modal_metadata.php', - '/administrator/components/com_contact/views/contact/tmpl/modal_params.php', - '/administrator/components/com_contact/views/contact/view.html.php', - '/administrator/components/com_contact/views/contacts/tmpl/default.php', - '/administrator/components/com_contact/views/contacts/tmpl/default_batch.php', - '/administrator/components/com_contact/views/contacts/tmpl/default_batch_body.php', - '/administrator/components/com_contact/views/contacts/tmpl/default_batch_footer.php', - '/administrator/components/com_contact/views/contacts/tmpl/modal.php', - '/administrator/components/com_contact/views/contacts/view.html.php', - '/administrator/components/com_content/content.php', - '/administrator/components/com_content/controller.php', - '/administrator/components/com_content/controllers/ajax.json.php', - '/administrator/components/com_content/controllers/article.php', - '/administrator/components/com_content/controllers/articles.php', - '/administrator/components/com_content/controllers/featured.php', - '/administrator/components/com_content/helpers/associations.php', - '/administrator/components/com_content/helpers/html/contentadministrator.php', - '/administrator/components/com_content/models/article.php', - '/administrator/components/com_content/models/articles.php', - '/administrator/components/com_content/models/feature.php', - '/administrator/components/com_content/models/featured.php', - '/administrator/components/com_content/models/fields/modal/article.php', - '/administrator/components/com_content/models/fields/voteradio.php', - '/administrator/components/com_content/models/forms/article.xml', - '/administrator/components/com_content/models/forms/filter_articles.xml', - '/administrator/components/com_content/models/forms/filter_featured.xml', - '/administrator/components/com_content/tables/featured.php', - '/administrator/components/com_content/views/article/tmpl/edit.php', - '/administrator/components/com_content/views/article/tmpl/edit.xml', - '/administrator/components/com_content/views/article/tmpl/edit_associations.php', - '/administrator/components/com_content/views/article/tmpl/edit_metadata.php', - '/administrator/components/com_content/views/article/tmpl/modal.php', - '/administrator/components/com_content/views/article/tmpl/modal_associations.php', - '/administrator/components/com_content/views/article/tmpl/modal_metadata.php', - '/administrator/components/com_content/views/article/tmpl/pagebreak.php', - '/administrator/components/com_content/views/article/view.html.php', - '/administrator/components/com_content/views/articles/tmpl/default.php', - '/administrator/components/com_content/views/articles/tmpl/default.xml', - '/administrator/components/com_content/views/articles/tmpl/default_batch_body.php', - '/administrator/components/com_content/views/articles/tmpl/default_batch_footer.php', - '/administrator/components/com_content/views/articles/tmpl/modal.php', - '/administrator/components/com_content/views/articles/view.html.php', - '/administrator/components/com_content/views/featured/tmpl/default.php', - '/administrator/components/com_content/views/featured/tmpl/default.xml', - '/administrator/components/com_content/views/featured/view.html.php', - '/administrator/components/com_contenthistory/contenthistory.php', - '/administrator/components/com_contenthistory/controller.php', - '/administrator/components/com_contenthistory/controllers/history.php', - '/administrator/components/com_contenthistory/controllers/preview.php', - '/administrator/components/com_contenthistory/helpers/html/textdiff.php', - '/administrator/components/com_contenthistory/models/compare.php', - '/administrator/components/com_contenthistory/models/history.php', - '/administrator/components/com_contenthistory/models/preview.php', - '/administrator/components/com_contenthistory/views/compare/tmpl/compare.php', - '/administrator/components/com_contenthistory/views/compare/view.html.php', - '/administrator/components/com_contenthistory/views/history/tmpl/modal.php', - '/administrator/components/com_contenthistory/views/history/view.html.php', - '/administrator/components/com_contenthistory/views/preview/tmpl/preview.php', - '/administrator/components/com_contenthistory/views/preview/view.html.php', - '/administrator/components/com_cpanel/controller.php', - '/administrator/components/com_cpanel/cpanel.php', - '/administrator/components/com_cpanel/views/cpanel/tmpl/default.php', - '/administrator/components/com_cpanel/views/cpanel/tmpl/default.xml', - '/administrator/components/com_cpanel/views/cpanel/view.html.php', - '/administrator/components/com_fields/controller.php', - '/administrator/components/com_fields/controllers/field.php', - '/administrator/components/com_fields/controllers/fields.php', - '/administrator/components/com_fields/controllers/group.php', - '/administrator/components/com_fields/controllers/groups.php', - '/administrator/components/com_fields/fields.php', - '/administrator/components/com_fields/libraries/fieldslistplugin.php', - '/administrator/components/com_fields/libraries/fieldsplugin.php', - '/administrator/components/com_fields/models/field.php', - '/administrator/components/com_fields/models/fields.php', - '/administrator/components/com_fields/models/fields/fieldcontexts.php', - '/administrator/components/com_fields/models/fields/fieldgroups.php', - '/administrator/components/com_fields/models/fields/fieldlayout.php', - '/administrator/components/com_fields/models/fields/section.php', - '/administrator/components/com_fields/models/fields/type.php', - '/administrator/components/com_fields/models/forms/field.xml', - '/administrator/components/com_fields/models/forms/filter_fields.xml', - '/administrator/components/com_fields/models/forms/filter_groups.xml', - '/administrator/components/com_fields/models/forms/group.xml', - '/administrator/components/com_fields/models/group.php', - '/administrator/components/com_fields/models/groups.php', - '/administrator/components/com_fields/tables/field.php', - '/administrator/components/com_fields/tables/group.php', - '/administrator/components/com_fields/views/field/tmpl/edit.php', - '/administrator/components/com_fields/views/field/view.html.php', - '/administrator/components/com_fields/views/fields/tmpl/default.php', - '/administrator/components/com_fields/views/fields/tmpl/default_batch_body.php', - '/administrator/components/com_fields/views/fields/tmpl/default_batch_footer.php', - '/administrator/components/com_fields/views/fields/tmpl/modal.php', - '/administrator/components/com_fields/views/fields/view.html.php', - '/administrator/components/com_fields/views/group/tmpl/edit.php', - '/administrator/components/com_fields/views/group/view.html.php', - '/administrator/components/com_fields/views/groups/tmpl/default.php', - '/administrator/components/com_fields/views/groups/tmpl/default_batch_body.php', - '/administrator/components/com_fields/views/groups/tmpl/default_batch_footer.php', - '/administrator/components/com_fields/views/groups/view.html.php', - '/administrator/components/com_finder/controller.php', - '/administrator/components/com_finder/controllers/filter.php', - '/administrator/components/com_finder/controllers/filters.php', - '/administrator/components/com_finder/controllers/index.php', - '/administrator/components/com_finder/controllers/indexer.json.php', - '/administrator/components/com_finder/controllers/maps.php', - '/administrator/components/com_finder/finder.php', - '/administrator/components/com_finder/helpers/finder.php', - '/administrator/components/com_finder/helpers/html/finder.php', - '/administrator/components/com_finder/helpers/indexer/driver/mysql.php', - '/administrator/components/com_finder/helpers/indexer/driver/postgresql.php', - '/administrator/components/com_finder/helpers/indexer/driver/sqlsrv.php', - '/administrator/components/com_finder/helpers/indexer/indexer.php', - '/administrator/components/com_finder/helpers/indexer/parser/html.php', - '/administrator/components/com_finder/helpers/indexer/parser/rtf.php', - '/administrator/components/com_finder/helpers/indexer/parser/txt.php', - '/administrator/components/com_finder/helpers/indexer/stemmer.php', - '/administrator/components/com_finder/helpers/indexer/stemmer/fr.php', - '/administrator/components/com_finder/helpers/indexer/stemmer/porter_en.php', - '/administrator/components/com_finder/helpers/indexer/stemmer/snowball.php', - '/administrator/components/com_finder/models/fields/branches.php', - '/administrator/components/com_finder/models/fields/contentmap.php', - '/administrator/components/com_finder/models/fields/contenttypes.php', - '/administrator/components/com_finder/models/fields/directories.php', - '/administrator/components/com_finder/models/fields/searchfilter.php', - '/administrator/components/com_finder/models/filter.php', - '/administrator/components/com_finder/models/filters.php', - '/administrator/components/com_finder/models/forms/filter.xml', - '/administrator/components/com_finder/models/forms/filter_filters.xml', - '/administrator/components/com_finder/models/forms/filter_index.xml', - '/administrator/components/com_finder/models/forms/filter_maps.xml', - '/administrator/components/com_finder/models/index.php', - '/administrator/components/com_finder/models/indexer.php', - '/administrator/components/com_finder/models/maps.php', - '/administrator/components/com_finder/models/statistics.php', - '/administrator/components/com_finder/tables/filter.php', - '/administrator/components/com_finder/tables/link.php', - '/administrator/components/com_finder/tables/map.php', - '/administrator/components/com_finder/views/filter/tmpl/edit.php', - '/administrator/components/com_finder/views/filter/view.html.php', - '/administrator/components/com_finder/views/filters/tmpl/default.php', - '/administrator/components/com_finder/views/filters/view.html.php', - '/administrator/components/com_finder/views/index/tmpl/default.php', - '/administrator/components/com_finder/views/index/view.html.php', - '/administrator/components/com_finder/views/indexer/tmpl/default.php', - '/administrator/components/com_finder/views/indexer/view.html.php', - '/administrator/components/com_finder/views/maps/tmpl/default.php', - '/administrator/components/com_finder/views/maps/view.html.php', - '/administrator/components/com_finder/views/statistics/tmpl/default.php', - '/administrator/components/com_finder/views/statistics/view.html.php', - '/administrator/components/com_installer/controller.php', - '/administrator/components/com_installer/controllers/database.php', - '/administrator/components/com_installer/controllers/discover.php', - '/administrator/components/com_installer/controllers/install.php', - '/administrator/components/com_installer/controllers/manage.php', - '/administrator/components/com_installer/controllers/update.php', - '/administrator/components/com_installer/controllers/updatesites.php', - '/administrator/components/com_installer/helpers/html/manage.php', - '/administrator/components/com_installer/helpers/html/updatesites.php', - '/administrator/components/com_installer/installer.php', - '/administrator/components/com_installer/models/database.php', - '/administrator/components/com_installer/models/discover.php', - '/administrator/components/com_installer/models/extension.php', - '/administrator/components/com_installer/models/fields/extensionstatus.php', - '/administrator/components/com_installer/models/fields/folder.php', - '/administrator/components/com_installer/models/fields/location.php', - '/administrator/components/com_installer/models/fields/type.php', - '/administrator/components/com_installer/models/forms/filter_discover.xml', - '/administrator/components/com_installer/models/forms/filter_languages.xml', - '/administrator/components/com_installer/models/forms/filter_manage.xml', - '/administrator/components/com_installer/models/forms/filter_update.xml', - '/administrator/components/com_installer/models/forms/filter_updatesites.xml', - '/administrator/components/com_installer/models/install.php', - '/administrator/components/com_installer/models/languages.php', - '/administrator/components/com_installer/models/manage.php', - '/administrator/components/com_installer/models/update.php', - '/administrator/components/com_installer/models/updatesites.php', - '/administrator/components/com_installer/models/warnings.php', - '/administrator/components/com_installer/views/database/tmpl/default.php', - '/administrator/components/com_installer/views/database/tmpl/default.xml', - '/administrator/components/com_installer/views/database/view.html.php', - '/administrator/components/com_installer/views/default/tmpl/default_ftp.php', - '/administrator/components/com_installer/views/default/tmpl/default_message.php', - '/administrator/components/com_installer/views/default/view.php', - '/administrator/components/com_installer/views/discover/tmpl/default.php', - '/administrator/components/com_installer/views/discover/tmpl/default.xml', - '/administrator/components/com_installer/views/discover/tmpl/default_item.php', - '/administrator/components/com_installer/views/discover/view.html.php', - '/administrator/components/com_installer/views/install/tmpl/default.php', - '/administrator/components/com_installer/views/install/tmpl/default.xml', - '/administrator/components/com_installer/views/install/view.html.php', - '/administrator/components/com_installer/views/languages/tmpl/default.php', - '/administrator/components/com_installer/views/languages/tmpl/default.xml', - '/administrator/components/com_installer/views/languages/view.html.php', - '/administrator/components/com_installer/views/manage/tmpl/default.php', - '/administrator/components/com_installer/views/manage/tmpl/default.xml', - '/administrator/components/com_installer/views/manage/view.html.php', - '/administrator/components/com_installer/views/update/tmpl/default.php', - '/administrator/components/com_installer/views/update/tmpl/default.xml', - '/administrator/components/com_installer/views/update/view.html.php', - '/administrator/components/com_installer/views/updatesites/tmpl/default.php', - '/administrator/components/com_installer/views/updatesites/tmpl/default.xml', - '/administrator/components/com_installer/views/updatesites/view.html.php', - '/administrator/components/com_installer/views/warnings/tmpl/default.php', - '/administrator/components/com_installer/views/warnings/tmpl/default.xml', - '/administrator/components/com_installer/views/warnings/view.html.php', - '/administrator/components/com_joomlaupdate/controller.php', - '/administrator/components/com_joomlaupdate/controllers/update.php', - '/administrator/components/com_joomlaupdate/helpers/joomlaupdate.php', - '/administrator/components/com_joomlaupdate/helpers/select.php', - '/administrator/components/com_joomlaupdate/joomlaupdate.php', - '/administrator/components/com_joomlaupdate/models/default.php', - '/administrator/components/com_joomlaupdate/restore.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/complete.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default.xml', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default_nodownload.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default_noupdate.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default_preupdatecheck.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default_reinstall.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default_update.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default_updatemefirst.php', - '/administrator/components/com_joomlaupdate/views/default/tmpl/default_upload.php', - '/administrator/components/com_joomlaupdate/views/default/view.html.php', - '/administrator/components/com_joomlaupdate/views/update/tmpl/default.php', - '/administrator/components/com_joomlaupdate/views/update/tmpl/finaliseconfirm.php', - '/administrator/components/com_joomlaupdate/views/update/view.html.php', - '/administrator/components/com_joomlaupdate/views/upload/tmpl/captive.php', - '/administrator/components/com_joomlaupdate/views/upload/view.html.php', - '/administrator/components/com_languages/controller.php', - '/administrator/components/com_languages/controllers/installed.php', - '/administrator/components/com_languages/controllers/language.php', - '/administrator/components/com_languages/controllers/languages.php', - '/administrator/components/com_languages/controllers/override.php', - '/administrator/components/com_languages/controllers/overrides.php', - '/administrator/components/com_languages/controllers/strings.json.php', - '/administrator/components/com_languages/helpers/html/languages.php', - '/administrator/components/com_languages/helpers/jsonresponse.php', - '/administrator/components/com_languages/helpers/languages.php', - '/administrator/components/com_languages/helpers/multilangstatus.php', - '/administrator/components/com_languages/languages.php', - '/administrator/components/com_languages/layouts/joomla/searchtools/default/bar.php', - '/administrator/components/com_languages/models/fields/languageclient.php', - '/administrator/components/com_languages/models/forms/filter_installed.xml', - '/administrator/components/com_languages/models/forms/filter_languages.xml', - '/administrator/components/com_languages/models/forms/filter_overrides.xml', - '/administrator/components/com_languages/models/forms/language.xml', - '/administrator/components/com_languages/models/forms/override.xml', - '/administrator/components/com_languages/models/installed.php', - '/administrator/components/com_languages/models/language.php', - '/administrator/components/com_languages/models/languages.php', - '/administrator/components/com_languages/models/override.php', - '/administrator/components/com_languages/models/overrides.php', - '/administrator/components/com_languages/models/strings.php', - '/administrator/components/com_languages/views/installed/tmpl/default.php', - '/administrator/components/com_languages/views/installed/tmpl/default.xml', - '/administrator/components/com_languages/views/installed/view.html.php', - '/administrator/components/com_languages/views/language/tmpl/edit.php', - '/administrator/components/com_languages/views/language/view.html.php', - '/administrator/components/com_languages/views/languages/tmpl/default.php', - '/administrator/components/com_languages/views/languages/tmpl/default.xml', - '/administrator/components/com_languages/views/languages/view.html.php', - '/administrator/components/com_languages/views/multilangstatus/tmpl/default.php', - '/administrator/components/com_languages/views/multilangstatus/view.html.php', - '/administrator/components/com_languages/views/override/tmpl/edit.php', - '/administrator/components/com_languages/views/override/view.html.php', - '/administrator/components/com_languages/views/overrides/tmpl/default.php', - '/administrator/components/com_languages/views/overrides/tmpl/default.xml', - '/administrator/components/com_languages/views/overrides/view.html.php', - '/administrator/components/com_login/controller.php', - '/administrator/components/com_login/login.php', - '/administrator/components/com_login/models/login.php', - '/administrator/components/com_login/views/login/tmpl/default.php', - '/administrator/components/com_login/views/login/view.html.php', - '/administrator/components/com_media/controller.php', - '/administrator/components/com_media/controllers/file.json.php', - '/administrator/components/com_media/controllers/file.php', - '/administrator/components/com_media/controllers/folder.php', - '/administrator/components/com_media/layouts/toolbar/deletemedia.php', - '/administrator/components/com_media/layouts/toolbar/newfolder.php', - '/administrator/components/com_media/layouts/toolbar/uploadmedia.php', - '/administrator/components/com_media/media.php', - '/administrator/components/com_media/models/list.php', - '/administrator/components/com_media/models/manager.php', - '/administrator/components/com_media/views/images/tmpl/default.php', - '/administrator/components/com_media/views/images/view.html.php', - '/administrator/components/com_media/views/imageslist/tmpl/default.php', - '/administrator/components/com_media/views/imageslist/tmpl/default_folder.php', - '/administrator/components/com_media/views/imageslist/tmpl/default_image.php', - '/administrator/components/com_media/views/imageslist/view.html.php', - '/administrator/components/com_media/views/media/tmpl/default.php', - '/administrator/components/com_media/views/media/tmpl/default.xml', - '/administrator/components/com_media/views/media/tmpl/default_folders.php', - '/administrator/components/com_media/views/media/tmpl/default_navigation.php', - '/administrator/components/com_media/views/media/view.html.php', - '/administrator/components/com_media/views/medialist/tmpl/default.php', - '/administrator/components/com_media/views/medialist/tmpl/details.php', - '/administrator/components/com_media/views/medialist/tmpl/details_doc.php', - '/administrator/components/com_media/views/medialist/tmpl/details_docs.php', - '/administrator/components/com_media/views/medialist/tmpl/details_folder.php', - '/administrator/components/com_media/views/medialist/tmpl/details_folders.php', - '/administrator/components/com_media/views/medialist/tmpl/details_img.php', - '/administrator/components/com_media/views/medialist/tmpl/details_imgs.php', - '/administrator/components/com_media/views/medialist/tmpl/details_up.php', - '/administrator/components/com_media/views/medialist/tmpl/details_video.php', - '/administrator/components/com_media/views/medialist/tmpl/details_videos.php', - '/administrator/components/com_media/views/medialist/tmpl/thumbs.php', - '/administrator/components/com_media/views/medialist/tmpl/thumbs_docs.php', - '/administrator/components/com_media/views/medialist/tmpl/thumbs_folders.php', - '/administrator/components/com_media/views/medialist/tmpl/thumbs_imgs.php', - '/administrator/components/com_media/views/medialist/tmpl/thumbs_up.php', - '/administrator/components/com_media/views/medialist/tmpl/thumbs_videos.php', - '/administrator/components/com_media/views/medialist/view.html.php', - '/administrator/components/com_menus/controller.php', - '/administrator/components/com_menus/controllers/ajax.json.php', - '/administrator/components/com_menus/controllers/item.php', - '/administrator/components/com_menus/controllers/items.php', - '/administrator/components/com_menus/controllers/menu.php', - '/administrator/components/com_menus/controllers/menus.php', - '/administrator/components/com_menus/helpers/associations.php', - '/administrator/components/com_menus/helpers/html/menus.php', - '/administrator/components/com_menus/layouts/joomla/searchtools/default/bar.php', - '/administrator/components/com_menus/menus.php', - '/administrator/components/com_menus/models/fields/componentscategory.php', - '/administrator/components/com_menus/models/fields/menuitembytype.php', - '/administrator/components/com_menus/models/fields/menuordering.php', - '/administrator/components/com_menus/models/fields/menuparent.php', - '/administrator/components/com_menus/models/fields/menupreset.php', - '/administrator/components/com_menus/models/fields/menutype.php', - '/administrator/components/com_menus/models/fields/modal/menu.php', - '/administrator/components/com_menus/models/forms/filter_items.xml', - '/administrator/components/com_menus/models/forms/filter_itemsadmin.xml', - '/administrator/components/com_menus/models/forms/filter_menus.xml', - '/administrator/components/com_menus/models/forms/item.xml', - '/administrator/components/com_menus/models/forms/item_alias.xml', - '/administrator/components/com_menus/models/forms/item_component.xml', - '/administrator/components/com_menus/models/forms/item_heading.xml', - '/administrator/components/com_menus/models/forms/item_separator.xml', - '/administrator/components/com_menus/models/forms/item_url.xml', - '/administrator/components/com_menus/models/forms/itemadmin.xml', - '/administrator/components/com_menus/models/forms/itemadmin_alias.xml', - '/administrator/components/com_menus/models/forms/itemadmin_component.xml', - '/administrator/components/com_menus/models/forms/itemadmin_container.xml', - '/administrator/components/com_menus/models/forms/itemadmin_heading.xml', - '/administrator/components/com_menus/models/forms/itemadmin_separator.xml', - '/administrator/components/com_menus/models/forms/itemadmin_url.xml', - '/administrator/components/com_menus/models/forms/menu.xml', - '/administrator/components/com_menus/models/item.php', - '/administrator/components/com_menus/models/items.php', - '/administrator/components/com_menus/models/menu.php', - '/administrator/components/com_menus/models/menus.php', - '/administrator/components/com_menus/models/menutypes.php', - '/administrator/components/com_menus/presets/joomla.xml', - '/administrator/components/com_menus/presets/modern.xml', - '/administrator/components/com_menus/tables/menu.php', - '/administrator/components/com_menus/views/item/tmpl/edit.php', - '/administrator/components/com_menus/views/item/tmpl/edit.xml', - '/administrator/components/com_menus/views/item/tmpl/edit_associations.php', - '/administrator/components/com_menus/views/item/tmpl/edit_container.php', - '/administrator/components/com_menus/views/item/tmpl/edit_modules.php', - '/administrator/components/com_menus/views/item/tmpl/edit_options.php', - '/administrator/components/com_menus/views/item/tmpl/modal.php', - '/administrator/components/com_menus/views/item/tmpl/modal_associations.php', - '/administrator/components/com_menus/views/item/tmpl/modal_options.php', - '/administrator/components/com_menus/views/item/view.html.php', - '/administrator/components/com_menus/views/items/tmpl/default.php', - '/administrator/components/com_menus/views/items/tmpl/default.xml', - '/administrator/components/com_menus/views/items/tmpl/default_batch_body.php', - '/administrator/components/com_menus/views/items/tmpl/default_batch_footer.php', - '/administrator/components/com_menus/views/items/tmpl/modal.php', - '/administrator/components/com_menus/views/items/view.html.php', - '/administrator/components/com_menus/views/menu/tmpl/edit.php', - '/administrator/components/com_menus/views/menu/tmpl/edit.xml', - '/administrator/components/com_menus/views/menu/view.html.php', - '/administrator/components/com_menus/views/menu/view.xml.php', - '/administrator/components/com_menus/views/menus/tmpl/default.php', - '/administrator/components/com_menus/views/menus/tmpl/default.xml', - '/administrator/components/com_menus/views/menus/view.html.php', - '/administrator/components/com_menus/views/menutypes/tmpl/default.php', - '/administrator/components/com_menus/views/menutypes/view.html.php', - '/administrator/components/com_messages/controller.php', - '/administrator/components/com_messages/controllers/config.php', - '/administrator/components/com_messages/controllers/message.php', - '/administrator/components/com_messages/controllers/messages.php', - '/administrator/components/com_messages/helpers/html/messages.php', - '/administrator/components/com_messages/helpers/messages.php', - '/administrator/components/com_messages/messages.php', - '/administrator/components/com_messages/models/config.php', - '/administrator/components/com_messages/models/fields/messagestates.php', - '/administrator/components/com_messages/models/fields/usermessages.php', - '/administrator/components/com_messages/models/forms/config.xml', - '/administrator/components/com_messages/models/forms/filter_messages.xml', - '/administrator/components/com_messages/models/forms/message.xml', - '/administrator/components/com_messages/models/message.php', - '/administrator/components/com_messages/models/messages.php', - '/administrator/components/com_messages/tables/message.php', - '/administrator/components/com_messages/views/config/tmpl/default.php', - '/administrator/components/com_messages/views/config/view.html.php', - '/administrator/components/com_messages/views/message/tmpl/default.php', - '/administrator/components/com_messages/views/message/tmpl/edit.php', - '/administrator/components/com_messages/views/message/view.html.php', - '/administrator/components/com_messages/views/messages/tmpl/default.php', - '/administrator/components/com_messages/views/messages/view.html.php', - '/administrator/components/com_modules/controller.php', - '/administrator/components/com_modules/controllers/module.php', - '/administrator/components/com_modules/controllers/modules.php', - '/administrator/components/com_modules/helpers/html/modules.php', - '/administrator/components/com_modules/helpers/xml.php', - '/administrator/components/com_modules/layouts/toolbar/newmodule.php', - '/administrator/components/com_modules/models/fields/modulesmodule.php', - '/administrator/components/com_modules/models/fields/modulesposition.php', - '/administrator/components/com_modules/models/forms/advanced.xml', - '/administrator/components/com_modules/models/forms/filter_modules.xml', - '/administrator/components/com_modules/models/forms/filter_modulesadmin.xml', - '/administrator/components/com_modules/models/forms/module.xml', - '/administrator/components/com_modules/models/forms/moduleadmin.xml', - '/administrator/components/com_modules/models/module.php', - '/administrator/components/com_modules/models/modules.php', - '/administrator/components/com_modules/models/positions.php', - '/administrator/components/com_modules/models/select.php', - '/administrator/components/com_modules/modules.php', - '/administrator/components/com_modules/views/module/tmpl/edit.php', - '/administrator/components/com_modules/views/module/tmpl/edit_assignment.php', - '/administrator/components/com_modules/views/module/tmpl/edit_options.php', - '/administrator/components/com_modules/views/module/tmpl/edit_positions.php', - '/administrator/components/com_modules/views/module/tmpl/modal.php', - '/administrator/components/com_modules/views/module/view.html.php', - '/administrator/components/com_modules/views/module/view.json.php', - '/administrator/components/com_modules/views/modules/tmpl/default.php', - '/administrator/components/com_modules/views/modules/tmpl/default.xml', - '/administrator/components/com_modules/views/modules/tmpl/default_batch_body.php', - '/administrator/components/com_modules/views/modules/tmpl/default_batch_footer.php', - '/administrator/components/com_modules/views/modules/tmpl/modal.php', - '/administrator/components/com_modules/views/modules/view.html.php', - '/administrator/components/com_modules/views/positions/tmpl/modal.php', - '/administrator/components/com_modules/views/positions/view.html.php', - '/administrator/components/com_modules/views/preview/tmpl/default.php', - '/administrator/components/com_modules/views/preview/view.html.php', - '/administrator/components/com_modules/views/select/tmpl/default.php', - '/administrator/components/com_modules/views/select/view.html.php', - '/administrator/components/com_newsfeeds/controller.php', - '/administrator/components/com_newsfeeds/controllers/ajax.json.php', - '/administrator/components/com_newsfeeds/controllers/newsfeed.php', - '/administrator/components/com_newsfeeds/controllers/newsfeeds.php', - '/administrator/components/com_newsfeeds/helpers/associations.php', - '/administrator/components/com_newsfeeds/helpers/html/newsfeed.php', - '/administrator/components/com_newsfeeds/models/fields/modal/newsfeed.php', - '/administrator/components/com_newsfeeds/models/fields/newsfeeds.php', - '/administrator/components/com_newsfeeds/models/forms/filter_newsfeeds.xml', - '/administrator/components/com_newsfeeds/models/forms/newsfeed.xml', - '/administrator/components/com_newsfeeds/models/newsfeed.php', - '/administrator/components/com_newsfeeds/models/newsfeeds.php', - '/administrator/components/com_newsfeeds/newsfeeds.php', - '/administrator/components/com_newsfeeds/tables/newsfeed.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_associations.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_display.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_metadata.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_params.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_associations.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_display.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_metadata.php', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_params.php', - '/administrator/components/com_newsfeeds/views/newsfeed/view.html.php', - '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/default.php', - '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/default_batch_body.php', - '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/default_batch_footer.php', - '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/modal.php', - '/administrator/components/com_newsfeeds/views/newsfeeds/view.html.php', - '/administrator/components/com_plugins/controller.php', - '/administrator/components/com_plugins/controllers/plugin.php', - '/administrator/components/com_plugins/controllers/plugins.php', - '/administrator/components/com_plugins/models/fields/pluginelement.php', - '/administrator/components/com_plugins/models/fields/pluginordering.php', - '/administrator/components/com_plugins/models/fields/plugintype.php', - '/administrator/components/com_plugins/models/forms/filter_plugins.xml', - '/administrator/components/com_plugins/models/forms/plugin.xml', - '/administrator/components/com_plugins/models/plugin.php', - '/administrator/components/com_plugins/models/plugins.php', - '/administrator/components/com_plugins/plugins.php', - '/administrator/components/com_plugins/views/plugin/tmpl/edit.php', - '/administrator/components/com_plugins/views/plugin/tmpl/edit_options.php', - '/administrator/components/com_plugins/views/plugin/tmpl/modal.php', - '/administrator/components/com_plugins/views/plugin/view.html.php', - '/administrator/components/com_plugins/views/plugins/tmpl/default.php', - '/administrator/components/com_plugins/views/plugins/tmpl/default.xml', - '/administrator/components/com_plugins/views/plugins/view.html.php', - '/administrator/components/com_postinstall/controllers/message.php', - '/administrator/components/com_postinstall/fof.xml', - '/administrator/components/com_postinstall/models/messages.php', - '/administrator/components/com_postinstall/postinstall.php', - '/administrator/components/com_postinstall/toolbar.php', - '/administrator/components/com_postinstall/views/messages/tmpl/default.php', - '/administrator/components/com_postinstall/views/messages/tmpl/default.xml', - '/administrator/components/com_postinstall/views/messages/view.html.php', - '/administrator/components/com_privacy/controller.php', - '/administrator/components/com_privacy/controllers/consents.php', - '/administrator/components/com_privacy/controllers/request.php', - '/administrator/components/com_privacy/controllers/request.xml.php', - '/administrator/components/com_privacy/controllers/requests.php', - '/administrator/components/com_privacy/helpers/export/domain.php', - '/administrator/components/com_privacy/helpers/export/field.php', - '/administrator/components/com_privacy/helpers/export/item.php', - '/administrator/components/com_privacy/helpers/html/helper.php', - '/administrator/components/com_privacy/helpers/plugin.php', - '/administrator/components/com_privacy/helpers/privacy.php', - '/administrator/components/com_privacy/helpers/removal/status.php', - '/administrator/components/com_privacy/models/capabilities.php', - '/administrator/components/com_privacy/models/consents.php', - '/administrator/components/com_privacy/models/dashboard.php', - '/administrator/components/com_privacy/models/export.php', - '/administrator/components/com_privacy/models/fields/requeststatus.php', - '/administrator/components/com_privacy/models/fields/requesttype.php', - '/administrator/components/com_privacy/models/forms/filter_consents.xml', - '/administrator/components/com_privacy/models/forms/filter_requests.xml', - '/administrator/components/com_privacy/models/forms/request.xml', - '/administrator/components/com_privacy/models/remove.php', - '/administrator/components/com_privacy/models/request.php', - '/administrator/components/com_privacy/models/requests.php', - '/administrator/components/com_privacy/privacy.php', - '/administrator/components/com_privacy/tables/consent.php', - '/administrator/components/com_privacy/tables/request.php', - '/administrator/components/com_privacy/views/capabilities/tmpl/default.php', - '/administrator/components/com_privacy/views/capabilities/view.html.php', - '/administrator/components/com_privacy/views/consents/tmpl/default.php', - '/administrator/components/com_privacy/views/consents/tmpl/default.xml', - '/administrator/components/com_privacy/views/consents/view.html.php', - '/administrator/components/com_privacy/views/dashboard/tmpl/default.php', - '/administrator/components/com_privacy/views/dashboard/tmpl/default.xml', - '/administrator/components/com_privacy/views/dashboard/view.html.php', - '/administrator/components/com_privacy/views/export/view.xml.php', - '/administrator/components/com_privacy/views/request/tmpl/default.php', - '/administrator/components/com_privacy/views/request/tmpl/edit.php', - '/administrator/components/com_privacy/views/request/view.html.php', - '/administrator/components/com_privacy/views/requests/tmpl/default.php', - '/administrator/components/com_privacy/views/requests/tmpl/default.xml', - '/administrator/components/com_privacy/views/requests/view.html.php', - '/administrator/components/com_redirect/controller.php', - '/administrator/components/com_redirect/controllers/link.php', - '/administrator/components/com_redirect/controllers/links.php', - '/administrator/components/com_redirect/helpers/html/redirect.php', - '/administrator/components/com_redirect/models/fields/redirect.php', - '/administrator/components/com_redirect/models/forms/filter_links.xml', - '/administrator/components/com_redirect/models/forms/link.xml', - '/administrator/components/com_redirect/models/link.php', - '/administrator/components/com_redirect/models/links.php', - '/administrator/components/com_redirect/redirect.php', - '/administrator/components/com_redirect/tables/link.php', - '/administrator/components/com_redirect/views/link/tmpl/edit.php', - '/administrator/components/com_redirect/views/link/view.html.php', - '/administrator/components/com_redirect/views/links/tmpl/default.php', - '/administrator/components/com_redirect/views/links/tmpl/default.xml', - '/administrator/components/com_redirect/views/links/tmpl/default_addform.php', - '/administrator/components/com_redirect/views/links/tmpl/default_batch_body.php', - '/administrator/components/com_redirect/views/links/tmpl/default_batch_footer.php', - '/administrator/components/com_redirect/views/links/view.html.php', - '/administrator/components/com_tags/controller.php', - '/administrator/components/com_tags/controllers/tag.php', - '/administrator/components/com_tags/controllers/tags.php', - '/administrator/components/com_tags/helpers/tags.php', - '/administrator/components/com_tags/models/forms/filter_tags.xml', - '/administrator/components/com_tags/models/forms/tag.xml', - '/administrator/components/com_tags/models/tag.php', - '/administrator/components/com_tags/models/tags.php', - '/administrator/components/com_tags/tables/tag.php', - '/administrator/components/com_tags/tags.php', - '/administrator/components/com_tags/views/tag/tmpl/edit.php', - '/administrator/components/com_tags/views/tag/tmpl/edit_metadata.php', - '/administrator/components/com_tags/views/tag/tmpl/edit_options.php', - '/administrator/components/com_tags/views/tag/view.html.php', - '/administrator/components/com_tags/views/tags/tmpl/default.php', - '/administrator/components/com_tags/views/tags/tmpl/default.xml', - '/administrator/components/com_tags/views/tags/tmpl/default_batch_body.php', - '/administrator/components/com_tags/views/tags/tmpl/default_batch_footer.php', - '/administrator/components/com_tags/views/tags/view.html.php', - '/administrator/components/com_templates/controller.php', - '/administrator/components/com_templates/controllers/style.php', - '/administrator/components/com_templates/controllers/styles.php', - '/administrator/components/com_templates/controllers/template.php', - '/administrator/components/com_templates/helpers/html/templates.php', - '/administrator/components/com_templates/models/fields/templatelocation.php', - '/administrator/components/com_templates/models/fields/templatename.php', - '/administrator/components/com_templates/models/forms/filter_styles.xml', - '/administrator/components/com_templates/models/forms/filter_templates.xml', - '/administrator/components/com_templates/models/forms/source.xml', - '/administrator/components/com_templates/models/forms/style.xml', - '/administrator/components/com_templates/models/forms/style_administrator.xml', - '/administrator/components/com_templates/models/forms/style_site.xml', - '/administrator/components/com_templates/models/style.php', - '/administrator/components/com_templates/models/styles.php', - '/administrator/components/com_templates/models/template.php', - '/administrator/components/com_templates/models/templates.php', - '/administrator/components/com_templates/tables/style.php', - '/administrator/components/com_templates/templates.php', - '/administrator/components/com_templates/views/style/tmpl/edit.php', - '/administrator/components/com_templates/views/style/tmpl/edit_assignment.php', - '/administrator/components/com_templates/views/style/tmpl/edit_options.php', - '/administrator/components/com_templates/views/style/view.html.php', - '/administrator/components/com_templates/views/style/view.json.php', - '/administrator/components/com_templates/views/styles/tmpl/default.php', - '/administrator/components/com_templates/views/styles/tmpl/default.xml', - '/administrator/components/com_templates/views/styles/view.html.php', - '/administrator/components/com_templates/views/template/tmpl/default.php', - '/administrator/components/com_templates/views/template/tmpl/default_description.php', - '/administrator/components/com_templates/views/template/tmpl/default_folders.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_copy_body.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_copy_footer.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_delete_body.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_delete_footer.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_file_body.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_file_footer.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_folder_body.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_folder_footer.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_rename_body.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_rename_footer.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_resize_body.php', - '/administrator/components/com_templates/views/template/tmpl/default_modal_resize_footer.php', - '/administrator/components/com_templates/views/template/tmpl/default_tree.php', - '/administrator/components/com_templates/views/template/tmpl/readonly.php', - '/administrator/components/com_templates/views/template/view.html.php', - '/administrator/components/com_templates/views/templates/tmpl/default.php', - '/administrator/components/com_templates/views/templates/tmpl/default.xml', - '/administrator/components/com_templates/views/templates/view.html.php', - '/administrator/components/com_users/controller.php', - '/administrator/components/com_users/controllers/group.php', - '/administrator/components/com_users/controllers/groups.php', - '/administrator/components/com_users/controllers/level.php', - '/administrator/components/com_users/controllers/levels.php', - '/administrator/components/com_users/controllers/mail.php', - '/administrator/components/com_users/controllers/note.php', - '/administrator/components/com_users/controllers/notes.php', - '/administrator/components/com_users/controllers/user.php', - '/administrator/components/com_users/controllers/users.php', - '/administrator/components/com_users/helpers/html/users.php', - '/administrator/components/com_users/models/debuggroup.php', - '/administrator/components/com_users/models/debuguser.php', - '/administrator/components/com_users/models/fields/groupparent.php', - '/administrator/components/com_users/models/fields/levels.php', - '/administrator/components/com_users/models/forms/config_domain.xml', - '/administrator/components/com_users/models/forms/fields/user.xml', - '/administrator/components/com_users/models/forms/filter_debuggroup.xml', - '/administrator/components/com_users/models/forms/filter_debuguser.xml', - '/administrator/components/com_users/models/forms/filter_groups.xml', - '/administrator/components/com_users/models/forms/filter_levels.xml', - '/administrator/components/com_users/models/forms/filter_notes.xml', - '/administrator/components/com_users/models/forms/filter_users.xml', - '/administrator/components/com_users/models/forms/group.xml', - '/administrator/components/com_users/models/forms/level.xml', - '/administrator/components/com_users/models/forms/mail.xml', - '/administrator/components/com_users/models/forms/note.xml', - '/administrator/components/com_users/models/forms/user.xml', - '/administrator/components/com_users/models/group.php', - '/administrator/components/com_users/models/groups.php', - '/administrator/components/com_users/models/level.php', - '/administrator/components/com_users/models/levels.php', - '/administrator/components/com_users/models/mail.php', - '/administrator/components/com_users/models/note.php', - '/administrator/components/com_users/models/notes.php', - '/administrator/components/com_users/models/user.php', - '/administrator/components/com_users/models/users.php', - '/administrator/components/com_users/tables/note.php', - '/administrator/components/com_users/users.php', - '/administrator/components/com_users/views/debuggroup/tmpl/default.php', - '/administrator/components/com_users/views/debuggroup/view.html.php', - '/administrator/components/com_users/views/debuguser/tmpl/default.php', - '/administrator/components/com_users/views/debuguser/view.html.php', - '/administrator/components/com_users/views/group/tmpl/edit.php', - '/administrator/components/com_users/views/group/tmpl/edit.xml', - '/administrator/components/com_users/views/group/view.html.php', - '/administrator/components/com_users/views/groups/tmpl/default.php', - '/administrator/components/com_users/views/groups/tmpl/default.xml', - '/administrator/components/com_users/views/groups/view.html.php', - '/administrator/components/com_users/views/level/tmpl/edit.php', - '/administrator/components/com_users/views/level/tmpl/edit.xml', - '/administrator/components/com_users/views/level/view.html.php', - '/administrator/components/com_users/views/levels/tmpl/default.php', - '/administrator/components/com_users/views/levels/tmpl/default.xml', - '/administrator/components/com_users/views/levels/view.html.php', - '/administrator/components/com_users/views/mail/tmpl/default.php', - '/administrator/components/com_users/views/mail/tmpl/default.xml', - '/administrator/components/com_users/views/mail/view.html.php', - '/administrator/components/com_users/views/note/tmpl/edit.php', - '/administrator/components/com_users/views/note/tmpl/edit.xml', - '/administrator/components/com_users/views/note/view.html.php', - '/administrator/components/com_users/views/notes/tmpl/default.php', - '/administrator/components/com_users/views/notes/tmpl/default.xml', - '/administrator/components/com_users/views/notes/tmpl/modal.php', - '/administrator/components/com_users/views/notes/view.html.php', - '/administrator/components/com_users/views/user/tmpl/edit.php', - '/administrator/components/com_users/views/user/tmpl/edit.xml', - '/administrator/components/com_users/views/user/tmpl/edit_groups.php', - '/administrator/components/com_users/views/user/view.html.php', - '/administrator/components/com_users/views/users/tmpl/default.php', - '/administrator/components/com_users/views/users/tmpl/default.xml', - '/administrator/components/com_users/views/users/tmpl/default_batch_body.php', - '/administrator/components/com_users/views/users/tmpl/default_batch_footer.php', - '/administrator/components/com_users/views/users/tmpl/modal.php', - '/administrator/components/com_users/views/users/view.html.php', - '/administrator/help/helpsites.xml', - '/administrator/includes/helper.php', - '/administrator/includes/subtoolbar.php', - '/administrator/language/en-GB/en-GB.com_actionlogs.ini', - '/administrator/language/en-GB/en-GB.com_actionlogs.sys.ini', - '/administrator/language/en-GB/en-GB.com_admin.ini', - '/administrator/language/en-GB/en-GB.com_admin.sys.ini', - '/administrator/language/en-GB/en-GB.com_ajax.ini', - '/administrator/language/en-GB/en-GB.com_ajax.sys.ini', - '/administrator/language/en-GB/en-GB.com_associations.ini', - '/administrator/language/en-GB/en-GB.com_associations.sys.ini', - '/administrator/language/en-GB/en-GB.com_banners.ini', - '/administrator/language/en-GB/en-GB.com_banners.sys.ini', - '/administrator/language/en-GB/en-GB.com_cache.ini', - '/administrator/language/en-GB/en-GB.com_cache.sys.ini', - '/administrator/language/en-GB/en-GB.com_categories.ini', - '/administrator/language/en-GB/en-GB.com_categories.sys.ini', - '/administrator/language/en-GB/en-GB.com_checkin.ini', - '/administrator/language/en-GB/en-GB.com_checkin.sys.ini', - '/administrator/language/en-GB/en-GB.com_config.ini', - '/administrator/language/en-GB/en-GB.com_config.sys.ini', - '/administrator/language/en-GB/en-GB.com_contact.ini', - '/administrator/language/en-GB/en-GB.com_contact.sys.ini', - '/administrator/language/en-GB/en-GB.com_content.ini', - '/administrator/language/en-GB/en-GB.com_content.sys.ini', - '/administrator/language/en-GB/en-GB.com_contenthistory.ini', - '/administrator/language/en-GB/en-GB.com_contenthistory.sys.ini', - '/administrator/language/en-GB/en-GB.com_cpanel.ini', - '/administrator/language/en-GB/en-GB.com_cpanel.sys.ini', - '/administrator/language/en-GB/en-GB.com_fields.ini', - '/administrator/language/en-GB/en-GB.com_fields.sys.ini', - '/administrator/language/en-GB/en-GB.com_finder.ini', - '/administrator/language/en-GB/en-GB.com_finder.sys.ini', - '/administrator/language/en-GB/en-GB.com_installer.ini', - '/administrator/language/en-GB/en-GB.com_installer.sys.ini', - '/administrator/language/en-GB/en-GB.com_joomlaupdate.ini', - '/administrator/language/en-GB/en-GB.com_joomlaupdate.sys.ini', - '/administrator/language/en-GB/en-GB.com_languages.ini', - '/administrator/language/en-GB/en-GB.com_languages.sys.ini', - '/administrator/language/en-GB/en-GB.com_login.ini', - '/administrator/language/en-GB/en-GB.com_login.sys.ini', - '/administrator/language/en-GB/en-GB.com_mailto.sys.ini', - '/administrator/language/en-GB/en-GB.com_media.ini', - '/administrator/language/en-GB/en-GB.com_media.sys.ini', - '/administrator/language/en-GB/en-GB.com_menus.ini', - '/administrator/language/en-GB/en-GB.com_menus.sys.ini', - '/administrator/language/en-GB/en-GB.com_messages.ini', - '/administrator/language/en-GB/en-GB.com_messages.sys.ini', - '/administrator/language/en-GB/en-GB.com_modules.ini', - '/administrator/language/en-GB/en-GB.com_modules.sys.ini', - '/administrator/language/en-GB/en-GB.com_newsfeeds.ini', - '/administrator/language/en-GB/en-GB.com_newsfeeds.sys.ini', - '/administrator/language/en-GB/en-GB.com_plugins.ini', - '/administrator/language/en-GB/en-GB.com_plugins.sys.ini', - '/administrator/language/en-GB/en-GB.com_postinstall.ini', - '/administrator/language/en-GB/en-GB.com_postinstall.sys.ini', - '/administrator/language/en-GB/en-GB.com_privacy.ini', - '/administrator/language/en-GB/en-GB.com_privacy.sys.ini', - '/administrator/language/en-GB/en-GB.com_redirect.ini', - '/administrator/language/en-GB/en-GB.com_redirect.sys.ini', - '/administrator/language/en-GB/en-GB.com_tags.ini', - '/administrator/language/en-GB/en-GB.com_tags.sys.ini', - '/administrator/language/en-GB/en-GB.com_templates.ini', - '/administrator/language/en-GB/en-GB.com_templates.sys.ini', - '/administrator/language/en-GB/en-GB.com_users.ini', - '/administrator/language/en-GB/en-GB.com_users.sys.ini', - '/administrator/language/en-GB/en-GB.com_weblinks.ini', - '/administrator/language/en-GB/en-GB.com_weblinks.sys.ini', - '/administrator/language/en-GB/en-GB.com_wrapper.ini', - '/administrator/language/en-GB/en-GB.com_wrapper.sys.ini', - '/administrator/language/en-GB/en-GB.ini', - '/administrator/language/en-GB/en-GB.lib_joomla.ini', - '/administrator/language/en-GB/en-GB.localise.php', - '/administrator/language/en-GB/en-GB.mod_custom.ini', - '/administrator/language/en-GB/en-GB.mod_custom.sys.ini', - '/administrator/language/en-GB/en-GB.mod_feed.ini', - '/administrator/language/en-GB/en-GB.mod_feed.sys.ini', - '/administrator/language/en-GB/en-GB.mod_latest.ini', - '/administrator/language/en-GB/en-GB.mod_latest.sys.ini', - '/administrator/language/en-GB/en-GB.mod_latestactions.ini', - '/administrator/language/en-GB/en-GB.mod_latestactions.sys.ini', - '/administrator/language/en-GB/en-GB.mod_logged.ini', - '/administrator/language/en-GB/en-GB.mod_logged.sys.ini', - '/administrator/language/en-GB/en-GB.mod_login.ini', - '/administrator/language/en-GB/en-GB.mod_login.sys.ini', - '/administrator/language/en-GB/en-GB.mod_menu.ini', - '/administrator/language/en-GB/en-GB.mod_menu.sys.ini', - '/administrator/language/en-GB/en-GB.mod_multilangstatus.ini', - '/administrator/language/en-GB/en-GB.mod_multilangstatus.sys.ini', - '/administrator/language/en-GB/en-GB.mod_popular.ini', - '/administrator/language/en-GB/en-GB.mod_popular.sys.ini', - '/administrator/language/en-GB/en-GB.mod_privacy_dashboard.ini', - '/administrator/language/en-GB/en-GB.mod_privacy_dashboard.sys.ini', - '/administrator/language/en-GB/en-GB.mod_quickicon.ini', - '/administrator/language/en-GB/en-GB.mod_quickicon.sys.ini', - '/administrator/language/en-GB/en-GB.mod_sampledata.ini', - '/administrator/language/en-GB/en-GB.mod_sampledata.sys.ini', - '/administrator/language/en-GB/en-GB.mod_stats_admin.ini', - '/administrator/language/en-GB/en-GB.mod_stats_admin.sys.ini', - '/administrator/language/en-GB/en-GB.mod_status.ini', - '/administrator/language/en-GB/en-GB.mod_status.sys.ini', - '/administrator/language/en-GB/en-GB.mod_submenu.ini', - '/administrator/language/en-GB/en-GB.mod_submenu.sys.ini', - '/administrator/language/en-GB/en-GB.mod_title.ini', - '/administrator/language/en-GB/en-GB.mod_title.sys.ini', - '/administrator/language/en-GB/en-GB.mod_toolbar.ini', - '/administrator/language/en-GB/en-GB.mod_toolbar.sys.ini', - '/administrator/language/en-GB/en-GB.mod_version.ini', - '/administrator/language/en-GB/en-GB.mod_version.sys.ini', - '/administrator/language/en-GB/en-GB.plg_actionlog_joomla.ini', - '/administrator/language/en-GB/en-GB.plg_actionlog_joomla.sys.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_cookie.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_cookie.sys.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_gmail.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_gmail.sys.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_joomla.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_joomla.sys.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_ldap.ini', - '/administrator/language/en-GB/en-GB.plg_authentication_ldap.sys.ini', - '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha.ini', - '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha.sys.ini', - '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha_invisible.ini', - '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha_invisible.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_confirmconsent.ini', - '/administrator/language/en-GB/en-GB.plg_content_confirmconsent.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_contact.ini', - '/administrator/language/en-GB/en-GB.plg_content_contact.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_emailcloak.ini', - '/administrator/language/en-GB/en-GB.plg_content_emailcloak.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_fields.ini', - '/administrator/language/en-GB/en-GB.plg_content_fields.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_finder.ini', - '/administrator/language/en-GB/en-GB.plg_content_finder.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_joomla.ini', - '/administrator/language/en-GB/en-GB.plg_content_joomla.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_loadmodule.ini', - '/administrator/language/en-GB/en-GB.plg_content_loadmodule.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_pagebreak.ini', - '/administrator/language/en-GB/en-GB.plg_content_pagebreak.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_pagenavigation.ini', - '/administrator/language/en-GB/en-GB.plg_content_pagenavigation.sys.ini', - '/administrator/language/en-GB/en-GB.plg_content_vote.ini', - '/administrator/language/en-GB/en-GB.plg_content_vote.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_article.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_article.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_contact.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_contact.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_fields.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_fields.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_image.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_image.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_menu.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_menu.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_module.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_module.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_pagebreak.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_pagebreak.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_readmore.ini', - '/administrator/language/en-GB/en-GB.plg_editors-xtd_readmore.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors_codemirror.ini', - '/administrator/language/en-GB/en-GB.plg_editors_codemirror.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors_none.ini', - '/administrator/language/en-GB/en-GB.plg_editors_none.sys.ini', - '/administrator/language/en-GB/en-GB.plg_editors_tinymce.ini', - '/administrator/language/en-GB/en-GB.plg_editors_tinymce.sys.ini', - '/administrator/language/en-GB/en-GB.plg_extension_joomla.ini', - '/administrator/language/en-GB/en-GB.plg_extension_joomla.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_calendar.ini', - '/administrator/language/en-GB/en-GB.plg_fields_calendar.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_checkboxes.ini', - '/administrator/language/en-GB/en-GB.plg_fields_checkboxes.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_color.ini', - '/administrator/language/en-GB/en-GB.plg_fields_color.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_editor.ini', - '/administrator/language/en-GB/en-GB.plg_fields_editor.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_image.ini', - '/administrator/language/en-GB/en-GB.plg_fields_image.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_imagelist.ini', - '/administrator/language/en-GB/en-GB.plg_fields_imagelist.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_integer.ini', - '/administrator/language/en-GB/en-GB.plg_fields_integer.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_list.ini', - '/administrator/language/en-GB/en-GB.plg_fields_list.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_media.ini', - '/administrator/language/en-GB/en-GB.plg_fields_media.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_radio.ini', - '/administrator/language/en-GB/en-GB.plg_fields_radio.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_sql.ini', - '/administrator/language/en-GB/en-GB.plg_fields_sql.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_text.ini', - '/administrator/language/en-GB/en-GB.plg_fields_text.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_textarea.ini', - '/administrator/language/en-GB/en-GB.plg_fields_textarea.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_url.ini', - '/administrator/language/en-GB/en-GB.plg_fields_url.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_user.ini', - '/administrator/language/en-GB/en-GB.plg_fields_user.sys.ini', - '/administrator/language/en-GB/en-GB.plg_fields_usergrouplist.ini', - '/administrator/language/en-GB/en-GB.plg_fields_usergrouplist.sys.ini', - '/administrator/language/en-GB/en-GB.plg_finder_categories.ini', - '/administrator/language/en-GB/en-GB.plg_finder_categories.sys.ini', - '/administrator/language/en-GB/en-GB.plg_finder_contacts.ini', - '/administrator/language/en-GB/en-GB.plg_finder_contacts.sys.ini', - '/administrator/language/en-GB/en-GB.plg_finder_content.ini', - '/administrator/language/en-GB/en-GB.plg_finder_content.sys.ini', - '/administrator/language/en-GB/en-GB.plg_finder_newsfeeds.ini', - '/administrator/language/en-GB/en-GB.plg_finder_newsfeeds.sys.ini', - '/administrator/language/en-GB/en-GB.plg_finder_tags.ini', - '/administrator/language/en-GB/en-GB.plg_finder_tags.sys.ini', - '/administrator/language/en-GB/en-GB.plg_finder_weblinks.ini', - '/administrator/language/en-GB/en-GB.plg_finder_weblinks.sys.ini', - '/administrator/language/en-GB/en-GB.plg_installer_folderinstaller.ini', - '/administrator/language/en-GB/en-GB.plg_installer_folderinstaller.sys.ini', - '/administrator/language/en-GB/en-GB.plg_installer_packageinstaller.ini', - '/administrator/language/en-GB/en-GB.plg_installer_packageinstaller.sys.ini', - '/administrator/language/en-GB/en-GB.plg_installer_urlinstaller.ini', - '/administrator/language/en-GB/en-GB.plg_installer_urlinstaller.sys.ini', - '/administrator/language/en-GB/en-GB.plg_installer_webinstaller.ini', - '/administrator/language/en-GB/en-GB.plg_installer_webinstaller.sys.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_actionlogs.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_actionlogs.sys.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_consents.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_consents.sys.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_contact.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_contact.sys.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_content.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_content.sys.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_message.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_message.sys.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_user.ini', - '/administrator/language/en-GB/en-GB.plg_privacy_user.sys.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_extensionupdate.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_extensionupdate.sys.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.sys.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_phpversioncheck.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_phpversioncheck.sys.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_privacycheck.ini', - '/administrator/language/en-GB/en-GB.plg_quickicon_privacycheck.sys.ini', - '/administrator/language/en-GB/en-GB.plg_sampledata_blog.ini', - '/administrator/language/en-GB/en-GB.plg_sampledata_blog.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_actionlogs.ini', - '/administrator/language/en-GB/en-GB.plg_system_actionlogs.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_cache.ini', - '/administrator/language/en-GB/en-GB.plg_system_cache.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_debug.ini', - '/administrator/language/en-GB/en-GB.plg_system_debug.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_fields.ini', - '/administrator/language/en-GB/en-GB.plg_system_fields.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_highlight.ini', - '/administrator/language/en-GB/en-GB.plg_system_highlight.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_languagecode.ini', - '/administrator/language/en-GB/en-GB.plg_system_languagecode.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_languagefilter.ini', - '/administrator/language/en-GB/en-GB.plg_system_languagefilter.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_log.ini', - '/administrator/language/en-GB/en-GB.plg_system_log.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_logout.ini', - '/administrator/language/en-GB/en-GB.plg_system_logout.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_logrotation.ini', - '/administrator/language/en-GB/en-GB.plg_system_logrotation.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_p3p.ini', - '/administrator/language/en-GB/en-GB.plg_system_p3p.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_privacyconsent.ini', - '/administrator/language/en-GB/en-GB.plg_system_privacyconsent.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_redirect.ini', - '/administrator/language/en-GB/en-GB.plg_system_redirect.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_remember.ini', - '/administrator/language/en-GB/en-GB.plg_system_remember.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_sef.ini', - '/administrator/language/en-GB/en-GB.plg_system_sef.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_sessiongc.ini', - '/administrator/language/en-GB/en-GB.plg_system_sessiongc.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_stats.ini', - '/administrator/language/en-GB/en-GB.plg_system_stats.sys.ini', - '/administrator/language/en-GB/en-GB.plg_system_updatenotification.ini', - '/administrator/language/en-GB/en-GB.plg_system_updatenotification.sys.ini', - '/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.ini', - '/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.sys.ini', - '/administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.ini', - '/administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.sys.ini', - '/administrator/language/en-GB/en-GB.plg_user_contactcreator.ini', - '/administrator/language/en-GB/en-GB.plg_user_contactcreator.sys.ini', - '/administrator/language/en-GB/en-GB.plg_user_joomla.ini', - '/administrator/language/en-GB/en-GB.plg_user_joomla.sys.ini', - '/administrator/language/en-GB/en-GB.plg_user_profile.ini', - '/administrator/language/en-GB/en-GB.plg_user_profile.sys.ini', - '/administrator/language/en-GB/en-GB.plg_user_terms.ini', - '/administrator/language/en-GB/en-GB.plg_user_terms.sys.ini', - '/administrator/language/en-GB/en-GB.tpl_hathor.ini', - '/administrator/language/en-GB/en-GB.tpl_hathor.sys.ini', - '/administrator/language/en-GB/en-GB.tpl_isis.ini', - '/administrator/language/en-GB/en-GB.tpl_isis.sys.ini', - '/administrator/language/en-GB/en-GB.xml', - '/administrator/manifests/libraries/fof.xml', - '/administrator/manifests/libraries/idna_convert.xml', - '/administrator/manifests/libraries/phputf8.xml', - '/administrator/modules/mod_feed/helper.php', - '/administrator/modules/mod_latest/helper.php', - '/administrator/modules/mod_latestactions/helper.php', - '/administrator/modules/mod_logged/helper.php', - '/administrator/modules/mod_login/helper.php', - '/administrator/modules/mod_menu/helper.php', - '/administrator/modules/mod_menu/menu.php', - '/administrator/modules/mod_multilangstatus/language/en-GB/en-GB.mod_multilangstatus.ini', - '/administrator/modules/mod_multilangstatus/language/en-GB/en-GB.mod_multilangstatus.sys.ini', - '/administrator/modules/mod_popular/helper.php', - '/administrator/modules/mod_privacy_dashboard/helper.php', - '/administrator/modules/mod_quickicon/helper.php', - '/administrator/modules/mod_quickicon/mod_quickicon.php', - '/administrator/modules/mod_sampledata/helper.php', - '/administrator/modules/mod_stats_admin/helper.php', - '/administrator/modules/mod_stats_admin/language/en-GB.mod_stats_admin.ini', - '/administrator/modules/mod_stats_admin/language/en-GB.mod_stats_admin.sys.ini', - '/administrator/modules/mod_status/mod_status.php', - '/administrator/modules/mod_status/mod_status.xml', - '/administrator/modules/mod_status/tmpl/default.php', - '/administrator/modules/mod_version/helper.php', - '/administrator/modules/mod_version/language/en-GB/en-GB.mod_version.ini', - '/administrator/modules/mod_version/language/en-GB/en-GB.mod_version.sys.ini', - '/administrator/templates/hathor/LICENSE.txt', - '/administrator/templates/hathor/component.php', - '/administrator/templates/hathor/cpanel.php', - '/administrator/templates/hathor/css/boldtext.css', - '/administrator/templates/hathor/css/colour_blue.css', - '/administrator/templates/hathor/css/colour_blue_rtl.css', - '/administrator/templates/hathor/css/colour_brown.css', - '/administrator/templates/hathor/css/colour_brown_rtl.css', - '/administrator/templates/hathor/css/colour_highcontrast.css', - '/administrator/templates/hathor/css/colour_highcontrast_rtl.css', - '/administrator/templates/hathor/css/colour_standard.css', - '/administrator/templates/hathor/css/colour_standard_rtl.css', - '/administrator/templates/hathor/css/error.css', - '/administrator/templates/hathor/css/ie7.css', - '/administrator/templates/hathor/css/ie8.css', - '/administrator/templates/hathor/css/template.css', - '/administrator/templates/hathor/css/template_rtl.css', - '/administrator/templates/hathor/css/theme.css', - '/administrator/templates/hathor/error.php', - '/administrator/templates/hathor/favicon.ico', - '/administrator/templates/hathor/html/com_admin/help/default.php', - '/administrator/templates/hathor/html/com_admin/profile/edit.php', - '/administrator/templates/hathor/html/com_admin/sysinfo/default.php', - '/administrator/templates/hathor/html/com_admin/sysinfo/default_config.php', - '/administrator/templates/hathor/html/com_admin/sysinfo/default_directory.php', - '/administrator/templates/hathor/html/com_admin/sysinfo/default_navigation.php', - '/administrator/templates/hathor/html/com_admin/sysinfo/default_phpsettings.php', - '/administrator/templates/hathor/html/com_admin/sysinfo/default_system.php', - '/administrator/templates/hathor/html/com_associations/associations/default.php', - '/administrator/templates/hathor/html/com_banners/banner/edit.php', - '/administrator/templates/hathor/html/com_banners/banners/default.php', - '/administrator/templates/hathor/html/com_banners/client/edit.php', - '/administrator/templates/hathor/html/com_banners/clients/default.php', - '/administrator/templates/hathor/html/com_banners/download/default.php', - '/administrator/templates/hathor/html/com_banners/tracks/default.php', - '/administrator/templates/hathor/html/com_cache/cache/default.php', - '/administrator/templates/hathor/html/com_cache/purge/default.php', - '/administrator/templates/hathor/html/com_categories/categories/default.php', - '/administrator/templates/hathor/html/com_categories/category/edit.php', - '/administrator/templates/hathor/html/com_categories/category/edit_options.php', - '/administrator/templates/hathor/html/com_checkin/checkin/default.php', - '/administrator/templates/hathor/html/com_config/application/default.php', - '/administrator/templates/hathor/html/com_config/application/default_cache.php', - '/administrator/templates/hathor/html/com_config/application/default_cookie.php', - '/administrator/templates/hathor/html/com_config/application/default_database.php', - '/administrator/templates/hathor/html/com_config/application/default_debug.php', - '/administrator/templates/hathor/html/com_config/application/default_filters.php', - '/administrator/templates/hathor/html/com_config/application/default_ftp.php', - '/administrator/templates/hathor/html/com_config/application/default_ftplogin.php', - '/administrator/templates/hathor/html/com_config/application/default_locale.php', - '/administrator/templates/hathor/html/com_config/application/default_mail.php', - '/administrator/templates/hathor/html/com_config/application/default_metadata.php', - '/administrator/templates/hathor/html/com_config/application/default_navigation.php', - '/administrator/templates/hathor/html/com_config/application/default_permissions.php', - '/administrator/templates/hathor/html/com_config/application/default_seo.php', - '/administrator/templates/hathor/html/com_config/application/default_server.php', - '/administrator/templates/hathor/html/com_config/application/default_session.php', - '/administrator/templates/hathor/html/com_config/application/default_site.php', - '/administrator/templates/hathor/html/com_config/application/default_system.php', - '/administrator/templates/hathor/html/com_config/component/default.php', - '/administrator/templates/hathor/html/com_contact/contact/edit.php', - '/administrator/templates/hathor/html/com_contact/contact/edit_params.php', - '/administrator/templates/hathor/html/com_contact/contacts/default.php', - '/administrator/templates/hathor/html/com_contact/contacts/modal.php', - '/administrator/templates/hathor/html/com_content/article/edit.php', - '/administrator/templates/hathor/html/com_content/articles/default.php', - '/administrator/templates/hathor/html/com_content/articles/modal.php', - '/administrator/templates/hathor/html/com_content/featured/default.php', - '/administrator/templates/hathor/html/com_contenthistory/history/modal.php', - '/administrator/templates/hathor/html/com_cpanel/cpanel/default.php', - '/administrator/templates/hathor/html/com_fields/field/edit.php', - '/administrator/templates/hathor/html/com_fields/fields/default.php', - '/administrator/templates/hathor/html/com_fields/group/edit.php', - '/administrator/templates/hathor/html/com_fields/groups/default.php', - '/administrator/templates/hathor/html/com_finder/filters/default.php', - '/administrator/templates/hathor/html/com_finder/index/default.php', - '/administrator/templates/hathor/html/com_finder/maps/default.php', - '/administrator/templates/hathor/html/com_installer/database/default.php', - '/administrator/templates/hathor/html/com_installer/default/default_ftp.php', - '/administrator/templates/hathor/html/com_installer/discover/default.php', - '/administrator/templates/hathor/html/com_installer/install/default.php', - '/administrator/templates/hathor/html/com_installer/install/default_form.php', - '/administrator/templates/hathor/html/com_installer/languages/default.php', - '/administrator/templates/hathor/html/com_installer/languages/default_filter.php', - '/administrator/templates/hathor/html/com_installer/manage/default.php', - '/administrator/templates/hathor/html/com_installer/manage/default_filter.php', - '/administrator/templates/hathor/html/com_installer/update/default.php', - '/administrator/templates/hathor/html/com_installer/warnings/default.php', - '/administrator/templates/hathor/html/com_joomlaupdate/default/default.php', - '/administrator/templates/hathor/html/com_languages/installed/default.php', - '/administrator/templates/hathor/html/com_languages/installed/default_ftp.php', - '/administrator/templates/hathor/html/com_languages/languages/default.php', - '/administrator/templates/hathor/html/com_languages/overrides/default.php', - '/administrator/templates/hathor/html/com_menus/item/edit.php', - '/administrator/templates/hathor/html/com_menus/item/edit_options.php', - '/administrator/templates/hathor/html/com_menus/items/default.php', - '/administrator/templates/hathor/html/com_menus/menu/edit.php', - '/administrator/templates/hathor/html/com_menus/menus/default.php', - '/administrator/templates/hathor/html/com_menus/menutypes/default.php', - '/administrator/templates/hathor/html/com_messages/message/edit.php', - '/administrator/templates/hathor/html/com_messages/messages/default.php', - '/administrator/templates/hathor/html/com_modules/module/edit.php', - '/administrator/templates/hathor/html/com_modules/module/edit_assignment.php', - '/administrator/templates/hathor/html/com_modules/module/edit_options.php', - '/administrator/templates/hathor/html/com_modules/modules/default.php', - '/administrator/templates/hathor/html/com_modules/positions/modal.php', - '/administrator/templates/hathor/html/com_newsfeeds/newsfeed/edit.php', - '/administrator/templates/hathor/html/com_newsfeeds/newsfeed/edit_params.php', - '/administrator/templates/hathor/html/com_newsfeeds/newsfeeds/default.php', - '/administrator/templates/hathor/html/com_newsfeeds/newsfeeds/modal.php', - '/administrator/templates/hathor/html/com_plugins/plugin/edit.php', - '/administrator/templates/hathor/html/com_plugins/plugin/edit_options.php', - '/administrator/templates/hathor/html/com_plugins/plugins/default.php', - '/administrator/templates/hathor/html/com_postinstall/messages/default.php', - '/administrator/templates/hathor/html/com_redirect/links/default.php', - '/administrator/templates/hathor/html/com_search/searches/default.php', - '/administrator/templates/hathor/html/com_tags/tag/edit.php', - '/administrator/templates/hathor/html/com_tags/tag/edit_metadata.php', - '/administrator/templates/hathor/html/com_tags/tag/edit_options.php', - '/administrator/templates/hathor/html/com_tags/tags/default.php', - '/administrator/templates/hathor/html/com_templates/style/edit.php', - '/administrator/templates/hathor/html/com_templates/style/edit_assignment.php', - '/administrator/templates/hathor/html/com_templates/style/edit_options.php', - '/administrator/templates/hathor/html/com_templates/styles/default.php', - '/administrator/templates/hathor/html/com_templates/template/default.php', - '/administrator/templates/hathor/html/com_templates/template/default_description.php', - '/administrator/templates/hathor/html/com_templates/template/default_folders.php', - '/administrator/templates/hathor/html/com_templates/template/default_tree.php', - '/administrator/templates/hathor/html/com_templates/templates/default.php', - '/administrator/templates/hathor/html/com_users/debuggroup/default.php', - '/administrator/templates/hathor/html/com_users/debuguser/default.php', - '/administrator/templates/hathor/html/com_users/groups/default.php', - '/administrator/templates/hathor/html/com_users/levels/default.php', - '/administrator/templates/hathor/html/com_users/note/edit.php', - '/administrator/templates/hathor/html/com_users/notes/default.php', - '/administrator/templates/hathor/html/com_users/user/edit.php', - '/administrator/templates/hathor/html/com_users/users/default.php', - '/administrator/templates/hathor/html/com_users/users/modal.php', - '/administrator/templates/hathor/html/com_weblinks/weblink/edit.php', - '/administrator/templates/hathor/html/com_weblinks/weblink/edit_params.php', - '/administrator/templates/hathor/html/com_weblinks/weblinks/default.php', - '/administrator/templates/hathor/html/layouts/com_media/toolbar/deletemedia.php', - '/administrator/templates/hathor/html/layouts/com_media/toolbar/newfolder.php', - '/administrator/templates/hathor/html/layouts/com_media/toolbar/uploadmedia.php', - '/administrator/templates/hathor/html/layouts/com_messages/toolbar/mysettings.php', - '/administrator/templates/hathor/html/layouts/com_modules/toolbar/cancelselect.php', - '/administrator/templates/hathor/html/layouts/com_modules/toolbar/newmodule.php', - '/administrator/templates/hathor/html/layouts/joomla/edit/details.php', - '/administrator/templates/hathor/html/layouts/joomla/edit/fieldset.php', - '/administrator/templates/hathor/html/layouts/joomla/edit/global.php', - '/administrator/templates/hathor/html/layouts/joomla/edit/metadata.php', - '/administrator/templates/hathor/html/layouts/joomla/edit/params.php', - '/administrator/templates/hathor/html/layouts/joomla/quickicons/icon.php', - '/administrator/templates/hathor/html/layouts/joomla/sidebars/submenu.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/base.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/batch.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/confirm.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/containerclose.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/containeropen.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/help.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/iconclass.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/link.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/modal.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/popup.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/separator.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/slider.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/standard.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/title.php', - '/administrator/templates/hathor/html/layouts/joomla/toolbar/versions.php', - '/administrator/templates/hathor/html/layouts/plugins/user/profile/fields/dob.php', - '/administrator/templates/hathor/html/mod_login/default.php', - '/administrator/templates/hathor/html/mod_quickicon/default.php', - '/administrator/templates/hathor/html/modules.php', - '/administrator/templates/hathor/html/pagination.php', - '/administrator/templates/hathor/images/admin/blank.png', - '/administrator/templates/hathor/images/admin/checked_out.png', - '/administrator/templates/hathor/images/admin/collapseall.png', - '/administrator/templates/hathor/images/admin/disabled.png', - '/administrator/templates/hathor/images/admin/downarrow-1.png', - '/administrator/templates/hathor/images/admin/downarrow.png', - '/administrator/templates/hathor/images/admin/downarrow0.png', - '/administrator/templates/hathor/images/admin/expandall.png', - '/administrator/templates/hathor/images/admin/featured.png', - '/administrator/templates/hathor/images/admin/filesave.png', - '/administrator/templates/hathor/images/admin/filter_16.png', - '/administrator/templates/hathor/images/admin/icon-16-allow.png', - '/administrator/templates/hathor/images/admin/icon-16-allowinactive.png', - '/administrator/templates/hathor/images/admin/icon-16-deny.png', - '/administrator/templates/hathor/images/admin/icon-16-denyinactive.png', - '/administrator/templates/hathor/images/admin/icon-16-links.png', - '/administrator/templates/hathor/images/admin/icon-16-notice-note.png', - '/administrator/templates/hathor/images/admin/icon-16-protected.png', - '/administrator/templates/hathor/images/admin/menu_divider.png', - '/administrator/templates/hathor/images/admin/note_add_16.png', - '/administrator/templates/hathor/images/admin/publish_g.png', - '/administrator/templates/hathor/images/admin/publish_r.png', - '/administrator/templates/hathor/images/admin/publish_x.png', - '/administrator/templates/hathor/images/admin/publish_y.png', - '/administrator/templates/hathor/images/admin/sort_asc.png', - '/administrator/templates/hathor/images/admin/sort_desc.png', - '/administrator/templates/hathor/images/admin/tick.png', - '/administrator/templates/hathor/images/admin/trash.png', - '/administrator/templates/hathor/images/admin/uparrow-1.png', - '/administrator/templates/hathor/images/admin/uparrow.png', - '/administrator/templates/hathor/images/admin/uparrow0.png', - '/administrator/templates/hathor/images/arrow.png', - '/administrator/templates/hathor/images/bg-menu.gif', - '/administrator/templates/hathor/images/calendar.png', - '/administrator/templates/hathor/images/header/icon-48-alert.png', - '/administrator/templates/hathor/images/header/icon-48-apply.png', - '/administrator/templates/hathor/images/header/icon-48-archive.png', - '/administrator/templates/hathor/images/header/icon-48-article-add.png', - '/administrator/templates/hathor/images/header/icon-48-article-edit.png', - '/administrator/templates/hathor/images/header/icon-48-article.png', - '/administrator/templates/hathor/images/header/icon-48-assoc.png', - '/administrator/templates/hathor/images/header/icon-48-banner-categories.png', - '/administrator/templates/hathor/images/header/icon-48-banner-client.png', - '/administrator/templates/hathor/images/header/icon-48-banner-tracks.png', - '/administrator/templates/hathor/images/header/icon-48-banner.png', - '/administrator/templates/hathor/images/header/icon-48-calendar.png', - '/administrator/templates/hathor/images/header/icon-48-category-add.png', - '/administrator/templates/hathor/images/header/icon-48-category.png', - '/administrator/templates/hathor/images/header/icon-48-checkin.png', - '/administrator/templates/hathor/images/header/icon-48-clear.png', - '/administrator/templates/hathor/images/header/icon-48-component.png', - '/administrator/templates/hathor/images/header/icon-48-config.png', - '/administrator/templates/hathor/images/header/icon-48-contacts-categories.png', - '/administrator/templates/hathor/images/header/icon-48-contacts.png', - '/administrator/templates/hathor/images/header/icon-48-content.png', - '/administrator/templates/hathor/images/header/icon-48-cpanel.png', - '/administrator/templates/hathor/images/header/icon-48-default.png', - '/administrator/templates/hathor/images/header/icon-48-deny.png', - '/administrator/templates/hathor/images/header/icon-48-download.png', - '/administrator/templates/hathor/images/header/icon-48-edit.png', - '/administrator/templates/hathor/images/header/icon-48-extension.png', - '/administrator/templates/hathor/images/header/icon-48-featured.png', - '/administrator/templates/hathor/images/header/icon-48-frontpage.png', - '/administrator/templates/hathor/images/header/icon-48-generic.png', - '/administrator/templates/hathor/images/header/icon-48-groups-add.png', - '/administrator/templates/hathor/images/header/icon-48-groups.png', - '/administrator/templates/hathor/images/header/icon-48-help-forum.png', - '/administrator/templates/hathor/images/header/icon-48-help-this.png', - '/administrator/templates/hathor/images/header/icon-48-help_header.png', - '/administrator/templates/hathor/images/header/icon-48-inbox.png', - '/administrator/templates/hathor/images/header/icon-48-info.png', - '/administrator/templates/hathor/images/header/icon-48-install.png', - '/administrator/templates/hathor/images/header/icon-48-jupdate-updatefound.png', - '/administrator/templates/hathor/images/header/icon-48-jupdate-uptodate.png', - '/administrator/templates/hathor/images/header/icon-48-language.png', - '/administrator/templates/hathor/images/header/icon-48-levels-add.png', - '/administrator/templates/hathor/images/header/icon-48-levels.png', - '/administrator/templates/hathor/images/header/icon-48-links-cat.png', - '/administrator/templates/hathor/images/header/icon-48-links.png', - '/administrator/templates/hathor/images/header/icon-48-massmail.png', - '/administrator/templates/hathor/images/header/icon-48-media.png', - '/administrator/templates/hathor/images/header/icon-48-menu-add.png', - '/administrator/templates/hathor/images/header/icon-48-menu.png', - '/administrator/templates/hathor/images/header/icon-48-menumgr.png', - '/administrator/templates/hathor/images/header/icon-48-module.png', - '/administrator/templates/hathor/images/header/icon-48-move.png', - '/administrator/templates/hathor/images/header/icon-48-new-privatemessage.png', - '/administrator/templates/hathor/images/header/icon-48-newcategory.png', - '/administrator/templates/hathor/images/header/icon-48-newsfeeds-cat.png', - '/administrator/templates/hathor/images/header/icon-48-newsfeeds.png', - '/administrator/templates/hathor/images/header/icon-48-notice.png', - '/administrator/templates/hathor/images/header/icon-48-plugin.png', - '/administrator/templates/hathor/images/header/icon-48-preview.png', - '/administrator/templates/hathor/images/header/icon-48-print.png', - '/administrator/templates/hathor/images/header/icon-48-purge.png', - '/administrator/templates/hathor/images/header/icon-48-puzzle.png', - '/administrator/templates/hathor/images/header/icon-48-read-privatemessage.png', - '/administrator/templates/hathor/images/header/icon-48-readmess.png', - '/administrator/templates/hathor/images/header/icon-48-redirect.png', - '/administrator/templates/hathor/images/header/icon-48-revert.png', - '/administrator/templates/hathor/images/header/icon-48-search.png', - '/administrator/templates/hathor/images/header/icon-48-section.png', - '/administrator/templates/hathor/images/header/icon-48-send.png', - '/administrator/templates/hathor/images/header/icon-48-static.png', - '/administrator/templates/hathor/images/header/icon-48-stats.png', - '/administrator/templates/hathor/images/header/icon-48-tags.png', - '/administrator/templates/hathor/images/header/icon-48-themes.png', - '/administrator/templates/hathor/images/header/icon-48-trash.png', - '/administrator/templates/hathor/images/header/icon-48-unarchive.png', - '/administrator/templates/hathor/images/header/icon-48-upload.png', - '/administrator/templates/hathor/images/header/icon-48-user-add.png', - '/administrator/templates/hathor/images/header/icon-48-user-edit.png', - '/administrator/templates/hathor/images/header/icon-48-user-profile.png', - '/administrator/templates/hathor/images/header/icon-48-user.png', - '/administrator/templates/hathor/images/header/icon-48-writemess.png', - '/administrator/templates/hathor/images/header/icon-messaging.png', - '/administrator/templates/hathor/images/j_arrow.png', - '/administrator/templates/hathor/images/j_arrow_down.png', - '/administrator/templates/hathor/images/j_arrow_left.png', - '/administrator/templates/hathor/images/j_arrow_right.png', - '/administrator/templates/hathor/images/j_login_lock.png', - '/administrator/templates/hathor/images/j_logo.png', - '/administrator/templates/hathor/images/logo.png', - '/administrator/templates/hathor/images/menu/icon-16-alert.png', - '/administrator/templates/hathor/images/menu/icon-16-apply.png', - '/administrator/templates/hathor/images/menu/icon-16-archive.png', - '/administrator/templates/hathor/images/menu/icon-16-article.png', - '/administrator/templates/hathor/images/menu/icon-16-assoc.png', - '/administrator/templates/hathor/images/menu/icon-16-back-user.png', - '/administrator/templates/hathor/images/menu/icon-16-banner-categories.png', - '/administrator/templates/hathor/images/menu/icon-16-banner-client.png', - '/administrator/templates/hathor/images/menu/icon-16-banner-tracks.png', - '/administrator/templates/hathor/images/menu/icon-16-banner.png', - '/administrator/templates/hathor/images/menu/icon-16-calendar.png', - '/administrator/templates/hathor/images/menu/icon-16-category.png', - '/administrator/templates/hathor/images/menu/icon-16-checkin.png', - '/administrator/templates/hathor/images/menu/icon-16-clear.png', - '/administrator/templates/hathor/images/menu/icon-16-component.png', - '/administrator/templates/hathor/images/menu/icon-16-config.png', - '/administrator/templates/hathor/images/menu/icon-16-contacts-categories.png', - '/administrator/templates/hathor/images/menu/icon-16-contacts.png', - '/administrator/templates/hathor/images/menu/icon-16-content.png', - '/administrator/templates/hathor/images/menu/icon-16-cpanel.png', - '/administrator/templates/hathor/images/menu/icon-16-default.png', - '/administrator/templates/hathor/images/menu/icon-16-delete.png', - '/administrator/templates/hathor/images/menu/icon-16-deny.png', - '/administrator/templates/hathor/images/menu/icon-16-download.png', - '/administrator/templates/hathor/images/menu/icon-16-edit.png', - '/administrator/templates/hathor/images/menu/icon-16-featured.png', - '/administrator/templates/hathor/images/menu/icon-16-frontpage.png', - '/administrator/templates/hathor/images/menu/icon-16-generic.png', - '/administrator/templates/hathor/images/menu/icon-16-groups.png', - '/administrator/templates/hathor/images/menu/icon-16-help-community.png', - '/administrator/templates/hathor/images/menu/icon-16-help-dev.png', - '/administrator/templates/hathor/images/menu/icon-16-help-docs.png', - '/administrator/templates/hathor/images/menu/icon-16-help-forum.png', - '/administrator/templates/hathor/images/menu/icon-16-help-jed.png', - '/administrator/templates/hathor/images/menu/icon-16-help-jrd.png', - '/administrator/templates/hathor/images/menu/icon-16-help-security.png', - '/administrator/templates/hathor/images/menu/icon-16-help-shop.png', - '/administrator/templates/hathor/images/menu/icon-16-help-this.png', - '/administrator/templates/hathor/images/menu/icon-16-help-trans.png', - '/administrator/templates/hathor/images/menu/icon-16-help.png', - '/administrator/templates/hathor/images/menu/icon-16-inbox.png', - '/administrator/templates/hathor/images/menu/icon-16-info.png', - '/administrator/templates/hathor/images/menu/icon-16-install.png', - '/administrator/templates/hathor/images/menu/icon-16-language.png', - '/administrator/templates/hathor/images/menu/icon-16-levels.png', - '/administrator/templates/hathor/images/menu/icon-16-links-cat.png', - '/administrator/templates/hathor/images/menu/icon-16-links.png', - '/administrator/templates/hathor/images/menu/icon-16-logout.png', - '/administrator/templates/hathor/images/menu/icon-16-maintenance.png', - '/administrator/templates/hathor/images/menu/icon-16-massmail.png', - '/administrator/templates/hathor/images/menu/icon-16-media.png', - '/administrator/templates/hathor/images/menu/icon-16-menu.png', - '/administrator/templates/hathor/images/menu/icon-16-menumgr.png', - '/administrator/templates/hathor/images/menu/icon-16-messages.png', - '/administrator/templates/hathor/images/menu/icon-16-messaging.png', - '/administrator/templates/hathor/images/menu/icon-16-module.png', - '/administrator/templates/hathor/images/menu/icon-16-move.png', - '/administrator/templates/hathor/images/menu/icon-16-new-privatemessage.png', - '/administrator/templates/hathor/images/menu/icon-16-new.png', - '/administrator/templates/hathor/images/menu/icon-16-newarticle.png', - '/administrator/templates/hathor/images/menu/icon-16-newcategory.png', - '/administrator/templates/hathor/images/menu/icon-16-newgroup.png', - '/administrator/templates/hathor/images/menu/icon-16-newlevel.png', - '/administrator/templates/hathor/images/menu/icon-16-newsfeeds-cat.png', - '/administrator/templates/hathor/images/menu/icon-16-newsfeeds.png', - '/administrator/templates/hathor/images/menu/icon-16-newuser.png', - '/administrator/templates/hathor/images/menu/icon-16-nopreview.png', - '/administrator/templates/hathor/images/menu/icon-16-notdefault.png', - '/administrator/templates/hathor/images/menu/icon-16-notice.png', - '/administrator/templates/hathor/images/menu/icon-16-plugin.png', - '/administrator/templates/hathor/images/menu/icon-16-preview.png', - '/administrator/templates/hathor/images/menu/icon-16-print.png', - '/administrator/templates/hathor/images/menu/icon-16-purge.png', - '/administrator/templates/hathor/images/menu/icon-16-puzzle.png', - '/administrator/templates/hathor/images/menu/icon-16-read-privatemessage.png', - '/administrator/templates/hathor/images/menu/icon-16-readmess.png', - '/administrator/templates/hathor/images/menu/icon-16-redirect.png', - '/administrator/templates/hathor/images/menu/icon-16-revert.png', - '/administrator/templates/hathor/images/menu/icon-16-search.png', - '/administrator/templates/hathor/images/menu/icon-16-send.png', - '/administrator/templates/hathor/images/menu/icon-16-stats.png', - '/administrator/templates/hathor/images/menu/icon-16-tags.png', - '/administrator/templates/hathor/images/menu/icon-16-themes.png', - '/administrator/templates/hathor/images/menu/icon-16-trash.png', - '/administrator/templates/hathor/images/menu/icon-16-unarticle.png', - '/administrator/templates/hathor/images/menu/icon-16-upload.png', - '/administrator/templates/hathor/images/menu/icon-16-user-dd.png', - '/administrator/templates/hathor/images/menu/icon-16-user-note.png', - '/administrator/templates/hathor/images/menu/icon-16-user.png', - '/administrator/templates/hathor/images/menu/icon-16-viewsite.png', - '/administrator/templates/hathor/images/menu/icon-16-writemess.png', - '/administrator/templates/hathor/images/mini_icon.png', - '/administrator/templates/hathor/images/notice-alert.png', - '/administrator/templates/hathor/images/notice-info.png', - '/administrator/templates/hathor/images/notice-note.png', - '/administrator/templates/hathor/images/required.png', - '/administrator/templates/hathor/images/selector-arrow-hc.png', - '/administrator/templates/hathor/images/selector-arrow-rtl.png', - '/administrator/templates/hathor/images/selector-arrow-std.png', - '/administrator/templates/hathor/images/selector-arrow.png', - '/administrator/templates/hathor/images/system/calendar.png', - '/administrator/templates/hathor/images/system/selector-arrow.png', - '/administrator/templates/hathor/images/toolbar/icon-32-adduser.png', - '/administrator/templates/hathor/images/toolbar/icon-32-alert.png', - '/administrator/templates/hathor/images/toolbar/icon-32-apply.png', - '/administrator/templates/hathor/images/toolbar/icon-32-archive.png', - '/administrator/templates/hathor/images/toolbar/icon-32-article-add.png', - '/administrator/templates/hathor/images/toolbar/icon-32-article.png', - '/administrator/templates/hathor/images/toolbar/icon-32-back.png', - '/administrator/templates/hathor/images/toolbar/icon-32-banner-categories.png', - '/administrator/templates/hathor/images/toolbar/icon-32-banner-client.png', - '/administrator/templates/hathor/images/toolbar/icon-32-banner-tracks.png', - '/administrator/templates/hathor/images/toolbar/icon-32-banner.png', - '/administrator/templates/hathor/images/toolbar/icon-32-batch.png', - '/administrator/templates/hathor/images/toolbar/icon-32-calendar.png', - '/administrator/templates/hathor/images/toolbar/icon-32-cancel.png', - '/administrator/templates/hathor/images/toolbar/icon-32-checkin.png', - '/administrator/templates/hathor/images/toolbar/icon-32-cog.png', - '/administrator/templates/hathor/images/toolbar/icon-32-component.png', - '/administrator/templates/hathor/images/toolbar/icon-32-config.png', - '/administrator/templates/hathor/images/toolbar/icon-32-contacts-categories.png', - '/administrator/templates/hathor/images/toolbar/icon-32-contacts.png', - '/administrator/templates/hathor/images/toolbar/icon-32-copy.png', - '/administrator/templates/hathor/images/toolbar/icon-32-css.png', - '/administrator/templates/hathor/images/toolbar/icon-32-default.png', - '/administrator/templates/hathor/images/toolbar/icon-32-delete-style.png', - '/administrator/templates/hathor/images/toolbar/icon-32-delete.png', - '/administrator/templates/hathor/images/toolbar/icon-32-deny.png', - '/administrator/templates/hathor/images/toolbar/icon-32-download.png', - '/administrator/templates/hathor/images/toolbar/icon-32-edit.png', - '/administrator/templates/hathor/images/toolbar/icon-32-error.png', - '/administrator/templates/hathor/images/toolbar/icon-32-export.png', - '/administrator/templates/hathor/images/toolbar/icon-32-extension.png', - '/administrator/templates/hathor/images/toolbar/icon-32-featured.png', - '/administrator/templates/hathor/images/toolbar/icon-32-forward.png', - '/administrator/templates/hathor/images/toolbar/icon-32-help.png', - '/administrator/templates/hathor/images/toolbar/icon-32-html.png', - '/administrator/templates/hathor/images/toolbar/icon-32-inbox.png', - '/administrator/templates/hathor/images/toolbar/icon-32-info.png', - '/administrator/templates/hathor/images/toolbar/icon-32-links.png', - '/administrator/templates/hathor/images/toolbar/icon-32-lock.png', - '/administrator/templates/hathor/images/toolbar/icon-32-menu.png', - '/administrator/templates/hathor/images/toolbar/icon-32-messaging.png', - '/administrator/templates/hathor/images/toolbar/icon-32-messanging.png', - '/administrator/templates/hathor/images/toolbar/icon-32-module.png', - '/administrator/templates/hathor/images/toolbar/icon-32-move.png', - '/administrator/templates/hathor/images/toolbar/icon-32-new-privatemessage.png', - '/administrator/templates/hathor/images/toolbar/icon-32-new-style.png', - '/administrator/templates/hathor/images/toolbar/icon-32-new.png', - '/administrator/templates/hathor/images/toolbar/icon-32-notice.png', - '/administrator/templates/hathor/images/toolbar/icon-32-preview.png', - '/administrator/templates/hathor/images/toolbar/icon-32-print.png', - '/administrator/templates/hathor/images/toolbar/icon-32-publish.png', - '/administrator/templates/hathor/images/toolbar/icon-32-purge.png', - '/administrator/templates/hathor/images/toolbar/icon-32-read-privatemessage.png', - '/administrator/templates/hathor/images/toolbar/icon-32-refresh.png', - '/administrator/templates/hathor/images/toolbar/icon-32-remove.png', - '/administrator/templates/hathor/images/toolbar/icon-32-revert.png', - '/administrator/templates/hathor/images/toolbar/icon-32-save-copy.png', - '/administrator/templates/hathor/images/toolbar/icon-32-save-new.png', - '/administrator/templates/hathor/images/toolbar/icon-32-save.png', - '/administrator/templates/hathor/images/toolbar/icon-32-search.png', - '/administrator/templates/hathor/images/toolbar/icon-32-send.png', - '/administrator/templates/hathor/images/toolbar/icon-32-stats.png', - '/administrator/templates/hathor/images/toolbar/icon-32-trash.png', - '/administrator/templates/hathor/images/toolbar/icon-32-unarchive.png', - '/administrator/templates/hathor/images/toolbar/icon-32-unblock.png', - '/administrator/templates/hathor/images/toolbar/icon-32-unpublish.png', - '/administrator/templates/hathor/images/toolbar/icon-32-upload.png', - '/administrator/templates/hathor/images/toolbar/icon-32-user-add.png', - '/administrator/templates/hathor/images/toolbar/icon-32-xml.png', - '/administrator/templates/hathor/index.php', - '/administrator/templates/hathor/js/template.js', - '/administrator/templates/hathor/language/en-GB/en-GB.tpl_hathor.ini', - '/administrator/templates/hathor/language/en-GB/en-GB.tpl_hathor.sys.ini', - '/administrator/templates/hathor/less/buttons.less', - '/administrator/templates/hathor/less/colour_baseline.less', - '/administrator/templates/hathor/less/colour_blue.less', - '/administrator/templates/hathor/less/colour_brown.less', - '/administrator/templates/hathor/less/colour_standard.less', - '/administrator/templates/hathor/less/forms.less', - '/administrator/templates/hathor/less/hathor_variables.less', - '/administrator/templates/hathor/less/icomoon.less', - '/administrator/templates/hathor/less/modals.less', - '/administrator/templates/hathor/less/template.less', - '/administrator/templates/hathor/less/variables.less', - '/administrator/templates/hathor/login.php', - '/administrator/templates/hathor/postinstall/hathormessage.php', - '/administrator/templates/hathor/templateDetails.xml', - '/administrator/templates/hathor/template_preview.png', - '/administrator/templates/hathor/template_thumbnail.png', - '/administrator/templates/isis/component.php', - '/administrator/templates/isis/cpanel.php', - '/administrator/templates/isis/css/template-rtl.css', - '/administrator/templates/isis/css/template.css', - '/administrator/templates/isis/error.php', - '/administrator/templates/isis/favicon.ico', - '/administrator/templates/isis/html/com_media/imageslist/default_folder.php', - '/administrator/templates/isis/html/com_media/imageslist/default_image.php', - '/administrator/templates/isis/html/com_media/medialist/thumbs_folders.php', - '/administrator/templates/isis/html/com_media/medialist/thumbs_imgs.php', - '/administrator/templates/isis/html/editor_content.css', - '/administrator/templates/isis/html/layouts/joomla/form/field/media.php', - '/administrator/templates/isis/html/layouts/joomla/form/field/user.php', - '/administrator/templates/isis/html/layouts/joomla/pagination/link.php', - '/administrator/templates/isis/html/layouts/joomla/pagination/links.php', - '/administrator/templates/isis/html/layouts/joomla/system/message.php', - '/administrator/templates/isis/html/layouts/joomla/toolbar/versions.php', - '/administrator/templates/isis/html/mod_version/default.php', - '/administrator/templates/isis/html/modules.php', - '/administrator/templates/isis/html/pagination.php', - '/administrator/templates/isis/images/admin/blank.png', - '/administrator/templates/isis/images/admin/checked_out.png', - '/administrator/templates/isis/images/admin/collapseall.png', - '/administrator/templates/isis/images/admin/disabled.png', - '/administrator/templates/isis/images/admin/downarrow-1.png', - '/administrator/templates/isis/images/admin/downarrow.png', - '/administrator/templates/isis/images/admin/downarrow0.png', - '/administrator/templates/isis/images/admin/expandall.png', - '/administrator/templates/isis/images/admin/featured.png', - '/administrator/templates/isis/images/admin/filesave.png', - '/administrator/templates/isis/images/admin/filter_16.png', - '/administrator/templates/isis/images/admin/icon-16-add.png', - '/administrator/templates/isis/images/admin/icon-16-allow.png', - '/administrator/templates/isis/images/admin/icon-16-allowinactive.png', - '/administrator/templates/isis/images/admin/icon-16-deny.png', - '/administrator/templates/isis/images/admin/icon-16-denyinactive.png', - '/administrator/templates/isis/images/admin/icon-16-links.png', - '/administrator/templates/isis/images/admin/icon-16-notice-note.png', - '/administrator/templates/isis/images/admin/icon-16-protected.png', - '/administrator/templates/isis/images/admin/menu_divider.png', - '/administrator/templates/isis/images/admin/note_add_16.png', - '/administrator/templates/isis/images/admin/publish_g.png', - '/administrator/templates/isis/images/admin/publish_r.png', - '/administrator/templates/isis/images/admin/publish_x.png', - '/administrator/templates/isis/images/admin/publish_y.png', - '/administrator/templates/isis/images/admin/sort_asc.png', - '/administrator/templates/isis/images/admin/sort_desc.png', - '/administrator/templates/isis/images/admin/tick.png', - '/administrator/templates/isis/images/admin/trash.png', - '/administrator/templates/isis/images/admin/uparrow-1.png', - '/administrator/templates/isis/images/admin/uparrow.png', - '/administrator/templates/isis/images/admin/uparrow0.png', - '/administrator/templates/isis/images/emailButton.png', - '/administrator/templates/isis/images/joomla.png', - '/administrator/templates/isis/images/login-joomla-inverse.png', - '/administrator/templates/isis/images/login-joomla.png', - '/administrator/templates/isis/images/logo-inverse.png', - '/administrator/templates/isis/images/logo.png', - '/administrator/templates/isis/images/pdf_button.png', - '/administrator/templates/isis/images/printButton.png', - '/administrator/templates/isis/images/system/sort_asc.png', - '/administrator/templates/isis/images/system/sort_desc.png', - '/administrator/templates/isis/img/glyphicons-halflings-white.png', - '/administrator/templates/isis/img/glyphicons-halflings.png', - '/administrator/templates/isis/index.php', - '/administrator/templates/isis/js/application.js', - '/administrator/templates/isis/js/classes.js', - '/administrator/templates/isis/js/template.js', - '/administrator/templates/isis/language/en-GB/en-GB.tpl_isis.ini', - '/administrator/templates/isis/language/en-GB/en-GB.tpl_isis.sys.ini', - '/administrator/templates/isis/less/blocks/_chzn-override.less', - '/administrator/templates/isis/less/blocks/_custom.less', - '/administrator/templates/isis/less/blocks/_editors.less', - '/administrator/templates/isis/less/blocks/_forms.less', - '/administrator/templates/isis/less/blocks/_global.less', - '/administrator/templates/isis/less/blocks/_header.less', - '/administrator/templates/isis/less/blocks/_login.less', - '/administrator/templates/isis/less/blocks/_media.less', - '/administrator/templates/isis/less/blocks/_modals.less', - '/administrator/templates/isis/less/blocks/_navbar.less', - '/administrator/templates/isis/less/blocks/_quickicons.less', - '/administrator/templates/isis/less/blocks/_sidebar.less', - '/administrator/templates/isis/less/blocks/_status.less', - '/administrator/templates/isis/less/blocks/_tables.less', - '/administrator/templates/isis/less/blocks/_toolbar.less', - '/administrator/templates/isis/less/blocks/_treeselect.less', - '/administrator/templates/isis/less/blocks/_utility-classes.less', - '/administrator/templates/isis/less/bootstrap/button-groups.less', - '/administrator/templates/isis/less/bootstrap/buttons.less', - '/administrator/templates/isis/less/bootstrap/mixins.less', - '/administrator/templates/isis/less/bootstrap/responsive-1200px-min.less', - '/administrator/templates/isis/less/bootstrap/responsive-768px-979px.less', - '/administrator/templates/isis/less/bootstrap/wells.less', - '/administrator/templates/isis/less/icomoon.less', - '/administrator/templates/isis/less/pages/_com_cpanel.less', - '/administrator/templates/isis/less/pages/_com_postinstall.less', - '/administrator/templates/isis/less/pages/_com_privacy.less', - '/administrator/templates/isis/less/pages/_com_templates.less', - '/administrator/templates/isis/less/template-rtl.less', - '/administrator/templates/isis/less/template.less', - '/administrator/templates/isis/less/variables.less', - '/administrator/templates/isis/login.php', - '/administrator/templates/isis/templateDetails.xml', - '/administrator/templates/isis/template_preview.png', - '/administrator/templates/isis/template_thumbnail.png', - '/administrator/templates/system/html/modules.php', - '/bin/index.html', - '/bin/keychain.php', - '/cli/deletefiles.php', - '/cli/finder_indexer.php', - '/cli/garbagecron.php', - '/cli/sessionGc.php', - '/cli/sessionMetadataGc.php', - '/cli/update_cron.php', - '/components/com_banners/banners.php', - '/components/com_banners/controller.php', - '/components/com_banners/helpers/banner.php', - '/components/com_banners/helpers/category.php', - '/components/com_banners/models/banner.php', - '/components/com_banners/models/banners.php', - '/components/com_banners/router.php', - '/components/com_config/config.php', - '/components/com_config/controller/cancel.php', - '/components/com_config/controller/canceladmin.php', - '/components/com_config/controller/cmsbase.php', - '/components/com_config/controller/config/display.php', - '/components/com_config/controller/config/save.php', - '/components/com_config/controller/display.php', - '/components/com_config/controller/helper.php', - '/components/com_config/controller/modules/cancel.php', - '/components/com_config/controller/modules/display.php', - '/components/com_config/controller/modules/save.php', - '/components/com_config/controller/templates/display.php', - '/components/com_config/controller/templates/save.php', - '/components/com_config/model/cms.php', - '/components/com_config/model/config.php', - '/components/com_config/model/form.php', - '/components/com_config/model/form/config.xml', - '/components/com_config/model/form/modules.xml', - '/components/com_config/model/form/modules_advanced.xml', - '/components/com_config/model/form/templates.xml', - '/components/com_config/model/modules.php', - '/components/com_config/model/templates.php', - '/components/com_config/view/cms/html.php', - '/components/com_config/view/cms/json.php', - '/components/com_config/view/config/html.php', - '/components/com_config/view/config/tmpl/default.php', - '/components/com_config/view/config/tmpl/default.xml', - '/components/com_config/view/config/tmpl/default_metadata.php', - '/components/com_config/view/config/tmpl/default_seo.php', - '/components/com_config/view/config/tmpl/default_site.php', - '/components/com_config/view/modules/html.php', - '/components/com_config/view/modules/tmpl/default.php', - '/components/com_config/view/modules/tmpl/default_options.php', - '/components/com_config/view/modules/tmpl/default_positions.php', - '/components/com_config/view/templates/html.php', - '/components/com_config/view/templates/tmpl/default.php', - '/components/com_config/view/templates/tmpl/default.xml', - '/components/com_config/view/templates/tmpl/default_options.php', - '/components/com_contact/contact.php', - '/components/com_contact/controller.php', - '/components/com_contact/controllers/contact.php', - '/components/com_contact/helpers/association.php', - '/components/com_contact/helpers/category.php', - '/components/com_contact/helpers/legacyrouter.php', - '/components/com_contact/layouts/joomla/form/renderfield.php', - '/components/com_contact/models/categories.php', - '/components/com_contact/models/category.php', - '/components/com_contact/models/contact.php', - '/components/com_contact/models/featured.php', - '/components/com_contact/models/forms/contact.xml', - '/components/com_contact/models/forms/filter_contacts.xml', - '/components/com_contact/models/forms/form.xml', - '/components/com_contact/models/rules/contactemail.php', - '/components/com_contact/models/rules/contactemailmessage.php', - '/components/com_contact/models/rules/contactemailsubject.php', - '/components/com_contact/router.php', - '/components/com_contact/views/categories/tmpl/default.php', - '/components/com_contact/views/categories/tmpl/default.xml', - '/components/com_contact/views/categories/tmpl/default_items.php', - '/components/com_contact/views/categories/view.html.php', - '/components/com_contact/views/category/tmpl/default.php', - '/components/com_contact/views/category/tmpl/default.xml', - '/components/com_contact/views/category/tmpl/default_children.php', - '/components/com_contact/views/category/tmpl/default_items.php', - '/components/com_contact/views/category/view.feed.php', - '/components/com_contact/views/category/view.html.php', - '/components/com_contact/views/contact/tmpl/default.php', - '/components/com_contact/views/contact/tmpl/default.xml', - '/components/com_contact/views/contact/tmpl/default_address.php', - '/components/com_contact/views/contact/tmpl/default_articles.php', - '/components/com_contact/views/contact/tmpl/default_form.php', - '/components/com_contact/views/contact/tmpl/default_links.php', - '/components/com_contact/views/contact/tmpl/default_profile.php', - '/components/com_contact/views/contact/tmpl/default_user_custom_fields.php', - '/components/com_contact/views/contact/view.html.php', - '/components/com_contact/views/contact/view.vcf.php', - '/components/com_contact/views/featured/tmpl/default.php', - '/components/com_contact/views/featured/tmpl/default.xml', - '/components/com_contact/views/featured/tmpl/default_items.php', - '/components/com_contact/views/featured/view.html.php', - '/components/com_content/content.php', - '/components/com_content/controller.php', - '/components/com_content/controllers/article.php', - '/components/com_content/helpers/association.php', - '/components/com_content/helpers/category.php', - '/components/com_content/helpers/legacyrouter.php', - '/components/com_content/helpers/query.php', - '/components/com_content/helpers/route.php', - '/components/com_content/models/archive.php', - '/components/com_content/models/article.php', - '/components/com_content/models/articles.php', - '/components/com_content/models/categories.php', - '/components/com_content/models/category.php', - '/components/com_content/models/featured.php', - '/components/com_content/models/form.php', - '/components/com_content/models/forms/article.xml', - '/components/com_content/models/forms/filter_articles.xml', - '/components/com_content/router.php', - '/components/com_content/views/archive/tmpl/default.php', - '/components/com_content/views/archive/tmpl/default.xml', - '/components/com_content/views/archive/tmpl/default_items.php', - '/components/com_content/views/archive/view.html.php', - '/components/com_content/views/article/tmpl/default.php', - '/components/com_content/views/article/tmpl/default.xml', - '/components/com_content/views/article/tmpl/default_links.php', - '/components/com_content/views/article/view.html.php', - '/components/com_content/views/categories/tmpl/default.php', - '/components/com_content/views/categories/tmpl/default.xml', - '/components/com_content/views/categories/tmpl/default_items.php', - '/components/com_content/views/categories/view.html.php', - '/components/com_content/views/category/tmpl/blog.php', - '/components/com_content/views/category/tmpl/blog.xml', - '/components/com_content/views/category/tmpl/blog_children.php', - '/components/com_content/views/category/tmpl/blog_item.php', - '/components/com_content/views/category/tmpl/blog_links.php', - '/components/com_content/views/category/tmpl/default.php', - '/components/com_content/views/category/tmpl/default.xml', - '/components/com_content/views/category/tmpl/default_articles.php', - '/components/com_content/views/category/tmpl/default_children.php', - '/components/com_content/views/category/view.feed.php', - '/components/com_content/views/category/view.html.php', - '/components/com_content/views/featured/tmpl/default.php', - '/components/com_content/views/featured/tmpl/default.xml', - '/components/com_content/views/featured/tmpl/default_item.php', - '/components/com_content/views/featured/tmpl/default_links.php', - '/components/com_content/views/featured/view.feed.php', - '/components/com_content/views/featured/view.html.php', - '/components/com_content/views/form/tmpl/edit.php', - '/components/com_content/views/form/tmpl/edit.xml', - '/components/com_content/views/form/view.html.php', - '/components/com_contenthistory/contenthistory.php', - '/components/com_fields/controller.php', - '/components/com_fields/fields.php', - '/components/com_fields/models/forms/filter_fields.xml', - '/components/com_finder/controller.php', - '/components/com_finder/controllers/suggestions.json.php', - '/components/com_finder/finder.php', - '/components/com_finder/helpers/html/filter.php', - '/components/com_finder/helpers/html/query.php', - '/components/com_finder/models/search.php', - '/components/com_finder/models/suggestions.php', - '/components/com_finder/router.php', - '/components/com_finder/views/search/tmpl/default.php', - '/components/com_finder/views/search/tmpl/default.xml', - '/components/com_finder/views/search/tmpl/default_form.php', - '/components/com_finder/views/search/tmpl/default_result.php', - '/components/com_finder/views/search/tmpl/default_results.php', - '/components/com_finder/views/search/view.feed.php', - '/components/com_finder/views/search/view.html.php', - '/components/com_finder/views/search/view.opensearch.php', - '/components/com_mailto/controller.php', - '/components/com_mailto/helpers/mailto.php', - '/components/com_mailto/mailto.php', - '/components/com_mailto/mailto.xml', - '/components/com_mailto/models/forms/mailto.xml', - '/components/com_mailto/models/mailto.php', - '/components/com_mailto/views/mailto/tmpl/default.php', - '/components/com_mailto/views/mailto/view.html.php', - '/components/com_mailto/views/sent/tmpl/default.php', - '/components/com_mailto/views/sent/view.html.php', - '/components/com_media/media.php', - '/components/com_menus/controller.php', - '/components/com_menus/menus.php', - '/components/com_menus/models/forms/filter_items.xml', - '/components/com_modules/controller.php', - '/components/com_modules/models/forms/filter_modules.xml', - '/components/com_modules/modules.php', - '/components/com_newsfeeds/controller.php', - '/components/com_newsfeeds/helpers/association.php', - '/components/com_newsfeeds/helpers/category.php', - '/components/com_newsfeeds/helpers/legacyrouter.php', - '/components/com_newsfeeds/models/categories.php', - '/components/com_newsfeeds/models/category.php', - '/components/com_newsfeeds/models/newsfeed.php', - '/components/com_newsfeeds/newsfeeds.php', - '/components/com_newsfeeds/router.php', - '/components/com_newsfeeds/views/categories/tmpl/default.php', - '/components/com_newsfeeds/views/categories/tmpl/default.xml', - '/components/com_newsfeeds/views/categories/tmpl/default_items.php', - '/components/com_newsfeeds/views/categories/view.html.php', - '/components/com_newsfeeds/views/category/tmpl/default.php', - '/components/com_newsfeeds/views/category/tmpl/default.xml', - '/components/com_newsfeeds/views/category/tmpl/default_children.php', - '/components/com_newsfeeds/views/category/tmpl/default_items.php', - '/components/com_newsfeeds/views/category/view.html.php', - '/components/com_newsfeeds/views/newsfeed/tmpl/default.php', - '/components/com_newsfeeds/views/newsfeed/tmpl/default.xml', - '/components/com_newsfeeds/views/newsfeed/view.html.php', - '/components/com_privacy/controller.php', - '/components/com_privacy/controllers/request.php', - '/components/com_privacy/models/confirm.php', - '/components/com_privacy/models/forms/confirm.xml', - '/components/com_privacy/models/forms/remind.xml', - '/components/com_privacy/models/forms/request.xml', - '/components/com_privacy/models/remind.php', - '/components/com_privacy/models/request.php', - '/components/com_privacy/privacy.php', - '/components/com_privacy/router.php', - '/components/com_privacy/views/confirm/tmpl/default.php', - '/components/com_privacy/views/confirm/tmpl/default.xml', - '/components/com_privacy/views/confirm/view.html.php', - '/components/com_privacy/views/remind/tmpl/default.php', - '/components/com_privacy/views/remind/tmpl/default.xml', - '/components/com_privacy/views/remind/view.html.php', - '/components/com_privacy/views/request/tmpl/default.php', - '/components/com_privacy/views/request/tmpl/default.xml', - '/components/com_privacy/views/request/view.html.php', - '/components/com_tags/controller.php', - '/components/com_tags/controllers/tags.php', - '/components/com_tags/models/tag.php', - '/components/com_tags/models/tags.php', - '/components/com_tags/router.php', - '/components/com_tags/tags.php', - '/components/com_tags/views/tag/tmpl/default.php', - '/components/com_tags/views/tag/tmpl/default.xml', - '/components/com_tags/views/tag/tmpl/default_items.php', - '/components/com_tags/views/tag/tmpl/list.php', - '/components/com_tags/views/tag/tmpl/list.xml', - '/components/com_tags/views/tag/tmpl/list_items.php', - '/components/com_tags/views/tag/view.feed.php', - '/components/com_tags/views/tag/view.html.php', - '/components/com_tags/views/tags/tmpl/default.php', - '/components/com_tags/views/tags/tmpl/default.xml', - '/components/com_tags/views/tags/tmpl/default_items.php', - '/components/com_tags/views/tags/view.feed.php', - '/components/com_tags/views/tags/view.html.php', - '/components/com_users/controller.php', - '/components/com_users/controllers/profile.php', - '/components/com_users/controllers/registration.php', - '/components/com_users/controllers/remind.php', - '/components/com_users/controllers/reset.php', - '/components/com_users/controllers/user.php', - '/components/com_users/helpers/html/users.php', - '/components/com_users/helpers/legacyrouter.php', - '/components/com_users/helpers/route.php', - '/components/com_users/layouts/joomla/form/renderfield.php', - '/components/com_users/models/forms/frontend.xml', - '/components/com_users/models/forms/frontend_admin.xml', - '/components/com_users/models/forms/login.xml', - '/components/com_users/models/forms/profile.xml', - '/components/com_users/models/forms/registration.xml', - '/components/com_users/models/forms/remind.xml', - '/components/com_users/models/forms/reset_complete.xml', - '/components/com_users/models/forms/reset_confirm.xml', - '/components/com_users/models/forms/reset_request.xml', - '/components/com_users/models/forms/sitelang.xml', - '/components/com_users/models/login.php', - '/components/com_users/models/profile.php', - '/components/com_users/models/registration.php', - '/components/com_users/models/remind.php', - '/components/com_users/models/reset.php', - '/components/com_users/models/rules/loginuniquefield.php', - '/components/com_users/models/rules/logoutuniquefield.php', - '/components/com_users/router.php', - '/components/com_users/users.php', - '/components/com_users/views/login/tmpl/default.php', - '/components/com_users/views/login/tmpl/default.xml', - '/components/com_users/views/login/tmpl/default_login.php', - '/components/com_users/views/login/tmpl/default_logout.php', - '/components/com_users/views/login/tmpl/logout.xml', - '/components/com_users/views/login/view.html.php', - '/components/com_users/views/profile/tmpl/default.php', - '/components/com_users/views/profile/tmpl/default.xml', - '/components/com_users/views/profile/tmpl/default_core.php', - '/components/com_users/views/profile/tmpl/default_custom.php', - '/components/com_users/views/profile/tmpl/default_params.php', - '/components/com_users/views/profile/tmpl/edit.php', - '/components/com_users/views/profile/tmpl/edit.xml', - '/components/com_users/views/profile/view.html.php', - '/components/com_users/views/registration/tmpl/complete.php', - '/components/com_users/views/registration/tmpl/default.php', - '/components/com_users/views/registration/tmpl/default.xml', - '/components/com_users/views/registration/view.html.php', - '/components/com_users/views/remind/tmpl/default.php', - '/components/com_users/views/remind/tmpl/default.xml', - '/components/com_users/views/remind/view.html.php', - '/components/com_users/views/reset/tmpl/complete.php', - '/components/com_users/views/reset/tmpl/confirm.php', - '/components/com_users/views/reset/tmpl/default.php', - '/components/com_users/views/reset/tmpl/default.xml', - '/components/com_users/views/reset/view.html.php', - '/components/com_wrapper/controller.php', - '/components/com_wrapper/router.php', - '/components/com_wrapper/views/wrapper/tmpl/default.php', - '/components/com_wrapper/views/wrapper/tmpl/default.xml', - '/components/com_wrapper/views/wrapper/view.html.php', - '/components/com_wrapper/wrapper.php', - '/components/com_wrapper/wrapper.xml', - '/language/en-GB/en-GB.com_ajax.ini', - '/language/en-GB/en-GB.com_config.ini', - '/language/en-GB/en-GB.com_contact.ini', - '/language/en-GB/en-GB.com_content.ini', - '/language/en-GB/en-GB.com_finder.ini', - '/language/en-GB/en-GB.com_mailto.ini', - '/language/en-GB/en-GB.com_media.ini', - '/language/en-GB/en-GB.com_messages.ini', - '/language/en-GB/en-GB.com_newsfeeds.ini', - '/language/en-GB/en-GB.com_privacy.ini', - '/language/en-GB/en-GB.com_tags.ini', - '/language/en-GB/en-GB.com_users.ini', - '/language/en-GB/en-GB.com_weblinks.ini', - '/language/en-GB/en-GB.com_wrapper.ini', - '/language/en-GB/en-GB.files_joomla.sys.ini', - '/language/en-GB/en-GB.finder_cli.ini', - '/language/en-GB/en-GB.ini', - '/language/en-GB/en-GB.lib_fof.ini', - '/language/en-GB/en-GB.lib_fof.sys.ini', - '/language/en-GB/en-GB.lib_idna_convert.sys.ini', - '/language/en-GB/en-GB.lib_joomla.ini', - '/language/en-GB/en-GB.lib_joomla.sys.ini', - '/language/en-GB/en-GB.lib_phpass.sys.ini', - '/language/en-GB/en-GB.lib_phputf8.sys.ini', - '/language/en-GB/en-GB.lib_simplepie.sys.ini', - '/language/en-GB/en-GB.localise.php', - '/language/en-GB/en-GB.mod_articles_archive.ini', - '/language/en-GB/en-GB.mod_articles_archive.sys.ini', - '/language/en-GB/en-GB.mod_articles_categories.ini', - '/language/en-GB/en-GB.mod_articles_categories.sys.ini', - '/language/en-GB/en-GB.mod_articles_category.ini', - '/language/en-GB/en-GB.mod_articles_category.sys.ini', - '/language/en-GB/en-GB.mod_articles_latest.ini', - '/language/en-GB/en-GB.mod_articles_latest.sys.ini', - '/language/en-GB/en-GB.mod_articles_news.ini', - '/language/en-GB/en-GB.mod_articles_news.sys.ini', - '/language/en-GB/en-GB.mod_articles_popular.ini', - '/language/en-GB/en-GB.mod_articles_popular.sys.ini', - '/language/en-GB/en-GB.mod_banners.ini', - '/language/en-GB/en-GB.mod_banners.sys.ini', - '/language/en-GB/en-GB.mod_breadcrumbs.ini', - '/language/en-GB/en-GB.mod_breadcrumbs.sys.ini', - '/language/en-GB/en-GB.mod_custom.ini', - '/language/en-GB/en-GB.mod_custom.sys.ini', - '/language/en-GB/en-GB.mod_feed.ini', - '/language/en-GB/en-GB.mod_feed.sys.ini', - '/language/en-GB/en-GB.mod_finder.ini', - '/language/en-GB/en-GB.mod_finder.sys.ini', - '/language/en-GB/en-GB.mod_footer.ini', - '/language/en-GB/en-GB.mod_footer.sys.ini', - '/language/en-GB/en-GB.mod_languages.ini', - '/language/en-GB/en-GB.mod_languages.sys.ini', - '/language/en-GB/en-GB.mod_login.ini', - '/language/en-GB/en-GB.mod_login.sys.ini', - '/language/en-GB/en-GB.mod_menu.ini', - '/language/en-GB/en-GB.mod_menu.sys.ini', - '/language/en-GB/en-GB.mod_random_image.ini', - '/language/en-GB/en-GB.mod_random_image.sys.ini', - '/language/en-GB/en-GB.mod_related_items.ini', - '/language/en-GB/en-GB.mod_related_items.sys.ini', - '/language/en-GB/en-GB.mod_stats.ini', - '/language/en-GB/en-GB.mod_stats.sys.ini', - '/language/en-GB/en-GB.mod_syndicate.ini', - '/language/en-GB/en-GB.mod_syndicate.sys.ini', - '/language/en-GB/en-GB.mod_tags_popular.ini', - '/language/en-GB/en-GB.mod_tags_popular.sys.ini', - '/language/en-GB/en-GB.mod_tags_similar.ini', - '/language/en-GB/en-GB.mod_tags_similar.sys.ini', - '/language/en-GB/en-GB.mod_users_latest.ini', - '/language/en-GB/en-GB.mod_users_latest.sys.ini', - '/language/en-GB/en-GB.mod_weblinks.ini', - '/language/en-GB/en-GB.mod_weblinks.sys.ini', - '/language/en-GB/en-GB.mod_whosonline.ini', - '/language/en-GB/en-GB.mod_whosonline.sys.ini', - '/language/en-GB/en-GB.mod_wrapper.ini', - '/language/en-GB/en-GB.mod_wrapper.sys.ini', - '/language/en-GB/en-GB.tpl_beez3.ini', - '/language/en-GB/en-GB.tpl_beez3.sys.ini', - '/language/en-GB/en-GB.tpl_protostar.ini', - '/language/en-GB/en-GB.tpl_protostar.sys.ini', - '/language/en-GB/en-GB.xml', - '/layouts/joomla/content/blog_style_default_links.php', - '/layouts/joomla/content/icons/email.php', - '/layouts/joomla/content/icons/print_popup.php', - '/layouts/joomla/content/icons/print_screen.php', - '/layouts/joomla/content/info_block/block.php', - '/layouts/joomla/edit/details.php', - '/layouts/joomla/edit/item_title.php', - '/layouts/joomla/form/field/radio.php', - '/layouts/joomla/html/formbehavior/ajaxchosen.php', - '/layouts/joomla/html/formbehavior/chosen.php', - '/layouts/joomla/html/sortablelist.php', - '/layouts/joomla/html/tag.php', - '/layouts/joomla/modal/body.php', - '/layouts/joomla/modal/footer.php', - '/layouts/joomla/modal/header.php', - '/layouts/joomla/modal/iframe.php', - '/layouts/joomla/modal/main.php', - '/layouts/joomla/sidebars/toggle.php', - '/layouts/joomla/tinymce/buttons.php', - '/layouts/joomla/tinymce/buttons/button.php', - '/layouts/joomla/toolbar/confirm.php', - '/layouts/joomla/toolbar/help.php', - '/layouts/joomla/toolbar/modal.php', - '/layouts/joomla/toolbar/slider.php', - '/layouts/libraries/cms/html/bootstrap/addtab.php', - '/layouts/libraries/cms/html/bootstrap/addtabscript.php', - '/layouts/libraries/cms/html/bootstrap/endtab.php', - '/layouts/libraries/cms/html/bootstrap/endtabset.php', - '/layouts/libraries/cms/html/bootstrap/starttabset.php', - '/layouts/libraries/cms/html/bootstrap/starttabsetscript.php', - '/libraries/cms/class/loader.php', - '/libraries/cms/html/access.php', - '/libraries/cms/html/actionsdropdown.php', - '/libraries/cms/html/adminlanguage.php', - '/libraries/cms/html/batch.php', - '/libraries/cms/html/behavior.php', - '/libraries/cms/html/bootstrap.php', - '/libraries/cms/html/category.php', - '/libraries/cms/html/content.php', - '/libraries/cms/html/contentlanguage.php', - '/libraries/cms/html/date.php', - '/libraries/cms/html/debug.php', - '/libraries/cms/html/dropdown.php', - '/libraries/cms/html/email.php', - '/libraries/cms/html/form.php', - '/libraries/cms/html/formbehavior.php', - '/libraries/cms/html/grid.php', - '/libraries/cms/html/icons.php', - '/libraries/cms/html/jgrid.php', - '/libraries/cms/html/jquery.php', - '/libraries/cms/html/language/en-GB/en-GB.jhtmldate.ini', - '/libraries/cms/html/links.php', - '/libraries/cms/html/list.php', - '/libraries/cms/html/menu.php', - '/libraries/cms/html/number.php', - '/libraries/cms/html/rules.php', - '/libraries/cms/html/searchtools.php', - '/libraries/cms/html/select.php', - '/libraries/cms/html/sidebar.php', - '/libraries/cms/html/sliders.php', - '/libraries/cms/html/sortablelist.php', - '/libraries/cms/html/string.php', - '/libraries/cms/html/tabs.php', - '/libraries/cms/html/tag.php', - '/libraries/cms/html/tel.php', - '/libraries/cms/html/user.php', - '/libraries/cms/less/formatter/joomla.php', - '/libraries/cms/less/less.php', - '/libraries/fof/LICENSE.txt', - '/libraries/fof/autoloader/component.php', - '/libraries/fof/autoloader/fof.php', - '/libraries/fof/config/domain/dispatcher.php', - '/libraries/fof/config/domain/interface.php', - '/libraries/fof/config/domain/tables.php', - '/libraries/fof/config/domain/views.php', - '/libraries/fof/config/provider.php', - '/libraries/fof/controller/controller.php', - '/libraries/fof/database/database.php', - '/libraries/fof/database/driver.php', - '/libraries/fof/database/driver/joomla.php', - '/libraries/fof/database/driver/mysql.php', - '/libraries/fof/database/driver/mysqli.php', - '/libraries/fof/database/driver/oracle.php', - '/libraries/fof/database/driver/pdo.php', - '/libraries/fof/database/driver/pdomysql.php', - '/libraries/fof/database/driver/postgresql.php', - '/libraries/fof/database/driver/sqlazure.php', - '/libraries/fof/database/driver/sqlite.php', - '/libraries/fof/database/driver/sqlsrv.php', - '/libraries/fof/database/factory.php', - '/libraries/fof/database/installer.php', - '/libraries/fof/database/interface.php', - '/libraries/fof/database/iterator.php', - '/libraries/fof/database/iterator/azure.php', - '/libraries/fof/database/iterator/mysql.php', - '/libraries/fof/database/iterator/mysqli.php', - '/libraries/fof/database/iterator/oracle.php', - '/libraries/fof/database/iterator/pdo.php', - '/libraries/fof/database/iterator/pdomysql.php', - '/libraries/fof/database/iterator/postgresql.php', - '/libraries/fof/database/iterator/sqlite.php', - '/libraries/fof/database/iterator/sqlsrv.php', - '/libraries/fof/database/query.php', - '/libraries/fof/database/query/element.php', - '/libraries/fof/database/query/limitable.php', - '/libraries/fof/database/query/mysql.php', - '/libraries/fof/database/query/mysqli.php', - '/libraries/fof/database/query/oracle.php', - '/libraries/fof/database/query/pdo.php', - '/libraries/fof/database/query/pdomysql.php', - '/libraries/fof/database/query/postgresql.php', - '/libraries/fof/database/query/preparable.php', - '/libraries/fof/database/query/sqlazure.php', - '/libraries/fof/database/query/sqlite.php', - '/libraries/fof/database/query/sqlsrv.php', - '/libraries/fof/dispatcher/dispatcher.php', - '/libraries/fof/download/adapter/abstract.php', - '/libraries/fof/download/adapter/cacert.pem', - '/libraries/fof/download/adapter/curl.php', - '/libraries/fof/download/adapter/fopen.php', - '/libraries/fof/download/download.php', - '/libraries/fof/download/interface.php', - '/libraries/fof/encrypt/aes.php', - '/libraries/fof/encrypt/aes/abstract.php', - '/libraries/fof/encrypt/aes/interface.php', - '/libraries/fof/encrypt/aes/mcrypt.php', - '/libraries/fof/encrypt/aes/openssl.php', - '/libraries/fof/encrypt/base32.php', - '/libraries/fof/encrypt/randval.php', - '/libraries/fof/encrypt/randvalinterface.php', - '/libraries/fof/encrypt/totp.php', - '/libraries/fof/form/field.php', - '/libraries/fof/form/field/accesslevel.php', - '/libraries/fof/form/field/actions.php', - '/libraries/fof/form/field/button.php', - '/libraries/fof/form/field/cachehandler.php', - '/libraries/fof/form/field/calendar.php', - '/libraries/fof/form/field/captcha.php', - '/libraries/fof/form/field/checkbox.php', - '/libraries/fof/form/field/checkboxes.php', - '/libraries/fof/form/field/components.php', - '/libraries/fof/form/field/editor.php', - '/libraries/fof/form/field/email.php', - '/libraries/fof/form/field/groupedbutton.php', - '/libraries/fof/form/field/groupedlist.php', - '/libraries/fof/form/field/hidden.php', - '/libraries/fof/form/field/image.php', - '/libraries/fof/form/field/imagelist.php', - '/libraries/fof/form/field/integer.php', - '/libraries/fof/form/field/language.php', - '/libraries/fof/form/field/list.php', - '/libraries/fof/form/field/media.php', - '/libraries/fof/form/field/model.php', - '/libraries/fof/form/field/ordering.php', - '/libraries/fof/form/field/password.php', - '/libraries/fof/form/field/plugins.php', - '/libraries/fof/form/field/published.php', - '/libraries/fof/form/field/radio.php', - '/libraries/fof/form/field/relation.php', - '/libraries/fof/form/field/rules.php', - '/libraries/fof/form/field/selectrow.php', - '/libraries/fof/form/field/sessionhandler.php', - '/libraries/fof/form/field/spacer.php', - '/libraries/fof/form/field/sql.php', - '/libraries/fof/form/field/tag.php', - '/libraries/fof/form/field/tel.php', - '/libraries/fof/form/field/text.php', - '/libraries/fof/form/field/textarea.php', - '/libraries/fof/form/field/timezone.php', - '/libraries/fof/form/field/title.php', - '/libraries/fof/form/field/url.php', - '/libraries/fof/form/field/user.php', - '/libraries/fof/form/field/usergroup.php', - '/libraries/fof/form/form.php', - '/libraries/fof/form/header.php', - '/libraries/fof/form/header/accesslevel.php', - '/libraries/fof/form/header/field.php', - '/libraries/fof/form/header/fielddate.php', - '/libraries/fof/form/header/fieldfilterable.php', - '/libraries/fof/form/header/fieldsearchable.php', - '/libraries/fof/form/header/fieldselectable.php', - '/libraries/fof/form/header/fieldsql.php', - '/libraries/fof/form/header/filterdate.php', - '/libraries/fof/form/header/filterfilterable.php', - '/libraries/fof/form/header/filtersearchable.php', - '/libraries/fof/form/header/filterselectable.php', - '/libraries/fof/form/header/filtersql.php', - '/libraries/fof/form/header/language.php', - '/libraries/fof/form/header/model.php', - '/libraries/fof/form/header/ordering.php', - '/libraries/fof/form/header/published.php', - '/libraries/fof/form/header/rowselect.php', - '/libraries/fof/form/helper.php', - '/libraries/fof/hal/document.php', - '/libraries/fof/hal/link.php', - '/libraries/fof/hal/links.php', - '/libraries/fof/hal/render/interface.php', - '/libraries/fof/hal/render/json.php', - '/libraries/fof/include.php', - '/libraries/fof/inflector/inflector.php', - '/libraries/fof/input/input.php', - '/libraries/fof/input/jinput/cli.php', - '/libraries/fof/input/jinput/cookie.php', - '/libraries/fof/input/jinput/files.php', - '/libraries/fof/input/jinput/input.php', - '/libraries/fof/input/jinput/json.php', - '/libraries/fof/integration/joomla/filesystem/filesystem.php', - '/libraries/fof/integration/joomla/platform.php', - '/libraries/fof/layout/file.php', - '/libraries/fof/layout/helper.php', - '/libraries/fof/less/formatter/classic.php', - '/libraries/fof/less/formatter/compressed.php', - '/libraries/fof/less/formatter/joomla.php', - '/libraries/fof/less/formatter/lessjs.php', - '/libraries/fof/less/less.php', - '/libraries/fof/less/parser/parser.php', - '/libraries/fof/model/behavior.php', - '/libraries/fof/model/behavior/access.php', - '/libraries/fof/model/behavior/emptynonzero.php', - '/libraries/fof/model/behavior/enabled.php', - '/libraries/fof/model/behavior/filters.php', - '/libraries/fof/model/behavior/language.php', - '/libraries/fof/model/behavior/private.php', - '/libraries/fof/model/dispatcher/behavior.php', - '/libraries/fof/model/field.php', - '/libraries/fof/model/field/boolean.php', - '/libraries/fof/model/field/date.php', - '/libraries/fof/model/field/number.php', - '/libraries/fof/model/field/text.php', - '/libraries/fof/model/model.php', - '/libraries/fof/platform/filesystem/filesystem.php', - '/libraries/fof/platform/filesystem/interface.php', - '/libraries/fof/platform/interface.php', - '/libraries/fof/platform/platform.php', - '/libraries/fof/query/abstract.php', - '/libraries/fof/render/abstract.php', - '/libraries/fof/render/joomla.php', - '/libraries/fof/render/joomla3.php', - '/libraries/fof/render/strapper.php', - '/libraries/fof/string/utils.php', - '/libraries/fof/table/behavior.php', - '/libraries/fof/table/behavior/assets.php', - '/libraries/fof/table/behavior/contenthistory.php', - '/libraries/fof/table/behavior/tags.php', - '/libraries/fof/table/dispatcher/behavior.php', - '/libraries/fof/table/nested.php', - '/libraries/fof/table/relations.php', - '/libraries/fof/table/table.php', - '/libraries/fof/template/utils.php', - '/libraries/fof/toolbar/toolbar.php', - '/libraries/fof/utils/array/array.php', - '/libraries/fof/utils/cache/cleaner.php', - '/libraries/fof/utils/config/helper.php', - '/libraries/fof/utils/filescheck/filescheck.php', - '/libraries/fof/utils/ini/parser.php', - '/libraries/fof/utils/installscript/installscript.php', - '/libraries/fof/utils/ip/ip.php', - '/libraries/fof/utils/object/object.php', - '/libraries/fof/utils/observable/dispatcher.php', - '/libraries/fof/utils/observable/event.php', - '/libraries/fof/utils/phpfunc/phpfunc.php', - '/libraries/fof/utils/timer/timer.php', - '/libraries/fof/utils/update/collection.php', - '/libraries/fof/utils/update/extension.php', - '/libraries/fof/utils/update/joomla.php', - '/libraries/fof/utils/update/update.php', - '/libraries/fof/version.txt', - '/libraries/fof/view/csv.php', - '/libraries/fof/view/form.php', - '/libraries/fof/view/html.php', - '/libraries/fof/view/json.php', - '/libraries/fof/view/raw.php', - '/libraries/fof/view/view.php', - '/libraries/idna_convert/LICENCE', - '/libraries/idna_convert/ReadMe.txt', - '/libraries/idna_convert/idna_convert.class.php', - '/libraries/idna_convert/transcode_wrapper.php', - '/libraries/idna_convert/uctc.php', - '/libraries/joomla/application/web/router.php', - '/libraries/joomla/application/web/router/base.php', - '/libraries/joomla/application/web/router/rest.php', - '/libraries/joomla/archive/archive.php', - '/libraries/joomla/archive/bzip2.php', - '/libraries/joomla/archive/extractable.php', - '/libraries/joomla/archive/gzip.php', - '/libraries/joomla/archive/tar.php', - '/libraries/joomla/archive/wrapper/archive.php', - '/libraries/joomla/archive/zip.php', - '/libraries/joomla/controller/base.php', - '/libraries/joomla/controller/controller.php', - '/libraries/joomla/database/database.php', - '/libraries/joomla/database/driver.php', - '/libraries/joomla/database/driver/mysql.php', - '/libraries/joomla/database/driver/mysqli.php', - '/libraries/joomla/database/driver/oracle.php', - '/libraries/joomla/database/driver/pdo.php', - '/libraries/joomla/database/driver/pdomysql.php', - '/libraries/joomla/database/driver/pgsql.php', - '/libraries/joomla/database/driver/postgresql.php', - '/libraries/joomla/database/driver/sqlazure.php', - '/libraries/joomla/database/driver/sqlite.php', - '/libraries/joomla/database/driver/sqlsrv.php', - '/libraries/joomla/database/exception/connecting.php', - '/libraries/joomla/database/exception/executing.php', - '/libraries/joomla/database/exception/unsupported.php', - '/libraries/joomla/database/exporter.php', - '/libraries/joomla/database/exporter/mysql.php', - '/libraries/joomla/database/exporter/mysqli.php', - '/libraries/joomla/database/exporter/pdomysql.php', - '/libraries/joomla/database/exporter/pgsql.php', - '/libraries/joomla/database/exporter/postgresql.php', - '/libraries/joomla/database/factory.php', - '/libraries/joomla/database/importer.php', - '/libraries/joomla/database/importer/mysql.php', - '/libraries/joomla/database/importer/mysqli.php', - '/libraries/joomla/database/importer/pdomysql.php', - '/libraries/joomla/database/importer/pgsql.php', - '/libraries/joomla/database/importer/postgresql.php', - '/libraries/joomla/database/interface.php', - '/libraries/joomla/database/iterator.php', - '/libraries/joomla/database/iterator/mysql.php', - '/libraries/joomla/database/iterator/mysqli.php', - '/libraries/joomla/database/iterator/oracle.php', - '/libraries/joomla/database/iterator/pdo.php', - '/libraries/joomla/database/iterator/pdomysql.php', - '/libraries/joomla/database/iterator/pgsql.php', - '/libraries/joomla/database/iterator/postgresql.php', - '/libraries/joomla/database/iterator/sqlazure.php', - '/libraries/joomla/database/iterator/sqlite.php', - '/libraries/joomla/database/iterator/sqlsrv.php', - '/libraries/joomla/database/query.php', - '/libraries/joomla/database/query/element.php', - '/libraries/joomla/database/query/limitable.php', - '/libraries/joomla/database/query/mysql.php', - '/libraries/joomla/database/query/mysqli.php', - '/libraries/joomla/database/query/oracle.php', - '/libraries/joomla/database/query/pdo.php', - '/libraries/joomla/database/query/pdomysql.php', - '/libraries/joomla/database/query/pgsql.php', - '/libraries/joomla/database/query/postgresql.php', - '/libraries/joomla/database/query/preparable.php', - '/libraries/joomla/database/query/sqlazure.php', - '/libraries/joomla/database/query/sqlite.php', - '/libraries/joomla/database/query/sqlsrv.php', - '/libraries/joomla/event/dispatcher.php', - '/libraries/joomla/event/event.php', - '/libraries/joomla/facebook/album.php', - '/libraries/joomla/facebook/checkin.php', - '/libraries/joomla/facebook/comment.php', - '/libraries/joomla/facebook/event.php', - '/libraries/joomla/facebook/facebook.php', - '/libraries/joomla/facebook/group.php', - '/libraries/joomla/facebook/link.php', - '/libraries/joomla/facebook/note.php', - '/libraries/joomla/facebook/oauth.php', - '/libraries/joomla/facebook/object.php', - '/libraries/joomla/facebook/photo.php', - '/libraries/joomla/facebook/post.php', - '/libraries/joomla/facebook/status.php', - '/libraries/joomla/facebook/user.php', - '/libraries/joomla/facebook/video.php', - '/libraries/joomla/form/fields/accesslevel.php', - '/libraries/joomla/form/fields/aliastag.php', - '/libraries/joomla/form/fields/cachehandler.php', - '/libraries/joomla/form/fields/calendar.php', - '/libraries/joomla/form/fields/checkbox.php', - '/libraries/joomla/form/fields/checkboxes.php', - '/libraries/joomla/form/fields/color.php', - '/libraries/joomla/form/fields/combo.php', - '/libraries/joomla/form/fields/components.php', - '/libraries/joomla/form/fields/databaseconnection.php', - '/libraries/joomla/form/fields/email.php', - '/libraries/joomla/form/fields/file.php', - '/libraries/joomla/form/fields/filelist.php', - '/libraries/joomla/form/fields/folderlist.php', - '/libraries/joomla/form/fields/groupedlist.php', - '/libraries/joomla/form/fields/hidden.php', - '/libraries/joomla/form/fields/imagelist.php', - '/libraries/joomla/form/fields/integer.php', - '/libraries/joomla/form/fields/language.php', - '/libraries/joomla/form/fields/list.php', - '/libraries/joomla/form/fields/meter.php', - '/libraries/joomla/form/fields/note.php', - '/libraries/joomla/form/fields/number.php', - '/libraries/joomla/form/fields/password.php', - '/libraries/joomla/form/fields/plugins.php', - '/libraries/joomla/form/fields/predefinedlist.php', - '/libraries/joomla/form/fields/radio.php', - '/libraries/joomla/form/fields/range.php', - '/libraries/joomla/form/fields/repeatable.php', - '/libraries/joomla/form/fields/rules.php', - '/libraries/joomla/form/fields/sessionhandler.php', - '/libraries/joomla/form/fields/spacer.php', - '/libraries/joomla/form/fields/sql.php', - '/libraries/joomla/form/fields/subform.php', - '/libraries/joomla/form/fields/tel.php', - '/libraries/joomla/form/fields/text.php', - '/libraries/joomla/form/fields/textarea.php', - '/libraries/joomla/form/fields/timezone.php', - '/libraries/joomla/form/fields/url.php', - '/libraries/joomla/form/fields/usergroup.php', - '/libraries/joomla/github/account.php', - '/libraries/joomla/github/commits.php', - '/libraries/joomla/github/forks.php', - '/libraries/joomla/github/github.php', - '/libraries/joomla/github/hooks.php', - '/libraries/joomla/github/http.php', - '/libraries/joomla/github/meta.php', - '/libraries/joomla/github/milestones.php', - '/libraries/joomla/github/object.php', - '/libraries/joomla/github/package.php', - '/libraries/joomla/github/package/activity.php', - '/libraries/joomla/github/package/activity/events.php', - '/libraries/joomla/github/package/activity/notifications.php', - '/libraries/joomla/github/package/activity/starring.php', - '/libraries/joomla/github/package/activity/watching.php', - '/libraries/joomla/github/package/authorization.php', - '/libraries/joomla/github/package/data.php', - '/libraries/joomla/github/package/data/blobs.php', - '/libraries/joomla/github/package/data/commits.php', - '/libraries/joomla/github/package/data/refs.php', - '/libraries/joomla/github/package/data/tags.php', - '/libraries/joomla/github/package/data/trees.php', - '/libraries/joomla/github/package/gists.php', - '/libraries/joomla/github/package/gists/comments.php', - '/libraries/joomla/github/package/gitignore.php', - '/libraries/joomla/github/package/issues.php', - '/libraries/joomla/github/package/issues/assignees.php', - '/libraries/joomla/github/package/issues/comments.php', - '/libraries/joomla/github/package/issues/events.php', - '/libraries/joomla/github/package/issues/labels.php', - '/libraries/joomla/github/package/issues/milestones.php', - '/libraries/joomla/github/package/markdown.php', - '/libraries/joomla/github/package/orgs.php', - '/libraries/joomla/github/package/orgs/members.php', - '/libraries/joomla/github/package/orgs/teams.php', - '/libraries/joomla/github/package/pulls.php', - '/libraries/joomla/github/package/pulls/comments.php', - '/libraries/joomla/github/package/repositories.php', - '/libraries/joomla/github/package/repositories/collaborators.php', - '/libraries/joomla/github/package/repositories/comments.php', - '/libraries/joomla/github/package/repositories/commits.php', - '/libraries/joomla/github/package/repositories/contents.php', - '/libraries/joomla/github/package/repositories/downloads.php', - '/libraries/joomla/github/package/repositories/forks.php', - '/libraries/joomla/github/package/repositories/hooks.php', - '/libraries/joomla/github/package/repositories/keys.php', - '/libraries/joomla/github/package/repositories/merging.php', - '/libraries/joomla/github/package/repositories/statistics.php', - '/libraries/joomla/github/package/repositories/statuses.php', - '/libraries/joomla/github/package/search.php', - '/libraries/joomla/github/package/users.php', - '/libraries/joomla/github/package/users/emails.php', - '/libraries/joomla/github/package/users/followers.php', - '/libraries/joomla/github/package/users/keys.php', - '/libraries/joomla/github/refs.php', - '/libraries/joomla/github/statuses.php', - '/libraries/joomla/google/auth.php', - '/libraries/joomla/google/auth/oauth2.php', - '/libraries/joomla/google/data.php', - '/libraries/joomla/google/data/adsense.php', - '/libraries/joomla/google/data/calendar.php', - '/libraries/joomla/google/data/picasa.php', - '/libraries/joomla/google/data/picasa/album.php', - '/libraries/joomla/google/data/picasa/photo.php', - '/libraries/joomla/google/data/plus.php', - '/libraries/joomla/google/data/plus/activities.php', - '/libraries/joomla/google/data/plus/comments.php', - '/libraries/joomla/google/data/plus/people.php', - '/libraries/joomla/google/embed.php', - '/libraries/joomla/google/embed/analytics.php', - '/libraries/joomla/google/embed/maps.php', - '/libraries/joomla/google/google.php', - '/libraries/joomla/grid/grid.php', - '/libraries/joomla/keychain/keychain.php', - '/libraries/joomla/linkedin/communications.php', - '/libraries/joomla/linkedin/companies.php', - '/libraries/joomla/linkedin/groups.php', - '/libraries/joomla/linkedin/jobs.php', - '/libraries/joomla/linkedin/linkedin.php', - '/libraries/joomla/linkedin/oauth.php', - '/libraries/joomla/linkedin/object.php', - '/libraries/joomla/linkedin/people.php', - '/libraries/joomla/linkedin/stream.php', - '/libraries/joomla/mediawiki/categories.php', - '/libraries/joomla/mediawiki/http.php', - '/libraries/joomla/mediawiki/images.php', - '/libraries/joomla/mediawiki/links.php', - '/libraries/joomla/mediawiki/mediawiki.php', - '/libraries/joomla/mediawiki/object.php', - '/libraries/joomla/mediawiki/pages.php', - '/libraries/joomla/mediawiki/search.php', - '/libraries/joomla/mediawiki/sites.php', - '/libraries/joomla/mediawiki/users.php', - '/libraries/joomla/model/base.php', - '/libraries/joomla/model/database.php', - '/libraries/joomla/model/model.php', - '/libraries/joomla/oauth1/client.php', - '/libraries/joomla/oauth2/client.php', - '/libraries/joomla/observable/interface.php', - '/libraries/joomla/observer/interface.php', - '/libraries/joomla/observer/mapper.php', - '/libraries/joomla/observer/updater.php', - '/libraries/joomla/observer/updater/interface.php', - '/libraries/joomla/observer/wrapper/mapper.php', - '/libraries/joomla/openstreetmap/changesets.php', - '/libraries/joomla/openstreetmap/elements.php', - '/libraries/joomla/openstreetmap/gps.php', - '/libraries/joomla/openstreetmap/info.php', - '/libraries/joomla/openstreetmap/oauth.php', - '/libraries/joomla/openstreetmap/object.php', - '/libraries/joomla/openstreetmap/openstreetmap.php', - '/libraries/joomla/openstreetmap/user.php', - '/libraries/joomla/platform.php', - '/libraries/joomla/route/wrapper/route.php', - '/libraries/joomla/session/handler/interface.php', - '/libraries/joomla/session/handler/joomla.php', - '/libraries/joomla/session/handler/native.php', - '/libraries/joomla/session/storage.php', - '/libraries/joomla/session/storage/apc.php', - '/libraries/joomla/session/storage/apcu.php', - '/libraries/joomla/session/storage/database.php', - '/libraries/joomla/session/storage/memcache.php', - '/libraries/joomla/session/storage/memcached.php', - '/libraries/joomla/session/storage/none.php', - '/libraries/joomla/session/storage/redis.php', - '/libraries/joomla/session/storage/wincache.php', - '/libraries/joomla/session/storage/xcache.php', - '/libraries/joomla/string/string.php', - '/libraries/joomla/string/wrapper/normalise.php', - '/libraries/joomla/string/wrapper/punycode.php', - '/libraries/joomla/twitter/block.php', - '/libraries/joomla/twitter/directmessages.php', - '/libraries/joomla/twitter/favorites.php', - '/libraries/joomla/twitter/friends.php', - '/libraries/joomla/twitter/help.php', - '/libraries/joomla/twitter/lists.php', - '/libraries/joomla/twitter/oauth.php', - '/libraries/joomla/twitter/object.php', - '/libraries/joomla/twitter/places.php', - '/libraries/joomla/twitter/profile.php', - '/libraries/joomla/twitter/search.php', - '/libraries/joomla/twitter/statuses.php', - '/libraries/joomla/twitter/trends.php', - '/libraries/joomla/twitter/twitter.php', - '/libraries/joomla/twitter/users.php', - '/libraries/joomla/utilities/arrayhelper.php', - '/libraries/joomla/view/base.php', - '/libraries/joomla/view/html.php', - '/libraries/joomla/view/view.php', - '/libraries/legacy/application/application.php', - '/libraries/legacy/base/node.php', - '/libraries/legacy/base/observable.php', - '/libraries/legacy/base/observer.php', - '/libraries/legacy/base/tree.php', - '/libraries/legacy/database/exception.php', - '/libraries/legacy/database/mysql.php', - '/libraries/legacy/database/mysqli.php', - '/libraries/legacy/database/sqlazure.php', - '/libraries/legacy/database/sqlsrv.php', - '/libraries/legacy/dispatcher/dispatcher.php', - '/libraries/legacy/error/error.php', - '/libraries/legacy/exception/exception.php', - '/libraries/legacy/form/field/category.php', - '/libraries/legacy/form/field/componentlayout.php', - '/libraries/legacy/form/field/modulelayout.php', - '/libraries/legacy/log/logexception.php', - '/libraries/legacy/request/request.php', - '/libraries/legacy/response/response.php', - '/libraries/legacy/simplecrypt/simplecrypt.php', - '/libraries/legacy/simplepie/factory.php', - '/libraries/legacy/table/session.php', - '/libraries/legacy/utilities/xmlelement.php', - '/libraries/phputf8/LICENSE', - '/libraries/phputf8/README', - '/libraries/phputf8/mbstring/core.php', - '/libraries/phputf8/native/core.php', - '/libraries/phputf8/ord.php', - '/libraries/phputf8/str_ireplace.php', - '/libraries/phputf8/str_pad.php', - '/libraries/phputf8/str_split.php', - '/libraries/phputf8/strcasecmp.php', - '/libraries/phputf8/strcspn.php', - '/libraries/phputf8/stristr.php', - '/libraries/phputf8/strrev.php', - '/libraries/phputf8/strspn.php', - '/libraries/phputf8/substr_replace.php', - '/libraries/phputf8/trim.php', - '/libraries/phputf8/ucfirst.php', - '/libraries/phputf8/ucwords.php', - '/libraries/phputf8/utf8.php', - '/libraries/phputf8/utils/ascii.php', - '/libraries/phputf8/utils/bad.php', - '/libraries/phputf8/utils/patterns.php', - '/libraries/phputf8/utils/position.php', - '/libraries/phputf8/utils/specials.php', - '/libraries/phputf8/utils/unicode.php', - '/libraries/phputf8/utils/validation.php', - '/libraries/src/Access/Wrapper/Access.php', - '/libraries/src/Cache/Storage/ApcStorage.php', - '/libraries/src/Cache/Storage/CacheliteStorage.php', - '/libraries/src/Cache/Storage/MemcacheStorage.php', - '/libraries/src/Cache/Storage/XcacheStorage.php', - '/libraries/src/Client/ClientWrapper.php', - '/libraries/src/Crypt/Cipher/BlowfishCipher.php', - '/libraries/src/Crypt/Cipher/McryptCipher.php', - '/libraries/src/Crypt/Cipher/Rijndael256Cipher.php', - '/libraries/src/Crypt/Cipher/SimpleCipher.php', - '/libraries/src/Crypt/Cipher/TripleDesCipher.php', - '/libraries/src/Crypt/CipherInterface.php', - '/libraries/src/Crypt/CryptPassword.php', - '/libraries/src/Crypt/Key.php', - '/libraries/src/Crypt/Password/SimpleCryptPassword.php', - '/libraries/src/Crypt/README.md', - '/libraries/src/Filesystem/Wrapper/FileWrapper.php', - '/libraries/src/Filesystem/Wrapper/FolderWrapper.php', - '/libraries/src/Filesystem/Wrapper/PathWrapper.php', - '/libraries/src/Filter/Wrapper/OutputFilterWrapper.php', - '/libraries/src/Form/Field/HelpsiteField.php', - '/libraries/src/Form/FormWrapper.php', - '/libraries/src/Helper/ContentHistoryHelper.php', - '/libraries/src/Helper/SearchHelper.php', - '/libraries/src/Http/Transport/cacert.pem', - '/libraries/src/Http/Wrapper/FactoryWrapper.php', - '/libraries/src/Language/LanguageStemmer.php', - '/libraries/src/Language/Stemmer/Porteren.php', - '/libraries/src/Language/Wrapper/JTextWrapper.php', - '/libraries/src/Language/Wrapper/LanguageHelperWrapper.php', - '/libraries/src/Language/Wrapper/TransliterateWrapper.php', - '/libraries/src/Mail/MailWrapper.php', - '/libraries/src/Menu/MenuHelper.php', - '/libraries/src/Menu/Node.php', - '/libraries/src/Menu/Node/Component.php', - '/libraries/src/Menu/Node/Container.php', - '/libraries/src/Menu/Node/Heading.php', - '/libraries/src/Menu/Node/Separator.php', - '/libraries/src/Menu/Node/Url.php', - '/libraries/src/Menu/Tree.php', - '/libraries/src/Table/Observer/AbstractObserver.php', - '/libraries/src/Table/Observer/ContentHistory.php', - '/libraries/src/Table/Observer/Tags.php', - '/libraries/src/Toolbar/Button/SliderButton.php', - '/libraries/src/User/UserWrapper.php', - '/libraries/vendor/.htaccess', - '/libraries/vendor/brumann/polyfill-unserialize/LICENSE', - '/libraries/vendor/brumann/polyfill-unserialize/composer.json', - '/libraries/vendor/brumann/polyfill-unserialize/src/DisallowedClassesSubstitutor.php', - '/libraries/vendor/brumann/polyfill-unserialize/src/Unserialize.php', - '/libraries/vendor/ircmaxell/password-compat/LICENSE.md', - '/libraries/vendor/ircmaxell/password-compat/lib/password.php', - '/libraries/vendor/joomla/application/src/AbstractCliApplication.php', - '/libraries/vendor/joomla/application/src/AbstractDaemonApplication.php', - '/libraries/vendor/joomla/application/src/Cli/CliInput.php', - '/libraries/vendor/joomla/application/src/Cli/CliOutput.php', - '/libraries/vendor/joomla/application/src/Cli/ColorProcessor.php', - '/libraries/vendor/joomla/application/src/Cli/ColorStyle.php', - '/libraries/vendor/joomla/application/src/Cli/Output/Processor/ColorProcessor.php', - '/libraries/vendor/joomla/application/src/Cli/Output/Processor/ProcessorInterface.php', - '/libraries/vendor/joomla/application/src/Cli/Output/Stdout.php', - '/libraries/vendor/joomla/application/src/Cli/Output/Xml.php', - '/libraries/vendor/joomla/compat/LICENSE', - '/libraries/vendor/joomla/compat/src/CallbackFilterIterator.php', - '/libraries/vendor/joomla/compat/src/JsonSerializable.php', - '/libraries/vendor/joomla/event/src/DelegatingDispatcher.php', - '/libraries/vendor/joomla/filesystem/src/Stream/String.php', - '/libraries/vendor/joomla/image/LICENSE', - '/libraries/vendor/joomla/image/src/Filter/Backgroundfill.php', - '/libraries/vendor/joomla/image/src/Filter/Brightness.php', - '/libraries/vendor/joomla/image/src/Filter/Contrast.php', - '/libraries/vendor/joomla/image/src/Filter/Edgedetect.php', - '/libraries/vendor/joomla/image/src/Filter/Emboss.php', - '/libraries/vendor/joomla/image/src/Filter/Grayscale.php', - '/libraries/vendor/joomla/image/src/Filter/Negate.php', - '/libraries/vendor/joomla/image/src/Filter/Sketchy.php', - '/libraries/vendor/joomla/image/src/Filter/Smooth.php', - '/libraries/vendor/joomla/image/src/Image.php', - '/libraries/vendor/joomla/image/src/ImageFilter.php', - '/libraries/vendor/joomla/input/src/Cli.php', - '/libraries/vendor/joomla/registry/src/AbstractRegistryFormat.php', - '/libraries/vendor/joomla/session/Joomla/Session/LICENSE', - '/libraries/vendor/joomla/session/Joomla/Session/Session.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/Apc.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/Apcu.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/Database.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/Memcache.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/Memcached.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/None.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/Wincache.php', - '/libraries/vendor/joomla/session/Joomla/Session/Storage/Xcache.php', - '/libraries/vendor/joomla/string/src/String.php', - '/libraries/vendor/leafo/lessphp/LICENSE', - '/libraries/vendor/leafo/lessphp/lessc.inc.php', - '/libraries/vendor/leafo/lessphp/lessify', - '/libraries/vendor/leafo/lessphp/lessify.inc.php', - '/libraries/vendor/leafo/lessphp/plessc', - '/libraries/vendor/paragonie/random_compat/LICENSE', - '/libraries/vendor/paragonie/random_compat/lib/byte_safe_strings.php', - '/libraries/vendor/paragonie/random_compat/lib/cast_to_int.php', - '/libraries/vendor/paragonie/random_compat/lib/error_polyfill.php', - '/libraries/vendor/paragonie/random_compat/lib/random.php', - '/libraries/vendor/paragonie/random_compat/lib/random_bytes_com_dotnet.php', - '/libraries/vendor/paragonie/random_compat/lib/random_bytes_dev_urandom.php', - '/libraries/vendor/paragonie/random_compat/lib/random_bytes_libsodium.php', - '/libraries/vendor/paragonie/random_compat/lib/random_bytes_libsodium_legacy.php', - '/libraries/vendor/paragonie/random_compat/lib/random_bytes_mcrypt.php', - '/libraries/vendor/paragonie/random_compat/lib/random_bytes_openssl.php', - '/libraries/vendor/paragonie/random_compat/lib/random_int.php', - '/libraries/vendor/paragonie/sodium_compat/src/Core32/Curve25519/README.md', - '/libraries/vendor/phpmailer/phpmailer/PHPMailerAutoload.php', - '/libraries/vendor/phpmailer/phpmailer/class.phpmailer.php', - '/libraries/vendor/phpmailer/phpmailer/class.phpmaileroauth.php', - '/libraries/vendor/phpmailer/phpmailer/class.phpmaileroauthgoogle.php', - '/libraries/vendor/phpmailer/phpmailer/class.pop3.php', - '/libraries/vendor/phpmailer/phpmailer/class.smtp.php', - '/libraries/vendor/phpmailer/phpmailer/extras/EasyPeasyICS.php', - '/libraries/vendor/phpmailer/phpmailer/extras/htmlfilter.php', - '/libraries/vendor/phpmailer/phpmailer/extras/ntlm_sasl_client.php', - '/libraries/vendor/simplepie/simplepie/LICENSE.txt', - '/libraries/vendor/simplepie/simplepie/autoloader.php', - '/libraries/vendor/simplepie/simplepie/db.sql', - '/libraries/vendor/simplepie/simplepie/idn/LICENCE', - '/libraries/vendor/simplepie/simplepie/idn/idna_convert.class.php', - '/libraries/vendor/simplepie/simplepie/idn/npdata.ser', - '/libraries/vendor/simplepie/simplepie/library/SimplePie.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Author.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/Base.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/DB.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/File.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/Memcache.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/MySQL.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Caption.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Category.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Content/Type/Sniffer.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Copyright.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Core.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Credit.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Decode/HTML/Entities.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Enclosure.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Exception.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/File.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/HTTP/Parser.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/IRI.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Item.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Locator.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Misc.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Net/IPv6.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Parse/Date.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Parser.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Rating.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Registry.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Restriction.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Sanitize.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Source.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/XML/Declaration/Parser.php', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/gzdecode.php', - '/libraries/vendor/symfony/polyfill-php55/LICENSE', - '/libraries/vendor/symfony/polyfill-php55/Php55.php', - '/libraries/vendor/symfony/polyfill-php55/Php55ArrayColumn.php', - '/libraries/vendor/symfony/polyfill-php55/bootstrap.php', - '/libraries/vendor/symfony/polyfill-php56/LICENSE', - '/libraries/vendor/symfony/polyfill-php56/Php56.php', - '/libraries/vendor/symfony/polyfill-php56/bootstrap.php', - '/libraries/vendor/symfony/polyfill-php71/LICENSE', - '/libraries/vendor/symfony/polyfill-php71/Php71.php', - '/libraries/vendor/symfony/polyfill-php71/bootstrap.php', - '/libraries/vendor/symfony/polyfill-util/Binary.php', - '/libraries/vendor/symfony/polyfill-util/BinaryNoFuncOverload.php', - '/libraries/vendor/symfony/polyfill-util/BinaryOnFuncOverload.php', - '/libraries/vendor/symfony/polyfill-util/LICENSE', - '/libraries/vendor/typo3/phar-stream-wrapper/composer.json', - '/libraries/vendor/web.config', - '/media/cms/css/debug.css', - '/media/com_associations/js/sidebyside-uncompressed.js', - '/media/com_contenthistory/css/jquery.pretty-text-diff.css', - '/media/com_contenthistory/js/diff_match_patch.js', - '/media/com_contenthistory/js/jquery.pretty-text-diff.js', - '/media/com_contenthistory/js/jquery.pretty-text-diff.min.js', - '/media/com_finder/js/autocompleter.js', - '/media/com_joomlaupdate/js/encryption.js', - '/media/com_joomlaupdate/js/encryption.min.js', - '/media/com_joomlaupdate/js/json2.js', - '/media/com_joomlaupdate/js/json2.min.js', - '/media/com_joomlaupdate/js/update.js', - '/media/com_joomlaupdate/js/update.min.js', - '/media/contacts/images/con_address.png', - '/media/contacts/images/con_fax.png', - '/media/contacts/images/con_info.png', - '/media/contacts/images/con_mobile.png', - '/media/contacts/images/con_tel.png', - '/media/contacts/images/emailButton.png', - '/media/editors/codemirror/LICENSE', - '/media/editors/codemirror/addon/comment/comment.js', - '/media/editors/codemirror/addon/comment/comment.min.js', - '/media/editors/codemirror/addon/comment/continuecomment.js', - '/media/editors/codemirror/addon/comment/continuecomment.min.js', - '/media/editors/codemirror/addon/dialog/dialog.css', - '/media/editors/codemirror/addon/dialog/dialog.js', - '/media/editors/codemirror/addon/dialog/dialog.min.css', - '/media/editors/codemirror/addon/dialog/dialog.min.js', - '/media/editors/codemirror/addon/display/autorefresh.js', - '/media/editors/codemirror/addon/display/autorefresh.min.js', - '/media/editors/codemirror/addon/display/fullscreen.css', - '/media/editors/codemirror/addon/display/fullscreen.js', - '/media/editors/codemirror/addon/display/fullscreen.min.css', - '/media/editors/codemirror/addon/display/fullscreen.min.js', - '/media/editors/codemirror/addon/display/panel.js', - '/media/editors/codemirror/addon/display/panel.min.js', - '/media/editors/codemirror/addon/display/placeholder.js', - '/media/editors/codemirror/addon/display/placeholder.min.js', - '/media/editors/codemirror/addon/display/rulers.js', - '/media/editors/codemirror/addon/display/rulers.min.js', - '/media/editors/codemirror/addon/edit/closebrackets.js', - '/media/editors/codemirror/addon/edit/closebrackets.min.js', - '/media/editors/codemirror/addon/edit/closetag.js', - '/media/editors/codemirror/addon/edit/closetag.min.js', - '/media/editors/codemirror/addon/edit/continuelist.js', - '/media/editors/codemirror/addon/edit/continuelist.min.js', - '/media/editors/codemirror/addon/edit/matchbrackets.js', - '/media/editors/codemirror/addon/edit/matchbrackets.min.js', - '/media/editors/codemirror/addon/edit/matchtags.js', - '/media/editors/codemirror/addon/edit/matchtags.min.js', - '/media/editors/codemirror/addon/edit/trailingspace.js', - '/media/editors/codemirror/addon/edit/trailingspace.min.js', - '/media/editors/codemirror/addon/fold/brace-fold.js', - '/media/editors/codemirror/addon/fold/brace-fold.min.js', - '/media/editors/codemirror/addon/fold/comment-fold.js', - '/media/editors/codemirror/addon/fold/comment-fold.min.js', - '/media/editors/codemirror/addon/fold/foldcode.js', - '/media/editors/codemirror/addon/fold/foldcode.min.js', - '/media/editors/codemirror/addon/fold/foldgutter.css', - '/media/editors/codemirror/addon/fold/foldgutter.js', - '/media/editors/codemirror/addon/fold/foldgutter.min.css', - '/media/editors/codemirror/addon/fold/foldgutter.min.js', - '/media/editors/codemirror/addon/fold/indent-fold.js', - '/media/editors/codemirror/addon/fold/indent-fold.min.js', - '/media/editors/codemirror/addon/fold/markdown-fold.js', - '/media/editors/codemirror/addon/fold/markdown-fold.min.js', - '/media/editors/codemirror/addon/fold/xml-fold.js', - '/media/editors/codemirror/addon/fold/xml-fold.min.js', - '/media/editors/codemirror/addon/hint/anyword-hint.js', - '/media/editors/codemirror/addon/hint/anyword-hint.min.js', - '/media/editors/codemirror/addon/hint/css-hint.js', - '/media/editors/codemirror/addon/hint/css-hint.min.js', - '/media/editors/codemirror/addon/hint/html-hint.js', - '/media/editors/codemirror/addon/hint/html-hint.min.js', - '/media/editors/codemirror/addon/hint/javascript-hint.js', - '/media/editors/codemirror/addon/hint/javascript-hint.min.js', - '/media/editors/codemirror/addon/hint/show-hint.css', - '/media/editors/codemirror/addon/hint/show-hint.js', - '/media/editors/codemirror/addon/hint/show-hint.min.css', - '/media/editors/codemirror/addon/hint/show-hint.min.js', - '/media/editors/codemirror/addon/hint/sql-hint.js', - '/media/editors/codemirror/addon/hint/sql-hint.min.js', - '/media/editors/codemirror/addon/hint/xml-hint.js', - '/media/editors/codemirror/addon/hint/xml-hint.min.js', - '/media/editors/codemirror/addon/lint/coffeescript-lint.js', - '/media/editors/codemirror/addon/lint/coffeescript-lint.min.js', - '/media/editors/codemirror/addon/lint/css-lint.js', - '/media/editors/codemirror/addon/lint/css-lint.min.js', - '/media/editors/codemirror/addon/lint/html-lint.js', - '/media/editors/codemirror/addon/lint/html-lint.min.js', - '/media/editors/codemirror/addon/lint/javascript-lint.js', - '/media/editors/codemirror/addon/lint/javascript-lint.min.js', - '/media/editors/codemirror/addon/lint/json-lint.js', - '/media/editors/codemirror/addon/lint/json-lint.min.js', - '/media/editors/codemirror/addon/lint/lint.css', - '/media/editors/codemirror/addon/lint/lint.js', - '/media/editors/codemirror/addon/lint/lint.min.css', - '/media/editors/codemirror/addon/lint/lint.min.js', - '/media/editors/codemirror/addon/lint/yaml-lint.js', - '/media/editors/codemirror/addon/lint/yaml-lint.min.js', - '/media/editors/codemirror/addon/merge/merge.css', - '/media/editors/codemirror/addon/merge/merge.js', - '/media/editors/codemirror/addon/merge/merge.min.css', - '/media/editors/codemirror/addon/merge/merge.min.js', - '/media/editors/codemirror/addon/mode/loadmode.js', - '/media/editors/codemirror/addon/mode/loadmode.min.js', - '/media/editors/codemirror/addon/mode/multiplex.js', - '/media/editors/codemirror/addon/mode/multiplex.min.js', - '/media/editors/codemirror/addon/mode/multiplex_test.js', - '/media/editors/codemirror/addon/mode/multiplex_test.min.js', - '/media/editors/codemirror/addon/mode/overlay.js', - '/media/editors/codemirror/addon/mode/overlay.min.js', - '/media/editors/codemirror/addon/mode/simple.js', - '/media/editors/codemirror/addon/mode/simple.min.js', - '/media/editors/codemirror/addon/runmode/colorize.js', - '/media/editors/codemirror/addon/runmode/colorize.min.js', - '/media/editors/codemirror/addon/runmode/runmode-standalone.js', - '/media/editors/codemirror/addon/runmode/runmode-standalone.min.js', - '/media/editors/codemirror/addon/runmode/runmode.js', - '/media/editors/codemirror/addon/runmode/runmode.min.js', - '/media/editors/codemirror/addon/runmode/runmode.node.js', - '/media/editors/codemirror/addon/scroll/annotatescrollbar.js', - '/media/editors/codemirror/addon/scroll/annotatescrollbar.min.js', - '/media/editors/codemirror/addon/scroll/scrollpastend.js', - '/media/editors/codemirror/addon/scroll/scrollpastend.min.js', - '/media/editors/codemirror/addon/scroll/simplescrollbars.css', - '/media/editors/codemirror/addon/scroll/simplescrollbars.js', - '/media/editors/codemirror/addon/scroll/simplescrollbars.min.css', - '/media/editors/codemirror/addon/scroll/simplescrollbars.min.js', - '/media/editors/codemirror/addon/search/jump-to-line.js', - '/media/editors/codemirror/addon/search/jump-to-line.min.js', - '/media/editors/codemirror/addon/search/match-highlighter.js', - '/media/editors/codemirror/addon/search/match-highlighter.min.js', - '/media/editors/codemirror/addon/search/matchesonscrollbar.css', - '/media/editors/codemirror/addon/search/matchesonscrollbar.js', - '/media/editors/codemirror/addon/search/matchesonscrollbar.min.css', - '/media/editors/codemirror/addon/search/matchesonscrollbar.min.js', - '/media/editors/codemirror/addon/search/search.js', - '/media/editors/codemirror/addon/search/search.min.js', - '/media/editors/codemirror/addon/search/searchcursor.js', - '/media/editors/codemirror/addon/search/searchcursor.min.js', - '/media/editors/codemirror/addon/selection/active-line.js', - '/media/editors/codemirror/addon/selection/active-line.min.js', - '/media/editors/codemirror/addon/selection/mark-selection.js', - '/media/editors/codemirror/addon/selection/mark-selection.min.js', - '/media/editors/codemirror/addon/selection/selection-pointer.js', - '/media/editors/codemirror/addon/selection/selection-pointer.min.js', - '/media/editors/codemirror/addon/tern/tern.css', - '/media/editors/codemirror/addon/tern/tern.js', - '/media/editors/codemirror/addon/tern/tern.min.css', - '/media/editors/codemirror/addon/tern/tern.min.js', - '/media/editors/codemirror/addon/tern/worker.js', - '/media/editors/codemirror/addon/tern/worker.min.js', - '/media/editors/codemirror/addon/wrap/hardwrap.js', - '/media/editors/codemirror/addon/wrap/hardwrap.min.js', - '/media/editors/codemirror/keymap/emacs.js', - '/media/editors/codemirror/keymap/emacs.min.js', - '/media/editors/codemirror/keymap/sublime.js', - '/media/editors/codemirror/keymap/sublime.min.js', - '/media/editors/codemirror/keymap/vim.js', - '/media/editors/codemirror/keymap/vim.min.js', - '/media/editors/codemirror/lib/addons.css', - '/media/editors/codemirror/lib/addons.js', - '/media/editors/codemirror/lib/addons.min.css', - '/media/editors/codemirror/lib/addons.min.js', - '/media/editors/codemirror/lib/codemirror.css', - '/media/editors/codemirror/lib/codemirror.js', - '/media/editors/codemirror/lib/codemirror.min.css', - '/media/editors/codemirror/lib/codemirror.min.js', - '/media/editors/codemirror/mode/apl/apl.js', - '/media/editors/codemirror/mode/apl/apl.min.js', - '/media/editors/codemirror/mode/asciiarmor/asciiarmor.js', - '/media/editors/codemirror/mode/asciiarmor/asciiarmor.min.js', - '/media/editors/codemirror/mode/asn.1/asn.1.js', - '/media/editors/codemirror/mode/asn.1/asn.min.js', - '/media/editors/codemirror/mode/asterisk/asterisk.js', - '/media/editors/codemirror/mode/asterisk/asterisk.min.js', - '/media/editors/codemirror/mode/brainfuck/brainfuck.js', - '/media/editors/codemirror/mode/brainfuck/brainfuck.min.js', - '/media/editors/codemirror/mode/clike/clike.js', - '/media/editors/codemirror/mode/clike/clike.min.js', - '/media/editors/codemirror/mode/clojure/clojure.js', - '/media/editors/codemirror/mode/clojure/clojure.min.js', - '/media/editors/codemirror/mode/cmake/cmake.js', - '/media/editors/codemirror/mode/cmake/cmake.min.js', - '/media/editors/codemirror/mode/cobol/cobol.js', - '/media/editors/codemirror/mode/cobol/cobol.min.js', - '/media/editors/codemirror/mode/coffeescript/coffeescript.js', - '/media/editors/codemirror/mode/coffeescript/coffeescript.min.js', - '/media/editors/codemirror/mode/commonlisp/commonlisp.js', - '/media/editors/codemirror/mode/commonlisp/commonlisp.min.js', - '/media/editors/codemirror/mode/crystal/crystal.js', - '/media/editors/codemirror/mode/crystal/crystal.min.js', - '/media/editors/codemirror/mode/css/css.js', - '/media/editors/codemirror/mode/css/css.min.js', - '/media/editors/codemirror/mode/cypher/cypher.js', - '/media/editors/codemirror/mode/cypher/cypher.min.js', - '/media/editors/codemirror/mode/d/d.js', - '/media/editors/codemirror/mode/d/d.min.js', - '/media/editors/codemirror/mode/dart/dart.js', - '/media/editors/codemirror/mode/dart/dart.min.js', - '/media/editors/codemirror/mode/diff/diff.js', - '/media/editors/codemirror/mode/diff/diff.min.js', - '/media/editors/codemirror/mode/django/django.js', - '/media/editors/codemirror/mode/django/django.min.js', - '/media/editors/codemirror/mode/dockerfile/dockerfile.js', - '/media/editors/codemirror/mode/dockerfile/dockerfile.min.js', - '/media/editors/codemirror/mode/dtd/dtd.js', - '/media/editors/codemirror/mode/dtd/dtd.min.js', - '/media/editors/codemirror/mode/dylan/dylan.js', - '/media/editors/codemirror/mode/dylan/dylan.min.js', - '/media/editors/codemirror/mode/ebnf/ebnf.js', - '/media/editors/codemirror/mode/ebnf/ebnf.min.js', - '/media/editors/codemirror/mode/ecl/ecl.js', - '/media/editors/codemirror/mode/ecl/ecl.min.js', - '/media/editors/codemirror/mode/eiffel/eiffel.js', - '/media/editors/codemirror/mode/eiffel/eiffel.min.js', - '/media/editors/codemirror/mode/elm/elm.js', - '/media/editors/codemirror/mode/elm/elm.min.js', - '/media/editors/codemirror/mode/erlang/erlang.js', - '/media/editors/codemirror/mode/erlang/erlang.min.js', - '/media/editors/codemirror/mode/factor/factor.js', - '/media/editors/codemirror/mode/factor/factor.min.js', - '/media/editors/codemirror/mode/fcl/fcl.js', - '/media/editors/codemirror/mode/fcl/fcl.min.js', - '/media/editors/codemirror/mode/forth/forth.js', - '/media/editors/codemirror/mode/forth/forth.min.js', - '/media/editors/codemirror/mode/fortran/fortran.js', - '/media/editors/codemirror/mode/fortran/fortran.min.js', - '/media/editors/codemirror/mode/gas/gas.js', - '/media/editors/codemirror/mode/gas/gas.min.js', - '/media/editors/codemirror/mode/gfm/gfm.js', - '/media/editors/codemirror/mode/gfm/gfm.min.js', - '/media/editors/codemirror/mode/gherkin/gherkin.js', - '/media/editors/codemirror/mode/gherkin/gherkin.min.js', - '/media/editors/codemirror/mode/go/go.js', - '/media/editors/codemirror/mode/go/go.min.js', - '/media/editors/codemirror/mode/groovy/groovy.js', - '/media/editors/codemirror/mode/groovy/groovy.min.js', - '/media/editors/codemirror/mode/haml/haml.js', - '/media/editors/codemirror/mode/haml/haml.min.js', - '/media/editors/codemirror/mode/handlebars/handlebars.js', - '/media/editors/codemirror/mode/handlebars/handlebars.min.js', - '/media/editors/codemirror/mode/haskell-literate/haskell-literate.js', - '/media/editors/codemirror/mode/haskell-literate/haskell-literate.min.js', - '/media/editors/codemirror/mode/haskell/haskell.js', - '/media/editors/codemirror/mode/haskell/haskell.min.js', - '/media/editors/codemirror/mode/haxe/haxe.js', - '/media/editors/codemirror/mode/haxe/haxe.min.js', - '/media/editors/codemirror/mode/htmlembedded/htmlembedded.js', - '/media/editors/codemirror/mode/htmlembedded/htmlembedded.min.js', - '/media/editors/codemirror/mode/htmlmixed/htmlmixed.js', - '/media/editors/codemirror/mode/htmlmixed/htmlmixed.min.js', - '/media/editors/codemirror/mode/http/http.js', - '/media/editors/codemirror/mode/http/http.min.js', - '/media/editors/codemirror/mode/idl/idl.js', - '/media/editors/codemirror/mode/idl/idl.min.js', - '/media/editors/codemirror/mode/javascript/javascript.js', - '/media/editors/codemirror/mode/javascript/javascript.min.js', - '/media/editors/codemirror/mode/jinja2/jinja2.js', - '/media/editors/codemirror/mode/jinja2/jinja2.min.js', - '/media/editors/codemirror/mode/jsx/jsx.js', - '/media/editors/codemirror/mode/jsx/jsx.min.js', - '/media/editors/codemirror/mode/julia/julia.js', - '/media/editors/codemirror/mode/julia/julia.min.js', - '/media/editors/codemirror/mode/livescript/livescript.js', - '/media/editors/codemirror/mode/livescript/livescript.min.js', - '/media/editors/codemirror/mode/lua/lua.js', - '/media/editors/codemirror/mode/lua/lua.min.js', - '/media/editors/codemirror/mode/markdown/markdown.js', - '/media/editors/codemirror/mode/markdown/markdown.min.js', - '/media/editors/codemirror/mode/mathematica/mathematica.js', - '/media/editors/codemirror/mode/mathematica/mathematica.min.js', - '/media/editors/codemirror/mode/mbox/mbox.js', - '/media/editors/codemirror/mode/mbox/mbox.min.js', - '/media/editors/codemirror/mode/meta.js', - '/media/editors/codemirror/mode/meta.min.js', - '/media/editors/codemirror/mode/mirc/mirc.js', - '/media/editors/codemirror/mode/mirc/mirc.min.js', - '/media/editors/codemirror/mode/mllike/mllike.js', - '/media/editors/codemirror/mode/mllike/mllike.min.js', - '/media/editors/codemirror/mode/modelica/modelica.js', - '/media/editors/codemirror/mode/modelica/modelica.min.js', - '/media/editors/codemirror/mode/mscgen/mscgen.js', - '/media/editors/codemirror/mode/mscgen/mscgen.min.js', - '/media/editors/codemirror/mode/mumps/mumps.js', - '/media/editors/codemirror/mode/mumps/mumps.min.js', - '/media/editors/codemirror/mode/nginx/nginx.js', - '/media/editors/codemirror/mode/nginx/nginx.min.js', - '/media/editors/codemirror/mode/nsis/nsis.js', - '/media/editors/codemirror/mode/nsis/nsis.min.js', - '/media/editors/codemirror/mode/ntriples/ntriples.js', - '/media/editors/codemirror/mode/ntriples/ntriples.min.js', - '/media/editors/codemirror/mode/octave/octave.js', - '/media/editors/codemirror/mode/octave/octave.min.js', - '/media/editors/codemirror/mode/oz/oz.js', - '/media/editors/codemirror/mode/oz/oz.min.js', - '/media/editors/codemirror/mode/pascal/pascal.js', - '/media/editors/codemirror/mode/pascal/pascal.min.js', - '/media/editors/codemirror/mode/pegjs/pegjs.js', - '/media/editors/codemirror/mode/pegjs/pegjs.min.js', - '/media/editors/codemirror/mode/perl/perl.js', - '/media/editors/codemirror/mode/perl/perl.min.js', - '/media/editors/codemirror/mode/php/php.js', - '/media/editors/codemirror/mode/php/php.min.js', - '/media/editors/codemirror/mode/pig/pig.js', - '/media/editors/codemirror/mode/pig/pig.min.js', - '/media/editors/codemirror/mode/powershell/powershell.js', - '/media/editors/codemirror/mode/powershell/powershell.min.js', - '/media/editors/codemirror/mode/properties/properties.js', - '/media/editors/codemirror/mode/properties/properties.min.js', - '/media/editors/codemirror/mode/protobuf/protobuf.js', - '/media/editors/codemirror/mode/protobuf/protobuf.min.js', - '/media/editors/codemirror/mode/pug/pug.js', - '/media/editors/codemirror/mode/pug/pug.min.js', - '/media/editors/codemirror/mode/puppet/puppet.js', - '/media/editors/codemirror/mode/puppet/puppet.min.js', - '/media/editors/codemirror/mode/python/python.js', - '/media/editors/codemirror/mode/python/python.min.js', - '/media/editors/codemirror/mode/q/q.js', - '/media/editors/codemirror/mode/q/q.min.js', - '/media/editors/codemirror/mode/r/r.js', - '/media/editors/codemirror/mode/r/r.min.js', - '/media/editors/codemirror/mode/rpm/changes/index.html', - '/media/editors/codemirror/mode/rpm/rpm.js', - '/media/editors/codemirror/mode/rpm/rpm.min.js', - '/media/editors/codemirror/mode/rst/rst.js', - '/media/editors/codemirror/mode/rst/rst.min.js', - '/media/editors/codemirror/mode/ruby/ruby.js', - '/media/editors/codemirror/mode/ruby/ruby.min.js', - '/media/editors/codemirror/mode/rust/rust.js', - '/media/editors/codemirror/mode/rust/rust.min.js', - '/media/editors/codemirror/mode/sas/sas.js', - '/media/editors/codemirror/mode/sas/sas.min.js', - '/media/editors/codemirror/mode/sass/sass.js', - '/media/editors/codemirror/mode/sass/sass.min.js', - '/media/editors/codemirror/mode/scheme/scheme.js', - '/media/editors/codemirror/mode/scheme/scheme.min.js', - '/media/editors/codemirror/mode/shell/shell.js', - '/media/editors/codemirror/mode/shell/shell.min.js', - '/media/editors/codemirror/mode/sieve/sieve.js', - '/media/editors/codemirror/mode/sieve/sieve.min.js', - '/media/editors/codemirror/mode/slim/slim.js', - '/media/editors/codemirror/mode/slim/slim.min.js', - '/media/editors/codemirror/mode/smalltalk/smalltalk.js', - '/media/editors/codemirror/mode/smalltalk/smalltalk.min.js', - '/media/editors/codemirror/mode/smarty/smarty.js', - '/media/editors/codemirror/mode/smarty/smarty.min.js', - '/media/editors/codemirror/mode/solr/solr.js', - '/media/editors/codemirror/mode/solr/solr.min.js', - '/media/editors/codemirror/mode/soy/soy.js', - '/media/editors/codemirror/mode/soy/soy.min.js', - '/media/editors/codemirror/mode/sparql/sparql.js', - '/media/editors/codemirror/mode/sparql/sparql.min.js', - '/media/editors/codemirror/mode/spreadsheet/spreadsheet.js', - '/media/editors/codemirror/mode/spreadsheet/spreadsheet.min.js', - '/media/editors/codemirror/mode/sql/sql.js', - '/media/editors/codemirror/mode/sql/sql.min.js', - '/media/editors/codemirror/mode/stex/stex.js', - '/media/editors/codemirror/mode/stex/stex.min.js', - '/media/editors/codemirror/mode/stylus/stylus.js', - '/media/editors/codemirror/mode/stylus/stylus.min.js', - '/media/editors/codemirror/mode/swift/swift.js', - '/media/editors/codemirror/mode/swift/swift.min.js', - '/media/editors/codemirror/mode/tcl/tcl.js', - '/media/editors/codemirror/mode/tcl/tcl.min.js', - '/media/editors/codemirror/mode/textile/textile.js', - '/media/editors/codemirror/mode/textile/textile.min.js', - '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.css', - '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.js', - '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.min.css', - '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.min.js', - '/media/editors/codemirror/mode/tiki/tiki.css', - '/media/editors/codemirror/mode/tiki/tiki.js', - '/media/editors/codemirror/mode/tiki/tiki.min.css', - '/media/editors/codemirror/mode/tiki/tiki.min.js', - '/media/editors/codemirror/mode/toml/toml.js', - '/media/editors/codemirror/mode/toml/toml.min.js', - '/media/editors/codemirror/mode/tornado/tornado.js', - '/media/editors/codemirror/mode/tornado/tornado.min.js', - '/media/editors/codemirror/mode/troff/troff.js', - '/media/editors/codemirror/mode/troff/troff.min.js', - '/media/editors/codemirror/mode/ttcn-cfg/ttcn-cfg.js', - '/media/editors/codemirror/mode/ttcn-cfg/ttcn-cfg.min.js', - '/media/editors/codemirror/mode/ttcn/ttcn.js', - '/media/editors/codemirror/mode/ttcn/ttcn.min.js', - '/media/editors/codemirror/mode/turtle/turtle.js', - '/media/editors/codemirror/mode/turtle/turtle.min.js', - '/media/editors/codemirror/mode/twig/twig.js', - '/media/editors/codemirror/mode/twig/twig.min.js', - '/media/editors/codemirror/mode/vb/vb.js', - '/media/editors/codemirror/mode/vb/vb.min.js', - '/media/editors/codemirror/mode/vbscript/vbscript.js', - '/media/editors/codemirror/mode/vbscript/vbscript.min.js', - '/media/editors/codemirror/mode/velocity/velocity.js', - '/media/editors/codemirror/mode/velocity/velocity.min.js', - '/media/editors/codemirror/mode/verilog/verilog.js', - '/media/editors/codemirror/mode/verilog/verilog.min.js', - '/media/editors/codemirror/mode/vhdl/vhdl.js', - '/media/editors/codemirror/mode/vhdl/vhdl.min.js', - '/media/editors/codemirror/mode/vue/vue.js', - '/media/editors/codemirror/mode/vue/vue.min.js', - '/media/editors/codemirror/mode/wast/wast.js', - '/media/editors/codemirror/mode/wast/wast.min.js', - '/media/editors/codemirror/mode/webidl/webidl.js', - '/media/editors/codemirror/mode/webidl/webidl.min.js', - '/media/editors/codemirror/mode/xml/xml.js', - '/media/editors/codemirror/mode/xml/xml.min.js', - '/media/editors/codemirror/mode/xquery/xquery.js', - '/media/editors/codemirror/mode/xquery/xquery.min.js', - '/media/editors/codemirror/mode/yacas/yacas.js', - '/media/editors/codemirror/mode/yacas/yacas.min.js', - '/media/editors/codemirror/mode/yaml-frontmatter/yaml-frontmatter.js', - '/media/editors/codemirror/mode/yaml-frontmatter/yaml-frontmatter.min.js', - '/media/editors/codemirror/mode/yaml/yaml.js', - '/media/editors/codemirror/mode/yaml/yaml.min.js', - '/media/editors/codemirror/mode/z80/z80.js', - '/media/editors/codemirror/mode/z80/z80.min.js', - '/media/editors/codemirror/theme/3024-day.css', - '/media/editors/codemirror/theme/3024-night.css', - '/media/editors/codemirror/theme/abcdef.css', - '/media/editors/codemirror/theme/ambiance-mobile.css', - '/media/editors/codemirror/theme/ambiance.css', - '/media/editors/codemirror/theme/ayu-dark.css', - '/media/editors/codemirror/theme/ayu-mirage.css', - '/media/editors/codemirror/theme/base16-dark.css', - '/media/editors/codemirror/theme/base16-light.css', - '/media/editors/codemirror/theme/bespin.css', - '/media/editors/codemirror/theme/blackboard.css', - '/media/editors/codemirror/theme/cobalt.css', - '/media/editors/codemirror/theme/colorforth.css', - '/media/editors/codemirror/theme/darcula.css', - '/media/editors/codemirror/theme/dracula.css', - '/media/editors/codemirror/theme/duotone-dark.css', - '/media/editors/codemirror/theme/duotone-light.css', - '/media/editors/codemirror/theme/eclipse.css', - '/media/editors/codemirror/theme/elegant.css', - '/media/editors/codemirror/theme/erlang-dark.css', - '/media/editors/codemirror/theme/gruvbox-dark.css', - '/media/editors/codemirror/theme/hopscotch.css', - '/media/editors/codemirror/theme/icecoder.css', - '/media/editors/codemirror/theme/idea.css', - '/media/editors/codemirror/theme/isotope.css', - '/media/editors/codemirror/theme/lesser-dark.css', - '/media/editors/codemirror/theme/liquibyte.css', - '/media/editors/codemirror/theme/lucario.css', - '/media/editors/codemirror/theme/material-darker.css', - '/media/editors/codemirror/theme/material-ocean.css', - '/media/editors/codemirror/theme/material-palenight.css', - '/media/editors/codemirror/theme/material.css', - '/media/editors/codemirror/theme/mbo.css', - '/media/editors/codemirror/theme/mdn-like.css', - '/media/editors/codemirror/theme/midnight.css', - '/media/editors/codemirror/theme/monokai.css', - '/media/editors/codemirror/theme/moxer.css', - '/media/editors/codemirror/theme/neat.css', - '/media/editors/codemirror/theme/neo.css', - '/media/editors/codemirror/theme/night.css', - '/media/editors/codemirror/theme/nord.css', - '/media/editors/codemirror/theme/oceanic-next.css', - '/media/editors/codemirror/theme/panda-syntax.css', - '/media/editors/codemirror/theme/paraiso-dark.css', - '/media/editors/codemirror/theme/paraiso-light.css', - '/media/editors/codemirror/theme/pastel-on-dark.css', - '/media/editors/codemirror/theme/railscasts.css', - '/media/editors/codemirror/theme/rubyblue.css', - '/media/editors/codemirror/theme/seti.css', - '/media/editors/codemirror/theme/shadowfox.css', - '/media/editors/codemirror/theme/solarized.css', - '/media/editors/codemirror/theme/ssms.css', - '/media/editors/codemirror/theme/the-matrix.css', - '/media/editors/codemirror/theme/tomorrow-night-bright.css', - '/media/editors/codemirror/theme/tomorrow-night-eighties.css', - '/media/editors/codemirror/theme/ttcn.css', - '/media/editors/codemirror/theme/twilight.css', - '/media/editors/codemirror/theme/vibrant-ink.css', - '/media/editors/codemirror/theme/xq-dark.css', - '/media/editors/codemirror/theme/xq-light.css', - '/media/editors/codemirror/theme/yeti.css', - '/media/editors/codemirror/theme/yonce.css', - '/media/editors/codemirror/theme/zenburn.css', - '/media/editors/none/js/none.js', - '/media/editors/none/js/none.min.js', - '/media/editors/tinymce/changelog.txt', - '/media/editors/tinymce/js/plugins/dragdrop/plugin.js', - '/media/editors/tinymce/js/plugins/dragdrop/plugin.min.js', - '/media/editors/tinymce/js/tiny-close.js', - '/media/editors/tinymce/js/tiny-close.min.js', - '/media/editors/tinymce/js/tinymce-builder.js', - '/media/editors/tinymce/js/tinymce.js', - '/media/editors/tinymce/js/tinymce.min.js', - '/media/editors/tinymce/langs/af.js', - '/media/editors/tinymce/langs/ar.js', - '/media/editors/tinymce/langs/be.js', - '/media/editors/tinymce/langs/bg.js', - '/media/editors/tinymce/langs/bs.js', - '/media/editors/tinymce/langs/ca.js', - '/media/editors/tinymce/langs/cs.js', - '/media/editors/tinymce/langs/cy.js', - '/media/editors/tinymce/langs/da.js', - '/media/editors/tinymce/langs/de.js', - '/media/editors/tinymce/langs/el.js', - '/media/editors/tinymce/langs/es.js', - '/media/editors/tinymce/langs/et.js', - '/media/editors/tinymce/langs/eu.js', - '/media/editors/tinymce/langs/fa.js', - '/media/editors/tinymce/langs/fi.js', - '/media/editors/tinymce/langs/fo.js', - '/media/editors/tinymce/langs/fr.js', - '/media/editors/tinymce/langs/ga.js', - '/media/editors/tinymce/langs/gl.js', - '/media/editors/tinymce/langs/he.js', - '/media/editors/tinymce/langs/hr.js', - '/media/editors/tinymce/langs/hu.js', - '/media/editors/tinymce/langs/id.js', - '/media/editors/tinymce/langs/it.js', - '/media/editors/tinymce/langs/ja.js', - '/media/editors/tinymce/langs/ka.js', - '/media/editors/tinymce/langs/kk.js', - '/media/editors/tinymce/langs/km.js', - '/media/editors/tinymce/langs/ko.js', - '/media/editors/tinymce/langs/lb.js', - '/media/editors/tinymce/langs/lt.js', - '/media/editors/tinymce/langs/lv.js', - '/media/editors/tinymce/langs/mk.js', - '/media/editors/tinymce/langs/ms.js', - '/media/editors/tinymce/langs/nb.js', - '/media/editors/tinymce/langs/nl.js', - '/media/editors/tinymce/langs/pl.js', - '/media/editors/tinymce/langs/pt-BR.js', - '/media/editors/tinymce/langs/pt-PT.js', - '/media/editors/tinymce/langs/readme.md', - '/media/editors/tinymce/langs/ro.js', - '/media/editors/tinymce/langs/ru.js', - '/media/editors/tinymce/langs/si-LK.js', - '/media/editors/tinymce/langs/sk.js', - '/media/editors/tinymce/langs/sl.js', - '/media/editors/tinymce/langs/sr.js', - '/media/editors/tinymce/langs/sv.js', - '/media/editors/tinymce/langs/sw.js', - '/media/editors/tinymce/langs/sy.js', - '/media/editors/tinymce/langs/ta.js', - '/media/editors/tinymce/langs/th.js', - '/media/editors/tinymce/langs/tr.js', - '/media/editors/tinymce/langs/ug.js', - '/media/editors/tinymce/langs/uk.js', - '/media/editors/tinymce/langs/vi.js', - '/media/editors/tinymce/langs/zh-CN.js', - '/media/editors/tinymce/langs/zh-TW.js', - '/media/editors/tinymce/license.txt', - '/media/editors/tinymce/plugins/advlist/plugin.min.js', - '/media/editors/tinymce/plugins/anchor/plugin.min.js', - '/media/editors/tinymce/plugins/autolink/plugin.min.js', - '/media/editors/tinymce/plugins/autoresize/plugin.min.js', - '/media/editors/tinymce/plugins/autosave/plugin.min.js', - '/media/editors/tinymce/plugins/bbcode/plugin.min.js', - '/media/editors/tinymce/plugins/charmap/plugin.min.js', - '/media/editors/tinymce/plugins/code/plugin.min.js', - '/media/editors/tinymce/plugins/codesample/css/prism.css', - '/media/editors/tinymce/plugins/codesample/plugin.min.js', - '/media/editors/tinymce/plugins/colorpicker/plugin.min.js', - '/media/editors/tinymce/plugins/contextmenu/plugin.min.js', - '/media/editors/tinymce/plugins/directionality/plugin.min.js', - '/media/editors/tinymce/plugins/emoticons/img/smiley-cool.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-cry.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-embarassed.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-foot-in-mouth.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-frown.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-innocent.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-kiss.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-laughing.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-money-mouth.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-sealed.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-smile.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-surprised.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-tongue-out.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-undecided.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-wink.gif', - '/media/editors/tinymce/plugins/emoticons/img/smiley-yell.gif', - '/media/editors/tinymce/plugins/emoticons/plugin.min.js', - '/media/editors/tinymce/plugins/example/dialog.html', - '/media/editors/tinymce/plugins/example/plugin.min.js', - '/media/editors/tinymce/plugins/example_dependency/plugin.min.js', - '/media/editors/tinymce/plugins/fullpage/plugin.min.js', - '/media/editors/tinymce/plugins/fullscreen/plugin.min.js', - '/media/editors/tinymce/plugins/hr/plugin.min.js', - '/media/editors/tinymce/plugins/image/plugin.min.js', - '/media/editors/tinymce/plugins/imagetools/plugin.min.js', - '/media/editors/tinymce/plugins/importcss/plugin.min.js', - '/media/editors/tinymce/plugins/insertdatetime/plugin.min.js', - '/media/editors/tinymce/plugins/layer/plugin.min.js', - '/media/editors/tinymce/plugins/legacyoutput/plugin.min.js', - '/media/editors/tinymce/plugins/link/plugin.min.js', - '/media/editors/tinymce/plugins/lists/plugin.min.js', - '/media/editors/tinymce/plugins/media/plugin.min.js', - '/media/editors/tinymce/plugins/nonbreaking/plugin.min.js', - '/media/editors/tinymce/plugins/noneditable/plugin.min.js', - '/media/editors/tinymce/plugins/pagebreak/plugin.min.js', - '/media/editors/tinymce/plugins/paste/plugin.min.js', - '/media/editors/tinymce/plugins/preview/plugin.min.js', - '/media/editors/tinymce/plugins/print/plugin.min.js', - '/media/editors/tinymce/plugins/save/plugin.min.js', - '/media/editors/tinymce/plugins/searchreplace/plugin.min.js', - '/media/editors/tinymce/plugins/spellchecker/plugin.min.js', - '/media/editors/tinymce/plugins/tabfocus/plugin.min.js', - '/media/editors/tinymce/plugins/table/plugin.min.js', - '/media/editors/tinymce/plugins/template/plugin.min.js', - '/media/editors/tinymce/plugins/textcolor/plugin.min.js', - '/media/editors/tinymce/plugins/textpattern/plugin.min.js', - '/media/editors/tinymce/plugins/toc/plugin.min.js', - '/media/editors/tinymce/plugins/visualblocks/css/visualblocks.css', - '/media/editors/tinymce/plugins/visualblocks/plugin.min.js', - '/media/editors/tinymce/plugins/visualchars/plugin.min.js', - '/media/editors/tinymce/plugins/wordcount/plugin.min.js', - '/media/editors/tinymce/skins/lightgray/content.inline.min.css', - '/media/editors/tinymce/skins/lightgray/content.min.css', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.eot', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.svg', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.ttf', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.woff', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce.eot', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce.svg', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce.ttf', - '/media/editors/tinymce/skins/lightgray/fonts/tinymce.woff', - '/media/editors/tinymce/skins/lightgray/img/anchor.gif', - '/media/editors/tinymce/skins/lightgray/img/loader.gif', - '/media/editors/tinymce/skins/lightgray/img/object.gif', - '/media/editors/tinymce/skins/lightgray/img/trans.gif', - '/media/editors/tinymce/skins/lightgray/skin.ie7.min.css', - '/media/editors/tinymce/skins/lightgray/skin.min.css', - '/media/editors/tinymce/templates/layout1.html', - '/media/editors/tinymce/templates/snippet1.html', - '/media/editors/tinymce/themes/modern/theme.min.js', - '/media/editors/tinymce/tinymce.min.js', - '/media/jui/css/bootstrap-extended.css', - '/media/jui/css/bootstrap-responsive.css', - '/media/jui/css/bootstrap-responsive.min.css', - '/media/jui/css/bootstrap-rtl.css', - '/media/jui/css/bootstrap-tooltip-extended.css', - '/media/jui/css/bootstrap.css', - '/media/jui/css/bootstrap.min.css', - '/media/jui/css/chosen-sprite.png', - '/media/jui/css/chosen-sprite@2x.png', - '/media/jui/css/chosen.css', - '/media/jui/css/icomoon.css', - '/media/jui/css/jquery.minicolors.css', - '/media/jui/css/jquery.searchtools.css', - '/media/jui/css/jquery.simplecolors.css', - '/media/jui/css/sortablelist.css', - '/media/jui/fonts/IcoMoon.dev.commented.svg', - '/media/jui/fonts/IcoMoon.dev.svg', - '/media/jui/fonts/IcoMoon.eot', - '/media/jui/fonts/IcoMoon.svg', - '/media/jui/fonts/IcoMoon.ttf', - '/media/jui/fonts/IcoMoon.woff', - '/media/jui/fonts/icomoon-license.txt', - '/media/jui/images/ajax-loader.gif', - '/media/jui/img/ajax-loader.gif', - '/media/jui/img/alpha.png', - '/media/jui/img/bg-overlay.png', - '/media/jui/img/glyphicons-halflings-white.png', - '/media/jui/img/glyphicons-halflings.png', - '/media/jui/img/hue.png', - '/media/jui/img/joomla.png', - '/media/jui/img/jquery.minicolors.png', - '/media/jui/img/saturation.png', - '/media/jui/js/ajax-chosen.js', - '/media/jui/js/ajax-chosen.min.js', - '/media/jui/js/bootstrap-tooltip-extended.js', - '/media/jui/js/bootstrap-tooltip-extended.min.js', - '/media/jui/js/bootstrap.js', - '/media/jui/js/bootstrap.min.js', - '/media/jui/js/chosen.jquery.js', - '/media/jui/js/chosen.jquery.min.js', - '/media/jui/js/cms-uncompressed.js', - '/media/jui/js/cms.js', - '/media/jui/js/fielduser.js', - '/media/jui/js/fielduser.min.js', - '/media/jui/js/html5-uncompressed.js', - '/media/jui/js/html5.js', - '/media/jui/js/icomoon-lte-ie7.js', - '/media/jui/js/jquery-migrate.js', - '/media/jui/js/jquery-migrate.min.js', - '/media/jui/js/jquery-noconflict.js', - '/media/jui/js/jquery.autocomplete.js', - '/media/jui/js/jquery.autocomplete.min.js', - '/media/jui/js/jquery.js', - '/media/jui/js/jquery.min.js', - '/media/jui/js/jquery.minicolors.js', - '/media/jui/js/jquery.minicolors.min.js', - '/media/jui/js/jquery.searchtools.js', - '/media/jui/js/jquery.searchtools.min.js', - '/media/jui/js/jquery.simplecolors.js', - '/media/jui/js/jquery.simplecolors.min.js', - '/media/jui/js/jquery.ui.core.js', - '/media/jui/js/jquery.ui.core.min.js', - '/media/jui/js/jquery.ui.sortable.js', - '/media/jui/js/jquery.ui.sortable.min.js', - '/media/jui/js/sortablelist.js', - '/media/jui/js/treeselectmenu.jquery.js', - '/media/jui/js/treeselectmenu.jquery.min.js', - '/media/jui/less/accordion.less', - '/media/jui/less/alerts.less', - '/media/jui/less/bootstrap-extended.less', - '/media/jui/less/bootstrap-rtl.less', - '/media/jui/less/bootstrap.less', - '/media/jui/less/breadcrumbs.less', - '/media/jui/less/button-groups.less', - '/media/jui/less/buttons.less', - '/media/jui/less/carousel.less', - '/media/jui/less/close.less', - '/media/jui/less/code.less', - '/media/jui/less/component-animations.less', - '/media/jui/less/dropdowns.less', - '/media/jui/less/forms.less', - '/media/jui/less/grid.less', - '/media/jui/less/hero-unit.less', - '/media/jui/less/icomoon.less', - '/media/jui/less/labels-badges.less', - '/media/jui/less/layouts.less', - '/media/jui/less/media.less', - '/media/jui/less/mixins.less', - '/media/jui/less/modals.joomla.less', - '/media/jui/less/modals.less', - '/media/jui/less/navbar.less', - '/media/jui/less/navs.less', - '/media/jui/less/pager.less', - '/media/jui/less/pagination.less', - '/media/jui/less/popovers.less', - '/media/jui/less/progress-bars.less', - '/media/jui/less/reset.less', - '/media/jui/less/responsive-1200px-min.less', - '/media/jui/less/responsive-767px-max.joomla.less', - '/media/jui/less/responsive-767px-max.less', - '/media/jui/less/responsive-768px-979px.less', - '/media/jui/less/responsive-navbar.less', - '/media/jui/less/responsive-utilities.less', - '/media/jui/less/responsive.less', - '/media/jui/less/scaffolding.less', - '/media/jui/less/sprites.less', - '/media/jui/less/tables.less', - '/media/jui/less/thumbnails.less', - '/media/jui/less/tooltip.less', - '/media/jui/less/type.less', - '/media/jui/less/utilities.less', - '/media/jui/less/variables.less', - '/media/jui/less/wells.less', - '/media/media/css/background.png', - '/media/media/css/bigplay.fw.png', - '/media/media/css/bigplay.png', - '/media/media/css/bigplay.svg', - '/media/media/css/controls-ted.png', - '/media/media/css/controls-wmp-bg.png', - '/media/media/css/controls-wmp.png', - '/media/media/css/controls.fw.png', - '/media/media/css/controls.png', - '/media/media/css/controls.svg', - '/media/media/css/jumpforward.png', - '/media/media/css/loading.gif', - '/media/media/css/mediaelementplayer.css', - '/media/media/css/mediaelementplayer.min.css', - '/media/media/css/medialist-details.css', - '/media/media/css/medialist-details_rtl.css', - '/media/media/css/medialist-thumbs.css', - '/media/media/css/medialist-thumbs_rtl.css', - '/media/media/css/mediamanager.css', - '/media/media/css/mediamanager_rtl.css', - '/media/media/css/mejs-skins.css', - '/media/media/css/popup-imagelist.css', - '/media/media/css/popup-imagelist_rtl.css', - '/media/media/css/popup-imagemanager.css', - '/media/media/css/popup-imagemanager_rtl.css', - '/media/media/css/skipback.png', - '/media/media/images/bar.gif', - '/media/media/images/con_info.png', - '/media/media/images/delete.png', - '/media/media/images/dots.gif', - '/media/media/images/failed.png', - '/media/media/images/folder.gif', - '/media/media/images/folder.png', - '/media/media/images/folder_sm.png', - '/media/media/images/folderup_16.png', - '/media/media/images/folderup_32.png', - '/media/media/images/mime-icon-16/avi.png', - '/media/media/images/mime-icon-16/doc.png', - '/media/media/images/mime-icon-16/mov.png', - '/media/media/images/mime-icon-16/mp3.png', - '/media/media/images/mime-icon-16/mp4.png', - '/media/media/images/mime-icon-16/odc.png', - '/media/media/images/mime-icon-16/odd.png', - '/media/media/images/mime-icon-16/odt.png', - '/media/media/images/mime-icon-16/ogg.png', - '/media/media/images/mime-icon-16/pdf.png', - '/media/media/images/mime-icon-16/ppt.png', - '/media/media/images/mime-icon-16/rar.png', - '/media/media/images/mime-icon-16/rtf.png', - '/media/media/images/mime-icon-16/svg.png', - '/media/media/images/mime-icon-16/sxd.png', - '/media/media/images/mime-icon-16/tar.png', - '/media/media/images/mime-icon-16/tgz.png', - '/media/media/images/mime-icon-16/wma.png', - '/media/media/images/mime-icon-16/wmv.png', - '/media/media/images/mime-icon-16/xls.png', - '/media/media/images/mime-icon-16/zip.png', - '/media/media/images/mime-icon-32/avi.png', - '/media/media/images/mime-icon-32/doc.png', - '/media/media/images/mime-icon-32/mov.png', - '/media/media/images/mime-icon-32/mp3.png', - '/media/media/images/mime-icon-32/mp4.png', - '/media/media/images/mime-icon-32/odc.png', - '/media/media/images/mime-icon-32/odd.png', - '/media/media/images/mime-icon-32/odt.png', - '/media/media/images/mime-icon-32/ogg.png', - '/media/media/images/mime-icon-32/pdf.png', - '/media/media/images/mime-icon-32/ppt.png', - '/media/media/images/mime-icon-32/rar.png', - '/media/media/images/mime-icon-32/rtf.png', - '/media/media/images/mime-icon-32/svg.png', - '/media/media/images/mime-icon-32/sxd.png', - '/media/media/images/mime-icon-32/tar.png', - '/media/media/images/mime-icon-32/tgz.png', - '/media/media/images/mime-icon-32/wma.png', - '/media/media/images/mime-icon-32/wmv.png', - '/media/media/images/mime-icon-32/xls.png', - '/media/media/images/mime-icon-32/zip.png', - '/media/media/images/progress.gif', - '/media/media/images/remove.png', - '/media/media/images/success.png', - '/media/media/images/upload.png', - '/media/media/images/uploading.png', - '/media/media/js/flashmediaelement-cdn.swf', - '/media/media/js/flashmediaelement.swf', - '/media/media/js/mediaelement-and-player.js', - '/media/media/js/mediaelement-and-player.min.js', - '/media/media/js/mediafield-mootools.js', - '/media/media/js/mediafield-mootools.min.js', - '/media/media/js/mediafield.js', - '/media/media/js/mediafield.min.js', - '/media/media/js/mediamanager.js', - '/media/media/js/mediamanager.min.js', - '/media/media/js/popup-imagemanager.js', - '/media/media/js/popup-imagemanager.min.js', - '/media/media/js/silverlightmediaelement.xap', - '/media/overrider/css/overrider.css', - '/media/overrider/js/overrider.js', - '/media/overrider/js/overrider.min.js', - '/media/plg_system_highlight/highlight.css', - '/media/plg_twofactorauth_totp/js/qrcode.js', - '/media/plg_twofactorauth_totp/js/qrcode.min.js', - '/media/plg_twofactorauth_totp/js/qrcode_SJIS.js', - '/media/plg_twofactorauth_totp/js/qrcode_UTF8.js', - '/media/system/css/adminlist.css', - '/media/system/css/jquery.Jcrop.min.css', - '/media/system/css/modal.css', - '/media/system/css/system.css', - '/media/system/js/associations-edit-uncompressed.js', - '/media/system/js/associations-edit.js', - '/media/system/js/calendar-setup-uncompressed.js', - '/media/system/js/calendar-setup.js', - '/media/system/js/calendar-uncompressed.js', - '/media/system/js/calendar.js', - '/media/system/js/caption-uncompressed.js', - '/media/system/js/caption.js', - '/media/system/js/color-field-adv-init.js', - '/media/system/js/color-field-adv-init.min.js', - '/media/system/js/color-field-init.js', - '/media/system/js/color-field-init.min.js', - '/media/system/js/combobox-uncompressed.js', - '/media/system/js/combobox.js', - '/media/system/js/core-uncompressed.js', - '/media/system/js/fields/calendar-locales/af.js', - '/media/system/js/fields/calendar-locales/ar.js', - '/media/system/js/fields/calendar-locales/bg.js', - '/media/system/js/fields/calendar-locales/bn.js', - '/media/system/js/fields/calendar-locales/bs.js', - '/media/system/js/fields/calendar-locales/ca.js', - '/media/system/js/fields/calendar-locales/cs.js', - '/media/system/js/fields/calendar-locales/cy.js', - '/media/system/js/fields/calendar-locales/da.js', - '/media/system/js/fields/calendar-locales/de.js', - '/media/system/js/fields/calendar-locales/el.js', - '/media/system/js/fields/calendar-locales/en.js', - '/media/system/js/fields/calendar-locales/es.js', - '/media/system/js/fields/calendar-locales/eu.js', - '/media/system/js/fields/calendar-locales/fa-ir.js', - '/media/system/js/fields/calendar-locales/fi.js', - '/media/system/js/fields/calendar-locales/fr.js', - '/media/system/js/fields/calendar-locales/ga.js', - '/media/system/js/fields/calendar-locales/hr.js', - '/media/system/js/fields/calendar-locales/hu.js', - '/media/system/js/fields/calendar-locales/it.js', - '/media/system/js/fields/calendar-locales/ja.js', - '/media/system/js/fields/calendar-locales/ka.js', - '/media/system/js/fields/calendar-locales/kk.js', - '/media/system/js/fields/calendar-locales/ko.js', - '/media/system/js/fields/calendar-locales/lt.js', - '/media/system/js/fields/calendar-locales/mk.js', - '/media/system/js/fields/calendar-locales/nb.js', - '/media/system/js/fields/calendar-locales/nl.js', - '/media/system/js/fields/calendar-locales/pl.js', - '/media/system/js/fields/calendar-locales/prs-af.js', - '/media/system/js/fields/calendar-locales/pt.js', - '/media/system/js/fields/calendar-locales/ru.js', - '/media/system/js/fields/calendar-locales/sk.js', - '/media/system/js/fields/calendar-locales/sl.js', - '/media/system/js/fields/calendar-locales/sr-rs.js', - '/media/system/js/fields/calendar-locales/sr-yu.js', - '/media/system/js/fields/calendar-locales/sv.js', - '/media/system/js/fields/calendar-locales/sw.js', - '/media/system/js/fields/calendar-locales/ta.js', - '/media/system/js/fields/calendar-locales/th.js', - '/media/system/js/fields/calendar-locales/uk.js', - '/media/system/js/fields/calendar-locales/zh-CN.js', - '/media/system/js/fields/calendar-locales/zh-TW.js', - '/media/system/js/frontediting-uncompressed.js', - '/media/system/js/frontediting.js', - '/media/system/js/helpsite.js', - '/media/system/js/highlighter-uncompressed.js', - '/media/system/js/highlighter.js', - '/media/system/js/html5fallback-uncompressed.js', - '/media/system/js/html5fallback.js', - '/media/system/js/jquery.Jcrop.js', - '/media/system/js/jquery.Jcrop.min.js', - '/media/system/js/keepalive-uncompressed.js', - '/media/system/js/modal-fields-uncompressed.js', - '/media/system/js/modal-fields.js', - '/media/system/js/modal-uncompressed.js', - '/media/system/js/modal.js', - '/media/system/js/moduleorder.js', - '/media/system/js/mootools-core-uncompressed.js', - '/media/system/js/mootools-core.js', - '/media/system/js/mootools-more-uncompressed.js', - '/media/system/js/mootools-more.js', - '/media/system/js/mootree-uncompressed.js', - '/media/system/js/mootree.js', - '/media/system/js/multiselect-uncompressed.js', - '/media/system/js/passwordstrength.js', - '/media/system/js/permissions-uncompressed.js', - '/media/system/js/permissions.js', - '/media/system/js/polyfill.classlist-uncompressed.js', - '/media/system/js/polyfill.classlist.js', - '/media/system/js/polyfill.event-uncompressed.js', - '/media/system/js/polyfill.event.js', - '/media/system/js/polyfill.filter-uncompressed.js', - '/media/system/js/polyfill.filter.js', - '/media/system/js/polyfill.map-uncompressed.js', - '/media/system/js/polyfill.map.js', - '/media/system/js/polyfill.xpath-uncompressed.js', - '/media/system/js/polyfill.xpath.js', - '/media/system/js/progressbar-uncompressed.js', - '/media/system/js/progressbar.js', - '/media/system/js/punycode-uncompressed.js', - '/media/system/js/punycode.js', - '/media/system/js/repeatable-uncompressed.js', - '/media/system/js/repeatable.js', - '/media/system/js/sendtestmail-uncompressed.js', - '/media/system/js/sendtestmail.js', - '/media/system/js/subform-repeatable-uncompressed.js', - '/media/system/js/subform-repeatable.js', - '/media/system/js/switcher-uncompressed.js', - '/media/system/js/switcher.js', - '/media/system/js/tabs-state-uncompressed.js', - '/media/system/js/tabs-state.js', - '/media/system/js/tabs.js', - '/media/system/js/validate-uncompressed.js', - '/media/system/js/validate.js', - '/modules/mod_articles_archive/helper.php', - '/modules/mod_articles_categories/helper.php', - '/modules/mod_articles_category/helper.php', - '/modules/mod_articles_latest/helper.php', - '/modules/mod_articles_news/helper.php', - '/modules/mod_articles_popular/helper.php', - '/modules/mod_banners/helper.php', - '/modules/mod_breadcrumbs/helper.php', - '/modules/mod_feed/helper.php', - '/modules/mod_finder/helper.php', - '/modules/mod_languages/helper.php', - '/modules/mod_login/helper.php', - '/modules/mod_menu/helper.php', - '/modules/mod_random_image/helper.php', - '/modules/mod_related_items/helper.php', - '/modules/mod_stats/helper.php', - '/modules/mod_syndicate/helper.php', - '/modules/mod_tags_popular/helper.php', - '/modules/mod_tags_similar/helper.php', - '/modules/mod_users_latest/helper.php', - '/modules/mod_whosonline/helper.php', - '/modules/mod_wrapper/helper.php', - '/plugins/authentication/gmail/gmail.php', - '/plugins/authentication/gmail/gmail.xml', - '/plugins/captcha/recaptcha/postinstall/actions.php', - '/plugins/content/confirmconsent/fields/consentbox.php', - '/plugins/editors/codemirror/fonts.php', - '/plugins/editors/codemirror/layouts/editors/codemirror/init.php', - '/plugins/editors/tinymce/field/skins.php', - '/plugins/editors/tinymce/field/tinymcebuilder.php', - '/plugins/editors/tinymce/field/uploaddirs.php', - '/plugins/editors/tinymce/form/setoptions.xml', - '/plugins/quickicon/joomlaupdate/joomlaupdate.php', - '/plugins/system/languagecode/language/en-GB/en-GB.plg_system_languagecode.ini', - '/plugins/system/languagecode/language/en-GB/en-GB.plg_system_languagecode.sys.ini', - '/plugins/system/p3p/p3p.php', - '/plugins/system/p3p/p3p.xml', - '/plugins/system/privacyconsent/field/privacy.php', - '/plugins/system/privacyconsent/privacyconsent/privacyconsent.xml', - '/plugins/system/stats/field/base.php', - '/plugins/system/stats/field/data.php', - '/plugins/system/stats/field/uniqueid.php', - '/plugins/user/profile/field/dob.php', - '/plugins/user/profile/field/tos.php', - '/plugins/user/profile/profiles/profile.xml', - '/plugins/user/terms/field/terms.php', - '/plugins/user/terms/terms/terms.xml', - '/templates/beez3/component.php', - '/templates/beez3/css/general.css', - '/templates/beez3/css/ie7only.css', - '/templates/beez3/css/ieonly.css', - '/templates/beez3/css/layout.css', - '/templates/beez3/css/nature.css', - '/templates/beez3/css/nature_rtl.css', - '/templates/beez3/css/personal.css', - '/templates/beez3/css/personal_rtl.css', - '/templates/beez3/css/position.css', - '/templates/beez3/css/print.css', - '/templates/beez3/css/red.css', - '/templates/beez3/css/template.css', - '/templates/beez3/css/template_rtl.css', - '/templates/beez3/css/turq.css', - '/templates/beez3/css/turq.less', - '/templates/beez3/error.php', - '/templates/beez3/favicon.ico', - '/templates/beez3/html/com_contact/categories/default.php', - '/templates/beez3/html/com_contact/categories/default_items.php', - '/templates/beez3/html/com_contact/category/default.php', - '/templates/beez3/html/com_contact/category/default_children.php', - '/templates/beez3/html/com_contact/category/default_items.php', - '/templates/beez3/html/com_contact/contact/default.php', - '/templates/beez3/html/com_contact/contact/default_address.php', - '/templates/beez3/html/com_contact/contact/default_articles.php', - '/templates/beez3/html/com_contact/contact/default_form.php', - '/templates/beez3/html/com_contact/contact/default_links.php', - '/templates/beez3/html/com_contact/contact/default_profile.php', - '/templates/beez3/html/com_contact/contact/default_user_custom_fields.php', - '/templates/beez3/html/com_contact/contact/encyclopedia.php', - '/templates/beez3/html/com_content/archive/default.php', - '/templates/beez3/html/com_content/archive/default_items.php', - '/templates/beez3/html/com_content/article/default.php', - '/templates/beez3/html/com_content/article/default_links.php', - '/templates/beez3/html/com_content/categories/default.php', - '/templates/beez3/html/com_content/categories/default_items.php', - '/templates/beez3/html/com_content/category/blog.php', - '/templates/beez3/html/com_content/category/blog_children.php', - '/templates/beez3/html/com_content/category/blog_item.php', - '/templates/beez3/html/com_content/category/blog_links.php', - '/templates/beez3/html/com_content/category/default.php', - '/templates/beez3/html/com_content/category/default_articles.php', - '/templates/beez3/html/com_content/category/default_children.php', - '/templates/beez3/html/com_content/featured/default.php', - '/templates/beez3/html/com_content/featured/default_item.php', - '/templates/beez3/html/com_content/featured/default_links.php', - '/templates/beez3/html/com_content/form/edit.php', - '/templates/beez3/html/com_newsfeeds/categories/default.php', - '/templates/beez3/html/com_newsfeeds/categories/default_items.php', - '/templates/beez3/html/com_newsfeeds/category/default.php', - '/templates/beez3/html/com_newsfeeds/category/default_children.php', - '/templates/beez3/html/com_newsfeeds/category/default_items.php', - '/templates/beez3/html/com_weblinks/categories/default.php', - '/templates/beez3/html/com_weblinks/categories/default_items.php', - '/templates/beez3/html/com_weblinks/category/default.php', - '/templates/beez3/html/com_weblinks/category/default_children.php', - '/templates/beez3/html/com_weblinks/category/default_items.php', - '/templates/beez3/html/com_weblinks/form/edit.php', - '/templates/beez3/html/layouts/joomla/system/message.php', - '/templates/beez3/html/mod_breadcrumbs/default.php', - '/templates/beez3/html/mod_languages/default.php', - '/templates/beez3/html/mod_login/default.php', - '/templates/beez3/html/mod_login/default_logout.php', - '/templates/beez3/html/modules.php', - '/templates/beez3/images/all_bg.gif', - '/templates/beez3/images/arrow.png', - '/templates/beez3/images/arrow2_grey.png', - '/templates/beez3/images/arrow_white_grey.png', - '/templates/beez3/images/blog_more.gif', - '/templates/beez3/images/blog_more_hover.gif', - '/templates/beez3/images/close.png', - '/templates/beez3/images/content_bg.gif', - '/templates/beez3/images/footer_bg.gif', - '/templates/beez3/images/footer_bg.png', - '/templates/beez3/images/header-bg.gif', - '/templates/beez3/images/minus.png', - '/templates/beez3/images/nature/arrow1.gif', - '/templates/beez3/images/nature/arrow1_rtl.gif', - '/templates/beez3/images/nature/arrow2.gif', - '/templates/beez3/images/nature/arrow2_grey.png', - '/templates/beez3/images/nature/arrow2_rtl.gif', - '/templates/beez3/images/nature/arrow_nav.gif', - '/templates/beez3/images/nature/arrow_small.png', - '/templates/beez3/images/nature/arrow_small_rtl.png', - '/templates/beez3/images/nature/blog_more.gif', - '/templates/beez3/images/nature/box.png', - '/templates/beez3/images/nature/box1.png', - '/templates/beez3/images/nature/grey_bg.png', - '/templates/beez3/images/nature/headingback.png', - '/templates/beez3/images/nature/karo.gif', - '/templates/beez3/images/nature/level4.png', - '/templates/beez3/images/nature/nav_level1_a.gif', - '/templates/beez3/images/nature/nav_level_1.gif', - '/templates/beez3/images/nature/pfeil.gif', - '/templates/beez3/images/nature/readmore_arrow.png', - '/templates/beez3/images/nature/searchbutton.png', - '/templates/beez3/images/nature/tabs.gif', - '/templates/beez3/images/nav_level_1.gif', - '/templates/beez3/images/news.gif', - '/templates/beez3/images/personal/arrow2_grey.jpg', - '/templates/beez3/images/personal/arrow2_grey.png', - '/templates/beez3/images/personal/bg2.png', - '/templates/beez3/images/personal/button.png', - '/templates/beez3/images/personal/dot.png', - '/templates/beez3/images/personal/ecke.gif', - '/templates/beez3/images/personal/footer.jpg', - '/templates/beez3/images/personal/grey_bg.png', - '/templates/beez3/images/personal/navi_active.png', - '/templates/beez3/images/personal/personal2.png', - '/templates/beez3/images/personal/readmore_arrow.png', - '/templates/beez3/images/personal/readmore_arrow_hover.png', - '/templates/beez3/images/personal/tabs_back.png', - '/templates/beez3/images/plus.png', - '/templates/beez3/images/req.png', - '/templates/beez3/images/slider_minus.png', - '/templates/beez3/images/slider_minus_rtl.png', - '/templates/beez3/images/slider_plus.png', - '/templates/beez3/images/slider_plus_rtl.png', - '/templates/beez3/images/system/arrow.png', - '/templates/beez3/images/system/arrow_rtl.png', - '/templates/beez3/images/system/calendar.png', - '/templates/beez3/images/system/j_button2_blank.png', - '/templates/beez3/images/system/j_button2_image.png', - '/templates/beez3/images/system/j_button2_left.png', - '/templates/beez3/images/system/j_button2_pagebreak.png', - '/templates/beez3/images/system/j_button2_readmore.png', - '/templates/beez3/images/system/notice-alert.png', - '/templates/beez3/images/system/notice-alert_rtl.png', - '/templates/beez3/images/system/notice-info.png', - '/templates/beez3/images/system/notice-info_rtl.png', - '/templates/beez3/images/system/notice-note.png', - '/templates/beez3/images/system/notice-note_rtl.png', - '/templates/beez3/images/system/selector-arrow.png', - '/templates/beez3/images/table_footer.gif', - '/templates/beez3/images/trans.gif', - '/templates/beez3/index.php', - '/templates/beez3/javascript/hide.js', - '/templates/beez3/javascript/md_stylechanger.js', - '/templates/beez3/javascript/respond.js', - '/templates/beez3/javascript/respond.src.js', - '/templates/beez3/javascript/template.js', - '/templates/beez3/jsstrings.php', - '/templates/beez3/language/en-GB/en-GB.tpl_beez3.ini', - '/templates/beez3/language/en-GB/en-GB.tpl_beez3.sys.ini', - '/templates/beez3/templateDetails.xml', - '/templates/beez3/template_preview.png', - '/templates/beez3/template_thumbnail.png', - '/templates/protostar/component.php', - '/templates/protostar/css/offline.css', - '/templates/protostar/css/template.css', - '/templates/protostar/error.php', - '/templates/protostar/favicon.ico', - '/templates/protostar/html/com_media/imageslist/default_folder.php', - '/templates/protostar/html/com_media/imageslist/default_image.php', - '/templates/protostar/html/layouts/joomla/form/field/contenthistory.php', - '/templates/protostar/html/layouts/joomla/form/field/media.php', - '/templates/protostar/html/layouts/joomla/form/field/user.php', - '/templates/protostar/html/layouts/joomla/system/message.php', - '/templates/protostar/html/modules.php', - '/templates/protostar/html/pagination.php', - '/templates/protostar/images/logo.png', - '/templates/protostar/images/system/rating_star.png', - '/templates/protostar/images/system/rating_star_blank.png', - '/templates/protostar/images/system/sort_asc.png', - '/templates/protostar/images/system/sort_desc.png', - '/templates/protostar/img/glyphicons-halflings-white.png', - '/templates/protostar/img/glyphicons-halflings.png', - '/templates/protostar/index.php', - '/templates/protostar/js/application.js', - '/templates/protostar/js/classes.js', - '/templates/protostar/js/template.js', - '/templates/protostar/language/en-GB/en-GB.tpl_protostar.ini', - '/templates/protostar/language/en-GB/en-GB.tpl_protostar.sys.ini', - '/templates/protostar/less/icomoon.less', - '/templates/protostar/less/template.less', - '/templates/protostar/less/template_rtl.less', - '/templates/protostar/less/variables.less', - '/templates/protostar/offline.php', - '/templates/protostar/templateDetails.xml', - '/templates/protostar/template_preview.png', - '/templates/protostar/template_thumbnail.png', - '/templates/system/css/system.css', - '/templates/system/css/toolbar.css', - '/templates/system/html/modules.php', - '/templates/system/images/calendar.png', - '/templates/system/images/j_button2_blank.png', - '/templates/system/images/j_button2_image.png', - '/templates/system/images/j_button2_left.png', - '/templates/system/images/j_button2_pagebreak.png', - '/templates/system/images/j_button2_readmore.png', - '/templates/system/images/j_button2_right.png', - '/templates/system/images/selector-arrow.png', - // 4.0 from Beta 1 to Beta 2 - '/administrator/components/com_finder/src/Indexer/Driver/Mysql.php', - '/administrator/components/com_finder/src/Indexer/Driver/Postgresql.php', - '/administrator/components/com_workflow/access.xml', - '/api/components/com_installer/src/Controller/LanguagesController.php', - '/api/components/com_installer/src/View/Languages/JsonapiView.php', - '/libraries/vendor/joomla/controller/LICENSE', - '/libraries/vendor/joomla/controller/src/AbstractController.php', - '/libraries/vendor/joomla/controller/src/ControllerInterface.php', - '/media/com_users/js/admin-users-user.es6.js', - '/media/com_users/js/admin-users-user.es6.min.js', - '/media/com_users/js/admin-users-user.es6.min.js.gz', - '/media/com_users/js/admin-users-user.js', - '/media/com_users/js/admin-users-user.min.js', - '/media/com_users/js/admin-users-user.min.js.gz', - // 4.0 from Beta 2 to Beta 3 - '/administrator/templates/atum/images/logo-blue.svg', - '/administrator/templates/atum/images/logo-joomla-blue.svg', - '/administrator/templates/atum/images/logo-joomla-white.svg', - '/administrator/templates/atum/images/logo.svg', - // 4.0 from Beta 3 to Beta 4 - '/components/com_config/src/Model/CmsModel.php', - // 4.0 from Beta 4 to Beta 5 - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-06-11.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-04-18.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-06-11.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-04-18.sql', - '/administrator/components/com_config/tmpl/application/default_system.php', - '/administrator/language/en-GB/plg_content_imagelazyload.sys.ini', - '/administrator/language/en-GB/plg_fields_image.ini', - '/administrator/language/en-GB/plg_fields_image.sys.ini', - '/administrator/templates/atum/scss/vendor/bootstrap/_nav.scss', - '/libraries/vendor/spomky-labs/base64url/phpstan.neon', - '/media/plg_system_webauthn/images/webauthn-black.png', - '/media/plg_system_webauthn/images/webauthn-color.png', - '/media/plg_system_webauthn/images/webauthn-white.png', - '/media/system/css/system.min.css', - '/media/system/css/system.min.css.gz', - '/plugins/content/imagelazyload/imagelazyload.php', - '/plugins/content/imagelazyload/imagelazyload.xml', - '/templates/cassiopeia/html/layouts/chromes/cardGrey.php', - '/templates/cassiopeia/html/layouts/chromes/default.php', - '/templates/cassiopeia/scss/vendor/bootstrap/_card.scss', - // 4.0 from Beta 5 to Beta 6 - '/administrator/modules/mod_multilangstatus/src/Helper/MultilangstatusAdminHelper.php', - '/administrator/templates/atum/favicon.ico', - '/libraries/vendor/nyholm/psr7/phpstan.baseline.dist', - '/libraries/vendor/spomky-labs/base64url/.php_cs.dist', - '/libraries/vendor/spomky-labs/base64url/infection.json.dist', - '/media/layouts/js/joomla/html/batch/batch-language.es6.js', - '/media/layouts/js/joomla/html/batch/batch-language.es6.min.js', - '/media/layouts/js/joomla/html/batch/batch-language.es6.min.js.gz', - '/media/layouts/js/joomla/html/batch/batch-language.js', - '/media/layouts/js/joomla/html/batch/batch-language.min.js', - '/media/layouts/js/joomla/html/batch/batch-language.min.js.gz', - '/media/plg_system_webauthn/images/webauthn-black.svg', - '/media/plg_system_webauthn/images/webauthn-white.svg', - '/media/system/js/core.es6/ajax.es6', - '/media/system/js/core.es6/customevent.es6', - '/media/system/js/core.es6/event.es6', - '/media/system/js/core.es6/form.es6', - '/media/system/js/core.es6/message.es6', - '/media/system/js/core.es6/options.es6', - '/media/system/js/core.es6/text.es6', - '/media/system/js/core.es6/token.es6', - '/media/system/js/core.es6/webcomponent.es6', - '/templates/cassiopeia/favicon.ico', - '/templates/cassiopeia/scss/_mixin.scss', - '/templates/cassiopeia/scss/_variables.scss', - '/templates/cassiopeia/scss/blocks/_demo-styling.scss', - // 4.0 from Beta 6 to Beta 7 - '/media/legacy/js/bootstrap-init.js', - '/media/legacy/js/bootstrap-init.min.js', - '/media/legacy/js/bootstrap-init.min.js.gz', - '/media/legacy/js/frontediting.js', - '/media/legacy/js/frontediting.min.js', - '/media/legacy/js/frontediting.min.js.gz', - '/media/vendor/bootstrap/js/bootstrap.bundle.js', - '/media/vendor/bootstrap/js/bootstrap.bundle.min.js', - '/media/vendor/bootstrap/js/bootstrap.bundle.min.js.gz', - '/media/vendor/bootstrap/js/bootstrap.bundle.min.js.map', - '/media/vendor/bootstrap/js/bootstrap.js', - '/media/vendor/bootstrap/js/bootstrap.min.js', - '/media/vendor/bootstrap/js/bootstrap.min.js.gz', - '/media/vendor/bootstrap/scss/_code.scss', - '/media/vendor/bootstrap/scss/_custom-forms.scss', - '/media/vendor/bootstrap/scss/_input-group.scss', - '/media/vendor/bootstrap/scss/_jumbotron.scss', - '/media/vendor/bootstrap/scss/_media.scss', - '/media/vendor/bootstrap/scss/_print.scss', - '/media/vendor/bootstrap/scss/mixins/_background-variant.scss', - '/media/vendor/bootstrap/scss/mixins/_badge.scss', - '/media/vendor/bootstrap/scss/mixins/_float.scss', - '/media/vendor/bootstrap/scss/mixins/_grid-framework.scss', - '/media/vendor/bootstrap/scss/mixins/_hover.scss', - '/media/vendor/bootstrap/scss/mixins/_nav-divider.scss', - '/media/vendor/bootstrap/scss/mixins/_screen-reader.scss', - '/media/vendor/bootstrap/scss/mixins/_size.scss', - '/media/vendor/bootstrap/scss/mixins/_table-row.scss', - '/media/vendor/bootstrap/scss/mixins/_text-emphasis.scss', - '/media/vendor/bootstrap/scss/mixins/_text-hide.scss', - '/media/vendor/bootstrap/scss/mixins/_visibility.scss', - '/media/vendor/bootstrap/scss/utilities/_align.scss', - '/media/vendor/bootstrap/scss/utilities/_background.scss', - '/media/vendor/bootstrap/scss/utilities/_borders.scss', - '/media/vendor/bootstrap/scss/utilities/_clearfix.scss', - '/media/vendor/bootstrap/scss/utilities/_display.scss', - '/media/vendor/bootstrap/scss/utilities/_embed.scss', - '/media/vendor/bootstrap/scss/utilities/_flex.scss', - '/media/vendor/bootstrap/scss/utilities/_float.scss', - '/media/vendor/bootstrap/scss/utilities/_interactions.scss', - '/media/vendor/bootstrap/scss/utilities/_overflow.scss', - '/media/vendor/bootstrap/scss/utilities/_position.scss', - '/media/vendor/bootstrap/scss/utilities/_screenreaders.scss', - '/media/vendor/bootstrap/scss/utilities/_shadows.scss', - '/media/vendor/bootstrap/scss/utilities/_sizing.scss', - '/media/vendor/bootstrap/scss/utilities/_spacing.scss', - '/media/vendor/bootstrap/scss/utilities/_stretched-link.scss', - '/media/vendor/bootstrap/scss/utilities/_text.scss', - '/media/vendor/bootstrap/scss/utilities/_visibility.scss', - '/media/vendor/skipto/css/SkipTo.css', - '/media/vendor/skipto/js/dropMenu.js', - // 4.0 from Beta 7 to RC 1 - '/administrator/components/com_admin/forms/profile.xml', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-07-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-09-22.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-09-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-10-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-10-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-03-18.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-04-25.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-05-31.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-06-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-10-10.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-02-24.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-06-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-06-26.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-07-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-08-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-09-12.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-10-18.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-01-05.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-01-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-02-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-03-31.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-05-05.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-06-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-07-02.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-07-14.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-07-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-08-03.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-08-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-08-21.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-14.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-23.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-24.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-25.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-26.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-27.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-29.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-10-13.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-10-29.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-11-07.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-11-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-08.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-22.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-29.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-04-11.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-04-16.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-05-21.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-09-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-09-22.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-12-08.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-12-19.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-02-28.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-04-11.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-04-20.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-01.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-04.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-07.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-10.sql', - '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-21.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-07-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-09-22.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-09-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-10-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-10-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-03-18.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-04-25.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-05-31.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-06-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-10-10.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-02-24.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-06-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-06-26.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-07-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-08-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-09-12.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-10-18.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-01-05.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-01-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-02-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-03-31.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-05-05.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-06-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-02.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-14.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-08-03.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-08-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-08-21.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-14.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-23.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-24.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-25.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-26.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-27.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-29.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-10-13.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-10-29.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-11-07.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-11-19.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-08.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-22.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-29.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-04-11.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-04-16.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-05-21.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-09-19.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-09-22.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-12-08.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-12-19.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-02-28.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-04-11.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-04-20.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-01.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-04.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-07.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-10.sql', - '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-21.sql', - '/administrator/components/com_admin/src/Controller/ProfileController.php', - '/administrator/components/com_admin/src/Model/ProfileModel.php', - '/administrator/components/com_admin/src/View/Profile/HtmlView.php', - '/administrator/components/com_admin/tmpl/profile/edit.php', - '/administrator/components/com_config/tmpl/application/default_ftp.php', - '/administrator/components/com_config/tmpl/application/default_ftplogin.php', - '/administrator/components/com_csp/access.xml', - '/administrator/components/com_csp/config.xml', - '/administrator/components/com_csp/csp.xml', - '/administrator/components/com_csp/forms/filter_reports.xml', - '/administrator/components/com_csp/services/provider.php', - '/administrator/components/com_csp/src/Controller/DisplayController.php', - '/administrator/components/com_csp/src/Controller/ReportsController.php', - '/administrator/components/com_csp/src/Helper/ReporterHelper.php', - '/administrator/components/com_csp/src/Model/ReportModel.php', - '/administrator/components/com_csp/src/Model/ReportsModel.php', - '/administrator/components/com_csp/src/Table/ReportTable.php', - '/administrator/components/com_csp/src/View/Reports/HtmlView.php', - '/administrator/components/com_csp/tmpl/reports/default.php', - '/administrator/components/com_csp/tmpl/reports/default.xml', - '/administrator/components/com_fields/src/Field/SubfieldstypeField.php', - '/administrator/components/com_installer/tmpl/installer/default_ftp.php', - '/administrator/components/com_joomlaupdate/src/Helper/Select.php', - '/administrator/language/en-GB/com_csp.ini', - '/administrator/language/en-GB/com_csp.sys.ini', - '/administrator/language/en-GB/plg_fields_subfields.ini', - '/administrator/language/en-GB/plg_fields_subfields.sys.ini', - '/administrator/templates/atum/Service/HTML/Atum.php', - '/components/com_csp/src/Controller/ReportController.php', - '/components/com_menus/src/Controller/DisplayController.php', - '/libraries/vendor/algo26-matthias/idna-convert/CODE_OF_CONDUCT.md', - '/libraries/vendor/algo26-matthias/idna-convert/UPGRADING.md', - '/libraries/vendor/algo26-matthias/idna-convert/docker-compose.yml', - '/libraries/vendor/beberlei/assert/phpstan-code.neon', - '/libraries/vendor/beberlei/assert/phpstan-tests.neon', - '/libraries/vendor/bin/generate-defuse-key', - '/libraries/vendor/bin/var-dump-server', - '/libraries/vendor/bin/yaml-lint', - '/libraries/vendor/brick/math/psalm-baseline.xml', - '/libraries/vendor/doctrine/inflector/phpstan.neon.dist', - '/libraries/vendor/jakeasmith/http_build_url/readme.md', - '/libraries/vendor/nyholm/psr7/src/LowercaseTrait.php', - '/libraries/vendor/ozdemirburak/iris/LICENSE.md', - '/libraries/vendor/ozdemirburak/iris/src/BaseColor.php', - '/libraries/vendor/ozdemirburak/iris/src/Color/Factory.php', - '/libraries/vendor/ozdemirburak/iris/src/Color/Hex.php', - '/libraries/vendor/ozdemirburak/iris/src/Color/Hsl.php', - '/libraries/vendor/ozdemirburak/iris/src/Color/Hsla.php', - '/libraries/vendor/ozdemirburak/iris/src/Color/Hsv.php', - '/libraries/vendor/ozdemirburak/iris/src/Color/Rgb.php', - '/libraries/vendor/ozdemirburak/iris/src/Color/Rgba.php', - '/libraries/vendor/ozdemirburak/iris/src/Exceptions/AmbiguousColorString.php', - '/libraries/vendor/ozdemirburak/iris/src/Exceptions/InvalidColorException.php', - '/libraries/vendor/ozdemirburak/iris/src/Helpers/DefinedColor.php', - '/libraries/vendor/ozdemirburak/iris/src/Traits/AlphaTrait.php', - '/libraries/vendor/ozdemirburak/iris/src/Traits/HsTrait.php', - '/libraries/vendor/ozdemirburak/iris/src/Traits/HslTrait.php', - '/libraries/vendor/ozdemirburak/iris/src/Traits/RgbTrait.php', - '/libraries/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey', - '/libraries/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey.asc', - '/libraries/vendor/psr/http-factory/.pullapprove.yml', - '/libraries/vendor/spomky-labs/cbor-php/.php_cs.dist', - '/libraries/vendor/spomky-labs/cbor-php/CODE_OF_CONDUCT.md', - '/libraries/vendor/spomky-labs/cbor-php/infection.json.dist', - '/libraries/vendor/spomky-labs/cbor-php/phpstan.neon', - '/libraries/vendor/typo3/phar-stream-wrapper/_config.yml', - '/libraries/vendor/voku/portable-utf8/SUMMARY.md', - '/libraries/vendor/willdurand/negotiation/src/Negotiation/Match.php', - '/media/com_actionlogs/js/admin-actionlogs-default.es6.js', - '/media/com_actionlogs/js/admin-actionlogs-default.es6.min.js', - '/media/com_actionlogs/js/admin-actionlogs-default.es6.min.js.gz', - '/media/com_associations/js/admin-associations-default.es6.js', - '/media/com_associations/js/admin-associations-default.es6.min.js', - '/media/com_associations/js/admin-associations-default.es6.min.js.gz', - '/media/com_associations/js/admin-associations-modal.es6.js', - '/media/com_associations/js/admin-associations-modal.es6.min.js', - '/media/com_associations/js/admin-associations-modal.es6.min.js.gz', - '/media/com_associations/js/associations-edit.es6.js', - '/media/com_associations/js/associations-edit.es6.min.js', - '/media/com_associations/js/associations-edit.es6.min.js.gz', - '/media/com_banners/js/admin-banner-edit.es6.js', - '/media/com_banners/js/admin-banner-edit.es6.min.js', - '/media/com_banners/js/admin-banner-edit.es6.min.js.gz', - '/media/com_cache/js/admin-cache-default.es6.js', - '/media/com_cache/js/admin-cache-default.es6.min.js', - '/media/com_cache/js/admin-cache-default.es6.min.js.gz', - '/media/com_categories/js/shared-categories-accordion.es6.js', - '/media/com_categories/js/shared-categories-accordion.es6.min.js', - '/media/com_categories/js/shared-categories-accordion.es6.min.js.gz', - '/media/com_config/js/config-default.es6.js', - '/media/com_config/js/config-default.es6.min.js', - '/media/com_config/js/config-default.es6.min.js.gz', - '/media/com_config/js/modules-default.es6.js', - '/media/com_config/js/modules-default.es6.min.js', - '/media/com_config/js/modules-default.es6.min.js.gz', - '/media/com_config/js/templates-default.es6.js', - '/media/com_config/js/templates-default.es6.min.js', - '/media/com_config/js/templates-default.es6.min.js.gz', - '/media/com_contact/js/admin-contacts-modal.es6.js', - '/media/com_contact/js/admin-contacts-modal.es6.min.js', - '/media/com_contact/js/admin-contacts-modal.es6.min.js.gz', - '/media/com_contact/js/contacts-list.es6.js', - '/media/com_contact/js/contacts-list.es6.min.js', - '/media/com_contact/js/contacts-list.es6.min.js.gz', - '/media/com_content/js/admin-article-pagebreak.es6.js', - '/media/com_content/js/admin-article-pagebreak.es6.min.js', - '/media/com_content/js/admin-article-pagebreak.es6.min.js.gz', - '/media/com_content/js/admin-article-readmore.es6.js', - '/media/com_content/js/admin-article-readmore.es6.min.js', - '/media/com_content/js/admin-article-readmore.es6.min.js.gz', - '/media/com_content/js/admin-articles-default-batch-footer.es6.js', - '/media/com_content/js/admin-articles-default-batch-footer.es6.min.js', - '/media/com_content/js/admin-articles-default-batch-footer.es6.min.js.gz', - '/media/com_content/js/admin-articles-default-stage-footer.es6.js', - '/media/com_content/js/admin-articles-default-stage-footer.es6.min.js', - '/media/com_content/js/admin-articles-default-stage-footer.es6.min.js.gz', - '/media/com_content/js/admin-articles-modal.es6.js', - '/media/com_content/js/admin-articles-modal.es6.min.js', - '/media/com_content/js/admin-articles-modal.es6.min.js.gz', - '/media/com_content/js/articles-list.es6.js', - '/media/com_content/js/articles-list.es6.min.js', - '/media/com_content/js/articles-list.es6.min.js.gz', - '/media/com_content/js/form-edit.es6.js', - '/media/com_content/js/form-edit.es6.min.js', - '/media/com_content/js/form-edit.es6.min.js.gz', - '/media/com_contenthistory/js/admin-compare-compare.es6.js', - '/media/com_contenthistory/js/admin-compare-compare.es6.min.js', - '/media/com_contenthistory/js/admin-compare-compare.es6.min.js.gz', - '/media/com_contenthistory/js/admin-history-modal.es6.js', - '/media/com_contenthistory/js/admin-history-modal.es6.min.js', - '/media/com_contenthistory/js/admin-history-modal.es6.min.js.gz', - '/media/com_contenthistory/js/admin-history-versions.es6.js', - '/media/com_contenthistory/js/admin-history-versions.es6.min.js', - '/media/com_contenthistory/js/admin-history-versions.es6.min.js.gz', - '/media/com_cpanel/js/admin-add_module.es6.js', - '/media/com_cpanel/js/admin-add_module.es6.min.js', - '/media/com_cpanel/js/admin-add_module.es6.min.js.gz', - '/media/com_cpanel/js/admin-cpanel-default.es6.js', - '/media/com_cpanel/js/admin-cpanel-default.es6.min.js', - '/media/com_cpanel/js/admin-cpanel-default.es6.min.js.gz', - '/media/com_cpanel/js/admin-system-loader.es6.js', - '/media/com_cpanel/js/admin-system-loader.es6.min.js', - '/media/com_cpanel/js/admin-system-loader.es6.min.js.gz', - '/media/com_fields/js/admin-field-changecontext.es6.js', - '/media/com_fields/js/admin-field-changecontext.es6.min.js', - '/media/com_fields/js/admin-field-changecontext.es6.min.js.gz', - '/media/com_fields/js/admin-field-edit-modal.es6.js', - '/media/com_fields/js/admin-field-edit-modal.es6.min.js', - '/media/com_fields/js/admin-field-edit-modal.es6.min.js.gz', - '/media/com_fields/js/admin-field-edit.es6.js', - '/media/com_fields/js/admin-field-edit.es6.min.js', - '/media/com_fields/js/admin-field-edit.es6.min.js.gz', - '/media/com_fields/js/admin-field-typehaschanged.es6.js', - '/media/com_fields/js/admin-field-typehaschanged.es6.min.js', - '/media/com_fields/js/admin-field-typehaschanged.es6.min.js.gz', - '/media/com_fields/js/admin-fields-default-batch.es6.js', - '/media/com_fields/js/admin-fields-default-batch.es6.min.js', - '/media/com_fields/js/admin-fields-default-batch.es6.min.js.gz', - '/media/com_fields/js/admin-fields-modal.es6.js', - '/media/com_fields/js/admin-fields-modal.es6.min.js', - '/media/com_fields/js/admin-fields-modal.es6.min.js.gz', - '/media/com_finder/js/filters.es6.js', - '/media/com_finder/js/filters.es6.min.js', - '/media/com_finder/js/filters.es6.min.js.gz', - '/media/com_finder/js/finder-edit.es6.js', - '/media/com_finder/js/finder-edit.es6.min.js', - '/media/com_finder/js/finder-edit.es6.min.js.gz', - '/media/com_finder/js/finder.es6.js', - '/media/com_finder/js/finder.es6.min.js', - '/media/com_finder/js/finder.es6.min.js.gz', - '/media/com_finder/js/index.es6.js', - '/media/com_finder/js/index.es6.min.js', - '/media/com_finder/js/index.es6.min.js.gz', - '/media/com_finder/js/indexer.es6.js', - '/media/com_finder/js/indexer.es6.min.js', - '/media/com_finder/js/indexer.es6.min.js.gz', - '/media/com_finder/js/maps.es6.js', - '/media/com_finder/js/maps.es6.min.js', - '/media/com_finder/js/maps.es6.min.js.gz', - '/media/com_installer/js/changelog.es6.js', - '/media/com_installer/js/changelog.es6.min.js', - '/media/com_installer/js/changelog.es6.min.js.gz', - '/media/com_installer/js/installer.es6.js', - '/media/com_installer/js/installer.es6.min.js', - '/media/com_installer/js/installer.es6.min.js.gz', - '/media/com_joomlaupdate/js/admin-update-default.es6.js', - '/media/com_joomlaupdate/js/admin-update-default.es6.min.js', - '/media/com_joomlaupdate/js/admin-update-default.es6.min.js.gz', - '/media/com_languages/js/admin-language-edit-change-flag.es6.js', - '/media/com_languages/js/admin-language-edit-change-flag.es6.min.js', - '/media/com_languages/js/admin-language-edit-change-flag.es6.min.js.gz', - '/media/com_languages/js/admin-override-edit-refresh-searchstring.es6.js', - '/media/com_languages/js/admin-override-edit-refresh-searchstring.es6.min.js', - '/media/com_languages/js/admin-override-edit-refresh-searchstring.es6.min.js.gz', - '/media/com_languages/js/overrider.es6.js', - '/media/com_languages/js/overrider.es6.min.js', - '/media/com_languages/js/overrider.es6.min.js.gz', - '/media/com_mails/js/admin-email-template-edit.es6.js', - '/media/com_mails/js/admin-email-template-edit.es6.min.js', - '/media/com_mails/js/admin-email-template-edit.es6.min.js.gz', - '/media/com_media/css/mediamanager.min.css', - '/media/com_media/css/mediamanager.min.css.gz', - '/media/com_media/css/mediamanager.min.css.map', - '/media/com_media/js/edit-images.es6.js', - '/media/com_media/js/edit-images.es6.min.js', - '/media/com_media/js/mediamanager.min.js', - '/media/com_media/js/mediamanager.min.js.gz', - '/media/com_media/js/mediamanager.min.js.map', - '/media/com_menus/js/admin-item-edit.es6.js', - '/media/com_menus/js/admin-item-edit.es6.min.js', - '/media/com_menus/js/admin-item-edit.es6.min.js.gz', - '/media/com_menus/js/admin-item-edit_container.es6.js', - '/media/com_menus/js/admin-item-edit_container.es6.min.js', - '/media/com_menus/js/admin-item-edit_container.es6.min.js.gz', - '/media/com_menus/js/admin-item-edit_modules.es6.js', - '/media/com_menus/js/admin-item-edit_modules.es6.min.js', - '/media/com_menus/js/admin-item-edit_modules.es6.min.js.gz', - '/media/com_menus/js/admin-item-modal.es6.js', - '/media/com_menus/js/admin-item-modal.es6.min.js', - '/media/com_menus/js/admin-item-modal.es6.min.js.gz', - '/media/com_menus/js/admin-items-modal.es6.js', - '/media/com_menus/js/admin-items-modal.es6.min.js', - '/media/com_menus/js/admin-items-modal.es6.min.js.gz', - '/media/com_menus/js/admin-menus-default.es6.js', - '/media/com_menus/js/admin-menus-default.es6.min.js', - '/media/com_menus/js/admin-menus-default.es6.min.js.gz', - '/media/com_menus/js/default-batch-body.es6.js', - '/media/com_menus/js/default-batch-body.es6.min.js', - '/media/com_menus/js/default-batch-body.es6.min.js.gz', - '/media/com_modules/js/admin-module-edit.es6.js', - '/media/com_modules/js/admin-module-edit.es6.min.js', - '/media/com_modules/js/admin-module-edit.es6.min.js.gz', - '/media/com_modules/js/admin-module-edit_assignment.es6.js', - '/media/com_modules/js/admin-module-edit_assignment.es6.min.js', - '/media/com_modules/js/admin-module-edit_assignment.es6.min.js.gz', - '/media/com_modules/js/admin-module-search.es6.js', - '/media/com_modules/js/admin-module-search.es6.min.js', - '/media/com_modules/js/admin-module-search.es6.min.js.gz', - '/media/com_modules/js/admin-modules-modal.es6.js', - '/media/com_modules/js/admin-modules-modal.es6.min.js', - '/media/com_modules/js/admin-modules-modal.es6.min.js.gz', - '/media/com_modules/js/admin-select-modal.es6.js', - '/media/com_modules/js/admin-select-modal.es6.min.js', - '/media/com_modules/js/admin-select-modal.es6.min.js.gz', - '/media/com_tags/js/tag-default.es6.js', - '/media/com_tags/js/tag-default.es6.min.js', - '/media/com_tags/js/tag-default.es6.min.js.gz', - '/media/com_tags/js/tag-list.es6.js', - '/media/com_tags/js/tag-list.es6.min.js', - '/media/com_tags/js/tag-list.es6.min.js.gz', - '/media/com_tags/js/tags-default.es6.js', - '/media/com_tags/js/tags-default.es6.min.js', - '/media/com_tags/js/tags-default.es6.min.js.gz', - '/media/com_templates/js/admin-template-compare.es6.js', - '/media/com_templates/js/admin-template-compare.es6.min.js', - '/media/com_templates/js/admin-template-compare.es6.min.js.gz', - '/media/com_templates/js/admin-template-toggle-assignment.es6.js', - '/media/com_templates/js/admin-template-toggle-assignment.es6.min.js', - '/media/com_templates/js/admin-template-toggle-assignment.es6.min.js.gz', - '/media/com_templates/js/admin-template-toggle-switch.es6.js', - '/media/com_templates/js/admin-template-toggle-switch.es6.min.js', - '/media/com_templates/js/admin-template-toggle-switch.es6.min.js.gz', - '/media/com_templates/js/admin-templates-default.es6.js', - '/media/com_templates/js/admin-templates-default.es6.min.js', - '/media/com_templates/js/admin-templates-default.es6.min.js.gz', - '/media/com_users/js/admin-users-groups.es6.js', - '/media/com_users/js/admin-users-groups.es6.min.js', - '/media/com_users/js/admin-users-groups.es6.min.js.gz', - '/media/com_users/js/admin-users-mail.es6.js', - '/media/com_users/js/admin-users-mail.es6.min.js', - '/media/com_users/js/admin-users-mail.es6.min.js.gz', - '/media/com_users/js/two-factor-switcher.es6.js', - '/media/com_users/js/two-factor-switcher.es6.min.js', - '/media/com_users/js/two-factor-switcher.es6.min.js.gz', - '/media/com_workflow/js/admin-items-workflow-buttons.es6.js', - '/media/com_workflow/js/admin-items-workflow-buttons.es6.min.js', - '/media/com_workflow/js/admin-items-workflow-buttons.es6.min.js.gz', - '/media/com_wrapper/js/iframe-height.es6.js', - '/media/com_wrapper/js/iframe-height.es6.min.js', - '/media/com_wrapper/js/iframe-height.es6.min.js.gz', - '/media/layouts/js/joomla/form/field/category-change.es6.js', - '/media/layouts/js/joomla/form/field/category-change.es6.min.js', - '/media/layouts/js/joomla/form/field/category-change.es6.min.js.gz', - '/media/layouts/js/joomla/html/batch/batch-copymove.es6.js', - '/media/layouts/js/joomla/html/batch/batch-copymove.es6.min.js', - '/media/layouts/js/joomla/html/batch/batch-copymove.es6.min.js.gz', - '/media/legacy/js/highlighter.js', - '/media/legacy/js/highlighter.min.js', - '/media/legacy/js/highlighter.min.js.gz', - '/media/mod_login/js/admin-login.es6.js', - '/media/mod_login/js/admin-login.es6.min.js', - '/media/mod_login/js/admin-login.es6.min.js.gz', - '/media/mod_menu/js/admin-menu.es6.js', - '/media/mod_menu/js/admin-menu.es6.min.js', - '/media/mod_menu/js/admin-menu.es6.min.js.gz', - '/media/mod_menu/js/menu.es6.js', - '/media/mod_menu/js/menu.es6.min.js', - '/media/mod_menu/js/menu.es6.min.js.gz', - '/media/mod_multilangstatus/js/admin-multilangstatus.es6.js', - '/media/mod_multilangstatus/js/admin-multilangstatus.es6.min.js', - '/media/mod_multilangstatus/js/admin-multilangstatus.es6.min.js.gz', - '/media/mod_quickicon/js/quickicon.es6.js', - '/media/mod_quickicon/js/quickicon.es6.min.js', - '/media/mod_quickicon/js/quickicon.es6.min.js.gz', - '/media/mod_sampledata/js/sampledata-process.es6.js', - '/media/mod_sampledata/js/sampledata-process.es6.min.js', - '/media/mod_sampledata/js/sampledata-process.es6.min.js.gz', - '/media/plg_captcha_recaptcha/js/recaptcha.es6.js', - '/media/plg_captcha_recaptcha/js/recaptcha.es6.min.js', - '/media/plg_captcha_recaptcha/js/recaptcha.es6.min.js.gz', - '/media/plg_captcha_recaptcha_invisible/js/recaptcha.es6.js', - '/media/plg_captcha_recaptcha_invisible/js/recaptcha.es6.min.js', - '/media/plg_captcha_recaptcha_invisible/js/recaptcha.es6.min.js.gz', - '/media/plg_editors_tinymce/js/plugins/dragdrop/plugin.es6.js', - '/media/plg_editors_tinymce/js/plugins/dragdrop/plugin.es6.min.js', - '/media/plg_editors_tinymce/js/plugins/dragdrop/plugin.es6.min.js.gz', - '/media/plg_editors_tinymce/js/tinymce-builder.es6.js', - '/media/plg_editors_tinymce/js/tinymce-builder.es6.min.js', - '/media/plg_editors_tinymce/js/tinymce-builder.es6.min.js.gz', - '/media/plg_editors_tinymce/js/tinymce.es6.js', - '/media/plg_editors_tinymce/js/tinymce.es6.min.js', - '/media/plg_editors_tinymce/js/tinymce.es6.min.js.gz', - '/media/plg_installer_folderinstaller/js/folderinstaller.es6.js', - '/media/plg_installer_folderinstaller/js/folderinstaller.es6.min.js', - '/media/plg_installer_folderinstaller/js/folderinstaller.es6.min.js.gz', - '/media/plg_installer_packageinstaller/js/packageinstaller.es6.js', - '/media/plg_installer_packageinstaller/js/packageinstaller.es6.min.js', - '/media/plg_installer_packageinstaller/js/packageinstaller.es6.min.js.gz', - '/media/plg_installer_urlinstaller/js/urlinstaller.es6.js', - '/media/plg_installer_urlinstaller/js/urlinstaller.es6.min.js', - '/media/plg_installer_urlinstaller/js/urlinstaller.es6.min.js.gz', - '/media/plg_installer_webinstaller/js/client.es6.js', - '/media/plg_installer_webinstaller/js/client.es6.min.js', - '/media/plg_installer_webinstaller/js/client.es6.min.js.gz', - '/media/plg_media-action_crop/js/crop.es6.js', - '/media/plg_media-action_crop/js/crop.es6.min.js', - '/media/plg_media-action_crop/js/crop.es6.min.js.gz', - '/media/plg_media-action_resize/js/resize.es6.js', - '/media/plg_media-action_resize/js/resize.es6.min.js', - '/media/plg_media-action_resize/js/resize.es6.min.js.gz', - '/media/plg_media-action_rotate/js/rotate.es6.js', - '/media/plg_media-action_rotate/js/rotate.es6.min.js', - '/media/plg_media-action_rotate/js/rotate.es6.min.js.gz', - '/media/plg_quickicon_extensionupdate/js/extensionupdatecheck.es6.js', - '/media/plg_quickicon_extensionupdate/js/extensionupdatecheck.es6.min.js', - '/media/plg_quickicon_extensionupdate/js/extensionupdatecheck.es6.min.js.gz', - '/media/plg_quickicon_joomlaupdate/js/jupdatecheck.es6.js', - '/media/plg_quickicon_joomlaupdate/js/jupdatecheck.es6.min.js', - '/media/plg_quickicon_joomlaupdate/js/jupdatecheck.es6.min.js.gz', - '/media/plg_quickicon_overridecheck/js/overridecheck.es6.js', - '/media/plg_quickicon_overridecheck/js/overridecheck.es6.min.js', - '/media/plg_quickicon_overridecheck/js/overridecheck.es6.min.js.gz', - '/media/plg_quickicon_privacycheck/js/privacycheck.es6.js', - '/media/plg_quickicon_privacycheck/js/privacycheck.es6.min.js', - '/media/plg_quickicon_privacycheck/js/privacycheck.es6.min.js.gz', - '/media/plg_system_debug/js/debug.es6.js', - '/media/plg_system_debug/js/debug.es6.min.js', - '/media/plg_system_debug/js/debug.es6.min.js.gz', - '/media/plg_system_highlight/highlight.min.css', - '/media/plg_system_highlight/highlight.min.css.gz', - '/media/plg_system_stats/js/stats-message.es6.js', - '/media/plg_system_stats/js/stats-message.es6.min.js', - '/media/plg_system_stats/js/stats-message.es6.min.js.gz', - '/media/plg_system_stats/js/stats.es6.js', - '/media/plg_system_stats/js/stats.es6.min.js', - '/media/plg_system_stats/js/stats.es6.min.js.gz', - '/media/plg_system_webauthn/js/login.es6.js', - '/media/plg_system_webauthn/js/login.es6.min.js', - '/media/plg_system_webauthn/js/login.es6.min.js.gz', - '/media/plg_system_webauthn/js/management.es6.js', - '/media/plg_system_webauthn/js/management.es6.min.js', - '/media/plg_system_webauthn/js/management.es6.min.js.gz', - '/media/plg_user_token/js/token.es6.js', - '/media/plg_user_token/js/token.es6.min.js', - '/media/plg_user_token/js/token.es6.min.js.gz', - '/media/system/js/core.es6.js', - '/media/system/js/core.es6.min.js', - '/media/system/js/core.es6.min.js.gz', - '/media/system/js/draggable.es6.js', - '/media/system/js/draggable.es6.min.js', - '/media/system/js/draggable.es6.min.js.gz', - '/media/system/js/fields/joomla-field-color-slider.es6.js', - '/media/system/js/fields/joomla-field-color-slider.es6.min.js', - '/media/system/js/fields/joomla-field-color-slider.es6.min.js.gz', - '/media/system/js/fields/passwordstrength.es6.js', - '/media/system/js/fields/passwordstrength.es6.min.js', - '/media/system/js/fields/passwordstrength.es6.min.js.gz', - '/media/system/js/fields/passwordview.es6.js', - '/media/system/js/fields/passwordview.es6.min.js', - '/media/system/js/fields/passwordview.es6.min.js.gz', - '/media/system/js/fields/select-colour.es6.js', - '/media/system/js/fields/select-colour.es6.min.js', - '/media/system/js/fields/select-colour.es6.min.js.gz', - '/media/system/js/fields/validate.es6.js', - '/media/system/js/fields/validate.es6.min.js', - '/media/system/js/fields/validate.es6.min.js.gz', - '/media/system/js/keepalive.es6.js', - '/media/system/js/keepalive.es6.min.js', - '/media/system/js/keepalive.es6.min.js.gz', - '/media/system/js/multiselect.es6.js', - '/media/system/js/multiselect.es6.min.js', - '/media/system/js/multiselect.es6.min.js.gz', - '/media/system/js/searchtools.es6.js', - '/media/system/js/searchtools.es6.min.js', - '/media/system/js/searchtools.es6.min.js.gz', - '/media/system/js/showon.es6.js', - '/media/system/js/showon.es6.min.js', - '/media/system/js/showon.es6.min.js.gz', - '/media/templates/atum/js/template.es6.js', - '/media/templates/atum/js/template.es6.min.js', - '/media/templates/atum/js/template.es6.min.js.gz', - '/media/templates/atum/js/template.js', - '/media/templates/atum/js/template.min.js', - '/media/templates/atum/js/template.min.js.gz', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.es6.js', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.es6.min.js', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.es6.min.js.gz', - '/media/vendor/bootstrap/js/alert.es6.js', - '/media/vendor/bootstrap/js/alert.es6.min.js', - '/media/vendor/bootstrap/js/alert.es6.min.js.gz', - '/media/vendor/bootstrap/js/bootstrap.es5.js', - '/media/vendor/bootstrap/js/bootstrap.es5.min.js', - '/media/vendor/bootstrap/js/bootstrap.es5.min.js.gz', - '/media/vendor/bootstrap/js/button.es6.js', - '/media/vendor/bootstrap/js/button.es6.min.js', - '/media/vendor/bootstrap/js/button.es6.min.js.gz', - '/media/vendor/bootstrap/js/carousel.es6.js', - '/media/vendor/bootstrap/js/carousel.es6.min.js', - '/media/vendor/bootstrap/js/carousel.es6.min.js.gz', - '/media/vendor/bootstrap/js/collapse.es6.js', - '/media/vendor/bootstrap/js/collapse.es6.min.js', - '/media/vendor/bootstrap/js/collapse.es6.min.js.gz', - '/media/vendor/bootstrap/js/dom-8eef6b5f.js', - '/media/vendor/bootstrap/js/dropdown.es6.js', - '/media/vendor/bootstrap/js/dropdown.es6.min.js', - '/media/vendor/bootstrap/js/dropdown.es6.min.js.gz', - '/media/vendor/bootstrap/js/modal.es6.js', - '/media/vendor/bootstrap/js/modal.es6.min.js', - '/media/vendor/bootstrap/js/modal.es6.min.js.gz', - '/media/vendor/bootstrap/js/popover.es6.js', - '/media/vendor/bootstrap/js/popover.es6.min.js', - '/media/vendor/bootstrap/js/popover.es6.min.js.gz', - '/media/vendor/bootstrap/js/popper-5304749a.js', - '/media/vendor/bootstrap/js/scrollspy.es6.js', - '/media/vendor/bootstrap/js/scrollspy.es6.min.js', - '/media/vendor/bootstrap/js/scrollspy.es6.min.js.gz', - '/media/vendor/bootstrap/js/tab.es6.js', - '/media/vendor/bootstrap/js/tab.es6.min.js', - '/media/vendor/bootstrap/js/tab.es6.min.js.gz', - '/media/vendor/bootstrap/js/toast.es6.js', - '/media/vendor/bootstrap/js/toast.es6.min.js', - '/media/vendor/bootstrap/js/toast.es6.min.js.gz', - '/media/vendor/codemirror/lib/codemirror-ce.js', - '/media/vendor/codemirror/lib/codemirror-ce.min.js', - '/media/vendor/codemirror/lib/codemirror-ce.min.js.gz', - '/media/vendor/punycode/js/punycode.js', - '/media/vendor/punycode/js/punycode.min.js', - '/media/vendor/punycode/js/punycode.min.js.gz', - '/media/vendor/tinymce/changelog.txt', - '/media/vendor/webcomponentsjs/js/webcomponents-ce.js', - '/media/vendor/webcomponentsjs/js/webcomponents-ce.min.js', - '/media/vendor/webcomponentsjs/js/webcomponents-ce.min.js.gz', - '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce-pf.js', - '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce-pf.min.js', - '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce-pf.min.js.gz', - '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce.js', - '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce.min.js', - '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce.min.js.gz', - '/media/vendor/webcomponentsjs/js/webcomponents-sd.js', - '/media/vendor/webcomponentsjs/js/webcomponents-sd.min.js', - '/media/vendor/webcomponentsjs/js/webcomponents-sd.min.js.gz', - '/plugins/fields/subfields/params/subfields.xml', - '/plugins/fields/subfields/subfields.php', - '/plugins/fields/subfields/subfields.xml', - '/plugins/fields/subfields/tmpl/subfields.php', - '/templates/cassiopeia/images/system/rating_star.png', - '/templates/cassiopeia/images/system/rating_star_blank.png', - '/templates/cassiopeia/scss/tools/mixins/_margin.scss', - '/templates/cassiopeia/scss/tools/mixins/_visually-hidden.scss', - '/templates/system/js/error-locales.js', - // 4.0 from RC 1 to RC 2 - '/administrator/components/com_fields/tmpl/field/modal.php', - '/administrator/templates/atum/scss/pages/_com_admin.scss', - '/administrator/templates/atum/scss/pages/_com_finder.scss', - '/libraries/src/Error/JsonApi/InstallLanguageExceptionHandler.php', - '/libraries/src/MVC/Controller/Exception/InstallLanguage.php', - '/media/com_fields/js/admin-field-edit-modal-es5.js', - '/media/com_fields/js/admin-field-edit-modal-es5.min.js', - '/media/com_fields/js/admin-field-edit-modal-es5.min.js.gz', - '/media/com_fields/js/admin-field-edit-modal.js', - '/media/com_fields/js/admin-field-edit-modal.min.js', - '/media/com_fields/js/admin-field-edit-modal.min.js.gz', - // 4.0 from RC 3 to RC 4 - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default.php', - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_nodownload.php', - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_noupdate.php', - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_preupdatecheck.php', - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_reinstall.php', - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_update.php', - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_updatemefirst.php', - '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_upload.php', - '/language/en-GB/com_messages.ini', - '/media/system/css/fields/joomla-image-select.css', - '/media/system/css/fields/joomla-image-select.min.css', - '/media/system/css/fields/joomla-image-select.min.css.gz', - '/media/system/js/fields/joomla-image-select-es5.js', - '/media/system/js/fields/joomla-image-select-es5.min.js', - '/media/system/js/fields/joomla-image-select-es5.min.js.gz', - '/media/system/js/fields/joomla-image-select.js', - '/media/system/js/fields/joomla-image-select.min.js', - '/media/system/js/fields/joomla-image-select.min.js.gz', - // 4.0 from RC 4 to RC 5 - '/media/system/js/fields/calendar-locales/af.min.js', - '/media/system/js/fields/calendar-locales/af.min.js.gz', - '/media/system/js/fields/calendar-locales/ar.min.js', - '/media/system/js/fields/calendar-locales/ar.min.js.gz', - '/media/system/js/fields/calendar-locales/bg.min.js', - '/media/system/js/fields/calendar-locales/bg.min.js.gz', - '/media/system/js/fields/calendar-locales/bn.min.js', - '/media/system/js/fields/calendar-locales/bn.min.js.gz', - '/media/system/js/fields/calendar-locales/bs.min.js', - '/media/system/js/fields/calendar-locales/bs.min.js.gz', - '/media/system/js/fields/calendar-locales/ca.min.js', - '/media/system/js/fields/calendar-locales/ca.min.js.gz', - '/media/system/js/fields/calendar-locales/cs.min.js', - '/media/system/js/fields/calendar-locales/cs.min.js.gz', - '/media/system/js/fields/calendar-locales/cy.min.js', - '/media/system/js/fields/calendar-locales/cy.min.js.gz', - '/media/system/js/fields/calendar-locales/da.min.js', - '/media/system/js/fields/calendar-locales/da.min.js.gz', - '/media/system/js/fields/calendar-locales/de.min.js', - '/media/system/js/fields/calendar-locales/de.min.js.gz', - '/media/system/js/fields/calendar-locales/el.min.js', - '/media/system/js/fields/calendar-locales/el.min.js.gz', - '/media/system/js/fields/calendar-locales/en.min.js', - '/media/system/js/fields/calendar-locales/en.min.js.gz', - '/media/system/js/fields/calendar-locales/es.min.js', - '/media/system/js/fields/calendar-locales/es.min.js.gz', - '/media/system/js/fields/calendar-locales/eu.min.js', - '/media/system/js/fields/calendar-locales/eu.min.js.gz', - '/media/system/js/fields/calendar-locales/fa-ir.min.js', - '/media/system/js/fields/calendar-locales/fa-ir.min.js.gz', - '/media/system/js/fields/calendar-locales/fi.min.js', - '/media/system/js/fields/calendar-locales/fi.min.js.gz', - '/media/system/js/fields/calendar-locales/fr.min.js', - '/media/system/js/fields/calendar-locales/fr.min.js.gz', - '/media/system/js/fields/calendar-locales/ga.min.js', - '/media/system/js/fields/calendar-locales/ga.min.js.gz', - '/media/system/js/fields/calendar-locales/hr.min.js', - '/media/system/js/fields/calendar-locales/hr.min.js.gz', - '/media/system/js/fields/calendar-locales/hu.min.js', - '/media/system/js/fields/calendar-locales/hu.min.js.gz', - '/media/system/js/fields/calendar-locales/it.min.js', - '/media/system/js/fields/calendar-locales/it.min.js.gz', - '/media/system/js/fields/calendar-locales/ja.min.js', - '/media/system/js/fields/calendar-locales/ja.min.js.gz', - '/media/system/js/fields/calendar-locales/ka.min.js', - '/media/system/js/fields/calendar-locales/ka.min.js.gz', - '/media/system/js/fields/calendar-locales/kk.min.js', - '/media/system/js/fields/calendar-locales/kk.min.js.gz', - '/media/system/js/fields/calendar-locales/ko.min.js', - '/media/system/js/fields/calendar-locales/ko.min.js.gz', - '/media/system/js/fields/calendar-locales/lt.min.js', - '/media/system/js/fields/calendar-locales/lt.min.js.gz', - '/media/system/js/fields/calendar-locales/mk.min.js', - '/media/system/js/fields/calendar-locales/mk.min.js.gz', - '/media/system/js/fields/calendar-locales/nb.min.js', - '/media/system/js/fields/calendar-locales/nb.min.js.gz', - '/media/system/js/fields/calendar-locales/nl.min.js', - '/media/system/js/fields/calendar-locales/nl.min.js.gz', - '/media/system/js/fields/calendar-locales/pl.min.js', - '/media/system/js/fields/calendar-locales/pl.min.js.gz', - '/media/system/js/fields/calendar-locales/prs-af.min.js', - '/media/system/js/fields/calendar-locales/prs-af.min.js.gz', - '/media/system/js/fields/calendar-locales/pt.min.js', - '/media/system/js/fields/calendar-locales/pt.min.js.gz', - '/media/system/js/fields/calendar-locales/ru.min.js', - '/media/system/js/fields/calendar-locales/ru.min.js.gz', - '/media/system/js/fields/calendar-locales/sk.min.js', - '/media/system/js/fields/calendar-locales/sk.min.js.gz', - '/media/system/js/fields/calendar-locales/sl.min.js', - '/media/system/js/fields/calendar-locales/sl.min.js.gz', - '/media/system/js/fields/calendar-locales/sr-rs.min.js', - '/media/system/js/fields/calendar-locales/sr-rs.min.js.gz', - '/media/system/js/fields/calendar-locales/sr-yu.min.js', - '/media/system/js/fields/calendar-locales/sr-yu.min.js.gz', - '/media/system/js/fields/calendar-locales/sv.min.js', - '/media/system/js/fields/calendar-locales/sv.min.js.gz', - '/media/system/js/fields/calendar-locales/sw.min.js', - '/media/system/js/fields/calendar-locales/sw.min.js.gz', - '/media/system/js/fields/calendar-locales/ta.min.js', - '/media/system/js/fields/calendar-locales/ta.min.js.gz', - '/media/system/js/fields/calendar-locales/th.min.js', - '/media/system/js/fields/calendar-locales/th.min.js.gz', - '/media/system/js/fields/calendar-locales/uk.min.js', - '/media/system/js/fields/calendar-locales/uk.min.js.gz', - '/media/system/js/fields/calendar-locales/zh-CN.min.js', - '/media/system/js/fields/calendar-locales/zh-CN.min.js.gz', - '/media/system/js/fields/calendar-locales/zh-TW.min.js', - '/media/system/js/fields/calendar-locales/zh-TW.min.js.gz', - // 4.0 from RC 5 to RC 6 - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu-es5.js', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu-es5.min.js', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu-es5.min.js.gz', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.js', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.min.js', - '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.min.js.gz', - '/templates/cassiopeia/css/vendor/fontawesome-free/fontawesome.css', - '/templates/cassiopeia/css/vendor/fontawesome-free/fontawesome.min.css', - '/templates/cassiopeia/css/vendor/fontawesome-free/fontawesome.min.css.gz', - '/templates/cassiopeia/scss/vendor/fontawesome-free/fontawesome.scss', - // 4.0 from RC 6 to 4.0.0 (stable) - '/libraries/vendor/algo26-matthias/idna-convert/tests/integration/ToIdnTest.php', - '/libraries/vendor/algo26-matthias/idna-convert/tests/integration/ToUnicodeTest.php', - '/libraries/vendor/algo26-matthias/idna-convert/tests/unit/.gitkeep', - '/libraries/vendor/algo26-matthias/idna-convert/tests/unit/namePrepTest.php', - '/libraries/vendor/doctrine/inflector/docs/en/index.rst', - '/libraries/vendor/jakeasmith/http_build_url/tests/HttpBuildUrlTest.php', - '/libraries/vendor/jakeasmith/http_build_url/tests/bootstrap.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/AcceptLanguageTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/AcceptTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/BaseAcceptTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/CharsetNegotiatorTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/EncodingNegotiatorTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/LanguageNegotiatorTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/MatchTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/NegotiatorTest.php', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/TestCase.php', - '/libraries/vendor/willdurand/negotiation/tests/bootstrap.php', - // From 4.0.2 to 4.0.3 - '/templates/cassiopeia/css/global/fonts-web_fira-sans.css', - '/templates/cassiopeia/css/global/fonts-web_fira-sans.min.css', - '/templates/cassiopeia/css/global/fonts-web_fira-sans.min.css.gz', - '/templates/cassiopeia/css/global/fonts-web_roboto+noto-sans.css', - '/templates/cassiopeia/css/global/fonts-web_roboto+noto-sans.min.css', - '/templates/cassiopeia/css/global/fonts-web_roboto+noto-sans.min.css.gz', - '/templates/cassiopeia/scss/global/fonts-web_fira-sans.scss', - '/templates/cassiopeia/scss/global/fonts-web_roboto+noto-sans.scss', - // From 4.0.3 to 4.0.4 - '/administrator/templates/atum/scss/_mixin.scss', - '/media/com_joomlaupdate/js/encryption.min.js.gz', - '/media/com_joomlaupdate/js/update.min.js.gz', - '/templates/cassiopeia/images/system/sort_asc.png', - '/templates/cassiopeia/images/system/sort_desc.png', - // From 4.0.4 to 4.0.5 - '/media/vendor/codemirror/lib/#codemirror.js#', - // From 4.0.5 to 4.0.6 - '/media/vendor/mediaelement/css/mejs-controls.png', - // From 4.0.x to 4.1.0-beta1 - '/administrator/templates/atum/css/system/searchtools/searchtools.css', - '/administrator/templates/atum/css/system/searchtools/searchtools.min.css', - '/administrator/templates/atum/css/system/searchtools/searchtools.min.css.gz', - '/administrator/templates/atum/css/template-rtl.css', - '/administrator/templates/atum/css/template-rtl.min.css', - '/administrator/templates/atum/css/template-rtl.min.css.gz', - '/administrator/templates/atum/css/template.css', - '/administrator/templates/atum/css/template.min.css', - '/administrator/templates/atum/css/template.min.css.gz', - '/administrator/templates/atum/css/vendor/awesomplete/awesomplete.css', - '/administrator/templates/atum/css/vendor/awesomplete/awesomplete.min.css', - '/administrator/templates/atum/css/vendor/awesomplete/awesomplete.min.css.gz', - '/administrator/templates/atum/css/vendor/choicesjs/choices.css', - '/administrator/templates/atum/css/vendor/choicesjs/choices.min.css', - '/administrator/templates/atum/css/vendor/choicesjs/choices.min.css.gz', - '/administrator/templates/atum/css/vendor/fontawesome-free/fontawesome.css', - '/administrator/templates/atum/css/vendor/fontawesome-free/fontawesome.min.css', - '/administrator/templates/atum/css/vendor/fontawesome-free/fontawesome.min.css.gz', - '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-alert.css', - '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-alert.min.css', - '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-alert.min.css.gz', - '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-tab.css', - '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-tab.min.css', - '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-tab.min.css.gz', - '/administrator/templates/atum/css/vendor/minicolors/minicolors.css', - '/administrator/templates/atum/css/vendor/minicolors/minicolors.min.css', - '/administrator/templates/atum/css/vendor/minicolors/minicolors.min.css.gz', - '/administrator/templates/atum/images/joomla-pattern.svg', - '/administrator/templates/atum/images/logos/brand-large.svg', - '/administrator/templates/atum/images/logos/brand-small.svg', - '/administrator/templates/atum/images/logos/login.svg', - '/administrator/templates/atum/images/select-bg-active-rtl.svg', - '/administrator/templates/atum/images/select-bg-active.svg', - '/administrator/templates/atum/images/select-bg-rtl.svg', - '/administrator/templates/atum/images/select-bg.svg', - '/administrator/templates/atum/scss/_root.scss', - '/administrator/templates/atum/scss/_variables.scss', - '/administrator/templates/atum/scss/blocks/_alerts.scss', - '/administrator/templates/atum/scss/blocks/_edit.scss', - '/administrator/templates/atum/scss/blocks/_form.scss', - '/administrator/templates/atum/scss/blocks/_global.scss', - '/administrator/templates/atum/scss/blocks/_header.scss', - '/administrator/templates/atum/scss/blocks/_icons.scss', - '/administrator/templates/atum/scss/blocks/_iframe.scss', - '/administrator/templates/atum/scss/blocks/_layout.scss', - '/administrator/templates/atum/scss/blocks/_lists.scss', - '/administrator/templates/atum/scss/blocks/_login.scss', - '/administrator/templates/atum/scss/blocks/_modals.scss', - '/administrator/templates/atum/scss/blocks/_quickicons.scss', - '/administrator/templates/atum/scss/blocks/_sidebar-nav.scss', - '/administrator/templates/atum/scss/blocks/_sidebar.scss', - '/administrator/templates/atum/scss/blocks/_switcher.scss', - '/administrator/templates/atum/scss/blocks/_toolbar.scss', - '/administrator/templates/atum/scss/blocks/_treeselect.scss', - '/administrator/templates/atum/scss/blocks/_utilities.scss', - '/administrator/templates/atum/scss/pages/_com_config.scss', - '/administrator/templates/atum/scss/pages/_com_content.scss', - '/administrator/templates/atum/scss/pages/_com_cpanel.scss', - '/administrator/templates/atum/scss/pages/_com_joomlaupdate.scss', - '/administrator/templates/atum/scss/pages/_com_modules.scss', - '/administrator/templates/atum/scss/pages/_com_privacy.scss', - '/administrator/templates/atum/scss/pages/_com_tags.scss', - '/administrator/templates/atum/scss/pages/_com_templates.scss', - '/administrator/templates/atum/scss/pages/_com_users.scss', - '/administrator/templates/atum/scss/system/searchtools/searchtools.scss', - '/administrator/templates/atum/scss/template-rtl.scss', - '/administrator/templates/atum/scss/template.scss', - '/administrator/templates/atum/scss/vendor/_bootstrap.scss', - '/administrator/templates/atum/scss/vendor/_codemirror.scss', - '/administrator/templates/atum/scss/vendor/_dragula.scss', - '/administrator/templates/atum/scss/vendor/_tinymce.scss', - '/administrator/templates/atum/scss/vendor/awesomplete/awesomplete.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_badge.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_bootstrap-rtl.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_buttons.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_card.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_collapse.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_custom-forms.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_dropdown.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_form.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_lists.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_modal.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_pagination.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_reboot.scss', - '/administrator/templates/atum/scss/vendor/bootstrap/_table.scss', - '/administrator/templates/atum/scss/vendor/choicesjs/choices.scss', - '/administrator/templates/atum/scss/vendor/fontawesome-free/fontawesome.scss', - '/administrator/templates/atum/scss/vendor/joomla-custom-elements/joomla-alert.scss', - '/administrator/templates/atum/scss/vendor/joomla-custom-elements/joomla-tab.scss', - '/administrator/templates/atum/scss/vendor/minicolors/minicolors.scss', - '/administrator/templates/atum/template_preview.png', - '/administrator/templates/atum/template_thumbnail.png', - '/administrator/templates/system/css/error.css', - '/administrator/templates/system/css/error.min.css', - '/administrator/templates/system/css/error.min.css.gz', - '/administrator/templates/system/css/system.css', - '/administrator/templates/system/css/system.min.css', - '/administrator/templates/system/css/system.min.css.gz', - '/administrator/templates/system/images/calendar.png', - '/administrator/templates/system/scss/error.scss', - '/administrator/templates/system/scss/system.scss', - '/templates/cassiopeia/css/editor.css', - '/templates/cassiopeia/css/editor.min.css', - '/templates/cassiopeia/css/editor.min.css.gz', - '/templates/cassiopeia/css/global/colors_alternative.css', - '/templates/cassiopeia/css/global/colors_alternative.min.css', - '/templates/cassiopeia/css/global/colors_alternative.min.css.gz', - '/templates/cassiopeia/css/global/colors_standard.css', - '/templates/cassiopeia/css/global/colors_standard.min.css', - '/templates/cassiopeia/css/global/colors_standard.min.css.gz', - '/templates/cassiopeia/css/global/fonts-local_roboto.css', - '/templates/cassiopeia/css/global/fonts-local_roboto.min.css', - '/templates/cassiopeia/css/global/fonts-local_roboto.min.css.gz', - '/templates/cassiopeia/css/offline.css', - '/templates/cassiopeia/css/offline.min.css', - '/templates/cassiopeia/css/offline.min.css.gz', - '/templates/cassiopeia/css/system/searchtools/searchtools.css', - '/templates/cassiopeia/css/system/searchtools/searchtools.min.css', - '/templates/cassiopeia/css/system/searchtools/searchtools.min.css.gz', - '/templates/cassiopeia/css/template-rtl.css', - '/templates/cassiopeia/css/template-rtl.min.css', - '/templates/cassiopeia/css/template-rtl.min.css.gz', - '/templates/cassiopeia/css/template.css', - '/templates/cassiopeia/css/template.min.css', - '/templates/cassiopeia/css/template.min.css.gz', - '/templates/cassiopeia/css/vendor/choicesjs/choices.css', - '/templates/cassiopeia/css/vendor/choicesjs/choices.min.css', - '/templates/cassiopeia/css/vendor/choicesjs/choices.min.css.gz', - '/templates/cassiopeia/css/vendor/joomla-custom-elements/joomla-alert.css', - '/templates/cassiopeia/css/vendor/joomla-custom-elements/joomla-alert.min.css', - '/templates/cassiopeia/css/vendor/joomla-custom-elements/joomla-alert.min.css.gz', - '/templates/cassiopeia/images/logo.svg', - '/templates/cassiopeia/images/select-bg-active-rtl.svg', - '/templates/cassiopeia/images/select-bg-active.svg', - '/templates/cassiopeia/images/select-bg-rtl.svg', - '/templates/cassiopeia/images/select-bg.svg', - '/templates/cassiopeia/js/template.es5.js', - '/templates/cassiopeia/js/template.js', - '/templates/cassiopeia/js/template.min.js', - '/templates/cassiopeia/js/template.min.js.gz', - '/templates/cassiopeia/scss/blocks/_alerts.scss', - '/templates/cassiopeia/scss/blocks/_back-to-top.scss', - '/templates/cassiopeia/scss/blocks/_banner.scss', - '/templates/cassiopeia/scss/blocks/_css-grid.scss', - '/templates/cassiopeia/scss/blocks/_footer.scss', - '/templates/cassiopeia/scss/blocks/_form.scss', - '/templates/cassiopeia/scss/blocks/_frontend-edit.scss', - '/templates/cassiopeia/scss/blocks/_global.scss', - '/templates/cassiopeia/scss/blocks/_header.scss', - '/templates/cassiopeia/scss/blocks/_icons.scss', - '/templates/cassiopeia/scss/blocks/_iframe.scss', - '/templates/cassiopeia/scss/blocks/_layout.scss', - '/templates/cassiopeia/scss/blocks/_legacy.scss', - '/templates/cassiopeia/scss/blocks/_modals.scss', - '/templates/cassiopeia/scss/blocks/_modifiers.scss', - '/templates/cassiopeia/scss/blocks/_tags.scss', - '/templates/cassiopeia/scss/blocks/_toolbar.scss', - '/templates/cassiopeia/scss/blocks/_utilities.scss', - '/templates/cassiopeia/scss/editor.scss', - '/templates/cassiopeia/scss/global/colors_alternative.scss', - '/templates/cassiopeia/scss/global/colors_standard.scss', - '/templates/cassiopeia/scss/global/fonts-local_roboto.scss', - '/templates/cassiopeia/scss/offline.scss', - '/templates/cassiopeia/scss/system/searchtools/searchtools.scss', - '/templates/cassiopeia/scss/template-rtl.scss', - '/templates/cassiopeia/scss/template.scss', - '/templates/cassiopeia/scss/tools/_tools.scss', - '/templates/cassiopeia/scss/tools/functions/_max-width.scss', - '/templates/cassiopeia/scss/tools/variables/_variables.scss', - '/templates/cassiopeia/scss/vendor/_awesomplete.scss', - '/templates/cassiopeia/scss/vendor/_chosen.scss', - '/templates/cassiopeia/scss/vendor/_dragula.scss', - '/templates/cassiopeia/scss/vendor/_minicolors.scss', - '/templates/cassiopeia/scss/vendor/_tinymce.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_bootstrap-rtl.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_buttons.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_collapse.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_custom-forms.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_dropdown.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_forms.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_lists.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_modal.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_nav.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_pagination.scss', - '/templates/cassiopeia/scss/vendor/bootstrap/_table.scss', - '/templates/cassiopeia/scss/vendor/choicesjs/choices.scss', - '/templates/cassiopeia/scss/vendor/joomla-custom-elements/joomla-alert.scss', - '/templates/cassiopeia/scss/vendor/metismenu/_metismenu.scss', - '/templates/cassiopeia/template_preview.png', - '/templates/cassiopeia/template_thumbnail.png', - '/templates/system/css/editor.css', - '/templates/system/css/editor.min.css', - '/templates/system/css/editor.min.css.gz', - '/templates/system/css/error.css', - '/templates/system/css/error.min.css', - '/templates/system/css/error.min.css.gz', - '/templates/system/css/error_rtl.css', - '/templates/system/css/error_rtl.min.css', - '/templates/system/css/error_rtl.min.css.gz', - '/templates/system/css/general.css', - '/templates/system/css/general.min.css', - '/templates/system/css/general.min.css.gz', - '/templates/system/css/offline.css', - '/templates/system/css/offline.min.css', - '/templates/system/css/offline.min.css.gz', - '/templates/system/css/offline_rtl.css', - '/templates/system/css/offline_rtl.min.css', - '/templates/system/css/offline_rtl.min.css.gz', - '/templates/system/scss/editor.scss', - '/templates/system/scss/error.scss', - '/templates/system/scss/error_rtl.scss', - '/templates/system/scss/general.scss', - '/templates/system/scss/offline.scss', - '/templates/system/scss/offline_rtl.scss', - // From 4.1.0-beta3 to 4.1.0-rc1 - '/api/components/com_media/src/Helper/AdapterTrait.php', - // From 4.1.0 to 4.1.1 - '/libraries/vendor/tobscure/json-api/.git/HEAD', - '/libraries/vendor/tobscure/json-api/.git/ORIG_HEAD', - '/libraries/vendor/tobscure/json-api/.git/config', - '/libraries/vendor/tobscure/json-api/.git/description', - '/libraries/vendor/tobscure/json-api/.git/hooks/applypatch-msg.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/commit-msg.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/fsmonitor-watchman.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/post-update.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/pre-applypatch.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/pre-commit.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/pre-merge-commit.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/pre-push.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/pre-rebase.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/pre-receive.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/prepare-commit-msg.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/push-to-checkout.sample', - '/libraries/vendor/tobscure/json-api/.git/hooks/update.sample', - '/libraries/vendor/tobscure/json-api/.git/index', - '/libraries/vendor/tobscure/json-api/.git/info/exclude', - '/libraries/vendor/tobscure/json-api/.git/info/refs', - '/libraries/vendor/tobscure/json-api/.git/logs/HEAD', - '/libraries/vendor/tobscure/json-api/.git/logs/refs/heads/joomla-backports', - '/libraries/vendor/tobscure/json-api/.git/logs/refs/remotes/origin/HEAD', - '/libraries/vendor/tobscure/json-api/.git/objects/info/packs', - '/libraries/vendor/tobscure/json-api/.git/objects/pack/pack-51530cba04703b17f3c11b9e8458a171092cf5e3.idx', - '/libraries/vendor/tobscure/json-api/.git/objects/pack/pack-51530cba04703b17f3c11b9e8458a171092cf5e3.pack', - '/libraries/vendor/tobscure/json-api/.git/packed-refs', - '/libraries/vendor/tobscure/json-api/.git/refs/heads/joomla-backports', - '/libraries/vendor/tobscure/json-api/.git/refs/remotes/origin/HEAD', - '/libraries/vendor/tobscure/json-api/.php_cs', - '/libraries/vendor/tobscure/json-api/tests/AbstractSerializerTest.php', - '/libraries/vendor/tobscure/json-api/tests/AbstractTestCase.php', - '/libraries/vendor/tobscure/json-api/tests/CollectionTest.php', - '/libraries/vendor/tobscure/json-api/tests/DocumentTest.php', - '/libraries/vendor/tobscure/json-api/tests/ErrorHandlerTest.php', - '/libraries/vendor/tobscure/json-api/tests/Exception/Handler/FallbackExceptionHandlerTest.php', - '/libraries/vendor/tobscure/json-api/tests/Exception/Handler/InvalidParameterExceptionHandlerTest.php', - '/libraries/vendor/tobscure/json-api/tests/LinksTraitTest.php', - '/libraries/vendor/tobscure/json-api/tests/ParametersTest.php', - '/libraries/vendor/tobscure/json-api/tests/ResourceTest.php', - '/libraries/vendor/tobscure/json-api/tests/UtilTest.php', - // From 4.1.1 to 4.1.2 - '/administrator/components/com_users/src/Field/PrimaryauthprovidersField.php', - // From 4.1.2 to 4.1.3 - '/libraries/vendor/webmozart/assert/.php_cs', - // From 4.1.3 to 4.1.4 - '/libraries/vendor/maximebf/debugbar/.bowerrc', - '/libraries/vendor/maximebf/debugbar/bower.json', - '/libraries/vendor/maximebf/debugbar/build/namespaceFontAwesome.php', - '/libraries/vendor/maximebf/debugbar/demo/ajax.php', - '/libraries/vendor/maximebf/debugbar/demo/ajax_exception.php', - '/libraries/vendor/maximebf/debugbar/demo/bootstrap.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/cachecache/index.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/bootstrap.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/build.sh', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/cli-config.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/index.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/src/Demo/Product.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/monolog/index.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/build.properties', - '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/build.sh', - '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/index.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/runtime-conf.xml', - '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/schema.xml', - '/libraries/vendor/maximebf/debugbar/demo/bridge/slim/index.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/swiftmailer/index.php', - '/libraries/vendor/maximebf/debugbar/demo/bridge/twig/foobar.html', - '/libraries/vendor/maximebf/debugbar/demo/bridge/twig/hello.html', - '/libraries/vendor/maximebf/debugbar/demo/bridge/twig/index.php', - '/libraries/vendor/maximebf/debugbar/demo/dump_assets.php', - '/libraries/vendor/maximebf/debugbar/demo/index.php', - '/libraries/vendor/maximebf/debugbar/demo/open.php', - '/libraries/vendor/maximebf/debugbar/demo/pdo.php', - '/libraries/vendor/maximebf/debugbar/demo/stack.php', - '/libraries/vendor/maximebf/debugbar/docs/ajax_and_stack.md', - '/libraries/vendor/maximebf/debugbar/docs/base_collectors.md', - '/libraries/vendor/maximebf/debugbar/docs/bridge_collectors.md', - '/libraries/vendor/maximebf/debugbar/docs/data_collectors.md', - '/libraries/vendor/maximebf/debugbar/docs/data_formatter.md', - '/libraries/vendor/maximebf/debugbar/docs/http_drivers.md', - '/libraries/vendor/maximebf/debugbar/docs/javascript_bar.md', - '/libraries/vendor/maximebf/debugbar/docs/manifest.json', - '/libraries/vendor/maximebf/debugbar/docs/openhandler.md', - '/libraries/vendor/maximebf/debugbar/docs/rendering.md', - '/libraries/vendor/maximebf/debugbar/docs/screenshot.png', - '/libraries/vendor/maximebf/debugbar/docs/storage.md', - '/libraries/vendor/maximebf/debugbar/docs/style.css', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/AggregatedCollectorTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/ConfigCollectorTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/MessagesCollectorTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/MockCollector.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/Propel2CollectorTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/TimeDataCollectorTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataFormatter/DataFormatterTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataFormatter/DebugBarVarDumperTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DebugBarTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DebugBarTestCase.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/JavascriptRendererTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/MockHttpDriver.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/OpenHandlerTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/Storage/FileStorageTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/Storage/MockStorage.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/TracedStatementTest.php', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/full_init.html', - '/libraries/vendor/maximebf/debugbar/tests/bootstrap.php', - // From 4.1 to 4.2.0-beta1 - '/libraries/src/Service/Provider/ApiRouter.php', - '/libraries/vendor/nyholm/psr7/doc/final.md', - '/media/com_finder/js/index-es5.js', - '/media/com_finder/js/index-es5.min.js', - '/media/com_finder/js/index-es5.min.js.gz', - '/media/com_finder/js/index.js', - '/media/com_finder/js/index.min.js', - '/media/com_finder/js/index.min.js.gz', - '/media/com_users/js/two-factor-switcher-es5.js', - '/media/com_users/js/two-factor-switcher-es5.min.js', - '/media/com_users/js/two-factor-switcher-es5.min.js.gz', - '/media/com_users/js/two-factor-switcher.js', - '/media/com_users/js/two-factor-switcher.min.js', - '/media/com_users/js/two-factor-switcher.min.js.gz', - '/modules/mod_articles_news/mod_articles_news.php', - '/plugins/actionlog/joomla/joomla.php', - '/plugins/api-authentication/basic/basic.php', - '/plugins/api-authentication/token/token.php', - '/plugins/system/cache/cache.php', - '/plugins/twofactorauth/totp/postinstall/actions.php', - '/plugins/twofactorauth/totp/tmpl/form.php', - '/plugins/twofactorauth/totp/totp.php', - '/plugins/twofactorauth/totp/totp.xml', - '/plugins/twofactorauth/yubikey/tmpl/form.php', - '/plugins/twofactorauth/yubikey/yubikey.php', - '/plugins/twofactorauth/yubikey/yubikey.xml', - // From 4.2.0-beta1 to 4.2.0-beta2 - '/layouts/plugins/user/profile/fields/dob.php', - '/modules/mod_articles_latest/mod_articles_latest.php', - '/plugins/behaviour/taggable/taggable.php', - '/plugins/behaviour/versionable/versionable.php', - '/plugins/task/requests/requests.php', - '/plugins/task/sitestatus/sitestatus.php', - '/plugins/user/profile/src/Field/DobField.php', - ); - - $folders = array( - // From 3.10 to 4.1 - '/templates/system/images', - '/templates/system/html', - '/templates/protostar/less', - '/templates/protostar/language/en-GB', - '/templates/protostar/language', - '/templates/protostar/js', - '/templates/protostar/img', - '/templates/protostar/images/system', - '/templates/protostar/images', - '/templates/protostar/html/layouts/joomla/system', - '/templates/protostar/html/layouts/joomla/form/field', - '/templates/protostar/html/layouts/joomla/form', - '/templates/protostar/html/layouts/joomla', - '/templates/protostar/html/layouts', - '/templates/protostar/html/com_media/imageslist', - '/templates/protostar/html/com_media', - '/templates/protostar/html', - '/templates/protostar/css', - '/templates/protostar', - '/templates/beez3/language/en-GB', - '/templates/beez3/language', - '/templates/beez3/javascript', - '/templates/beez3/images/system', - '/templates/beez3/images/personal', - '/templates/beez3/images/nature', - '/templates/beez3/images', - '/templates/beez3/html/mod_login', - '/templates/beez3/html/mod_languages', - '/templates/beez3/html/mod_breadcrumbs', - '/templates/beez3/html/layouts/joomla/system', - '/templates/beez3/html/layouts/joomla', - '/templates/beez3/html/layouts', - '/templates/beez3/html/com_weblinks/form', - '/templates/beez3/html/com_weblinks/category', - '/templates/beez3/html/com_weblinks/categories', - '/templates/beez3/html/com_weblinks', - '/templates/beez3/html/com_newsfeeds/category', - '/templates/beez3/html/com_newsfeeds/categories', - '/templates/beez3/html/com_newsfeeds', - '/templates/beez3/html/com_content/form', - '/templates/beez3/html/com_content/featured', - '/templates/beez3/html/com_content/category', - '/templates/beez3/html/com_content/categories', - '/templates/beez3/html/com_content/article', - '/templates/beez3/html/com_content/archive', - '/templates/beez3/html/com_content', - '/templates/beez3/html/com_contact/contact', - '/templates/beez3/html/com_contact/category', - '/templates/beez3/html/com_contact/categories', - '/templates/beez3/html/com_contact', - '/templates/beez3/html', - '/templates/beez3/css', - '/templates/beez3', - '/plugins/user/terms/terms', - '/plugins/user/terms/field', - '/plugins/user/profile/profiles', - '/plugins/user/profile/field', - '/plugins/system/stats/field', - '/plugins/system/privacyconsent/privacyconsent', - '/plugins/system/privacyconsent/field', - '/plugins/system/p3p', - '/plugins/system/languagecode/language/en-GB', - '/plugins/system/languagecode/language', - '/plugins/editors/tinymce/form', - '/plugins/editors/tinymce/field', - '/plugins/content/confirmconsent/fields', - '/plugins/captcha/recaptcha/postinstall', - '/plugins/authentication/gmail', - '/media/plg_twofactorauth_totp/js', - '/media/plg_twofactorauth_totp', - '/media/plg_system_highlight', - '/media/overrider/js', - '/media/overrider/css', - '/media/overrider', - '/media/media/js', - '/media/media/images/mime-icon-32', - '/media/media/images/mime-icon-16', - '/media/media/images', - '/media/media/css', - '/media/media', - '/media/jui/less', - '/media/jui/js', - '/media/jui/img', - '/media/jui/images', - '/media/jui/fonts', - '/media/jui/css', - '/media/jui', - '/media/editors/tinymce/themes/modern', - '/media/editors/tinymce/themes', - '/media/editors/tinymce/templates', - '/media/editors/tinymce/skins/lightgray/img', - '/media/editors/tinymce/skins/lightgray/fonts', - '/media/editors/tinymce/skins/lightgray', - '/media/editors/tinymce/skins', - '/media/editors/tinymce/plugins/wordcount', - '/media/editors/tinymce/plugins/visualchars', - '/media/editors/tinymce/plugins/visualblocks/css', - '/media/editors/tinymce/plugins/visualblocks', - '/media/editors/tinymce/plugins/toc', - '/media/editors/tinymce/plugins/textpattern', - '/media/editors/tinymce/plugins/textcolor', - '/media/editors/tinymce/plugins/template', - '/media/editors/tinymce/plugins/table', - '/media/editors/tinymce/plugins/tabfocus', - '/media/editors/tinymce/plugins/spellchecker', - '/media/editors/tinymce/plugins/searchreplace', - '/media/editors/tinymce/plugins/save', - '/media/editors/tinymce/plugins/print', - '/media/editors/tinymce/plugins/preview', - '/media/editors/tinymce/plugins/paste', - '/media/editors/tinymce/plugins/pagebreak', - '/media/editors/tinymce/plugins/noneditable', - '/media/editors/tinymce/plugins/nonbreaking', - '/media/editors/tinymce/plugins/media', - '/media/editors/tinymce/plugins/lists', - '/media/editors/tinymce/plugins/link', - '/media/editors/tinymce/plugins/legacyoutput', - '/media/editors/tinymce/plugins/layer', - '/media/editors/tinymce/plugins/insertdatetime', - '/media/editors/tinymce/plugins/importcss', - '/media/editors/tinymce/plugins/imagetools', - '/media/editors/tinymce/plugins/image', - '/media/editors/tinymce/plugins/hr', - '/media/editors/tinymce/plugins/fullscreen', - '/media/editors/tinymce/plugins/fullpage', - '/media/editors/tinymce/plugins/example_dependency', - '/media/editors/tinymce/plugins/example', - '/media/editors/tinymce/plugins/emoticons/img', - '/media/editors/tinymce/plugins/emoticons', - '/media/editors/tinymce/plugins/directionality', - '/media/editors/tinymce/plugins/contextmenu', - '/media/editors/tinymce/plugins/colorpicker', - '/media/editors/tinymce/plugins/codesample/css', - '/media/editors/tinymce/plugins/codesample', - '/media/editors/tinymce/plugins/code', - '/media/editors/tinymce/plugins/charmap', - '/media/editors/tinymce/plugins/bbcode', - '/media/editors/tinymce/plugins/autosave', - '/media/editors/tinymce/plugins/autoresize', - '/media/editors/tinymce/plugins/autolink', - '/media/editors/tinymce/plugins/anchor', - '/media/editors/tinymce/plugins/advlist', - '/media/editors/tinymce/plugins', - '/media/editors/tinymce/langs', - '/media/editors/tinymce/js/plugins/dragdrop', - '/media/editors/tinymce/js/plugins', - '/media/editors/tinymce/js', - '/media/editors/tinymce', - '/media/editors/none/js', - '/media/editors/none', - '/media/editors/codemirror/theme', - '/media/editors/codemirror/mode/z80', - '/media/editors/codemirror/mode/yaml-frontmatter', - '/media/editors/codemirror/mode/yaml', - '/media/editors/codemirror/mode/yacas', - '/media/editors/codemirror/mode/xquery', - '/media/editors/codemirror/mode/xml', - '/media/editors/codemirror/mode/webidl', - '/media/editors/codemirror/mode/wast', - '/media/editors/codemirror/mode/vue', - '/media/editors/codemirror/mode/vhdl', - '/media/editors/codemirror/mode/verilog', - '/media/editors/codemirror/mode/velocity', - '/media/editors/codemirror/mode/vbscript', - '/media/editors/codemirror/mode/vb', - '/media/editors/codemirror/mode/twig', - '/media/editors/codemirror/mode/turtle', - '/media/editors/codemirror/mode/ttcn-cfg', - '/media/editors/codemirror/mode/ttcn', - '/media/editors/codemirror/mode/troff', - '/media/editors/codemirror/mode/tornado', - '/media/editors/codemirror/mode/toml', - '/media/editors/codemirror/mode/tiki', - '/media/editors/codemirror/mode/tiddlywiki', - '/media/editors/codemirror/mode/textile', - '/media/editors/codemirror/mode/tcl', - '/media/editors/codemirror/mode/swift', - '/media/editors/codemirror/mode/stylus', - '/media/editors/codemirror/mode/stex', - '/media/editors/codemirror/mode/sql', - '/media/editors/codemirror/mode/spreadsheet', - '/media/editors/codemirror/mode/sparql', - '/media/editors/codemirror/mode/soy', - '/media/editors/codemirror/mode/solr', - '/media/editors/codemirror/mode/smarty', - '/media/editors/codemirror/mode/smalltalk', - '/media/editors/codemirror/mode/slim', - '/media/editors/codemirror/mode/sieve', - '/media/editors/codemirror/mode/shell', - '/media/editors/codemirror/mode/scheme', - '/media/editors/codemirror/mode/sass', - '/media/editors/codemirror/mode/sas', - '/media/editors/codemirror/mode/rust', - '/media/editors/codemirror/mode/ruby', - '/media/editors/codemirror/mode/rst', - '/media/editors/codemirror/mode/rpm/changes', - '/media/editors/codemirror/mode/rpm', - '/media/editors/codemirror/mode/r', - '/media/editors/codemirror/mode/q', - '/media/editors/codemirror/mode/python', - '/media/editors/codemirror/mode/puppet', - '/media/editors/codemirror/mode/pug', - '/media/editors/codemirror/mode/protobuf', - '/media/editors/codemirror/mode/properties', - '/media/editors/codemirror/mode/powershell', - '/media/editors/codemirror/mode/pig', - '/media/editors/codemirror/mode/php', - '/media/editors/codemirror/mode/perl', - '/media/editors/codemirror/mode/pegjs', - '/media/editors/codemirror/mode/pascal', - '/media/editors/codemirror/mode/oz', - '/media/editors/codemirror/mode/octave', - '/media/editors/codemirror/mode/ntriples', - '/media/editors/codemirror/mode/nsis', - '/media/editors/codemirror/mode/nginx', - '/media/editors/codemirror/mode/mumps', - '/media/editors/codemirror/mode/mscgen', - '/media/editors/codemirror/mode/modelica', - '/media/editors/codemirror/mode/mllike', - '/media/editors/codemirror/mode/mirc', - '/media/editors/codemirror/mode/mbox', - '/media/editors/codemirror/mode/mathematica', - '/media/editors/codemirror/mode/markdown', - '/media/editors/codemirror/mode/lua', - '/media/editors/codemirror/mode/livescript', - '/media/editors/codemirror/mode/julia', - '/media/editors/codemirror/mode/jsx', - '/media/editors/codemirror/mode/jinja2', - '/media/editors/codemirror/mode/javascript', - '/media/editors/codemirror/mode/idl', - '/media/editors/codemirror/mode/http', - '/media/editors/codemirror/mode/htmlmixed', - '/media/editors/codemirror/mode/htmlembedded', - '/media/editors/codemirror/mode/haxe', - '/media/editors/codemirror/mode/haskell-literate', - '/media/editors/codemirror/mode/haskell', - '/media/editors/codemirror/mode/handlebars', - '/media/editors/codemirror/mode/haml', - '/media/editors/codemirror/mode/groovy', - '/media/editors/codemirror/mode/go', - '/media/editors/codemirror/mode/gherkin', - '/media/editors/codemirror/mode/gfm', - '/media/editors/codemirror/mode/gas', - '/media/editors/codemirror/mode/fortran', - '/media/editors/codemirror/mode/forth', - '/media/editors/codemirror/mode/fcl', - '/media/editors/codemirror/mode/factor', - '/media/editors/codemirror/mode/erlang', - '/media/editors/codemirror/mode/elm', - '/media/editors/codemirror/mode/eiffel', - '/media/editors/codemirror/mode/ecl', - '/media/editors/codemirror/mode/ebnf', - '/media/editors/codemirror/mode/dylan', - '/media/editors/codemirror/mode/dtd', - '/media/editors/codemirror/mode/dockerfile', - '/media/editors/codemirror/mode/django', - '/media/editors/codemirror/mode/diff', - '/media/editors/codemirror/mode/dart', - '/media/editors/codemirror/mode/d', - '/media/editors/codemirror/mode/cypher', - '/media/editors/codemirror/mode/css', - '/media/editors/codemirror/mode/crystal', - '/media/editors/codemirror/mode/commonlisp', - '/media/editors/codemirror/mode/coffeescript', - '/media/editors/codemirror/mode/cobol', - '/media/editors/codemirror/mode/cmake', - '/media/editors/codemirror/mode/clojure', - '/media/editors/codemirror/mode/clike', - '/media/editors/codemirror/mode/brainfuck', - '/media/editors/codemirror/mode/asterisk', - '/media/editors/codemirror/mode/asn.1', - '/media/editors/codemirror/mode/asciiarmor', - '/media/editors/codemirror/mode/apl', - '/media/editors/codemirror/mode', - '/media/editors/codemirror/lib', - '/media/editors/codemirror/keymap', - '/media/editors/codemirror/addon/wrap', - '/media/editors/codemirror/addon/tern', - '/media/editors/codemirror/addon/selection', - '/media/editors/codemirror/addon/search', - '/media/editors/codemirror/addon/scroll', - '/media/editors/codemirror/addon/runmode', - '/media/editors/codemirror/addon/mode', - '/media/editors/codemirror/addon/merge', - '/media/editors/codemirror/addon/lint', - '/media/editors/codemirror/addon/hint', - '/media/editors/codemirror/addon/fold', - '/media/editors/codemirror/addon/edit', - '/media/editors/codemirror/addon/display', - '/media/editors/codemirror/addon/dialog', - '/media/editors/codemirror/addon/comment', - '/media/editors/codemirror/addon', - '/media/editors/codemirror', - '/media/editors', - '/media/contacts/images', - '/media/contacts', - '/media/com_contenthistory/css', - '/media/cms/css', - '/media/cms', - '/libraries/vendor/symfony/polyfill-util', - '/libraries/vendor/symfony/polyfill-php71', - '/libraries/vendor/symfony/polyfill-php56', - '/libraries/vendor/symfony/polyfill-php55', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/XML/Declaration', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/XML', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Parse', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Net', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/HTTP', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Decode/HTML', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Decode', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Content/Type', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Content', - '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache', - '/libraries/vendor/simplepie/simplepie/library/SimplePie', - '/libraries/vendor/simplepie/simplepie/library', - '/libraries/vendor/simplepie/simplepie/idn', - '/libraries/vendor/simplepie/simplepie', - '/libraries/vendor/simplepie', - '/libraries/vendor/phpmailer/phpmailer/extras', - '/libraries/vendor/paragonie/random_compat/lib', - '/libraries/vendor/leafo/lessphp', - '/libraries/vendor/leafo', - '/libraries/vendor/joomla/session/Joomla/Session/Storage', - '/libraries/vendor/joomla/session/Joomla/Session', - '/libraries/vendor/joomla/session/Joomla', - '/libraries/vendor/joomla/image/src/Filter', - '/libraries/vendor/joomla/image/src', - '/libraries/vendor/joomla/image', - '/libraries/vendor/joomla/compat/src', - '/libraries/vendor/joomla/compat', - '/libraries/vendor/joomla/application/src/Cli/Output/Processor', - '/libraries/vendor/joomla/application/src/Cli/Output', - '/libraries/vendor/joomla/application/src/Cli', - '/libraries/vendor/ircmaxell/password-compat/lib', - '/libraries/vendor/ircmaxell/password-compat', - '/libraries/vendor/ircmaxell', - '/libraries/vendor/brumann/polyfill-unserialize/src', - '/libraries/vendor/brumann/polyfill-unserialize', - '/libraries/vendor/brumann', - '/libraries/src/Table/Observer', - '/libraries/src/Menu/Node', - '/libraries/src/Language/Wrapper', - '/libraries/src/Language/Stemmer', - '/libraries/src/Http/Wrapper', - '/libraries/src/Filter/Wrapper', - '/libraries/src/Filesystem/Wrapper', - '/libraries/src/Crypt/Password', - '/libraries/src/Access/Wrapper', - '/libraries/phputf8/utils', - '/libraries/phputf8/native', - '/libraries/phputf8/mbstring', - '/libraries/phputf8', - '/libraries/legacy/utilities', - '/libraries/legacy/table', - '/libraries/legacy/simplepie', - '/libraries/legacy/simplecrypt', - '/libraries/legacy/response', - '/libraries/legacy/request', - '/libraries/legacy/log', - '/libraries/legacy/form/field', - '/libraries/legacy/form', - '/libraries/legacy/exception', - '/libraries/legacy/error', - '/libraries/legacy/dispatcher', - '/libraries/legacy/database', - '/libraries/legacy/base', - '/libraries/legacy/application', - '/libraries/legacy', - '/libraries/joomla/view', - '/libraries/joomla/utilities', - '/libraries/joomla/twitter', - '/libraries/joomla/string/wrapper', - '/libraries/joomla/string', - '/libraries/joomla/session/storage', - '/libraries/joomla/session/handler', - '/libraries/joomla/session', - '/libraries/joomla/route/wrapper', - '/libraries/joomla/route', - '/libraries/joomla/openstreetmap', - '/libraries/joomla/observer/wrapper', - '/libraries/joomla/observer/updater', - '/libraries/joomla/observer', - '/libraries/joomla/observable', - '/libraries/joomla/oauth2', - '/libraries/joomla/oauth1', - '/libraries/joomla/model', - '/libraries/joomla/mediawiki', - '/libraries/joomla/linkedin', - '/libraries/joomla/keychain', - '/libraries/joomla/grid', - '/libraries/joomla/google/embed', - '/libraries/joomla/google/data/plus', - '/libraries/joomla/google/data/picasa', - '/libraries/joomla/google/data', - '/libraries/joomla/google/auth', - '/libraries/joomla/google', - '/libraries/joomla/github/package/users', - '/libraries/joomla/github/package/repositories', - '/libraries/joomla/github/package/pulls', - '/libraries/joomla/github/package/orgs', - '/libraries/joomla/github/package/issues', - '/libraries/joomla/github/package/gists', - '/libraries/joomla/github/package/data', - '/libraries/joomla/github/package/activity', - '/libraries/joomla/github/package', - '/libraries/joomla/github', - '/libraries/joomla/form/fields', - '/libraries/joomla/form', - '/libraries/joomla/facebook', - '/libraries/joomla/event', - '/libraries/joomla/database/query', - '/libraries/joomla/database/iterator', - '/libraries/joomla/database/importer', - '/libraries/joomla/database/exporter', - '/libraries/joomla/database/exception', - '/libraries/joomla/database/driver', - '/libraries/joomla/database', - '/libraries/joomla/controller', - '/libraries/joomla/archive/wrapper', - '/libraries/joomla/archive', - '/libraries/joomla/application/web/router', - '/libraries/joomla/application/web', - '/libraries/joomla/application', - '/libraries/joomla', - '/libraries/idna_convert', - '/libraries/fof/view', - '/libraries/fof/utils/update', - '/libraries/fof/utils/timer', - '/libraries/fof/utils/phpfunc', - '/libraries/fof/utils/observable', - '/libraries/fof/utils/object', - '/libraries/fof/utils/ip', - '/libraries/fof/utils/installscript', - '/libraries/fof/utils/ini', - '/libraries/fof/utils/filescheck', - '/libraries/fof/utils/config', - '/libraries/fof/utils/cache', - '/libraries/fof/utils/array', - '/libraries/fof/utils', - '/libraries/fof/toolbar', - '/libraries/fof/template', - '/libraries/fof/table/dispatcher', - '/libraries/fof/table/behavior', - '/libraries/fof/table', - '/libraries/fof/string', - '/libraries/fof/render', - '/libraries/fof/query', - '/libraries/fof/platform/filesystem', - '/libraries/fof/platform', - '/libraries/fof/model/field', - '/libraries/fof/model/dispatcher', - '/libraries/fof/model/behavior', - '/libraries/fof/model', - '/libraries/fof/less/parser', - '/libraries/fof/less/formatter', - '/libraries/fof/less', - '/libraries/fof/layout', - '/libraries/fof/integration/joomla/filesystem', - '/libraries/fof/integration/joomla', - '/libraries/fof/integration', - '/libraries/fof/input/jinput', - '/libraries/fof/input', - '/libraries/fof/inflector', - '/libraries/fof/hal/render', - '/libraries/fof/hal', - '/libraries/fof/form/header', - '/libraries/fof/form/field', - '/libraries/fof/form', - '/libraries/fof/encrypt/aes', - '/libraries/fof/encrypt', - '/libraries/fof/download/adapter', - '/libraries/fof/download', - '/libraries/fof/dispatcher', - '/libraries/fof/database/query', - '/libraries/fof/database/iterator', - '/libraries/fof/database/driver', - '/libraries/fof/database', - '/libraries/fof/controller', - '/libraries/fof/config/domain', - '/libraries/fof/config', - '/libraries/fof/autoloader', - '/libraries/fof', - '/libraries/cms/less/formatter', - '/libraries/cms/less', - '/libraries/cms/html/language/en-GB', - '/libraries/cms/html/language', - '/libraries/cms/html', - '/libraries/cms/class', - '/libraries/cms', - '/layouts/libraries/cms/html/bootstrap', - '/layouts/libraries/cms/html', - '/layouts/libraries/cms', - '/layouts/joomla/tinymce/buttons', - '/layouts/joomla/modal', - '/layouts/joomla/html/formbehavior', - '/components/com_wrapper/views/wrapper/tmpl', - '/components/com_wrapper/views/wrapper', - '/components/com_wrapper/views', - '/components/com_users/views/reset/tmpl', - '/components/com_users/views/reset', - '/components/com_users/views/remind/tmpl', - '/components/com_users/views/remind', - '/components/com_users/views/registration/tmpl', - '/components/com_users/views/registration', - '/components/com_users/views/profile/tmpl', - '/components/com_users/views/profile', - '/components/com_users/views/login/tmpl', - '/components/com_users/views/login', - '/components/com_users/views', - '/components/com_users/models/rules', - '/components/com_users/models/forms', - '/components/com_users/models', - '/components/com_users/layouts/joomla/form', - '/components/com_users/layouts/joomla', - '/components/com_users/layouts', - '/components/com_users/helpers/html', - '/components/com_users/helpers', - '/components/com_users/controllers', - '/components/com_tags/views/tags/tmpl', - '/components/com_tags/views/tags', - '/components/com_tags/views/tag/tmpl', - '/components/com_tags/views/tag', - '/components/com_tags/views', - '/components/com_tags/models', - '/components/com_tags/controllers', - '/components/com_privacy/views/request/tmpl', - '/components/com_privacy/views/request', - '/components/com_privacy/views/remind/tmpl', - '/components/com_privacy/views/remind', - '/components/com_privacy/views/confirm/tmpl', - '/components/com_privacy/views/confirm', - '/components/com_privacy/views', - '/components/com_privacy/models/forms', - '/components/com_privacy/models', - '/components/com_privacy/controllers', - '/components/com_newsfeeds/views/newsfeed/tmpl', - '/components/com_newsfeeds/views/newsfeed', - '/components/com_newsfeeds/views/category/tmpl', - '/components/com_newsfeeds/views/category', - '/components/com_newsfeeds/views/categories/tmpl', - '/components/com_newsfeeds/views/categories', - '/components/com_newsfeeds/views', - '/components/com_newsfeeds/models', - '/components/com_modules/models/forms', - '/components/com_modules/models', - '/components/com_menus/models/forms', - '/components/com_menus/models', - '/components/com_mailto/views/sent/tmpl', - '/components/com_mailto/views/sent', - '/components/com_mailto/views/mailto/tmpl', - '/components/com_mailto/views/mailto', - '/components/com_mailto/views', - '/components/com_mailto/models/forms', - '/components/com_mailto/models', - '/components/com_mailto/helpers', - '/components/com_mailto', - '/components/com_finder/views/search/tmpl', - '/components/com_finder/views/search', - '/components/com_finder/views', - '/components/com_finder/models', - '/components/com_finder/helpers/html', - '/components/com_finder/controllers', - '/components/com_fields/models/forms', - '/components/com_fields/models', - '/components/com_content/views/form/tmpl', - '/components/com_content/views/form', - '/components/com_content/views/featured/tmpl', - '/components/com_content/views/featured', - '/components/com_content/views/category/tmpl', - '/components/com_content/views/category', - '/components/com_content/views/categories/tmpl', - '/components/com_content/views/categories', - '/components/com_content/views/article/tmpl', - '/components/com_content/views/article', - '/components/com_content/views/archive/tmpl', - '/components/com_content/views/archive', - '/components/com_content/views', - '/components/com_content/models/forms', - '/components/com_content/models', - '/components/com_content/controllers', - '/components/com_contact/views/featured/tmpl', - '/components/com_contact/views/featured', - '/components/com_contact/views/contact/tmpl', - '/components/com_contact/views/contact', - '/components/com_contact/views/category/tmpl', - '/components/com_contact/views/category', - '/components/com_contact/views/categories/tmpl', - '/components/com_contact/views/categories', - '/components/com_contact/views', - '/components/com_contact/models/rules', - '/components/com_contact/models/forms', - '/components/com_contact/models', - '/components/com_contact/layouts/joomla/form', - '/components/com_contact/layouts/joomla', - '/components/com_contact/controllers', - '/components/com_config/view/templates/tmpl', - '/components/com_config/view/templates', - '/components/com_config/view/modules/tmpl', - '/components/com_config/view/modules', - '/components/com_config/view/config/tmpl', - '/components/com_config/view/config', - '/components/com_config/view/cms', - '/components/com_config/view', - '/components/com_config/model/form', - '/components/com_config/model', - '/components/com_config/controller/templates', - '/components/com_config/controller/modules', - '/components/com_config/controller/config', - '/components/com_config/controller', - '/components/com_banners/models', - '/components/com_banners/helpers', - '/administrator/templates/system/html', - '/administrator/templates/isis/less/pages', - '/administrator/templates/isis/less/bootstrap', - '/administrator/templates/isis/less/blocks', - '/administrator/templates/isis/less', - '/administrator/templates/isis/language/en-GB', - '/administrator/templates/isis/language', - '/administrator/templates/isis/js', - '/administrator/templates/isis/img', - '/administrator/templates/isis/images/system', - '/administrator/templates/isis/images/admin', - '/administrator/templates/isis/images', - '/administrator/templates/isis/html/mod_version', - '/administrator/templates/isis/html/layouts/joomla/toolbar', - '/administrator/templates/isis/html/layouts/joomla/system', - '/administrator/templates/isis/html/layouts/joomla/pagination', - '/administrator/templates/isis/html/layouts/joomla/form/field', - '/administrator/templates/isis/html/layouts/joomla/form', - '/administrator/templates/isis/html/layouts/joomla', - '/administrator/templates/isis/html/layouts', - '/administrator/templates/isis/html/com_media/medialist', - '/administrator/templates/isis/html/com_media/imageslist', - '/administrator/templates/isis/html/com_media', - '/administrator/templates/isis/html', - '/administrator/templates/isis/css', - '/administrator/templates/isis', - '/administrator/templates/hathor/postinstall', - '/administrator/templates/hathor/less', - '/administrator/templates/hathor/language/en-GB', - '/administrator/templates/hathor/language', - '/administrator/templates/hathor/js', - '/administrator/templates/hathor/images/toolbar', - '/administrator/templates/hathor/images/system', - '/administrator/templates/hathor/images/menu', - '/administrator/templates/hathor/images/header', - '/administrator/templates/hathor/images/admin', - '/administrator/templates/hathor/images', - '/administrator/templates/hathor/html/mod_quickicon', - '/administrator/templates/hathor/html/mod_login', - '/administrator/templates/hathor/html/layouts/plugins/user/profile/fields', - '/administrator/templates/hathor/html/layouts/plugins/user/profile', - '/administrator/templates/hathor/html/layouts/plugins/user', - '/administrator/templates/hathor/html/layouts/plugins', - '/administrator/templates/hathor/html/layouts/joomla/toolbar', - '/administrator/templates/hathor/html/layouts/joomla/sidebars', - '/administrator/templates/hathor/html/layouts/joomla/quickicons', - '/administrator/templates/hathor/html/layouts/joomla/edit', - '/administrator/templates/hathor/html/layouts/joomla', - '/administrator/templates/hathor/html/layouts/com_modules/toolbar', - '/administrator/templates/hathor/html/layouts/com_modules', - '/administrator/templates/hathor/html/layouts/com_messages/toolbar', - '/administrator/templates/hathor/html/layouts/com_messages', - '/administrator/templates/hathor/html/layouts/com_media/toolbar', - '/administrator/templates/hathor/html/layouts/com_media', - '/administrator/templates/hathor/html/layouts', - '/administrator/templates/hathor/html/com_weblinks/weblinks', - '/administrator/templates/hathor/html/com_weblinks/weblink', - '/administrator/templates/hathor/html/com_weblinks', - '/administrator/templates/hathor/html/com_users/users', - '/administrator/templates/hathor/html/com_users/user', - '/administrator/templates/hathor/html/com_users/notes', - '/administrator/templates/hathor/html/com_users/note', - '/administrator/templates/hathor/html/com_users/levels', - '/administrator/templates/hathor/html/com_users/groups', - '/administrator/templates/hathor/html/com_users/debuguser', - '/administrator/templates/hathor/html/com_users/debuggroup', - '/administrator/templates/hathor/html/com_users', - '/administrator/templates/hathor/html/com_templates/templates', - '/administrator/templates/hathor/html/com_templates/template', - '/administrator/templates/hathor/html/com_templates/styles', - '/administrator/templates/hathor/html/com_templates/style', - '/administrator/templates/hathor/html/com_templates', - '/administrator/templates/hathor/html/com_tags/tags', - '/administrator/templates/hathor/html/com_tags/tag', - '/administrator/templates/hathor/html/com_tags', - '/administrator/templates/hathor/html/com_search/searches', - '/administrator/templates/hathor/html/com_search', - '/administrator/templates/hathor/html/com_redirect/links', - '/administrator/templates/hathor/html/com_redirect', - '/administrator/templates/hathor/html/com_postinstall/messages', - '/administrator/templates/hathor/html/com_postinstall', - '/administrator/templates/hathor/html/com_plugins/plugins', - '/administrator/templates/hathor/html/com_plugins/plugin', - '/administrator/templates/hathor/html/com_plugins', - '/administrator/templates/hathor/html/com_newsfeeds/newsfeeds', - '/administrator/templates/hathor/html/com_newsfeeds/newsfeed', - '/administrator/templates/hathor/html/com_newsfeeds', - '/administrator/templates/hathor/html/com_modules/positions', - '/administrator/templates/hathor/html/com_modules/modules', - '/administrator/templates/hathor/html/com_modules/module', - '/administrator/templates/hathor/html/com_modules', - '/administrator/templates/hathor/html/com_messages/messages', - '/administrator/templates/hathor/html/com_messages/message', - '/administrator/templates/hathor/html/com_messages', - '/administrator/templates/hathor/html/com_menus/menutypes', - '/administrator/templates/hathor/html/com_menus/menus', - '/administrator/templates/hathor/html/com_menus/menu', - '/administrator/templates/hathor/html/com_menus/items', - '/administrator/templates/hathor/html/com_menus/item', - '/administrator/templates/hathor/html/com_menus', - '/administrator/templates/hathor/html/com_languages/overrides', - '/administrator/templates/hathor/html/com_languages/languages', - '/administrator/templates/hathor/html/com_languages/installed', - '/administrator/templates/hathor/html/com_languages', - '/administrator/templates/hathor/html/com_joomlaupdate/default', - '/administrator/templates/hathor/html/com_joomlaupdate', - '/administrator/templates/hathor/html/com_installer/warnings', - '/administrator/templates/hathor/html/com_installer/update', - '/administrator/templates/hathor/html/com_installer/manage', - '/administrator/templates/hathor/html/com_installer/languages', - '/administrator/templates/hathor/html/com_installer/install', - '/administrator/templates/hathor/html/com_installer/discover', - '/administrator/templates/hathor/html/com_installer/default', - '/administrator/templates/hathor/html/com_installer/database', - '/administrator/templates/hathor/html/com_installer', - '/administrator/templates/hathor/html/com_finder/maps', - '/administrator/templates/hathor/html/com_finder/index', - '/administrator/templates/hathor/html/com_finder/filters', - '/administrator/templates/hathor/html/com_finder', - '/administrator/templates/hathor/html/com_fields/groups', - '/administrator/templates/hathor/html/com_fields/group', - '/administrator/templates/hathor/html/com_fields/fields', - '/administrator/templates/hathor/html/com_fields/field', - '/administrator/templates/hathor/html/com_fields', - '/administrator/templates/hathor/html/com_cpanel/cpanel', - '/administrator/templates/hathor/html/com_cpanel', - '/administrator/templates/hathor/html/com_contenthistory/history', - '/administrator/templates/hathor/html/com_contenthistory', - '/administrator/templates/hathor/html/com_content/featured', - '/administrator/templates/hathor/html/com_content/articles', - '/administrator/templates/hathor/html/com_content/article', - '/administrator/templates/hathor/html/com_content', - '/administrator/templates/hathor/html/com_contact/contacts', - '/administrator/templates/hathor/html/com_contact/contact', - '/administrator/templates/hathor/html/com_contact', - '/administrator/templates/hathor/html/com_config/component', - '/administrator/templates/hathor/html/com_config/application', - '/administrator/templates/hathor/html/com_config', - '/administrator/templates/hathor/html/com_checkin/checkin', - '/administrator/templates/hathor/html/com_checkin', - '/administrator/templates/hathor/html/com_categories/category', - '/administrator/templates/hathor/html/com_categories/categories', - '/administrator/templates/hathor/html/com_categories', - '/administrator/templates/hathor/html/com_cache/purge', - '/administrator/templates/hathor/html/com_cache/cache', - '/administrator/templates/hathor/html/com_cache', - '/administrator/templates/hathor/html/com_banners/tracks', - '/administrator/templates/hathor/html/com_banners/download', - '/administrator/templates/hathor/html/com_banners/clients', - '/administrator/templates/hathor/html/com_banners/client', - '/administrator/templates/hathor/html/com_banners/banners', - '/administrator/templates/hathor/html/com_banners/banner', - '/administrator/templates/hathor/html/com_banners', - '/administrator/templates/hathor/html/com_associations/associations', - '/administrator/templates/hathor/html/com_associations', - '/administrator/templates/hathor/html/com_admin/sysinfo', - '/administrator/templates/hathor/html/com_admin/profile', - '/administrator/templates/hathor/html/com_admin/help', - '/administrator/templates/hathor/html/com_admin', - '/administrator/templates/hathor/html', - '/administrator/templates/hathor/css', - '/administrator/templates/hathor', - '/administrator/modules/mod_version/language/en-GB', - '/administrator/modules/mod_version/language', - '/administrator/modules/mod_status/tmpl', - '/administrator/modules/mod_status', - '/administrator/modules/mod_stats_admin/language', - '/administrator/modules/mod_multilangstatus/language/en-GB', - '/administrator/modules/mod_multilangstatus/language', - '/administrator/components/com_users/views/users/tmpl', - '/administrator/components/com_users/views/users', - '/administrator/components/com_users/views/user/tmpl', - '/administrator/components/com_users/views/user', - '/administrator/components/com_users/views/notes/tmpl', - '/administrator/components/com_users/views/notes', - '/administrator/components/com_users/views/note/tmpl', - '/administrator/components/com_users/views/note', - '/administrator/components/com_users/views/mail/tmpl', - '/administrator/components/com_users/views/mail', - '/administrator/components/com_users/views/levels/tmpl', - '/administrator/components/com_users/views/levels', - '/administrator/components/com_users/views/level/tmpl', - '/administrator/components/com_users/views/level', - '/administrator/components/com_users/views/groups/tmpl', - '/administrator/components/com_users/views/groups', - '/administrator/components/com_users/views/group/tmpl', - '/administrator/components/com_users/views/group', - '/administrator/components/com_users/views/debuguser/tmpl', - '/administrator/components/com_users/views/debuguser', - '/administrator/components/com_users/views/debuggroup/tmpl', - '/administrator/components/com_users/views/debuggroup', - '/administrator/components/com_users/views', - '/administrator/components/com_users/tables', - '/administrator/components/com_users/models/forms/fields', - '/administrator/components/com_users/models/forms', - '/administrator/components/com_users/models/fields', - '/administrator/components/com_users/models', - '/administrator/components/com_users/helpers/html', - '/administrator/components/com_users/controllers', - '/administrator/components/com_templates/views/templates/tmpl', - '/administrator/components/com_templates/views/templates', - '/administrator/components/com_templates/views/template/tmpl', - '/administrator/components/com_templates/views/template', - '/administrator/components/com_templates/views/styles/tmpl', - '/administrator/components/com_templates/views/styles', - '/administrator/components/com_templates/views/style/tmpl', - '/administrator/components/com_templates/views/style', - '/administrator/components/com_templates/views', - '/administrator/components/com_templates/tables', - '/administrator/components/com_templates/models/forms', - '/administrator/components/com_templates/models/fields', - '/administrator/components/com_templates/models', - '/administrator/components/com_templates/helpers/html', - '/administrator/components/com_templates/controllers', - '/administrator/components/com_tags/views/tags/tmpl', - '/administrator/components/com_tags/views/tags', - '/administrator/components/com_tags/views/tag/tmpl', - '/administrator/components/com_tags/views/tag', - '/administrator/components/com_tags/views', - '/administrator/components/com_tags/tables', - '/administrator/components/com_tags/models/forms', - '/administrator/components/com_tags/models', - '/administrator/components/com_tags/helpers', - '/administrator/components/com_tags/controllers', - '/administrator/components/com_redirect/views/links/tmpl', - '/administrator/components/com_redirect/views/links', - '/administrator/components/com_redirect/views/link/tmpl', - '/administrator/components/com_redirect/views/link', - '/administrator/components/com_redirect/views', - '/administrator/components/com_redirect/tables', - '/administrator/components/com_redirect/models/forms', - '/administrator/components/com_redirect/models/fields', - '/administrator/components/com_redirect/models', - '/administrator/components/com_redirect/helpers/html', - '/administrator/components/com_redirect/controllers', - '/administrator/components/com_privacy/views/requests/tmpl', - '/administrator/components/com_privacy/views/requests', - '/administrator/components/com_privacy/views/request/tmpl', - '/administrator/components/com_privacy/views/request', - '/administrator/components/com_privacy/views/export', - '/administrator/components/com_privacy/views/dashboard/tmpl', - '/administrator/components/com_privacy/views/dashboard', - '/administrator/components/com_privacy/views/consents/tmpl', - '/administrator/components/com_privacy/views/consents', - '/administrator/components/com_privacy/views/capabilities/tmpl', - '/administrator/components/com_privacy/views/capabilities', - '/administrator/components/com_privacy/views', - '/administrator/components/com_privacy/tables', - '/administrator/components/com_privacy/models/forms', - '/administrator/components/com_privacy/models/fields', - '/administrator/components/com_privacy/models', - '/administrator/components/com_privacy/helpers/removal', - '/administrator/components/com_privacy/helpers/html', - '/administrator/components/com_privacy/helpers/export', - '/administrator/components/com_privacy/helpers', - '/administrator/components/com_privacy/controllers', - '/administrator/components/com_postinstall/views/messages/tmpl', - '/administrator/components/com_postinstall/views/messages', - '/administrator/components/com_postinstall/views', - '/administrator/components/com_postinstall/models', - '/administrator/components/com_postinstall/controllers', - '/administrator/components/com_plugins/views/plugins/tmpl', - '/administrator/components/com_plugins/views/plugins', - '/administrator/components/com_plugins/views/plugin/tmpl', - '/administrator/components/com_plugins/views/plugin', - '/administrator/components/com_plugins/views', - '/administrator/components/com_plugins/models/forms', - '/administrator/components/com_plugins/models/fields', - '/administrator/components/com_plugins/models', - '/administrator/components/com_plugins/controllers', - '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl', - '/administrator/components/com_newsfeeds/views/newsfeeds', - '/administrator/components/com_newsfeeds/views/newsfeed/tmpl', - '/administrator/components/com_newsfeeds/views/newsfeed', - '/administrator/components/com_newsfeeds/views', - '/administrator/components/com_newsfeeds/tables', - '/administrator/components/com_newsfeeds/models/forms', - '/administrator/components/com_newsfeeds/models/fields/modal', - '/administrator/components/com_newsfeeds/models/fields', - '/administrator/components/com_newsfeeds/models', - '/administrator/components/com_newsfeeds/helpers/html', - '/administrator/components/com_newsfeeds/controllers', - '/administrator/components/com_modules/views/select/tmpl', - '/administrator/components/com_modules/views/select', - '/administrator/components/com_modules/views/preview/tmpl', - '/administrator/components/com_modules/views/preview', - '/administrator/components/com_modules/views/positions/tmpl', - '/administrator/components/com_modules/views/positions', - '/administrator/components/com_modules/views/modules/tmpl', - '/administrator/components/com_modules/views/modules', - '/administrator/components/com_modules/views/module/tmpl', - '/administrator/components/com_modules/views/module', - '/administrator/components/com_modules/views', - '/administrator/components/com_modules/models/forms', - '/administrator/components/com_modules/models/fields', - '/administrator/components/com_modules/models', - '/administrator/components/com_modules/helpers/html', - '/administrator/components/com_modules/controllers', - '/administrator/components/com_messages/views/messages/tmpl', - '/administrator/components/com_messages/views/messages', - '/administrator/components/com_messages/views/message/tmpl', - '/administrator/components/com_messages/views/message', - '/administrator/components/com_messages/views/config/tmpl', - '/administrator/components/com_messages/views/config', - '/administrator/components/com_messages/views', - '/administrator/components/com_messages/tables', - '/administrator/components/com_messages/models/forms', - '/administrator/components/com_messages/models/fields', - '/administrator/components/com_messages/models', - '/administrator/components/com_messages/helpers/html', - '/administrator/components/com_messages/helpers', - '/administrator/components/com_messages/controllers', - '/administrator/components/com_menus/views/menutypes/tmpl', - '/administrator/components/com_menus/views/menutypes', - '/administrator/components/com_menus/views/menus/tmpl', - '/administrator/components/com_menus/views/menus', - '/administrator/components/com_menus/views/menu/tmpl', - '/administrator/components/com_menus/views/menu', - '/administrator/components/com_menus/views/items/tmpl', - '/administrator/components/com_menus/views/items', - '/administrator/components/com_menus/views/item/tmpl', - '/administrator/components/com_menus/views/item', - '/administrator/components/com_menus/views', - '/administrator/components/com_menus/tables', - '/administrator/components/com_menus/models/forms', - '/administrator/components/com_menus/models/fields/modal', - '/administrator/components/com_menus/models/fields', - '/administrator/components/com_menus/models', - '/administrator/components/com_menus/layouts/joomla/searchtools/default', - '/administrator/components/com_menus/helpers/html', - '/administrator/components/com_menus/controllers', - '/administrator/components/com_media/views/medialist/tmpl', - '/administrator/components/com_media/views/medialist', - '/administrator/components/com_media/views/media/tmpl', - '/administrator/components/com_media/views/media', - '/administrator/components/com_media/views/imageslist/tmpl', - '/administrator/components/com_media/views/imageslist', - '/administrator/components/com_media/views/images/tmpl', - '/administrator/components/com_media/views/images', - '/administrator/components/com_media/views', - '/administrator/components/com_media/models', - '/administrator/components/com_media/controllers', - '/administrator/components/com_login/views/login/tmpl', - '/administrator/components/com_login/views/login', - '/administrator/components/com_login/views', - '/administrator/components/com_login/models', - '/administrator/components/com_languages/views/overrides/tmpl', - '/administrator/components/com_languages/views/overrides', - '/administrator/components/com_languages/views/override/tmpl', - '/administrator/components/com_languages/views/override', - '/administrator/components/com_languages/views/multilangstatus/tmpl', - '/administrator/components/com_languages/views/multilangstatus', - '/administrator/components/com_languages/views/languages/tmpl', - '/administrator/components/com_languages/views/languages', - '/administrator/components/com_languages/views/language/tmpl', - '/administrator/components/com_languages/views/language', - '/administrator/components/com_languages/views/installed/tmpl', - '/administrator/components/com_languages/views/installed', - '/administrator/components/com_languages/views', - '/administrator/components/com_languages/models/forms', - '/administrator/components/com_languages/models/fields', - '/administrator/components/com_languages/models', - '/administrator/components/com_languages/layouts/joomla/searchtools/default', - '/administrator/components/com_languages/layouts/joomla/searchtools', - '/administrator/components/com_languages/layouts/joomla', - '/administrator/components/com_languages/layouts', - '/administrator/components/com_languages/helpers/html', - '/administrator/components/com_languages/helpers', - '/administrator/components/com_languages/controllers', - '/administrator/components/com_joomlaupdate/views/upload/tmpl', - '/administrator/components/com_joomlaupdate/views/upload', - '/administrator/components/com_joomlaupdate/views/update/tmpl', - '/administrator/components/com_joomlaupdate/views/update', - '/administrator/components/com_joomlaupdate/views/default/tmpl', - '/administrator/components/com_joomlaupdate/views/default', - '/administrator/components/com_joomlaupdate/views', - '/administrator/components/com_joomlaupdate/models', - '/administrator/components/com_joomlaupdate/helpers', - '/administrator/components/com_joomlaupdate/controllers', - '/administrator/components/com_installer/views/warnings/tmpl', - '/administrator/components/com_installer/views/warnings', - '/administrator/components/com_installer/views/updatesites/tmpl', - '/administrator/components/com_installer/views/updatesites', - '/administrator/components/com_installer/views/update/tmpl', - '/administrator/components/com_installer/views/update', - '/administrator/components/com_installer/views/manage/tmpl', - '/administrator/components/com_installer/views/manage', - '/administrator/components/com_installer/views/languages/tmpl', - '/administrator/components/com_installer/views/languages', - '/administrator/components/com_installer/views/install/tmpl', - '/administrator/components/com_installer/views/install', - '/administrator/components/com_installer/views/discover/tmpl', - '/administrator/components/com_installer/views/discover', - '/administrator/components/com_installer/views/default/tmpl', - '/administrator/components/com_installer/views/default', - '/administrator/components/com_installer/views/database/tmpl', - '/administrator/components/com_installer/views/database', - '/administrator/components/com_installer/views', - '/administrator/components/com_installer/models/forms', - '/administrator/components/com_installer/models/fields', - '/administrator/components/com_installer/models', - '/administrator/components/com_installer/helpers/html', - '/administrator/components/com_installer/controllers', - '/administrator/components/com_finder/views/statistics/tmpl', - '/administrator/components/com_finder/views/statistics', - '/administrator/components/com_finder/views/maps/tmpl', - '/administrator/components/com_finder/views/maps', - '/administrator/components/com_finder/views/indexer/tmpl', - '/administrator/components/com_finder/views/indexer', - '/administrator/components/com_finder/views/index/tmpl', - '/administrator/components/com_finder/views/index', - '/administrator/components/com_finder/views/filters/tmpl', - '/administrator/components/com_finder/views/filters', - '/administrator/components/com_finder/views/filter/tmpl', - '/administrator/components/com_finder/views/filter', - '/administrator/components/com_finder/views', - '/administrator/components/com_finder/tables', - '/administrator/components/com_finder/models/forms', - '/administrator/components/com_finder/models/fields', - '/administrator/components/com_finder/models', - '/administrator/components/com_finder/helpers/indexer/stemmer', - '/administrator/components/com_finder/helpers/indexer/parser', - '/administrator/components/com_finder/helpers/indexer/driver', - '/administrator/components/com_finder/helpers/html', - '/administrator/components/com_finder/controllers', - '/administrator/components/com_fields/views/groups/tmpl', - '/administrator/components/com_fields/views/groups', - '/administrator/components/com_fields/views/group/tmpl', - '/administrator/components/com_fields/views/group', - '/administrator/components/com_fields/views/fields/tmpl', - '/administrator/components/com_fields/views/fields', - '/administrator/components/com_fields/views/field/tmpl', - '/administrator/components/com_fields/views/field', - '/administrator/components/com_fields/views', - '/administrator/components/com_fields/tables', - '/administrator/components/com_fields/models/forms', - '/administrator/components/com_fields/models/fields', - '/administrator/components/com_fields/models', - '/administrator/components/com_fields/libraries', - '/administrator/components/com_fields/controllers', - '/administrator/components/com_cpanel/views/cpanel/tmpl', - '/administrator/components/com_cpanel/views/cpanel', - '/administrator/components/com_cpanel/views', - '/administrator/components/com_contenthistory/views/preview/tmpl', - '/administrator/components/com_contenthistory/views/preview', - '/administrator/components/com_contenthistory/views/history/tmpl', - '/administrator/components/com_contenthistory/views/history', - '/administrator/components/com_contenthistory/views/compare/tmpl', - '/administrator/components/com_contenthistory/views/compare', - '/administrator/components/com_contenthistory/views', - '/administrator/components/com_contenthistory/models', - '/administrator/components/com_contenthistory/helpers/html', - '/administrator/components/com_contenthistory/controllers', - '/administrator/components/com_content/views/featured/tmpl', - '/administrator/components/com_content/views/featured', - '/administrator/components/com_content/views/articles/tmpl', - '/administrator/components/com_content/views/articles', - '/administrator/components/com_content/views/article/tmpl', - '/administrator/components/com_content/views/article', - '/administrator/components/com_content/views', - '/administrator/components/com_content/tables', - '/administrator/components/com_content/models/forms', - '/administrator/components/com_content/models/fields/modal', - '/administrator/components/com_content/models/fields', - '/administrator/components/com_content/models', - '/administrator/components/com_content/helpers/html', - '/administrator/components/com_content/controllers', - '/administrator/components/com_contact/views/contacts/tmpl', - '/administrator/components/com_contact/views/contacts', - '/administrator/components/com_contact/views/contact/tmpl', - '/administrator/components/com_contact/views/contact', - '/administrator/components/com_contact/views', - '/administrator/components/com_contact/tables', - '/administrator/components/com_contact/models/forms/fields', - '/administrator/components/com_contact/models/forms', - '/administrator/components/com_contact/models/fields/modal', - '/administrator/components/com_contact/models/fields', - '/administrator/components/com_contact/models', - '/administrator/components/com_contact/helpers/html', - '/administrator/components/com_contact/controllers', - '/administrator/components/com_config/view/component/tmpl', - '/administrator/components/com_config/view/component', - '/administrator/components/com_config/view/application/tmpl', - '/administrator/components/com_config/view/application', - '/administrator/components/com_config/view', - '/administrator/components/com_config/models', - '/administrator/components/com_config/model/form', - '/administrator/components/com_config/model/field', - '/administrator/components/com_config/model', - '/administrator/components/com_config/helper', - '/administrator/components/com_config/controllers', - '/administrator/components/com_config/controller/component', - '/administrator/components/com_config/controller/application', - '/administrator/components/com_config/controller', - '/administrator/components/com_checkin/views/checkin/tmpl', - '/administrator/components/com_checkin/views/checkin', - '/administrator/components/com_checkin/views', - '/administrator/components/com_checkin/models/forms', - '/administrator/components/com_checkin/models', - '/administrator/components/com_categories/views/category/tmpl', - '/administrator/components/com_categories/views/category', - '/administrator/components/com_categories/views/categories/tmpl', - '/administrator/components/com_categories/views/categories', - '/administrator/components/com_categories/views', - '/administrator/components/com_categories/tables', - '/administrator/components/com_categories/models/forms', - '/administrator/components/com_categories/models/fields/modal', - '/administrator/components/com_categories/models/fields', - '/administrator/components/com_categories/models', - '/administrator/components/com_categories/helpers/html', - '/administrator/components/com_categories/controllers', - '/administrator/components/com_cache/views/purge/tmpl', - '/administrator/components/com_cache/views/purge', - '/administrator/components/com_cache/views/cache/tmpl', - '/administrator/components/com_cache/views/cache', - '/administrator/components/com_cache/views', - '/administrator/components/com_cache/models/forms', - '/administrator/components/com_cache/models', - '/administrator/components/com_cache/helpers', - '/administrator/components/com_banners/views/tracks/tmpl', - '/administrator/components/com_banners/views/tracks', - '/administrator/components/com_banners/views/download/tmpl', - '/administrator/components/com_banners/views/download', - '/administrator/components/com_banners/views/clients/tmpl', - '/administrator/components/com_banners/views/clients', - '/administrator/components/com_banners/views/client/tmpl', - '/administrator/components/com_banners/views/client', - '/administrator/components/com_banners/views/banners/tmpl', - '/administrator/components/com_banners/views/banners', - '/administrator/components/com_banners/views/banner/tmpl', - '/administrator/components/com_banners/views/banner', - '/administrator/components/com_banners/views', - '/administrator/components/com_banners/tables', - '/administrator/components/com_banners/models/forms', - '/administrator/components/com_banners/models/fields', - '/administrator/components/com_banners/models', - '/administrator/components/com_banners/helpers/html', - '/administrator/components/com_banners/controllers', - '/administrator/components/com_associations/views/associations/tmpl', - '/administrator/components/com_associations/views/associations', - '/administrator/components/com_associations/views/association/tmpl', - '/administrator/components/com_associations/views/association', - '/administrator/components/com_associations/views', - '/administrator/components/com_associations/models/forms', - '/administrator/components/com_associations/models/fields', - '/administrator/components/com_associations/models', - '/administrator/components/com_associations/layouts/joomla/searchtools/default', - '/administrator/components/com_associations/helpers', - '/administrator/components/com_associations/controllers', - '/administrator/components/com_admin/views/sysinfo/tmpl', - '/administrator/components/com_admin/views/sysinfo', - '/administrator/components/com_admin/views/profile/tmpl', - '/administrator/components/com_admin/views/profile', - '/administrator/components/com_admin/views/help/tmpl', - '/administrator/components/com_admin/views/help', - '/administrator/components/com_admin/views', - '/administrator/components/com_admin/sql/updates/sqlazure', - '/administrator/components/com_admin/models/forms', - '/administrator/components/com_admin/models', - '/administrator/components/com_admin/helpers/html', - '/administrator/components/com_admin/helpers', - '/administrator/components/com_admin/controllers', - '/administrator/components/com_actionlogs/views/actionlogs/tmpl', - '/administrator/components/com_actionlogs/views/actionlogs', - '/administrator/components/com_actionlogs/views', - '/administrator/components/com_actionlogs/models/forms', - '/administrator/components/com_actionlogs/models/fields', - '/administrator/components/com_actionlogs/models', - '/administrator/components/com_actionlogs/libraries', - '/administrator/components/com_actionlogs/layouts', - '/administrator/components/com_actionlogs/helpers', - '/administrator/components/com_actionlogs/controllers', - // 4.0 from Beta 1 to Beta 2 - '/libraries/vendor/joomla/controller/src', - '/libraries/vendor/joomla/controller', - '/api/components/com_installer/src/View/Languages', - '/administrator/components/com_finder/src/Indexer/Driver', - // 4.0 from Beta 4 to Beta 5 - '/plugins/content/imagelazyload', - // 4.0 from Beta 5 to Beta 6 - '/media/system/js/core.es6', - '/administrator/modules/mod_multilangstatus/src/Helper', - '/administrator/modules/mod_multilangstatus/src', - // 4.0 from Beta 6 to Beta 7 - '/media/vendor/skipto/css', - // 4.0 from Beta 7 to RC 1 - '/templates/system/js', - '/templates/cassiopeia/scss/tools/mixins', - '/plugins/fields/subfields/tmpl', - '/plugins/fields/subfields/params', - '/plugins/fields/subfields', - '/media/vendor/punycode/js', - '/media/templates/atum/js', - '/media/templates/atum', - '/libraries/vendor/paragonie/random_compat/dist', - '/libraries/vendor/paragonie/random_compat', - '/libraries/vendor/ozdemirburak/iris/src/Traits', - '/libraries/vendor/ozdemirburak/iris/src/Helpers', - '/libraries/vendor/ozdemirburak/iris/src/Exceptions', - '/libraries/vendor/ozdemirburak/iris/src/Color', - '/libraries/vendor/ozdemirburak/iris/src', - '/libraries/vendor/ozdemirburak/iris', - '/libraries/vendor/ozdemirburak', - '/libraries/vendor/bin', - '/components/com_menus/src/Controller', - '/components/com_csp/src/Controller', - '/components/com_csp/src', - '/components/com_csp', - '/administrator/templates/atum/Service/HTML', - '/administrator/templates/atum/Service', - '/administrator/components/com_joomlaupdate/src/Helper', - '/administrator/components/com_csp/tmpl/reports', - '/administrator/components/com_csp/tmpl', - '/administrator/components/com_csp/src/View/Reports', - '/administrator/components/com_csp/src/View', - '/administrator/components/com_csp/src/Table', - '/administrator/components/com_csp/src/Model', - '/administrator/components/com_csp/src/Helper', - '/administrator/components/com_csp/src/Controller', - '/administrator/components/com_csp/src', - '/administrator/components/com_csp/services', - '/administrator/components/com_csp/forms', - '/administrator/components/com_csp', - '/administrator/components/com_admin/tmpl/profile', - '/administrator/components/com_admin/src/View/Profile', - '/administrator/components/com_admin/forms', - // 4.0 from RC 5 to RC 6 - '/templates/cassiopeia/scss/vendor/fontawesome-free', - '/templates/cassiopeia/css/vendor/fontawesome-free', - '/media/templates/cassiopeia/js/mod_menu', - '/media/templates/cassiopeia/js', - '/media/templates/cassiopeia', - // 4.0 from RC 6 to 4.0.0 (stable) - '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests', - '/libraries/vendor/willdurand/negotiation/tests/Negotiation', - '/libraries/vendor/willdurand/negotiation/tests', - '/libraries/vendor/jakeasmith/http_build_url/tests', - '/libraries/vendor/doctrine/inflector/docs/en', - '/libraries/vendor/doctrine/inflector/docs', - '/libraries/vendor/algo26-matthias/idna-convert/tests/unit', - '/libraries/vendor/algo26-matthias/idna-convert/tests/integration', - '/libraries/vendor/algo26-matthias/idna-convert/tests', - // From 4.0.3 to 4.0.4 - '/templates/cassiopeia/images/system', - // From 4.0.x to 4.1.0-beta1 - '/templates/system/scss', - '/templates/system/css', - '/templates/cassiopeia/scss/vendor/metismenu', - '/templates/cassiopeia/scss/vendor/joomla-custom-elements', - '/templates/cassiopeia/scss/vendor/choicesjs', - '/templates/cassiopeia/scss/vendor/bootstrap', - '/templates/cassiopeia/scss/vendor', - '/templates/cassiopeia/scss/tools/variables', - '/templates/cassiopeia/scss/tools/functions', - '/templates/cassiopeia/scss/tools', - '/templates/cassiopeia/scss/system/searchtools', - '/templates/cassiopeia/scss/system', - '/templates/cassiopeia/scss/global', - '/templates/cassiopeia/scss/blocks', - '/templates/cassiopeia/scss', - '/templates/cassiopeia/js', - '/templates/cassiopeia/images', - '/templates/cassiopeia/css/vendor/joomla-custom-elements', - '/templates/cassiopeia/css/vendor/choicesjs', - '/templates/cassiopeia/css/vendor', - '/templates/cassiopeia/css/system/searchtools', - '/templates/cassiopeia/css/system', - '/templates/cassiopeia/css/global', - '/templates/cassiopeia/css', - '/administrator/templates/system/scss', - '/administrator/templates/system/images', - '/administrator/templates/system/css', - '/administrator/templates/atum/scss/vendor/minicolors', - '/administrator/templates/atum/scss/vendor/joomla-custom-elements', - '/administrator/templates/atum/scss/vendor/fontawesome-free', - '/administrator/templates/atum/scss/vendor/choicesjs', - '/administrator/templates/atum/scss/vendor/bootstrap', - '/administrator/templates/atum/scss/vendor/awesomplete', - '/administrator/templates/atum/scss/vendor', - '/administrator/templates/atum/scss/system/searchtools', - '/administrator/templates/atum/scss/system', - '/administrator/templates/atum/scss/pages', - '/administrator/templates/atum/scss/blocks', - '/administrator/templates/atum/scss', - '/administrator/templates/atum/images/logos', - '/administrator/templates/atum/images', - '/administrator/templates/atum/css/vendor/minicolors', - '/administrator/templates/atum/css/vendor/joomla-custom-elements', - '/administrator/templates/atum/css/vendor/fontawesome-free', - '/administrator/templates/atum/css/vendor/choicesjs', - '/administrator/templates/atum/css/vendor/awesomplete', - '/administrator/templates/atum/css/vendor', - '/administrator/templates/atum/css/system/searchtools', - '/administrator/templates/atum/css/system', - '/administrator/templates/atum/css', - // From 4.1.0-beta3 to 4.1.0-rc1 - '/api/components/com_media/src/Helper', - // From 4.1.0 to 4.1.1 - '/libraries/vendor/tobscure/json-api/tests/Exception/Handler', - '/libraries/vendor/tobscure/json-api/tests/Exception', - '/libraries/vendor/tobscure/json-api/tests', - '/libraries/vendor/tobscure/json-api/.git/refs/tags', - '/libraries/vendor/tobscure/json-api/.git/refs/remotes/origin', - '/libraries/vendor/tobscure/json-api/.git/refs/remotes', - '/libraries/vendor/tobscure/json-api/.git/refs/heads', - '/libraries/vendor/tobscure/json-api/.git/refs', - '/libraries/vendor/tobscure/json-api/.git/objects/pack', - '/libraries/vendor/tobscure/json-api/.git/objects/info', - '/libraries/vendor/tobscure/json-api/.git/objects', - '/libraries/vendor/tobscure/json-api/.git/logs/refs/remotes/origin', - '/libraries/vendor/tobscure/json-api/.git/logs/refs/remotes', - '/libraries/vendor/tobscure/json-api/.git/logs/refs/heads', - '/libraries/vendor/tobscure/json-api/.git/logs/refs', - '/libraries/vendor/tobscure/json-api/.git/logs', - '/libraries/vendor/tobscure/json-api/.git/info', - '/libraries/vendor/tobscure/json-api/.git/hooks', - '/libraries/vendor/tobscure/json-api/.git/branches', - '/libraries/vendor/tobscure/json-api/.git', - // From 4.1.3 to 4.1.4 - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/Storage', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataFormatter', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests', - '/libraries/vendor/maximebf/debugbar/tests/DebugBar', - '/libraries/vendor/maximebf/debugbar/tests', - '/libraries/vendor/maximebf/debugbar/docs', - '/libraries/vendor/maximebf/debugbar/demo/bridge/twig', - '/libraries/vendor/maximebf/debugbar/demo/bridge/swiftmailer', - '/libraries/vendor/maximebf/debugbar/demo/bridge/slim', - '/libraries/vendor/maximebf/debugbar/demo/bridge/propel', - '/libraries/vendor/maximebf/debugbar/demo/bridge/monolog', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/src/Demo', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/src', - '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine', - '/libraries/vendor/maximebf/debugbar/demo/bridge/cachecache', - '/libraries/vendor/maximebf/debugbar/demo/bridge', - '/libraries/vendor/maximebf/debugbar/demo', - '/libraries/vendor/maximebf/debugbar/build', - // From 4.1 to 4.2.0-beta1 - '/plugins/twofactorauth/yubikey/tmpl', - '/plugins/twofactorauth/yubikey', - '/plugins/twofactorauth/totp/tmpl', - '/plugins/twofactorauth/totp/postinstall', - '/plugins/twofactorauth/totp', - '/plugins/twofactorauth', - '/libraries/vendor/nyholm/psr7/doc', - // From 4.2.0-beta1 to 4.2.0-beta2 - '/layouts/plugins/user/profile/fields', - '/layouts/plugins/user/profile', - ); - - $status['files_checked'] = $files; - $status['folders_checked'] = $folders; - - foreach ($files as $file) - { - if ($fileExists = File::exists(JPATH_ROOT . $file)) - { - $status['files_exist'][] = $file; - - if ($dryRun === false) - { - if (File::delete(JPATH_ROOT . $file)) - { - $status['files_deleted'][] = $file; - } - else - { - $status['files_errors'][] = Text::sprintf('FILES_JOOMLA_ERROR_FILE_FOLDER', $file); - } - } - } - } - - $this->moveRemainingTemplateFiles(); - - foreach ($folders as $folder) - { - if ($folderExists = Folder::exists(JPATH_ROOT . $folder)) - { - $status['folders_exist'][] = $folder; - - if ($dryRun === false) - { - if (Folder::delete(JPATH_ROOT . $folder)) - { - $status['folders_deleted'][] = $folder; - } - else - { - $status['folders_errors'][] = Text::sprintf('FILES_JOOMLA_ERROR_FILE_FOLDER', $folder); - } - } - } - } - - $this->fixFilenameCasing(); - - /* - * Needed for updates from 3.10 - * If com_search doesn't exist then assume we can delete the search package manifest (included in the update packages) - * We deliberately check for the presence of the files in case people have previously uninstalled their search extension - * but an update has put the files back. In that case it exists even if they don't believe in it! - */ - if (!File::exists(JPATH_ROOT . '/administrator/components/com_search/search.php') - && File::exists(JPATH_ROOT . '/administrator/manifests/packages/pkg_search.xml')) - { - File::delete(JPATH_ROOT . '/administrator/manifests/packages/pkg_search.xml'); - } - - if ($suppressOutput === false && count($status['folders_errors'])) - { - echo implode('
', $status['folders_errors']); - } - - if ($suppressOutput === false && count($status['files_errors'])) - { - echo implode('
', $status['files_errors']); - } - - return $status; - } - - /** - * Method to create assets for newly installed components - * - * @param Installer $installer The class calling this method - * - * @return boolean - * - * @since 3.2 - */ - public function updateAssets($installer) - { - // List all components added since 4.0 - $newComponents = array( - // Components to be added here - ); - - foreach ($newComponents as $component) - { - /** @var \Joomla\CMS\Table\Asset $asset */ - $asset = Table::getInstance('Asset'); - - if ($asset->loadByName($component)) - { - continue; - } - - $asset->name = $component; - $asset->parent_id = 1; - $asset->rules = '{}'; - $asset->title = $component; - $asset->setLocation(1, 'last-child'); - - if (!$asset->store()) - { - // Install failed, roll back changes - $installer->abort(Text::sprintf('JLIB_INSTALLER_ABORT_COMP_INSTALL_ROLLBACK', $asset->getError(true))); - - return false; - } - } - - return true; - } - - /** - * Converts the site's database tables to support UTF-8 Multibyte. - * - * @param boolean $doDbFixMsg Flag if message to be shown to check db fix - * - * @return void - * - * @since 3.5 - */ - public function convertTablesToUtf8mb4($doDbFixMsg = false) - { - $db = Factory::getDbo(); - - if ($db->getServerType() !== 'mysql') - { - return; - } - - // Check if the #__utf8_conversion table exists - $db->setQuery('SHOW TABLES LIKE ' . $db->quote($db->getPrefix() . 'utf8_conversion')); - - try - { - $rows = $db->loadRowList(0); - } - catch (Exception $e) - { - // Render the error message from the Exception object - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - if ($doDbFixMsg) - { - // Show an error message telling to check database problems - Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_DATABASE_UPGRADE_FAILED'), 'error'); - } - - return; - } - - // Nothing to do if the table doesn't exist because the CMS has never been updated from a pre-4.0 version - if (count($rows) === 0) - { - return; - } - - // Set required conversion status - $converted = 5; - - // Check conversion status in database - $db->setQuery( - 'SELECT ' . $db->quoteName('converted') - . ' FROM ' . $db->quoteName('#__utf8_conversion') - ); - - try - { - $convertedDB = $db->loadResult(); - } - catch (Exception $e) - { - // Render the error message from the Exception object - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - if ($doDbFixMsg) - { - // Show an error message telling to check database problems - Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_DATABASE_UPGRADE_FAILED'), 'error'); - } - - return; - } - - // If conversion status from DB is equal to required final status, try to drop the #__utf8_conversion table - if ($convertedDB === $converted) - { - $this->dropUtf8ConversionTable(); - - return; - } - - // Perform the required conversions of core tables if not done already in a previous step - if ($convertedDB !== 99) - { - $fileName1 = JPATH_ROOT . '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion.sql'; - - if (is_file($fileName1)) - { - $fileContents1 = @file_get_contents($fileName1); - $queries1 = $db->splitSql($fileContents1); - - if (!empty($queries1)) - { - foreach ($queries1 as $query1) - { - try - { - $db->setQuery($query1)->execute(); - } - catch (Exception $e) - { - $converted = $convertedDB; - - // Still render the error message from the Exception object - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - } - } - } - - // If no error before, perform the optional conversions of tables which might or might not exist - if ($converted === 5) - { - $fileName2 = JPATH_ROOT . '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion_optional.sql'; - - if (is_file($fileName2)) - { - $fileContents2 = @file_get_contents($fileName2); - $queries2 = $db->splitSql($fileContents2); - - if (!empty($queries2)) - { - foreach ($queries2 as $query2) - { - // Get table name from query - if (preg_match('/^ALTER\s+TABLE\s+([^\s]+)\s+/i', $query2, $matches) === 1) - { - $tableName = str_replace('`', '', $matches[1]); - $tableName = str_replace('#__', $db->getPrefix(), $tableName); - - // Check if the table exists and if yes, run the query - try - { - $db->setQuery('SHOW TABLES LIKE ' . $db->quote($tableName)); - - $rows = $db->loadRowList(0); - - if (count($rows) > 0) - { - $db->setQuery($query2)->execute(); - } - } - catch (Exception $e) - { - $converted = 99; - - // Still render the error message from the Exception object - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - } - } - } - } - - if ($doDbFixMsg && $converted !== 5) - { - // Show an error message telling to check database problems - Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_DATABASE_UPGRADE_FAILED'), 'error'); - } - - // If the conversion was successful try to drop the #__utf8_conversion table - if ($converted === 5 && $this->dropUtf8ConversionTable()) - { - // Table successfully dropped - return; - } - - // Set flag in database if the conversion status has changed. - if ($converted !== $convertedDB) - { - $db->setQuery('UPDATE ' . $db->quoteName('#__utf8_conversion') - . ' SET ' . $db->quoteName('converted') . ' = ' . $converted . ';' - )->execute(); - } - } - - /** - * This method clean the Joomla Cache using the method `clean` from the com_cache model - * - * @return void - * - * @since 3.5.1 - */ - private function cleanJoomlaCache() - { - /** @var \Joomla\Component\Cache\Administrator\Model\CacheModel $model */ - $model = Factory::getApplication()->bootComponent('com_cache')->getMVCFactory() - ->createModel('Cache', 'Administrator', ['ignore_request' => true]); - - // Clean frontend cache - $model->clean(); - - // Clean admin cache - $model->setState('client_id', 1); - $model->clean(); - } - - /** - * This method drops the #__utf8_conversion table - * - * @return boolean True on success - * - * @since 4.0.0 - */ - private function dropUtf8ConversionTable() - { - $db = Factory::getDbo(); - - try - { - $db->setQuery('DROP TABLE ' . $db->quoteName('#__utf8_conversion') . ';' - )->execute(); - } - catch (Exception $e) - { - return false; - } - - return true; - } - - /** - * Called after any type of action - * - * @param string $action Which action is happening (install|uninstall|discover_install|update) - * @param Installer $installer The class calling this method - * - * @return boolean True on success - * - * @since 4.0.0 - */ - public function postflight($action, $installer) - { - if ($action !== 'update') - { - return true; - } - - if (empty($this->fromVersion) || version_compare($this->fromVersion, '4.0.0', 'ge')) - { - return true; - } - - // Update UCM content types. - $this->updateContentTypes(); - - $db = Factory::getDbo(); - Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_menus/Table/'); - - $tableItem = new \Joomla\Component\Menus\Administrator\Table\MenuTable($db); - - $contactItems = $this->contactItems($tableItem); - $finderItems = $this->finderItems($tableItem); - - $menuItems = array_merge($contactItems, $finderItems); - - foreach ($menuItems as $menuItem) - { - // Check an existing record - $keys = [ - 'menutype' => $menuItem['menutype'], - 'type' => $menuItem['type'], - 'title' => $menuItem['title'], - 'parent_id' => $menuItem['parent_id'], - 'client_id' => $menuItem['client_id'], - ]; - - if ($tableItem->load($keys)) - { - continue; - } - - $newTableItem = new \Joomla\Component\Menus\Administrator\Table\MenuTable($db); - - // Bind the data. - if (!$newTableItem->bind($menuItem)) - { - return false; - } - - $newTableItem->setLocation($menuItem['parent_id'], 'last-child'); - - // Check the data. - if (!$newTableItem->check()) - { - return false; - } - - // Store the data. - if (!$newTableItem->store()) - { - return false; - } - - // Rebuild the tree path. - if (!$newTableItem->rebuildPath($newTableItem->id)) - { - return false; - } - } - - return true; - } - - /** - * Prepare the contact menu items - * - * @return array Menu items - * - * @since 4.0.0 - */ - private function contactItems(Table $tableItem): array - { - // Check for the Contact parent Id Menu Item - $keys = [ - 'menutype' => 'main', - 'type' => 'component', - 'title' => 'com_contact', - 'parent_id' => 1, - 'client_id' => 1, - ]; - - $contactMenuitem = $tableItem->load($keys); - - if (!$contactMenuitem) - { - return []; - } - - $parentId = $tableItem->id; - $componentId = ExtensionHelper::getExtensionRecord('com_fields', 'component')->extension_id; - - // Add Contact Fields Menu Items. - $menuItems = [ - [ - 'menutype' => 'main', - 'title' => '-', - 'alias' => microtime(true), - 'note' => '', - 'path' => '', - 'link' => '#', - 'type' => 'separator', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ], - [ - 'menutype' => 'main', - 'title' => 'mod_menu_fields', - 'alias' => 'Contact Custom Fields', - 'note' => '', - 'path' => 'contact/Custom Fields', - 'link' => 'index.php?option=com_fields&context=com_contact.contact', - 'type' => 'component', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ], - [ - 'menutype' => 'main', - 'title' => 'mod_menu_fields_group', - 'alias' => 'Contact Custom Fields Group', - 'note' => '', - 'path' => 'contact/Custom Fields Group', - 'link' => 'index.php?option=com_fields&view=groups&context=com_contact.contact', - 'type' => 'component', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ] - ]; - - return $menuItems; - } - - /** - * Prepare the finder menu items - * - * @return array Menu items - * - * @since 4.0.0 - */ - private function finderItems(Table $tableItem): array - { - // Check for the Finder parent Id Menu Item - $keys = [ - 'menutype' => 'main', - 'type' => 'component', - 'title' => 'com_finder', - 'parent_id' => 1, - 'client_id' => 1, - ]; - - $finderMenuitem = $tableItem->load($keys); - - if (!$finderMenuitem) - { - return []; - } - - $parentId = $tableItem->id; - $componentId = ExtensionHelper::getExtensionRecord('com_finder', 'component')->extension_id; - - // Add Finder Fields Menu Items. - $menuItems = [ - [ - 'menutype' => 'main', - 'title' => '-', - 'alias' => microtime(true), - 'note' => '', - 'path' => '', - 'link' => '#', - 'type' => 'separator', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ], - [ - 'menutype' => 'main', - 'title' => 'com_finder_index', - 'alias' => 'Smart-Search-Index', - 'note' => '', - 'path' => 'Smart Search/Index', - 'link' => 'index.php?option=com_finder&view=index', - 'type' => 'component', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ], - [ - 'menutype' => 'main', - 'title' => 'com_finder_maps', - 'alias' => 'Smart-Search-Maps', - 'note' => '', - 'path' => 'Smart Search/Maps', - 'link' => 'index.php?option=com_finder&view=maps', - 'type' => 'component', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ], - [ - 'menutype' => 'main', - 'title' => 'com_finder_filters', - 'alias' => 'Smart-Search-Filters', - 'note' => '', - 'path' => 'Smart Search/Filters', - 'link' => 'index.php?option=com_finder&view=filters', - 'type' => 'component', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ], - [ - 'menutype' => 'main', - 'title' => 'com_finder_searches', - 'alias' => 'Smart-Search-Searches', - 'note' => '', - 'path' => 'Smart Search/Searches', - 'link' => 'index.php?option=com_finder&view=searches', - 'type' => 'component', - 'published' => 1, - 'parent_id' => $parentId, - 'level' => 2, - 'component_id' => $componentId, - 'checked_out' => null, - 'checked_out_time' => null, - 'browserNav' => 0, - 'access' => 0, - 'img' => '', - 'template_style_id' => 0, - 'params' => '{}', - 'home' => 0, - 'language' => '*', - 'client_id' => 1, - 'publish_up' => null, - 'publish_down' => null, - ] - ]; - - return $menuItems; - } - - /** - * Updates content type table classes. - * - * @return void - * - * @since 4.0.0 - */ - private function updateContentTypes(): void - { - // Content types to update. - $contentTypes = [ - 'com_content.article', - 'com_contact.contact', - 'com_newsfeeds.newsfeed', - 'com_tags.tag', - 'com_banners.banner', - 'com_banners.client', - 'com_users.note', - 'com_content.category', - 'com_contact.category', - 'com_newsfeeds.category', - 'com_banners.category', - 'com_users.category', - 'com_users.user', - ]; - - // Get table definitions. - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('type_alias'), - $db->quoteName('table'), - ] - ) - ->from($db->quoteName('#__content_types')) - ->whereIn($db->quoteName('type_alias'), $contentTypes, ParameterType::STRING); - - $db->setQuery($query); - $contentTypes = $db->loadObjectList(); - - // Prepare the update query. - $query = $db->getQuery(true) - ->update($db->quoteName('#__content_types')) - ->set($db->quoteName('table') . ' = :table') - ->where($db->quoteName('type_alias') . ' = :typeAlias') - ->bind(':table', $table) - ->bind(':typeAlias', $typeAlias); - - $db->setQuery($query); - - foreach ($contentTypes as $contentType) - { - list($component, $tableType) = explode('.', $contentType->type_alias); - - // Special case for core table classes. - if ($contentType->type_alias === 'com_users.users' || $tableType === 'category') - { - $tablePrefix = 'Joomla\\CMS\Table\\'; - $tableType = ucfirst($tableType); - } - else - { - $tablePrefix = 'Joomla\\Component\\' . ucfirst(substr($component, 4)) . '\\Administrator\\Table\\'; - $tableType = ucfirst($tableType) . 'Table'; - } - - // Bind type alias. - $typeAlias = $contentType->type_alias; - - $table = json_decode($contentType->table); - - // Update table definitions. - $table->special->type = $tableType; - $table->special->prefix = $tablePrefix; - - // Some content types don't have this property. - if (!empty($table->common->prefix)) - { - $table->common->prefix = 'Joomla\\CMS\\Table\\'; - } - - $table = json_encode($table); - - // Execute the query. - $db->execute(); - } - } - - /** - * Renames or removes incorrectly cased files. - * - * @return void - * - * @since 3.9.25 - */ - protected function fixFilenameCasing() - { - $files = array( - // 3.10 changes - '/libraries/src/Filesystem/Support/Stringcontroller.php' => '/libraries/src/Filesystem/Support/StringController.php', - '/libraries/src/Form/Rule/SubFormRule.php' => '/libraries/src/Form/Rule/SubformRule.php', - // 4.0.0 - '/media/vendor/skipto/js/skipTo.js' => '/media/vendor/skipto/js/skipto.js', - ); - - foreach ($files as $old => $expected) - { - $oldRealpath = realpath(JPATH_ROOT . $old); - - // On Unix without incorrectly cased file. - if ($oldRealpath === false) - { - continue; - } - - $oldBasename = basename($oldRealpath); - $newRealpath = realpath(JPATH_ROOT . $expected); - $newBasename = basename($newRealpath); - $expectedBasename = basename($expected); - - // On Windows or Unix with only the incorrectly cased file. - if ($newBasename !== $expectedBasename) - { - // Rename the file. - File::move(JPATH_ROOT . $old, JPATH_ROOT . $old . '.tmp'); - File::move(JPATH_ROOT . $old . '.tmp', JPATH_ROOT . $expected); - - continue; - } - - // There might still be an incorrectly cased file on other OS than Windows. - if ($oldBasename === basename($old)) - { - // Check if case-insensitive file system, eg on OSX. - if (fileinode($oldRealpath) === fileinode($newRealpath)) - { - // Check deeper because even realpath or glob might not return the actual case. - if (!in_array($expectedBasename, scandir(dirname($newRealpath)))) - { - // Rename the file. - File::move(JPATH_ROOT . $old, JPATH_ROOT . $old . '.tmp'); - File::move(JPATH_ROOT . $old . '.tmp', JPATH_ROOT . $expected); - } - } - else - { - // On Unix with both files: Delete the incorrectly cased file. - File::delete(JPATH_ROOT . $old); - } - } - } - } - - /** - * Move core template (s)css or js or image files which are left after deleting - * obsolete core files to the right place in media folder. - * - * @return void - * - * @since 4.1.0 - */ - protected function moveRemainingTemplateFiles() - { - $folders = [ - '/administrator/templates/atum/css' => '/media/templates/administrator/atum/css', - '/administrator/templates/atum/images' => '/media/templates/administrator/atum/images', - '/administrator/templates/atum/js' => '/media/templates/administrator/atum/js', - '/administrator/templates/atum/scss' => '/media/templates/administrator/atum/scss', - '/templates/cassiopeia/css' => '/media/templates/site/cassiopeia/css', - '/templates/cassiopeia/images' => '/media/templates/site/cassiopeia/images', - '/templates/cassiopeia/js' => '/media/templates/site/cassiopeia/js', - '/templates/cassiopeia/scss' => '/media/templates/site/cassiopeia/scss', - ]; - - foreach ($folders as $oldFolder => $newFolder) - { - if (Folder::exists(JPATH_ROOT . $oldFolder)) - { - $oldPath = realpath(JPATH_ROOT . $oldFolder); - $newPath = realpath(JPATH_ROOT . $newFolder); - $directory = new \RecursiveDirectoryIterator($oldPath); - $directory->setFlags(RecursiveDirectoryIterator::SKIP_DOTS); - $iterator = new \RecursiveIteratorIterator($directory); - - // Handle all files in this folder and all sub-folders - foreach ($iterator as $oldFile) - { - if ($oldFile->isDir()) - { - continue; - } - - $newFile = $newPath . substr($oldFile, strlen($oldPath)); - - // Create target folder and parent folders if they don't exist yet - if (is_dir(dirname($newFile)) || @mkdir(dirname($newFile), 0755, true)) - { - File::move($oldFile, $newFile); - } - } - } - } - } - - /** - * Ensure the core templates are correctly moved to the new mode. - * - * @return void - * - * @since 4.1.0 - */ - protected function fixTemplateMode(): void - { - $db = Factory::getContainer()->get('DatabaseDriver'); - - array_map( - function ($template) use ($db) - { - $clientId = $template === 'atum' ? 1 : 0; - $query = $db->getQuery(true) - ->update($db->quoteName('#__template_styles')) - ->set($db->quoteName('inheritable') . ' = 1') - ->where($db->quoteName('template') . ' = ' . $db->quote($template)) - ->where($db->quoteName('client_id') . ' = ' . $clientId); - - try - { - $db->setQuery($query)->execute(); - } - catch (Exception $e) - { - echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; - - return; - } - }, - ['atum', 'cassiopeia'] - ); - } - - /** - * Add the user Auth Provider Column as it could be present from 3.10 already - * - * @return void - * - * @since 4.1.1 - */ - protected function addUserAuthProviderColumn(): void - { - $db = Factory::getContainer()->get('DatabaseDriver'); - - // Check if the column already exists - $fields = $db->getTableColumns('#__users'); - - // Column exists, skip - if (isset($fields['authProvider'])) - { - return; - } - - $query = 'ALTER TABLE ' . $db->quoteName('#__users') - . ' ADD COLUMN ' . $db->quoteName('authProvider') . ' varchar(100) DEFAULT ' . $db->quote('') . ' NOT NULL'; - - // Add column - try - { - $db->setQuery($query)->execute(); - } - catch (Exception $e) - { - echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; - - return; - } - } + /** + * The Joomla Version we are updating from + * + * @var string + * @since 3.7 + */ + protected $fromVersion = null; + + /** + * Function to act prior to installation process begins + * + * @param string $action Which action is happening (install|uninstall|discover_install|update) + * @param Installer $installer The class calling this method + * + * @return boolean True on success + * + * @since 3.7.0 + */ + public function preflight($action, $installer) + { + if ($action === 'update') { + // Get the version we are updating from + if (!empty($installer->extension->manifest_cache)) { + $manifestValues = json_decode($installer->extension->manifest_cache, true); + + if (array_key_exists('version', $manifestValues)) { + $this->fromVersion = $manifestValues['version']; + + // Ensure templates are moved to the correct mode + $this->fixTemplateMode(); + + return true; + } + } + + return false; + } + + return true; + } + + /** + * Method to update Joomla! + * + * @param Installer $installer The class calling this method + * + * @return void + */ + public function update($installer) + { + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'joomla_update.php'; + + Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); + + try { + Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_DELETE_FILES'), Log::INFO, 'Update'); + } catch (RuntimeException $exception) { + // Informational log only + } + + // Uninstall plugins before removing their files and folders + $this->uninstallRepeatableFieldsPlugin(); + $this->uninstallEosPlugin(); + + // This needs to stay for 2.5 update compatibility + $this->deleteUnexistingFiles(); + $this->updateManifestCaches(); + $this->updateDatabase(); + $this->updateAssets($installer); + $this->clearStatsCache(); + $this->convertTablesToUtf8mb4(true); + $this->addUserAuthProviderColumn(); + $this->cleanJoomlaCache(); + } + + /** + * Method to clear our stats plugin cache to ensure we get fresh data on Joomla Update + * + * @return void + * + * @since 3.5 + */ + protected function clearStatsCache() + { + $db = Factory::getDbo(); + + try { + // Get the params for the stats plugin + $params = $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('params')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ->where($db->quoteName('element') . ' = ' . $db->quote('stats')) + )->loadResult(); + } catch (Exception $e) { + echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; + + return; + } + + $params = json_decode($params, true); + + // Reset the last run parameter + if (isset($params['lastrun'])) { + $params['lastrun'] = ''; + } + + $params = json_encode($params); + + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('params') . ' = ' . $db->quote($params)) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ->where($db->quoteName('element') . ' = ' . $db->quote('stats')); + + try { + $db->setQuery($query)->execute(); + } catch (Exception $e) { + echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; + + return; + } + } + + /** + * Method to update Database + * + * @return void + */ + protected function updateDatabase() + { + if (Factory::getDbo()->getServerType() === 'mysql') { + $this->updateDatabaseMysql(); + } + } + + /** + * Method to update MySQL Database + * + * @return void + */ + protected function updateDatabaseMysql() + { + $db = Factory::getDbo(); + + $db->setQuery('SHOW ENGINES'); + + try { + $results = $db->loadObjectList(); + } catch (Exception $e) { + echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; + + return; + } + + foreach ($results as $result) { + if ($result->Support != 'DEFAULT') { + continue; + } + + $db->setQuery('ALTER TABLE #__update_sites_extensions ENGINE = ' . $result->Engine); + + try { + $db->execute(); + } catch (Exception $e) { + echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; + + return; + } + + break; + } + } + + /** + * Uninstalls the plg_fields_repeatable plugin and transforms its custom field instances + * to instances of the plg_fields_subfields plugin. + * + * @return void + * + * @since 4.0.0 + */ + protected function uninstallRepeatableFieldsPlugin() + { + $app = Factory::getApplication(); + $db = Factory::getDbo(); + + // Check if the plg_fields_repeatable plugin is present + $extensionId = $db->setQuery( + $db->getQuery(true) + ->select('extension_id') + ->from('#__extensions') + ->where('name = ' . $db->quote('plg_fields_repeatable')) + )->loadResult(); + + // Skip uninstalling when it doesn't exist + if (!$extensionId) { + return; + } + + // Ensure the FieldsHelper class is loaded for the Repeatable fields plugin we're about to remove + \JLoader::register('FieldsHelper', JPATH_ADMINISTRATOR . '/components/com_fields/helpers/fields.php'); + + try { + $db->transactionStart(); + + // Get the FieldsModelField, we need it in a sec + $fieldModel = $app->bootComponent('com_fields')->getMVCFactory()->createModel('Field', 'Administrator', ['ignore_request' => true]); + /** @var FieldModel $fieldModel */ + + // Now get a list of all `repeatable` custom field instances + $db->setQuery( + $db->getQuery(true) + ->select('*') + ->from('#__fields') + ->where($db->quoteName('type') . ' = ' . $db->quote('repeatable')) + ); + + // Execute the query and iterate over the `repeatable` instances + foreach ($db->loadObjectList() as $row) { + // Skip broken rows - just a security measure, should not happen + if (!isset($row->fieldparams) || !($oldFieldparams = json_decode($row->fieldparams)) || !is_object($oldFieldparams)) { + continue; + } + + /** + * We basically want to transform this `repeatable` type into a `subfields` type. While $oldFieldparams + * holds the `fieldparams` of the `repeatable` type, $newFieldparams shall hold the `fieldparams` + * of the `subfields` type. + */ + $newFieldparams = [ + 'repeat' => '1', + 'options' => [], + ]; + + /** + * This array is used to store the mapping between the name of form fields from Repeatable field + * with ID of the child-fields. It will then be used to migrate data later + */ + $mapping = []; + + /** + * Store name of media fields which we need to convert data from old format (string) to new + * format (json) during the migration + */ + $mediaFields = []; + + // If this repeatable fields actually had child-fields (normally this is always the case) + if (isset($oldFieldparams->fields) && is_object($oldFieldparams->fields)) { + // Small counter for the child-fields (aka sub fields) + $newFieldCount = 0; + + // Iterate over the sub fields + foreach (get_object_vars($oldFieldparams->fields) as $oldField) { + // Used for field name collision prevention + $fieldname_prefix = ''; + $fieldname_suffix = 0; + + // Try to save the new sub field in a loop because of field name collisions + while (true) { + /** + * We basically want to create a completely new custom fields instance for every sub field + * of the `repeatable` instance. This is what we use $data for, we create a new custom field + * for each of the sub fields of the `repeatable` instance. + */ + $data = [ + 'context' => $row->context, + 'group_id' => $row->group_id, + 'title' => $oldField->fieldname, + 'name' => ( + $fieldname_prefix + . $oldField->fieldname + . ($fieldname_suffix > 0 ? ('_' . $fieldname_suffix) : '') + ), + 'label' => $oldField->fieldname, + 'default_value' => $row->default_value, + 'type' => $oldField->fieldtype, + 'description' => $row->description, + 'state' => '1', + 'params' => $row->params, + 'language' => '*', + 'assigned_cat_ids' => [-1], + 'only_use_in_subform' => 1, + ]; + + // `number` is not a valid custom field type, so use `text` instead. + if ($data['type'] == 'number') { + $data['type'] = 'text'; + } + + if ($data['type'] == 'media') { + $mediaFields[] = $oldField->fieldname; + } + + // Reset the state because else \Joomla\CMS\MVC\Model\AdminModel will take an already + // existing value (e.g. from previous save) and do an UPDATE instead of INSERT. + $fieldModel->setState('field.id', 0); + + // If an error occurred when trying to save this. + if (!$fieldModel->save($data)) { + // If the error is, that the name collided, increase the collision prevention + $error = $fieldModel->getError(); + + if ($error == 'COM_FIELDS_ERROR_UNIQUE_NAME') { + // If this is the first time this error occurs, set only the prefix + if ($fieldname_prefix == '') { + $fieldname_prefix = ($row->name . '_'); + } else { + // Else increase the suffix + $fieldname_suffix++; + } + + // And start again with the while loop. + continue 1; + } + + // Else bail out with the error. Something is totally wrong. + throw new \Exception($error); + } + + // Break out of the while loop, saving was successful. + break 1; + } + + // Get the newly created id + $subfield_id = $fieldModel->getState('field.id'); + + // Really check that it is valid + if (!is_numeric($subfield_id) || $subfield_id < 1) { + throw new \Exception('Something went wrong.'); + } + + // And tell our new `subfields` field about his child + $newFieldparams['options'][('option' . $newFieldCount)] = [ + 'customfield' => $subfield_id, + 'render_values' => '1', + ]; + + $newFieldCount++; + + $mapping[$oldField->fieldname] = 'field' . $subfield_id; + } + } + + // Write back the changed stuff to the database + $db->setQuery( + $db->getQuery(true) + ->update('#__fields') + ->set($db->quoteName('type') . ' = ' . $db->quote('subform')) + ->set($db->quoteName('fieldparams') . ' = ' . $db->quote(json_encode($newFieldparams))) + ->where($db->quoteName('id') . ' = ' . $db->quote($row->id)) + )->execute(); + + // Migrate data for this field + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__fields_values')) + ->where($db->quoteName('field_id') . ' = ' . $row->id); + $db->setQuery($query); + + foreach ($db->loadObjectList() as $rowFieldValue) { + // Do not do the version if no data is entered for the custom field this item + if (!$rowFieldValue->value) { + continue; + } + + /** + * Here we will have to update the stored value of the field to new format + * The key for each row changes from repeatable to row, for example repeatable0 to row0, and so on + * The key for each sub-field change from name of field to field + ID of the new sub-field + * Example data format stored in J3: {"repeatable0":{"id":"1","username":"admin"}} + * Example data format stored in J4: {"row0":{"field1":"1","field2":"admin"}} + */ + $newFieldValue = []; + + // Convert to array to change key + $fieldValue = json_decode($rowFieldValue->value, true); + + // If data could not be decoded for some reason, ignore + if (!$fieldValue) { + continue; + } + + $rowIndex = 0; + + foreach ($fieldValue as $rowKey => $rowValue) { + $rowKey = 'row' . ($rowIndex++); + $newFieldValue[$rowKey] = []; + + foreach ($rowValue as $subFieldName => $subFieldValue) { + // This is a media field, so we need to convert data to new format required in Joomla! 4 + if (in_array($subFieldName, $mediaFields)) { + $subFieldValue = ['imagefile' => $subFieldValue, 'alt_text' => '']; + } + + if (isset($mapping[$subFieldName])) { + $newFieldValue[$rowKey][$mapping[$subFieldName]] = $subFieldValue; + } else { + // Not found, use the old key to avoid data lost + $newFieldValue[$subFieldName] = $subFieldValue; + } + } + } + + $query->clear() + ->update($db->quoteName('#__fields_values')) + ->set($db->quoteName('value') . ' = ' . $db->quote(json_encode($newFieldValue))) + ->where($db->quoteName('field_id') . ' = ' . $rowFieldValue->field_id) + ->where($db->quoteName('item_id') . ' =' . $rowFieldValue->item_id); + $db->setQuery($query) + ->execute(); + } + } + + // Now, unprotect the plugin so we can uninstall it + $db->setQuery( + $db->getQuery(true) + ->update('#__extensions') + ->set('protected = 0') + ->where($db->quoteName('extension_id') . ' = ' . $extensionId) + )->execute(); + + // And now uninstall the plugin + $installer = new Installer(); + $installer->setDatabase($db); + $installer->uninstall('plugin', $extensionId); + + $db->transactionCommit(); + } catch (\Exception $e) { + $db->transactionRollback(); + throw $e; + } + } + + /** + * Uninstall the 3.10 EOS plugin + * + * @return void + * + * @since 4.0.0 + */ + protected function uninstallEosPlugin() + { + $db = Factory::getDbo(); + + // Check if the plg_quickicon_eos310 plugin is present + $extensionId = $db->setQuery( + $db->getQuery(true) + ->select('extension_id') + ->from('#__extensions') + ->where('name = ' . $db->quote('plg_quickicon_eos310')) + )->loadResult(); + + // Skip uninstalling if it doesn't exist + if (!$extensionId) { + return; + } + + try { + $db->transactionStart(); + + // Unprotect the plugin so we can uninstall it + $db->setQuery( + $db->getQuery(true) + ->update('#__extensions') + ->set('protected = 0') + ->where($db->quoteName('extension_id') . ' = ' . $extensionId) + )->execute(); + + // Uninstall the plugin + $installer = new Installer(); + $installer->setDatabase($db); + $installer->uninstall('plugin', $extensionId); + + $db->transactionCommit(); + } catch (\Exception $e) { + $db->transactionRollback(); + throw $e; + } + } + + /** + * Update the manifest caches + * + * @return void + */ + protected function updateManifestCaches() + { + $extensions = ExtensionHelper::getCoreExtensions(); + + // If we have the search package around, it may not have a manifest cache entry after upgrades from 3.x, so add it to the list + if (File::exists(JPATH_ROOT . '/administrator/manifests/packages/pkg_search.xml')) { + $extensions[] = array('package', 'pkg_search', '', 0); + } + + // Attempt to refresh manifest caches + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('*') + ->from('#__extensions'); + + foreach ($extensions as $extension) { + $query->where( + 'type=' . $db->quote($extension[0]) + . ' AND element=' . $db->quote($extension[1]) + . ' AND folder=' . $db->quote($extension[2]) + . ' AND client_id=' . $extension[3], + 'OR' + ); + } + + $db->setQuery($query); + + try { + $extensions = $db->loadObjectList(); + } catch (Exception $e) { + echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; + + return; + } + + $installer = new Installer(); + $installer->setDatabase($db); + + foreach ($extensions as $extension) { + if (!$installer->refreshManifestCache($extension->extension_id)) { + echo Text::sprintf('FILES_JOOMLA_ERROR_MANIFEST', $extension->type, $extension->element, $extension->name, $extension->client_id) . '
'; + } + } + } + + /** + * Delete files that should not exist + * + * @param bool $dryRun If set to true, will not actually delete files, but just report their status for use in CLI + * @param bool $suppressOutput Set to true to suppress echoing any errors, and just return the $status array + * + * @return array + */ + public function deleteUnexistingFiles($dryRun = false, $suppressOutput = false) + { + $status = [ + 'files_exist' => [], + 'folders_exist' => [], + 'files_deleted' => [], + 'folders_deleted' => [], + 'files_errors' => [], + 'folders_errors' => [], + 'folders_checked' => [], + 'files_checked' => [], + ]; + + $files = array( + // From 3.10 to 4.1 + '/administrator/components/com_actionlogs/actionlogs.php', + '/administrator/components/com_actionlogs/controller.php', + '/administrator/components/com_actionlogs/controllers/actionlogs.php', + '/administrator/components/com_actionlogs/helpers/actionlogs.php', + '/administrator/components/com_actionlogs/helpers/actionlogsphp55.php', + '/administrator/components/com_actionlogs/layouts/logstable.php', + '/administrator/components/com_actionlogs/libraries/actionlogplugin.php', + '/administrator/components/com_actionlogs/models/actionlog.php', + '/administrator/components/com_actionlogs/models/actionlogs.php', + '/administrator/components/com_actionlogs/models/fields/extension.php', + '/administrator/components/com_actionlogs/models/fields/logcreator.php', + '/administrator/components/com_actionlogs/models/fields/logsdaterange.php', + '/administrator/components/com_actionlogs/models/fields/logtype.php', + '/administrator/components/com_actionlogs/models/fields/plugininfo.php', + '/administrator/components/com_actionlogs/models/forms/filter_actionlogs.xml', + '/administrator/components/com_actionlogs/views/actionlogs/tmpl/default.php', + '/administrator/components/com_actionlogs/views/actionlogs/tmpl/default.xml', + '/administrator/components/com_actionlogs/views/actionlogs/view.html.php', + '/administrator/components/com_admin/admin.php', + '/administrator/components/com_admin/controller.php', + '/administrator/components/com_admin/controllers/profile.php', + '/administrator/components/com_admin/helpers/html/directory.php', + '/administrator/components/com_admin/helpers/html/phpsetting.php', + '/administrator/components/com_admin/helpers/html/system.php', + '/administrator/components/com_admin/models/forms/profile.xml', + '/administrator/components/com_admin/models/help.php', + '/administrator/components/com_admin/models/profile.php', + '/administrator/components/com_admin/models/sysinfo.php', + '/administrator/components/com_admin/postinstall/eaccelerator.php', + '/administrator/components/com_admin/postinstall/htaccess.php', + '/administrator/components/com_admin/postinstall/joomla40checks.php', + '/administrator/components/com_admin/postinstall/updatedefaultsettings.php', + '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion-01.sql', + '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion-02.sql', + '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-06.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-21-1.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-21-2.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-22.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-23.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2011-12-24.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2012-01-10.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.0-2012-01-14.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.1-2012-01-26.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.2-2012-03-05.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.3-2012-03-13.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.4-2012-03-18.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.4-2012-03-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.5.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.6.sql', + '/administrator/components/com_admin/sql/updates/mysql/2.5.7.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.0.0.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.0.1.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.0.2.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.0.3.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.1.0.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.1.1.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.1.2.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.1.3.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.1.4.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.1.5.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.10.0-2020-08-10.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.10.0-2021-05-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.10.7-2022-02-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.10.7-2022-03-18.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.0.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.1.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2013-12-22.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2013-12-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-08.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-15.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-18.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.2-2014-01-23.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.2.3-2014-02-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.3.0-2014-02-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.3.0-2014-04-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.3.4-2014-08-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.3.6-2014-09-30.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-08-24.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-09-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-09-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-10-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2014-12-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2015-01-21.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.4.0-2015-02-26.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-07-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-10-13.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-10-26.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-10-30.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-11-04.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2015-11-05.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2016-02-26.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.0-2016-03-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.1-2016-03-25.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.5.1-2016-03-29.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-06.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-08.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-04-09.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-05-06.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-06-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.0-2016-06-05.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.3-2016-08-15.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.6.3-2016-08-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-08-06.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-08-22.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-08-29.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-09-29.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-10-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-10-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-04.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-21.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-24.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2016-11-27.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-08.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-09.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-15.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-17.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-01-31.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-02-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-02-15.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-02-17.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-03-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-03-09.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-03-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-04-10.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.0-2017-04-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.3-2017-06-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.7.4-2017-07-05.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.8.0-2017-07-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.8.0-2017-07-31.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.8.2-2017-10-14.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.8.4-2018-01-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.8.6-2018-02-14.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.8.8-2018-05-18.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.8.9-2018-06-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-05.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-24.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-05-27.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-12.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-13.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-14.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-06-17.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-07-09.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-07-10.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-07-11.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-08-12.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-08-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-08-29.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-09-04.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-10-15.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-10-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.0-2018-10-21.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.10-2019-07-09.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.16-2020-02-15.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.16-2020-03-04.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.19-2020-05-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.19-2020-06-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.21-2020-08-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.22-2020-09-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.26-2021-04-07.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.27-2021-04-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.3-2019-01-12.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.3-2019-02-07.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.7-2019-04-23.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.7-2019-04-26.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.7-2019-05-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.8-2019-06-11.sql', + '/administrator/components/com_admin/sql/updates/mysql/3.9.8-2019-06-15.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.0.0.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.0.1.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.0.2.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.0.3.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.1.0.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.1.1.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.1.2.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.1.3.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.1.4.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.1.5.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.10.0-2020-08-10.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.10.0-2021-05-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.10.7-2022-02-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.10.7-2022-02-20.sql.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.10.7-2022-03-18.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.0.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.1.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2013-12-22.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2013-12-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-08.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-15.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-18.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.2-2014-01-23.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.2.3-2014-02-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.3.0-2013-12-21.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.3.0-2014-02-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.3.0-2014-04-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.3.4-2014-08-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.3.6-2014-09-30.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-08-24.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-09-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-09-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-10-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2014-12-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2015-01-21.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.0-2015-02-26.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.4.4-2015-07-11.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-10-13.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-10-26.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-10-30.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-11-04.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2015-11-05.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.5.0-2016-03-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-04-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-04-08.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-04-09.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-05-06.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-06-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.0-2016-06-05.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.3-2016-08-15.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.3-2016-08-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.6.3-2016-10-04.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-08-06.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-08-22.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-08-29.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-09-29.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-10-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-10-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-04.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-19.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-21.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2016-11-24.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-08.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-09.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-15.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-17.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-01-31.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-02-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-02-15.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-02-17.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-03-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-03-09.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-04-10.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.0-2017-04-19.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.7.4-2017-07-05.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.8.0-2017-07-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.8.0-2017-07-31.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.8.2-2017-10-14.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.8.4-2018-01-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.8.6-2018-02-14.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.8.8-2018-05-18.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.8.9-2018-06-19.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-05.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-19.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-24.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-05-27.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-12.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-13.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-14.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-06-17.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-07-09.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-07-10.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-07-11.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-08-12.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-08-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-08-29.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-09-04.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-10-15.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-10-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.0-2018-10-21.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.10-2019-07-09.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.15-2020-01-08.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.16-2020-02-15.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.16-2020-03-04.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.19-2020-06-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.21-2020-08-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.22-2020-09-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.26-2021-04-07.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.27-2021-04-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.3-2019-01-12.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.3-2019-02-07.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.7-2019-04-23.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.7-2019-04-26.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.7-2019-05-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.8-2019-06-11.sql', + '/administrator/components/com_admin/sql/updates/postgresql/3.9.8-2019-06-15.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/2.5.2-2012-03-05.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/2.5.3-2012-03-13.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/2.5.4-2012-03-18.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/2.5.4-2012-03-19.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/2.5.5.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/2.5.6.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/2.5.7.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.0.0.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.0.1.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.0.2.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.0.3.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.1.0.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.1.1.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.1.2.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.1.3.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.1.4.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.1.5.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.10.0-2021-05-28.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.10.1-2021-08-17.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.10.7-2022-02-20.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.10.7-2022-02-20.sql.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.10.7-2022-03-18.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.0.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.1.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2013-12-22.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2013-12-28.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-08.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-15.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-18.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.2-2014-01-23.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.2.3-2014-02-20.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.3.0-2014-02-16.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.3.0-2014-04-02.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.3.4-2014-08-03.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.3.6-2014-09-30.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-08-24.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-09-01.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-09-16.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-10-20.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2014-12-03.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2015-01-21.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.0-2015-02-26.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.4.4-2015-07-11.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-10-13.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-10-26.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-10-30.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-11-04.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2015-11-05.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.5.0-2016-03-01.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-01.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-06.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-08.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-04-09.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-05-06.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-06-01.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.0-2016-06-05.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.3-2016-08-15.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.6.3-2016-08-16.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-08-06.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-08-22.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-08-29.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-09-29.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-10-01.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-10-02.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-11-04.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-11-19.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2016-11-24.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-08.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-09.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-15.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-17.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-01-31.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-02.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-15.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-16.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-02-17.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-03-03.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-03-09.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-04-10.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.0-2017-04-19.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.7.4-2017-07-05.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.8.0-2017-07-28.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.8.0-2017-07-31.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.8.2-2017-10-14.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.8.4-2018-01-16.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.8.6-2018-02-14.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.8.8-2018-05-18.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.8.9-2018-06-19.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-02.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-03.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-05.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-19.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-20.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-24.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-05-27.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-02.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-12.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-13.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-14.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-06-17.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-07-09.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-07-10.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-07-11.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-08-12.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-08-28.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-08-29.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-09-04.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-10-15.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-10-20.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.0-2018-10-21.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.10-2019-07-09.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.16-2020-03-04.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.19-2020-06-01.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.21-2020-08-02.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.22-2020-09-16.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.26-2021-04-07.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.27-2021-04-20.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.3-2019-01-12.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.3-2019-02-07.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.4-2019-03-06.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.7-2019-04-23.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.7-2019-04-26.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.7-2019-05-16.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.8-2019-06-11.sql', + '/administrator/components/com_admin/sql/updates/sqlazure/3.9.8-2019-06-15.sql', + '/administrator/components/com_admin/views/help/tmpl/default.php', + '/administrator/components/com_admin/views/help/tmpl/default.xml', + '/administrator/components/com_admin/views/help/tmpl/langforum.php', + '/administrator/components/com_admin/views/help/view.html.php', + '/administrator/components/com_admin/views/profile/tmpl/edit.php', + '/administrator/components/com_admin/views/profile/view.html.php', + '/administrator/components/com_admin/views/sysinfo/tmpl/default.php', + '/administrator/components/com_admin/views/sysinfo/tmpl/default.xml', + '/administrator/components/com_admin/views/sysinfo/tmpl/default_config.php', + '/administrator/components/com_admin/views/sysinfo/tmpl/default_directory.php', + '/administrator/components/com_admin/views/sysinfo/tmpl/default_phpinfo.php', + '/administrator/components/com_admin/views/sysinfo/tmpl/default_phpsettings.php', + '/administrator/components/com_admin/views/sysinfo/tmpl/default_system.php', + '/administrator/components/com_admin/views/sysinfo/view.html.php', + '/administrator/components/com_admin/views/sysinfo/view.json.php', + '/administrator/components/com_admin/views/sysinfo/view.text.php', + '/administrator/components/com_associations/associations.php', + '/administrator/components/com_associations/controller.php', + '/administrator/components/com_associations/controllers/association.php', + '/administrator/components/com_associations/controllers/associations.php', + '/administrator/components/com_associations/helpers/associations.php', + '/administrator/components/com_associations/layouts/joomla/searchtools/default/bar.php', + '/administrator/components/com_associations/models/association.php', + '/administrator/components/com_associations/models/associations.php', + '/administrator/components/com_associations/models/fields/itemlanguage.php', + '/administrator/components/com_associations/models/fields/itemtype.php', + '/administrator/components/com_associations/models/fields/modalassociation.php', + '/administrator/components/com_associations/models/forms/association.xml', + '/administrator/components/com_associations/models/forms/filter_associations.xml', + '/administrator/components/com_associations/views/association/tmpl/edit.php', + '/administrator/components/com_associations/views/association/view.html.php', + '/administrator/components/com_associations/views/associations/tmpl/default.php', + '/administrator/components/com_associations/views/associations/tmpl/default.xml', + '/administrator/components/com_associations/views/associations/tmpl/modal.php', + '/administrator/components/com_associations/views/associations/view.html.php', + '/administrator/components/com_banners/banners.php', + '/administrator/components/com_banners/controller.php', + '/administrator/components/com_banners/controllers/banner.php', + '/administrator/components/com_banners/controllers/banners.php', + '/administrator/components/com_banners/controllers/client.php', + '/administrator/components/com_banners/controllers/clients.php', + '/administrator/components/com_banners/controllers/tracks.php', + '/administrator/components/com_banners/controllers/tracks.raw.php', + '/administrator/components/com_banners/helpers/html/banner.php', + '/administrator/components/com_banners/models/banner.php', + '/administrator/components/com_banners/models/banners.php', + '/administrator/components/com_banners/models/client.php', + '/administrator/components/com_banners/models/clients.php', + '/administrator/components/com_banners/models/download.php', + '/administrator/components/com_banners/models/fields/bannerclient.php', + '/administrator/components/com_banners/models/fields/clicks.php', + '/administrator/components/com_banners/models/fields/impmade.php', + '/administrator/components/com_banners/models/fields/imptotal.php', + '/administrator/components/com_banners/models/forms/banner.xml', + '/administrator/components/com_banners/models/forms/client.xml', + '/administrator/components/com_banners/models/forms/download.xml', + '/administrator/components/com_banners/models/forms/filter_banners.xml', + '/administrator/components/com_banners/models/forms/filter_clients.xml', + '/administrator/components/com_banners/models/forms/filter_tracks.xml', + '/administrator/components/com_banners/models/tracks.php', + '/administrator/components/com_banners/tables/banner.php', + '/administrator/components/com_banners/tables/client.php', + '/administrator/components/com_banners/views/banner/tmpl/edit.php', + '/administrator/components/com_banners/views/banner/view.html.php', + '/administrator/components/com_banners/views/banners/tmpl/default.php', + '/administrator/components/com_banners/views/banners/tmpl/default_batch_body.php', + '/administrator/components/com_banners/views/banners/tmpl/default_batch_footer.php', + '/administrator/components/com_banners/views/banners/view.html.php', + '/administrator/components/com_banners/views/client/tmpl/edit.php', + '/administrator/components/com_banners/views/client/view.html.php', + '/administrator/components/com_banners/views/clients/tmpl/default.php', + '/administrator/components/com_banners/views/clients/view.html.php', + '/administrator/components/com_banners/views/download/tmpl/default.php', + '/administrator/components/com_banners/views/download/view.html.php', + '/administrator/components/com_banners/views/tracks/tmpl/default.php', + '/administrator/components/com_banners/views/tracks/view.html.php', + '/administrator/components/com_banners/views/tracks/view.raw.php', + '/administrator/components/com_cache/cache.php', + '/administrator/components/com_cache/controller.php', + '/administrator/components/com_cache/helpers/cache.php', + '/administrator/components/com_cache/models/cache.php', + '/administrator/components/com_cache/models/forms/filter_cache.xml', + '/administrator/components/com_cache/views/cache/tmpl/default.php', + '/administrator/components/com_cache/views/cache/tmpl/default.xml', + '/administrator/components/com_cache/views/cache/view.html.php', + '/administrator/components/com_cache/views/purge/tmpl/default.php', + '/administrator/components/com_cache/views/purge/tmpl/default.xml', + '/administrator/components/com_cache/views/purge/view.html.php', + '/administrator/components/com_categories/categories.php', + '/administrator/components/com_categories/controller.php', + '/administrator/components/com_categories/controllers/ajax.json.php', + '/administrator/components/com_categories/controllers/categories.php', + '/administrator/components/com_categories/controllers/category.php', + '/administrator/components/com_categories/helpers/association.php', + '/administrator/components/com_categories/helpers/html/categoriesadministrator.php', + '/administrator/components/com_categories/models/categories.php', + '/administrator/components/com_categories/models/category.php', + '/administrator/components/com_categories/models/fields/categoryedit.php', + '/administrator/components/com_categories/models/fields/categoryparent.php', + '/administrator/components/com_categories/models/fields/modal/category.php', + '/administrator/components/com_categories/models/forms/category.xml', + '/administrator/components/com_categories/models/forms/filter_categories.xml', + '/administrator/components/com_categories/tables/category.php', + '/administrator/components/com_categories/views/categories/tmpl/default.php', + '/administrator/components/com_categories/views/categories/tmpl/default.xml', + '/administrator/components/com_categories/views/categories/tmpl/default_batch_body.php', + '/administrator/components/com_categories/views/categories/tmpl/default_batch_footer.php', + '/administrator/components/com_categories/views/categories/tmpl/modal.php', + '/administrator/components/com_categories/views/categories/view.html.php', + '/administrator/components/com_categories/views/category/tmpl/edit.php', + '/administrator/components/com_categories/views/category/tmpl/edit.xml', + '/administrator/components/com_categories/views/category/tmpl/edit_associations.php', + '/administrator/components/com_categories/views/category/tmpl/edit_metadata.php', + '/administrator/components/com_categories/views/category/tmpl/modal.php', + '/administrator/components/com_categories/views/category/tmpl/modal_associations.php', + '/administrator/components/com_categories/views/category/tmpl/modal_extrafields.php', + '/administrator/components/com_categories/views/category/tmpl/modal_metadata.php', + '/administrator/components/com_categories/views/category/tmpl/modal_options.php', + '/administrator/components/com_categories/views/category/view.html.php', + '/administrator/components/com_checkin/checkin.php', + '/administrator/components/com_checkin/controller.php', + '/administrator/components/com_checkin/models/checkin.php', + '/administrator/components/com_checkin/models/forms/filter_checkin.xml', + '/administrator/components/com_checkin/views/checkin/tmpl/default.php', + '/administrator/components/com_checkin/views/checkin/tmpl/default.xml', + '/administrator/components/com_checkin/views/checkin/view.html.php', + '/administrator/components/com_config/config.php', + '/administrator/components/com_config/controller.php', + '/administrator/components/com_config/controller/application/cancel.php', + '/administrator/components/com_config/controller/application/display.php', + '/administrator/components/com_config/controller/application/removeroot.php', + '/administrator/components/com_config/controller/application/save.php', + '/administrator/components/com_config/controller/application/sendtestmail.php', + '/administrator/components/com_config/controller/application/store.php', + '/administrator/components/com_config/controller/component/cancel.php', + '/administrator/components/com_config/controller/component/display.php', + '/administrator/components/com_config/controller/component/save.php', + '/administrator/components/com_config/controllers/application.php', + '/administrator/components/com_config/controllers/component.php', + '/administrator/components/com_config/helper/config.php', + '/administrator/components/com_config/model/application.php', + '/administrator/components/com_config/model/component.php', + '/administrator/components/com_config/model/field/configcomponents.php', + '/administrator/components/com_config/model/field/filters.php', + '/administrator/components/com_config/model/form/application.xml', + '/administrator/components/com_config/models/application.php', + '/administrator/components/com_config/models/component.php', + '/administrator/components/com_config/view/application/html.php', + '/administrator/components/com_config/view/application/json.php', + '/administrator/components/com_config/view/application/tmpl/default.php', + '/administrator/components/com_config/view/application/tmpl/default.xml', + '/administrator/components/com_config/view/application/tmpl/default_cache.php', + '/administrator/components/com_config/view/application/tmpl/default_cookie.php', + '/administrator/components/com_config/view/application/tmpl/default_database.php', + '/administrator/components/com_config/view/application/tmpl/default_debug.php', + '/administrator/components/com_config/view/application/tmpl/default_filters.php', + '/administrator/components/com_config/view/application/tmpl/default_ftp.php', + '/administrator/components/com_config/view/application/tmpl/default_ftplogin.php', + '/administrator/components/com_config/view/application/tmpl/default_locale.php', + '/administrator/components/com_config/view/application/tmpl/default_mail.php', + '/administrator/components/com_config/view/application/tmpl/default_metadata.php', + '/administrator/components/com_config/view/application/tmpl/default_navigation.php', + '/administrator/components/com_config/view/application/tmpl/default_permissions.php', + '/administrator/components/com_config/view/application/tmpl/default_proxy.php', + '/administrator/components/com_config/view/application/tmpl/default_seo.php', + '/administrator/components/com_config/view/application/tmpl/default_server.php', + '/administrator/components/com_config/view/application/tmpl/default_session.php', + '/administrator/components/com_config/view/application/tmpl/default_site.php', + '/administrator/components/com_config/view/application/tmpl/default_system.php', + '/administrator/components/com_config/view/component/html.php', + '/administrator/components/com_config/view/component/tmpl/default.php', + '/administrator/components/com_config/view/component/tmpl/default.xml', + '/administrator/components/com_config/view/component/tmpl/default_navigation.php', + '/administrator/components/com_contact/contact.php', + '/administrator/components/com_contact/controller.php', + '/administrator/components/com_contact/controllers/ajax.json.php', + '/administrator/components/com_contact/controllers/contact.php', + '/administrator/components/com_contact/controllers/contacts.php', + '/administrator/components/com_contact/helpers/associations.php', + '/administrator/components/com_contact/helpers/html/contact.php', + '/administrator/components/com_contact/models/contact.php', + '/administrator/components/com_contact/models/contacts.php', + '/administrator/components/com_contact/models/fields/modal/contact.php', + '/administrator/components/com_contact/models/forms/contact.xml', + '/administrator/components/com_contact/models/forms/fields/mail.xml', + '/administrator/components/com_contact/models/forms/filter_contacts.xml', + '/administrator/components/com_contact/tables/contact.php', + '/administrator/components/com_contact/views/contact/tmpl/edit.php', + '/administrator/components/com_contact/views/contact/tmpl/edit_associations.php', + '/administrator/components/com_contact/views/contact/tmpl/edit_metadata.php', + '/administrator/components/com_contact/views/contact/tmpl/edit_params.php', + '/administrator/components/com_contact/views/contact/tmpl/modal.php', + '/administrator/components/com_contact/views/contact/tmpl/modal_associations.php', + '/administrator/components/com_contact/views/contact/tmpl/modal_metadata.php', + '/administrator/components/com_contact/views/contact/tmpl/modal_params.php', + '/administrator/components/com_contact/views/contact/view.html.php', + '/administrator/components/com_contact/views/contacts/tmpl/default.php', + '/administrator/components/com_contact/views/contacts/tmpl/default_batch.php', + '/administrator/components/com_contact/views/contacts/tmpl/default_batch_body.php', + '/administrator/components/com_contact/views/contacts/tmpl/default_batch_footer.php', + '/administrator/components/com_contact/views/contacts/tmpl/modal.php', + '/administrator/components/com_contact/views/contacts/view.html.php', + '/administrator/components/com_content/content.php', + '/administrator/components/com_content/controller.php', + '/administrator/components/com_content/controllers/ajax.json.php', + '/administrator/components/com_content/controllers/article.php', + '/administrator/components/com_content/controllers/articles.php', + '/administrator/components/com_content/controllers/featured.php', + '/administrator/components/com_content/helpers/associations.php', + '/administrator/components/com_content/helpers/html/contentadministrator.php', + '/administrator/components/com_content/models/article.php', + '/administrator/components/com_content/models/articles.php', + '/administrator/components/com_content/models/feature.php', + '/administrator/components/com_content/models/featured.php', + '/administrator/components/com_content/models/fields/modal/article.php', + '/administrator/components/com_content/models/fields/voteradio.php', + '/administrator/components/com_content/models/forms/article.xml', + '/administrator/components/com_content/models/forms/filter_articles.xml', + '/administrator/components/com_content/models/forms/filter_featured.xml', + '/administrator/components/com_content/tables/featured.php', + '/administrator/components/com_content/views/article/tmpl/edit.php', + '/administrator/components/com_content/views/article/tmpl/edit.xml', + '/administrator/components/com_content/views/article/tmpl/edit_associations.php', + '/administrator/components/com_content/views/article/tmpl/edit_metadata.php', + '/administrator/components/com_content/views/article/tmpl/modal.php', + '/administrator/components/com_content/views/article/tmpl/modal_associations.php', + '/administrator/components/com_content/views/article/tmpl/modal_metadata.php', + '/administrator/components/com_content/views/article/tmpl/pagebreak.php', + '/administrator/components/com_content/views/article/view.html.php', + '/administrator/components/com_content/views/articles/tmpl/default.php', + '/administrator/components/com_content/views/articles/tmpl/default.xml', + '/administrator/components/com_content/views/articles/tmpl/default_batch_body.php', + '/administrator/components/com_content/views/articles/tmpl/default_batch_footer.php', + '/administrator/components/com_content/views/articles/tmpl/modal.php', + '/administrator/components/com_content/views/articles/view.html.php', + '/administrator/components/com_content/views/featured/tmpl/default.php', + '/administrator/components/com_content/views/featured/tmpl/default.xml', + '/administrator/components/com_content/views/featured/view.html.php', + '/administrator/components/com_contenthistory/contenthistory.php', + '/administrator/components/com_contenthistory/controller.php', + '/administrator/components/com_contenthistory/controllers/history.php', + '/administrator/components/com_contenthistory/controllers/preview.php', + '/administrator/components/com_contenthistory/helpers/html/textdiff.php', + '/administrator/components/com_contenthistory/models/compare.php', + '/administrator/components/com_contenthistory/models/history.php', + '/administrator/components/com_contenthistory/models/preview.php', + '/administrator/components/com_contenthistory/views/compare/tmpl/compare.php', + '/administrator/components/com_contenthistory/views/compare/view.html.php', + '/administrator/components/com_contenthistory/views/history/tmpl/modal.php', + '/administrator/components/com_contenthistory/views/history/view.html.php', + '/administrator/components/com_contenthistory/views/preview/tmpl/preview.php', + '/administrator/components/com_contenthistory/views/preview/view.html.php', + '/administrator/components/com_cpanel/controller.php', + '/administrator/components/com_cpanel/cpanel.php', + '/administrator/components/com_cpanel/views/cpanel/tmpl/default.php', + '/administrator/components/com_cpanel/views/cpanel/tmpl/default.xml', + '/administrator/components/com_cpanel/views/cpanel/view.html.php', + '/administrator/components/com_fields/controller.php', + '/administrator/components/com_fields/controllers/field.php', + '/administrator/components/com_fields/controllers/fields.php', + '/administrator/components/com_fields/controllers/group.php', + '/administrator/components/com_fields/controllers/groups.php', + '/administrator/components/com_fields/fields.php', + '/administrator/components/com_fields/libraries/fieldslistplugin.php', + '/administrator/components/com_fields/libraries/fieldsplugin.php', + '/administrator/components/com_fields/models/field.php', + '/administrator/components/com_fields/models/fields.php', + '/administrator/components/com_fields/models/fields/fieldcontexts.php', + '/administrator/components/com_fields/models/fields/fieldgroups.php', + '/administrator/components/com_fields/models/fields/fieldlayout.php', + '/administrator/components/com_fields/models/fields/section.php', + '/administrator/components/com_fields/models/fields/type.php', + '/administrator/components/com_fields/models/forms/field.xml', + '/administrator/components/com_fields/models/forms/filter_fields.xml', + '/administrator/components/com_fields/models/forms/filter_groups.xml', + '/administrator/components/com_fields/models/forms/group.xml', + '/administrator/components/com_fields/models/group.php', + '/administrator/components/com_fields/models/groups.php', + '/administrator/components/com_fields/tables/field.php', + '/administrator/components/com_fields/tables/group.php', + '/administrator/components/com_fields/views/field/tmpl/edit.php', + '/administrator/components/com_fields/views/field/view.html.php', + '/administrator/components/com_fields/views/fields/tmpl/default.php', + '/administrator/components/com_fields/views/fields/tmpl/default_batch_body.php', + '/administrator/components/com_fields/views/fields/tmpl/default_batch_footer.php', + '/administrator/components/com_fields/views/fields/tmpl/modal.php', + '/administrator/components/com_fields/views/fields/view.html.php', + '/administrator/components/com_fields/views/group/tmpl/edit.php', + '/administrator/components/com_fields/views/group/view.html.php', + '/administrator/components/com_fields/views/groups/tmpl/default.php', + '/administrator/components/com_fields/views/groups/tmpl/default_batch_body.php', + '/administrator/components/com_fields/views/groups/tmpl/default_batch_footer.php', + '/administrator/components/com_fields/views/groups/view.html.php', + '/administrator/components/com_finder/controller.php', + '/administrator/components/com_finder/controllers/filter.php', + '/administrator/components/com_finder/controllers/filters.php', + '/administrator/components/com_finder/controllers/index.php', + '/administrator/components/com_finder/controllers/indexer.json.php', + '/administrator/components/com_finder/controllers/maps.php', + '/administrator/components/com_finder/finder.php', + '/administrator/components/com_finder/helpers/finder.php', + '/administrator/components/com_finder/helpers/html/finder.php', + '/administrator/components/com_finder/helpers/indexer/driver/mysql.php', + '/administrator/components/com_finder/helpers/indexer/driver/postgresql.php', + '/administrator/components/com_finder/helpers/indexer/driver/sqlsrv.php', + '/administrator/components/com_finder/helpers/indexer/indexer.php', + '/administrator/components/com_finder/helpers/indexer/parser/html.php', + '/administrator/components/com_finder/helpers/indexer/parser/rtf.php', + '/administrator/components/com_finder/helpers/indexer/parser/txt.php', + '/administrator/components/com_finder/helpers/indexer/stemmer.php', + '/administrator/components/com_finder/helpers/indexer/stemmer/fr.php', + '/administrator/components/com_finder/helpers/indexer/stemmer/porter_en.php', + '/administrator/components/com_finder/helpers/indexer/stemmer/snowball.php', + '/administrator/components/com_finder/models/fields/branches.php', + '/administrator/components/com_finder/models/fields/contentmap.php', + '/administrator/components/com_finder/models/fields/contenttypes.php', + '/administrator/components/com_finder/models/fields/directories.php', + '/administrator/components/com_finder/models/fields/searchfilter.php', + '/administrator/components/com_finder/models/filter.php', + '/administrator/components/com_finder/models/filters.php', + '/administrator/components/com_finder/models/forms/filter.xml', + '/administrator/components/com_finder/models/forms/filter_filters.xml', + '/administrator/components/com_finder/models/forms/filter_index.xml', + '/administrator/components/com_finder/models/forms/filter_maps.xml', + '/administrator/components/com_finder/models/index.php', + '/administrator/components/com_finder/models/indexer.php', + '/administrator/components/com_finder/models/maps.php', + '/administrator/components/com_finder/models/statistics.php', + '/administrator/components/com_finder/tables/filter.php', + '/administrator/components/com_finder/tables/link.php', + '/administrator/components/com_finder/tables/map.php', + '/administrator/components/com_finder/views/filter/tmpl/edit.php', + '/administrator/components/com_finder/views/filter/view.html.php', + '/administrator/components/com_finder/views/filters/tmpl/default.php', + '/administrator/components/com_finder/views/filters/view.html.php', + '/administrator/components/com_finder/views/index/tmpl/default.php', + '/administrator/components/com_finder/views/index/view.html.php', + '/administrator/components/com_finder/views/indexer/tmpl/default.php', + '/administrator/components/com_finder/views/indexer/view.html.php', + '/administrator/components/com_finder/views/maps/tmpl/default.php', + '/administrator/components/com_finder/views/maps/view.html.php', + '/administrator/components/com_finder/views/statistics/tmpl/default.php', + '/administrator/components/com_finder/views/statistics/view.html.php', + '/administrator/components/com_installer/controller.php', + '/administrator/components/com_installer/controllers/database.php', + '/administrator/components/com_installer/controllers/discover.php', + '/administrator/components/com_installer/controllers/install.php', + '/administrator/components/com_installer/controllers/manage.php', + '/administrator/components/com_installer/controllers/update.php', + '/administrator/components/com_installer/controllers/updatesites.php', + '/administrator/components/com_installer/helpers/html/manage.php', + '/administrator/components/com_installer/helpers/html/updatesites.php', + '/administrator/components/com_installer/installer.php', + '/administrator/components/com_installer/models/database.php', + '/administrator/components/com_installer/models/discover.php', + '/administrator/components/com_installer/models/extension.php', + '/administrator/components/com_installer/models/fields/extensionstatus.php', + '/administrator/components/com_installer/models/fields/folder.php', + '/administrator/components/com_installer/models/fields/location.php', + '/administrator/components/com_installer/models/fields/type.php', + '/administrator/components/com_installer/models/forms/filter_discover.xml', + '/administrator/components/com_installer/models/forms/filter_languages.xml', + '/administrator/components/com_installer/models/forms/filter_manage.xml', + '/administrator/components/com_installer/models/forms/filter_update.xml', + '/administrator/components/com_installer/models/forms/filter_updatesites.xml', + '/administrator/components/com_installer/models/install.php', + '/administrator/components/com_installer/models/languages.php', + '/administrator/components/com_installer/models/manage.php', + '/administrator/components/com_installer/models/update.php', + '/administrator/components/com_installer/models/updatesites.php', + '/administrator/components/com_installer/models/warnings.php', + '/administrator/components/com_installer/views/database/tmpl/default.php', + '/administrator/components/com_installer/views/database/tmpl/default.xml', + '/administrator/components/com_installer/views/database/view.html.php', + '/administrator/components/com_installer/views/default/tmpl/default_ftp.php', + '/administrator/components/com_installer/views/default/tmpl/default_message.php', + '/administrator/components/com_installer/views/default/view.php', + '/administrator/components/com_installer/views/discover/tmpl/default.php', + '/administrator/components/com_installer/views/discover/tmpl/default.xml', + '/administrator/components/com_installer/views/discover/tmpl/default_item.php', + '/administrator/components/com_installer/views/discover/view.html.php', + '/administrator/components/com_installer/views/install/tmpl/default.php', + '/administrator/components/com_installer/views/install/tmpl/default.xml', + '/administrator/components/com_installer/views/install/view.html.php', + '/administrator/components/com_installer/views/languages/tmpl/default.php', + '/administrator/components/com_installer/views/languages/tmpl/default.xml', + '/administrator/components/com_installer/views/languages/view.html.php', + '/administrator/components/com_installer/views/manage/tmpl/default.php', + '/administrator/components/com_installer/views/manage/tmpl/default.xml', + '/administrator/components/com_installer/views/manage/view.html.php', + '/administrator/components/com_installer/views/update/tmpl/default.php', + '/administrator/components/com_installer/views/update/tmpl/default.xml', + '/administrator/components/com_installer/views/update/view.html.php', + '/administrator/components/com_installer/views/updatesites/tmpl/default.php', + '/administrator/components/com_installer/views/updatesites/tmpl/default.xml', + '/administrator/components/com_installer/views/updatesites/view.html.php', + '/administrator/components/com_installer/views/warnings/tmpl/default.php', + '/administrator/components/com_installer/views/warnings/tmpl/default.xml', + '/administrator/components/com_installer/views/warnings/view.html.php', + '/administrator/components/com_joomlaupdate/controller.php', + '/administrator/components/com_joomlaupdate/controllers/update.php', + '/administrator/components/com_joomlaupdate/helpers/joomlaupdate.php', + '/administrator/components/com_joomlaupdate/helpers/select.php', + '/administrator/components/com_joomlaupdate/joomlaupdate.php', + '/administrator/components/com_joomlaupdate/models/default.php', + '/administrator/components/com_joomlaupdate/restore.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/complete.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default.xml', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default_nodownload.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default_noupdate.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default_preupdatecheck.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default_reinstall.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default_update.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default_updatemefirst.php', + '/administrator/components/com_joomlaupdate/views/default/tmpl/default_upload.php', + '/administrator/components/com_joomlaupdate/views/default/view.html.php', + '/administrator/components/com_joomlaupdate/views/update/tmpl/default.php', + '/administrator/components/com_joomlaupdate/views/update/tmpl/finaliseconfirm.php', + '/administrator/components/com_joomlaupdate/views/update/view.html.php', + '/administrator/components/com_joomlaupdate/views/upload/tmpl/captive.php', + '/administrator/components/com_joomlaupdate/views/upload/view.html.php', + '/administrator/components/com_languages/controller.php', + '/administrator/components/com_languages/controllers/installed.php', + '/administrator/components/com_languages/controllers/language.php', + '/administrator/components/com_languages/controllers/languages.php', + '/administrator/components/com_languages/controllers/override.php', + '/administrator/components/com_languages/controllers/overrides.php', + '/administrator/components/com_languages/controllers/strings.json.php', + '/administrator/components/com_languages/helpers/html/languages.php', + '/administrator/components/com_languages/helpers/jsonresponse.php', + '/administrator/components/com_languages/helpers/languages.php', + '/administrator/components/com_languages/helpers/multilangstatus.php', + '/administrator/components/com_languages/languages.php', + '/administrator/components/com_languages/layouts/joomla/searchtools/default/bar.php', + '/administrator/components/com_languages/models/fields/languageclient.php', + '/administrator/components/com_languages/models/forms/filter_installed.xml', + '/administrator/components/com_languages/models/forms/filter_languages.xml', + '/administrator/components/com_languages/models/forms/filter_overrides.xml', + '/administrator/components/com_languages/models/forms/language.xml', + '/administrator/components/com_languages/models/forms/override.xml', + '/administrator/components/com_languages/models/installed.php', + '/administrator/components/com_languages/models/language.php', + '/administrator/components/com_languages/models/languages.php', + '/administrator/components/com_languages/models/override.php', + '/administrator/components/com_languages/models/overrides.php', + '/administrator/components/com_languages/models/strings.php', + '/administrator/components/com_languages/views/installed/tmpl/default.php', + '/administrator/components/com_languages/views/installed/tmpl/default.xml', + '/administrator/components/com_languages/views/installed/view.html.php', + '/administrator/components/com_languages/views/language/tmpl/edit.php', + '/administrator/components/com_languages/views/language/view.html.php', + '/administrator/components/com_languages/views/languages/tmpl/default.php', + '/administrator/components/com_languages/views/languages/tmpl/default.xml', + '/administrator/components/com_languages/views/languages/view.html.php', + '/administrator/components/com_languages/views/multilangstatus/tmpl/default.php', + '/administrator/components/com_languages/views/multilangstatus/view.html.php', + '/administrator/components/com_languages/views/override/tmpl/edit.php', + '/administrator/components/com_languages/views/override/view.html.php', + '/administrator/components/com_languages/views/overrides/tmpl/default.php', + '/administrator/components/com_languages/views/overrides/tmpl/default.xml', + '/administrator/components/com_languages/views/overrides/view.html.php', + '/administrator/components/com_login/controller.php', + '/administrator/components/com_login/login.php', + '/administrator/components/com_login/models/login.php', + '/administrator/components/com_login/views/login/tmpl/default.php', + '/administrator/components/com_login/views/login/view.html.php', + '/administrator/components/com_media/controller.php', + '/administrator/components/com_media/controllers/file.json.php', + '/administrator/components/com_media/controllers/file.php', + '/administrator/components/com_media/controllers/folder.php', + '/administrator/components/com_media/layouts/toolbar/deletemedia.php', + '/administrator/components/com_media/layouts/toolbar/newfolder.php', + '/administrator/components/com_media/layouts/toolbar/uploadmedia.php', + '/administrator/components/com_media/media.php', + '/administrator/components/com_media/models/list.php', + '/administrator/components/com_media/models/manager.php', + '/administrator/components/com_media/views/images/tmpl/default.php', + '/administrator/components/com_media/views/images/view.html.php', + '/administrator/components/com_media/views/imageslist/tmpl/default.php', + '/administrator/components/com_media/views/imageslist/tmpl/default_folder.php', + '/administrator/components/com_media/views/imageslist/tmpl/default_image.php', + '/administrator/components/com_media/views/imageslist/view.html.php', + '/administrator/components/com_media/views/media/tmpl/default.php', + '/administrator/components/com_media/views/media/tmpl/default.xml', + '/administrator/components/com_media/views/media/tmpl/default_folders.php', + '/administrator/components/com_media/views/media/tmpl/default_navigation.php', + '/administrator/components/com_media/views/media/view.html.php', + '/administrator/components/com_media/views/medialist/tmpl/default.php', + '/administrator/components/com_media/views/medialist/tmpl/details.php', + '/administrator/components/com_media/views/medialist/tmpl/details_doc.php', + '/administrator/components/com_media/views/medialist/tmpl/details_docs.php', + '/administrator/components/com_media/views/medialist/tmpl/details_folder.php', + '/administrator/components/com_media/views/medialist/tmpl/details_folders.php', + '/administrator/components/com_media/views/medialist/tmpl/details_img.php', + '/administrator/components/com_media/views/medialist/tmpl/details_imgs.php', + '/administrator/components/com_media/views/medialist/tmpl/details_up.php', + '/administrator/components/com_media/views/medialist/tmpl/details_video.php', + '/administrator/components/com_media/views/medialist/tmpl/details_videos.php', + '/administrator/components/com_media/views/medialist/tmpl/thumbs.php', + '/administrator/components/com_media/views/medialist/tmpl/thumbs_docs.php', + '/administrator/components/com_media/views/medialist/tmpl/thumbs_folders.php', + '/administrator/components/com_media/views/medialist/tmpl/thumbs_imgs.php', + '/administrator/components/com_media/views/medialist/tmpl/thumbs_up.php', + '/administrator/components/com_media/views/medialist/tmpl/thumbs_videos.php', + '/administrator/components/com_media/views/medialist/view.html.php', + '/administrator/components/com_menus/controller.php', + '/administrator/components/com_menus/controllers/ajax.json.php', + '/administrator/components/com_menus/controllers/item.php', + '/administrator/components/com_menus/controllers/items.php', + '/administrator/components/com_menus/controllers/menu.php', + '/administrator/components/com_menus/controllers/menus.php', + '/administrator/components/com_menus/helpers/associations.php', + '/administrator/components/com_menus/helpers/html/menus.php', + '/administrator/components/com_menus/layouts/joomla/searchtools/default/bar.php', + '/administrator/components/com_menus/menus.php', + '/administrator/components/com_menus/models/fields/componentscategory.php', + '/administrator/components/com_menus/models/fields/menuitembytype.php', + '/administrator/components/com_menus/models/fields/menuordering.php', + '/administrator/components/com_menus/models/fields/menuparent.php', + '/administrator/components/com_menus/models/fields/menupreset.php', + '/administrator/components/com_menus/models/fields/menutype.php', + '/administrator/components/com_menus/models/fields/modal/menu.php', + '/administrator/components/com_menus/models/forms/filter_items.xml', + '/administrator/components/com_menus/models/forms/filter_itemsadmin.xml', + '/administrator/components/com_menus/models/forms/filter_menus.xml', + '/administrator/components/com_menus/models/forms/item.xml', + '/administrator/components/com_menus/models/forms/item_alias.xml', + '/administrator/components/com_menus/models/forms/item_component.xml', + '/administrator/components/com_menus/models/forms/item_heading.xml', + '/administrator/components/com_menus/models/forms/item_separator.xml', + '/administrator/components/com_menus/models/forms/item_url.xml', + '/administrator/components/com_menus/models/forms/itemadmin.xml', + '/administrator/components/com_menus/models/forms/itemadmin_alias.xml', + '/administrator/components/com_menus/models/forms/itemadmin_component.xml', + '/administrator/components/com_menus/models/forms/itemadmin_container.xml', + '/administrator/components/com_menus/models/forms/itemadmin_heading.xml', + '/administrator/components/com_menus/models/forms/itemadmin_separator.xml', + '/administrator/components/com_menus/models/forms/itemadmin_url.xml', + '/administrator/components/com_menus/models/forms/menu.xml', + '/administrator/components/com_menus/models/item.php', + '/administrator/components/com_menus/models/items.php', + '/administrator/components/com_menus/models/menu.php', + '/administrator/components/com_menus/models/menus.php', + '/administrator/components/com_menus/models/menutypes.php', + '/administrator/components/com_menus/presets/joomla.xml', + '/administrator/components/com_menus/presets/modern.xml', + '/administrator/components/com_menus/tables/menu.php', + '/administrator/components/com_menus/views/item/tmpl/edit.php', + '/administrator/components/com_menus/views/item/tmpl/edit.xml', + '/administrator/components/com_menus/views/item/tmpl/edit_associations.php', + '/administrator/components/com_menus/views/item/tmpl/edit_container.php', + '/administrator/components/com_menus/views/item/tmpl/edit_modules.php', + '/administrator/components/com_menus/views/item/tmpl/edit_options.php', + '/administrator/components/com_menus/views/item/tmpl/modal.php', + '/administrator/components/com_menus/views/item/tmpl/modal_associations.php', + '/administrator/components/com_menus/views/item/tmpl/modal_options.php', + '/administrator/components/com_menus/views/item/view.html.php', + '/administrator/components/com_menus/views/items/tmpl/default.php', + '/administrator/components/com_menus/views/items/tmpl/default.xml', + '/administrator/components/com_menus/views/items/tmpl/default_batch_body.php', + '/administrator/components/com_menus/views/items/tmpl/default_batch_footer.php', + '/administrator/components/com_menus/views/items/tmpl/modal.php', + '/administrator/components/com_menus/views/items/view.html.php', + '/administrator/components/com_menus/views/menu/tmpl/edit.php', + '/administrator/components/com_menus/views/menu/tmpl/edit.xml', + '/administrator/components/com_menus/views/menu/view.html.php', + '/administrator/components/com_menus/views/menu/view.xml.php', + '/administrator/components/com_menus/views/menus/tmpl/default.php', + '/administrator/components/com_menus/views/menus/tmpl/default.xml', + '/administrator/components/com_menus/views/menus/view.html.php', + '/administrator/components/com_menus/views/menutypes/tmpl/default.php', + '/administrator/components/com_menus/views/menutypes/view.html.php', + '/administrator/components/com_messages/controller.php', + '/administrator/components/com_messages/controllers/config.php', + '/administrator/components/com_messages/controllers/message.php', + '/administrator/components/com_messages/controllers/messages.php', + '/administrator/components/com_messages/helpers/html/messages.php', + '/administrator/components/com_messages/helpers/messages.php', + '/administrator/components/com_messages/messages.php', + '/administrator/components/com_messages/models/config.php', + '/administrator/components/com_messages/models/fields/messagestates.php', + '/administrator/components/com_messages/models/fields/usermessages.php', + '/administrator/components/com_messages/models/forms/config.xml', + '/administrator/components/com_messages/models/forms/filter_messages.xml', + '/administrator/components/com_messages/models/forms/message.xml', + '/administrator/components/com_messages/models/message.php', + '/administrator/components/com_messages/models/messages.php', + '/administrator/components/com_messages/tables/message.php', + '/administrator/components/com_messages/views/config/tmpl/default.php', + '/administrator/components/com_messages/views/config/view.html.php', + '/administrator/components/com_messages/views/message/tmpl/default.php', + '/administrator/components/com_messages/views/message/tmpl/edit.php', + '/administrator/components/com_messages/views/message/view.html.php', + '/administrator/components/com_messages/views/messages/tmpl/default.php', + '/administrator/components/com_messages/views/messages/view.html.php', + '/administrator/components/com_modules/controller.php', + '/administrator/components/com_modules/controllers/module.php', + '/administrator/components/com_modules/controllers/modules.php', + '/administrator/components/com_modules/helpers/html/modules.php', + '/administrator/components/com_modules/helpers/xml.php', + '/administrator/components/com_modules/layouts/toolbar/newmodule.php', + '/administrator/components/com_modules/models/fields/modulesmodule.php', + '/administrator/components/com_modules/models/fields/modulesposition.php', + '/administrator/components/com_modules/models/forms/advanced.xml', + '/administrator/components/com_modules/models/forms/filter_modules.xml', + '/administrator/components/com_modules/models/forms/filter_modulesadmin.xml', + '/administrator/components/com_modules/models/forms/module.xml', + '/administrator/components/com_modules/models/forms/moduleadmin.xml', + '/administrator/components/com_modules/models/module.php', + '/administrator/components/com_modules/models/modules.php', + '/administrator/components/com_modules/models/positions.php', + '/administrator/components/com_modules/models/select.php', + '/administrator/components/com_modules/modules.php', + '/administrator/components/com_modules/views/module/tmpl/edit.php', + '/administrator/components/com_modules/views/module/tmpl/edit_assignment.php', + '/administrator/components/com_modules/views/module/tmpl/edit_options.php', + '/administrator/components/com_modules/views/module/tmpl/edit_positions.php', + '/administrator/components/com_modules/views/module/tmpl/modal.php', + '/administrator/components/com_modules/views/module/view.html.php', + '/administrator/components/com_modules/views/module/view.json.php', + '/administrator/components/com_modules/views/modules/tmpl/default.php', + '/administrator/components/com_modules/views/modules/tmpl/default.xml', + '/administrator/components/com_modules/views/modules/tmpl/default_batch_body.php', + '/administrator/components/com_modules/views/modules/tmpl/default_batch_footer.php', + '/administrator/components/com_modules/views/modules/tmpl/modal.php', + '/administrator/components/com_modules/views/modules/view.html.php', + '/administrator/components/com_modules/views/positions/tmpl/modal.php', + '/administrator/components/com_modules/views/positions/view.html.php', + '/administrator/components/com_modules/views/preview/tmpl/default.php', + '/administrator/components/com_modules/views/preview/view.html.php', + '/administrator/components/com_modules/views/select/tmpl/default.php', + '/administrator/components/com_modules/views/select/view.html.php', + '/administrator/components/com_newsfeeds/controller.php', + '/administrator/components/com_newsfeeds/controllers/ajax.json.php', + '/administrator/components/com_newsfeeds/controllers/newsfeed.php', + '/administrator/components/com_newsfeeds/controllers/newsfeeds.php', + '/administrator/components/com_newsfeeds/helpers/associations.php', + '/administrator/components/com_newsfeeds/helpers/html/newsfeed.php', + '/administrator/components/com_newsfeeds/models/fields/modal/newsfeed.php', + '/administrator/components/com_newsfeeds/models/fields/newsfeeds.php', + '/administrator/components/com_newsfeeds/models/forms/filter_newsfeeds.xml', + '/administrator/components/com_newsfeeds/models/forms/newsfeed.xml', + '/administrator/components/com_newsfeeds/models/newsfeed.php', + '/administrator/components/com_newsfeeds/models/newsfeeds.php', + '/administrator/components/com_newsfeeds/newsfeeds.php', + '/administrator/components/com_newsfeeds/tables/newsfeed.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_associations.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_display.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_metadata.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/edit_params.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_associations.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_display.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_metadata.php', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl/modal_params.php', + '/administrator/components/com_newsfeeds/views/newsfeed/view.html.php', + '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/default.php', + '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/default_batch_body.php', + '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/default_batch_footer.php', + '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl/modal.php', + '/administrator/components/com_newsfeeds/views/newsfeeds/view.html.php', + '/administrator/components/com_plugins/controller.php', + '/administrator/components/com_plugins/controllers/plugin.php', + '/administrator/components/com_plugins/controllers/plugins.php', + '/administrator/components/com_plugins/models/fields/pluginelement.php', + '/administrator/components/com_plugins/models/fields/pluginordering.php', + '/administrator/components/com_plugins/models/fields/plugintype.php', + '/administrator/components/com_plugins/models/forms/filter_plugins.xml', + '/administrator/components/com_plugins/models/forms/plugin.xml', + '/administrator/components/com_plugins/models/plugin.php', + '/administrator/components/com_plugins/models/plugins.php', + '/administrator/components/com_plugins/plugins.php', + '/administrator/components/com_plugins/views/plugin/tmpl/edit.php', + '/administrator/components/com_plugins/views/plugin/tmpl/edit_options.php', + '/administrator/components/com_plugins/views/plugin/tmpl/modal.php', + '/administrator/components/com_plugins/views/plugin/view.html.php', + '/administrator/components/com_plugins/views/plugins/tmpl/default.php', + '/administrator/components/com_plugins/views/plugins/tmpl/default.xml', + '/administrator/components/com_plugins/views/plugins/view.html.php', + '/administrator/components/com_postinstall/controllers/message.php', + '/administrator/components/com_postinstall/fof.xml', + '/administrator/components/com_postinstall/models/messages.php', + '/administrator/components/com_postinstall/postinstall.php', + '/administrator/components/com_postinstall/toolbar.php', + '/administrator/components/com_postinstall/views/messages/tmpl/default.php', + '/administrator/components/com_postinstall/views/messages/tmpl/default.xml', + '/administrator/components/com_postinstall/views/messages/view.html.php', + '/administrator/components/com_privacy/controller.php', + '/administrator/components/com_privacy/controllers/consents.php', + '/administrator/components/com_privacy/controllers/request.php', + '/administrator/components/com_privacy/controllers/request.xml.php', + '/administrator/components/com_privacy/controllers/requests.php', + '/administrator/components/com_privacy/helpers/export/domain.php', + '/administrator/components/com_privacy/helpers/export/field.php', + '/administrator/components/com_privacy/helpers/export/item.php', + '/administrator/components/com_privacy/helpers/html/helper.php', + '/administrator/components/com_privacy/helpers/plugin.php', + '/administrator/components/com_privacy/helpers/privacy.php', + '/administrator/components/com_privacy/helpers/removal/status.php', + '/administrator/components/com_privacy/models/capabilities.php', + '/administrator/components/com_privacy/models/consents.php', + '/administrator/components/com_privacy/models/dashboard.php', + '/administrator/components/com_privacy/models/export.php', + '/administrator/components/com_privacy/models/fields/requeststatus.php', + '/administrator/components/com_privacy/models/fields/requesttype.php', + '/administrator/components/com_privacy/models/forms/filter_consents.xml', + '/administrator/components/com_privacy/models/forms/filter_requests.xml', + '/administrator/components/com_privacy/models/forms/request.xml', + '/administrator/components/com_privacy/models/remove.php', + '/administrator/components/com_privacy/models/request.php', + '/administrator/components/com_privacy/models/requests.php', + '/administrator/components/com_privacy/privacy.php', + '/administrator/components/com_privacy/tables/consent.php', + '/administrator/components/com_privacy/tables/request.php', + '/administrator/components/com_privacy/views/capabilities/tmpl/default.php', + '/administrator/components/com_privacy/views/capabilities/view.html.php', + '/administrator/components/com_privacy/views/consents/tmpl/default.php', + '/administrator/components/com_privacy/views/consents/tmpl/default.xml', + '/administrator/components/com_privacy/views/consents/view.html.php', + '/administrator/components/com_privacy/views/dashboard/tmpl/default.php', + '/administrator/components/com_privacy/views/dashboard/tmpl/default.xml', + '/administrator/components/com_privacy/views/dashboard/view.html.php', + '/administrator/components/com_privacy/views/export/view.xml.php', + '/administrator/components/com_privacy/views/request/tmpl/default.php', + '/administrator/components/com_privacy/views/request/tmpl/edit.php', + '/administrator/components/com_privacy/views/request/view.html.php', + '/administrator/components/com_privacy/views/requests/tmpl/default.php', + '/administrator/components/com_privacy/views/requests/tmpl/default.xml', + '/administrator/components/com_privacy/views/requests/view.html.php', + '/administrator/components/com_redirect/controller.php', + '/administrator/components/com_redirect/controllers/link.php', + '/administrator/components/com_redirect/controllers/links.php', + '/administrator/components/com_redirect/helpers/html/redirect.php', + '/administrator/components/com_redirect/models/fields/redirect.php', + '/administrator/components/com_redirect/models/forms/filter_links.xml', + '/administrator/components/com_redirect/models/forms/link.xml', + '/administrator/components/com_redirect/models/link.php', + '/administrator/components/com_redirect/models/links.php', + '/administrator/components/com_redirect/redirect.php', + '/administrator/components/com_redirect/tables/link.php', + '/administrator/components/com_redirect/views/link/tmpl/edit.php', + '/administrator/components/com_redirect/views/link/view.html.php', + '/administrator/components/com_redirect/views/links/tmpl/default.php', + '/administrator/components/com_redirect/views/links/tmpl/default.xml', + '/administrator/components/com_redirect/views/links/tmpl/default_addform.php', + '/administrator/components/com_redirect/views/links/tmpl/default_batch_body.php', + '/administrator/components/com_redirect/views/links/tmpl/default_batch_footer.php', + '/administrator/components/com_redirect/views/links/view.html.php', + '/administrator/components/com_tags/controller.php', + '/administrator/components/com_tags/controllers/tag.php', + '/administrator/components/com_tags/controllers/tags.php', + '/administrator/components/com_tags/helpers/tags.php', + '/administrator/components/com_tags/models/forms/filter_tags.xml', + '/administrator/components/com_tags/models/forms/tag.xml', + '/administrator/components/com_tags/models/tag.php', + '/administrator/components/com_tags/models/tags.php', + '/administrator/components/com_tags/tables/tag.php', + '/administrator/components/com_tags/tags.php', + '/administrator/components/com_tags/views/tag/tmpl/edit.php', + '/administrator/components/com_tags/views/tag/tmpl/edit_metadata.php', + '/administrator/components/com_tags/views/tag/tmpl/edit_options.php', + '/administrator/components/com_tags/views/tag/view.html.php', + '/administrator/components/com_tags/views/tags/tmpl/default.php', + '/administrator/components/com_tags/views/tags/tmpl/default.xml', + '/administrator/components/com_tags/views/tags/tmpl/default_batch_body.php', + '/administrator/components/com_tags/views/tags/tmpl/default_batch_footer.php', + '/administrator/components/com_tags/views/tags/view.html.php', + '/administrator/components/com_templates/controller.php', + '/administrator/components/com_templates/controllers/style.php', + '/administrator/components/com_templates/controllers/styles.php', + '/administrator/components/com_templates/controllers/template.php', + '/administrator/components/com_templates/helpers/html/templates.php', + '/administrator/components/com_templates/models/fields/templatelocation.php', + '/administrator/components/com_templates/models/fields/templatename.php', + '/administrator/components/com_templates/models/forms/filter_styles.xml', + '/administrator/components/com_templates/models/forms/filter_templates.xml', + '/administrator/components/com_templates/models/forms/source.xml', + '/administrator/components/com_templates/models/forms/style.xml', + '/administrator/components/com_templates/models/forms/style_administrator.xml', + '/administrator/components/com_templates/models/forms/style_site.xml', + '/administrator/components/com_templates/models/style.php', + '/administrator/components/com_templates/models/styles.php', + '/administrator/components/com_templates/models/template.php', + '/administrator/components/com_templates/models/templates.php', + '/administrator/components/com_templates/tables/style.php', + '/administrator/components/com_templates/templates.php', + '/administrator/components/com_templates/views/style/tmpl/edit.php', + '/administrator/components/com_templates/views/style/tmpl/edit_assignment.php', + '/administrator/components/com_templates/views/style/tmpl/edit_options.php', + '/administrator/components/com_templates/views/style/view.html.php', + '/administrator/components/com_templates/views/style/view.json.php', + '/administrator/components/com_templates/views/styles/tmpl/default.php', + '/administrator/components/com_templates/views/styles/tmpl/default.xml', + '/administrator/components/com_templates/views/styles/view.html.php', + '/administrator/components/com_templates/views/template/tmpl/default.php', + '/administrator/components/com_templates/views/template/tmpl/default_description.php', + '/administrator/components/com_templates/views/template/tmpl/default_folders.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_copy_body.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_copy_footer.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_delete_body.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_delete_footer.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_file_body.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_file_footer.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_folder_body.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_folder_footer.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_rename_body.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_rename_footer.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_resize_body.php', + '/administrator/components/com_templates/views/template/tmpl/default_modal_resize_footer.php', + '/administrator/components/com_templates/views/template/tmpl/default_tree.php', + '/administrator/components/com_templates/views/template/tmpl/readonly.php', + '/administrator/components/com_templates/views/template/view.html.php', + '/administrator/components/com_templates/views/templates/tmpl/default.php', + '/administrator/components/com_templates/views/templates/tmpl/default.xml', + '/administrator/components/com_templates/views/templates/view.html.php', + '/administrator/components/com_users/controller.php', + '/administrator/components/com_users/controllers/group.php', + '/administrator/components/com_users/controllers/groups.php', + '/administrator/components/com_users/controllers/level.php', + '/administrator/components/com_users/controllers/levels.php', + '/administrator/components/com_users/controllers/mail.php', + '/administrator/components/com_users/controllers/note.php', + '/administrator/components/com_users/controllers/notes.php', + '/administrator/components/com_users/controllers/user.php', + '/administrator/components/com_users/controllers/users.php', + '/administrator/components/com_users/helpers/html/users.php', + '/administrator/components/com_users/models/debuggroup.php', + '/administrator/components/com_users/models/debuguser.php', + '/administrator/components/com_users/models/fields/groupparent.php', + '/administrator/components/com_users/models/fields/levels.php', + '/administrator/components/com_users/models/forms/config_domain.xml', + '/administrator/components/com_users/models/forms/fields/user.xml', + '/administrator/components/com_users/models/forms/filter_debuggroup.xml', + '/administrator/components/com_users/models/forms/filter_debuguser.xml', + '/administrator/components/com_users/models/forms/filter_groups.xml', + '/administrator/components/com_users/models/forms/filter_levels.xml', + '/administrator/components/com_users/models/forms/filter_notes.xml', + '/administrator/components/com_users/models/forms/filter_users.xml', + '/administrator/components/com_users/models/forms/group.xml', + '/administrator/components/com_users/models/forms/level.xml', + '/administrator/components/com_users/models/forms/mail.xml', + '/administrator/components/com_users/models/forms/note.xml', + '/administrator/components/com_users/models/forms/user.xml', + '/administrator/components/com_users/models/group.php', + '/administrator/components/com_users/models/groups.php', + '/administrator/components/com_users/models/level.php', + '/administrator/components/com_users/models/levels.php', + '/administrator/components/com_users/models/mail.php', + '/administrator/components/com_users/models/note.php', + '/administrator/components/com_users/models/notes.php', + '/administrator/components/com_users/models/user.php', + '/administrator/components/com_users/models/users.php', + '/administrator/components/com_users/tables/note.php', + '/administrator/components/com_users/users.php', + '/administrator/components/com_users/views/debuggroup/tmpl/default.php', + '/administrator/components/com_users/views/debuggroup/view.html.php', + '/administrator/components/com_users/views/debuguser/tmpl/default.php', + '/administrator/components/com_users/views/debuguser/view.html.php', + '/administrator/components/com_users/views/group/tmpl/edit.php', + '/administrator/components/com_users/views/group/tmpl/edit.xml', + '/administrator/components/com_users/views/group/view.html.php', + '/administrator/components/com_users/views/groups/tmpl/default.php', + '/administrator/components/com_users/views/groups/tmpl/default.xml', + '/administrator/components/com_users/views/groups/view.html.php', + '/administrator/components/com_users/views/level/tmpl/edit.php', + '/administrator/components/com_users/views/level/tmpl/edit.xml', + '/administrator/components/com_users/views/level/view.html.php', + '/administrator/components/com_users/views/levels/tmpl/default.php', + '/administrator/components/com_users/views/levels/tmpl/default.xml', + '/administrator/components/com_users/views/levels/view.html.php', + '/administrator/components/com_users/views/mail/tmpl/default.php', + '/administrator/components/com_users/views/mail/tmpl/default.xml', + '/administrator/components/com_users/views/mail/view.html.php', + '/administrator/components/com_users/views/note/tmpl/edit.php', + '/administrator/components/com_users/views/note/tmpl/edit.xml', + '/administrator/components/com_users/views/note/view.html.php', + '/administrator/components/com_users/views/notes/tmpl/default.php', + '/administrator/components/com_users/views/notes/tmpl/default.xml', + '/administrator/components/com_users/views/notes/tmpl/modal.php', + '/administrator/components/com_users/views/notes/view.html.php', + '/administrator/components/com_users/views/user/tmpl/edit.php', + '/administrator/components/com_users/views/user/tmpl/edit.xml', + '/administrator/components/com_users/views/user/tmpl/edit_groups.php', + '/administrator/components/com_users/views/user/view.html.php', + '/administrator/components/com_users/views/users/tmpl/default.php', + '/administrator/components/com_users/views/users/tmpl/default.xml', + '/administrator/components/com_users/views/users/tmpl/default_batch_body.php', + '/administrator/components/com_users/views/users/tmpl/default_batch_footer.php', + '/administrator/components/com_users/views/users/tmpl/modal.php', + '/administrator/components/com_users/views/users/view.html.php', + '/administrator/help/helpsites.xml', + '/administrator/includes/helper.php', + '/administrator/includes/subtoolbar.php', + '/administrator/language/en-GB/en-GB.com_actionlogs.ini', + '/administrator/language/en-GB/en-GB.com_actionlogs.sys.ini', + '/administrator/language/en-GB/en-GB.com_admin.ini', + '/administrator/language/en-GB/en-GB.com_admin.sys.ini', + '/administrator/language/en-GB/en-GB.com_ajax.ini', + '/administrator/language/en-GB/en-GB.com_ajax.sys.ini', + '/administrator/language/en-GB/en-GB.com_associations.ini', + '/administrator/language/en-GB/en-GB.com_associations.sys.ini', + '/administrator/language/en-GB/en-GB.com_banners.ini', + '/administrator/language/en-GB/en-GB.com_banners.sys.ini', + '/administrator/language/en-GB/en-GB.com_cache.ini', + '/administrator/language/en-GB/en-GB.com_cache.sys.ini', + '/administrator/language/en-GB/en-GB.com_categories.ini', + '/administrator/language/en-GB/en-GB.com_categories.sys.ini', + '/administrator/language/en-GB/en-GB.com_checkin.ini', + '/administrator/language/en-GB/en-GB.com_checkin.sys.ini', + '/administrator/language/en-GB/en-GB.com_config.ini', + '/administrator/language/en-GB/en-GB.com_config.sys.ini', + '/administrator/language/en-GB/en-GB.com_contact.ini', + '/administrator/language/en-GB/en-GB.com_contact.sys.ini', + '/administrator/language/en-GB/en-GB.com_content.ini', + '/administrator/language/en-GB/en-GB.com_content.sys.ini', + '/administrator/language/en-GB/en-GB.com_contenthistory.ini', + '/administrator/language/en-GB/en-GB.com_contenthistory.sys.ini', + '/administrator/language/en-GB/en-GB.com_cpanel.ini', + '/administrator/language/en-GB/en-GB.com_cpanel.sys.ini', + '/administrator/language/en-GB/en-GB.com_fields.ini', + '/administrator/language/en-GB/en-GB.com_fields.sys.ini', + '/administrator/language/en-GB/en-GB.com_finder.ini', + '/administrator/language/en-GB/en-GB.com_finder.sys.ini', + '/administrator/language/en-GB/en-GB.com_installer.ini', + '/administrator/language/en-GB/en-GB.com_installer.sys.ini', + '/administrator/language/en-GB/en-GB.com_joomlaupdate.ini', + '/administrator/language/en-GB/en-GB.com_joomlaupdate.sys.ini', + '/administrator/language/en-GB/en-GB.com_languages.ini', + '/administrator/language/en-GB/en-GB.com_languages.sys.ini', + '/administrator/language/en-GB/en-GB.com_login.ini', + '/administrator/language/en-GB/en-GB.com_login.sys.ini', + '/administrator/language/en-GB/en-GB.com_mailto.sys.ini', + '/administrator/language/en-GB/en-GB.com_media.ini', + '/administrator/language/en-GB/en-GB.com_media.sys.ini', + '/administrator/language/en-GB/en-GB.com_menus.ini', + '/administrator/language/en-GB/en-GB.com_menus.sys.ini', + '/administrator/language/en-GB/en-GB.com_messages.ini', + '/administrator/language/en-GB/en-GB.com_messages.sys.ini', + '/administrator/language/en-GB/en-GB.com_modules.ini', + '/administrator/language/en-GB/en-GB.com_modules.sys.ini', + '/administrator/language/en-GB/en-GB.com_newsfeeds.ini', + '/administrator/language/en-GB/en-GB.com_newsfeeds.sys.ini', + '/administrator/language/en-GB/en-GB.com_plugins.ini', + '/administrator/language/en-GB/en-GB.com_plugins.sys.ini', + '/administrator/language/en-GB/en-GB.com_postinstall.ini', + '/administrator/language/en-GB/en-GB.com_postinstall.sys.ini', + '/administrator/language/en-GB/en-GB.com_privacy.ini', + '/administrator/language/en-GB/en-GB.com_privacy.sys.ini', + '/administrator/language/en-GB/en-GB.com_redirect.ini', + '/administrator/language/en-GB/en-GB.com_redirect.sys.ini', + '/administrator/language/en-GB/en-GB.com_tags.ini', + '/administrator/language/en-GB/en-GB.com_tags.sys.ini', + '/administrator/language/en-GB/en-GB.com_templates.ini', + '/administrator/language/en-GB/en-GB.com_templates.sys.ini', + '/administrator/language/en-GB/en-GB.com_users.ini', + '/administrator/language/en-GB/en-GB.com_users.sys.ini', + '/administrator/language/en-GB/en-GB.com_weblinks.ini', + '/administrator/language/en-GB/en-GB.com_weblinks.sys.ini', + '/administrator/language/en-GB/en-GB.com_wrapper.ini', + '/administrator/language/en-GB/en-GB.com_wrapper.sys.ini', + '/administrator/language/en-GB/en-GB.ini', + '/administrator/language/en-GB/en-GB.lib_joomla.ini', + '/administrator/language/en-GB/en-GB.localise.php', + '/administrator/language/en-GB/en-GB.mod_custom.ini', + '/administrator/language/en-GB/en-GB.mod_custom.sys.ini', + '/administrator/language/en-GB/en-GB.mod_feed.ini', + '/administrator/language/en-GB/en-GB.mod_feed.sys.ini', + '/administrator/language/en-GB/en-GB.mod_latest.ini', + '/administrator/language/en-GB/en-GB.mod_latest.sys.ini', + '/administrator/language/en-GB/en-GB.mod_latestactions.ini', + '/administrator/language/en-GB/en-GB.mod_latestactions.sys.ini', + '/administrator/language/en-GB/en-GB.mod_logged.ini', + '/administrator/language/en-GB/en-GB.mod_logged.sys.ini', + '/administrator/language/en-GB/en-GB.mod_login.ini', + '/administrator/language/en-GB/en-GB.mod_login.sys.ini', + '/administrator/language/en-GB/en-GB.mod_menu.ini', + '/administrator/language/en-GB/en-GB.mod_menu.sys.ini', + '/administrator/language/en-GB/en-GB.mod_multilangstatus.ini', + '/administrator/language/en-GB/en-GB.mod_multilangstatus.sys.ini', + '/administrator/language/en-GB/en-GB.mod_popular.ini', + '/administrator/language/en-GB/en-GB.mod_popular.sys.ini', + '/administrator/language/en-GB/en-GB.mod_privacy_dashboard.ini', + '/administrator/language/en-GB/en-GB.mod_privacy_dashboard.sys.ini', + '/administrator/language/en-GB/en-GB.mod_quickicon.ini', + '/administrator/language/en-GB/en-GB.mod_quickicon.sys.ini', + '/administrator/language/en-GB/en-GB.mod_sampledata.ini', + '/administrator/language/en-GB/en-GB.mod_sampledata.sys.ini', + '/administrator/language/en-GB/en-GB.mod_stats_admin.ini', + '/administrator/language/en-GB/en-GB.mod_stats_admin.sys.ini', + '/administrator/language/en-GB/en-GB.mod_status.ini', + '/administrator/language/en-GB/en-GB.mod_status.sys.ini', + '/administrator/language/en-GB/en-GB.mod_submenu.ini', + '/administrator/language/en-GB/en-GB.mod_submenu.sys.ini', + '/administrator/language/en-GB/en-GB.mod_title.ini', + '/administrator/language/en-GB/en-GB.mod_title.sys.ini', + '/administrator/language/en-GB/en-GB.mod_toolbar.ini', + '/administrator/language/en-GB/en-GB.mod_toolbar.sys.ini', + '/administrator/language/en-GB/en-GB.mod_version.ini', + '/administrator/language/en-GB/en-GB.mod_version.sys.ini', + '/administrator/language/en-GB/en-GB.plg_actionlog_joomla.ini', + '/administrator/language/en-GB/en-GB.plg_actionlog_joomla.sys.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_cookie.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_cookie.sys.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_gmail.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_gmail.sys.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_joomla.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_joomla.sys.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_ldap.ini', + '/administrator/language/en-GB/en-GB.plg_authentication_ldap.sys.ini', + '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha.ini', + '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha.sys.ini', + '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha_invisible.ini', + '/administrator/language/en-GB/en-GB.plg_captcha_recaptcha_invisible.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_confirmconsent.ini', + '/administrator/language/en-GB/en-GB.plg_content_confirmconsent.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_contact.ini', + '/administrator/language/en-GB/en-GB.plg_content_contact.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_emailcloak.ini', + '/administrator/language/en-GB/en-GB.plg_content_emailcloak.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_fields.ini', + '/administrator/language/en-GB/en-GB.plg_content_fields.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_finder.ini', + '/administrator/language/en-GB/en-GB.plg_content_finder.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_joomla.ini', + '/administrator/language/en-GB/en-GB.plg_content_joomla.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_loadmodule.ini', + '/administrator/language/en-GB/en-GB.plg_content_loadmodule.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_pagebreak.ini', + '/administrator/language/en-GB/en-GB.plg_content_pagebreak.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_pagenavigation.ini', + '/administrator/language/en-GB/en-GB.plg_content_pagenavigation.sys.ini', + '/administrator/language/en-GB/en-GB.plg_content_vote.ini', + '/administrator/language/en-GB/en-GB.plg_content_vote.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_article.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_article.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_contact.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_contact.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_fields.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_fields.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_image.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_image.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_menu.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_menu.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_module.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_module.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_pagebreak.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_pagebreak.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_readmore.ini', + '/administrator/language/en-GB/en-GB.plg_editors-xtd_readmore.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors_codemirror.ini', + '/administrator/language/en-GB/en-GB.plg_editors_codemirror.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors_none.ini', + '/administrator/language/en-GB/en-GB.plg_editors_none.sys.ini', + '/administrator/language/en-GB/en-GB.plg_editors_tinymce.ini', + '/administrator/language/en-GB/en-GB.plg_editors_tinymce.sys.ini', + '/administrator/language/en-GB/en-GB.plg_extension_joomla.ini', + '/administrator/language/en-GB/en-GB.plg_extension_joomla.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_calendar.ini', + '/administrator/language/en-GB/en-GB.plg_fields_calendar.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_checkboxes.ini', + '/administrator/language/en-GB/en-GB.plg_fields_checkboxes.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_color.ini', + '/administrator/language/en-GB/en-GB.plg_fields_color.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_editor.ini', + '/administrator/language/en-GB/en-GB.plg_fields_editor.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_image.ini', + '/administrator/language/en-GB/en-GB.plg_fields_image.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_imagelist.ini', + '/administrator/language/en-GB/en-GB.plg_fields_imagelist.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_integer.ini', + '/administrator/language/en-GB/en-GB.plg_fields_integer.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_list.ini', + '/administrator/language/en-GB/en-GB.plg_fields_list.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_media.ini', + '/administrator/language/en-GB/en-GB.plg_fields_media.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_radio.ini', + '/administrator/language/en-GB/en-GB.plg_fields_radio.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_sql.ini', + '/administrator/language/en-GB/en-GB.plg_fields_sql.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_text.ini', + '/administrator/language/en-GB/en-GB.plg_fields_text.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_textarea.ini', + '/administrator/language/en-GB/en-GB.plg_fields_textarea.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_url.ini', + '/administrator/language/en-GB/en-GB.plg_fields_url.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_user.ini', + '/administrator/language/en-GB/en-GB.plg_fields_user.sys.ini', + '/administrator/language/en-GB/en-GB.plg_fields_usergrouplist.ini', + '/administrator/language/en-GB/en-GB.plg_fields_usergrouplist.sys.ini', + '/administrator/language/en-GB/en-GB.plg_finder_categories.ini', + '/administrator/language/en-GB/en-GB.plg_finder_categories.sys.ini', + '/administrator/language/en-GB/en-GB.plg_finder_contacts.ini', + '/administrator/language/en-GB/en-GB.plg_finder_contacts.sys.ini', + '/administrator/language/en-GB/en-GB.plg_finder_content.ini', + '/administrator/language/en-GB/en-GB.plg_finder_content.sys.ini', + '/administrator/language/en-GB/en-GB.plg_finder_newsfeeds.ini', + '/administrator/language/en-GB/en-GB.plg_finder_newsfeeds.sys.ini', + '/administrator/language/en-GB/en-GB.plg_finder_tags.ini', + '/administrator/language/en-GB/en-GB.plg_finder_tags.sys.ini', + '/administrator/language/en-GB/en-GB.plg_finder_weblinks.ini', + '/administrator/language/en-GB/en-GB.plg_finder_weblinks.sys.ini', + '/administrator/language/en-GB/en-GB.plg_installer_folderinstaller.ini', + '/administrator/language/en-GB/en-GB.plg_installer_folderinstaller.sys.ini', + '/administrator/language/en-GB/en-GB.plg_installer_packageinstaller.ini', + '/administrator/language/en-GB/en-GB.plg_installer_packageinstaller.sys.ini', + '/administrator/language/en-GB/en-GB.plg_installer_urlinstaller.ini', + '/administrator/language/en-GB/en-GB.plg_installer_urlinstaller.sys.ini', + '/administrator/language/en-GB/en-GB.plg_installer_webinstaller.ini', + '/administrator/language/en-GB/en-GB.plg_installer_webinstaller.sys.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_actionlogs.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_actionlogs.sys.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_consents.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_consents.sys.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_contact.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_contact.sys.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_content.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_content.sys.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_message.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_message.sys.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_user.ini', + '/administrator/language/en-GB/en-GB.plg_privacy_user.sys.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_extensionupdate.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_extensionupdate.sys.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_joomlaupdate.sys.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_phpversioncheck.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_phpversioncheck.sys.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_privacycheck.ini', + '/administrator/language/en-GB/en-GB.plg_quickicon_privacycheck.sys.ini', + '/administrator/language/en-GB/en-GB.plg_sampledata_blog.ini', + '/administrator/language/en-GB/en-GB.plg_sampledata_blog.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_actionlogs.ini', + '/administrator/language/en-GB/en-GB.plg_system_actionlogs.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_cache.ini', + '/administrator/language/en-GB/en-GB.plg_system_cache.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_debug.ini', + '/administrator/language/en-GB/en-GB.plg_system_debug.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_fields.ini', + '/administrator/language/en-GB/en-GB.plg_system_fields.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_highlight.ini', + '/administrator/language/en-GB/en-GB.plg_system_highlight.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_languagecode.ini', + '/administrator/language/en-GB/en-GB.plg_system_languagecode.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_languagefilter.ini', + '/administrator/language/en-GB/en-GB.plg_system_languagefilter.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_log.ini', + '/administrator/language/en-GB/en-GB.plg_system_log.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_logout.ini', + '/administrator/language/en-GB/en-GB.plg_system_logout.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_logrotation.ini', + '/administrator/language/en-GB/en-GB.plg_system_logrotation.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_p3p.ini', + '/administrator/language/en-GB/en-GB.plg_system_p3p.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_privacyconsent.ini', + '/administrator/language/en-GB/en-GB.plg_system_privacyconsent.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_redirect.ini', + '/administrator/language/en-GB/en-GB.plg_system_redirect.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_remember.ini', + '/administrator/language/en-GB/en-GB.plg_system_remember.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_sef.ini', + '/administrator/language/en-GB/en-GB.plg_system_sef.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_sessiongc.ini', + '/administrator/language/en-GB/en-GB.plg_system_sessiongc.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_stats.ini', + '/administrator/language/en-GB/en-GB.plg_system_stats.sys.ini', + '/administrator/language/en-GB/en-GB.plg_system_updatenotification.ini', + '/administrator/language/en-GB/en-GB.plg_system_updatenotification.sys.ini', + '/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.ini', + '/administrator/language/en-GB/en-GB.plg_twofactorauth_totp.sys.ini', + '/administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.ini', + '/administrator/language/en-GB/en-GB.plg_twofactorauth_yubikey.sys.ini', + '/administrator/language/en-GB/en-GB.plg_user_contactcreator.ini', + '/administrator/language/en-GB/en-GB.plg_user_contactcreator.sys.ini', + '/administrator/language/en-GB/en-GB.plg_user_joomla.ini', + '/administrator/language/en-GB/en-GB.plg_user_joomla.sys.ini', + '/administrator/language/en-GB/en-GB.plg_user_profile.ini', + '/administrator/language/en-GB/en-GB.plg_user_profile.sys.ini', + '/administrator/language/en-GB/en-GB.plg_user_terms.ini', + '/administrator/language/en-GB/en-GB.plg_user_terms.sys.ini', + '/administrator/language/en-GB/en-GB.tpl_hathor.ini', + '/administrator/language/en-GB/en-GB.tpl_hathor.sys.ini', + '/administrator/language/en-GB/en-GB.tpl_isis.ini', + '/administrator/language/en-GB/en-GB.tpl_isis.sys.ini', + '/administrator/language/en-GB/en-GB.xml', + '/administrator/manifests/libraries/fof.xml', + '/administrator/manifests/libraries/idna_convert.xml', + '/administrator/manifests/libraries/phputf8.xml', + '/administrator/modules/mod_feed/helper.php', + '/administrator/modules/mod_latest/helper.php', + '/administrator/modules/mod_latestactions/helper.php', + '/administrator/modules/mod_logged/helper.php', + '/administrator/modules/mod_login/helper.php', + '/administrator/modules/mod_menu/helper.php', + '/administrator/modules/mod_menu/menu.php', + '/administrator/modules/mod_multilangstatus/language/en-GB/en-GB.mod_multilangstatus.ini', + '/administrator/modules/mod_multilangstatus/language/en-GB/en-GB.mod_multilangstatus.sys.ini', + '/administrator/modules/mod_popular/helper.php', + '/administrator/modules/mod_privacy_dashboard/helper.php', + '/administrator/modules/mod_quickicon/helper.php', + '/administrator/modules/mod_quickicon/mod_quickicon.php', + '/administrator/modules/mod_sampledata/helper.php', + '/administrator/modules/mod_stats_admin/helper.php', + '/administrator/modules/mod_stats_admin/language/en-GB.mod_stats_admin.ini', + '/administrator/modules/mod_stats_admin/language/en-GB.mod_stats_admin.sys.ini', + '/administrator/modules/mod_status/mod_status.php', + '/administrator/modules/mod_status/mod_status.xml', + '/administrator/modules/mod_status/tmpl/default.php', + '/administrator/modules/mod_version/helper.php', + '/administrator/modules/mod_version/language/en-GB/en-GB.mod_version.ini', + '/administrator/modules/mod_version/language/en-GB/en-GB.mod_version.sys.ini', + '/administrator/templates/hathor/LICENSE.txt', + '/administrator/templates/hathor/component.php', + '/administrator/templates/hathor/cpanel.php', + '/administrator/templates/hathor/css/boldtext.css', + '/administrator/templates/hathor/css/colour_blue.css', + '/administrator/templates/hathor/css/colour_blue_rtl.css', + '/administrator/templates/hathor/css/colour_brown.css', + '/administrator/templates/hathor/css/colour_brown_rtl.css', + '/administrator/templates/hathor/css/colour_highcontrast.css', + '/administrator/templates/hathor/css/colour_highcontrast_rtl.css', + '/administrator/templates/hathor/css/colour_standard.css', + '/administrator/templates/hathor/css/colour_standard_rtl.css', + '/administrator/templates/hathor/css/error.css', + '/administrator/templates/hathor/css/ie7.css', + '/administrator/templates/hathor/css/ie8.css', + '/administrator/templates/hathor/css/template.css', + '/administrator/templates/hathor/css/template_rtl.css', + '/administrator/templates/hathor/css/theme.css', + '/administrator/templates/hathor/error.php', + '/administrator/templates/hathor/favicon.ico', + '/administrator/templates/hathor/html/com_admin/help/default.php', + '/administrator/templates/hathor/html/com_admin/profile/edit.php', + '/administrator/templates/hathor/html/com_admin/sysinfo/default.php', + '/administrator/templates/hathor/html/com_admin/sysinfo/default_config.php', + '/administrator/templates/hathor/html/com_admin/sysinfo/default_directory.php', + '/administrator/templates/hathor/html/com_admin/sysinfo/default_navigation.php', + '/administrator/templates/hathor/html/com_admin/sysinfo/default_phpsettings.php', + '/administrator/templates/hathor/html/com_admin/sysinfo/default_system.php', + '/administrator/templates/hathor/html/com_associations/associations/default.php', + '/administrator/templates/hathor/html/com_banners/banner/edit.php', + '/administrator/templates/hathor/html/com_banners/banners/default.php', + '/administrator/templates/hathor/html/com_banners/client/edit.php', + '/administrator/templates/hathor/html/com_banners/clients/default.php', + '/administrator/templates/hathor/html/com_banners/download/default.php', + '/administrator/templates/hathor/html/com_banners/tracks/default.php', + '/administrator/templates/hathor/html/com_cache/cache/default.php', + '/administrator/templates/hathor/html/com_cache/purge/default.php', + '/administrator/templates/hathor/html/com_categories/categories/default.php', + '/administrator/templates/hathor/html/com_categories/category/edit.php', + '/administrator/templates/hathor/html/com_categories/category/edit_options.php', + '/administrator/templates/hathor/html/com_checkin/checkin/default.php', + '/administrator/templates/hathor/html/com_config/application/default.php', + '/administrator/templates/hathor/html/com_config/application/default_cache.php', + '/administrator/templates/hathor/html/com_config/application/default_cookie.php', + '/administrator/templates/hathor/html/com_config/application/default_database.php', + '/administrator/templates/hathor/html/com_config/application/default_debug.php', + '/administrator/templates/hathor/html/com_config/application/default_filters.php', + '/administrator/templates/hathor/html/com_config/application/default_ftp.php', + '/administrator/templates/hathor/html/com_config/application/default_ftplogin.php', + '/administrator/templates/hathor/html/com_config/application/default_locale.php', + '/administrator/templates/hathor/html/com_config/application/default_mail.php', + '/administrator/templates/hathor/html/com_config/application/default_metadata.php', + '/administrator/templates/hathor/html/com_config/application/default_navigation.php', + '/administrator/templates/hathor/html/com_config/application/default_permissions.php', + '/administrator/templates/hathor/html/com_config/application/default_seo.php', + '/administrator/templates/hathor/html/com_config/application/default_server.php', + '/administrator/templates/hathor/html/com_config/application/default_session.php', + '/administrator/templates/hathor/html/com_config/application/default_site.php', + '/administrator/templates/hathor/html/com_config/application/default_system.php', + '/administrator/templates/hathor/html/com_config/component/default.php', + '/administrator/templates/hathor/html/com_contact/contact/edit.php', + '/administrator/templates/hathor/html/com_contact/contact/edit_params.php', + '/administrator/templates/hathor/html/com_contact/contacts/default.php', + '/administrator/templates/hathor/html/com_contact/contacts/modal.php', + '/administrator/templates/hathor/html/com_content/article/edit.php', + '/administrator/templates/hathor/html/com_content/articles/default.php', + '/administrator/templates/hathor/html/com_content/articles/modal.php', + '/administrator/templates/hathor/html/com_content/featured/default.php', + '/administrator/templates/hathor/html/com_contenthistory/history/modal.php', + '/administrator/templates/hathor/html/com_cpanel/cpanel/default.php', + '/administrator/templates/hathor/html/com_fields/field/edit.php', + '/administrator/templates/hathor/html/com_fields/fields/default.php', + '/administrator/templates/hathor/html/com_fields/group/edit.php', + '/administrator/templates/hathor/html/com_fields/groups/default.php', + '/administrator/templates/hathor/html/com_finder/filters/default.php', + '/administrator/templates/hathor/html/com_finder/index/default.php', + '/administrator/templates/hathor/html/com_finder/maps/default.php', + '/administrator/templates/hathor/html/com_installer/database/default.php', + '/administrator/templates/hathor/html/com_installer/default/default_ftp.php', + '/administrator/templates/hathor/html/com_installer/discover/default.php', + '/administrator/templates/hathor/html/com_installer/install/default.php', + '/administrator/templates/hathor/html/com_installer/install/default_form.php', + '/administrator/templates/hathor/html/com_installer/languages/default.php', + '/administrator/templates/hathor/html/com_installer/languages/default_filter.php', + '/administrator/templates/hathor/html/com_installer/manage/default.php', + '/administrator/templates/hathor/html/com_installer/manage/default_filter.php', + '/administrator/templates/hathor/html/com_installer/update/default.php', + '/administrator/templates/hathor/html/com_installer/warnings/default.php', + '/administrator/templates/hathor/html/com_joomlaupdate/default/default.php', + '/administrator/templates/hathor/html/com_languages/installed/default.php', + '/administrator/templates/hathor/html/com_languages/installed/default_ftp.php', + '/administrator/templates/hathor/html/com_languages/languages/default.php', + '/administrator/templates/hathor/html/com_languages/overrides/default.php', + '/administrator/templates/hathor/html/com_menus/item/edit.php', + '/administrator/templates/hathor/html/com_menus/item/edit_options.php', + '/administrator/templates/hathor/html/com_menus/items/default.php', + '/administrator/templates/hathor/html/com_menus/menu/edit.php', + '/administrator/templates/hathor/html/com_menus/menus/default.php', + '/administrator/templates/hathor/html/com_menus/menutypes/default.php', + '/administrator/templates/hathor/html/com_messages/message/edit.php', + '/administrator/templates/hathor/html/com_messages/messages/default.php', + '/administrator/templates/hathor/html/com_modules/module/edit.php', + '/administrator/templates/hathor/html/com_modules/module/edit_assignment.php', + '/administrator/templates/hathor/html/com_modules/module/edit_options.php', + '/administrator/templates/hathor/html/com_modules/modules/default.php', + '/administrator/templates/hathor/html/com_modules/positions/modal.php', + '/administrator/templates/hathor/html/com_newsfeeds/newsfeed/edit.php', + '/administrator/templates/hathor/html/com_newsfeeds/newsfeed/edit_params.php', + '/administrator/templates/hathor/html/com_newsfeeds/newsfeeds/default.php', + '/administrator/templates/hathor/html/com_newsfeeds/newsfeeds/modal.php', + '/administrator/templates/hathor/html/com_plugins/plugin/edit.php', + '/administrator/templates/hathor/html/com_plugins/plugin/edit_options.php', + '/administrator/templates/hathor/html/com_plugins/plugins/default.php', + '/administrator/templates/hathor/html/com_postinstall/messages/default.php', + '/administrator/templates/hathor/html/com_redirect/links/default.php', + '/administrator/templates/hathor/html/com_search/searches/default.php', + '/administrator/templates/hathor/html/com_tags/tag/edit.php', + '/administrator/templates/hathor/html/com_tags/tag/edit_metadata.php', + '/administrator/templates/hathor/html/com_tags/tag/edit_options.php', + '/administrator/templates/hathor/html/com_tags/tags/default.php', + '/administrator/templates/hathor/html/com_templates/style/edit.php', + '/administrator/templates/hathor/html/com_templates/style/edit_assignment.php', + '/administrator/templates/hathor/html/com_templates/style/edit_options.php', + '/administrator/templates/hathor/html/com_templates/styles/default.php', + '/administrator/templates/hathor/html/com_templates/template/default.php', + '/administrator/templates/hathor/html/com_templates/template/default_description.php', + '/administrator/templates/hathor/html/com_templates/template/default_folders.php', + '/administrator/templates/hathor/html/com_templates/template/default_tree.php', + '/administrator/templates/hathor/html/com_templates/templates/default.php', + '/administrator/templates/hathor/html/com_users/debuggroup/default.php', + '/administrator/templates/hathor/html/com_users/debuguser/default.php', + '/administrator/templates/hathor/html/com_users/groups/default.php', + '/administrator/templates/hathor/html/com_users/levels/default.php', + '/administrator/templates/hathor/html/com_users/note/edit.php', + '/administrator/templates/hathor/html/com_users/notes/default.php', + '/administrator/templates/hathor/html/com_users/user/edit.php', + '/administrator/templates/hathor/html/com_users/users/default.php', + '/administrator/templates/hathor/html/com_users/users/modal.php', + '/administrator/templates/hathor/html/com_weblinks/weblink/edit.php', + '/administrator/templates/hathor/html/com_weblinks/weblink/edit_params.php', + '/administrator/templates/hathor/html/com_weblinks/weblinks/default.php', + '/administrator/templates/hathor/html/layouts/com_media/toolbar/deletemedia.php', + '/administrator/templates/hathor/html/layouts/com_media/toolbar/newfolder.php', + '/administrator/templates/hathor/html/layouts/com_media/toolbar/uploadmedia.php', + '/administrator/templates/hathor/html/layouts/com_messages/toolbar/mysettings.php', + '/administrator/templates/hathor/html/layouts/com_modules/toolbar/cancelselect.php', + '/administrator/templates/hathor/html/layouts/com_modules/toolbar/newmodule.php', + '/administrator/templates/hathor/html/layouts/joomla/edit/details.php', + '/administrator/templates/hathor/html/layouts/joomla/edit/fieldset.php', + '/administrator/templates/hathor/html/layouts/joomla/edit/global.php', + '/administrator/templates/hathor/html/layouts/joomla/edit/metadata.php', + '/administrator/templates/hathor/html/layouts/joomla/edit/params.php', + '/administrator/templates/hathor/html/layouts/joomla/quickicons/icon.php', + '/administrator/templates/hathor/html/layouts/joomla/sidebars/submenu.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/base.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/batch.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/confirm.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/containerclose.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/containeropen.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/help.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/iconclass.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/link.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/modal.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/popup.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/separator.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/slider.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/standard.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/title.php', + '/administrator/templates/hathor/html/layouts/joomla/toolbar/versions.php', + '/administrator/templates/hathor/html/layouts/plugins/user/profile/fields/dob.php', + '/administrator/templates/hathor/html/mod_login/default.php', + '/administrator/templates/hathor/html/mod_quickicon/default.php', + '/administrator/templates/hathor/html/modules.php', + '/administrator/templates/hathor/html/pagination.php', + '/administrator/templates/hathor/images/admin/blank.png', + '/administrator/templates/hathor/images/admin/checked_out.png', + '/administrator/templates/hathor/images/admin/collapseall.png', + '/administrator/templates/hathor/images/admin/disabled.png', + '/administrator/templates/hathor/images/admin/downarrow-1.png', + '/administrator/templates/hathor/images/admin/downarrow.png', + '/administrator/templates/hathor/images/admin/downarrow0.png', + '/administrator/templates/hathor/images/admin/expandall.png', + '/administrator/templates/hathor/images/admin/featured.png', + '/administrator/templates/hathor/images/admin/filesave.png', + '/administrator/templates/hathor/images/admin/filter_16.png', + '/administrator/templates/hathor/images/admin/icon-16-allow.png', + '/administrator/templates/hathor/images/admin/icon-16-allowinactive.png', + '/administrator/templates/hathor/images/admin/icon-16-deny.png', + '/administrator/templates/hathor/images/admin/icon-16-denyinactive.png', + '/administrator/templates/hathor/images/admin/icon-16-links.png', + '/administrator/templates/hathor/images/admin/icon-16-notice-note.png', + '/administrator/templates/hathor/images/admin/icon-16-protected.png', + '/administrator/templates/hathor/images/admin/menu_divider.png', + '/administrator/templates/hathor/images/admin/note_add_16.png', + '/administrator/templates/hathor/images/admin/publish_g.png', + '/administrator/templates/hathor/images/admin/publish_r.png', + '/administrator/templates/hathor/images/admin/publish_x.png', + '/administrator/templates/hathor/images/admin/publish_y.png', + '/administrator/templates/hathor/images/admin/sort_asc.png', + '/administrator/templates/hathor/images/admin/sort_desc.png', + '/administrator/templates/hathor/images/admin/tick.png', + '/administrator/templates/hathor/images/admin/trash.png', + '/administrator/templates/hathor/images/admin/uparrow-1.png', + '/administrator/templates/hathor/images/admin/uparrow.png', + '/administrator/templates/hathor/images/admin/uparrow0.png', + '/administrator/templates/hathor/images/arrow.png', + '/administrator/templates/hathor/images/bg-menu.gif', + '/administrator/templates/hathor/images/calendar.png', + '/administrator/templates/hathor/images/header/icon-48-alert.png', + '/administrator/templates/hathor/images/header/icon-48-apply.png', + '/administrator/templates/hathor/images/header/icon-48-archive.png', + '/administrator/templates/hathor/images/header/icon-48-article-add.png', + '/administrator/templates/hathor/images/header/icon-48-article-edit.png', + '/administrator/templates/hathor/images/header/icon-48-article.png', + '/administrator/templates/hathor/images/header/icon-48-assoc.png', + '/administrator/templates/hathor/images/header/icon-48-banner-categories.png', + '/administrator/templates/hathor/images/header/icon-48-banner-client.png', + '/administrator/templates/hathor/images/header/icon-48-banner-tracks.png', + '/administrator/templates/hathor/images/header/icon-48-banner.png', + '/administrator/templates/hathor/images/header/icon-48-calendar.png', + '/administrator/templates/hathor/images/header/icon-48-category-add.png', + '/administrator/templates/hathor/images/header/icon-48-category.png', + '/administrator/templates/hathor/images/header/icon-48-checkin.png', + '/administrator/templates/hathor/images/header/icon-48-clear.png', + '/administrator/templates/hathor/images/header/icon-48-component.png', + '/administrator/templates/hathor/images/header/icon-48-config.png', + '/administrator/templates/hathor/images/header/icon-48-contacts-categories.png', + '/administrator/templates/hathor/images/header/icon-48-contacts.png', + '/administrator/templates/hathor/images/header/icon-48-content.png', + '/administrator/templates/hathor/images/header/icon-48-cpanel.png', + '/administrator/templates/hathor/images/header/icon-48-default.png', + '/administrator/templates/hathor/images/header/icon-48-deny.png', + '/administrator/templates/hathor/images/header/icon-48-download.png', + '/administrator/templates/hathor/images/header/icon-48-edit.png', + '/administrator/templates/hathor/images/header/icon-48-extension.png', + '/administrator/templates/hathor/images/header/icon-48-featured.png', + '/administrator/templates/hathor/images/header/icon-48-frontpage.png', + '/administrator/templates/hathor/images/header/icon-48-generic.png', + '/administrator/templates/hathor/images/header/icon-48-groups-add.png', + '/administrator/templates/hathor/images/header/icon-48-groups.png', + '/administrator/templates/hathor/images/header/icon-48-help-forum.png', + '/administrator/templates/hathor/images/header/icon-48-help-this.png', + '/administrator/templates/hathor/images/header/icon-48-help_header.png', + '/administrator/templates/hathor/images/header/icon-48-inbox.png', + '/administrator/templates/hathor/images/header/icon-48-info.png', + '/administrator/templates/hathor/images/header/icon-48-install.png', + '/administrator/templates/hathor/images/header/icon-48-jupdate-updatefound.png', + '/administrator/templates/hathor/images/header/icon-48-jupdate-uptodate.png', + '/administrator/templates/hathor/images/header/icon-48-language.png', + '/administrator/templates/hathor/images/header/icon-48-levels-add.png', + '/administrator/templates/hathor/images/header/icon-48-levels.png', + '/administrator/templates/hathor/images/header/icon-48-links-cat.png', + '/administrator/templates/hathor/images/header/icon-48-links.png', + '/administrator/templates/hathor/images/header/icon-48-massmail.png', + '/administrator/templates/hathor/images/header/icon-48-media.png', + '/administrator/templates/hathor/images/header/icon-48-menu-add.png', + '/administrator/templates/hathor/images/header/icon-48-menu.png', + '/administrator/templates/hathor/images/header/icon-48-menumgr.png', + '/administrator/templates/hathor/images/header/icon-48-module.png', + '/administrator/templates/hathor/images/header/icon-48-move.png', + '/administrator/templates/hathor/images/header/icon-48-new-privatemessage.png', + '/administrator/templates/hathor/images/header/icon-48-newcategory.png', + '/administrator/templates/hathor/images/header/icon-48-newsfeeds-cat.png', + '/administrator/templates/hathor/images/header/icon-48-newsfeeds.png', + '/administrator/templates/hathor/images/header/icon-48-notice.png', + '/administrator/templates/hathor/images/header/icon-48-plugin.png', + '/administrator/templates/hathor/images/header/icon-48-preview.png', + '/administrator/templates/hathor/images/header/icon-48-print.png', + '/administrator/templates/hathor/images/header/icon-48-purge.png', + '/administrator/templates/hathor/images/header/icon-48-puzzle.png', + '/administrator/templates/hathor/images/header/icon-48-read-privatemessage.png', + '/administrator/templates/hathor/images/header/icon-48-readmess.png', + '/administrator/templates/hathor/images/header/icon-48-redirect.png', + '/administrator/templates/hathor/images/header/icon-48-revert.png', + '/administrator/templates/hathor/images/header/icon-48-search.png', + '/administrator/templates/hathor/images/header/icon-48-section.png', + '/administrator/templates/hathor/images/header/icon-48-send.png', + '/administrator/templates/hathor/images/header/icon-48-static.png', + '/administrator/templates/hathor/images/header/icon-48-stats.png', + '/administrator/templates/hathor/images/header/icon-48-tags.png', + '/administrator/templates/hathor/images/header/icon-48-themes.png', + '/administrator/templates/hathor/images/header/icon-48-trash.png', + '/administrator/templates/hathor/images/header/icon-48-unarchive.png', + '/administrator/templates/hathor/images/header/icon-48-upload.png', + '/administrator/templates/hathor/images/header/icon-48-user-add.png', + '/administrator/templates/hathor/images/header/icon-48-user-edit.png', + '/administrator/templates/hathor/images/header/icon-48-user-profile.png', + '/administrator/templates/hathor/images/header/icon-48-user.png', + '/administrator/templates/hathor/images/header/icon-48-writemess.png', + '/administrator/templates/hathor/images/header/icon-messaging.png', + '/administrator/templates/hathor/images/j_arrow.png', + '/administrator/templates/hathor/images/j_arrow_down.png', + '/administrator/templates/hathor/images/j_arrow_left.png', + '/administrator/templates/hathor/images/j_arrow_right.png', + '/administrator/templates/hathor/images/j_login_lock.png', + '/administrator/templates/hathor/images/j_logo.png', + '/administrator/templates/hathor/images/logo.png', + '/administrator/templates/hathor/images/menu/icon-16-alert.png', + '/administrator/templates/hathor/images/menu/icon-16-apply.png', + '/administrator/templates/hathor/images/menu/icon-16-archive.png', + '/administrator/templates/hathor/images/menu/icon-16-article.png', + '/administrator/templates/hathor/images/menu/icon-16-assoc.png', + '/administrator/templates/hathor/images/menu/icon-16-back-user.png', + '/administrator/templates/hathor/images/menu/icon-16-banner-categories.png', + '/administrator/templates/hathor/images/menu/icon-16-banner-client.png', + '/administrator/templates/hathor/images/menu/icon-16-banner-tracks.png', + '/administrator/templates/hathor/images/menu/icon-16-banner.png', + '/administrator/templates/hathor/images/menu/icon-16-calendar.png', + '/administrator/templates/hathor/images/menu/icon-16-category.png', + '/administrator/templates/hathor/images/menu/icon-16-checkin.png', + '/administrator/templates/hathor/images/menu/icon-16-clear.png', + '/administrator/templates/hathor/images/menu/icon-16-component.png', + '/administrator/templates/hathor/images/menu/icon-16-config.png', + '/administrator/templates/hathor/images/menu/icon-16-contacts-categories.png', + '/administrator/templates/hathor/images/menu/icon-16-contacts.png', + '/administrator/templates/hathor/images/menu/icon-16-content.png', + '/administrator/templates/hathor/images/menu/icon-16-cpanel.png', + '/administrator/templates/hathor/images/menu/icon-16-default.png', + '/administrator/templates/hathor/images/menu/icon-16-delete.png', + '/administrator/templates/hathor/images/menu/icon-16-deny.png', + '/administrator/templates/hathor/images/menu/icon-16-download.png', + '/administrator/templates/hathor/images/menu/icon-16-edit.png', + '/administrator/templates/hathor/images/menu/icon-16-featured.png', + '/administrator/templates/hathor/images/menu/icon-16-frontpage.png', + '/administrator/templates/hathor/images/menu/icon-16-generic.png', + '/administrator/templates/hathor/images/menu/icon-16-groups.png', + '/administrator/templates/hathor/images/menu/icon-16-help-community.png', + '/administrator/templates/hathor/images/menu/icon-16-help-dev.png', + '/administrator/templates/hathor/images/menu/icon-16-help-docs.png', + '/administrator/templates/hathor/images/menu/icon-16-help-forum.png', + '/administrator/templates/hathor/images/menu/icon-16-help-jed.png', + '/administrator/templates/hathor/images/menu/icon-16-help-jrd.png', + '/administrator/templates/hathor/images/menu/icon-16-help-security.png', + '/administrator/templates/hathor/images/menu/icon-16-help-shop.png', + '/administrator/templates/hathor/images/menu/icon-16-help-this.png', + '/administrator/templates/hathor/images/menu/icon-16-help-trans.png', + '/administrator/templates/hathor/images/menu/icon-16-help.png', + '/administrator/templates/hathor/images/menu/icon-16-inbox.png', + '/administrator/templates/hathor/images/menu/icon-16-info.png', + '/administrator/templates/hathor/images/menu/icon-16-install.png', + '/administrator/templates/hathor/images/menu/icon-16-language.png', + '/administrator/templates/hathor/images/menu/icon-16-levels.png', + '/administrator/templates/hathor/images/menu/icon-16-links-cat.png', + '/administrator/templates/hathor/images/menu/icon-16-links.png', + '/administrator/templates/hathor/images/menu/icon-16-logout.png', + '/administrator/templates/hathor/images/menu/icon-16-maintenance.png', + '/administrator/templates/hathor/images/menu/icon-16-massmail.png', + '/administrator/templates/hathor/images/menu/icon-16-media.png', + '/administrator/templates/hathor/images/menu/icon-16-menu.png', + '/administrator/templates/hathor/images/menu/icon-16-menumgr.png', + '/administrator/templates/hathor/images/menu/icon-16-messages.png', + '/administrator/templates/hathor/images/menu/icon-16-messaging.png', + '/administrator/templates/hathor/images/menu/icon-16-module.png', + '/administrator/templates/hathor/images/menu/icon-16-move.png', + '/administrator/templates/hathor/images/menu/icon-16-new-privatemessage.png', + '/administrator/templates/hathor/images/menu/icon-16-new.png', + '/administrator/templates/hathor/images/menu/icon-16-newarticle.png', + '/administrator/templates/hathor/images/menu/icon-16-newcategory.png', + '/administrator/templates/hathor/images/menu/icon-16-newgroup.png', + '/administrator/templates/hathor/images/menu/icon-16-newlevel.png', + '/administrator/templates/hathor/images/menu/icon-16-newsfeeds-cat.png', + '/administrator/templates/hathor/images/menu/icon-16-newsfeeds.png', + '/administrator/templates/hathor/images/menu/icon-16-newuser.png', + '/administrator/templates/hathor/images/menu/icon-16-nopreview.png', + '/administrator/templates/hathor/images/menu/icon-16-notdefault.png', + '/administrator/templates/hathor/images/menu/icon-16-notice.png', + '/administrator/templates/hathor/images/menu/icon-16-plugin.png', + '/administrator/templates/hathor/images/menu/icon-16-preview.png', + '/administrator/templates/hathor/images/menu/icon-16-print.png', + '/administrator/templates/hathor/images/menu/icon-16-purge.png', + '/administrator/templates/hathor/images/menu/icon-16-puzzle.png', + '/administrator/templates/hathor/images/menu/icon-16-read-privatemessage.png', + '/administrator/templates/hathor/images/menu/icon-16-readmess.png', + '/administrator/templates/hathor/images/menu/icon-16-redirect.png', + '/administrator/templates/hathor/images/menu/icon-16-revert.png', + '/administrator/templates/hathor/images/menu/icon-16-search.png', + '/administrator/templates/hathor/images/menu/icon-16-send.png', + '/administrator/templates/hathor/images/menu/icon-16-stats.png', + '/administrator/templates/hathor/images/menu/icon-16-tags.png', + '/administrator/templates/hathor/images/menu/icon-16-themes.png', + '/administrator/templates/hathor/images/menu/icon-16-trash.png', + '/administrator/templates/hathor/images/menu/icon-16-unarticle.png', + '/administrator/templates/hathor/images/menu/icon-16-upload.png', + '/administrator/templates/hathor/images/menu/icon-16-user-dd.png', + '/administrator/templates/hathor/images/menu/icon-16-user-note.png', + '/administrator/templates/hathor/images/menu/icon-16-user.png', + '/administrator/templates/hathor/images/menu/icon-16-viewsite.png', + '/administrator/templates/hathor/images/menu/icon-16-writemess.png', + '/administrator/templates/hathor/images/mini_icon.png', + '/administrator/templates/hathor/images/notice-alert.png', + '/administrator/templates/hathor/images/notice-info.png', + '/administrator/templates/hathor/images/notice-note.png', + '/administrator/templates/hathor/images/required.png', + '/administrator/templates/hathor/images/selector-arrow-hc.png', + '/administrator/templates/hathor/images/selector-arrow-rtl.png', + '/administrator/templates/hathor/images/selector-arrow-std.png', + '/administrator/templates/hathor/images/selector-arrow.png', + '/administrator/templates/hathor/images/system/calendar.png', + '/administrator/templates/hathor/images/system/selector-arrow.png', + '/administrator/templates/hathor/images/toolbar/icon-32-adduser.png', + '/administrator/templates/hathor/images/toolbar/icon-32-alert.png', + '/administrator/templates/hathor/images/toolbar/icon-32-apply.png', + '/administrator/templates/hathor/images/toolbar/icon-32-archive.png', + '/administrator/templates/hathor/images/toolbar/icon-32-article-add.png', + '/administrator/templates/hathor/images/toolbar/icon-32-article.png', + '/administrator/templates/hathor/images/toolbar/icon-32-back.png', + '/administrator/templates/hathor/images/toolbar/icon-32-banner-categories.png', + '/administrator/templates/hathor/images/toolbar/icon-32-banner-client.png', + '/administrator/templates/hathor/images/toolbar/icon-32-banner-tracks.png', + '/administrator/templates/hathor/images/toolbar/icon-32-banner.png', + '/administrator/templates/hathor/images/toolbar/icon-32-batch.png', + '/administrator/templates/hathor/images/toolbar/icon-32-calendar.png', + '/administrator/templates/hathor/images/toolbar/icon-32-cancel.png', + '/administrator/templates/hathor/images/toolbar/icon-32-checkin.png', + '/administrator/templates/hathor/images/toolbar/icon-32-cog.png', + '/administrator/templates/hathor/images/toolbar/icon-32-component.png', + '/administrator/templates/hathor/images/toolbar/icon-32-config.png', + '/administrator/templates/hathor/images/toolbar/icon-32-contacts-categories.png', + '/administrator/templates/hathor/images/toolbar/icon-32-contacts.png', + '/administrator/templates/hathor/images/toolbar/icon-32-copy.png', + '/administrator/templates/hathor/images/toolbar/icon-32-css.png', + '/administrator/templates/hathor/images/toolbar/icon-32-default.png', + '/administrator/templates/hathor/images/toolbar/icon-32-delete-style.png', + '/administrator/templates/hathor/images/toolbar/icon-32-delete.png', + '/administrator/templates/hathor/images/toolbar/icon-32-deny.png', + '/administrator/templates/hathor/images/toolbar/icon-32-download.png', + '/administrator/templates/hathor/images/toolbar/icon-32-edit.png', + '/administrator/templates/hathor/images/toolbar/icon-32-error.png', + '/administrator/templates/hathor/images/toolbar/icon-32-export.png', + '/administrator/templates/hathor/images/toolbar/icon-32-extension.png', + '/administrator/templates/hathor/images/toolbar/icon-32-featured.png', + '/administrator/templates/hathor/images/toolbar/icon-32-forward.png', + '/administrator/templates/hathor/images/toolbar/icon-32-help.png', + '/administrator/templates/hathor/images/toolbar/icon-32-html.png', + '/administrator/templates/hathor/images/toolbar/icon-32-inbox.png', + '/administrator/templates/hathor/images/toolbar/icon-32-info.png', + '/administrator/templates/hathor/images/toolbar/icon-32-links.png', + '/administrator/templates/hathor/images/toolbar/icon-32-lock.png', + '/administrator/templates/hathor/images/toolbar/icon-32-menu.png', + '/administrator/templates/hathor/images/toolbar/icon-32-messaging.png', + '/administrator/templates/hathor/images/toolbar/icon-32-messanging.png', + '/administrator/templates/hathor/images/toolbar/icon-32-module.png', + '/administrator/templates/hathor/images/toolbar/icon-32-move.png', + '/administrator/templates/hathor/images/toolbar/icon-32-new-privatemessage.png', + '/administrator/templates/hathor/images/toolbar/icon-32-new-style.png', + '/administrator/templates/hathor/images/toolbar/icon-32-new.png', + '/administrator/templates/hathor/images/toolbar/icon-32-notice.png', + '/administrator/templates/hathor/images/toolbar/icon-32-preview.png', + '/administrator/templates/hathor/images/toolbar/icon-32-print.png', + '/administrator/templates/hathor/images/toolbar/icon-32-publish.png', + '/administrator/templates/hathor/images/toolbar/icon-32-purge.png', + '/administrator/templates/hathor/images/toolbar/icon-32-read-privatemessage.png', + '/administrator/templates/hathor/images/toolbar/icon-32-refresh.png', + '/administrator/templates/hathor/images/toolbar/icon-32-remove.png', + '/administrator/templates/hathor/images/toolbar/icon-32-revert.png', + '/administrator/templates/hathor/images/toolbar/icon-32-save-copy.png', + '/administrator/templates/hathor/images/toolbar/icon-32-save-new.png', + '/administrator/templates/hathor/images/toolbar/icon-32-save.png', + '/administrator/templates/hathor/images/toolbar/icon-32-search.png', + '/administrator/templates/hathor/images/toolbar/icon-32-send.png', + '/administrator/templates/hathor/images/toolbar/icon-32-stats.png', + '/administrator/templates/hathor/images/toolbar/icon-32-trash.png', + '/administrator/templates/hathor/images/toolbar/icon-32-unarchive.png', + '/administrator/templates/hathor/images/toolbar/icon-32-unblock.png', + '/administrator/templates/hathor/images/toolbar/icon-32-unpublish.png', + '/administrator/templates/hathor/images/toolbar/icon-32-upload.png', + '/administrator/templates/hathor/images/toolbar/icon-32-user-add.png', + '/administrator/templates/hathor/images/toolbar/icon-32-xml.png', + '/administrator/templates/hathor/index.php', + '/administrator/templates/hathor/js/template.js', + '/administrator/templates/hathor/language/en-GB/en-GB.tpl_hathor.ini', + '/administrator/templates/hathor/language/en-GB/en-GB.tpl_hathor.sys.ini', + '/administrator/templates/hathor/less/buttons.less', + '/administrator/templates/hathor/less/colour_baseline.less', + '/administrator/templates/hathor/less/colour_blue.less', + '/administrator/templates/hathor/less/colour_brown.less', + '/administrator/templates/hathor/less/colour_standard.less', + '/administrator/templates/hathor/less/forms.less', + '/administrator/templates/hathor/less/hathor_variables.less', + '/administrator/templates/hathor/less/icomoon.less', + '/administrator/templates/hathor/less/modals.less', + '/administrator/templates/hathor/less/template.less', + '/administrator/templates/hathor/less/variables.less', + '/administrator/templates/hathor/login.php', + '/administrator/templates/hathor/postinstall/hathormessage.php', + '/administrator/templates/hathor/templateDetails.xml', + '/administrator/templates/hathor/template_preview.png', + '/administrator/templates/hathor/template_thumbnail.png', + '/administrator/templates/isis/component.php', + '/administrator/templates/isis/cpanel.php', + '/administrator/templates/isis/css/template-rtl.css', + '/administrator/templates/isis/css/template.css', + '/administrator/templates/isis/error.php', + '/administrator/templates/isis/favicon.ico', + '/administrator/templates/isis/html/com_media/imageslist/default_folder.php', + '/administrator/templates/isis/html/com_media/imageslist/default_image.php', + '/administrator/templates/isis/html/com_media/medialist/thumbs_folders.php', + '/administrator/templates/isis/html/com_media/medialist/thumbs_imgs.php', + '/administrator/templates/isis/html/editor_content.css', + '/administrator/templates/isis/html/layouts/joomla/form/field/media.php', + '/administrator/templates/isis/html/layouts/joomla/form/field/user.php', + '/administrator/templates/isis/html/layouts/joomla/pagination/link.php', + '/administrator/templates/isis/html/layouts/joomla/pagination/links.php', + '/administrator/templates/isis/html/layouts/joomla/system/message.php', + '/administrator/templates/isis/html/layouts/joomla/toolbar/versions.php', + '/administrator/templates/isis/html/mod_version/default.php', + '/administrator/templates/isis/html/modules.php', + '/administrator/templates/isis/html/pagination.php', + '/administrator/templates/isis/images/admin/blank.png', + '/administrator/templates/isis/images/admin/checked_out.png', + '/administrator/templates/isis/images/admin/collapseall.png', + '/administrator/templates/isis/images/admin/disabled.png', + '/administrator/templates/isis/images/admin/downarrow-1.png', + '/administrator/templates/isis/images/admin/downarrow.png', + '/administrator/templates/isis/images/admin/downarrow0.png', + '/administrator/templates/isis/images/admin/expandall.png', + '/administrator/templates/isis/images/admin/featured.png', + '/administrator/templates/isis/images/admin/filesave.png', + '/administrator/templates/isis/images/admin/filter_16.png', + '/administrator/templates/isis/images/admin/icon-16-add.png', + '/administrator/templates/isis/images/admin/icon-16-allow.png', + '/administrator/templates/isis/images/admin/icon-16-allowinactive.png', + '/administrator/templates/isis/images/admin/icon-16-deny.png', + '/administrator/templates/isis/images/admin/icon-16-denyinactive.png', + '/administrator/templates/isis/images/admin/icon-16-links.png', + '/administrator/templates/isis/images/admin/icon-16-notice-note.png', + '/administrator/templates/isis/images/admin/icon-16-protected.png', + '/administrator/templates/isis/images/admin/menu_divider.png', + '/administrator/templates/isis/images/admin/note_add_16.png', + '/administrator/templates/isis/images/admin/publish_g.png', + '/administrator/templates/isis/images/admin/publish_r.png', + '/administrator/templates/isis/images/admin/publish_x.png', + '/administrator/templates/isis/images/admin/publish_y.png', + '/administrator/templates/isis/images/admin/sort_asc.png', + '/administrator/templates/isis/images/admin/sort_desc.png', + '/administrator/templates/isis/images/admin/tick.png', + '/administrator/templates/isis/images/admin/trash.png', + '/administrator/templates/isis/images/admin/uparrow-1.png', + '/administrator/templates/isis/images/admin/uparrow.png', + '/administrator/templates/isis/images/admin/uparrow0.png', + '/administrator/templates/isis/images/emailButton.png', + '/administrator/templates/isis/images/joomla.png', + '/administrator/templates/isis/images/login-joomla-inverse.png', + '/administrator/templates/isis/images/login-joomla.png', + '/administrator/templates/isis/images/logo-inverse.png', + '/administrator/templates/isis/images/logo.png', + '/administrator/templates/isis/images/pdf_button.png', + '/administrator/templates/isis/images/printButton.png', + '/administrator/templates/isis/images/system/sort_asc.png', + '/administrator/templates/isis/images/system/sort_desc.png', + '/administrator/templates/isis/img/glyphicons-halflings-white.png', + '/administrator/templates/isis/img/glyphicons-halflings.png', + '/administrator/templates/isis/index.php', + '/administrator/templates/isis/js/application.js', + '/administrator/templates/isis/js/classes.js', + '/administrator/templates/isis/js/template.js', + '/administrator/templates/isis/language/en-GB/en-GB.tpl_isis.ini', + '/administrator/templates/isis/language/en-GB/en-GB.tpl_isis.sys.ini', + '/administrator/templates/isis/less/blocks/_chzn-override.less', + '/administrator/templates/isis/less/blocks/_custom.less', + '/administrator/templates/isis/less/blocks/_editors.less', + '/administrator/templates/isis/less/blocks/_forms.less', + '/administrator/templates/isis/less/blocks/_global.less', + '/administrator/templates/isis/less/blocks/_header.less', + '/administrator/templates/isis/less/blocks/_login.less', + '/administrator/templates/isis/less/blocks/_media.less', + '/administrator/templates/isis/less/blocks/_modals.less', + '/administrator/templates/isis/less/blocks/_navbar.less', + '/administrator/templates/isis/less/blocks/_quickicons.less', + '/administrator/templates/isis/less/blocks/_sidebar.less', + '/administrator/templates/isis/less/blocks/_status.less', + '/administrator/templates/isis/less/blocks/_tables.less', + '/administrator/templates/isis/less/blocks/_toolbar.less', + '/administrator/templates/isis/less/blocks/_treeselect.less', + '/administrator/templates/isis/less/blocks/_utility-classes.less', + '/administrator/templates/isis/less/bootstrap/button-groups.less', + '/administrator/templates/isis/less/bootstrap/buttons.less', + '/administrator/templates/isis/less/bootstrap/mixins.less', + '/administrator/templates/isis/less/bootstrap/responsive-1200px-min.less', + '/administrator/templates/isis/less/bootstrap/responsive-768px-979px.less', + '/administrator/templates/isis/less/bootstrap/wells.less', + '/administrator/templates/isis/less/icomoon.less', + '/administrator/templates/isis/less/pages/_com_cpanel.less', + '/administrator/templates/isis/less/pages/_com_postinstall.less', + '/administrator/templates/isis/less/pages/_com_privacy.less', + '/administrator/templates/isis/less/pages/_com_templates.less', + '/administrator/templates/isis/less/template-rtl.less', + '/administrator/templates/isis/less/template.less', + '/administrator/templates/isis/less/variables.less', + '/administrator/templates/isis/login.php', + '/administrator/templates/isis/templateDetails.xml', + '/administrator/templates/isis/template_preview.png', + '/administrator/templates/isis/template_thumbnail.png', + '/administrator/templates/system/html/modules.php', + '/bin/index.html', + '/bin/keychain.php', + '/cli/deletefiles.php', + '/cli/finder_indexer.php', + '/cli/garbagecron.php', + '/cli/sessionGc.php', + '/cli/sessionMetadataGc.php', + '/cli/update_cron.php', + '/components/com_banners/banners.php', + '/components/com_banners/controller.php', + '/components/com_banners/helpers/banner.php', + '/components/com_banners/helpers/category.php', + '/components/com_banners/models/banner.php', + '/components/com_banners/models/banners.php', + '/components/com_banners/router.php', + '/components/com_config/config.php', + '/components/com_config/controller/cancel.php', + '/components/com_config/controller/canceladmin.php', + '/components/com_config/controller/cmsbase.php', + '/components/com_config/controller/config/display.php', + '/components/com_config/controller/config/save.php', + '/components/com_config/controller/display.php', + '/components/com_config/controller/helper.php', + '/components/com_config/controller/modules/cancel.php', + '/components/com_config/controller/modules/display.php', + '/components/com_config/controller/modules/save.php', + '/components/com_config/controller/templates/display.php', + '/components/com_config/controller/templates/save.php', + '/components/com_config/model/cms.php', + '/components/com_config/model/config.php', + '/components/com_config/model/form.php', + '/components/com_config/model/form/config.xml', + '/components/com_config/model/form/modules.xml', + '/components/com_config/model/form/modules_advanced.xml', + '/components/com_config/model/form/templates.xml', + '/components/com_config/model/modules.php', + '/components/com_config/model/templates.php', + '/components/com_config/view/cms/html.php', + '/components/com_config/view/cms/json.php', + '/components/com_config/view/config/html.php', + '/components/com_config/view/config/tmpl/default.php', + '/components/com_config/view/config/tmpl/default.xml', + '/components/com_config/view/config/tmpl/default_metadata.php', + '/components/com_config/view/config/tmpl/default_seo.php', + '/components/com_config/view/config/tmpl/default_site.php', + '/components/com_config/view/modules/html.php', + '/components/com_config/view/modules/tmpl/default.php', + '/components/com_config/view/modules/tmpl/default_options.php', + '/components/com_config/view/modules/tmpl/default_positions.php', + '/components/com_config/view/templates/html.php', + '/components/com_config/view/templates/tmpl/default.php', + '/components/com_config/view/templates/tmpl/default.xml', + '/components/com_config/view/templates/tmpl/default_options.php', + '/components/com_contact/contact.php', + '/components/com_contact/controller.php', + '/components/com_contact/controllers/contact.php', + '/components/com_contact/helpers/association.php', + '/components/com_contact/helpers/category.php', + '/components/com_contact/helpers/legacyrouter.php', + '/components/com_contact/layouts/joomla/form/renderfield.php', + '/components/com_contact/models/categories.php', + '/components/com_contact/models/category.php', + '/components/com_contact/models/contact.php', + '/components/com_contact/models/featured.php', + '/components/com_contact/models/forms/contact.xml', + '/components/com_contact/models/forms/filter_contacts.xml', + '/components/com_contact/models/forms/form.xml', + '/components/com_contact/models/rules/contactemail.php', + '/components/com_contact/models/rules/contactemailmessage.php', + '/components/com_contact/models/rules/contactemailsubject.php', + '/components/com_contact/router.php', + '/components/com_contact/views/categories/tmpl/default.php', + '/components/com_contact/views/categories/tmpl/default.xml', + '/components/com_contact/views/categories/tmpl/default_items.php', + '/components/com_contact/views/categories/view.html.php', + '/components/com_contact/views/category/tmpl/default.php', + '/components/com_contact/views/category/tmpl/default.xml', + '/components/com_contact/views/category/tmpl/default_children.php', + '/components/com_contact/views/category/tmpl/default_items.php', + '/components/com_contact/views/category/view.feed.php', + '/components/com_contact/views/category/view.html.php', + '/components/com_contact/views/contact/tmpl/default.php', + '/components/com_contact/views/contact/tmpl/default.xml', + '/components/com_contact/views/contact/tmpl/default_address.php', + '/components/com_contact/views/contact/tmpl/default_articles.php', + '/components/com_contact/views/contact/tmpl/default_form.php', + '/components/com_contact/views/contact/tmpl/default_links.php', + '/components/com_contact/views/contact/tmpl/default_profile.php', + '/components/com_contact/views/contact/tmpl/default_user_custom_fields.php', + '/components/com_contact/views/contact/view.html.php', + '/components/com_contact/views/contact/view.vcf.php', + '/components/com_contact/views/featured/tmpl/default.php', + '/components/com_contact/views/featured/tmpl/default.xml', + '/components/com_contact/views/featured/tmpl/default_items.php', + '/components/com_contact/views/featured/view.html.php', + '/components/com_content/content.php', + '/components/com_content/controller.php', + '/components/com_content/controllers/article.php', + '/components/com_content/helpers/association.php', + '/components/com_content/helpers/category.php', + '/components/com_content/helpers/legacyrouter.php', + '/components/com_content/helpers/query.php', + '/components/com_content/helpers/route.php', + '/components/com_content/models/archive.php', + '/components/com_content/models/article.php', + '/components/com_content/models/articles.php', + '/components/com_content/models/categories.php', + '/components/com_content/models/category.php', + '/components/com_content/models/featured.php', + '/components/com_content/models/form.php', + '/components/com_content/models/forms/article.xml', + '/components/com_content/models/forms/filter_articles.xml', + '/components/com_content/router.php', + '/components/com_content/views/archive/tmpl/default.php', + '/components/com_content/views/archive/tmpl/default.xml', + '/components/com_content/views/archive/tmpl/default_items.php', + '/components/com_content/views/archive/view.html.php', + '/components/com_content/views/article/tmpl/default.php', + '/components/com_content/views/article/tmpl/default.xml', + '/components/com_content/views/article/tmpl/default_links.php', + '/components/com_content/views/article/view.html.php', + '/components/com_content/views/categories/tmpl/default.php', + '/components/com_content/views/categories/tmpl/default.xml', + '/components/com_content/views/categories/tmpl/default_items.php', + '/components/com_content/views/categories/view.html.php', + '/components/com_content/views/category/tmpl/blog.php', + '/components/com_content/views/category/tmpl/blog.xml', + '/components/com_content/views/category/tmpl/blog_children.php', + '/components/com_content/views/category/tmpl/blog_item.php', + '/components/com_content/views/category/tmpl/blog_links.php', + '/components/com_content/views/category/tmpl/default.php', + '/components/com_content/views/category/tmpl/default.xml', + '/components/com_content/views/category/tmpl/default_articles.php', + '/components/com_content/views/category/tmpl/default_children.php', + '/components/com_content/views/category/view.feed.php', + '/components/com_content/views/category/view.html.php', + '/components/com_content/views/featured/tmpl/default.php', + '/components/com_content/views/featured/tmpl/default.xml', + '/components/com_content/views/featured/tmpl/default_item.php', + '/components/com_content/views/featured/tmpl/default_links.php', + '/components/com_content/views/featured/view.feed.php', + '/components/com_content/views/featured/view.html.php', + '/components/com_content/views/form/tmpl/edit.php', + '/components/com_content/views/form/tmpl/edit.xml', + '/components/com_content/views/form/view.html.php', + '/components/com_contenthistory/contenthistory.php', + '/components/com_fields/controller.php', + '/components/com_fields/fields.php', + '/components/com_fields/models/forms/filter_fields.xml', + '/components/com_finder/controller.php', + '/components/com_finder/controllers/suggestions.json.php', + '/components/com_finder/finder.php', + '/components/com_finder/helpers/html/filter.php', + '/components/com_finder/helpers/html/query.php', + '/components/com_finder/models/search.php', + '/components/com_finder/models/suggestions.php', + '/components/com_finder/router.php', + '/components/com_finder/views/search/tmpl/default.php', + '/components/com_finder/views/search/tmpl/default.xml', + '/components/com_finder/views/search/tmpl/default_form.php', + '/components/com_finder/views/search/tmpl/default_result.php', + '/components/com_finder/views/search/tmpl/default_results.php', + '/components/com_finder/views/search/view.feed.php', + '/components/com_finder/views/search/view.html.php', + '/components/com_finder/views/search/view.opensearch.php', + '/components/com_mailto/controller.php', + '/components/com_mailto/helpers/mailto.php', + '/components/com_mailto/mailto.php', + '/components/com_mailto/mailto.xml', + '/components/com_mailto/models/forms/mailto.xml', + '/components/com_mailto/models/mailto.php', + '/components/com_mailto/views/mailto/tmpl/default.php', + '/components/com_mailto/views/mailto/view.html.php', + '/components/com_mailto/views/sent/tmpl/default.php', + '/components/com_mailto/views/sent/view.html.php', + '/components/com_media/media.php', + '/components/com_menus/controller.php', + '/components/com_menus/menus.php', + '/components/com_menus/models/forms/filter_items.xml', + '/components/com_modules/controller.php', + '/components/com_modules/models/forms/filter_modules.xml', + '/components/com_modules/modules.php', + '/components/com_newsfeeds/controller.php', + '/components/com_newsfeeds/helpers/association.php', + '/components/com_newsfeeds/helpers/category.php', + '/components/com_newsfeeds/helpers/legacyrouter.php', + '/components/com_newsfeeds/models/categories.php', + '/components/com_newsfeeds/models/category.php', + '/components/com_newsfeeds/models/newsfeed.php', + '/components/com_newsfeeds/newsfeeds.php', + '/components/com_newsfeeds/router.php', + '/components/com_newsfeeds/views/categories/tmpl/default.php', + '/components/com_newsfeeds/views/categories/tmpl/default.xml', + '/components/com_newsfeeds/views/categories/tmpl/default_items.php', + '/components/com_newsfeeds/views/categories/view.html.php', + '/components/com_newsfeeds/views/category/tmpl/default.php', + '/components/com_newsfeeds/views/category/tmpl/default.xml', + '/components/com_newsfeeds/views/category/tmpl/default_children.php', + '/components/com_newsfeeds/views/category/tmpl/default_items.php', + '/components/com_newsfeeds/views/category/view.html.php', + '/components/com_newsfeeds/views/newsfeed/tmpl/default.php', + '/components/com_newsfeeds/views/newsfeed/tmpl/default.xml', + '/components/com_newsfeeds/views/newsfeed/view.html.php', + '/components/com_privacy/controller.php', + '/components/com_privacy/controllers/request.php', + '/components/com_privacy/models/confirm.php', + '/components/com_privacy/models/forms/confirm.xml', + '/components/com_privacy/models/forms/remind.xml', + '/components/com_privacy/models/forms/request.xml', + '/components/com_privacy/models/remind.php', + '/components/com_privacy/models/request.php', + '/components/com_privacy/privacy.php', + '/components/com_privacy/router.php', + '/components/com_privacy/views/confirm/tmpl/default.php', + '/components/com_privacy/views/confirm/tmpl/default.xml', + '/components/com_privacy/views/confirm/view.html.php', + '/components/com_privacy/views/remind/tmpl/default.php', + '/components/com_privacy/views/remind/tmpl/default.xml', + '/components/com_privacy/views/remind/view.html.php', + '/components/com_privacy/views/request/tmpl/default.php', + '/components/com_privacy/views/request/tmpl/default.xml', + '/components/com_privacy/views/request/view.html.php', + '/components/com_tags/controller.php', + '/components/com_tags/controllers/tags.php', + '/components/com_tags/models/tag.php', + '/components/com_tags/models/tags.php', + '/components/com_tags/router.php', + '/components/com_tags/tags.php', + '/components/com_tags/views/tag/tmpl/default.php', + '/components/com_tags/views/tag/tmpl/default.xml', + '/components/com_tags/views/tag/tmpl/default_items.php', + '/components/com_tags/views/tag/tmpl/list.php', + '/components/com_tags/views/tag/tmpl/list.xml', + '/components/com_tags/views/tag/tmpl/list_items.php', + '/components/com_tags/views/tag/view.feed.php', + '/components/com_tags/views/tag/view.html.php', + '/components/com_tags/views/tags/tmpl/default.php', + '/components/com_tags/views/tags/tmpl/default.xml', + '/components/com_tags/views/tags/tmpl/default_items.php', + '/components/com_tags/views/tags/view.feed.php', + '/components/com_tags/views/tags/view.html.php', + '/components/com_users/controller.php', + '/components/com_users/controllers/profile.php', + '/components/com_users/controllers/registration.php', + '/components/com_users/controllers/remind.php', + '/components/com_users/controllers/reset.php', + '/components/com_users/controllers/user.php', + '/components/com_users/helpers/html/users.php', + '/components/com_users/helpers/legacyrouter.php', + '/components/com_users/helpers/route.php', + '/components/com_users/layouts/joomla/form/renderfield.php', + '/components/com_users/models/forms/frontend.xml', + '/components/com_users/models/forms/frontend_admin.xml', + '/components/com_users/models/forms/login.xml', + '/components/com_users/models/forms/profile.xml', + '/components/com_users/models/forms/registration.xml', + '/components/com_users/models/forms/remind.xml', + '/components/com_users/models/forms/reset_complete.xml', + '/components/com_users/models/forms/reset_confirm.xml', + '/components/com_users/models/forms/reset_request.xml', + '/components/com_users/models/forms/sitelang.xml', + '/components/com_users/models/login.php', + '/components/com_users/models/profile.php', + '/components/com_users/models/registration.php', + '/components/com_users/models/remind.php', + '/components/com_users/models/reset.php', + '/components/com_users/models/rules/loginuniquefield.php', + '/components/com_users/models/rules/logoutuniquefield.php', + '/components/com_users/router.php', + '/components/com_users/users.php', + '/components/com_users/views/login/tmpl/default.php', + '/components/com_users/views/login/tmpl/default.xml', + '/components/com_users/views/login/tmpl/default_login.php', + '/components/com_users/views/login/tmpl/default_logout.php', + '/components/com_users/views/login/tmpl/logout.xml', + '/components/com_users/views/login/view.html.php', + '/components/com_users/views/profile/tmpl/default.php', + '/components/com_users/views/profile/tmpl/default.xml', + '/components/com_users/views/profile/tmpl/default_core.php', + '/components/com_users/views/profile/tmpl/default_custom.php', + '/components/com_users/views/profile/tmpl/default_params.php', + '/components/com_users/views/profile/tmpl/edit.php', + '/components/com_users/views/profile/tmpl/edit.xml', + '/components/com_users/views/profile/view.html.php', + '/components/com_users/views/registration/tmpl/complete.php', + '/components/com_users/views/registration/tmpl/default.php', + '/components/com_users/views/registration/tmpl/default.xml', + '/components/com_users/views/registration/view.html.php', + '/components/com_users/views/remind/tmpl/default.php', + '/components/com_users/views/remind/tmpl/default.xml', + '/components/com_users/views/remind/view.html.php', + '/components/com_users/views/reset/tmpl/complete.php', + '/components/com_users/views/reset/tmpl/confirm.php', + '/components/com_users/views/reset/tmpl/default.php', + '/components/com_users/views/reset/tmpl/default.xml', + '/components/com_users/views/reset/view.html.php', + '/components/com_wrapper/controller.php', + '/components/com_wrapper/router.php', + '/components/com_wrapper/views/wrapper/tmpl/default.php', + '/components/com_wrapper/views/wrapper/tmpl/default.xml', + '/components/com_wrapper/views/wrapper/view.html.php', + '/components/com_wrapper/wrapper.php', + '/components/com_wrapper/wrapper.xml', + '/language/en-GB/en-GB.com_ajax.ini', + '/language/en-GB/en-GB.com_config.ini', + '/language/en-GB/en-GB.com_contact.ini', + '/language/en-GB/en-GB.com_content.ini', + '/language/en-GB/en-GB.com_finder.ini', + '/language/en-GB/en-GB.com_mailto.ini', + '/language/en-GB/en-GB.com_media.ini', + '/language/en-GB/en-GB.com_messages.ini', + '/language/en-GB/en-GB.com_newsfeeds.ini', + '/language/en-GB/en-GB.com_privacy.ini', + '/language/en-GB/en-GB.com_tags.ini', + '/language/en-GB/en-GB.com_users.ini', + '/language/en-GB/en-GB.com_weblinks.ini', + '/language/en-GB/en-GB.com_wrapper.ini', + '/language/en-GB/en-GB.files_joomla.sys.ini', + '/language/en-GB/en-GB.finder_cli.ini', + '/language/en-GB/en-GB.ini', + '/language/en-GB/en-GB.lib_fof.ini', + '/language/en-GB/en-GB.lib_fof.sys.ini', + '/language/en-GB/en-GB.lib_idna_convert.sys.ini', + '/language/en-GB/en-GB.lib_joomla.ini', + '/language/en-GB/en-GB.lib_joomla.sys.ini', + '/language/en-GB/en-GB.lib_phpass.sys.ini', + '/language/en-GB/en-GB.lib_phputf8.sys.ini', + '/language/en-GB/en-GB.lib_simplepie.sys.ini', + '/language/en-GB/en-GB.localise.php', + '/language/en-GB/en-GB.mod_articles_archive.ini', + '/language/en-GB/en-GB.mod_articles_archive.sys.ini', + '/language/en-GB/en-GB.mod_articles_categories.ini', + '/language/en-GB/en-GB.mod_articles_categories.sys.ini', + '/language/en-GB/en-GB.mod_articles_category.ini', + '/language/en-GB/en-GB.mod_articles_category.sys.ini', + '/language/en-GB/en-GB.mod_articles_latest.ini', + '/language/en-GB/en-GB.mod_articles_latest.sys.ini', + '/language/en-GB/en-GB.mod_articles_news.ini', + '/language/en-GB/en-GB.mod_articles_news.sys.ini', + '/language/en-GB/en-GB.mod_articles_popular.ini', + '/language/en-GB/en-GB.mod_articles_popular.sys.ini', + '/language/en-GB/en-GB.mod_banners.ini', + '/language/en-GB/en-GB.mod_banners.sys.ini', + '/language/en-GB/en-GB.mod_breadcrumbs.ini', + '/language/en-GB/en-GB.mod_breadcrumbs.sys.ini', + '/language/en-GB/en-GB.mod_custom.ini', + '/language/en-GB/en-GB.mod_custom.sys.ini', + '/language/en-GB/en-GB.mod_feed.ini', + '/language/en-GB/en-GB.mod_feed.sys.ini', + '/language/en-GB/en-GB.mod_finder.ini', + '/language/en-GB/en-GB.mod_finder.sys.ini', + '/language/en-GB/en-GB.mod_footer.ini', + '/language/en-GB/en-GB.mod_footer.sys.ini', + '/language/en-GB/en-GB.mod_languages.ini', + '/language/en-GB/en-GB.mod_languages.sys.ini', + '/language/en-GB/en-GB.mod_login.ini', + '/language/en-GB/en-GB.mod_login.sys.ini', + '/language/en-GB/en-GB.mod_menu.ini', + '/language/en-GB/en-GB.mod_menu.sys.ini', + '/language/en-GB/en-GB.mod_random_image.ini', + '/language/en-GB/en-GB.mod_random_image.sys.ini', + '/language/en-GB/en-GB.mod_related_items.ini', + '/language/en-GB/en-GB.mod_related_items.sys.ini', + '/language/en-GB/en-GB.mod_stats.ini', + '/language/en-GB/en-GB.mod_stats.sys.ini', + '/language/en-GB/en-GB.mod_syndicate.ini', + '/language/en-GB/en-GB.mod_syndicate.sys.ini', + '/language/en-GB/en-GB.mod_tags_popular.ini', + '/language/en-GB/en-GB.mod_tags_popular.sys.ini', + '/language/en-GB/en-GB.mod_tags_similar.ini', + '/language/en-GB/en-GB.mod_tags_similar.sys.ini', + '/language/en-GB/en-GB.mod_users_latest.ini', + '/language/en-GB/en-GB.mod_users_latest.sys.ini', + '/language/en-GB/en-GB.mod_weblinks.ini', + '/language/en-GB/en-GB.mod_weblinks.sys.ini', + '/language/en-GB/en-GB.mod_whosonline.ini', + '/language/en-GB/en-GB.mod_whosonline.sys.ini', + '/language/en-GB/en-GB.mod_wrapper.ini', + '/language/en-GB/en-GB.mod_wrapper.sys.ini', + '/language/en-GB/en-GB.tpl_beez3.ini', + '/language/en-GB/en-GB.tpl_beez3.sys.ini', + '/language/en-GB/en-GB.tpl_protostar.ini', + '/language/en-GB/en-GB.tpl_protostar.sys.ini', + '/language/en-GB/en-GB.xml', + '/layouts/joomla/content/blog_style_default_links.php', + '/layouts/joomla/content/icons/email.php', + '/layouts/joomla/content/icons/print_popup.php', + '/layouts/joomla/content/icons/print_screen.php', + '/layouts/joomla/content/info_block/block.php', + '/layouts/joomla/edit/details.php', + '/layouts/joomla/edit/item_title.php', + '/layouts/joomla/form/field/radio.php', + '/layouts/joomla/html/formbehavior/ajaxchosen.php', + '/layouts/joomla/html/formbehavior/chosen.php', + '/layouts/joomla/html/sortablelist.php', + '/layouts/joomla/html/tag.php', + '/layouts/joomla/modal/body.php', + '/layouts/joomla/modal/footer.php', + '/layouts/joomla/modal/header.php', + '/layouts/joomla/modal/iframe.php', + '/layouts/joomla/modal/main.php', + '/layouts/joomla/sidebars/toggle.php', + '/layouts/joomla/tinymce/buttons.php', + '/layouts/joomla/tinymce/buttons/button.php', + '/layouts/joomla/toolbar/confirm.php', + '/layouts/joomla/toolbar/help.php', + '/layouts/joomla/toolbar/modal.php', + '/layouts/joomla/toolbar/slider.php', + '/layouts/libraries/cms/html/bootstrap/addtab.php', + '/layouts/libraries/cms/html/bootstrap/addtabscript.php', + '/layouts/libraries/cms/html/bootstrap/endtab.php', + '/layouts/libraries/cms/html/bootstrap/endtabset.php', + '/layouts/libraries/cms/html/bootstrap/starttabset.php', + '/layouts/libraries/cms/html/bootstrap/starttabsetscript.php', + '/libraries/cms/class/loader.php', + '/libraries/cms/html/access.php', + '/libraries/cms/html/actionsdropdown.php', + '/libraries/cms/html/adminlanguage.php', + '/libraries/cms/html/batch.php', + '/libraries/cms/html/behavior.php', + '/libraries/cms/html/bootstrap.php', + '/libraries/cms/html/category.php', + '/libraries/cms/html/content.php', + '/libraries/cms/html/contentlanguage.php', + '/libraries/cms/html/date.php', + '/libraries/cms/html/debug.php', + '/libraries/cms/html/dropdown.php', + '/libraries/cms/html/email.php', + '/libraries/cms/html/form.php', + '/libraries/cms/html/formbehavior.php', + '/libraries/cms/html/grid.php', + '/libraries/cms/html/icons.php', + '/libraries/cms/html/jgrid.php', + '/libraries/cms/html/jquery.php', + '/libraries/cms/html/language/en-GB/en-GB.jhtmldate.ini', + '/libraries/cms/html/links.php', + '/libraries/cms/html/list.php', + '/libraries/cms/html/menu.php', + '/libraries/cms/html/number.php', + '/libraries/cms/html/rules.php', + '/libraries/cms/html/searchtools.php', + '/libraries/cms/html/select.php', + '/libraries/cms/html/sidebar.php', + '/libraries/cms/html/sliders.php', + '/libraries/cms/html/sortablelist.php', + '/libraries/cms/html/string.php', + '/libraries/cms/html/tabs.php', + '/libraries/cms/html/tag.php', + '/libraries/cms/html/tel.php', + '/libraries/cms/html/user.php', + '/libraries/cms/less/formatter/joomla.php', + '/libraries/cms/less/less.php', + '/libraries/fof/LICENSE.txt', + '/libraries/fof/autoloader/component.php', + '/libraries/fof/autoloader/fof.php', + '/libraries/fof/config/domain/dispatcher.php', + '/libraries/fof/config/domain/interface.php', + '/libraries/fof/config/domain/tables.php', + '/libraries/fof/config/domain/views.php', + '/libraries/fof/config/provider.php', + '/libraries/fof/controller/controller.php', + '/libraries/fof/database/database.php', + '/libraries/fof/database/driver.php', + '/libraries/fof/database/driver/joomla.php', + '/libraries/fof/database/driver/mysql.php', + '/libraries/fof/database/driver/mysqli.php', + '/libraries/fof/database/driver/oracle.php', + '/libraries/fof/database/driver/pdo.php', + '/libraries/fof/database/driver/pdomysql.php', + '/libraries/fof/database/driver/postgresql.php', + '/libraries/fof/database/driver/sqlazure.php', + '/libraries/fof/database/driver/sqlite.php', + '/libraries/fof/database/driver/sqlsrv.php', + '/libraries/fof/database/factory.php', + '/libraries/fof/database/installer.php', + '/libraries/fof/database/interface.php', + '/libraries/fof/database/iterator.php', + '/libraries/fof/database/iterator/azure.php', + '/libraries/fof/database/iterator/mysql.php', + '/libraries/fof/database/iterator/mysqli.php', + '/libraries/fof/database/iterator/oracle.php', + '/libraries/fof/database/iterator/pdo.php', + '/libraries/fof/database/iterator/pdomysql.php', + '/libraries/fof/database/iterator/postgresql.php', + '/libraries/fof/database/iterator/sqlite.php', + '/libraries/fof/database/iterator/sqlsrv.php', + '/libraries/fof/database/query.php', + '/libraries/fof/database/query/element.php', + '/libraries/fof/database/query/limitable.php', + '/libraries/fof/database/query/mysql.php', + '/libraries/fof/database/query/mysqli.php', + '/libraries/fof/database/query/oracle.php', + '/libraries/fof/database/query/pdo.php', + '/libraries/fof/database/query/pdomysql.php', + '/libraries/fof/database/query/postgresql.php', + '/libraries/fof/database/query/preparable.php', + '/libraries/fof/database/query/sqlazure.php', + '/libraries/fof/database/query/sqlite.php', + '/libraries/fof/database/query/sqlsrv.php', + '/libraries/fof/dispatcher/dispatcher.php', + '/libraries/fof/download/adapter/abstract.php', + '/libraries/fof/download/adapter/cacert.pem', + '/libraries/fof/download/adapter/curl.php', + '/libraries/fof/download/adapter/fopen.php', + '/libraries/fof/download/download.php', + '/libraries/fof/download/interface.php', + '/libraries/fof/encrypt/aes.php', + '/libraries/fof/encrypt/aes/abstract.php', + '/libraries/fof/encrypt/aes/interface.php', + '/libraries/fof/encrypt/aes/mcrypt.php', + '/libraries/fof/encrypt/aes/openssl.php', + '/libraries/fof/encrypt/base32.php', + '/libraries/fof/encrypt/randval.php', + '/libraries/fof/encrypt/randvalinterface.php', + '/libraries/fof/encrypt/totp.php', + '/libraries/fof/form/field.php', + '/libraries/fof/form/field/accesslevel.php', + '/libraries/fof/form/field/actions.php', + '/libraries/fof/form/field/button.php', + '/libraries/fof/form/field/cachehandler.php', + '/libraries/fof/form/field/calendar.php', + '/libraries/fof/form/field/captcha.php', + '/libraries/fof/form/field/checkbox.php', + '/libraries/fof/form/field/checkboxes.php', + '/libraries/fof/form/field/components.php', + '/libraries/fof/form/field/editor.php', + '/libraries/fof/form/field/email.php', + '/libraries/fof/form/field/groupedbutton.php', + '/libraries/fof/form/field/groupedlist.php', + '/libraries/fof/form/field/hidden.php', + '/libraries/fof/form/field/image.php', + '/libraries/fof/form/field/imagelist.php', + '/libraries/fof/form/field/integer.php', + '/libraries/fof/form/field/language.php', + '/libraries/fof/form/field/list.php', + '/libraries/fof/form/field/media.php', + '/libraries/fof/form/field/model.php', + '/libraries/fof/form/field/ordering.php', + '/libraries/fof/form/field/password.php', + '/libraries/fof/form/field/plugins.php', + '/libraries/fof/form/field/published.php', + '/libraries/fof/form/field/radio.php', + '/libraries/fof/form/field/relation.php', + '/libraries/fof/form/field/rules.php', + '/libraries/fof/form/field/selectrow.php', + '/libraries/fof/form/field/sessionhandler.php', + '/libraries/fof/form/field/spacer.php', + '/libraries/fof/form/field/sql.php', + '/libraries/fof/form/field/tag.php', + '/libraries/fof/form/field/tel.php', + '/libraries/fof/form/field/text.php', + '/libraries/fof/form/field/textarea.php', + '/libraries/fof/form/field/timezone.php', + '/libraries/fof/form/field/title.php', + '/libraries/fof/form/field/url.php', + '/libraries/fof/form/field/user.php', + '/libraries/fof/form/field/usergroup.php', + '/libraries/fof/form/form.php', + '/libraries/fof/form/header.php', + '/libraries/fof/form/header/accesslevel.php', + '/libraries/fof/form/header/field.php', + '/libraries/fof/form/header/fielddate.php', + '/libraries/fof/form/header/fieldfilterable.php', + '/libraries/fof/form/header/fieldsearchable.php', + '/libraries/fof/form/header/fieldselectable.php', + '/libraries/fof/form/header/fieldsql.php', + '/libraries/fof/form/header/filterdate.php', + '/libraries/fof/form/header/filterfilterable.php', + '/libraries/fof/form/header/filtersearchable.php', + '/libraries/fof/form/header/filterselectable.php', + '/libraries/fof/form/header/filtersql.php', + '/libraries/fof/form/header/language.php', + '/libraries/fof/form/header/model.php', + '/libraries/fof/form/header/ordering.php', + '/libraries/fof/form/header/published.php', + '/libraries/fof/form/header/rowselect.php', + '/libraries/fof/form/helper.php', + '/libraries/fof/hal/document.php', + '/libraries/fof/hal/link.php', + '/libraries/fof/hal/links.php', + '/libraries/fof/hal/render/interface.php', + '/libraries/fof/hal/render/json.php', + '/libraries/fof/include.php', + '/libraries/fof/inflector/inflector.php', + '/libraries/fof/input/input.php', + '/libraries/fof/input/jinput/cli.php', + '/libraries/fof/input/jinput/cookie.php', + '/libraries/fof/input/jinput/files.php', + '/libraries/fof/input/jinput/input.php', + '/libraries/fof/input/jinput/json.php', + '/libraries/fof/integration/joomla/filesystem/filesystem.php', + '/libraries/fof/integration/joomla/platform.php', + '/libraries/fof/layout/file.php', + '/libraries/fof/layout/helper.php', + '/libraries/fof/less/formatter/classic.php', + '/libraries/fof/less/formatter/compressed.php', + '/libraries/fof/less/formatter/joomla.php', + '/libraries/fof/less/formatter/lessjs.php', + '/libraries/fof/less/less.php', + '/libraries/fof/less/parser/parser.php', + '/libraries/fof/model/behavior.php', + '/libraries/fof/model/behavior/access.php', + '/libraries/fof/model/behavior/emptynonzero.php', + '/libraries/fof/model/behavior/enabled.php', + '/libraries/fof/model/behavior/filters.php', + '/libraries/fof/model/behavior/language.php', + '/libraries/fof/model/behavior/private.php', + '/libraries/fof/model/dispatcher/behavior.php', + '/libraries/fof/model/field.php', + '/libraries/fof/model/field/boolean.php', + '/libraries/fof/model/field/date.php', + '/libraries/fof/model/field/number.php', + '/libraries/fof/model/field/text.php', + '/libraries/fof/model/model.php', + '/libraries/fof/platform/filesystem/filesystem.php', + '/libraries/fof/platform/filesystem/interface.php', + '/libraries/fof/platform/interface.php', + '/libraries/fof/platform/platform.php', + '/libraries/fof/query/abstract.php', + '/libraries/fof/render/abstract.php', + '/libraries/fof/render/joomla.php', + '/libraries/fof/render/joomla3.php', + '/libraries/fof/render/strapper.php', + '/libraries/fof/string/utils.php', + '/libraries/fof/table/behavior.php', + '/libraries/fof/table/behavior/assets.php', + '/libraries/fof/table/behavior/contenthistory.php', + '/libraries/fof/table/behavior/tags.php', + '/libraries/fof/table/dispatcher/behavior.php', + '/libraries/fof/table/nested.php', + '/libraries/fof/table/relations.php', + '/libraries/fof/table/table.php', + '/libraries/fof/template/utils.php', + '/libraries/fof/toolbar/toolbar.php', + '/libraries/fof/utils/array/array.php', + '/libraries/fof/utils/cache/cleaner.php', + '/libraries/fof/utils/config/helper.php', + '/libraries/fof/utils/filescheck/filescheck.php', + '/libraries/fof/utils/ini/parser.php', + '/libraries/fof/utils/installscript/installscript.php', + '/libraries/fof/utils/ip/ip.php', + '/libraries/fof/utils/object/object.php', + '/libraries/fof/utils/observable/dispatcher.php', + '/libraries/fof/utils/observable/event.php', + '/libraries/fof/utils/phpfunc/phpfunc.php', + '/libraries/fof/utils/timer/timer.php', + '/libraries/fof/utils/update/collection.php', + '/libraries/fof/utils/update/extension.php', + '/libraries/fof/utils/update/joomla.php', + '/libraries/fof/utils/update/update.php', + '/libraries/fof/version.txt', + '/libraries/fof/view/csv.php', + '/libraries/fof/view/form.php', + '/libraries/fof/view/html.php', + '/libraries/fof/view/json.php', + '/libraries/fof/view/raw.php', + '/libraries/fof/view/view.php', + '/libraries/idna_convert/LICENCE', + '/libraries/idna_convert/ReadMe.txt', + '/libraries/idna_convert/idna_convert.class.php', + '/libraries/idna_convert/transcode_wrapper.php', + '/libraries/idna_convert/uctc.php', + '/libraries/joomla/application/web/router.php', + '/libraries/joomla/application/web/router/base.php', + '/libraries/joomla/application/web/router/rest.php', + '/libraries/joomla/archive/archive.php', + '/libraries/joomla/archive/bzip2.php', + '/libraries/joomla/archive/extractable.php', + '/libraries/joomla/archive/gzip.php', + '/libraries/joomla/archive/tar.php', + '/libraries/joomla/archive/wrapper/archive.php', + '/libraries/joomla/archive/zip.php', + '/libraries/joomla/controller/base.php', + '/libraries/joomla/controller/controller.php', + '/libraries/joomla/database/database.php', + '/libraries/joomla/database/driver.php', + '/libraries/joomla/database/driver/mysql.php', + '/libraries/joomla/database/driver/mysqli.php', + '/libraries/joomla/database/driver/oracle.php', + '/libraries/joomla/database/driver/pdo.php', + '/libraries/joomla/database/driver/pdomysql.php', + '/libraries/joomla/database/driver/pgsql.php', + '/libraries/joomla/database/driver/postgresql.php', + '/libraries/joomla/database/driver/sqlazure.php', + '/libraries/joomla/database/driver/sqlite.php', + '/libraries/joomla/database/driver/sqlsrv.php', + '/libraries/joomla/database/exception/connecting.php', + '/libraries/joomla/database/exception/executing.php', + '/libraries/joomla/database/exception/unsupported.php', + '/libraries/joomla/database/exporter.php', + '/libraries/joomla/database/exporter/mysql.php', + '/libraries/joomla/database/exporter/mysqli.php', + '/libraries/joomla/database/exporter/pdomysql.php', + '/libraries/joomla/database/exporter/pgsql.php', + '/libraries/joomla/database/exporter/postgresql.php', + '/libraries/joomla/database/factory.php', + '/libraries/joomla/database/importer.php', + '/libraries/joomla/database/importer/mysql.php', + '/libraries/joomla/database/importer/mysqli.php', + '/libraries/joomla/database/importer/pdomysql.php', + '/libraries/joomla/database/importer/pgsql.php', + '/libraries/joomla/database/importer/postgresql.php', + '/libraries/joomla/database/interface.php', + '/libraries/joomla/database/iterator.php', + '/libraries/joomla/database/iterator/mysql.php', + '/libraries/joomla/database/iterator/mysqli.php', + '/libraries/joomla/database/iterator/oracle.php', + '/libraries/joomla/database/iterator/pdo.php', + '/libraries/joomla/database/iterator/pdomysql.php', + '/libraries/joomla/database/iterator/pgsql.php', + '/libraries/joomla/database/iterator/postgresql.php', + '/libraries/joomla/database/iterator/sqlazure.php', + '/libraries/joomla/database/iterator/sqlite.php', + '/libraries/joomla/database/iterator/sqlsrv.php', + '/libraries/joomla/database/query.php', + '/libraries/joomla/database/query/element.php', + '/libraries/joomla/database/query/limitable.php', + '/libraries/joomla/database/query/mysql.php', + '/libraries/joomla/database/query/mysqli.php', + '/libraries/joomla/database/query/oracle.php', + '/libraries/joomla/database/query/pdo.php', + '/libraries/joomla/database/query/pdomysql.php', + '/libraries/joomla/database/query/pgsql.php', + '/libraries/joomla/database/query/postgresql.php', + '/libraries/joomla/database/query/preparable.php', + '/libraries/joomla/database/query/sqlazure.php', + '/libraries/joomla/database/query/sqlite.php', + '/libraries/joomla/database/query/sqlsrv.php', + '/libraries/joomla/event/dispatcher.php', + '/libraries/joomla/event/event.php', + '/libraries/joomla/facebook/album.php', + '/libraries/joomla/facebook/checkin.php', + '/libraries/joomla/facebook/comment.php', + '/libraries/joomla/facebook/event.php', + '/libraries/joomla/facebook/facebook.php', + '/libraries/joomla/facebook/group.php', + '/libraries/joomla/facebook/link.php', + '/libraries/joomla/facebook/note.php', + '/libraries/joomla/facebook/oauth.php', + '/libraries/joomla/facebook/object.php', + '/libraries/joomla/facebook/photo.php', + '/libraries/joomla/facebook/post.php', + '/libraries/joomla/facebook/status.php', + '/libraries/joomla/facebook/user.php', + '/libraries/joomla/facebook/video.php', + '/libraries/joomla/form/fields/accesslevel.php', + '/libraries/joomla/form/fields/aliastag.php', + '/libraries/joomla/form/fields/cachehandler.php', + '/libraries/joomla/form/fields/calendar.php', + '/libraries/joomla/form/fields/checkbox.php', + '/libraries/joomla/form/fields/checkboxes.php', + '/libraries/joomla/form/fields/color.php', + '/libraries/joomla/form/fields/combo.php', + '/libraries/joomla/form/fields/components.php', + '/libraries/joomla/form/fields/databaseconnection.php', + '/libraries/joomla/form/fields/email.php', + '/libraries/joomla/form/fields/file.php', + '/libraries/joomla/form/fields/filelist.php', + '/libraries/joomla/form/fields/folderlist.php', + '/libraries/joomla/form/fields/groupedlist.php', + '/libraries/joomla/form/fields/hidden.php', + '/libraries/joomla/form/fields/imagelist.php', + '/libraries/joomla/form/fields/integer.php', + '/libraries/joomla/form/fields/language.php', + '/libraries/joomla/form/fields/list.php', + '/libraries/joomla/form/fields/meter.php', + '/libraries/joomla/form/fields/note.php', + '/libraries/joomla/form/fields/number.php', + '/libraries/joomla/form/fields/password.php', + '/libraries/joomla/form/fields/plugins.php', + '/libraries/joomla/form/fields/predefinedlist.php', + '/libraries/joomla/form/fields/radio.php', + '/libraries/joomla/form/fields/range.php', + '/libraries/joomla/form/fields/repeatable.php', + '/libraries/joomla/form/fields/rules.php', + '/libraries/joomla/form/fields/sessionhandler.php', + '/libraries/joomla/form/fields/spacer.php', + '/libraries/joomla/form/fields/sql.php', + '/libraries/joomla/form/fields/subform.php', + '/libraries/joomla/form/fields/tel.php', + '/libraries/joomla/form/fields/text.php', + '/libraries/joomla/form/fields/textarea.php', + '/libraries/joomla/form/fields/timezone.php', + '/libraries/joomla/form/fields/url.php', + '/libraries/joomla/form/fields/usergroup.php', + '/libraries/joomla/github/account.php', + '/libraries/joomla/github/commits.php', + '/libraries/joomla/github/forks.php', + '/libraries/joomla/github/github.php', + '/libraries/joomla/github/hooks.php', + '/libraries/joomla/github/http.php', + '/libraries/joomla/github/meta.php', + '/libraries/joomla/github/milestones.php', + '/libraries/joomla/github/object.php', + '/libraries/joomla/github/package.php', + '/libraries/joomla/github/package/activity.php', + '/libraries/joomla/github/package/activity/events.php', + '/libraries/joomla/github/package/activity/notifications.php', + '/libraries/joomla/github/package/activity/starring.php', + '/libraries/joomla/github/package/activity/watching.php', + '/libraries/joomla/github/package/authorization.php', + '/libraries/joomla/github/package/data.php', + '/libraries/joomla/github/package/data/blobs.php', + '/libraries/joomla/github/package/data/commits.php', + '/libraries/joomla/github/package/data/refs.php', + '/libraries/joomla/github/package/data/tags.php', + '/libraries/joomla/github/package/data/trees.php', + '/libraries/joomla/github/package/gists.php', + '/libraries/joomla/github/package/gists/comments.php', + '/libraries/joomla/github/package/gitignore.php', + '/libraries/joomla/github/package/issues.php', + '/libraries/joomla/github/package/issues/assignees.php', + '/libraries/joomla/github/package/issues/comments.php', + '/libraries/joomla/github/package/issues/events.php', + '/libraries/joomla/github/package/issues/labels.php', + '/libraries/joomla/github/package/issues/milestones.php', + '/libraries/joomla/github/package/markdown.php', + '/libraries/joomla/github/package/orgs.php', + '/libraries/joomla/github/package/orgs/members.php', + '/libraries/joomla/github/package/orgs/teams.php', + '/libraries/joomla/github/package/pulls.php', + '/libraries/joomla/github/package/pulls/comments.php', + '/libraries/joomla/github/package/repositories.php', + '/libraries/joomla/github/package/repositories/collaborators.php', + '/libraries/joomla/github/package/repositories/comments.php', + '/libraries/joomla/github/package/repositories/commits.php', + '/libraries/joomla/github/package/repositories/contents.php', + '/libraries/joomla/github/package/repositories/downloads.php', + '/libraries/joomla/github/package/repositories/forks.php', + '/libraries/joomla/github/package/repositories/hooks.php', + '/libraries/joomla/github/package/repositories/keys.php', + '/libraries/joomla/github/package/repositories/merging.php', + '/libraries/joomla/github/package/repositories/statistics.php', + '/libraries/joomla/github/package/repositories/statuses.php', + '/libraries/joomla/github/package/search.php', + '/libraries/joomla/github/package/users.php', + '/libraries/joomla/github/package/users/emails.php', + '/libraries/joomla/github/package/users/followers.php', + '/libraries/joomla/github/package/users/keys.php', + '/libraries/joomla/github/refs.php', + '/libraries/joomla/github/statuses.php', + '/libraries/joomla/google/auth.php', + '/libraries/joomla/google/auth/oauth2.php', + '/libraries/joomla/google/data.php', + '/libraries/joomla/google/data/adsense.php', + '/libraries/joomla/google/data/calendar.php', + '/libraries/joomla/google/data/picasa.php', + '/libraries/joomla/google/data/picasa/album.php', + '/libraries/joomla/google/data/picasa/photo.php', + '/libraries/joomla/google/data/plus.php', + '/libraries/joomla/google/data/plus/activities.php', + '/libraries/joomla/google/data/plus/comments.php', + '/libraries/joomla/google/data/plus/people.php', + '/libraries/joomla/google/embed.php', + '/libraries/joomla/google/embed/analytics.php', + '/libraries/joomla/google/embed/maps.php', + '/libraries/joomla/google/google.php', + '/libraries/joomla/grid/grid.php', + '/libraries/joomla/keychain/keychain.php', + '/libraries/joomla/linkedin/communications.php', + '/libraries/joomla/linkedin/companies.php', + '/libraries/joomla/linkedin/groups.php', + '/libraries/joomla/linkedin/jobs.php', + '/libraries/joomla/linkedin/linkedin.php', + '/libraries/joomla/linkedin/oauth.php', + '/libraries/joomla/linkedin/object.php', + '/libraries/joomla/linkedin/people.php', + '/libraries/joomla/linkedin/stream.php', + '/libraries/joomla/mediawiki/categories.php', + '/libraries/joomla/mediawiki/http.php', + '/libraries/joomla/mediawiki/images.php', + '/libraries/joomla/mediawiki/links.php', + '/libraries/joomla/mediawiki/mediawiki.php', + '/libraries/joomla/mediawiki/object.php', + '/libraries/joomla/mediawiki/pages.php', + '/libraries/joomla/mediawiki/search.php', + '/libraries/joomla/mediawiki/sites.php', + '/libraries/joomla/mediawiki/users.php', + '/libraries/joomla/model/base.php', + '/libraries/joomla/model/database.php', + '/libraries/joomla/model/model.php', + '/libraries/joomla/oauth1/client.php', + '/libraries/joomla/oauth2/client.php', + '/libraries/joomla/observable/interface.php', + '/libraries/joomla/observer/interface.php', + '/libraries/joomla/observer/mapper.php', + '/libraries/joomla/observer/updater.php', + '/libraries/joomla/observer/updater/interface.php', + '/libraries/joomla/observer/wrapper/mapper.php', + '/libraries/joomla/openstreetmap/changesets.php', + '/libraries/joomla/openstreetmap/elements.php', + '/libraries/joomla/openstreetmap/gps.php', + '/libraries/joomla/openstreetmap/info.php', + '/libraries/joomla/openstreetmap/oauth.php', + '/libraries/joomla/openstreetmap/object.php', + '/libraries/joomla/openstreetmap/openstreetmap.php', + '/libraries/joomla/openstreetmap/user.php', + '/libraries/joomla/platform.php', + '/libraries/joomla/route/wrapper/route.php', + '/libraries/joomla/session/handler/interface.php', + '/libraries/joomla/session/handler/joomla.php', + '/libraries/joomla/session/handler/native.php', + '/libraries/joomla/session/storage.php', + '/libraries/joomla/session/storage/apc.php', + '/libraries/joomla/session/storage/apcu.php', + '/libraries/joomla/session/storage/database.php', + '/libraries/joomla/session/storage/memcache.php', + '/libraries/joomla/session/storage/memcached.php', + '/libraries/joomla/session/storage/none.php', + '/libraries/joomla/session/storage/redis.php', + '/libraries/joomla/session/storage/wincache.php', + '/libraries/joomla/session/storage/xcache.php', + '/libraries/joomla/string/string.php', + '/libraries/joomla/string/wrapper/normalise.php', + '/libraries/joomla/string/wrapper/punycode.php', + '/libraries/joomla/twitter/block.php', + '/libraries/joomla/twitter/directmessages.php', + '/libraries/joomla/twitter/favorites.php', + '/libraries/joomla/twitter/friends.php', + '/libraries/joomla/twitter/help.php', + '/libraries/joomla/twitter/lists.php', + '/libraries/joomla/twitter/oauth.php', + '/libraries/joomla/twitter/object.php', + '/libraries/joomla/twitter/places.php', + '/libraries/joomla/twitter/profile.php', + '/libraries/joomla/twitter/search.php', + '/libraries/joomla/twitter/statuses.php', + '/libraries/joomla/twitter/trends.php', + '/libraries/joomla/twitter/twitter.php', + '/libraries/joomla/twitter/users.php', + '/libraries/joomla/utilities/arrayhelper.php', + '/libraries/joomla/view/base.php', + '/libraries/joomla/view/html.php', + '/libraries/joomla/view/view.php', + '/libraries/legacy/application/application.php', + '/libraries/legacy/base/node.php', + '/libraries/legacy/base/observable.php', + '/libraries/legacy/base/observer.php', + '/libraries/legacy/base/tree.php', + '/libraries/legacy/database/exception.php', + '/libraries/legacy/database/mysql.php', + '/libraries/legacy/database/mysqli.php', + '/libraries/legacy/database/sqlazure.php', + '/libraries/legacy/database/sqlsrv.php', + '/libraries/legacy/dispatcher/dispatcher.php', + '/libraries/legacy/error/error.php', + '/libraries/legacy/exception/exception.php', + '/libraries/legacy/form/field/category.php', + '/libraries/legacy/form/field/componentlayout.php', + '/libraries/legacy/form/field/modulelayout.php', + '/libraries/legacy/log/logexception.php', + '/libraries/legacy/request/request.php', + '/libraries/legacy/response/response.php', + '/libraries/legacy/simplecrypt/simplecrypt.php', + '/libraries/legacy/simplepie/factory.php', + '/libraries/legacy/table/session.php', + '/libraries/legacy/utilities/xmlelement.php', + '/libraries/phputf8/LICENSE', + '/libraries/phputf8/README', + '/libraries/phputf8/mbstring/core.php', + '/libraries/phputf8/native/core.php', + '/libraries/phputf8/ord.php', + '/libraries/phputf8/str_ireplace.php', + '/libraries/phputf8/str_pad.php', + '/libraries/phputf8/str_split.php', + '/libraries/phputf8/strcasecmp.php', + '/libraries/phputf8/strcspn.php', + '/libraries/phputf8/stristr.php', + '/libraries/phputf8/strrev.php', + '/libraries/phputf8/strspn.php', + '/libraries/phputf8/substr_replace.php', + '/libraries/phputf8/trim.php', + '/libraries/phputf8/ucfirst.php', + '/libraries/phputf8/ucwords.php', + '/libraries/phputf8/utf8.php', + '/libraries/phputf8/utils/ascii.php', + '/libraries/phputf8/utils/bad.php', + '/libraries/phputf8/utils/patterns.php', + '/libraries/phputf8/utils/position.php', + '/libraries/phputf8/utils/specials.php', + '/libraries/phputf8/utils/unicode.php', + '/libraries/phputf8/utils/validation.php', + '/libraries/src/Access/Wrapper/Access.php', + '/libraries/src/Cache/Storage/ApcStorage.php', + '/libraries/src/Cache/Storage/CacheliteStorage.php', + '/libraries/src/Cache/Storage/MemcacheStorage.php', + '/libraries/src/Cache/Storage/XcacheStorage.php', + '/libraries/src/Client/ClientWrapper.php', + '/libraries/src/Crypt/Cipher/BlowfishCipher.php', + '/libraries/src/Crypt/Cipher/McryptCipher.php', + '/libraries/src/Crypt/Cipher/Rijndael256Cipher.php', + '/libraries/src/Crypt/Cipher/SimpleCipher.php', + '/libraries/src/Crypt/Cipher/TripleDesCipher.php', + '/libraries/src/Crypt/CipherInterface.php', + '/libraries/src/Crypt/CryptPassword.php', + '/libraries/src/Crypt/Key.php', + '/libraries/src/Crypt/Password/SimpleCryptPassword.php', + '/libraries/src/Crypt/README.md', + '/libraries/src/Filesystem/Wrapper/FileWrapper.php', + '/libraries/src/Filesystem/Wrapper/FolderWrapper.php', + '/libraries/src/Filesystem/Wrapper/PathWrapper.php', + '/libraries/src/Filter/Wrapper/OutputFilterWrapper.php', + '/libraries/src/Form/Field/HelpsiteField.php', + '/libraries/src/Form/FormWrapper.php', + '/libraries/src/Helper/ContentHistoryHelper.php', + '/libraries/src/Helper/SearchHelper.php', + '/libraries/src/Http/Transport/cacert.pem', + '/libraries/src/Http/Wrapper/FactoryWrapper.php', + '/libraries/src/Language/LanguageStemmer.php', + '/libraries/src/Language/Stemmer/Porteren.php', + '/libraries/src/Language/Wrapper/JTextWrapper.php', + '/libraries/src/Language/Wrapper/LanguageHelperWrapper.php', + '/libraries/src/Language/Wrapper/TransliterateWrapper.php', + '/libraries/src/Mail/MailWrapper.php', + '/libraries/src/Menu/MenuHelper.php', + '/libraries/src/Menu/Node.php', + '/libraries/src/Menu/Node/Component.php', + '/libraries/src/Menu/Node/Container.php', + '/libraries/src/Menu/Node/Heading.php', + '/libraries/src/Menu/Node/Separator.php', + '/libraries/src/Menu/Node/Url.php', + '/libraries/src/Menu/Tree.php', + '/libraries/src/Table/Observer/AbstractObserver.php', + '/libraries/src/Table/Observer/ContentHistory.php', + '/libraries/src/Table/Observer/Tags.php', + '/libraries/src/Toolbar/Button/SliderButton.php', + '/libraries/src/User/UserWrapper.php', + '/libraries/vendor/.htaccess', + '/libraries/vendor/brumann/polyfill-unserialize/LICENSE', + '/libraries/vendor/brumann/polyfill-unserialize/composer.json', + '/libraries/vendor/brumann/polyfill-unserialize/src/DisallowedClassesSubstitutor.php', + '/libraries/vendor/brumann/polyfill-unserialize/src/Unserialize.php', + '/libraries/vendor/ircmaxell/password-compat/LICENSE.md', + '/libraries/vendor/ircmaxell/password-compat/lib/password.php', + '/libraries/vendor/joomla/application/src/AbstractCliApplication.php', + '/libraries/vendor/joomla/application/src/AbstractDaemonApplication.php', + '/libraries/vendor/joomla/application/src/Cli/CliInput.php', + '/libraries/vendor/joomla/application/src/Cli/CliOutput.php', + '/libraries/vendor/joomla/application/src/Cli/ColorProcessor.php', + '/libraries/vendor/joomla/application/src/Cli/ColorStyle.php', + '/libraries/vendor/joomla/application/src/Cli/Output/Processor/ColorProcessor.php', + '/libraries/vendor/joomla/application/src/Cli/Output/Processor/ProcessorInterface.php', + '/libraries/vendor/joomla/application/src/Cli/Output/Stdout.php', + '/libraries/vendor/joomla/application/src/Cli/Output/Xml.php', + '/libraries/vendor/joomla/compat/LICENSE', + '/libraries/vendor/joomla/compat/src/CallbackFilterIterator.php', + '/libraries/vendor/joomla/compat/src/JsonSerializable.php', + '/libraries/vendor/joomla/event/src/DelegatingDispatcher.php', + '/libraries/vendor/joomla/filesystem/src/Stream/String.php', + '/libraries/vendor/joomla/image/LICENSE', + '/libraries/vendor/joomla/image/src/Filter/Backgroundfill.php', + '/libraries/vendor/joomla/image/src/Filter/Brightness.php', + '/libraries/vendor/joomla/image/src/Filter/Contrast.php', + '/libraries/vendor/joomla/image/src/Filter/Edgedetect.php', + '/libraries/vendor/joomla/image/src/Filter/Emboss.php', + '/libraries/vendor/joomla/image/src/Filter/Grayscale.php', + '/libraries/vendor/joomla/image/src/Filter/Negate.php', + '/libraries/vendor/joomla/image/src/Filter/Sketchy.php', + '/libraries/vendor/joomla/image/src/Filter/Smooth.php', + '/libraries/vendor/joomla/image/src/Image.php', + '/libraries/vendor/joomla/image/src/ImageFilter.php', + '/libraries/vendor/joomla/input/src/Cli.php', + '/libraries/vendor/joomla/registry/src/AbstractRegistryFormat.php', + '/libraries/vendor/joomla/session/Joomla/Session/LICENSE', + '/libraries/vendor/joomla/session/Joomla/Session/Session.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/Apc.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/Apcu.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/Database.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/Memcache.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/Memcached.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/None.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/Wincache.php', + '/libraries/vendor/joomla/session/Joomla/Session/Storage/Xcache.php', + '/libraries/vendor/joomla/string/src/String.php', + '/libraries/vendor/leafo/lessphp/LICENSE', + '/libraries/vendor/leafo/lessphp/lessc.inc.php', + '/libraries/vendor/leafo/lessphp/lessify', + '/libraries/vendor/leafo/lessphp/lessify.inc.php', + '/libraries/vendor/leafo/lessphp/plessc', + '/libraries/vendor/paragonie/random_compat/LICENSE', + '/libraries/vendor/paragonie/random_compat/lib/byte_safe_strings.php', + '/libraries/vendor/paragonie/random_compat/lib/cast_to_int.php', + '/libraries/vendor/paragonie/random_compat/lib/error_polyfill.php', + '/libraries/vendor/paragonie/random_compat/lib/random.php', + '/libraries/vendor/paragonie/random_compat/lib/random_bytes_com_dotnet.php', + '/libraries/vendor/paragonie/random_compat/lib/random_bytes_dev_urandom.php', + '/libraries/vendor/paragonie/random_compat/lib/random_bytes_libsodium.php', + '/libraries/vendor/paragonie/random_compat/lib/random_bytes_libsodium_legacy.php', + '/libraries/vendor/paragonie/random_compat/lib/random_bytes_mcrypt.php', + '/libraries/vendor/paragonie/random_compat/lib/random_bytes_openssl.php', + '/libraries/vendor/paragonie/random_compat/lib/random_int.php', + '/libraries/vendor/paragonie/sodium_compat/src/Core32/Curve25519/README.md', + '/libraries/vendor/phpmailer/phpmailer/PHPMailerAutoload.php', + '/libraries/vendor/phpmailer/phpmailer/class.phpmailer.php', + '/libraries/vendor/phpmailer/phpmailer/class.phpmaileroauth.php', + '/libraries/vendor/phpmailer/phpmailer/class.phpmaileroauthgoogle.php', + '/libraries/vendor/phpmailer/phpmailer/class.pop3.php', + '/libraries/vendor/phpmailer/phpmailer/class.smtp.php', + '/libraries/vendor/phpmailer/phpmailer/extras/EasyPeasyICS.php', + '/libraries/vendor/phpmailer/phpmailer/extras/htmlfilter.php', + '/libraries/vendor/phpmailer/phpmailer/extras/ntlm_sasl_client.php', + '/libraries/vendor/simplepie/simplepie/LICENSE.txt', + '/libraries/vendor/simplepie/simplepie/autoloader.php', + '/libraries/vendor/simplepie/simplepie/db.sql', + '/libraries/vendor/simplepie/simplepie/idn/LICENCE', + '/libraries/vendor/simplepie/simplepie/idn/idna_convert.class.php', + '/libraries/vendor/simplepie/simplepie/idn/npdata.ser', + '/libraries/vendor/simplepie/simplepie/library/SimplePie.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Author.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/Base.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/DB.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/File.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/Memcache.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache/MySQL.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Caption.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Category.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Content/Type/Sniffer.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Copyright.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Core.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Credit.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Decode/HTML/Entities.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Enclosure.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Exception.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/File.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/HTTP/Parser.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/IRI.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Item.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Locator.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Misc.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Net/IPv6.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Parse/Date.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Parser.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Rating.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Registry.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Restriction.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Sanitize.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Source.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/XML/Declaration/Parser.php', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/gzdecode.php', + '/libraries/vendor/symfony/polyfill-php55/LICENSE', + '/libraries/vendor/symfony/polyfill-php55/Php55.php', + '/libraries/vendor/symfony/polyfill-php55/Php55ArrayColumn.php', + '/libraries/vendor/symfony/polyfill-php55/bootstrap.php', + '/libraries/vendor/symfony/polyfill-php56/LICENSE', + '/libraries/vendor/symfony/polyfill-php56/Php56.php', + '/libraries/vendor/symfony/polyfill-php56/bootstrap.php', + '/libraries/vendor/symfony/polyfill-php71/LICENSE', + '/libraries/vendor/symfony/polyfill-php71/Php71.php', + '/libraries/vendor/symfony/polyfill-php71/bootstrap.php', + '/libraries/vendor/symfony/polyfill-util/Binary.php', + '/libraries/vendor/symfony/polyfill-util/BinaryNoFuncOverload.php', + '/libraries/vendor/symfony/polyfill-util/BinaryOnFuncOverload.php', + '/libraries/vendor/symfony/polyfill-util/LICENSE', + '/libraries/vendor/typo3/phar-stream-wrapper/composer.json', + '/libraries/vendor/web.config', + '/media/cms/css/debug.css', + '/media/com_associations/js/sidebyside-uncompressed.js', + '/media/com_contenthistory/css/jquery.pretty-text-diff.css', + '/media/com_contenthistory/js/diff_match_patch.js', + '/media/com_contenthistory/js/jquery.pretty-text-diff.js', + '/media/com_contenthistory/js/jquery.pretty-text-diff.min.js', + '/media/com_finder/js/autocompleter.js', + '/media/com_joomlaupdate/js/encryption.js', + '/media/com_joomlaupdate/js/encryption.min.js', + '/media/com_joomlaupdate/js/json2.js', + '/media/com_joomlaupdate/js/json2.min.js', + '/media/com_joomlaupdate/js/update.js', + '/media/com_joomlaupdate/js/update.min.js', + '/media/contacts/images/con_address.png', + '/media/contacts/images/con_fax.png', + '/media/contacts/images/con_info.png', + '/media/contacts/images/con_mobile.png', + '/media/contacts/images/con_tel.png', + '/media/contacts/images/emailButton.png', + '/media/editors/codemirror/LICENSE', + '/media/editors/codemirror/addon/comment/comment.js', + '/media/editors/codemirror/addon/comment/comment.min.js', + '/media/editors/codemirror/addon/comment/continuecomment.js', + '/media/editors/codemirror/addon/comment/continuecomment.min.js', + '/media/editors/codemirror/addon/dialog/dialog.css', + '/media/editors/codemirror/addon/dialog/dialog.js', + '/media/editors/codemirror/addon/dialog/dialog.min.css', + '/media/editors/codemirror/addon/dialog/dialog.min.js', + '/media/editors/codemirror/addon/display/autorefresh.js', + '/media/editors/codemirror/addon/display/autorefresh.min.js', + '/media/editors/codemirror/addon/display/fullscreen.css', + '/media/editors/codemirror/addon/display/fullscreen.js', + '/media/editors/codemirror/addon/display/fullscreen.min.css', + '/media/editors/codemirror/addon/display/fullscreen.min.js', + '/media/editors/codemirror/addon/display/panel.js', + '/media/editors/codemirror/addon/display/panel.min.js', + '/media/editors/codemirror/addon/display/placeholder.js', + '/media/editors/codemirror/addon/display/placeholder.min.js', + '/media/editors/codemirror/addon/display/rulers.js', + '/media/editors/codemirror/addon/display/rulers.min.js', + '/media/editors/codemirror/addon/edit/closebrackets.js', + '/media/editors/codemirror/addon/edit/closebrackets.min.js', + '/media/editors/codemirror/addon/edit/closetag.js', + '/media/editors/codemirror/addon/edit/closetag.min.js', + '/media/editors/codemirror/addon/edit/continuelist.js', + '/media/editors/codemirror/addon/edit/continuelist.min.js', + '/media/editors/codemirror/addon/edit/matchbrackets.js', + '/media/editors/codemirror/addon/edit/matchbrackets.min.js', + '/media/editors/codemirror/addon/edit/matchtags.js', + '/media/editors/codemirror/addon/edit/matchtags.min.js', + '/media/editors/codemirror/addon/edit/trailingspace.js', + '/media/editors/codemirror/addon/edit/trailingspace.min.js', + '/media/editors/codemirror/addon/fold/brace-fold.js', + '/media/editors/codemirror/addon/fold/brace-fold.min.js', + '/media/editors/codemirror/addon/fold/comment-fold.js', + '/media/editors/codemirror/addon/fold/comment-fold.min.js', + '/media/editors/codemirror/addon/fold/foldcode.js', + '/media/editors/codemirror/addon/fold/foldcode.min.js', + '/media/editors/codemirror/addon/fold/foldgutter.css', + '/media/editors/codemirror/addon/fold/foldgutter.js', + '/media/editors/codemirror/addon/fold/foldgutter.min.css', + '/media/editors/codemirror/addon/fold/foldgutter.min.js', + '/media/editors/codemirror/addon/fold/indent-fold.js', + '/media/editors/codemirror/addon/fold/indent-fold.min.js', + '/media/editors/codemirror/addon/fold/markdown-fold.js', + '/media/editors/codemirror/addon/fold/markdown-fold.min.js', + '/media/editors/codemirror/addon/fold/xml-fold.js', + '/media/editors/codemirror/addon/fold/xml-fold.min.js', + '/media/editors/codemirror/addon/hint/anyword-hint.js', + '/media/editors/codemirror/addon/hint/anyword-hint.min.js', + '/media/editors/codemirror/addon/hint/css-hint.js', + '/media/editors/codemirror/addon/hint/css-hint.min.js', + '/media/editors/codemirror/addon/hint/html-hint.js', + '/media/editors/codemirror/addon/hint/html-hint.min.js', + '/media/editors/codemirror/addon/hint/javascript-hint.js', + '/media/editors/codemirror/addon/hint/javascript-hint.min.js', + '/media/editors/codemirror/addon/hint/show-hint.css', + '/media/editors/codemirror/addon/hint/show-hint.js', + '/media/editors/codemirror/addon/hint/show-hint.min.css', + '/media/editors/codemirror/addon/hint/show-hint.min.js', + '/media/editors/codemirror/addon/hint/sql-hint.js', + '/media/editors/codemirror/addon/hint/sql-hint.min.js', + '/media/editors/codemirror/addon/hint/xml-hint.js', + '/media/editors/codemirror/addon/hint/xml-hint.min.js', + '/media/editors/codemirror/addon/lint/coffeescript-lint.js', + '/media/editors/codemirror/addon/lint/coffeescript-lint.min.js', + '/media/editors/codemirror/addon/lint/css-lint.js', + '/media/editors/codemirror/addon/lint/css-lint.min.js', + '/media/editors/codemirror/addon/lint/html-lint.js', + '/media/editors/codemirror/addon/lint/html-lint.min.js', + '/media/editors/codemirror/addon/lint/javascript-lint.js', + '/media/editors/codemirror/addon/lint/javascript-lint.min.js', + '/media/editors/codemirror/addon/lint/json-lint.js', + '/media/editors/codemirror/addon/lint/json-lint.min.js', + '/media/editors/codemirror/addon/lint/lint.css', + '/media/editors/codemirror/addon/lint/lint.js', + '/media/editors/codemirror/addon/lint/lint.min.css', + '/media/editors/codemirror/addon/lint/lint.min.js', + '/media/editors/codemirror/addon/lint/yaml-lint.js', + '/media/editors/codemirror/addon/lint/yaml-lint.min.js', + '/media/editors/codemirror/addon/merge/merge.css', + '/media/editors/codemirror/addon/merge/merge.js', + '/media/editors/codemirror/addon/merge/merge.min.css', + '/media/editors/codemirror/addon/merge/merge.min.js', + '/media/editors/codemirror/addon/mode/loadmode.js', + '/media/editors/codemirror/addon/mode/loadmode.min.js', + '/media/editors/codemirror/addon/mode/multiplex.js', + '/media/editors/codemirror/addon/mode/multiplex.min.js', + '/media/editors/codemirror/addon/mode/multiplex_test.js', + '/media/editors/codemirror/addon/mode/multiplex_test.min.js', + '/media/editors/codemirror/addon/mode/overlay.js', + '/media/editors/codemirror/addon/mode/overlay.min.js', + '/media/editors/codemirror/addon/mode/simple.js', + '/media/editors/codemirror/addon/mode/simple.min.js', + '/media/editors/codemirror/addon/runmode/colorize.js', + '/media/editors/codemirror/addon/runmode/colorize.min.js', + '/media/editors/codemirror/addon/runmode/runmode-standalone.js', + '/media/editors/codemirror/addon/runmode/runmode-standalone.min.js', + '/media/editors/codemirror/addon/runmode/runmode.js', + '/media/editors/codemirror/addon/runmode/runmode.min.js', + '/media/editors/codemirror/addon/runmode/runmode.node.js', + '/media/editors/codemirror/addon/scroll/annotatescrollbar.js', + '/media/editors/codemirror/addon/scroll/annotatescrollbar.min.js', + '/media/editors/codemirror/addon/scroll/scrollpastend.js', + '/media/editors/codemirror/addon/scroll/scrollpastend.min.js', + '/media/editors/codemirror/addon/scroll/simplescrollbars.css', + '/media/editors/codemirror/addon/scroll/simplescrollbars.js', + '/media/editors/codemirror/addon/scroll/simplescrollbars.min.css', + '/media/editors/codemirror/addon/scroll/simplescrollbars.min.js', + '/media/editors/codemirror/addon/search/jump-to-line.js', + '/media/editors/codemirror/addon/search/jump-to-line.min.js', + '/media/editors/codemirror/addon/search/match-highlighter.js', + '/media/editors/codemirror/addon/search/match-highlighter.min.js', + '/media/editors/codemirror/addon/search/matchesonscrollbar.css', + '/media/editors/codemirror/addon/search/matchesonscrollbar.js', + '/media/editors/codemirror/addon/search/matchesonscrollbar.min.css', + '/media/editors/codemirror/addon/search/matchesonscrollbar.min.js', + '/media/editors/codemirror/addon/search/search.js', + '/media/editors/codemirror/addon/search/search.min.js', + '/media/editors/codemirror/addon/search/searchcursor.js', + '/media/editors/codemirror/addon/search/searchcursor.min.js', + '/media/editors/codemirror/addon/selection/active-line.js', + '/media/editors/codemirror/addon/selection/active-line.min.js', + '/media/editors/codemirror/addon/selection/mark-selection.js', + '/media/editors/codemirror/addon/selection/mark-selection.min.js', + '/media/editors/codemirror/addon/selection/selection-pointer.js', + '/media/editors/codemirror/addon/selection/selection-pointer.min.js', + '/media/editors/codemirror/addon/tern/tern.css', + '/media/editors/codemirror/addon/tern/tern.js', + '/media/editors/codemirror/addon/tern/tern.min.css', + '/media/editors/codemirror/addon/tern/tern.min.js', + '/media/editors/codemirror/addon/tern/worker.js', + '/media/editors/codemirror/addon/tern/worker.min.js', + '/media/editors/codemirror/addon/wrap/hardwrap.js', + '/media/editors/codemirror/addon/wrap/hardwrap.min.js', + '/media/editors/codemirror/keymap/emacs.js', + '/media/editors/codemirror/keymap/emacs.min.js', + '/media/editors/codemirror/keymap/sublime.js', + '/media/editors/codemirror/keymap/sublime.min.js', + '/media/editors/codemirror/keymap/vim.js', + '/media/editors/codemirror/keymap/vim.min.js', + '/media/editors/codemirror/lib/addons.css', + '/media/editors/codemirror/lib/addons.js', + '/media/editors/codemirror/lib/addons.min.css', + '/media/editors/codemirror/lib/addons.min.js', + '/media/editors/codemirror/lib/codemirror.css', + '/media/editors/codemirror/lib/codemirror.js', + '/media/editors/codemirror/lib/codemirror.min.css', + '/media/editors/codemirror/lib/codemirror.min.js', + '/media/editors/codemirror/mode/apl/apl.js', + '/media/editors/codemirror/mode/apl/apl.min.js', + '/media/editors/codemirror/mode/asciiarmor/asciiarmor.js', + '/media/editors/codemirror/mode/asciiarmor/asciiarmor.min.js', + '/media/editors/codemirror/mode/asn.1/asn.1.js', + '/media/editors/codemirror/mode/asn.1/asn.min.js', + '/media/editors/codemirror/mode/asterisk/asterisk.js', + '/media/editors/codemirror/mode/asterisk/asterisk.min.js', + '/media/editors/codemirror/mode/brainfuck/brainfuck.js', + '/media/editors/codemirror/mode/brainfuck/brainfuck.min.js', + '/media/editors/codemirror/mode/clike/clike.js', + '/media/editors/codemirror/mode/clike/clike.min.js', + '/media/editors/codemirror/mode/clojure/clojure.js', + '/media/editors/codemirror/mode/clojure/clojure.min.js', + '/media/editors/codemirror/mode/cmake/cmake.js', + '/media/editors/codemirror/mode/cmake/cmake.min.js', + '/media/editors/codemirror/mode/cobol/cobol.js', + '/media/editors/codemirror/mode/cobol/cobol.min.js', + '/media/editors/codemirror/mode/coffeescript/coffeescript.js', + '/media/editors/codemirror/mode/coffeescript/coffeescript.min.js', + '/media/editors/codemirror/mode/commonlisp/commonlisp.js', + '/media/editors/codemirror/mode/commonlisp/commonlisp.min.js', + '/media/editors/codemirror/mode/crystal/crystal.js', + '/media/editors/codemirror/mode/crystal/crystal.min.js', + '/media/editors/codemirror/mode/css/css.js', + '/media/editors/codemirror/mode/css/css.min.js', + '/media/editors/codemirror/mode/cypher/cypher.js', + '/media/editors/codemirror/mode/cypher/cypher.min.js', + '/media/editors/codemirror/mode/d/d.js', + '/media/editors/codemirror/mode/d/d.min.js', + '/media/editors/codemirror/mode/dart/dart.js', + '/media/editors/codemirror/mode/dart/dart.min.js', + '/media/editors/codemirror/mode/diff/diff.js', + '/media/editors/codemirror/mode/diff/diff.min.js', + '/media/editors/codemirror/mode/django/django.js', + '/media/editors/codemirror/mode/django/django.min.js', + '/media/editors/codemirror/mode/dockerfile/dockerfile.js', + '/media/editors/codemirror/mode/dockerfile/dockerfile.min.js', + '/media/editors/codemirror/mode/dtd/dtd.js', + '/media/editors/codemirror/mode/dtd/dtd.min.js', + '/media/editors/codemirror/mode/dylan/dylan.js', + '/media/editors/codemirror/mode/dylan/dylan.min.js', + '/media/editors/codemirror/mode/ebnf/ebnf.js', + '/media/editors/codemirror/mode/ebnf/ebnf.min.js', + '/media/editors/codemirror/mode/ecl/ecl.js', + '/media/editors/codemirror/mode/ecl/ecl.min.js', + '/media/editors/codemirror/mode/eiffel/eiffel.js', + '/media/editors/codemirror/mode/eiffel/eiffel.min.js', + '/media/editors/codemirror/mode/elm/elm.js', + '/media/editors/codemirror/mode/elm/elm.min.js', + '/media/editors/codemirror/mode/erlang/erlang.js', + '/media/editors/codemirror/mode/erlang/erlang.min.js', + '/media/editors/codemirror/mode/factor/factor.js', + '/media/editors/codemirror/mode/factor/factor.min.js', + '/media/editors/codemirror/mode/fcl/fcl.js', + '/media/editors/codemirror/mode/fcl/fcl.min.js', + '/media/editors/codemirror/mode/forth/forth.js', + '/media/editors/codemirror/mode/forth/forth.min.js', + '/media/editors/codemirror/mode/fortran/fortran.js', + '/media/editors/codemirror/mode/fortran/fortran.min.js', + '/media/editors/codemirror/mode/gas/gas.js', + '/media/editors/codemirror/mode/gas/gas.min.js', + '/media/editors/codemirror/mode/gfm/gfm.js', + '/media/editors/codemirror/mode/gfm/gfm.min.js', + '/media/editors/codemirror/mode/gherkin/gherkin.js', + '/media/editors/codemirror/mode/gherkin/gherkin.min.js', + '/media/editors/codemirror/mode/go/go.js', + '/media/editors/codemirror/mode/go/go.min.js', + '/media/editors/codemirror/mode/groovy/groovy.js', + '/media/editors/codemirror/mode/groovy/groovy.min.js', + '/media/editors/codemirror/mode/haml/haml.js', + '/media/editors/codemirror/mode/haml/haml.min.js', + '/media/editors/codemirror/mode/handlebars/handlebars.js', + '/media/editors/codemirror/mode/handlebars/handlebars.min.js', + '/media/editors/codemirror/mode/haskell-literate/haskell-literate.js', + '/media/editors/codemirror/mode/haskell-literate/haskell-literate.min.js', + '/media/editors/codemirror/mode/haskell/haskell.js', + '/media/editors/codemirror/mode/haskell/haskell.min.js', + '/media/editors/codemirror/mode/haxe/haxe.js', + '/media/editors/codemirror/mode/haxe/haxe.min.js', + '/media/editors/codemirror/mode/htmlembedded/htmlembedded.js', + '/media/editors/codemirror/mode/htmlembedded/htmlembedded.min.js', + '/media/editors/codemirror/mode/htmlmixed/htmlmixed.js', + '/media/editors/codemirror/mode/htmlmixed/htmlmixed.min.js', + '/media/editors/codemirror/mode/http/http.js', + '/media/editors/codemirror/mode/http/http.min.js', + '/media/editors/codemirror/mode/idl/idl.js', + '/media/editors/codemirror/mode/idl/idl.min.js', + '/media/editors/codemirror/mode/javascript/javascript.js', + '/media/editors/codemirror/mode/javascript/javascript.min.js', + '/media/editors/codemirror/mode/jinja2/jinja2.js', + '/media/editors/codemirror/mode/jinja2/jinja2.min.js', + '/media/editors/codemirror/mode/jsx/jsx.js', + '/media/editors/codemirror/mode/jsx/jsx.min.js', + '/media/editors/codemirror/mode/julia/julia.js', + '/media/editors/codemirror/mode/julia/julia.min.js', + '/media/editors/codemirror/mode/livescript/livescript.js', + '/media/editors/codemirror/mode/livescript/livescript.min.js', + '/media/editors/codemirror/mode/lua/lua.js', + '/media/editors/codemirror/mode/lua/lua.min.js', + '/media/editors/codemirror/mode/markdown/markdown.js', + '/media/editors/codemirror/mode/markdown/markdown.min.js', + '/media/editors/codemirror/mode/mathematica/mathematica.js', + '/media/editors/codemirror/mode/mathematica/mathematica.min.js', + '/media/editors/codemirror/mode/mbox/mbox.js', + '/media/editors/codemirror/mode/mbox/mbox.min.js', + '/media/editors/codemirror/mode/meta.js', + '/media/editors/codemirror/mode/meta.min.js', + '/media/editors/codemirror/mode/mirc/mirc.js', + '/media/editors/codemirror/mode/mirc/mirc.min.js', + '/media/editors/codemirror/mode/mllike/mllike.js', + '/media/editors/codemirror/mode/mllike/mllike.min.js', + '/media/editors/codemirror/mode/modelica/modelica.js', + '/media/editors/codemirror/mode/modelica/modelica.min.js', + '/media/editors/codemirror/mode/mscgen/mscgen.js', + '/media/editors/codemirror/mode/mscgen/mscgen.min.js', + '/media/editors/codemirror/mode/mumps/mumps.js', + '/media/editors/codemirror/mode/mumps/mumps.min.js', + '/media/editors/codemirror/mode/nginx/nginx.js', + '/media/editors/codemirror/mode/nginx/nginx.min.js', + '/media/editors/codemirror/mode/nsis/nsis.js', + '/media/editors/codemirror/mode/nsis/nsis.min.js', + '/media/editors/codemirror/mode/ntriples/ntriples.js', + '/media/editors/codemirror/mode/ntriples/ntriples.min.js', + '/media/editors/codemirror/mode/octave/octave.js', + '/media/editors/codemirror/mode/octave/octave.min.js', + '/media/editors/codemirror/mode/oz/oz.js', + '/media/editors/codemirror/mode/oz/oz.min.js', + '/media/editors/codemirror/mode/pascal/pascal.js', + '/media/editors/codemirror/mode/pascal/pascal.min.js', + '/media/editors/codemirror/mode/pegjs/pegjs.js', + '/media/editors/codemirror/mode/pegjs/pegjs.min.js', + '/media/editors/codemirror/mode/perl/perl.js', + '/media/editors/codemirror/mode/perl/perl.min.js', + '/media/editors/codemirror/mode/php/php.js', + '/media/editors/codemirror/mode/php/php.min.js', + '/media/editors/codemirror/mode/pig/pig.js', + '/media/editors/codemirror/mode/pig/pig.min.js', + '/media/editors/codemirror/mode/powershell/powershell.js', + '/media/editors/codemirror/mode/powershell/powershell.min.js', + '/media/editors/codemirror/mode/properties/properties.js', + '/media/editors/codemirror/mode/properties/properties.min.js', + '/media/editors/codemirror/mode/protobuf/protobuf.js', + '/media/editors/codemirror/mode/protobuf/protobuf.min.js', + '/media/editors/codemirror/mode/pug/pug.js', + '/media/editors/codemirror/mode/pug/pug.min.js', + '/media/editors/codemirror/mode/puppet/puppet.js', + '/media/editors/codemirror/mode/puppet/puppet.min.js', + '/media/editors/codemirror/mode/python/python.js', + '/media/editors/codemirror/mode/python/python.min.js', + '/media/editors/codemirror/mode/q/q.js', + '/media/editors/codemirror/mode/q/q.min.js', + '/media/editors/codemirror/mode/r/r.js', + '/media/editors/codemirror/mode/r/r.min.js', + '/media/editors/codemirror/mode/rpm/changes/index.html', + '/media/editors/codemirror/mode/rpm/rpm.js', + '/media/editors/codemirror/mode/rpm/rpm.min.js', + '/media/editors/codemirror/mode/rst/rst.js', + '/media/editors/codemirror/mode/rst/rst.min.js', + '/media/editors/codemirror/mode/ruby/ruby.js', + '/media/editors/codemirror/mode/ruby/ruby.min.js', + '/media/editors/codemirror/mode/rust/rust.js', + '/media/editors/codemirror/mode/rust/rust.min.js', + '/media/editors/codemirror/mode/sas/sas.js', + '/media/editors/codemirror/mode/sas/sas.min.js', + '/media/editors/codemirror/mode/sass/sass.js', + '/media/editors/codemirror/mode/sass/sass.min.js', + '/media/editors/codemirror/mode/scheme/scheme.js', + '/media/editors/codemirror/mode/scheme/scheme.min.js', + '/media/editors/codemirror/mode/shell/shell.js', + '/media/editors/codemirror/mode/shell/shell.min.js', + '/media/editors/codemirror/mode/sieve/sieve.js', + '/media/editors/codemirror/mode/sieve/sieve.min.js', + '/media/editors/codemirror/mode/slim/slim.js', + '/media/editors/codemirror/mode/slim/slim.min.js', + '/media/editors/codemirror/mode/smalltalk/smalltalk.js', + '/media/editors/codemirror/mode/smalltalk/smalltalk.min.js', + '/media/editors/codemirror/mode/smarty/smarty.js', + '/media/editors/codemirror/mode/smarty/smarty.min.js', + '/media/editors/codemirror/mode/solr/solr.js', + '/media/editors/codemirror/mode/solr/solr.min.js', + '/media/editors/codemirror/mode/soy/soy.js', + '/media/editors/codemirror/mode/soy/soy.min.js', + '/media/editors/codemirror/mode/sparql/sparql.js', + '/media/editors/codemirror/mode/sparql/sparql.min.js', + '/media/editors/codemirror/mode/spreadsheet/spreadsheet.js', + '/media/editors/codemirror/mode/spreadsheet/spreadsheet.min.js', + '/media/editors/codemirror/mode/sql/sql.js', + '/media/editors/codemirror/mode/sql/sql.min.js', + '/media/editors/codemirror/mode/stex/stex.js', + '/media/editors/codemirror/mode/stex/stex.min.js', + '/media/editors/codemirror/mode/stylus/stylus.js', + '/media/editors/codemirror/mode/stylus/stylus.min.js', + '/media/editors/codemirror/mode/swift/swift.js', + '/media/editors/codemirror/mode/swift/swift.min.js', + '/media/editors/codemirror/mode/tcl/tcl.js', + '/media/editors/codemirror/mode/tcl/tcl.min.js', + '/media/editors/codemirror/mode/textile/textile.js', + '/media/editors/codemirror/mode/textile/textile.min.js', + '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.css', + '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.js', + '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.min.css', + '/media/editors/codemirror/mode/tiddlywiki/tiddlywiki.min.js', + '/media/editors/codemirror/mode/tiki/tiki.css', + '/media/editors/codemirror/mode/tiki/tiki.js', + '/media/editors/codemirror/mode/tiki/tiki.min.css', + '/media/editors/codemirror/mode/tiki/tiki.min.js', + '/media/editors/codemirror/mode/toml/toml.js', + '/media/editors/codemirror/mode/toml/toml.min.js', + '/media/editors/codemirror/mode/tornado/tornado.js', + '/media/editors/codemirror/mode/tornado/tornado.min.js', + '/media/editors/codemirror/mode/troff/troff.js', + '/media/editors/codemirror/mode/troff/troff.min.js', + '/media/editors/codemirror/mode/ttcn-cfg/ttcn-cfg.js', + '/media/editors/codemirror/mode/ttcn-cfg/ttcn-cfg.min.js', + '/media/editors/codemirror/mode/ttcn/ttcn.js', + '/media/editors/codemirror/mode/ttcn/ttcn.min.js', + '/media/editors/codemirror/mode/turtle/turtle.js', + '/media/editors/codemirror/mode/turtle/turtle.min.js', + '/media/editors/codemirror/mode/twig/twig.js', + '/media/editors/codemirror/mode/twig/twig.min.js', + '/media/editors/codemirror/mode/vb/vb.js', + '/media/editors/codemirror/mode/vb/vb.min.js', + '/media/editors/codemirror/mode/vbscript/vbscript.js', + '/media/editors/codemirror/mode/vbscript/vbscript.min.js', + '/media/editors/codemirror/mode/velocity/velocity.js', + '/media/editors/codemirror/mode/velocity/velocity.min.js', + '/media/editors/codemirror/mode/verilog/verilog.js', + '/media/editors/codemirror/mode/verilog/verilog.min.js', + '/media/editors/codemirror/mode/vhdl/vhdl.js', + '/media/editors/codemirror/mode/vhdl/vhdl.min.js', + '/media/editors/codemirror/mode/vue/vue.js', + '/media/editors/codemirror/mode/vue/vue.min.js', + '/media/editors/codemirror/mode/wast/wast.js', + '/media/editors/codemirror/mode/wast/wast.min.js', + '/media/editors/codemirror/mode/webidl/webidl.js', + '/media/editors/codemirror/mode/webidl/webidl.min.js', + '/media/editors/codemirror/mode/xml/xml.js', + '/media/editors/codemirror/mode/xml/xml.min.js', + '/media/editors/codemirror/mode/xquery/xquery.js', + '/media/editors/codemirror/mode/xquery/xquery.min.js', + '/media/editors/codemirror/mode/yacas/yacas.js', + '/media/editors/codemirror/mode/yacas/yacas.min.js', + '/media/editors/codemirror/mode/yaml-frontmatter/yaml-frontmatter.js', + '/media/editors/codemirror/mode/yaml-frontmatter/yaml-frontmatter.min.js', + '/media/editors/codemirror/mode/yaml/yaml.js', + '/media/editors/codemirror/mode/yaml/yaml.min.js', + '/media/editors/codemirror/mode/z80/z80.js', + '/media/editors/codemirror/mode/z80/z80.min.js', + '/media/editors/codemirror/theme/3024-day.css', + '/media/editors/codemirror/theme/3024-night.css', + '/media/editors/codemirror/theme/abcdef.css', + '/media/editors/codemirror/theme/ambiance-mobile.css', + '/media/editors/codemirror/theme/ambiance.css', + '/media/editors/codemirror/theme/ayu-dark.css', + '/media/editors/codemirror/theme/ayu-mirage.css', + '/media/editors/codemirror/theme/base16-dark.css', + '/media/editors/codemirror/theme/base16-light.css', + '/media/editors/codemirror/theme/bespin.css', + '/media/editors/codemirror/theme/blackboard.css', + '/media/editors/codemirror/theme/cobalt.css', + '/media/editors/codemirror/theme/colorforth.css', + '/media/editors/codemirror/theme/darcula.css', + '/media/editors/codemirror/theme/dracula.css', + '/media/editors/codemirror/theme/duotone-dark.css', + '/media/editors/codemirror/theme/duotone-light.css', + '/media/editors/codemirror/theme/eclipse.css', + '/media/editors/codemirror/theme/elegant.css', + '/media/editors/codemirror/theme/erlang-dark.css', + '/media/editors/codemirror/theme/gruvbox-dark.css', + '/media/editors/codemirror/theme/hopscotch.css', + '/media/editors/codemirror/theme/icecoder.css', + '/media/editors/codemirror/theme/idea.css', + '/media/editors/codemirror/theme/isotope.css', + '/media/editors/codemirror/theme/lesser-dark.css', + '/media/editors/codemirror/theme/liquibyte.css', + '/media/editors/codemirror/theme/lucario.css', + '/media/editors/codemirror/theme/material-darker.css', + '/media/editors/codemirror/theme/material-ocean.css', + '/media/editors/codemirror/theme/material-palenight.css', + '/media/editors/codemirror/theme/material.css', + '/media/editors/codemirror/theme/mbo.css', + '/media/editors/codemirror/theme/mdn-like.css', + '/media/editors/codemirror/theme/midnight.css', + '/media/editors/codemirror/theme/monokai.css', + '/media/editors/codemirror/theme/moxer.css', + '/media/editors/codemirror/theme/neat.css', + '/media/editors/codemirror/theme/neo.css', + '/media/editors/codemirror/theme/night.css', + '/media/editors/codemirror/theme/nord.css', + '/media/editors/codemirror/theme/oceanic-next.css', + '/media/editors/codemirror/theme/panda-syntax.css', + '/media/editors/codemirror/theme/paraiso-dark.css', + '/media/editors/codemirror/theme/paraiso-light.css', + '/media/editors/codemirror/theme/pastel-on-dark.css', + '/media/editors/codemirror/theme/railscasts.css', + '/media/editors/codemirror/theme/rubyblue.css', + '/media/editors/codemirror/theme/seti.css', + '/media/editors/codemirror/theme/shadowfox.css', + '/media/editors/codemirror/theme/solarized.css', + '/media/editors/codemirror/theme/ssms.css', + '/media/editors/codemirror/theme/the-matrix.css', + '/media/editors/codemirror/theme/tomorrow-night-bright.css', + '/media/editors/codemirror/theme/tomorrow-night-eighties.css', + '/media/editors/codemirror/theme/ttcn.css', + '/media/editors/codemirror/theme/twilight.css', + '/media/editors/codemirror/theme/vibrant-ink.css', + '/media/editors/codemirror/theme/xq-dark.css', + '/media/editors/codemirror/theme/xq-light.css', + '/media/editors/codemirror/theme/yeti.css', + '/media/editors/codemirror/theme/yonce.css', + '/media/editors/codemirror/theme/zenburn.css', + '/media/editors/none/js/none.js', + '/media/editors/none/js/none.min.js', + '/media/editors/tinymce/changelog.txt', + '/media/editors/tinymce/js/plugins/dragdrop/plugin.js', + '/media/editors/tinymce/js/plugins/dragdrop/plugin.min.js', + '/media/editors/tinymce/js/tiny-close.js', + '/media/editors/tinymce/js/tiny-close.min.js', + '/media/editors/tinymce/js/tinymce-builder.js', + '/media/editors/tinymce/js/tinymce.js', + '/media/editors/tinymce/js/tinymce.min.js', + '/media/editors/tinymce/langs/af.js', + '/media/editors/tinymce/langs/ar.js', + '/media/editors/tinymce/langs/be.js', + '/media/editors/tinymce/langs/bg.js', + '/media/editors/tinymce/langs/bs.js', + '/media/editors/tinymce/langs/ca.js', + '/media/editors/tinymce/langs/cs.js', + '/media/editors/tinymce/langs/cy.js', + '/media/editors/tinymce/langs/da.js', + '/media/editors/tinymce/langs/de.js', + '/media/editors/tinymce/langs/el.js', + '/media/editors/tinymce/langs/es.js', + '/media/editors/tinymce/langs/et.js', + '/media/editors/tinymce/langs/eu.js', + '/media/editors/tinymce/langs/fa.js', + '/media/editors/tinymce/langs/fi.js', + '/media/editors/tinymce/langs/fo.js', + '/media/editors/tinymce/langs/fr.js', + '/media/editors/tinymce/langs/ga.js', + '/media/editors/tinymce/langs/gl.js', + '/media/editors/tinymce/langs/he.js', + '/media/editors/tinymce/langs/hr.js', + '/media/editors/tinymce/langs/hu.js', + '/media/editors/tinymce/langs/id.js', + '/media/editors/tinymce/langs/it.js', + '/media/editors/tinymce/langs/ja.js', + '/media/editors/tinymce/langs/ka.js', + '/media/editors/tinymce/langs/kk.js', + '/media/editors/tinymce/langs/km.js', + '/media/editors/tinymce/langs/ko.js', + '/media/editors/tinymce/langs/lb.js', + '/media/editors/tinymce/langs/lt.js', + '/media/editors/tinymce/langs/lv.js', + '/media/editors/tinymce/langs/mk.js', + '/media/editors/tinymce/langs/ms.js', + '/media/editors/tinymce/langs/nb.js', + '/media/editors/tinymce/langs/nl.js', + '/media/editors/tinymce/langs/pl.js', + '/media/editors/tinymce/langs/pt-BR.js', + '/media/editors/tinymce/langs/pt-PT.js', + '/media/editors/tinymce/langs/readme.md', + '/media/editors/tinymce/langs/ro.js', + '/media/editors/tinymce/langs/ru.js', + '/media/editors/tinymce/langs/si-LK.js', + '/media/editors/tinymce/langs/sk.js', + '/media/editors/tinymce/langs/sl.js', + '/media/editors/tinymce/langs/sr.js', + '/media/editors/tinymce/langs/sv.js', + '/media/editors/tinymce/langs/sw.js', + '/media/editors/tinymce/langs/sy.js', + '/media/editors/tinymce/langs/ta.js', + '/media/editors/tinymce/langs/th.js', + '/media/editors/tinymce/langs/tr.js', + '/media/editors/tinymce/langs/ug.js', + '/media/editors/tinymce/langs/uk.js', + '/media/editors/tinymce/langs/vi.js', + '/media/editors/tinymce/langs/zh-CN.js', + '/media/editors/tinymce/langs/zh-TW.js', + '/media/editors/tinymce/license.txt', + '/media/editors/tinymce/plugins/advlist/plugin.min.js', + '/media/editors/tinymce/plugins/anchor/plugin.min.js', + '/media/editors/tinymce/plugins/autolink/plugin.min.js', + '/media/editors/tinymce/plugins/autoresize/plugin.min.js', + '/media/editors/tinymce/plugins/autosave/plugin.min.js', + '/media/editors/tinymce/plugins/bbcode/plugin.min.js', + '/media/editors/tinymce/plugins/charmap/plugin.min.js', + '/media/editors/tinymce/plugins/code/plugin.min.js', + '/media/editors/tinymce/plugins/codesample/css/prism.css', + '/media/editors/tinymce/plugins/codesample/plugin.min.js', + '/media/editors/tinymce/plugins/colorpicker/plugin.min.js', + '/media/editors/tinymce/plugins/contextmenu/plugin.min.js', + '/media/editors/tinymce/plugins/directionality/plugin.min.js', + '/media/editors/tinymce/plugins/emoticons/img/smiley-cool.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-cry.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-embarassed.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-foot-in-mouth.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-frown.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-innocent.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-kiss.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-laughing.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-money-mouth.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-sealed.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-smile.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-surprised.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-tongue-out.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-undecided.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-wink.gif', + '/media/editors/tinymce/plugins/emoticons/img/smiley-yell.gif', + '/media/editors/tinymce/plugins/emoticons/plugin.min.js', + '/media/editors/tinymce/plugins/example/dialog.html', + '/media/editors/tinymce/plugins/example/plugin.min.js', + '/media/editors/tinymce/plugins/example_dependency/plugin.min.js', + '/media/editors/tinymce/plugins/fullpage/plugin.min.js', + '/media/editors/tinymce/plugins/fullscreen/plugin.min.js', + '/media/editors/tinymce/plugins/hr/plugin.min.js', + '/media/editors/tinymce/plugins/image/plugin.min.js', + '/media/editors/tinymce/plugins/imagetools/plugin.min.js', + '/media/editors/tinymce/plugins/importcss/plugin.min.js', + '/media/editors/tinymce/plugins/insertdatetime/plugin.min.js', + '/media/editors/tinymce/plugins/layer/plugin.min.js', + '/media/editors/tinymce/plugins/legacyoutput/plugin.min.js', + '/media/editors/tinymce/plugins/link/plugin.min.js', + '/media/editors/tinymce/plugins/lists/plugin.min.js', + '/media/editors/tinymce/plugins/media/plugin.min.js', + '/media/editors/tinymce/plugins/nonbreaking/plugin.min.js', + '/media/editors/tinymce/plugins/noneditable/plugin.min.js', + '/media/editors/tinymce/plugins/pagebreak/plugin.min.js', + '/media/editors/tinymce/plugins/paste/plugin.min.js', + '/media/editors/tinymce/plugins/preview/plugin.min.js', + '/media/editors/tinymce/plugins/print/plugin.min.js', + '/media/editors/tinymce/plugins/save/plugin.min.js', + '/media/editors/tinymce/plugins/searchreplace/plugin.min.js', + '/media/editors/tinymce/plugins/spellchecker/plugin.min.js', + '/media/editors/tinymce/plugins/tabfocus/plugin.min.js', + '/media/editors/tinymce/plugins/table/plugin.min.js', + '/media/editors/tinymce/plugins/template/plugin.min.js', + '/media/editors/tinymce/plugins/textcolor/plugin.min.js', + '/media/editors/tinymce/plugins/textpattern/plugin.min.js', + '/media/editors/tinymce/plugins/toc/plugin.min.js', + '/media/editors/tinymce/plugins/visualblocks/css/visualblocks.css', + '/media/editors/tinymce/plugins/visualblocks/plugin.min.js', + '/media/editors/tinymce/plugins/visualchars/plugin.min.js', + '/media/editors/tinymce/plugins/wordcount/plugin.min.js', + '/media/editors/tinymce/skins/lightgray/content.inline.min.css', + '/media/editors/tinymce/skins/lightgray/content.min.css', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.eot', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.svg', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.ttf', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce-small.woff', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce.eot', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce.svg', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce.ttf', + '/media/editors/tinymce/skins/lightgray/fonts/tinymce.woff', + '/media/editors/tinymce/skins/lightgray/img/anchor.gif', + '/media/editors/tinymce/skins/lightgray/img/loader.gif', + '/media/editors/tinymce/skins/lightgray/img/object.gif', + '/media/editors/tinymce/skins/lightgray/img/trans.gif', + '/media/editors/tinymce/skins/lightgray/skin.ie7.min.css', + '/media/editors/tinymce/skins/lightgray/skin.min.css', + '/media/editors/tinymce/templates/layout1.html', + '/media/editors/tinymce/templates/snippet1.html', + '/media/editors/tinymce/themes/modern/theme.min.js', + '/media/editors/tinymce/tinymce.min.js', + '/media/jui/css/bootstrap-extended.css', + '/media/jui/css/bootstrap-responsive.css', + '/media/jui/css/bootstrap-responsive.min.css', + '/media/jui/css/bootstrap-rtl.css', + '/media/jui/css/bootstrap-tooltip-extended.css', + '/media/jui/css/bootstrap.css', + '/media/jui/css/bootstrap.min.css', + '/media/jui/css/chosen-sprite.png', + '/media/jui/css/chosen-sprite@2x.png', + '/media/jui/css/chosen.css', + '/media/jui/css/icomoon.css', + '/media/jui/css/jquery.minicolors.css', + '/media/jui/css/jquery.searchtools.css', + '/media/jui/css/jquery.simplecolors.css', + '/media/jui/css/sortablelist.css', + '/media/jui/fonts/IcoMoon.dev.commented.svg', + '/media/jui/fonts/IcoMoon.dev.svg', + '/media/jui/fonts/IcoMoon.eot', + '/media/jui/fonts/IcoMoon.svg', + '/media/jui/fonts/IcoMoon.ttf', + '/media/jui/fonts/IcoMoon.woff', + '/media/jui/fonts/icomoon-license.txt', + '/media/jui/images/ajax-loader.gif', + '/media/jui/img/ajax-loader.gif', + '/media/jui/img/alpha.png', + '/media/jui/img/bg-overlay.png', + '/media/jui/img/glyphicons-halflings-white.png', + '/media/jui/img/glyphicons-halflings.png', + '/media/jui/img/hue.png', + '/media/jui/img/joomla.png', + '/media/jui/img/jquery.minicolors.png', + '/media/jui/img/saturation.png', + '/media/jui/js/ajax-chosen.js', + '/media/jui/js/ajax-chosen.min.js', + '/media/jui/js/bootstrap-tooltip-extended.js', + '/media/jui/js/bootstrap-tooltip-extended.min.js', + '/media/jui/js/bootstrap.js', + '/media/jui/js/bootstrap.min.js', + '/media/jui/js/chosen.jquery.js', + '/media/jui/js/chosen.jquery.min.js', + '/media/jui/js/cms-uncompressed.js', + '/media/jui/js/cms.js', + '/media/jui/js/fielduser.js', + '/media/jui/js/fielduser.min.js', + '/media/jui/js/html5-uncompressed.js', + '/media/jui/js/html5.js', + '/media/jui/js/icomoon-lte-ie7.js', + '/media/jui/js/jquery-migrate.js', + '/media/jui/js/jquery-migrate.min.js', + '/media/jui/js/jquery-noconflict.js', + '/media/jui/js/jquery.autocomplete.js', + '/media/jui/js/jquery.autocomplete.min.js', + '/media/jui/js/jquery.js', + '/media/jui/js/jquery.min.js', + '/media/jui/js/jquery.minicolors.js', + '/media/jui/js/jquery.minicolors.min.js', + '/media/jui/js/jquery.searchtools.js', + '/media/jui/js/jquery.searchtools.min.js', + '/media/jui/js/jquery.simplecolors.js', + '/media/jui/js/jquery.simplecolors.min.js', + '/media/jui/js/jquery.ui.core.js', + '/media/jui/js/jquery.ui.core.min.js', + '/media/jui/js/jquery.ui.sortable.js', + '/media/jui/js/jquery.ui.sortable.min.js', + '/media/jui/js/sortablelist.js', + '/media/jui/js/treeselectmenu.jquery.js', + '/media/jui/js/treeselectmenu.jquery.min.js', + '/media/jui/less/accordion.less', + '/media/jui/less/alerts.less', + '/media/jui/less/bootstrap-extended.less', + '/media/jui/less/bootstrap-rtl.less', + '/media/jui/less/bootstrap.less', + '/media/jui/less/breadcrumbs.less', + '/media/jui/less/button-groups.less', + '/media/jui/less/buttons.less', + '/media/jui/less/carousel.less', + '/media/jui/less/close.less', + '/media/jui/less/code.less', + '/media/jui/less/component-animations.less', + '/media/jui/less/dropdowns.less', + '/media/jui/less/forms.less', + '/media/jui/less/grid.less', + '/media/jui/less/hero-unit.less', + '/media/jui/less/icomoon.less', + '/media/jui/less/labels-badges.less', + '/media/jui/less/layouts.less', + '/media/jui/less/media.less', + '/media/jui/less/mixins.less', + '/media/jui/less/modals.joomla.less', + '/media/jui/less/modals.less', + '/media/jui/less/navbar.less', + '/media/jui/less/navs.less', + '/media/jui/less/pager.less', + '/media/jui/less/pagination.less', + '/media/jui/less/popovers.less', + '/media/jui/less/progress-bars.less', + '/media/jui/less/reset.less', + '/media/jui/less/responsive-1200px-min.less', + '/media/jui/less/responsive-767px-max.joomla.less', + '/media/jui/less/responsive-767px-max.less', + '/media/jui/less/responsive-768px-979px.less', + '/media/jui/less/responsive-navbar.less', + '/media/jui/less/responsive-utilities.less', + '/media/jui/less/responsive.less', + '/media/jui/less/scaffolding.less', + '/media/jui/less/sprites.less', + '/media/jui/less/tables.less', + '/media/jui/less/thumbnails.less', + '/media/jui/less/tooltip.less', + '/media/jui/less/type.less', + '/media/jui/less/utilities.less', + '/media/jui/less/variables.less', + '/media/jui/less/wells.less', + '/media/media/css/background.png', + '/media/media/css/bigplay.fw.png', + '/media/media/css/bigplay.png', + '/media/media/css/bigplay.svg', + '/media/media/css/controls-ted.png', + '/media/media/css/controls-wmp-bg.png', + '/media/media/css/controls-wmp.png', + '/media/media/css/controls.fw.png', + '/media/media/css/controls.png', + '/media/media/css/controls.svg', + '/media/media/css/jumpforward.png', + '/media/media/css/loading.gif', + '/media/media/css/mediaelementplayer.css', + '/media/media/css/mediaelementplayer.min.css', + '/media/media/css/medialist-details.css', + '/media/media/css/medialist-details_rtl.css', + '/media/media/css/medialist-thumbs.css', + '/media/media/css/medialist-thumbs_rtl.css', + '/media/media/css/mediamanager.css', + '/media/media/css/mediamanager_rtl.css', + '/media/media/css/mejs-skins.css', + '/media/media/css/popup-imagelist.css', + '/media/media/css/popup-imagelist_rtl.css', + '/media/media/css/popup-imagemanager.css', + '/media/media/css/popup-imagemanager_rtl.css', + '/media/media/css/skipback.png', + '/media/media/images/bar.gif', + '/media/media/images/con_info.png', + '/media/media/images/delete.png', + '/media/media/images/dots.gif', + '/media/media/images/failed.png', + '/media/media/images/folder.gif', + '/media/media/images/folder.png', + '/media/media/images/folder_sm.png', + '/media/media/images/folderup_16.png', + '/media/media/images/folderup_32.png', + '/media/media/images/mime-icon-16/avi.png', + '/media/media/images/mime-icon-16/doc.png', + '/media/media/images/mime-icon-16/mov.png', + '/media/media/images/mime-icon-16/mp3.png', + '/media/media/images/mime-icon-16/mp4.png', + '/media/media/images/mime-icon-16/odc.png', + '/media/media/images/mime-icon-16/odd.png', + '/media/media/images/mime-icon-16/odt.png', + '/media/media/images/mime-icon-16/ogg.png', + '/media/media/images/mime-icon-16/pdf.png', + '/media/media/images/mime-icon-16/ppt.png', + '/media/media/images/mime-icon-16/rar.png', + '/media/media/images/mime-icon-16/rtf.png', + '/media/media/images/mime-icon-16/svg.png', + '/media/media/images/mime-icon-16/sxd.png', + '/media/media/images/mime-icon-16/tar.png', + '/media/media/images/mime-icon-16/tgz.png', + '/media/media/images/mime-icon-16/wma.png', + '/media/media/images/mime-icon-16/wmv.png', + '/media/media/images/mime-icon-16/xls.png', + '/media/media/images/mime-icon-16/zip.png', + '/media/media/images/mime-icon-32/avi.png', + '/media/media/images/mime-icon-32/doc.png', + '/media/media/images/mime-icon-32/mov.png', + '/media/media/images/mime-icon-32/mp3.png', + '/media/media/images/mime-icon-32/mp4.png', + '/media/media/images/mime-icon-32/odc.png', + '/media/media/images/mime-icon-32/odd.png', + '/media/media/images/mime-icon-32/odt.png', + '/media/media/images/mime-icon-32/ogg.png', + '/media/media/images/mime-icon-32/pdf.png', + '/media/media/images/mime-icon-32/ppt.png', + '/media/media/images/mime-icon-32/rar.png', + '/media/media/images/mime-icon-32/rtf.png', + '/media/media/images/mime-icon-32/svg.png', + '/media/media/images/mime-icon-32/sxd.png', + '/media/media/images/mime-icon-32/tar.png', + '/media/media/images/mime-icon-32/tgz.png', + '/media/media/images/mime-icon-32/wma.png', + '/media/media/images/mime-icon-32/wmv.png', + '/media/media/images/mime-icon-32/xls.png', + '/media/media/images/mime-icon-32/zip.png', + '/media/media/images/progress.gif', + '/media/media/images/remove.png', + '/media/media/images/success.png', + '/media/media/images/upload.png', + '/media/media/images/uploading.png', + '/media/media/js/flashmediaelement-cdn.swf', + '/media/media/js/flashmediaelement.swf', + '/media/media/js/mediaelement-and-player.js', + '/media/media/js/mediaelement-and-player.min.js', + '/media/media/js/mediafield-mootools.js', + '/media/media/js/mediafield-mootools.min.js', + '/media/media/js/mediafield.js', + '/media/media/js/mediafield.min.js', + '/media/media/js/mediamanager.js', + '/media/media/js/mediamanager.min.js', + '/media/media/js/popup-imagemanager.js', + '/media/media/js/popup-imagemanager.min.js', + '/media/media/js/silverlightmediaelement.xap', + '/media/overrider/css/overrider.css', + '/media/overrider/js/overrider.js', + '/media/overrider/js/overrider.min.js', + '/media/plg_system_highlight/highlight.css', + '/media/plg_twofactorauth_totp/js/qrcode.js', + '/media/plg_twofactorauth_totp/js/qrcode.min.js', + '/media/plg_twofactorauth_totp/js/qrcode_SJIS.js', + '/media/plg_twofactorauth_totp/js/qrcode_UTF8.js', + '/media/system/css/adminlist.css', + '/media/system/css/jquery.Jcrop.min.css', + '/media/system/css/modal.css', + '/media/system/css/system.css', + '/media/system/js/associations-edit-uncompressed.js', + '/media/system/js/associations-edit.js', + '/media/system/js/calendar-setup-uncompressed.js', + '/media/system/js/calendar-setup.js', + '/media/system/js/calendar-uncompressed.js', + '/media/system/js/calendar.js', + '/media/system/js/caption-uncompressed.js', + '/media/system/js/caption.js', + '/media/system/js/color-field-adv-init.js', + '/media/system/js/color-field-adv-init.min.js', + '/media/system/js/color-field-init.js', + '/media/system/js/color-field-init.min.js', + '/media/system/js/combobox-uncompressed.js', + '/media/system/js/combobox.js', + '/media/system/js/core-uncompressed.js', + '/media/system/js/fields/calendar-locales/af.js', + '/media/system/js/fields/calendar-locales/ar.js', + '/media/system/js/fields/calendar-locales/bg.js', + '/media/system/js/fields/calendar-locales/bn.js', + '/media/system/js/fields/calendar-locales/bs.js', + '/media/system/js/fields/calendar-locales/ca.js', + '/media/system/js/fields/calendar-locales/cs.js', + '/media/system/js/fields/calendar-locales/cy.js', + '/media/system/js/fields/calendar-locales/da.js', + '/media/system/js/fields/calendar-locales/de.js', + '/media/system/js/fields/calendar-locales/el.js', + '/media/system/js/fields/calendar-locales/en.js', + '/media/system/js/fields/calendar-locales/es.js', + '/media/system/js/fields/calendar-locales/eu.js', + '/media/system/js/fields/calendar-locales/fa-ir.js', + '/media/system/js/fields/calendar-locales/fi.js', + '/media/system/js/fields/calendar-locales/fr.js', + '/media/system/js/fields/calendar-locales/ga.js', + '/media/system/js/fields/calendar-locales/hr.js', + '/media/system/js/fields/calendar-locales/hu.js', + '/media/system/js/fields/calendar-locales/it.js', + '/media/system/js/fields/calendar-locales/ja.js', + '/media/system/js/fields/calendar-locales/ka.js', + '/media/system/js/fields/calendar-locales/kk.js', + '/media/system/js/fields/calendar-locales/ko.js', + '/media/system/js/fields/calendar-locales/lt.js', + '/media/system/js/fields/calendar-locales/mk.js', + '/media/system/js/fields/calendar-locales/nb.js', + '/media/system/js/fields/calendar-locales/nl.js', + '/media/system/js/fields/calendar-locales/pl.js', + '/media/system/js/fields/calendar-locales/prs-af.js', + '/media/system/js/fields/calendar-locales/pt.js', + '/media/system/js/fields/calendar-locales/ru.js', + '/media/system/js/fields/calendar-locales/sk.js', + '/media/system/js/fields/calendar-locales/sl.js', + '/media/system/js/fields/calendar-locales/sr-rs.js', + '/media/system/js/fields/calendar-locales/sr-yu.js', + '/media/system/js/fields/calendar-locales/sv.js', + '/media/system/js/fields/calendar-locales/sw.js', + '/media/system/js/fields/calendar-locales/ta.js', + '/media/system/js/fields/calendar-locales/th.js', + '/media/system/js/fields/calendar-locales/uk.js', + '/media/system/js/fields/calendar-locales/zh-CN.js', + '/media/system/js/fields/calendar-locales/zh-TW.js', + '/media/system/js/frontediting-uncompressed.js', + '/media/system/js/frontediting.js', + '/media/system/js/helpsite.js', + '/media/system/js/highlighter-uncompressed.js', + '/media/system/js/highlighter.js', + '/media/system/js/html5fallback-uncompressed.js', + '/media/system/js/html5fallback.js', + '/media/system/js/jquery.Jcrop.js', + '/media/system/js/jquery.Jcrop.min.js', + '/media/system/js/keepalive-uncompressed.js', + '/media/system/js/modal-fields-uncompressed.js', + '/media/system/js/modal-fields.js', + '/media/system/js/modal-uncompressed.js', + '/media/system/js/modal.js', + '/media/system/js/moduleorder.js', + '/media/system/js/mootools-core-uncompressed.js', + '/media/system/js/mootools-core.js', + '/media/system/js/mootools-more-uncompressed.js', + '/media/system/js/mootools-more.js', + '/media/system/js/mootree-uncompressed.js', + '/media/system/js/mootree.js', + '/media/system/js/multiselect-uncompressed.js', + '/media/system/js/passwordstrength.js', + '/media/system/js/permissions-uncompressed.js', + '/media/system/js/permissions.js', + '/media/system/js/polyfill.classlist-uncompressed.js', + '/media/system/js/polyfill.classlist.js', + '/media/system/js/polyfill.event-uncompressed.js', + '/media/system/js/polyfill.event.js', + '/media/system/js/polyfill.filter-uncompressed.js', + '/media/system/js/polyfill.filter.js', + '/media/system/js/polyfill.map-uncompressed.js', + '/media/system/js/polyfill.map.js', + '/media/system/js/polyfill.xpath-uncompressed.js', + '/media/system/js/polyfill.xpath.js', + '/media/system/js/progressbar-uncompressed.js', + '/media/system/js/progressbar.js', + '/media/system/js/punycode-uncompressed.js', + '/media/system/js/punycode.js', + '/media/system/js/repeatable-uncompressed.js', + '/media/system/js/repeatable.js', + '/media/system/js/sendtestmail-uncompressed.js', + '/media/system/js/sendtestmail.js', + '/media/system/js/subform-repeatable-uncompressed.js', + '/media/system/js/subform-repeatable.js', + '/media/system/js/switcher-uncompressed.js', + '/media/system/js/switcher.js', + '/media/system/js/tabs-state-uncompressed.js', + '/media/system/js/tabs-state.js', + '/media/system/js/tabs.js', + '/media/system/js/validate-uncompressed.js', + '/media/system/js/validate.js', + '/modules/mod_articles_archive/helper.php', + '/modules/mod_articles_categories/helper.php', + '/modules/mod_articles_category/helper.php', + '/modules/mod_articles_latest/helper.php', + '/modules/mod_articles_news/helper.php', + '/modules/mod_articles_popular/helper.php', + '/modules/mod_banners/helper.php', + '/modules/mod_breadcrumbs/helper.php', + '/modules/mod_feed/helper.php', + '/modules/mod_finder/helper.php', + '/modules/mod_languages/helper.php', + '/modules/mod_login/helper.php', + '/modules/mod_menu/helper.php', + '/modules/mod_random_image/helper.php', + '/modules/mod_related_items/helper.php', + '/modules/mod_stats/helper.php', + '/modules/mod_syndicate/helper.php', + '/modules/mod_tags_popular/helper.php', + '/modules/mod_tags_similar/helper.php', + '/modules/mod_users_latest/helper.php', + '/modules/mod_whosonline/helper.php', + '/modules/mod_wrapper/helper.php', + '/plugins/authentication/gmail/gmail.php', + '/plugins/authentication/gmail/gmail.xml', + '/plugins/captcha/recaptcha/postinstall/actions.php', + '/plugins/content/confirmconsent/fields/consentbox.php', + '/plugins/editors/codemirror/fonts.php', + '/plugins/editors/codemirror/layouts/editors/codemirror/init.php', + '/plugins/editors/tinymce/field/skins.php', + '/plugins/editors/tinymce/field/tinymcebuilder.php', + '/plugins/editors/tinymce/field/uploaddirs.php', + '/plugins/editors/tinymce/form/setoptions.xml', + '/plugins/quickicon/joomlaupdate/joomlaupdate.php', + '/plugins/system/languagecode/language/en-GB/en-GB.plg_system_languagecode.ini', + '/plugins/system/languagecode/language/en-GB/en-GB.plg_system_languagecode.sys.ini', + '/plugins/system/p3p/p3p.php', + '/plugins/system/p3p/p3p.xml', + '/plugins/system/privacyconsent/field/privacy.php', + '/plugins/system/privacyconsent/privacyconsent/privacyconsent.xml', + '/plugins/system/stats/field/base.php', + '/plugins/system/stats/field/data.php', + '/plugins/system/stats/field/uniqueid.php', + '/plugins/user/profile/field/dob.php', + '/plugins/user/profile/field/tos.php', + '/plugins/user/profile/profiles/profile.xml', + '/plugins/user/terms/field/terms.php', + '/plugins/user/terms/terms/terms.xml', + '/templates/beez3/component.php', + '/templates/beez3/css/general.css', + '/templates/beez3/css/ie7only.css', + '/templates/beez3/css/ieonly.css', + '/templates/beez3/css/layout.css', + '/templates/beez3/css/nature.css', + '/templates/beez3/css/nature_rtl.css', + '/templates/beez3/css/personal.css', + '/templates/beez3/css/personal_rtl.css', + '/templates/beez3/css/position.css', + '/templates/beez3/css/print.css', + '/templates/beez3/css/red.css', + '/templates/beez3/css/template.css', + '/templates/beez3/css/template_rtl.css', + '/templates/beez3/css/turq.css', + '/templates/beez3/css/turq.less', + '/templates/beez3/error.php', + '/templates/beez3/favicon.ico', + '/templates/beez3/html/com_contact/categories/default.php', + '/templates/beez3/html/com_contact/categories/default_items.php', + '/templates/beez3/html/com_contact/category/default.php', + '/templates/beez3/html/com_contact/category/default_children.php', + '/templates/beez3/html/com_contact/category/default_items.php', + '/templates/beez3/html/com_contact/contact/default.php', + '/templates/beez3/html/com_contact/contact/default_address.php', + '/templates/beez3/html/com_contact/contact/default_articles.php', + '/templates/beez3/html/com_contact/contact/default_form.php', + '/templates/beez3/html/com_contact/contact/default_links.php', + '/templates/beez3/html/com_contact/contact/default_profile.php', + '/templates/beez3/html/com_contact/contact/default_user_custom_fields.php', + '/templates/beez3/html/com_contact/contact/encyclopedia.php', + '/templates/beez3/html/com_content/archive/default.php', + '/templates/beez3/html/com_content/archive/default_items.php', + '/templates/beez3/html/com_content/article/default.php', + '/templates/beez3/html/com_content/article/default_links.php', + '/templates/beez3/html/com_content/categories/default.php', + '/templates/beez3/html/com_content/categories/default_items.php', + '/templates/beez3/html/com_content/category/blog.php', + '/templates/beez3/html/com_content/category/blog_children.php', + '/templates/beez3/html/com_content/category/blog_item.php', + '/templates/beez3/html/com_content/category/blog_links.php', + '/templates/beez3/html/com_content/category/default.php', + '/templates/beez3/html/com_content/category/default_articles.php', + '/templates/beez3/html/com_content/category/default_children.php', + '/templates/beez3/html/com_content/featured/default.php', + '/templates/beez3/html/com_content/featured/default_item.php', + '/templates/beez3/html/com_content/featured/default_links.php', + '/templates/beez3/html/com_content/form/edit.php', + '/templates/beez3/html/com_newsfeeds/categories/default.php', + '/templates/beez3/html/com_newsfeeds/categories/default_items.php', + '/templates/beez3/html/com_newsfeeds/category/default.php', + '/templates/beez3/html/com_newsfeeds/category/default_children.php', + '/templates/beez3/html/com_newsfeeds/category/default_items.php', + '/templates/beez3/html/com_weblinks/categories/default.php', + '/templates/beez3/html/com_weblinks/categories/default_items.php', + '/templates/beez3/html/com_weblinks/category/default.php', + '/templates/beez3/html/com_weblinks/category/default_children.php', + '/templates/beez3/html/com_weblinks/category/default_items.php', + '/templates/beez3/html/com_weblinks/form/edit.php', + '/templates/beez3/html/layouts/joomla/system/message.php', + '/templates/beez3/html/mod_breadcrumbs/default.php', + '/templates/beez3/html/mod_languages/default.php', + '/templates/beez3/html/mod_login/default.php', + '/templates/beez3/html/mod_login/default_logout.php', + '/templates/beez3/html/modules.php', + '/templates/beez3/images/all_bg.gif', + '/templates/beez3/images/arrow.png', + '/templates/beez3/images/arrow2_grey.png', + '/templates/beez3/images/arrow_white_grey.png', + '/templates/beez3/images/blog_more.gif', + '/templates/beez3/images/blog_more_hover.gif', + '/templates/beez3/images/close.png', + '/templates/beez3/images/content_bg.gif', + '/templates/beez3/images/footer_bg.gif', + '/templates/beez3/images/footer_bg.png', + '/templates/beez3/images/header-bg.gif', + '/templates/beez3/images/minus.png', + '/templates/beez3/images/nature/arrow1.gif', + '/templates/beez3/images/nature/arrow1_rtl.gif', + '/templates/beez3/images/nature/arrow2.gif', + '/templates/beez3/images/nature/arrow2_grey.png', + '/templates/beez3/images/nature/arrow2_rtl.gif', + '/templates/beez3/images/nature/arrow_nav.gif', + '/templates/beez3/images/nature/arrow_small.png', + '/templates/beez3/images/nature/arrow_small_rtl.png', + '/templates/beez3/images/nature/blog_more.gif', + '/templates/beez3/images/nature/box.png', + '/templates/beez3/images/nature/box1.png', + '/templates/beez3/images/nature/grey_bg.png', + '/templates/beez3/images/nature/headingback.png', + '/templates/beez3/images/nature/karo.gif', + '/templates/beez3/images/nature/level4.png', + '/templates/beez3/images/nature/nav_level1_a.gif', + '/templates/beez3/images/nature/nav_level_1.gif', + '/templates/beez3/images/nature/pfeil.gif', + '/templates/beez3/images/nature/readmore_arrow.png', + '/templates/beez3/images/nature/searchbutton.png', + '/templates/beez3/images/nature/tabs.gif', + '/templates/beez3/images/nav_level_1.gif', + '/templates/beez3/images/news.gif', + '/templates/beez3/images/personal/arrow2_grey.jpg', + '/templates/beez3/images/personal/arrow2_grey.png', + '/templates/beez3/images/personal/bg2.png', + '/templates/beez3/images/personal/button.png', + '/templates/beez3/images/personal/dot.png', + '/templates/beez3/images/personal/ecke.gif', + '/templates/beez3/images/personal/footer.jpg', + '/templates/beez3/images/personal/grey_bg.png', + '/templates/beez3/images/personal/navi_active.png', + '/templates/beez3/images/personal/personal2.png', + '/templates/beez3/images/personal/readmore_arrow.png', + '/templates/beez3/images/personal/readmore_arrow_hover.png', + '/templates/beez3/images/personal/tabs_back.png', + '/templates/beez3/images/plus.png', + '/templates/beez3/images/req.png', + '/templates/beez3/images/slider_minus.png', + '/templates/beez3/images/slider_minus_rtl.png', + '/templates/beez3/images/slider_plus.png', + '/templates/beez3/images/slider_plus_rtl.png', + '/templates/beez3/images/system/arrow.png', + '/templates/beez3/images/system/arrow_rtl.png', + '/templates/beez3/images/system/calendar.png', + '/templates/beez3/images/system/j_button2_blank.png', + '/templates/beez3/images/system/j_button2_image.png', + '/templates/beez3/images/system/j_button2_left.png', + '/templates/beez3/images/system/j_button2_pagebreak.png', + '/templates/beez3/images/system/j_button2_readmore.png', + '/templates/beez3/images/system/notice-alert.png', + '/templates/beez3/images/system/notice-alert_rtl.png', + '/templates/beez3/images/system/notice-info.png', + '/templates/beez3/images/system/notice-info_rtl.png', + '/templates/beez3/images/system/notice-note.png', + '/templates/beez3/images/system/notice-note_rtl.png', + '/templates/beez3/images/system/selector-arrow.png', + '/templates/beez3/images/table_footer.gif', + '/templates/beez3/images/trans.gif', + '/templates/beez3/index.php', + '/templates/beez3/javascript/hide.js', + '/templates/beez3/javascript/md_stylechanger.js', + '/templates/beez3/javascript/respond.js', + '/templates/beez3/javascript/respond.src.js', + '/templates/beez3/javascript/template.js', + '/templates/beez3/jsstrings.php', + '/templates/beez3/language/en-GB/en-GB.tpl_beez3.ini', + '/templates/beez3/language/en-GB/en-GB.tpl_beez3.sys.ini', + '/templates/beez3/templateDetails.xml', + '/templates/beez3/template_preview.png', + '/templates/beez3/template_thumbnail.png', + '/templates/protostar/component.php', + '/templates/protostar/css/offline.css', + '/templates/protostar/css/template.css', + '/templates/protostar/error.php', + '/templates/protostar/favicon.ico', + '/templates/protostar/html/com_media/imageslist/default_folder.php', + '/templates/protostar/html/com_media/imageslist/default_image.php', + '/templates/protostar/html/layouts/joomla/form/field/contenthistory.php', + '/templates/protostar/html/layouts/joomla/form/field/media.php', + '/templates/protostar/html/layouts/joomla/form/field/user.php', + '/templates/protostar/html/layouts/joomla/system/message.php', + '/templates/protostar/html/modules.php', + '/templates/protostar/html/pagination.php', + '/templates/protostar/images/logo.png', + '/templates/protostar/images/system/rating_star.png', + '/templates/protostar/images/system/rating_star_blank.png', + '/templates/protostar/images/system/sort_asc.png', + '/templates/protostar/images/system/sort_desc.png', + '/templates/protostar/img/glyphicons-halflings-white.png', + '/templates/protostar/img/glyphicons-halflings.png', + '/templates/protostar/index.php', + '/templates/protostar/js/application.js', + '/templates/protostar/js/classes.js', + '/templates/protostar/js/template.js', + '/templates/protostar/language/en-GB/en-GB.tpl_protostar.ini', + '/templates/protostar/language/en-GB/en-GB.tpl_protostar.sys.ini', + '/templates/protostar/less/icomoon.less', + '/templates/protostar/less/template.less', + '/templates/protostar/less/template_rtl.less', + '/templates/protostar/less/variables.less', + '/templates/protostar/offline.php', + '/templates/protostar/templateDetails.xml', + '/templates/protostar/template_preview.png', + '/templates/protostar/template_thumbnail.png', + '/templates/system/css/system.css', + '/templates/system/css/toolbar.css', + '/templates/system/html/modules.php', + '/templates/system/images/calendar.png', + '/templates/system/images/j_button2_blank.png', + '/templates/system/images/j_button2_image.png', + '/templates/system/images/j_button2_left.png', + '/templates/system/images/j_button2_pagebreak.png', + '/templates/system/images/j_button2_readmore.png', + '/templates/system/images/j_button2_right.png', + '/templates/system/images/selector-arrow.png', + // 4.0 from Beta 1 to Beta 2 + '/administrator/components/com_finder/src/Indexer/Driver/Mysql.php', + '/administrator/components/com_finder/src/Indexer/Driver/Postgresql.php', + '/administrator/components/com_workflow/access.xml', + '/api/components/com_installer/src/Controller/LanguagesController.php', + '/api/components/com_installer/src/View/Languages/JsonapiView.php', + '/libraries/vendor/joomla/controller/LICENSE', + '/libraries/vendor/joomla/controller/src/AbstractController.php', + '/libraries/vendor/joomla/controller/src/ControllerInterface.php', + '/media/com_users/js/admin-users-user.es6.js', + '/media/com_users/js/admin-users-user.es6.min.js', + '/media/com_users/js/admin-users-user.es6.min.js.gz', + '/media/com_users/js/admin-users-user.js', + '/media/com_users/js/admin-users-user.min.js', + '/media/com_users/js/admin-users-user.min.js.gz', + // 4.0 from Beta 2 to Beta 3 + '/administrator/templates/atum/images/logo-blue.svg', + '/administrator/templates/atum/images/logo-joomla-blue.svg', + '/administrator/templates/atum/images/logo-joomla-white.svg', + '/administrator/templates/atum/images/logo.svg', + // 4.0 from Beta 3 to Beta 4 + '/components/com_config/src/Model/CmsModel.php', + // 4.0 from Beta 4 to Beta 5 + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-06-11.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-04-18.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-06-11.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-04-18.sql', + '/administrator/components/com_config/tmpl/application/default_system.php', + '/administrator/language/en-GB/plg_content_imagelazyload.sys.ini', + '/administrator/language/en-GB/plg_fields_image.ini', + '/administrator/language/en-GB/plg_fields_image.sys.ini', + '/administrator/templates/atum/scss/vendor/bootstrap/_nav.scss', + '/libraries/vendor/spomky-labs/base64url/phpstan.neon', + '/media/plg_system_webauthn/images/webauthn-black.png', + '/media/plg_system_webauthn/images/webauthn-color.png', + '/media/plg_system_webauthn/images/webauthn-white.png', + '/media/system/css/system.min.css', + '/media/system/css/system.min.css.gz', + '/plugins/content/imagelazyload/imagelazyload.php', + '/plugins/content/imagelazyload/imagelazyload.xml', + '/templates/cassiopeia/html/layouts/chromes/cardGrey.php', + '/templates/cassiopeia/html/layouts/chromes/default.php', + '/templates/cassiopeia/scss/vendor/bootstrap/_card.scss', + // 4.0 from Beta 5 to Beta 6 + '/administrator/modules/mod_multilangstatus/src/Helper/MultilangstatusAdminHelper.php', + '/administrator/templates/atum/favicon.ico', + '/libraries/vendor/nyholm/psr7/phpstan.baseline.dist', + '/libraries/vendor/spomky-labs/base64url/.php_cs.dist', + '/libraries/vendor/spomky-labs/base64url/infection.json.dist', + '/media/layouts/js/joomla/html/batch/batch-language.es6.js', + '/media/layouts/js/joomla/html/batch/batch-language.es6.min.js', + '/media/layouts/js/joomla/html/batch/batch-language.es6.min.js.gz', + '/media/layouts/js/joomla/html/batch/batch-language.js', + '/media/layouts/js/joomla/html/batch/batch-language.min.js', + '/media/layouts/js/joomla/html/batch/batch-language.min.js.gz', + '/media/plg_system_webauthn/images/webauthn-black.svg', + '/media/plg_system_webauthn/images/webauthn-white.svg', + '/media/system/js/core.es6/ajax.es6', + '/media/system/js/core.es6/customevent.es6', + '/media/system/js/core.es6/event.es6', + '/media/system/js/core.es6/form.es6', + '/media/system/js/core.es6/message.es6', + '/media/system/js/core.es6/options.es6', + '/media/system/js/core.es6/text.es6', + '/media/system/js/core.es6/token.es6', + '/media/system/js/core.es6/webcomponent.es6', + '/templates/cassiopeia/favicon.ico', + '/templates/cassiopeia/scss/_mixin.scss', + '/templates/cassiopeia/scss/_variables.scss', + '/templates/cassiopeia/scss/blocks/_demo-styling.scss', + // 4.0 from Beta 6 to Beta 7 + '/media/legacy/js/bootstrap-init.js', + '/media/legacy/js/bootstrap-init.min.js', + '/media/legacy/js/bootstrap-init.min.js.gz', + '/media/legacy/js/frontediting.js', + '/media/legacy/js/frontediting.min.js', + '/media/legacy/js/frontediting.min.js.gz', + '/media/vendor/bootstrap/js/bootstrap.bundle.js', + '/media/vendor/bootstrap/js/bootstrap.bundle.min.js', + '/media/vendor/bootstrap/js/bootstrap.bundle.min.js.gz', + '/media/vendor/bootstrap/js/bootstrap.bundle.min.js.map', + '/media/vendor/bootstrap/js/bootstrap.js', + '/media/vendor/bootstrap/js/bootstrap.min.js', + '/media/vendor/bootstrap/js/bootstrap.min.js.gz', + '/media/vendor/bootstrap/scss/_code.scss', + '/media/vendor/bootstrap/scss/_custom-forms.scss', + '/media/vendor/bootstrap/scss/_input-group.scss', + '/media/vendor/bootstrap/scss/_jumbotron.scss', + '/media/vendor/bootstrap/scss/_media.scss', + '/media/vendor/bootstrap/scss/_print.scss', + '/media/vendor/bootstrap/scss/mixins/_background-variant.scss', + '/media/vendor/bootstrap/scss/mixins/_badge.scss', + '/media/vendor/bootstrap/scss/mixins/_float.scss', + '/media/vendor/bootstrap/scss/mixins/_grid-framework.scss', + '/media/vendor/bootstrap/scss/mixins/_hover.scss', + '/media/vendor/bootstrap/scss/mixins/_nav-divider.scss', + '/media/vendor/bootstrap/scss/mixins/_screen-reader.scss', + '/media/vendor/bootstrap/scss/mixins/_size.scss', + '/media/vendor/bootstrap/scss/mixins/_table-row.scss', + '/media/vendor/bootstrap/scss/mixins/_text-emphasis.scss', + '/media/vendor/bootstrap/scss/mixins/_text-hide.scss', + '/media/vendor/bootstrap/scss/mixins/_visibility.scss', + '/media/vendor/bootstrap/scss/utilities/_align.scss', + '/media/vendor/bootstrap/scss/utilities/_background.scss', + '/media/vendor/bootstrap/scss/utilities/_borders.scss', + '/media/vendor/bootstrap/scss/utilities/_clearfix.scss', + '/media/vendor/bootstrap/scss/utilities/_display.scss', + '/media/vendor/bootstrap/scss/utilities/_embed.scss', + '/media/vendor/bootstrap/scss/utilities/_flex.scss', + '/media/vendor/bootstrap/scss/utilities/_float.scss', + '/media/vendor/bootstrap/scss/utilities/_interactions.scss', + '/media/vendor/bootstrap/scss/utilities/_overflow.scss', + '/media/vendor/bootstrap/scss/utilities/_position.scss', + '/media/vendor/bootstrap/scss/utilities/_screenreaders.scss', + '/media/vendor/bootstrap/scss/utilities/_shadows.scss', + '/media/vendor/bootstrap/scss/utilities/_sizing.scss', + '/media/vendor/bootstrap/scss/utilities/_spacing.scss', + '/media/vendor/bootstrap/scss/utilities/_stretched-link.scss', + '/media/vendor/bootstrap/scss/utilities/_text.scss', + '/media/vendor/bootstrap/scss/utilities/_visibility.scss', + '/media/vendor/skipto/css/SkipTo.css', + '/media/vendor/skipto/js/dropMenu.js', + // 4.0 from Beta 7 to RC 1 + '/administrator/components/com_admin/forms/profile.xml', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-07-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-09-22.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-09-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-10-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2016-10-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-03-18.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-04-25.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-05-31.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-06-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2017-10-10.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-02-24.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-06-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-06-26.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-07-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-08-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-09-12.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2018-10-18.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-01-05.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-01-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-02-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-03-31.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-05-05.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-06-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-07-02.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-07-14.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-07-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-08-03.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-08-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-08-21.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-14.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-23.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-24.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-25.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-26.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-27.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-09-29.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-10-13.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-10-29.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-11-07.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-11-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-08.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-22.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-02-29.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-04-11.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-04-16.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-05-21.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-09-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-09-22.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-12-08.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2020-12-19.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-02-28.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-04-11.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-04-20.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-01.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-04.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-07.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-10.sql', + '/administrator/components/com_admin/sql/updates/mysql/4.0.0-2021-05-21.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-07-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-09-22.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-09-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-10-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2016-10-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-03-18.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-04-25.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-05-31.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-06-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2017-10-10.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-02-24.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-06-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-06-26.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-07-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-08-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-09-12.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2018-10-18.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-01-05.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-01-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-02-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-03-31.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-05-05.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-06-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-02.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-14.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-07-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-08-03.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-08-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-08-21.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-14.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-23.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-24.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-25.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-26.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-27.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-09-29.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-10-13.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-10-29.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-11-07.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-11-19.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-08.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-22.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-02-29.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-04-11.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-04-16.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-05-21.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-09-19.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-09-22.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-12-08.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2020-12-19.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-02-28.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-04-11.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-04-20.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-01.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-04.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-07.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-10.sql', + '/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2021-05-21.sql', + '/administrator/components/com_admin/src/Controller/ProfileController.php', + '/administrator/components/com_admin/src/Model/ProfileModel.php', + '/administrator/components/com_admin/src/View/Profile/HtmlView.php', + '/administrator/components/com_admin/tmpl/profile/edit.php', + '/administrator/components/com_config/tmpl/application/default_ftp.php', + '/administrator/components/com_config/tmpl/application/default_ftplogin.php', + '/administrator/components/com_csp/access.xml', + '/administrator/components/com_csp/config.xml', + '/administrator/components/com_csp/csp.xml', + '/administrator/components/com_csp/forms/filter_reports.xml', + '/administrator/components/com_csp/services/provider.php', + '/administrator/components/com_csp/src/Controller/DisplayController.php', + '/administrator/components/com_csp/src/Controller/ReportsController.php', + '/administrator/components/com_csp/src/Helper/ReporterHelper.php', + '/administrator/components/com_csp/src/Model/ReportModel.php', + '/administrator/components/com_csp/src/Model/ReportsModel.php', + '/administrator/components/com_csp/src/Table/ReportTable.php', + '/administrator/components/com_csp/src/View/Reports/HtmlView.php', + '/administrator/components/com_csp/tmpl/reports/default.php', + '/administrator/components/com_csp/tmpl/reports/default.xml', + '/administrator/components/com_fields/src/Field/SubfieldstypeField.php', + '/administrator/components/com_installer/tmpl/installer/default_ftp.php', + '/administrator/components/com_joomlaupdate/src/Helper/Select.php', + '/administrator/language/en-GB/com_csp.ini', + '/administrator/language/en-GB/com_csp.sys.ini', + '/administrator/language/en-GB/plg_fields_subfields.ini', + '/administrator/language/en-GB/plg_fields_subfields.sys.ini', + '/administrator/templates/atum/Service/HTML/Atum.php', + '/components/com_csp/src/Controller/ReportController.php', + '/components/com_menus/src/Controller/DisplayController.php', + '/libraries/vendor/algo26-matthias/idna-convert/CODE_OF_CONDUCT.md', + '/libraries/vendor/algo26-matthias/idna-convert/UPGRADING.md', + '/libraries/vendor/algo26-matthias/idna-convert/docker-compose.yml', + '/libraries/vendor/beberlei/assert/phpstan-code.neon', + '/libraries/vendor/beberlei/assert/phpstan-tests.neon', + '/libraries/vendor/bin/generate-defuse-key', + '/libraries/vendor/bin/var-dump-server', + '/libraries/vendor/bin/yaml-lint', + '/libraries/vendor/brick/math/psalm-baseline.xml', + '/libraries/vendor/doctrine/inflector/phpstan.neon.dist', + '/libraries/vendor/jakeasmith/http_build_url/readme.md', + '/libraries/vendor/nyholm/psr7/src/LowercaseTrait.php', + '/libraries/vendor/ozdemirburak/iris/LICENSE.md', + '/libraries/vendor/ozdemirburak/iris/src/BaseColor.php', + '/libraries/vendor/ozdemirburak/iris/src/Color/Factory.php', + '/libraries/vendor/ozdemirburak/iris/src/Color/Hex.php', + '/libraries/vendor/ozdemirburak/iris/src/Color/Hsl.php', + '/libraries/vendor/ozdemirburak/iris/src/Color/Hsla.php', + '/libraries/vendor/ozdemirburak/iris/src/Color/Hsv.php', + '/libraries/vendor/ozdemirburak/iris/src/Color/Rgb.php', + '/libraries/vendor/ozdemirburak/iris/src/Color/Rgba.php', + '/libraries/vendor/ozdemirburak/iris/src/Exceptions/AmbiguousColorString.php', + '/libraries/vendor/ozdemirburak/iris/src/Exceptions/InvalidColorException.php', + '/libraries/vendor/ozdemirburak/iris/src/Helpers/DefinedColor.php', + '/libraries/vendor/ozdemirburak/iris/src/Traits/AlphaTrait.php', + '/libraries/vendor/ozdemirburak/iris/src/Traits/HsTrait.php', + '/libraries/vendor/ozdemirburak/iris/src/Traits/HslTrait.php', + '/libraries/vendor/ozdemirburak/iris/src/Traits/RgbTrait.php', + '/libraries/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey', + '/libraries/vendor/paragonie/random_compat/dist/random_compat.phar.pubkey.asc', + '/libraries/vendor/psr/http-factory/.pullapprove.yml', + '/libraries/vendor/spomky-labs/cbor-php/.php_cs.dist', + '/libraries/vendor/spomky-labs/cbor-php/CODE_OF_CONDUCT.md', + '/libraries/vendor/spomky-labs/cbor-php/infection.json.dist', + '/libraries/vendor/spomky-labs/cbor-php/phpstan.neon', + '/libraries/vendor/typo3/phar-stream-wrapper/_config.yml', + '/libraries/vendor/voku/portable-utf8/SUMMARY.md', + '/libraries/vendor/willdurand/negotiation/src/Negotiation/Match.php', + '/media/com_actionlogs/js/admin-actionlogs-default.es6.js', + '/media/com_actionlogs/js/admin-actionlogs-default.es6.min.js', + '/media/com_actionlogs/js/admin-actionlogs-default.es6.min.js.gz', + '/media/com_associations/js/admin-associations-default.es6.js', + '/media/com_associations/js/admin-associations-default.es6.min.js', + '/media/com_associations/js/admin-associations-default.es6.min.js.gz', + '/media/com_associations/js/admin-associations-modal.es6.js', + '/media/com_associations/js/admin-associations-modal.es6.min.js', + '/media/com_associations/js/admin-associations-modal.es6.min.js.gz', + '/media/com_associations/js/associations-edit.es6.js', + '/media/com_associations/js/associations-edit.es6.min.js', + '/media/com_associations/js/associations-edit.es6.min.js.gz', + '/media/com_banners/js/admin-banner-edit.es6.js', + '/media/com_banners/js/admin-banner-edit.es6.min.js', + '/media/com_banners/js/admin-banner-edit.es6.min.js.gz', + '/media/com_cache/js/admin-cache-default.es6.js', + '/media/com_cache/js/admin-cache-default.es6.min.js', + '/media/com_cache/js/admin-cache-default.es6.min.js.gz', + '/media/com_categories/js/shared-categories-accordion.es6.js', + '/media/com_categories/js/shared-categories-accordion.es6.min.js', + '/media/com_categories/js/shared-categories-accordion.es6.min.js.gz', + '/media/com_config/js/config-default.es6.js', + '/media/com_config/js/config-default.es6.min.js', + '/media/com_config/js/config-default.es6.min.js.gz', + '/media/com_config/js/modules-default.es6.js', + '/media/com_config/js/modules-default.es6.min.js', + '/media/com_config/js/modules-default.es6.min.js.gz', + '/media/com_config/js/templates-default.es6.js', + '/media/com_config/js/templates-default.es6.min.js', + '/media/com_config/js/templates-default.es6.min.js.gz', + '/media/com_contact/js/admin-contacts-modal.es6.js', + '/media/com_contact/js/admin-contacts-modal.es6.min.js', + '/media/com_contact/js/admin-contacts-modal.es6.min.js.gz', + '/media/com_contact/js/contacts-list.es6.js', + '/media/com_contact/js/contacts-list.es6.min.js', + '/media/com_contact/js/contacts-list.es6.min.js.gz', + '/media/com_content/js/admin-article-pagebreak.es6.js', + '/media/com_content/js/admin-article-pagebreak.es6.min.js', + '/media/com_content/js/admin-article-pagebreak.es6.min.js.gz', + '/media/com_content/js/admin-article-readmore.es6.js', + '/media/com_content/js/admin-article-readmore.es6.min.js', + '/media/com_content/js/admin-article-readmore.es6.min.js.gz', + '/media/com_content/js/admin-articles-default-batch-footer.es6.js', + '/media/com_content/js/admin-articles-default-batch-footer.es6.min.js', + '/media/com_content/js/admin-articles-default-batch-footer.es6.min.js.gz', + '/media/com_content/js/admin-articles-default-stage-footer.es6.js', + '/media/com_content/js/admin-articles-default-stage-footer.es6.min.js', + '/media/com_content/js/admin-articles-default-stage-footer.es6.min.js.gz', + '/media/com_content/js/admin-articles-modal.es6.js', + '/media/com_content/js/admin-articles-modal.es6.min.js', + '/media/com_content/js/admin-articles-modal.es6.min.js.gz', + '/media/com_content/js/articles-list.es6.js', + '/media/com_content/js/articles-list.es6.min.js', + '/media/com_content/js/articles-list.es6.min.js.gz', + '/media/com_content/js/form-edit.es6.js', + '/media/com_content/js/form-edit.es6.min.js', + '/media/com_content/js/form-edit.es6.min.js.gz', + '/media/com_contenthistory/js/admin-compare-compare.es6.js', + '/media/com_contenthistory/js/admin-compare-compare.es6.min.js', + '/media/com_contenthistory/js/admin-compare-compare.es6.min.js.gz', + '/media/com_contenthistory/js/admin-history-modal.es6.js', + '/media/com_contenthistory/js/admin-history-modal.es6.min.js', + '/media/com_contenthistory/js/admin-history-modal.es6.min.js.gz', + '/media/com_contenthistory/js/admin-history-versions.es6.js', + '/media/com_contenthistory/js/admin-history-versions.es6.min.js', + '/media/com_contenthistory/js/admin-history-versions.es6.min.js.gz', + '/media/com_cpanel/js/admin-add_module.es6.js', + '/media/com_cpanel/js/admin-add_module.es6.min.js', + '/media/com_cpanel/js/admin-add_module.es6.min.js.gz', + '/media/com_cpanel/js/admin-cpanel-default.es6.js', + '/media/com_cpanel/js/admin-cpanel-default.es6.min.js', + '/media/com_cpanel/js/admin-cpanel-default.es6.min.js.gz', + '/media/com_cpanel/js/admin-system-loader.es6.js', + '/media/com_cpanel/js/admin-system-loader.es6.min.js', + '/media/com_cpanel/js/admin-system-loader.es6.min.js.gz', + '/media/com_fields/js/admin-field-changecontext.es6.js', + '/media/com_fields/js/admin-field-changecontext.es6.min.js', + '/media/com_fields/js/admin-field-changecontext.es6.min.js.gz', + '/media/com_fields/js/admin-field-edit-modal.es6.js', + '/media/com_fields/js/admin-field-edit-modal.es6.min.js', + '/media/com_fields/js/admin-field-edit-modal.es6.min.js.gz', + '/media/com_fields/js/admin-field-edit.es6.js', + '/media/com_fields/js/admin-field-edit.es6.min.js', + '/media/com_fields/js/admin-field-edit.es6.min.js.gz', + '/media/com_fields/js/admin-field-typehaschanged.es6.js', + '/media/com_fields/js/admin-field-typehaschanged.es6.min.js', + '/media/com_fields/js/admin-field-typehaschanged.es6.min.js.gz', + '/media/com_fields/js/admin-fields-default-batch.es6.js', + '/media/com_fields/js/admin-fields-default-batch.es6.min.js', + '/media/com_fields/js/admin-fields-default-batch.es6.min.js.gz', + '/media/com_fields/js/admin-fields-modal.es6.js', + '/media/com_fields/js/admin-fields-modal.es6.min.js', + '/media/com_fields/js/admin-fields-modal.es6.min.js.gz', + '/media/com_finder/js/filters.es6.js', + '/media/com_finder/js/filters.es6.min.js', + '/media/com_finder/js/filters.es6.min.js.gz', + '/media/com_finder/js/finder-edit.es6.js', + '/media/com_finder/js/finder-edit.es6.min.js', + '/media/com_finder/js/finder-edit.es6.min.js.gz', + '/media/com_finder/js/finder.es6.js', + '/media/com_finder/js/finder.es6.min.js', + '/media/com_finder/js/finder.es6.min.js.gz', + '/media/com_finder/js/index.es6.js', + '/media/com_finder/js/index.es6.min.js', + '/media/com_finder/js/index.es6.min.js.gz', + '/media/com_finder/js/indexer.es6.js', + '/media/com_finder/js/indexer.es6.min.js', + '/media/com_finder/js/indexer.es6.min.js.gz', + '/media/com_finder/js/maps.es6.js', + '/media/com_finder/js/maps.es6.min.js', + '/media/com_finder/js/maps.es6.min.js.gz', + '/media/com_installer/js/changelog.es6.js', + '/media/com_installer/js/changelog.es6.min.js', + '/media/com_installer/js/changelog.es6.min.js.gz', + '/media/com_installer/js/installer.es6.js', + '/media/com_installer/js/installer.es6.min.js', + '/media/com_installer/js/installer.es6.min.js.gz', + '/media/com_joomlaupdate/js/admin-update-default.es6.js', + '/media/com_joomlaupdate/js/admin-update-default.es6.min.js', + '/media/com_joomlaupdate/js/admin-update-default.es6.min.js.gz', + '/media/com_languages/js/admin-language-edit-change-flag.es6.js', + '/media/com_languages/js/admin-language-edit-change-flag.es6.min.js', + '/media/com_languages/js/admin-language-edit-change-flag.es6.min.js.gz', + '/media/com_languages/js/admin-override-edit-refresh-searchstring.es6.js', + '/media/com_languages/js/admin-override-edit-refresh-searchstring.es6.min.js', + '/media/com_languages/js/admin-override-edit-refresh-searchstring.es6.min.js.gz', + '/media/com_languages/js/overrider.es6.js', + '/media/com_languages/js/overrider.es6.min.js', + '/media/com_languages/js/overrider.es6.min.js.gz', + '/media/com_mails/js/admin-email-template-edit.es6.js', + '/media/com_mails/js/admin-email-template-edit.es6.min.js', + '/media/com_mails/js/admin-email-template-edit.es6.min.js.gz', + '/media/com_media/css/mediamanager.min.css', + '/media/com_media/css/mediamanager.min.css.gz', + '/media/com_media/css/mediamanager.min.css.map', + '/media/com_media/js/edit-images.es6.js', + '/media/com_media/js/edit-images.es6.min.js', + '/media/com_media/js/mediamanager.min.js', + '/media/com_media/js/mediamanager.min.js.gz', + '/media/com_media/js/mediamanager.min.js.map', + '/media/com_menus/js/admin-item-edit.es6.js', + '/media/com_menus/js/admin-item-edit.es6.min.js', + '/media/com_menus/js/admin-item-edit.es6.min.js.gz', + '/media/com_menus/js/admin-item-edit_container.es6.js', + '/media/com_menus/js/admin-item-edit_container.es6.min.js', + '/media/com_menus/js/admin-item-edit_container.es6.min.js.gz', + '/media/com_menus/js/admin-item-edit_modules.es6.js', + '/media/com_menus/js/admin-item-edit_modules.es6.min.js', + '/media/com_menus/js/admin-item-edit_modules.es6.min.js.gz', + '/media/com_menus/js/admin-item-modal.es6.js', + '/media/com_menus/js/admin-item-modal.es6.min.js', + '/media/com_menus/js/admin-item-modal.es6.min.js.gz', + '/media/com_menus/js/admin-items-modal.es6.js', + '/media/com_menus/js/admin-items-modal.es6.min.js', + '/media/com_menus/js/admin-items-modal.es6.min.js.gz', + '/media/com_menus/js/admin-menus-default.es6.js', + '/media/com_menus/js/admin-menus-default.es6.min.js', + '/media/com_menus/js/admin-menus-default.es6.min.js.gz', + '/media/com_menus/js/default-batch-body.es6.js', + '/media/com_menus/js/default-batch-body.es6.min.js', + '/media/com_menus/js/default-batch-body.es6.min.js.gz', + '/media/com_modules/js/admin-module-edit.es6.js', + '/media/com_modules/js/admin-module-edit.es6.min.js', + '/media/com_modules/js/admin-module-edit.es6.min.js.gz', + '/media/com_modules/js/admin-module-edit_assignment.es6.js', + '/media/com_modules/js/admin-module-edit_assignment.es6.min.js', + '/media/com_modules/js/admin-module-edit_assignment.es6.min.js.gz', + '/media/com_modules/js/admin-module-search.es6.js', + '/media/com_modules/js/admin-module-search.es6.min.js', + '/media/com_modules/js/admin-module-search.es6.min.js.gz', + '/media/com_modules/js/admin-modules-modal.es6.js', + '/media/com_modules/js/admin-modules-modal.es6.min.js', + '/media/com_modules/js/admin-modules-modal.es6.min.js.gz', + '/media/com_modules/js/admin-select-modal.es6.js', + '/media/com_modules/js/admin-select-modal.es6.min.js', + '/media/com_modules/js/admin-select-modal.es6.min.js.gz', + '/media/com_tags/js/tag-default.es6.js', + '/media/com_tags/js/tag-default.es6.min.js', + '/media/com_tags/js/tag-default.es6.min.js.gz', + '/media/com_tags/js/tag-list.es6.js', + '/media/com_tags/js/tag-list.es6.min.js', + '/media/com_tags/js/tag-list.es6.min.js.gz', + '/media/com_tags/js/tags-default.es6.js', + '/media/com_tags/js/tags-default.es6.min.js', + '/media/com_tags/js/tags-default.es6.min.js.gz', + '/media/com_templates/js/admin-template-compare.es6.js', + '/media/com_templates/js/admin-template-compare.es6.min.js', + '/media/com_templates/js/admin-template-compare.es6.min.js.gz', + '/media/com_templates/js/admin-template-toggle-assignment.es6.js', + '/media/com_templates/js/admin-template-toggle-assignment.es6.min.js', + '/media/com_templates/js/admin-template-toggle-assignment.es6.min.js.gz', + '/media/com_templates/js/admin-template-toggle-switch.es6.js', + '/media/com_templates/js/admin-template-toggle-switch.es6.min.js', + '/media/com_templates/js/admin-template-toggle-switch.es6.min.js.gz', + '/media/com_templates/js/admin-templates-default.es6.js', + '/media/com_templates/js/admin-templates-default.es6.min.js', + '/media/com_templates/js/admin-templates-default.es6.min.js.gz', + '/media/com_users/js/admin-users-groups.es6.js', + '/media/com_users/js/admin-users-groups.es6.min.js', + '/media/com_users/js/admin-users-groups.es6.min.js.gz', + '/media/com_users/js/admin-users-mail.es6.js', + '/media/com_users/js/admin-users-mail.es6.min.js', + '/media/com_users/js/admin-users-mail.es6.min.js.gz', + '/media/com_users/js/two-factor-switcher.es6.js', + '/media/com_users/js/two-factor-switcher.es6.min.js', + '/media/com_users/js/two-factor-switcher.es6.min.js.gz', + '/media/com_workflow/js/admin-items-workflow-buttons.es6.js', + '/media/com_workflow/js/admin-items-workflow-buttons.es6.min.js', + '/media/com_workflow/js/admin-items-workflow-buttons.es6.min.js.gz', + '/media/com_wrapper/js/iframe-height.es6.js', + '/media/com_wrapper/js/iframe-height.es6.min.js', + '/media/com_wrapper/js/iframe-height.es6.min.js.gz', + '/media/layouts/js/joomla/form/field/category-change.es6.js', + '/media/layouts/js/joomla/form/field/category-change.es6.min.js', + '/media/layouts/js/joomla/form/field/category-change.es6.min.js.gz', + '/media/layouts/js/joomla/html/batch/batch-copymove.es6.js', + '/media/layouts/js/joomla/html/batch/batch-copymove.es6.min.js', + '/media/layouts/js/joomla/html/batch/batch-copymove.es6.min.js.gz', + '/media/legacy/js/highlighter.js', + '/media/legacy/js/highlighter.min.js', + '/media/legacy/js/highlighter.min.js.gz', + '/media/mod_login/js/admin-login.es6.js', + '/media/mod_login/js/admin-login.es6.min.js', + '/media/mod_login/js/admin-login.es6.min.js.gz', + '/media/mod_menu/js/admin-menu.es6.js', + '/media/mod_menu/js/admin-menu.es6.min.js', + '/media/mod_menu/js/admin-menu.es6.min.js.gz', + '/media/mod_menu/js/menu.es6.js', + '/media/mod_menu/js/menu.es6.min.js', + '/media/mod_menu/js/menu.es6.min.js.gz', + '/media/mod_multilangstatus/js/admin-multilangstatus.es6.js', + '/media/mod_multilangstatus/js/admin-multilangstatus.es6.min.js', + '/media/mod_multilangstatus/js/admin-multilangstatus.es6.min.js.gz', + '/media/mod_quickicon/js/quickicon.es6.js', + '/media/mod_quickicon/js/quickicon.es6.min.js', + '/media/mod_quickicon/js/quickicon.es6.min.js.gz', + '/media/mod_sampledata/js/sampledata-process.es6.js', + '/media/mod_sampledata/js/sampledata-process.es6.min.js', + '/media/mod_sampledata/js/sampledata-process.es6.min.js.gz', + '/media/plg_captcha_recaptcha/js/recaptcha.es6.js', + '/media/plg_captcha_recaptcha/js/recaptcha.es6.min.js', + '/media/plg_captcha_recaptcha/js/recaptcha.es6.min.js.gz', + '/media/plg_captcha_recaptcha_invisible/js/recaptcha.es6.js', + '/media/plg_captcha_recaptcha_invisible/js/recaptcha.es6.min.js', + '/media/plg_captcha_recaptcha_invisible/js/recaptcha.es6.min.js.gz', + '/media/plg_editors_tinymce/js/plugins/dragdrop/plugin.es6.js', + '/media/plg_editors_tinymce/js/plugins/dragdrop/plugin.es6.min.js', + '/media/plg_editors_tinymce/js/plugins/dragdrop/plugin.es6.min.js.gz', + '/media/plg_editors_tinymce/js/tinymce-builder.es6.js', + '/media/plg_editors_tinymce/js/tinymce-builder.es6.min.js', + '/media/plg_editors_tinymce/js/tinymce-builder.es6.min.js.gz', + '/media/plg_editors_tinymce/js/tinymce.es6.js', + '/media/plg_editors_tinymce/js/tinymce.es6.min.js', + '/media/plg_editors_tinymce/js/tinymce.es6.min.js.gz', + '/media/plg_installer_folderinstaller/js/folderinstaller.es6.js', + '/media/plg_installer_folderinstaller/js/folderinstaller.es6.min.js', + '/media/plg_installer_folderinstaller/js/folderinstaller.es6.min.js.gz', + '/media/plg_installer_packageinstaller/js/packageinstaller.es6.js', + '/media/plg_installer_packageinstaller/js/packageinstaller.es6.min.js', + '/media/plg_installer_packageinstaller/js/packageinstaller.es6.min.js.gz', + '/media/plg_installer_urlinstaller/js/urlinstaller.es6.js', + '/media/plg_installer_urlinstaller/js/urlinstaller.es6.min.js', + '/media/plg_installer_urlinstaller/js/urlinstaller.es6.min.js.gz', + '/media/plg_installer_webinstaller/js/client.es6.js', + '/media/plg_installer_webinstaller/js/client.es6.min.js', + '/media/plg_installer_webinstaller/js/client.es6.min.js.gz', + '/media/plg_media-action_crop/js/crop.es6.js', + '/media/plg_media-action_crop/js/crop.es6.min.js', + '/media/plg_media-action_crop/js/crop.es6.min.js.gz', + '/media/plg_media-action_resize/js/resize.es6.js', + '/media/plg_media-action_resize/js/resize.es6.min.js', + '/media/plg_media-action_resize/js/resize.es6.min.js.gz', + '/media/plg_media-action_rotate/js/rotate.es6.js', + '/media/plg_media-action_rotate/js/rotate.es6.min.js', + '/media/plg_media-action_rotate/js/rotate.es6.min.js.gz', + '/media/plg_quickicon_extensionupdate/js/extensionupdatecheck.es6.js', + '/media/plg_quickicon_extensionupdate/js/extensionupdatecheck.es6.min.js', + '/media/plg_quickicon_extensionupdate/js/extensionupdatecheck.es6.min.js.gz', + '/media/plg_quickicon_joomlaupdate/js/jupdatecheck.es6.js', + '/media/plg_quickicon_joomlaupdate/js/jupdatecheck.es6.min.js', + '/media/plg_quickicon_joomlaupdate/js/jupdatecheck.es6.min.js.gz', + '/media/plg_quickicon_overridecheck/js/overridecheck.es6.js', + '/media/plg_quickicon_overridecheck/js/overridecheck.es6.min.js', + '/media/plg_quickicon_overridecheck/js/overridecheck.es6.min.js.gz', + '/media/plg_quickicon_privacycheck/js/privacycheck.es6.js', + '/media/plg_quickicon_privacycheck/js/privacycheck.es6.min.js', + '/media/plg_quickicon_privacycheck/js/privacycheck.es6.min.js.gz', + '/media/plg_system_debug/js/debug.es6.js', + '/media/plg_system_debug/js/debug.es6.min.js', + '/media/plg_system_debug/js/debug.es6.min.js.gz', + '/media/plg_system_highlight/highlight.min.css', + '/media/plg_system_highlight/highlight.min.css.gz', + '/media/plg_system_stats/js/stats-message.es6.js', + '/media/plg_system_stats/js/stats-message.es6.min.js', + '/media/plg_system_stats/js/stats-message.es6.min.js.gz', + '/media/plg_system_stats/js/stats.es6.js', + '/media/plg_system_stats/js/stats.es6.min.js', + '/media/plg_system_stats/js/stats.es6.min.js.gz', + '/media/plg_system_webauthn/js/login.es6.js', + '/media/plg_system_webauthn/js/login.es6.min.js', + '/media/plg_system_webauthn/js/login.es6.min.js.gz', + '/media/plg_system_webauthn/js/management.es6.js', + '/media/plg_system_webauthn/js/management.es6.min.js', + '/media/plg_system_webauthn/js/management.es6.min.js.gz', + '/media/plg_user_token/js/token.es6.js', + '/media/plg_user_token/js/token.es6.min.js', + '/media/plg_user_token/js/token.es6.min.js.gz', + '/media/system/js/core.es6.js', + '/media/system/js/core.es6.min.js', + '/media/system/js/core.es6.min.js.gz', + '/media/system/js/draggable.es6.js', + '/media/system/js/draggable.es6.min.js', + '/media/system/js/draggable.es6.min.js.gz', + '/media/system/js/fields/joomla-field-color-slider.es6.js', + '/media/system/js/fields/joomla-field-color-slider.es6.min.js', + '/media/system/js/fields/joomla-field-color-slider.es6.min.js.gz', + '/media/system/js/fields/passwordstrength.es6.js', + '/media/system/js/fields/passwordstrength.es6.min.js', + '/media/system/js/fields/passwordstrength.es6.min.js.gz', + '/media/system/js/fields/passwordview.es6.js', + '/media/system/js/fields/passwordview.es6.min.js', + '/media/system/js/fields/passwordview.es6.min.js.gz', + '/media/system/js/fields/select-colour.es6.js', + '/media/system/js/fields/select-colour.es6.min.js', + '/media/system/js/fields/select-colour.es6.min.js.gz', + '/media/system/js/fields/validate.es6.js', + '/media/system/js/fields/validate.es6.min.js', + '/media/system/js/fields/validate.es6.min.js.gz', + '/media/system/js/keepalive.es6.js', + '/media/system/js/keepalive.es6.min.js', + '/media/system/js/keepalive.es6.min.js.gz', + '/media/system/js/multiselect.es6.js', + '/media/system/js/multiselect.es6.min.js', + '/media/system/js/multiselect.es6.min.js.gz', + '/media/system/js/searchtools.es6.js', + '/media/system/js/searchtools.es6.min.js', + '/media/system/js/searchtools.es6.min.js.gz', + '/media/system/js/showon.es6.js', + '/media/system/js/showon.es6.min.js', + '/media/system/js/showon.es6.min.js.gz', + '/media/templates/atum/js/template.es6.js', + '/media/templates/atum/js/template.es6.min.js', + '/media/templates/atum/js/template.es6.min.js.gz', + '/media/templates/atum/js/template.js', + '/media/templates/atum/js/template.min.js', + '/media/templates/atum/js/template.min.js.gz', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.es6.js', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.es6.min.js', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.es6.min.js.gz', + '/media/vendor/bootstrap/js/alert.es6.js', + '/media/vendor/bootstrap/js/alert.es6.min.js', + '/media/vendor/bootstrap/js/alert.es6.min.js.gz', + '/media/vendor/bootstrap/js/bootstrap.es5.js', + '/media/vendor/bootstrap/js/bootstrap.es5.min.js', + '/media/vendor/bootstrap/js/bootstrap.es5.min.js.gz', + '/media/vendor/bootstrap/js/button.es6.js', + '/media/vendor/bootstrap/js/button.es6.min.js', + '/media/vendor/bootstrap/js/button.es6.min.js.gz', + '/media/vendor/bootstrap/js/carousel.es6.js', + '/media/vendor/bootstrap/js/carousel.es6.min.js', + '/media/vendor/bootstrap/js/carousel.es6.min.js.gz', + '/media/vendor/bootstrap/js/collapse.es6.js', + '/media/vendor/bootstrap/js/collapse.es6.min.js', + '/media/vendor/bootstrap/js/collapse.es6.min.js.gz', + '/media/vendor/bootstrap/js/dom-8eef6b5f.js', + '/media/vendor/bootstrap/js/dropdown.es6.js', + '/media/vendor/bootstrap/js/dropdown.es6.min.js', + '/media/vendor/bootstrap/js/dropdown.es6.min.js.gz', + '/media/vendor/bootstrap/js/modal.es6.js', + '/media/vendor/bootstrap/js/modal.es6.min.js', + '/media/vendor/bootstrap/js/modal.es6.min.js.gz', + '/media/vendor/bootstrap/js/popover.es6.js', + '/media/vendor/bootstrap/js/popover.es6.min.js', + '/media/vendor/bootstrap/js/popover.es6.min.js.gz', + '/media/vendor/bootstrap/js/popper-5304749a.js', + '/media/vendor/bootstrap/js/scrollspy.es6.js', + '/media/vendor/bootstrap/js/scrollspy.es6.min.js', + '/media/vendor/bootstrap/js/scrollspy.es6.min.js.gz', + '/media/vendor/bootstrap/js/tab.es6.js', + '/media/vendor/bootstrap/js/tab.es6.min.js', + '/media/vendor/bootstrap/js/tab.es6.min.js.gz', + '/media/vendor/bootstrap/js/toast.es6.js', + '/media/vendor/bootstrap/js/toast.es6.min.js', + '/media/vendor/bootstrap/js/toast.es6.min.js.gz', + '/media/vendor/codemirror/lib/codemirror-ce.js', + '/media/vendor/codemirror/lib/codemirror-ce.min.js', + '/media/vendor/codemirror/lib/codemirror-ce.min.js.gz', + '/media/vendor/punycode/js/punycode.js', + '/media/vendor/punycode/js/punycode.min.js', + '/media/vendor/punycode/js/punycode.min.js.gz', + '/media/vendor/tinymce/changelog.txt', + '/media/vendor/webcomponentsjs/js/webcomponents-ce.js', + '/media/vendor/webcomponentsjs/js/webcomponents-ce.min.js', + '/media/vendor/webcomponentsjs/js/webcomponents-ce.min.js.gz', + '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce-pf.js', + '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce-pf.min.js', + '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce-pf.min.js.gz', + '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce.js', + '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce.min.js', + '/media/vendor/webcomponentsjs/js/webcomponents-sd-ce.min.js.gz', + '/media/vendor/webcomponentsjs/js/webcomponents-sd.js', + '/media/vendor/webcomponentsjs/js/webcomponents-sd.min.js', + '/media/vendor/webcomponentsjs/js/webcomponents-sd.min.js.gz', + '/plugins/fields/subfields/params/subfields.xml', + '/plugins/fields/subfields/subfields.php', + '/plugins/fields/subfields/subfields.xml', + '/plugins/fields/subfields/tmpl/subfields.php', + '/templates/cassiopeia/images/system/rating_star.png', + '/templates/cassiopeia/images/system/rating_star_blank.png', + '/templates/cassiopeia/scss/tools/mixins/_margin.scss', + '/templates/cassiopeia/scss/tools/mixins/_visually-hidden.scss', + '/templates/system/js/error-locales.js', + // 4.0 from RC 1 to RC 2 + '/administrator/components/com_fields/tmpl/field/modal.php', + '/administrator/templates/atum/scss/pages/_com_admin.scss', + '/administrator/templates/atum/scss/pages/_com_finder.scss', + '/libraries/src/Error/JsonApi/InstallLanguageExceptionHandler.php', + '/libraries/src/MVC/Controller/Exception/InstallLanguage.php', + '/media/com_fields/js/admin-field-edit-modal-es5.js', + '/media/com_fields/js/admin-field-edit-modal-es5.min.js', + '/media/com_fields/js/admin-field-edit-modal-es5.min.js.gz', + '/media/com_fields/js/admin-field-edit-modal.js', + '/media/com_fields/js/admin-field-edit-modal.min.js', + '/media/com_fields/js/admin-field-edit-modal.min.js.gz', + // 4.0 from RC 3 to RC 4 + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default.php', + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_nodownload.php', + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_noupdate.php', + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_preupdatecheck.php', + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_reinstall.php', + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_update.php', + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_updatemefirst.php', + '/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/default_upload.php', + '/language/en-GB/com_messages.ini', + '/media/system/css/fields/joomla-image-select.css', + '/media/system/css/fields/joomla-image-select.min.css', + '/media/system/css/fields/joomla-image-select.min.css.gz', + '/media/system/js/fields/joomla-image-select-es5.js', + '/media/system/js/fields/joomla-image-select-es5.min.js', + '/media/system/js/fields/joomla-image-select-es5.min.js.gz', + '/media/system/js/fields/joomla-image-select.js', + '/media/system/js/fields/joomla-image-select.min.js', + '/media/system/js/fields/joomla-image-select.min.js.gz', + // 4.0 from RC 4 to RC 5 + '/media/system/js/fields/calendar-locales/af.min.js', + '/media/system/js/fields/calendar-locales/af.min.js.gz', + '/media/system/js/fields/calendar-locales/ar.min.js', + '/media/system/js/fields/calendar-locales/ar.min.js.gz', + '/media/system/js/fields/calendar-locales/bg.min.js', + '/media/system/js/fields/calendar-locales/bg.min.js.gz', + '/media/system/js/fields/calendar-locales/bn.min.js', + '/media/system/js/fields/calendar-locales/bn.min.js.gz', + '/media/system/js/fields/calendar-locales/bs.min.js', + '/media/system/js/fields/calendar-locales/bs.min.js.gz', + '/media/system/js/fields/calendar-locales/ca.min.js', + '/media/system/js/fields/calendar-locales/ca.min.js.gz', + '/media/system/js/fields/calendar-locales/cs.min.js', + '/media/system/js/fields/calendar-locales/cs.min.js.gz', + '/media/system/js/fields/calendar-locales/cy.min.js', + '/media/system/js/fields/calendar-locales/cy.min.js.gz', + '/media/system/js/fields/calendar-locales/da.min.js', + '/media/system/js/fields/calendar-locales/da.min.js.gz', + '/media/system/js/fields/calendar-locales/de.min.js', + '/media/system/js/fields/calendar-locales/de.min.js.gz', + '/media/system/js/fields/calendar-locales/el.min.js', + '/media/system/js/fields/calendar-locales/el.min.js.gz', + '/media/system/js/fields/calendar-locales/en.min.js', + '/media/system/js/fields/calendar-locales/en.min.js.gz', + '/media/system/js/fields/calendar-locales/es.min.js', + '/media/system/js/fields/calendar-locales/es.min.js.gz', + '/media/system/js/fields/calendar-locales/eu.min.js', + '/media/system/js/fields/calendar-locales/eu.min.js.gz', + '/media/system/js/fields/calendar-locales/fa-ir.min.js', + '/media/system/js/fields/calendar-locales/fa-ir.min.js.gz', + '/media/system/js/fields/calendar-locales/fi.min.js', + '/media/system/js/fields/calendar-locales/fi.min.js.gz', + '/media/system/js/fields/calendar-locales/fr.min.js', + '/media/system/js/fields/calendar-locales/fr.min.js.gz', + '/media/system/js/fields/calendar-locales/ga.min.js', + '/media/system/js/fields/calendar-locales/ga.min.js.gz', + '/media/system/js/fields/calendar-locales/hr.min.js', + '/media/system/js/fields/calendar-locales/hr.min.js.gz', + '/media/system/js/fields/calendar-locales/hu.min.js', + '/media/system/js/fields/calendar-locales/hu.min.js.gz', + '/media/system/js/fields/calendar-locales/it.min.js', + '/media/system/js/fields/calendar-locales/it.min.js.gz', + '/media/system/js/fields/calendar-locales/ja.min.js', + '/media/system/js/fields/calendar-locales/ja.min.js.gz', + '/media/system/js/fields/calendar-locales/ka.min.js', + '/media/system/js/fields/calendar-locales/ka.min.js.gz', + '/media/system/js/fields/calendar-locales/kk.min.js', + '/media/system/js/fields/calendar-locales/kk.min.js.gz', + '/media/system/js/fields/calendar-locales/ko.min.js', + '/media/system/js/fields/calendar-locales/ko.min.js.gz', + '/media/system/js/fields/calendar-locales/lt.min.js', + '/media/system/js/fields/calendar-locales/lt.min.js.gz', + '/media/system/js/fields/calendar-locales/mk.min.js', + '/media/system/js/fields/calendar-locales/mk.min.js.gz', + '/media/system/js/fields/calendar-locales/nb.min.js', + '/media/system/js/fields/calendar-locales/nb.min.js.gz', + '/media/system/js/fields/calendar-locales/nl.min.js', + '/media/system/js/fields/calendar-locales/nl.min.js.gz', + '/media/system/js/fields/calendar-locales/pl.min.js', + '/media/system/js/fields/calendar-locales/pl.min.js.gz', + '/media/system/js/fields/calendar-locales/prs-af.min.js', + '/media/system/js/fields/calendar-locales/prs-af.min.js.gz', + '/media/system/js/fields/calendar-locales/pt.min.js', + '/media/system/js/fields/calendar-locales/pt.min.js.gz', + '/media/system/js/fields/calendar-locales/ru.min.js', + '/media/system/js/fields/calendar-locales/ru.min.js.gz', + '/media/system/js/fields/calendar-locales/sk.min.js', + '/media/system/js/fields/calendar-locales/sk.min.js.gz', + '/media/system/js/fields/calendar-locales/sl.min.js', + '/media/system/js/fields/calendar-locales/sl.min.js.gz', + '/media/system/js/fields/calendar-locales/sr-rs.min.js', + '/media/system/js/fields/calendar-locales/sr-rs.min.js.gz', + '/media/system/js/fields/calendar-locales/sr-yu.min.js', + '/media/system/js/fields/calendar-locales/sr-yu.min.js.gz', + '/media/system/js/fields/calendar-locales/sv.min.js', + '/media/system/js/fields/calendar-locales/sv.min.js.gz', + '/media/system/js/fields/calendar-locales/sw.min.js', + '/media/system/js/fields/calendar-locales/sw.min.js.gz', + '/media/system/js/fields/calendar-locales/ta.min.js', + '/media/system/js/fields/calendar-locales/ta.min.js.gz', + '/media/system/js/fields/calendar-locales/th.min.js', + '/media/system/js/fields/calendar-locales/th.min.js.gz', + '/media/system/js/fields/calendar-locales/uk.min.js', + '/media/system/js/fields/calendar-locales/uk.min.js.gz', + '/media/system/js/fields/calendar-locales/zh-CN.min.js', + '/media/system/js/fields/calendar-locales/zh-CN.min.js.gz', + '/media/system/js/fields/calendar-locales/zh-TW.min.js', + '/media/system/js/fields/calendar-locales/zh-TW.min.js.gz', + // 4.0 from RC 5 to RC 6 + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu-es5.js', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu-es5.min.js', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu-es5.min.js.gz', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.js', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.min.js', + '/media/templates/cassiopeia/js/mod_menu/menu-metismenu.min.js.gz', + '/templates/cassiopeia/css/vendor/fontawesome-free/fontawesome.css', + '/templates/cassiopeia/css/vendor/fontawesome-free/fontawesome.min.css', + '/templates/cassiopeia/css/vendor/fontawesome-free/fontawesome.min.css.gz', + '/templates/cassiopeia/scss/vendor/fontawesome-free/fontawesome.scss', + // 4.0 from RC 6 to 4.0.0 (stable) + '/libraries/vendor/algo26-matthias/idna-convert/tests/integration/ToIdnTest.php', + '/libraries/vendor/algo26-matthias/idna-convert/tests/integration/ToUnicodeTest.php', + '/libraries/vendor/algo26-matthias/idna-convert/tests/unit/.gitkeep', + '/libraries/vendor/algo26-matthias/idna-convert/tests/unit/namePrepTest.php', + '/libraries/vendor/doctrine/inflector/docs/en/index.rst', + '/libraries/vendor/jakeasmith/http_build_url/tests/HttpBuildUrlTest.php', + '/libraries/vendor/jakeasmith/http_build_url/tests/bootstrap.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/AcceptLanguageTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/AcceptTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/BaseAcceptTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/CharsetNegotiatorTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/EncodingNegotiatorTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/LanguageNegotiatorTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/MatchTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/NegotiatorTest.php', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests/TestCase.php', + '/libraries/vendor/willdurand/negotiation/tests/bootstrap.php', + // From 4.0.2 to 4.0.3 + '/templates/cassiopeia/css/global/fonts-web_fira-sans.css', + '/templates/cassiopeia/css/global/fonts-web_fira-sans.min.css', + '/templates/cassiopeia/css/global/fonts-web_fira-sans.min.css.gz', + '/templates/cassiopeia/css/global/fonts-web_roboto+noto-sans.css', + '/templates/cassiopeia/css/global/fonts-web_roboto+noto-sans.min.css', + '/templates/cassiopeia/css/global/fonts-web_roboto+noto-sans.min.css.gz', + '/templates/cassiopeia/scss/global/fonts-web_fira-sans.scss', + '/templates/cassiopeia/scss/global/fonts-web_roboto+noto-sans.scss', + // From 4.0.3 to 4.0.4 + '/administrator/templates/atum/scss/_mixin.scss', + '/media/com_joomlaupdate/js/encryption.min.js.gz', + '/media/com_joomlaupdate/js/update.min.js.gz', + '/templates/cassiopeia/images/system/sort_asc.png', + '/templates/cassiopeia/images/system/sort_desc.png', + // From 4.0.4 to 4.0.5 + '/media/vendor/codemirror/lib/#codemirror.js#', + // From 4.0.5 to 4.0.6 + '/media/vendor/mediaelement/css/mejs-controls.png', + // From 4.0.x to 4.1.0-beta1 + '/administrator/templates/atum/css/system/searchtools/searchtools.css', + '/administrator/templates/atum/css/system/searchtools/searchtools.min.css', + '/administrator/templates/atum/css/system/searchtools/searchtools.min.css.gz', + '/administrator/templates/atum/css/template-rtl.css', + '/administrator/templates/atum/css/template-rtl.min.css', + '/administrator/templates/atum/css/template-rtl.min.css.gz', + '/administrator/templates/atum/css/template.css', + '/administrator/templates/atum/css/template.min.css', + '/administrator/templates/atum/css/template.min.css.gz', + '/administrator/templates/atum/css/vendor/awesomplete/awesomplete.css', + '/administrator/templates/atum/css/vendor/awesomplete/awesomplete.min.css', + '/administrator/templates/atum/css/vendor/awesomplete/awesomplete.min.css.gz', + '/administrator/templates/atum/css/vendor/choicesjs/choices.css', + '/administrator/templates/atum/css/vendor/choicesjs/choices.min.css', + '/administrator/templates/atum/css/vendor/choicesjs/choices.min.css.gz', + '/administrator/templates/atum/css/vendor/fontawesome-free/fontawesome.css', + '/administrator/templates/atum/css/vendor/fontawesome-free/fontawesome.min.css', + '/administrator/templates/atum/css/vendor/fontawesome-free/fontawesome.min.css.gz', + '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-alert.css', + '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-alert.min.css', + '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-alert.min.css.gz', + '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-tab.css', + '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-tab.min.css', + '/administrator/templates/atum/css/vendor/joomla-custom-elements/joomla-tab.min.css.gz', + '/administrator/templates/atum/css/vendor/minicolors/minicolors.css', + '/administrator/templates/atum/css/vendor/minicolors/minicolors.min.css', + '/administrator/templates/atum/css/vendor/minicolors/minicolors.min.css.gz', + '/administrator/templates/atum/images/joomla-pattern.svg', + '/administrator/templates/atum/images/logos/brand-large.svg', + '/administrator/templates/atum/images/logos/brand-small.svg', + '/administrator/templates/atum/images/logos/login.svg', + '/administrator/templates/atum/images/select-bg-active-rtl.svg', + '/administrator/templates/atum/images/select-bg-active.svg', + '/administrator/templates/atum/images/select-bg-rtl.svg', + '/administrator/templates/atum/images/select-bg.svg', + '/administrator/templates/atum/scss/_root.scss', + '/administrator/templates/atum/scss/_variables.scss', + '/administrator/templates/atum/scss/blocks/_alerts.scss', + '/administrator/templates/atum/scss/blocks/_edit.scss', + '/administrator/templates/atum/scss/blocks/_form.scss', + '/administrator/templates/atum/scss/blocks/_global.scss', + '/administrator/templates/atum/scss/blocks/_header.scss', + '/administrator/templates/atum/scss/blocks/_icons.scss', + '/administrator/templates/atum/scss/blocks/_iframe.scss', + '/administrator/templates/atum/scss/blocks/_layout.scss', + '/administrator/templates/atum/scss/blocks/_lists.scss', + '/administrator/templates/atum/scss/blocks/_login.scss', + '/administrator/templates/atum/scss/blocks/_modals.scss', + '/administrator/templates/atum/scss/blocks/_quickicons.scss', + '/administrator/templates/atum/scss/blocks/_sidebar-nav.scss', + '/administrator/templates/atum/scss/blocks/_sidebar.scss', + '/administrator/templates/atum/scss/blocks/_switcher.scss', + '/administrator/templates/atum/scss/blocks/_toolbar.scss', + '/administrator/templates/atum/scss/blocks/_treeselect.scss', + '/administrator/templates/atum/scss/blocks/_utilities.scss', + '/administrator/templates/atum/scss/pages/_com_config.scss', + '/administrator/templates/atum/scss/pages/_com_content.scss', + '/administrator/templates/atum/scss/pages/_com_cpanel.scss', + '/administrator/templates/atum/scss/pages/_com_joomlaupdate.scss', + '/administrator/templates/atum/scss/pages/_com_modules.scss', + '/administrator/templates/atum/scss/pages/_com_privacy.scss', + '/administrator/templates/atum/scss/pages/_com_tags.scss', + '/administrator/templates/atum/scss/pages/_com_templates.scss', + '/administrator/templates/atum/scss/pages/_com_users.scss', + '/administrator/templates/atum/scss/system/searchtools/searchtools.scss', + '/administrator/templates/atum/scss/template-rtl.scss', + '/administrator/templates/atum/scss/template.scss', + '/administrator/templates/atum/scss/vendor/_bootstrap.scss', + '/administrator/templates/atum/scss/vendor/_codemirror.scss', + '/administrator/templates/atum/scss/vendor/_dragula.scss', + '/administrator/templates/atum/scss/vendor/_tinymce.scss', + '/administrator/templates/atum/scss/vendor/awesomplete/awesomplete.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_badge.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_bootstrap-rtl.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_buttons.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_card.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_collapse.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_custom-forms.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_dropdown.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_form.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_lists.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_modal.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_pagination.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_reboot.scss', + '/administrator/templates/atum/scss/vendor/bootstrap/_table.scss', + '/administrator/templates/atum/scss/vendor/choicesjs/choices.scss', + '/administrator/templates/atum/scss/vendor/fontawesome-free/fontawesome.scss', + '/administrator/templates/atum/scss/vendor/joomla-custom-elements/joomla-alert.scss', + '/administrator/templates/atum/scss/vendor/joomla-custom-elements/joomla-tab.scss', + '/administrator/templates/atum/scss/vendor/minicolors/minicolors.scss', + '/administrator/templates/atum/template_preview.png', + '/administrator/templates/atum/template_thumbnail.png', + '/administrator/templates/system/css/error.css', + '/administrator/templates/system/css/error.min.css', + '/administrator/templates/system/css/error.min.css.gz', + '/administrator/templates/system/css/system.css', + '/administrator/templates/system/css/system.min.css', + '/administrator/templates/system/css/system.min.css.gz', + '/administrator/templates/system/images/calendar.png', + '/administrator/templates/system/scss/error.scss', + '/administrator/templates/system/scss/system.scss', + '/templates/cassiopeia/css/editor.css', + '/templates/cassiopeia/css/editor.min.css', + '/templates/cassiopeia/css/editor.min.css.gz', + '/templates/cassiopeia/css/global/colors_alternative.css', + '/templates/cassiopeia/css/global/colors_alternative.min.css', + '/templates/cassiopeia/css/global/colors_alternative.min.css.gz', + '/templates/cassiopeia/css/global/colors_standard.css', + '/templates/cassiopeia/css/global/colors_standard.min.css', + '/templates/cassiopeia/css/global/colors_standard.min.css.gz', + '/templates/cassiopeia/css/global/fonts-local_roboto.css', + '/templates/cassiopeia/css/global/fonts-local_roboto.min.css', + '/templates/cassiopeia/css/global/fonts-local_roboto.min.css.gz', + '/templates/cassiopeia/css/offline.css', + '/templates/cassiopeia/css/offline.min.css', + '/templates/cassiopeia/css/offline.min.css.gz', + '/templates/cassiopeia/css/system/searchtools/searchtools.css', + '/templates/cassiopeia/css/system/searchtools/searchtools.min.css', + '/templates/cassiopeia/css/system/searchtools/searchtools.min.css.gz', + '/templates/cassiopeia/css/template-rtl.css', + '/templates/cassiopeia/css/template-rtl.min.css', + '/templates/cassiopeia/css/template-rtl.min.css.gz', + '/templates/cassiopeia/css/template.css', + '/templates/cassiopeia/css/template.min.css', + '/templates/cassiopeia/css/template.min.css.gz', + '/templates/cassiopeia/css/vendor/choicesjs/choices.css', + '/templates/cassiopeia/css/vendor/choicesjs/choices.min.css', + '/templates/cassiopeia/css/vendor/choicesjs/choices.min.css.gz', + '/templates/cassiopeia/css/vendor/joomla-custom-elements/joomla-alert.css', + '/templates/cassiopeia/css/vendor/joomla-custom-elements/joomla-alert.min.css', + '/templates/cassiopeia/css/vendor/joomla-custom-elements/joomla-alert.min.css.gz', + '/templates/cassiopeia/images/logo.svg', + '/templates/cassiopeia/images/select-bg-active-rtl.svg', + '/templates/cassiopeia/images/select-bg-active.svg', + '/templates/cassiopeia/images/select-bg-rtl.svg', + '/templates/cassiopeia/images/select-bg.svg', + '/templates/cassiopeia/js/template.es5.js', + '/templates/cassiopeia/js/template.js', + '/templates/cassiopeia/js/template.min.js', + '/templates/cassiopeia/js/template.min.js.gz', + '/templates/cassiopeia/scss/blocks/_alerts.scss', + '/templates/cassiopeia/scss/blocks/_back-to-top.scss', + '/templates/cassiopeia/scss/blocks/_banner.scss', + '/templates/cassiopeia/scss/blocks/_css-grid.scss', + '/templates/cassiopeia/scss/blocks/_footer.scss', + '/templates/cassiopeia/scss/blocks/_form.scss', + '/templates/cassiopeia/scss/blocks/_frontend-edit.scss', + '/templates/cassiopeia/scss/blocks/_global.scss', + '/templates/cassiopeia/scss/blocks/_header.scss', + '/templates/cassiopeia/scss/blocks/_icons.scss', + '/templates/cassiopeia/scss/blocks/_iframe.scss', + '/templates/cassiopeia/scss/blocks/_layout.scss', + '/templates/cassiopeia/scss/blocks/_legacy.scss', + '/templates/cassiopeia/scss/blocks/_modals.scss', + '/templates/cassiopeia/scss/blocks/_modifiers.scss', + '/templates/cassiopeia/scss/blocks/_tags.scss', + '/templates/cassiopeia/scss/blocks/_toolbar.scss', + '/templates/cassiopeia/scss/blocks/_utilities.scss', + '/templates/cassiopeia/scss/editor.scss', + '/templates/cassiopeia/scss/global/colors_alternative.scss', + '/templates/cassiopeia/scss/global/colors_standard.scss', + '/templates/cassiopeia/scss/global/fonts-local_roboto.scss', + '/templates/cassiopeia/scss/offline.scss', + '/templates/cassiopeia/scss/system/searchtools/searchtools.scss', + '/templates/cassiopeia/scss/template-rtl.scss', + '/templates/cassiopeia/scss/template.scss', + '/templates/cassiopeia/scss/tools/_tools.scss', + '/templates/cassiopeia/scss/tools/functions/_max-width.scss', + '/templates/cassiopeia/scss/tools/variables/_variables.scss', + '/templates/cassiopeia/scss/vendor/_awesomplete.scss', + '/templates/cassiopeia/scss/vendor/_chosen.scss', + '/templates/cassiopeia/scss/vendor/_dragula.scss', + '/templates/cassiopeia/scss/vendor/_minicolors.scss', + '/templates/cassiopeia/scss/vendor/_tinymce.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_bootstrap-rtl.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_buttons.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_collapse.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_custom-forms.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_dropdown.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_forms.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_lists.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_modal.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_nav.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_pagination.scss', + '/templates/cassiopeia/scss/vendor/bootstrap/_table.scss', + '/templates/cassiopeia/scss/vendor/choicesjs/choices.scss', + '/templates/cassiopeia/scss/vendor/joomla-custom-elements/joomla-alert.scss', + '/templates/cassiopeia/scss/vendor/metismenu/_metismenu.scss', + '/templates/cassiopeia/template_preview.png', + '/templates/cassiopeia/template_thumbnail.png', + '/templates/system/css/editor.css', + '/templates/system/css/editor.min.css', + '/templates/system/css/editor.min.css.gz', + '/templates/system/css/error.css', + '/templates/system/css/error.min.css', + '/templates/system/css/error.min.css.gz', + '/templates/system/css/error_rtl.css', + '/templates/system/css/error_rtl.min.css', + '/templates/system/css/error_rtl.min.css.gz', + '/templates/system/css/general.css', + '/templates/system/css/general.min.css', + '/templates/system/css/general.min.css.gz', + '/templates/system/css/offline.css', + '/templates/system/css/offline.min.css', + '/templates/system/css/offline.min.css.gz', + '/templates/system/css/offline_rtl.css', + '/templates/system/css/offline_rtl.min.css', + '/templates/system/css/offline_rtl.min.css.gz', + '/templates/system/scss/editor.scss', + '/templates/system/scss/error.scss', + '/templates/system/scss/error_rtl.scss', + '/templates/system/scss/general.scss', + '/templates/system/scss/offline.scss', + '/templates/system/scss/offline_rtl.scss', + // From 4.1.0-beta3 to 4.1.0-rc1 + '/api/components/com_media/src/Helper/AdapterTrait.php', + // From 4.1.0 to 4.1.1 + '/libraries/vendor/tobscure/json-api/.git/HEAD', + '/libraries/vendor/tobscure/json-api/.git/ORIG_HEAD', + '/libraries/vendor/tobscure/json-api/.git/config', + '/libraries/vendor/tobscure/json-api/.git/description', + '/libraries/vendor/tobscure/json-api/.git/hooks/applypatch-msg.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/commit-msg.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/fsmonitor-watchman.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/post-update.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/pre-applypatch.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/pre-commit.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/pre-merge-commit.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/pre-push.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/pre-rebase.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/pre-receive.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/prepare-commit-msg.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/push-to-checkout.sample', + '/libraries/vendor/tobscure/json-api/.git/hooks/update.sample', + '/libraries/vendor/tobscure/json-api/.git/index', + '/libraries/vendor/tobscure/json-api/.git/info/exclude', + '/libraries/vendor/tobscure/json-api/.git/info/refs', + '/libraries/vendor/tobscure/json-api/.git/logs/HEAD', + '/libraries/vendor/tobscure/json-api/.git/logs/refs/heads/joomla-backports', + '/libraries/vendor/tobscure/json-api/.git/logs/refs/remotes/origin/HEAD', + '/libraries/vendor/tobscure/json-api/.git/objects/info/packs', + '/libraries/vendor/tobscure/json-api/.git/objects/pack/pack-51530cba04703b17f3c11b9e8458a171092cf5e3.idx', + '/libraries/vendor/tobscure/json-api/.git/objects/pack/pack-51530cba04703b17f3c11b9e8458a171092cf5e3.pack', + '/libraries/vendor/tobscure/json-api/.git/packed-refs', + '/libraries/vendor/tobscure/json-api/.git/refs/heads/joomla-backports', + '/libraries/vendor/tobscure/json-api/.git/refs/remotes/origin/HEAD', + '/libraries/vendor/tobscure/json-api/.php_cs', + '/libraries/vendor/tobscure/json-api/tests/AbstractSerializerTest.php', + '/libraries/vendor/tobscure/json-api/tests/AbstractTestCase.php', + '/libraries/vendor/tobscure/json-api/tests/CollectionTest.php', + '/libraries/vendor/tobscure/json-api/tests/DocumentTest.php', + '/libraries/vendor/tobscure/json-api/tests/ErrorHandlerTest.php', + '/libraries/vendor/tobscure/json-api/tests/Exception/Handler/FallbackExceptionHandlerTest.php', + '/libraries/vendor/tobscure/json-api/tests/Exception/Handler/InvalidParameterExceptionHandlerTest.php', + '/libraries/vendor/tobscure/json-api/tests/LinksTraitTest.php', + '/libraries/vendor/tobscure/json-api/tests/ParametersTest.php', + '/libraries/vendor/tobscure/json-api/tests/ResourceTest.php', + '/libraries/vendor/tobscure/json-api/tests/UtilTest.php', + // From 4.1.1 to 4.1.2 + '/administrator/components/com_users/src/Field/PrimaryauthprovidersField.php', + // From 4.1.2 to 4.1.3 + '/libraries/vendor/webmozart/assert/.php_cs', + // From 4.1.3 to 4.1.4 + '/libraries/vendor/maximebf/debugbar/.bowerrc', + '/libraries/vendor/maximebf/debugbar/bower.json', + '/libraries/vendor/maximebf/debugbar/build/namespaceFontAwesome.php', + '/libraries/vendor/maximebf/debugbar/demo/ajax.php', + '/libraries/vendor/maximebf/debugbar/demo/ajax_exception.php', + '/libraries/vendor/maximebf/debugbar/demo/bootstrap.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/cachecache/index.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/bootstrap.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/build.sh', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/cli-config.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/index.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/src/Demo/Product.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/monolog/index.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/build.properties', + '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/build.sh', + '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/index.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/runtime-conf.xml', + '/libraries/vendor/maximebf/debugbar/demo/bridge/propel/schema.xml', + '/libraries/vendor/maximebf/debugbar/demo/bridge/slim/index.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/swiftmailer/index.php', + '/libraries/vendor/maximebf/debugbar/demo/bridge/twig/foobar.html', + '/libraries/vendor/maximebf/debugbar/demo/bridge/twig/hello.html', + '/libraries/vendor/maximebf/debugbar/demo/bridge/twig/index.php', + '/libraries/vendor/maximebf/debugbar/demo/dump_assets.php', + '/libraries/vendor/maximebf/debugbar/demo/index.php', + '/libraries/vendor/maximebf/debugbar/demo/open.php', + '/libraries/vendor/maximebf/debugbar/demo/pdo.php', + '/libraries/vendor/maximebf/debugbar/demo/stack.php', + '/libraries/vendor/maximebf/debugbar/docs/ajax_and_stack.md', + '/libraries/vendor/maximebf/debugbar/docs/base_collectors.md', + '/libraries/vendor/maximebf/debugbar/docs/bridge_collectors.md', + '/libraries/vendor/maximebf/debugbar/docs/data_collectors.md', + '/libraries/vendor/maximebf/debugbar/docs/data_formatter.md', + '/libraries/vendor/maximebf/debugbar/docs/http_drivers.md', + '/libraries/vendor/maximebf/debugbar/docs/javascript_bar.md', + '/libraries/vendor/maximebf/debugbar/docs/manifest.json', + '/libraries/vendor/maximebf/debugbar/docs/openhandler.md', + '/libraries/vendor/maximebf/debugbar/docs/rendering.md', + '/libraries/vendor/maximebf/debugbar/docs/screenshot.png', + '/libraries/vendor/maximebf/debugbar/docs/storage.md', + '/libraries/vendor/maximebf/debugbar/docs/style.css', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/AggregatedCollectorTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/ConfigCollectorTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/MessagesCollectorTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/MockCollector.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/Propel2CollectorTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector/TimeDataCollectorTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataFormatter/DataFormatterTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataFormatter/DebugBarVarDumperTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DebugBarTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DebugBarTestCase.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/JavascriptRendererTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/MockHttpDriver.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/OpenHandlerTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/Storage/FileStorageTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/Storage/MockStorage.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/TracedStatementTest.php', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/full_init.html', + '/libraries/vendor/maximebf/debugbar/tests/bootstrap.php', + // From 4.1 to 4.2.0-beta1 + '/libraries/src/Service/Provider/ApiRouter.php', + '/libraries/vendor/nyholm/psr7/doc/final.md', + '/media/com_finder/js/index-es5.js', + '/media/com_finder/js/index-es5.min.js', + '/media/com_finder/js/index-es5.min.js.gz', + '/media/com_finder/js/index.js', + '/media/com_finder/js/index.min.js', + '/media/com_finder/js/index.min.js.gz', + '/media/com_users/js/two-factor-switcher-es5.js', + '/media/com_users/js/two-factor-switcher-es5.min.js', + '/media/com_users/js/two-factor-switcher-es5.min.js.gz', + '/media/com_users/js/two-factor-switcher.js', + '/media/com_users/js/two-factor-switcher.min.js', + '/media/com_users/js/two-factor-switcher.min.js.gz', + '/modules/mod_articles_news/mod_articles_news.php', + '/plugins/actionlog/joomla/joomla.php', + '/plugins/api-authentication/basic/basic.php', + '/plugins/api-authentication/token/token.php', + '/plugins/system/cache/cache.php', + '/plugins/twofactorauth/totp/postinstall/actions.php', + '/plugins/twofactorauth/totp/tmpl/form.php', + '/plugins/twofactorauth/totp/totp.php', + '/plugins/twofactorauth/totp/totp.xml', + '/plugins/twofactorauth/yubikey/tmpl/form.php', + '/plugins/twofactorauth/yubikey/yubikey.php', + '/plugins/twofactorauth/yubikey/yubikey.xml', + // From 4.2.0-beta1 to 4.2.0-beta2 + '/layouts/plugins/user/profile/fields/dob.php', + '/modules/mod_articles_latest/mod_articles_latest.php', + '/plugins/behaviour/taggable/taggable.php', + '/plugins/behaviour/versionable/versionable.php', + '/plugins/task/requests/requests.php', + '/plugins/task/sitestatus/sitestatus.php', + '/plugins/user/profile/src/Field/DobField.php', + ); + + $folders = array( + // From 3.10 to 4.1 + '/templates/system/images', + '/templates/system/html', + '/templates/protostar/less', + '/templates/protostar/language/en-GB', + '/templates/protostar/language', + '/templates/protostar/js', + '/templates/protostar/img', + '/templates/protostar/images/system', + '/templates/protostar/images', + '/templates/protostar/html/layouts/joomla/system', + '/templates/protostar/html/layouts/joomla/form/field', + '/templates/protostar/html/layouts/joomla/form', + '/templates/protostar/html/layouts/joomla', + '/templates/protostar/html/layouts', + '/templates/protostar/html/com_media/imageslist', + '/templates/protostar/html/com_media', + '/templates/protostar/html', + '/templates/protostar/css', + '/templates/protostar', + '/templates/beez3/language/en-GB', + '/templates/beez3/language', + '/templates/beez3/javascript', + '/templates/beez3/images/system', + '/templates/beez3/images/personal', + '/templates/beez3/images/nature', + '/templates/beez3/images', + '/templates/beez3/html/mod_login', + '/templates/beez3/html/mod_languages', + '/templates/beez3/html/mod_breadcrumbs', + '/templates/beez3/html/layouts/joomla/system', + '/templates/beez3/html/layouts/joomla', + '/templates/beez3/html/layouts', + '/templates/beez3/html/com_weblinks/form', + '/templates/beez3/html/com_weblinks/category', + '/templates/beez3/html/com_weblinks/categories', + '/templates/beez3/html/com_weblinks', + '/templates/beez3/html/com_newsfeeds/category', + '/templates/beez3/html/com_newsfeeds/categories', + '/templates/beez3/html/com_newsfeeds', + '/templates/beez3/html/com_content/form', + '/templates/beez3/html/com_content/featured', + '/templates/beez3/html/com_content/category', + '/templates/beez3/html/com_content/categories', + '/templates/beez3/html/com_content/article', + '/templates/beez3/html/com_content/archive', + '/templates/beez3/html/com_content', + '/templates/beez3/html/com_contact/contact', + '/templates/beez3/html/com_contact/category', + '/templates/beez3/html/com_contact/categories', + '/templates/beez3/html/com_contact', + '/templates/beez3/html', + '/templates/beez3/css', + '/templates/beez3', + '/plugins/user/terms/terms', + '/plugins/user/terms/field', + '/plugins/user/profile/profiles', + '/plugins/user/profile/field', + '/plugins/system/stats/field', + '/plugins/system/privacyconsent/privacyconsent', + '/plugins/system/privacyconsent/field', + '/plugins/system/p3p', + '/plugins/system/languagecode/language/en-GB', + '/plugins/system/languagecode/language', + '/plugins/editors/tinymce/form', + '/plugins/editors/tinymce/field', + '/plugins/content/confirmconsent/fields', + '/plugins/captcha/recaptcha/postinstall', + '/plugins/authentication/gmail', + '/media/plg_twofactorauth_totp/js', + '/media/plg_twofactorauth_totp', + '/media/plg_system_highlight', + '/media/overrider/js', + '/media/overrider/css', + '/media/overrider', + '/media/media/js', + '/media/media/images/mime-icon-32', + '/media/media/images/mime-icon-16', + '/media/media/images', + '/media/media/css', + '/media/media', + '/media/jui/less', + '/media/jui/js', + '/media/jui/img', + '/media/jui/images', + '/media/jui/fonts', + '/media/jui/css', + '/media/jui', + '/media/editors/tinymce/themes/modern', + '/media/editors/tinymce/themes', + '/media/editors/tinymce/templates', + '/media/editors/tinymce/skins/lightgray/img', + '/media/editors/tinymce/skins/lightgray/fonts', + '/media/editors/tinymce/skins/lightgray', + '/media/editors/tinymce/skins', + '/media/editors/tinymce/plugins/wordcount', + '/media/editors/tinymce/plugins/visualchars', + '/media/editors/tinymce/plugins/visualblocks/css', + '/media/editors/tinymce/plugins/visualblocks', + '/media/editors/tinymce/plugins/toc', + '/media/editors/tinymce/plugins/textpattern', + '/media/editors/tinymce/plugins/textcolor', + '/media/editors/tinymce/plugins/template', + '/media/editors/tinymce/plugins/table', + '/media/editors/tinymce/plugins/tabfocus', + '/media/editors/tinymce/plugins/spellchecker', + '/media/editors/tinymce/plugins/searchreplace', + '/media/editors/tinymce/plugins/save', + '/media/editors/tinymce/plugins/print', + '/media/editors/tinymce/plugins/preview', + '/media/editors/tinymce/plugins/paste', + '/media/editors/tinymce/plugins/pagebreak', + '/media/editors/tinymce/plugins/noneditable', + '/media/editors/tinymce/plugins/nonbreaking', + '/media/editors/tinymce/plugins/media', + '/media/editors/tinymce/plugins/lists', + '/media/editors/tinymce/plugins/link', + '/media/editors/tinymce/plugins/legacyoutput', + '/media/editors/tinymce/plugins/layer', + '/media/editors/tinymce/plugins/insertdatetime', + '/media/editors/tinymce/plugins/importcss', + '/media/editors/tinymce/plugins/imagetools', + '/media/editors/tinymce/plugins/image', + '/media/editors/tinymce/plugins/hr', + '/media/editors/tinymce/plugins/fullscreen', + '/media/editors/tinymce/plugins/fullpage', + '/media/editors/tinymce/plugins/example_dependency', + '/media/editors/tinymce/plugins/example', + '/media/editors/tinymce/plugins/emoticons/img', + '/media/editors/tinymce/plugins/emoticons', + '/media/editors/tinymce/plugins/directionality', + '/media/editors/tinymce/plugins/contextmenu', + '/media/editors/tinymce/plugins/colorpicker', + '/media/editors/tinymce/plugins/codesample/css', + '/media/editors/tinymce/plugins/codesample', + '/media/editors/tinymce/plugins/code', + '/media/editors/tinymce/plugins/charmap', + '/media/editors/tinymce/plugins/bbcode', + '/media/editors/tinymce/plugins/autosave', + '/media/editors/tinymce/plugins/autoresize', + '/media/editors/tinymce/plugins/autolink', + '/media/editors/tinymce/plugins/anchor', + '/media/editors/tinymce/plugins/advlist', + '/media/editors/tinymce/plugins', + '/media/editors/tinymce/langs', + '/media/editors/tinymce/js/plugins/dragdrop', + '/media/editors/tinymce/js/plugins', + '/media/editors/tinymce/js', + '/media/editors/tinymce', + '/media/editors/none/js', + '/media/editors/none', + '/media/editors/codemirror/theme', + '/media/editors/codemirror/mode/z80', + '/media/editors/codemirror/mode/yaml-frontmatter', + '/media/editors/codemirror/mode/yaml', + '/media/editors/codemirror/mode/yacas', + '/media/editors/codemirror/mode/xquery', + '/media/editors/codemirror/mode/xml', + '/media/editors/codemirror/mode/webidl', + '/media/editors/codemirror/mode/wast', + '/media/editors/codemirror/mode/vue', + '/media/editors/codemirror/mode/vhdl', + '/media/editors/codemirror/mode/verilog', + '/media/editors/codemirror/mode/velocity', + '/media/editors/codemirror/mode/vbscript', + '/media/editors/codemirror/mode/vb', + '/media/editors/codemirror/mode/twig', + '/media/editors/codemirror/mode/turtle', + '/media/editors/codemirror/mode/ttcn-cfg', + '/media/editors/codemirror/mode/ttcn', + '/media/editors/codemirror/mode/troff', + '/media/editors/codemirror/mode/tornado', + '/media/editors/codemirror/mode/toml', + '/media/editors/codemirror/mode/tiki', + '/media/editors/codemirror/mode/tiddlywiki', + '/media/editors/codemirror/mode/textile', + '/media/editors/codemirror/mode/tcl', + '/media/editors/codemirror/mode/swift', + '/media/editors/codemirror/mode/stylus', + '/media/editors/codemirror/mode/stex', + '/media/editors/codemirror/mode/sql', + '/media/editors/codemirror/mode/spreadsheet', + '/media/editors/codemirror/mode/sparql', + '/media/editors/codemirror/mode/soy', + '/media/editors/codemirror/mode/solr', + '/media/editors/codemirror/mode/smarty', + '/media/editors/codemirror/mode/smalltalk', + '/media/editors/codemirror/mode/slim', + '/media/editors/codemirror/mode/sieve', + '/media/editors/codemirror/mode/shell', + '/media/editors/codemirror/mode/scheme', + '/media/editors/codemirror/mode/sass', + '/media/editors/codemirror/mode/sas', + '/media/editors/codemirror/mode/rust', + '/media/editors/codemirror/mode/ruby', + '/media/editors/codemirror/mode/rst', + '/media/editors/codemirror/mode/rpm/changes', + '/media/editors/codemirror/mode/rpm', + '/media/editors/codemirror/mode/r', + '/media/editors/codemirror/mode/q', + '/media/editors/codemirror/mode/python', + '/media/editors/codemirror/mode/puppet', + '/media/editors/codemirror/mode/pug', + '/media/editors/codemirror/mode/protobuf', + '/media/editors/codemirror/mode/properties', + '/media/editors/codemirror/mode/powershell', + '/media/editors/codemirror/mode/pig', + '/media/editors/codemirror/mode/php', + '/media/editors/codemirror/mode/perl', + '/media/editors/codemirror/mode/pegjs', + '/media/editors/codemirror/mode/pascal', + '/media/editors/codemirror/mode/oz', + '/media/editors/codemirror/mode/octave', + '/media/editors/codemirror/mode/ntriples', + '/media/editors/codemirror/mode/nsis', + '/media/editors/codemirror/mode/nginx', + '/media/editors/codemirror/mode/mumps', + '/media/editors/codemirror/mode/mscgen', + '/media/editors/codemirror/mode/modelica', + '/media/editors/codemirror/mode/mllike', + '/media/editors/codemirror/mode/mirc', + '/media/editors/codemirror/mode/mbox', + '/media/editors/codemirror/mode/mathematica', + '/media/editors/codemirror/mode/markdown', + '/media/editors/codemirror/mode/lua', + '/media/editors/codemirror/mode/livescript', + '/media/editors/codemirror/mode/julia', + '/media/editors/codemirror/mode/jsx', + '/media/editors/codemirror/mode/jinja2', + '/media/editors/codemirror/mode/javascript', + '/media/editors/codemirror/mode/idl', + '/media/editors/codemirror/mode/http', + '/media/editors/codemirror/mode/htmlmixed', + '/media/editors/codemirror/mode/htmlembedded', + '/media/editors/codemirror/mode/haxe', + '/media/editors/codemirror/mode/haskell-literate', + '/media/editors/codemirror/mode/haskell', + '/media/editors/codemirror/mode/handlebars', + '/media/editors/codemirror/mode/haml', + '/media/editors/codemirror/mode/groovy', + '/media/editors/codemirror/mode/go', + '/media/editors/codemirror/mode/gherkin', + '/media/editors/codemirror/mode/gfm', + '/media/editors/codemirror/mode/gas', + '/media/editors/codemirror/mode/fortran', + '/media/editors/codemirror/mode/forth', + '/media/editors/codemirror/mode/fcl', + '/media/editors/codemirror/mode/factor', + '/media/editors/codemirror/mode/erlang', + '/media/editors/codemirror/mode/elm', + '/media/editors/codemirror/mode/eiffel', + '/media/editors/codemirror/mode/ecl', + '/media/editors/codemirror/mode/ebnf', + '/media/editors/codemirror/mode/dylan', + '/media/editors/codemirror/mode/dtd', + '/media/editors/codemirror/mode/dockerfile', + '/media/editors/codemirror/mode/django', + '/media/editors/codemirror/mode/diff', + '/media/editors/codemirror/mode/dart', + '/media/editors/codemirror/mode/d', + '/media/editors/codemirror/mode/cypher', + '/media/editors/codemirror/mode/css', + '/media/editors/codemirror/mode/crystal', + '/media/editors/codemirror/mode/commonlisp', + '/media/editors/codemirror/mode/coffeescript', + '/media/editors/codemirror/mode/cobol', + '/media/editors/codemirror/mode/cmake', + '/media/editors/codemirror/mode/clojure', + '/media/editors/codemirror/mode/clike', + '/media/editors/codemirror/mode/brainfuck', + '/media/editors/codemirror/mode/asterisk', + '/media/editors/codemirror/mode/asn.1', + '/media/editors/codemirror/mode/asciiarmor', + '/media/editors/codemirror/mode/apl', + '/media/editors/codemirror/mode', + '/media/editors/codemirror/lib', + '/media/editors/codemirror/keymap', + '/media/editors/codemirror/addon/wrap', + '/media/editors/codemirror/addon/tern', + '/media/editors/codemirror/addon/selection', + '/media/editors/codemirror/addon/search', + '/media/editors/codemirror/addon/scroll', + '/media/editors/codemirror/addon/runmode', + '/media/editors/codemirror/addon/mode', + '/media/editors/codemirror/addon/merge', + '/media/editors/codemirror/addon/lint', + '/media/editors/codemirror/addon/hint', + '/media/editors/codemirror/addon/fold', + '/media/editors/codemirror/addon/edit', + '/media/editors/codemirror/addon/display', + '/media/editors/codemirror/addon/dialog', + '/media/editors/codemirror/addon/comment', + '/media/editors/codemirror/addon', + '/media/editors/codemirror', + '/media/editors', + '/media/contacts/images', + '/media/contacts', + '/media/com_contenthistory/css', + '/media/cms/css', + '/media/cms', + '/libraries/vendor/symfony/polyfill-util', + '/libraries/vendor/symfony/polyfill-php71', + '/libraries/vendor/symfony/polyfill-php56', + '/libraries/vendor/symfony/polyfill-php55', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/XML/Declaration', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/XML', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Parse', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Net', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/HTTP', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Decode/HTML', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Decode', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Content/Type', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Content', + '/libraries/vendor/simplepie/simplepie/library/SimplePie/Cache', + '/libraries/vendor/simplepie/simplepie/library/SimplePie', + '/libraries/vendor/simplepie/simplepie/library', + '/libraries/vendor/simplepie/simplepie/idn', + '/libraries/vendor/simplepie/simplepie', + '/libraries/vendor/simplepie', + '/libraries/vendor/phpmailer/phpmailer/extras', + '/libraries/vendor/paragonie/random_compat/lib', + '/libraries/vendor/leafo/lessphp', + '/libraries/vendor/leafo', + '/libraries/vendor/joomla/session/Joomla/Session/Storage', + '/libraries/vendor/joomla/session/Joomla/Session', + '/libraries/vendor/joomla/session/Joomla', + '/libraries/vendor/joomla/image/src/Filter', + '/libraries/vendor/joomla/image/src', + '/libraries/vendor/joomla/image', + '/libraries/vendor/joomla/compat/src', + '/libraries/vendor/joomla/compat', + '/libraries/vendor/joomla/application/src/Cli/Output/Processor', + '/libraries/vendor/joomla/application/src/Cli/Output', + '/libraries/vendor/joomla/application/src/Cli', + '/libraries/vendor/ircmaxell/password-compat/lib', + '/libraries/vendor/ircmaxell/password-compat', + '/libraries/vendor/ircmaxell', + '/libraries/vendor/brumann/polyfill-unserialize/src', + '/libraries/vendor/brumann/polyfill-unserialize', + '/libraries/vendor/brumann', + '/libraries/src/Table/Observer', + '/libraries/src/Menu/Node', + '/libraries/src/Language/Wrapper', + '/libraries/src/Language/Stemmer', + '/libraries/src/Http/Wrapper', + '/libraries/src/Filter/Wrapper', + '/libraries/src/Filesystem/Wrapper', + '/libraries/src/Crypt/Password', + '/libraries/src/Access/Wrapper', + '/libraries/phputf8/utils', + '/libraries/phputf8/native', + '/libraries/phputf8/mbstring', + '/libraries/phputf8', + '/libraries/legacy/utilities', + '/libraries/legacy/table', + '/libraries/legacy/simplepie', + '/libraries/legacy/simplecrypt', + '/libraries/legacy/response', + '/libraries/legacy/request', + '/libraries/legacy/log', + '/libraries/legacy/form/field', + '/libraries/legacy/form', + '/libraries/legacy/exception', + '/libraries/legacy/error', + '/libraries/legacy/dispatcher', + '/libraries/legacy/database', + '/libraries/legacy/base', + '/libraries/legacy/application', + '/libraries/legacy', + '/libraries/joomla/view', + '/libraries/joomla/utilities', + '/libraries/joomla/twitter', + '/libraries/joomla/string/wrapper', + '/libraries/joomla/string', + '/libraries/joomla/session/storage', + '/libraries/joomla/session/handler', + '/libraries/joomla/session', + '/libraries/joomla/route/wrapper', + '/libraries/joomla/route', + '/libraries/joomla/openstreetmap', + '/libraries/joomla/observer/wrapper', + '/libraries/joomla/observer/updater', + '/libraries/joomla/observer', + '/libraries/joomla/observable', + '/libraries/joomla/oauth2', + '/libraries/joomla/oauth1', + '/libraries/joomla/model', + '/libraries/joomla/mediawiki', + '/libraries/joomla/linkedin', + '/libraries/joomla/keychain', + '/libraries/joomla/grid', + '/libraries/joomla/google/embed', + '/libraries/joomla/google/data/plus', + '/libraries/joomla/google/data/picasa', + '/libraries/joomla/google/data', + '/libraries/joomla/google/auth', + '/libraries/joomla/google', + '/libraries/joomla/github/package/users', + '/libraries/joomla/github/package/repositories', + '/libraries/joomla/github/package/pulls', + '/libraries/joomla/github/package/orgs', + '/libraries/joomla/github/package/issues', + '/libraries/joomla/github/package/gists', + '/libraries/joomla/github/package/data', + '/libraries/joomla/github/package/activity', + '/libraries/joomla/github/package', + '/libraries/joomla/github', + '/libraries/joomla/form/fields', + '/libraries/joomla/form', + '/libraries/joomla/facebook', + '/libraries/joomla/event', + '/libraries/joomla/database/query', + '/libraries/joomla/database/iterator', + '/libraries/joomla/database/importer', + '/libraries/joomla/database/exporter', + '/libraries/joomla/database/exception', + '/libraries/joomla/database/driver', + '/libraries/joomla/database', + '/libraries/joomla/controller', + '/libraries/joomla/archive/wrapper', + '/libraries/joomla/archive', + '/libraries/joomla/application/web/router', + '/libraries/joomla/application/web', + '/libraries/joomla/application', + '/libraries/joomla', + '/libraries/idna_convert', + '/libraries/fof/view', + '/libraries/fof/utils/update', + '/libraries/fof/utils/timer', + '/libraries/fof/utils/phpfunc', + '/libraries/fof/utils/observable', + '/libraries/fof/utils/object', + '/libraries/fof/utils/ip', + '/libraries/fof/utils/installscript', + '/libraries/fof/utils/ini', + '/libraries/fof/utils/filescheck', + '/libraries/fof/utils/config', + '/libraries/fof/utils/cache', + '/libraries/fof/utils/array', + '/libraries/fof/utils', + '/libraries/fof/toolbar', + '/libraries/fof/template', + '/libraries/fof/table/dispatcher', + '/libraries/fof/table/behavior', + '/libraries/fof/table', + '/libraries/fof/string', + '/libraries/fof/render', + '/libraries/fof/query', + '/libraries/fof/platform/filesystem', + '/libraries/fof/platform', + '/libraries/fof/model/field', + '/libraries/fof/model/dispatcher', + '/libraries/fof/model/behavior', + '/libraries/fof/model', + '/libraries/fof/less/parser', + '/libraries/fof/less/formatter', + '/libraries/fof/less', + '/libraries/fof/layout', + '/libraries/fof/integration/joomla/filesystem', + '/libraries/fof/integration/joomla', + '/libraries/fof/integration', + '/libraries/fof/input/jinput', + '/libraries/fof/input', + '/libraries/fof/inflector', + '/libraries/fof/hal/render', + '/libraries/fof/hal', + '/libraries/fof/form/header', + '/libraries/fof/form/field', + '/libraries/fof/form', + '/libraries/fof/encrypt/aes', + '/libraries/fof/encrypt', + '/libraries/fof/download/adapter', + '/libraries/fof/download', + '/libraries/fof/dispatcher', + '/libraries/fof/database/query', + '/libraries/fof/database/iterator', + '/libraries/fof/database/driver', + '/libraries/fof/database', + '/libraries/fof/controller', + '/libraries/fof/config/domain', + '/libraries/fof/config', + '/libraries/fof/autoloader', + '/libraries/fof', + '/libraries/cms/less/formatter', + '/libraries/cms/less', + '/libraries/cms/html/language/en-GB', + '/libraries/cms/html/language', + '/libraries/cms/html', + '/libraries/cms/class', + '/libraries/cms', + '/layouts/libraries/cms/html/bootstrap', + '/layouts/libraries/cms/html', + '/layouts/libraries/cms', + '/layouts/joomla/tinymce/buttons', + '/layouts/joomla/modal', + '/layouts/joomla/html/formbehavior', + '/components/com_wrapper/views/wrapper/tmpl', + '/components/com_wrapper/views/wrapper', + '/components/com_wrapper/views', + '/components/com_users/views/reset/tmpl', + '/components/com_users/views/reset', + '/components/com_users/views/remind/tmpl', + '/components/com_users/views/remind', + '/components/com_users/views/registration/tmpl', + '/components/com_users/views/registration', + '/components/com_users/views/profile/tmpl', + '/components/com_users/views/profile', + '/components/com_users/views/login/tmpl', + '/components/com_users/views/login', + '/components/com_users/views', + '/components/com_users/models/rules', + '/components/com_users/models/forms', + '/components/com_users/models', + '/components/com_users/layouts/joomla/form', + '/components/com_users/layouts/joomla', + '/components/com_users/layouts', + '/components/com_users/helpers/html', + '/components/com_users/helpers', + '/components/com_users/controllers', + '/components/com_tags/views/tags/tmpl', + '/components/com_tags/views/tags', + '/components/com_tags/views/tag/tmpl', + '/components/com_tags/views/tag', + '/components/com_tags/views', + '/components/com_tags/models', + '/components/com_tags/controllers', + '/components/com_privacy/views/request/tmpl', + '/components/com_privacy/views/request', + '/components/com_privacy/views/remind/tmpl', + '/components/com_privacy/views/remind', + '/components/com_privacy/views/confirm/tmpl', + '/components/com_privacy/views/confirm', + '/components/com_privacy/views', + '/components/com_privacy/models/forms', + '/components/com_privacy/models', + '/components/com_privacy/controllers', + '/components/com_newsfeeds/views/newsfeed/tmpl', + '/components/com_newsfeeds/views/newsfeed', + '/components/com_newsfeeds/views/category/tmpl', + '/components/com_newsfeeds/views/category', + '/components/com_newsfeeds/views/categories/tmpl', + '/components/com_newsfeeds/views/categories', + '/components/com_newsfeeds/views', + '/components/com_newsfeeds/models', + '/components/com_modules/models/forms', + '/components/com_modules/models', + '/components/com_menus/models/forms', + '/components/com_menus/models', + '/components/com_mailto/views/sent/tmpl', + '/components/com_mailto/views/sent', + '/components/com_mailto/views/mailto/tmpl', + '/components/com_mailto/views/mailto', + '/components/com_mailto/views', + '/components/com_mailto/models/forms', + '/components/com_mailto/models', + '/components/com_mailto/helpers', + '/components/com_mailto', + '/components/com_finder/views/search/tmpl', + '/components/com_finder/views/search', + '/components/com_finder/views', + '/components/com_finder/models', + '/components/com_finder/helpers/html', + '/components/com_finder/controllers', + '/components/com_fields/models/forms', + '/components/com_fields/models', + '/components/com_content/views/form/tmpl', + '/components/com_content/views/form', + '/components/com_content/views/featured/tmpl', + '/components/com_content/views/featured', + '/components/com_content/views/category/tmpl', + '/components/com_content/views/category', + '/components/com_content/views/categories/tmpl', + '/components/com_content/views/categories', + '/components/com_content/views/article/tmpl', + '/components/com_content/views/article', + '/components/com_content/views/archive/tmpl', + '/components/com_content/views/archive', + '/components/com_content/views', + '/components/com_content/models/forms', + '/components/com_content/models', + '/components/com_content/controllers', + '/components/com_contact/views/featured/tmpl', + '/components/com_contact/views/featured', + '/components/com_contact/views/contact/tmpl', + '/components/com_contact/views/contact', + '/components/com_contact/views/category/tmpl', + '/components/com_contact/views/category', + '/components/com_contact/views/categories/tmpl', + '/components/com_contact/views/categories', + '/components/com_contact/views', + '/components/com_contact/models/rules', + '/components/com_contact/models/forms', + '/components/com_contact/models', + '/components/com_contact/layouts/joomla/form', + '/components/com_contact/layouts/joomla', + '/components/com_contact/controllers', + '/components/com_config/view/templates/tmpl', + '/components/com_config/view/templates', + '/components/com_config/view/modules/tmpl', + '/components/com_config/view/modules', + '/components/com_config/view/config/tmpl', + '/components/com_config/view/config', + '/components/com_config/view/cms', + '/components/com_config/view', + '/components/com_config/model/form', + '/components/com_config/model', + '/components/com_config/controller/templates', + '/components/com_config/controller/modules', + '/components/com_config/controller/config', + '/components/com_config/controller', + '/components/com_banners/models', + '/components/com_banners/helpers', + '/administrator/templates/system/html', + '/administrator/templates/isis/less/pages', + '/administrator/templates/isis/less/bootstrap', + '/administrator/templates/isis/less/blocks', + '/administrator/templates/isis/less', + '/administrator/templates/isis/language/en-GB', + '/administrator/templates/isis/language', + '/administrator/templates/isis/js', + '/administrator/templates/isis/img', + '/administrator/templates/isis/images/system', + '/administrator/templates/isis/images/admin', + '/administrator/templates/isis/images', + '/administrator/templates/isis/html/mod_version', + '/administrator/templates/isis/html/layouts/joomla/toolbar', + '/administrator/templates/isis/html/layouts/joomla/system', + '/administrator/templates/isis/html/layouts/joomla/pagination', + '/administrator/templates/isis/html/layouts/joomla/form/field', + '/administrator/templates/isis/html/layouts/joomla/form', + '/administrator/templates/isis/html/layouts/joomla', + '/administrator/templates/isis/html/layouts', + '/administrator/templates/isis/html/com_media/medialist', + '/administrator/templates/isis/html/com_media/imageslist', + '/administrator/templates/isis/html/com_media', + '/administrator/templates/isis/html', + '/administrator/templates/isis/css', + '/administrator/templates/isis', + '/administrator/templates/hathor/postinstall', + '/administrator/templates/hathor/less', + '/administrator/templates/hathor/language/en-GB', + '/administrator/templates/hathor/language', + '/administrator/templates/hathor/js', + '/administrator/templates/hathor/images/toolbar', + '/administrator/templates/hathor/images/system', + '/administrator/templates/hathor/images/menu', + '/administrator/templates/hathor/images/header', + '/administrator/templates/hathor/images/admin', + '/administrator/templates/hathor/images', + '/administrator/templates/hathor/html/mod_quickicon', + '/administrator/templates/hathor/html/mod_login', + '/administrator/templates/hathor/html/layouts/plugins/user/profile/fields', + '/administrator/templates/hathor/html/layouts/plugins/user/profile', + '/administrator/templates/hathor/html/layouts/plugins/user', + '/administrator/templates/hathor/html/layouts/plugins', + '/administrator/templates/hathor/html/layouts/joomla/toolbar', + '/administrator/templates/hathor/html/layouts/joomla/sidebars', + '/administrator/templates/hathor/html/layouts/joomla/quickicons', + '/administrator/templates/hathor/html/layouts/joomla/edit', + '/administrator/templates/hathor/html/layouts/joomla', + '/administrator/templates/hathor/html/layouts/com_modules/toolbar', + '/administrator/templates/hathor/html/layouts/com_modules', + '/administrator/templates/hathor/html/layouts/com_messages/toolbar', + '/administrator/templates/hathor/html/layouts/com_messages', + '/administrator/templates/hathor/html/layouts/com_media/toolbar', + '/administrator/templates/hathor/html/layouts/com_media', + '/administrator/templates/hathor/html/layouts', + '/administrator/templates/hathor/html/com_weblinks/weblinks', + '/administrator/templates/hathor/html/com_weblinks/weblink', + '/administrator/templates/hathor/html/com_weblinks', + '/administrator/templates/hathor/html/com_users/users', + '/administrator/templates/hathor/html/com_users/user', + '/administrator/templates/hathor/html/com_users/notes', + '/administrator/templates/hathor/html/com_users/note', + '/administrator/templates/hathor/html/com_users/levels', + '/administrator/templates/hathor/html/com_users/groups', + '/administrator/templates/hathor/html/com_users/debuguser', + '/administrator/templates/hathor/html/com_users/debuggroup', + '/administrator/templates/hathor/html/com_users', + '/administrator/templates/hathor/html/com_templates/templates', + '/administrator/templates/hathor/html/com_templates/template', + '/administrator/templates/hathor/html/com_templates/styles', + '/administrator/templates/hathor/html/com_templates/style', + '/administrator/templates/hathor/html/com_templates', + '/administrator/templates/hathor/html/com_tags/tags', + '/administrator/templates/hathor/html/com_tags/tag', + '/administrator/templates/hathor/html/com_tags', + '/administrator/templates/hathor/html/com_search/searches', + '/administrator/templates/hathor/html/com_search', + '/administrator/templates/hathor/html/com_redirect/links', + '/administrator/templates/hathor/html/com_redirect', + '/administrator/templates/hathor/html/com_postinstall/messages', + '/administrator/templates/hathor/html/com_postinstall', + '/administrator/templates/hathor/html/com_plugins/plugins', + '/administrator/templates/hathor/html/com_plugins/plugin', + '/administrator/templates/hathor/html/com_plugins', + '/administrator/templates/hathor/html/com_newsfeeds/newsfeeds', + '/administrator/templates/hathor/html/com_newsfeeds/newsfeed', + '/administrator/templates/hathor/html/com_newsfeeds', + '/administrator/templates/hathor/html/com_modules/positions', + '/administrator/templates/hathor/html/com_modules/modules', + '/administrator/templates/hathor/html/com_modules/module', + '/administrator/templates/hathor/html/com_modules', + '/administrator/templates/hathor/html/com_messages/messages', + '/administrator/templates/hathor/html/com_messages/message', + '/administrator/templates/hathor/html/com_messages', + '/administrator/templates/hathor/html/com_menus/menutypes', + '/administrator/templates/hathor/html/com_menus/menus', + '/administrator/templates/hathor/html/com_menus/menu', + '/administrator/templates/hathor/html/com_menus/items', + '/administrator/templates/hathor/html/com_menus/item', + '/administrator/templates/hathor/html/com_menus', + '/administrator/templates/hathor/html/com_languages/overrides', + '/administrator/templates/hathor/html/com_languages/languages', + '/administrator/templates/hathor/html/com_languages/installed', + '/administrator/templates/hathor/html/com_languages', + '/administrator/templates/hathor/html/com_joomlaupdate/default', + '/administrator/templates/hathor/html/com_joomlaupdate', + '/administrator/templates/hathor/html/com_installer/warnings', + '/administrator/templates/hathor/html/com_installer/update', + '/administrator/templates/hathor/html/com_installer/manage', + '/administrator/templates/hathor/html/com_installer/languages', + '/administrator/templates/hathor/html/com_installer/install', + '/administrator/templates/hathor/html/com_installer/discover', + '/administrator/templates/hathor/html/com_installer/default', + '/administrator/templates/hathor/html/com_installer/database', + '/administrator/templates/hathor/html/com_installer', + '/administrator/templates/hathor/html/com_finder/maps', + '/administrator/templates/hathor/html/com_finder/index', + '/administrator/templates/hathor/html/com_finder/filters', + '/administrator/templates/hathor/html/com_finder', + '/administrator/templates/hathor/html/com_fields/groups', + '/administrator/templates/hathor/html/com_fields/group', + '/administrator/templates/hathor/html/com_fields/fields', + '/administrator/templates/hathor/html/com_fields/field', + '/administrator/templates/hathor/html/com_fields', + '/administrator/templates/hathor/html/com_cpanel/cpanel', + '/administrator/templates/hathor/html/com_cpanel', + '/administrator/templates/hathor/html/com_contenthistory/history', + '/administrator/templates/hathor/html/com_contenthistory', + '/administrator/templates/hathor/html/com_content/featured', + '/administrator/templates/hathor/html/com_content/articles', + '/administrator/templates/hathor/html/com_content/article', + '/administrator/templates/hathor/html/com_content', + '/administrator/templates/hathor/html/com_contact/contacts', + '/administrator/templates/hathor/html/com_contact/contact', + '/administrator/templates/hathor/html/com_contact', + '/administrator/templates/hathor/html/com_config/component', + '/administrator/templates/hathor/html/com_config/application', + '/administrator/templates/hathor/html/com_config', + '/administrator/templates/hathor/html/com_checkin/checkin', + '/administrator/templates/hathor/html/com_checkin', + '/administrator/templates/hathor/html/com_categories/category', + '/administrator/templates/hathor/html/com_categories/categories', + '/administrator/templates/hathor/html/com_categories', + '/administrator/templates/hathor/html/com_cache/purge', + '/administrator/templates/hathor/html/com_cache/cache', + '/administrator/templates/hathor/html/com_cache', + '/administrator/templates/hathor/html/com_banners/tracks', + '/administrator/templates/hathor/html/com_banners/download', + '/administrator/templates/hathor/html/com_banners/clients', + '/administrator/templates/hathor/html/com_banners/client', + '/administrator/templates/hathor/html/com_banners/banners', + '/administrator/templates/hathor/html/com_banners/banner', + '/administrator/templates/hathor/html/com_banners', + '/administrator/templates/hathor/html/com_associations/associations', + '/administrator/templates/hathor/html/com_associations', + '/administrator/templates/hathor/html/com_admin/sysinfo', + '/administrator/templates/hathor/html/com_admin/profile', + '/administrator/templates/hathor/html/com_admin/help', + '/administrator/templates/hathor/html/com_admin', + '/administrator/templates/hathor/html', + '/administrator/templates/hathor/css', + '/administrator/templates/hathor', + '/administrator/modules/mod_version/language/en-GB', + '/administrator/modules/mod_version/language', + '/administrator/modules/mod_status/tmpl', + '/administrator/modules/mod_status', + '/administrator/modules/mod_stats_admin/language', + '/administrator/modules/mod_multilangstatus/language/en-GB', + '/administrator/modules/mod_multilangstatus/language', + '/administrator/components/com_users/views/users/tmpl', + '/administrator/components/com_users/views/users', + '/administrator/components/com_users/views/user/tmpl', + '/administrator/components/com_users/views/user', + '/administrator/components/com_users/views/notes/tmpl', + '/administrator/components/com_users/views/notes', + '/administrator/components/com_users/views/note/tmpl', + '/administrator/components/com_users/views/note', + '/administrator/components/com_users/views/mail/tmpl', + '/administrator/components/com_users/views/mail', + '/administrator/components/com_users/views/levels/tmpl', + '/administrator/components/com_users/views/levels', + '/administrator/components/com_users/views/level/tmpl', + '/administrator/components/com_users/views/level', + '/administrator/components/com_users/views/groups/tmpl', + '/administrator/components/com_users/views/groups', + '/administrator/components/com_users/views/group/tmpl', + '/administrator/components/com_users/views/group', + '/administrator/components/com_users/views/debuguser/tmpl', + '/administrator/components/com_users/views/debuguser', + '/administrator/components/com_users/views/debuggroup/tmpl', + '/administrator/components/com_users/views/debuggroup', + '/administrator/components/com_users/views', + '/administrator/components/com_users/tables', + '/administrator/components/com_users/models/forms/fields', + '/administrator/components/com_users/models/forms', + '/administrator/components/com_users/models/fields', + '/administrator/components/com_users/models', + '/administrator/components/com_users/helpers/html', + '/administrator/components/com_users/controllers', + '/administrator/components/com_templates/views/templates/tmpl', + '/administrator/components/com_templates/views/templates', + '/administrator/components/com_templates/views/template/tmpl', + '/administrator/components/com_templates/views/template', + '/administrator/components/com_templates/views/styles/tmpl', + '/administrator/components/com_templates/views/styles', + '/administrator/components/com_templates/views/style/tmpl', + '/administrator/components/com_templates/views/style', + '/administrator/components/com_templates/views', + '/administrator/components/com_templates/tables', + '/administrator/components/com_templates/models/forms', + '/administrator/components/com_templates/models/fields', + '/administrator/components/com_templates/models', + '/administrator/components/com_templates/helpers/html', + '/administrator/components/com_templates/controllers', + '/administrator/components/com_tags/views/tags/tmpl', + '/administrator/components/com_tags/views/tags', + '/administrator/components/com_tags/views/tag/tmpl', + '/administrator/components/com_tags/views/tag', + '/administrator/components/com_tags/views', + '/administrator/components/com_tags/tables', + '/administrator/components/com_tags/models/forms', + '/administrator/components/com_tags/models', + '/administrator/components/com_tags/helpers', + '/administrator/components/com_tags/controllers', + '/administrator/components/com_redirect/views/links/tmpl', + '/administrator/components/com_redirect/views/links', + '/administrator/components/com_redirect/views/link/tmpl', + '/administrator/components/com_redirect/views/link', + '/administrator/components/com_redirect/views', + '/administrator/components/com_redirect/tables', + '/administrator/components/com_redirect/models/forms', + '/administrator/components/com_redirect/models/fields', + '/administrator/components/com_redirect/models', + '/administrator/components/com_redirect/helpers/html', + '/administrator/components/com_redirect/controllers', + '/administrator/components/com_privacy/views/requests/tmpl', + '/administrator/components/com_privacy/views/requests', + '/administrator/components/com_privacy/views/request/tmpl', + '/administrator/components/com_privacy/views/request', + '/administrator/components/com_privacy/views/export', + '/administrator/components/com_privacy/views/dashboard/tmpl', + '/administrator/components/com_privacy/views/dashboard', + '/administrator/components/com_privacy/views/consents/tmpl', + '/administrator/components/com_privacy/views/consents', + '/administrator/components/com_privacy/views/capabilities/tmpl', + '/administrator/components/com_privacy/views/capabilities', + '/administrator/components/com_privacy/views', + '/administrator/components/com_privacy/tables', + '/administrator/components/com_privacy/models/forms', + '/administrator/components/com_privacy/models/fields', + '/administrator/components/com_privacy/models', + '/administrator/components/com_privacy/helpers/removal', + '/administrator/components/com_privacy/helpers/html', + '/administrator/components/com_privacy/helpers/export', + '/administrator/components/com_privacy/helpers', + '/administrator/components/com_privacy/controllers', + '/administrator/components/com_postinstall/views/messages/tmpl', + '/administrator/components/com_postinstall/views/messages', + '/administrator/components/com_postinstall/views', + '/administrator/components/com_postinstall/models', + '/administrator/components/com_postinstall/controllers', + '/administrator/components/com_plugins/views/plugins/tmpl', + '/administrator/components/com_plugins/views/plugins', + '/administrator/components/com_plugins/views/plugin/tmpl', + '/administrator/components/com_plugins/views/plugin', + '/administrator/components/com_plugins/views', + '/administrator/components/com_plugins/models/forms', + '/administrator/components/com_plugins/models/fields', + '/administrator/components/com_plugins/models', + '/administrator/components/com_plugins/controllers', + '/administrator/components/com_newsfeeds/views/newsfeeds/tmpl', + '/administrator/components/com_newsfeeds/views/newsfeeds', + '/administrator/components/com_newsfeeds/views/newsfeed/tmpl', + '/administrator/components/com_newsfeeds/views/newsfeed', + '/administrator/components/com_newsfeeds/views', + '/administrator/components/com_newsfeeds/tables', + '/administrator/components/com_newsfeeds/models/forms', + '/administrator/components/com_newsfeeds/models/fields/modal', + '/administrator/components/com_newsfeeds/models/fields', + '/administrator/components/com_newsfeeds/models', + '/administrator/components/com_newsfeeds/helpers/html', + '/administrator/components/com_newsfeeds/controllers', + '/administrator/components/com_modules/views/select/tmpl', + '/administrator/components/com_modules/views/select', + '/administrator/components/com_modules/views/preview/tmpl', + '/administrator/components/com_modules/views/preview', + '/administrator/components/com_modules/views/positions/tmpl', + '/administrator/components/com_modules/views/positions', + '/administrator/components/com_modules/views/modules/tmpl', + '/administrator/components/com_modules/views/modules', + '/administrator/components/com_modules/views/module/tmpl', + '/administrator/components/com_modules/views/module', + '/administrator/components/com_modules/views', + '/administrator/components/com_modules/models/forms', + '/administrator/components/com_modules/models/fields', + '/administrator/components/com_modules/models', + '/administrator/components/com_modules/helpers/html', + '/administrator/components/com_modules/controllers', + '/administrator/components/com_messages/views/messages/tmpl', + '/administrator/components/com_messages/views/messages', + '/administrator/components/com_messages/views/message/tmpl', + '/administrator/components/com_messages/views/message', + '/administrator/components/com_messages/views/config/tmpl', + '/administrator/components/com_messages/views/config', + '/administrator/components/com_messages/views', + '/administrator/components/com_messages/tables', + '/administrator/components/com_messages/models/forms', + '/administrator/components/com_messages/models/fields', + '/administrator/components/com_messages/models', + '/administrator/components/com_messages/helpers/html', + '/administrator/components/com_messages/helpers', + '/administrator/components/com_messages/controllers', + '/administrator/components/com_menus/views/menutypes/tmpl', + '/administrator/components/com_menus/views/menutypes', + '/administrator/components/com_menus/views/menus/tmpl', + '/administrator/components/com_menus/views/menus', + '/administrator/components/com_menus/views/menu/tmpl', + '/administrator/components/com_menus/views/menu', + '/administrator/components/com_menus/views/items/tmpl', + '/administrator/components/com_menus/views/items', + '/administrator/components/com_menus/views/item/tmpl', + '/administrator/components/com_menus/views/item', + '/administrator/components/com_menus/views', + '/administrator/components/com_menus/tables', + '/administrator/components/com_menus/models/forms', + '/administrator/components/com_menus/models/fields/modal', + '/administrator/components/com_menus/models/fields', + '/administrator/components/com_menus/models', + '/administrator/components/com_menus/layouts/joomla/searchtools/default', + '/administrator/components/com_menus/helpers/html', + '/administrator/components/com_menus/controllers', + '/administrator/components/com_media/views/medialist/tmpl', + '/administrator/components/com_media/views/medialist', + '/administrator/components/com_media/views/media/tmpl', + '/administrator/components/com_media/views/media', + '/administrator/components/com_media/views/imageslist/tmpl', + '/administrator/components/com_media/views/imageslist', + '/administrator/components/com_media/views/images/tmpl', + '/administrator/components/com_media/views/images', + '/administrator/components/com_media/views', + '/administrator/components/com_media/models', + '/administrator/components/com_media/controllers', + '/administrator/components/com_login/views/login/tmpl', + '/administrator/components/com_login/views/login', + '/administrator/components/com_login/views', + '/administrator/components/com_login/models', + '/administrator/components/com_languages/views/overrides/tmpl', + '/administrator/components/com_languages/views/overrides', + '/administrator/components/com_languages/views/override/tmpl', + '/administrator/components/com_languages/views/override', + '/administrator/components/com_languages/views/multilangstatus/tmpl', + '/administrator/components/com_languages/views/multilangstatus', + '/administrator/components/com_languages/views/languages/tmpl', + '/administrator/components/com_languages/views/languages', + '/administrator/components/com_languages/views/language/tmpl', + '/administrator/components/com_languages/views/language', + '/administrator/components/com_languages/views/installed/tmpl', + '/administrator/components/com_languages/views/installed', + '/administrator/components/com_languages/views', + '/administrator/components/com_languages/models/forms', + '/administrator/components/com_languages/models/fields', + '/administrator/components/com_languages/models', + '/administrator/components/com_languages/layouts/joomla/searchtools/default', + '/administrator/components/com_languages/layouts/joomla/searchtools', + '/administrator/components/com_languages/layouts/joomla', + '/administrator/components/com_languages/layouts', + '/administrator/components/com_languages/helpers/html', + '/administrator/components/com_languages/helpers', + '/administrator/components/com_languages/controllers', + '/administrator/components/com_joomlaupdate/views/upload/tmpl', + '/administrator/components/com_joomlaupdate/views/upload', + '/administrator/components/com_joomlaupdate/views/update/tmpl', + '/administrator/components/com_joomlaupdate/views/update', + '/administrator/components/com_joomlaupdate/views/default/tmpl', + '/administrator/components/com_joomlaupdate/views/default', + '/administrator/components/com_joomlaupdate/views', + '/administrator/components/com_joomlaupdate/models', + '/administrator/components/com_joomlaupdate/helpers', + '/administrator/components/com_joomlaupdate/controllers', + '/administrator/components/com_installer/views/warnings/tmpl', + '/administrator/components/com_installer/views/warnings', + '/administrator/components/com_installer/views/updatesites/tmpl', + '/administrator/components/com_installer/views/updatesites', + '/administrator/components/com_installer/views/update/tmpl', + '/administrator/components/com_installer/views/update', + '/administrator/components/com_installer/views/manage/tmpl', + '/administrator/components/com_installer/views/manage', + '/administrator/components/com_installer/views/languages/tmpl', + '/administrator/components/com_installer/views/languages', + '/administrator/components/com_installer/views/install/tmpl', + '/administrator/components/com_installer/views/install', + '/administrator/components/com_installer/views/discover/tmpl', + '/administrator/components/com_installer/views/discover', + '/administrator/components/com_installer/views/default/tmpl', + '/administrator/components/com_installer/views/default', + '/administrator/components/com_installer/views/database/tmpl', + '/administrator/components/com_installer/views/database', + '/administrator/components/com_installer/views', + '/administrator/components/com_installer/models/forms', + '/administrator/components/com_installer/models/fields', + '/administrator/components/com_installer/models', + '/administrator/components/com_installer/helpers/html', + '/administrator/components/com_installer/controllers', + '/administrator/components/com_finder/views/statistics/tmpl', + '/administrator/components/com_finder/views/statistics', + '/administrator/components/com_finder/views/maps/tmpl', + '/administrator/components/com_finder/views/maps', + '/administrator/components/com_finder/views/indexer/tmpl', + '/administrator/components/com_finder/views/indexer', + '/administrator/components/com_finder/views/index/tmpl', + '/administrator/components/com_finder/views/index', + '/administrator/components/com_finder/views/filters/tmpl', + '/administrator/components/com_finder/views/filters', + '/administrator/components/com_finder/views/filter/tmpl', + '/administrator/components/com_finder/views/filter', + '/administrator/components/com_finder/views', + '/administrator/components/com_finder/tables', + '/administrator/components/com_finder/models/forms', + '/administrator/components/com_finder/models/fields', + '/administrator/components/com_finder/models', + '/administrator/components/com_finder/helpers/indexer/stemmer', + '/administrator/components/com_finder/helpers/indexer/parser', + '/administrator/components/com_finder/helpers/indexer/driver', + '/administrator/components/com_finder/helpers/html', + '/administrator/components/com_finder/controllers', + '/administrator/components/com_fields/views/groups/tmpl', + '/administrator/components/com_fields/views/groups', + '/administrator/components/com_fields/views/group/tmpl', + '/administrator/components/com_fields/views/group', + '/administrator/components/com_fields/views/fields/tmpl', + '/administrator/components/com_fields/views/fields', + '/administrator/components/com_fields/views/field/tmpl', + '/administrator/components/com_fields/views/field', + '/administrator/components/com_fields/views', + '/administrator/components/com_fields/tables', + '/administrator/components/com_fields/models/forms', + '/administrator/components/com_fields/models/fields', + '/administrator/components/com_fields/models', + '/administrator/components/com_fields/libraries', + '/administrator/components/com_fields/controllers', + '/administrator/components/com_cpanel/views/cpanel/tmpl', + '/administrator/components/com_cpanel/views/cpanel', + '/administrator/components/com_cpanel/views', + '/administrator/components/com_contenthistory/views/preview/tmpl', + '/administrator/components/com_contenthistory/views/preview', + '/administrator/components/com_contenthistory/views/history/tmpl', + '/administrator/components/com_contenthistory/views/history', + '/administrator/components/com_contenthistory/views/compare/tmpl', + '/administrator/components/com_contenthistory/views/compare', + '/administrator/components/com_contenthistory/views', + '/administrator/components/com_contenthistory/models', + '/administrator/components/com_contenthistory/helpers/html', + '/administrator/components/com_contenthistory/controllers', + '/administrator/components/com_content/views/featured/tmpl', + '/administrator/components/com_content/views/featured', + '/administrator/components/com_content/views/articles/tmpl', + '/administrator/components/com_content/views/articles', + '/administrator/components/com_content/views/article/tmpl', + '/administrator/components/com_content/views/article', + '/administrator/components/com_content/views', + '/administrator/components/com_content/tables', + '/administrator/components/com_content/models/forms', + '/administrator/components/com_content/models/fields/modal', + '/administrator/components/com_content/models/fields', + '/administrator/components/com_content/models', + '/administrator/components/com_content/helpers/html', + '/administrator/components/com_content/controllers', + '/administrator/components/com_contact/views/contacts/tmpl', + '/administrator/components/com_contact/views/contacts', + '/administrator/components/com_contact/views/contact/tmpl', + '/administrator/components/com_contact/views/contact', + '/administrator/components/com_contact/views', + '/administrator/components/com_contact/tables', + '/administrator/components/com_contact/models/forms/fields', + '/administrator/components/com_contact/models/forms', + '/administrator/components/com_contact/models/fields/modal', + '/administrator/components/com_contact/models/fields', + '/administrator/components/com_contact/models', + '/administrator/components/com_contact/helpers/html', + '/administrator/components/com_contact/controllers', + '/administrator/components/com_config/view/component/tmpl', + '/administrator/components/com_config/view/component', + '/administrator/components/com_config/view/application/tmpl', + '/administrator/components/com_config/view/application', + '/administrator/components/com_config/view', + '/administrator/components/com_config/models', + '/administrator/components/com_config/model/form', + '/administrator/components/com_config/model/field', + '/administrator/components/com_config/model', + '/administrator/components/com_config/helper', + '/administrator/components/com_config/controllers', + '/administrator/components/com_config/controller/component', + '/administrator/components/com_config/controller/application', + '/administrator/components/com_config/controller', + '/administrator/components/com_checkin/views/checkin/tmpl', + '/administrator/components/com_checkin/views/checkin', + '/administrator/components/com_checkin/views', + '/administrator/components/com_checkin/models/forms', + '/administrator/components/com_checkin/models', + '/administrator/components/com_categories/views/category/tmpl', + '/administrator/components/com_categories/views/category', + '/administrator/components/com_categories/views/categories/tmpl', + '/administrator/components/com_categories/views/categories', + '/administrator/components/com_categories/views', + '/administrator/components/com_categories/tables', + '/administrator/components/com_categories/models/forms', + '/administrator/components/com_categories/models/fields/modal', + '/administrator/components/com_categories/models/fields', + '/administrator/components/com_categories/models', + '/administrator/components/com_categories/helpers/html', + '/administrator/components/com_categories/controllers', + '/administrator/components/com_cache/views/purge/tmpl', + '/administrator/components/com_cache/views/purge', + '/administrator/components/com_cache/views/cache/tmpl', + '/administrator/components/com_cache/views/cache', + '/administrator/components/com_cache/views', + '/administrator/components/com_cache/models/forms', + '/administrator/components/com_cache/models', + '/administrator/components/com_cache/helpers', + '/administrator/components/com_banners/views/tracks/tmpl', + '/administrator/components/com_banners/views/tracks', + '/administrator/components/com_banners/views/download/tmpl', + '/administrator/components/com_banners/views/download', + '/administrator/components/com_banners/views/clients/tmpl', + '/administrator/components/com_banners/views/clients', + '/administrator/components/com_banners/views/client/tmpl', + '/administrator/components/com_banners/views/client', + '/administrator/components/com_banners/views/banners/tmpl', + '/administrator/components/com_banners/views/banners', + '/administrator/components/com_banners/views/banner/tmpl', + '/administrator/components/com_banners/views/banner', + '/administrator/components/com_banners/views', + '/administrator/components/com_banners/tables', + '/administrator/components/com_banners/models/forms', + '/administrator/components/com_banners/models/fields', + '/administrator/components/com_banners/models', + '/administrator/components/com_banners/helpers/html', + '/administrator/components/com_banners/controllers', + '/administrator/components/com_associations/views/associations/tmpl', + '/administrator/components/com_associations/views/associations', + '/administrator/components/com_associations/views/association/tmpl', + '/administrator/components/com_associations/views/association', + '/administrator/components/com_associations/views', + '/administrator/components/com_associations/models/forms', + '/administrator/components/com_associations/models/fields', + '/administrator/components/com_associations/models', + '/administrator/components/com_associations/layouts/joomla/searchtools/default', + '/administrator/components/com_associations/helpers', + '/administrator/components/com_associations/controllers', + '/administrator/components/com_admin/views/sysinfo/tmpl', + '/administrator/components/com_admin/views/sysinfo', + '/administrator/components/com_admin/views/profile/tmpl', + '/administrator/components/com_admin/views/profile', + '/administrator/components/com_admin/views/help/tmpl', + '/administrator/components/com_admin/views/help', + '/administrator/components/com_admin/views', + '/administrator/components/com_admin/sql/updates/sqlazure', + '/administrator/components/com_admin/models/forms', + '/administrator/components/com_admin/models', + '/administrator/components/com_admin/helpers/html', + '/administrator/components/com_admin/helpers', + '/administrator/components/com_admin/controllers', + '/administrator/components/com_actionlogs/views/actionlogs/tmpl', + '/administrator/components/com_actionlogs/views/actionlogs', + '/administrator/components/com_actionlogs/views', + '/administrator/components/com_actionlogs/models/forms', + '/administrator/components/com_actionlogs/models/fields', + '/administrator/components/com_actionlogs/models', + '/administrator/components/com_actionlogs/libraries', + '/administrator/components/com_actionlogs/layouts', + '/administrator/components/com_actionlogs/helpers', + '/administrator/components/com_actionlogs/controllers', + // 4.0 from Beta 1 to Beta 2 + '/libraries/vendor/joomla/controller/src', + '/libraries/vendor/joomla/controller', + '/api/components/com_installer/src/View/Languages', + '/administrator/components/com_finder/src/Indexer/Driver', + // 4.0 from Beta 4 to Beta 5 + '/plugins/content/imagelazyload', + // 4.0 from Beta 5 to Beta 6 + '/media/system/js/core.es6', + '/administrator/modules/mod_multilangstatus/src/Helper', + '/administrator/modules/mod_multilangstatus/src', + // 4.0 from Beta 6 to Beta 7 + '/media/vendor/skipto/css', + // 4.0 from Beta 7 to RC 1 + '/templates/system/js', + '/templates/cassiopeia/scss/tools/mixins', + '/plugins/fields/subfields/tmpl', + '/plugins/fields/subfields/params', + '/plugins/fields/subfields', + '/media/vendor/punycode/js', + '/media/templates/atum/js', + '/media/templates/atum', + '/libraries/vendor/paragonie/random_compat/dist', + '/libraries/vendor/paragonie/random_compat', + '/libraries/vendor/ozdemirburak/iris/src/Traits', + '/libraries/vendor/ozdemirburak/iris/src/Helpers', + '/libraries/vendor/ozdemirburak/iris/src/Exceptions', + '/libraries/vendor/ozdemirburak/iris/src/Color', + '/libraries/vendor/ozdemirburak/iris/src', + '/libraries/vendor/ozdemirburak/iris', + '/libraries/vendor/ozdemirburak', + '/libraries/vendor/bin', + '/components/com_menus/src/Controller', + '/components/com_csp/src/Controller', + '/components/com_csp/src', + '/components/com_csp', + '/administrator/templates/atum/Service/HTML', + '/administrator/templates/atum/Service', + '/administrator/components/com_joomlaupdate/src/Helper', + '/administrator/components/com_csp/tmpl/reports', + '/administrator/components/com_csp/tmpl', + '/administrator/components/com_csp/src/View/Reports', + '/administrator/components/com_csp/src/View', + '/administrator/components/com_csp/src/Table', + '/administrator/components/com_csp/src/Model', + '/administrator/components/com_csp/src/Helper', + '/administrator/components/com_csp/src/Controller', + '/administrator/components/com_csp/src', + '/administrator/components/com_csp/services', + '/administrator/components/com_csp/forms', + '/administrator/components/com_csp', + '/administrator/components/com_admin/tmpl/profile', + '/administrator/components/com_admin/src/View/Profile', + '/administrator/components/com_admin/forms', + // 4.0 from RC 5 to RC 6 + '/templates/cassiopeia/scss/vendor/fontawesome-free', + '/templates/cassiopeia/css/vendor/fontawesome-free', + '/media/templates/cassiopeia/js/mod_menu', + '/media/templates/cassiopeia/js', + '/media/templates/cassiopeia', + // 4.0 from RC 6 to 4.0.0 (stable) + '/libraries/vendor/willdurand/negotiation/tests/Negotiation/Tests', + '/libraries/vendor/willdurand/negotiation/tests/Negotiation', + '/libraries/vendor/willdurand/negotiation/tests', + '/libraries/vendor/jakeasmith/http_build_url/tests', + '/libraries/vendor/doctrine/inflector/docs/en', + '/libraries/vendor/doctrine/inflector/docs', + '/libraries/vendor/algo26-matthias/idna-convert/tests/unit', + '/libraries/vendor/algo26-matthias/idna-convert/tests/integration', + '/libraries/vendor/algo26-matthias/idna-convert/tests', + // From 4.0.3 to 4.0.4 + '/templates/cassiopeia/images/system', + // From 4.0.x to 4.1.0-beta1 + '/templates/system/scss', + '/templates/system/css', + '/templates/cassiopeia/scss/vendor/metismenu', + '/templates/cassiopeia/scss/vendor/joomla-custom-elements', + '/templates/cassiopeia/scss/vendor/choicesjs', + '/templates/cassiopeia/scss/vendor/bootstrap', + '/templates/cassiopeia/scss/vendor', + '/templates/cassiopeia/scss/tools/variables', + '/templates/cassiopeia/scss/tools/functions', + '/templates/cassiopeia/scss/tools', + '/templates/cassiopeia/scss/system/searchtools', + '/templates/cassiopeia/scss/system', + '/templates/cassiopeia/scss/global', + '/templates/cassiopeia/scss/blocks', + '/templates/cassiopeia/scss', + '/templates/cassiopeia/js', + '/templates/cassiopeia/images', + '/templates/cassiopeia/css/vendor/joomla-custom-elements', + '/templates/cassiopeia/css/vendor/choicesjs', + '/templates/cassiopeia/css/vendor', + '/templates/cassiopeia/css/system/searchtools', + '/templates/cassiopeia/css/system', + '/templates/cassiopeia/css/global', + '/templates/cassiopeia/css', + '/administrator/templates/system/scss', + '/administrator/templates/system/images', + '/administrator/templates/system/css', + '/administrator/templates/atum/scss/vendor/minicolors', + '/administrator/templates/atum/scss/vendor/joomla-custom-elements', + '/administrator/templates/atum/scss/vendor/fontawesome-free', + '/administrator/templates/atum/scss/vendor/choicesjs', + '/administrator/templates/atum/scss/vendor/bootstrap', + '/administrator/templates/atum/scss/vendor/awesomplete', + '/administrator/templates/atum/scss/vendor', + '/administrator/templates/atum/scss/system/searchtools', + '/administrator/templates/atum/scss/system', + '/administrator/templates/atum/scss/pages', + '/administrator/templates/atum/scss/blocks', + '/administrator/templates/atum/scss', + '/administrator/templates/atum/images/logos', + '/administrator/templates/atum/images', + '/administrator/templates/atum/css/vendor/minicolors', + '/administrator/templates/atum/css/vendor/joomla-custom-elements', + '/administrator/templates/atum/css/vendor/fontawesome-free', + '/administrator/templates/atum/css/vendor/choicesjs', + '/administrator/templates/atum/css/vendor/awesomplete', + '/administrator/templates/atum/css/vendor', + '/administrator/templates/atum/css/system/searchtools', + '/administrator/templates/atum/css/system', + '/administrator/templates/atum/css', + // From 4.1.0-beta3 to 4.1.0-rc1 + '/api/components/com_media/src/Helper', + // From 4.1.0 to 4.1.1 + '/libraries/vendor/tobscure/json-api/tests/Exception/Handler', + '/libraries/vendor/tobscure/json-api/tests/Exception', + '/libraries/vendor/tobscure/json-api/tests', + '/libraries/vendor/tobscure/json-api/.git/refs/tags', + '/libraries/vendor/tobscure/json-api/.git/refs/remotes/origin', + '/libraries/vendor/tobscure/json-api/.git/refs/remotes', + '/libraries/vendor/tobscure/json-api/.git/refs/heads', + '/libraries/vendor/tobscure/json-api/.git/refs', + '/libraries/vendor/tobscure/json-api/.git/objects/pack', + '/libraries/vendor/tobscure/json-api/.git/objects/info', + '/libraries/vendor/tobscure/json-api/.git/objects', + '/libraries/vendor/tobscure/json-api/.git/logs/refs/remotes/origin', + '/libraries/vendor/tobscure/json-api/.git/logs/refs/remotes', + '/libraries/vendor/tobscure/json-api/.git/logs/refs/heads', + '/libraries/vendor/tobscure/json-api/.git/logs/refs', + '/libraries/vendor/tobscure/json-api/.git/logs', + '/libraries/vendor/tobscure/json-api/.git/info', + '/libraries/vendor/tobscure/json-api/.git/hooks', + '/libraries/vendor/tobscure/json-api/.git/branches', + '/libraries/vendor/tobscure/json-api/.git', + // From 4.1.3 to 4.1.4 + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/Storage', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataFormatter', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests/DataCollector', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar/Tests', + '/libraries/vendor/maximebf/debugbar/tests/DebugBar', + '/libraries/vendor/maximebf/debugbar/tests', + '/libraries/vendor/maximebf/debugbar/docs', + '/libraries/vendor/maximebf/debugbar/demo/bridge/twig', + '/libraries/vendor/maximebf/debugbar/demo/bridge/swiftmailer', + '/libraries/vendor/maximebf/debugbar/demo/bridge/slim', + '/libraries/vendor/maximebf/debugbar/demo/bridge/propel', + '/libraries/vendor/maximebf/debugbar/demo/bridge/monolog', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/src/Demo', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine/src', + '/libraries/vendor/maximebf/debugbar/demo/bridge/doctrine', + '/libraries/vendor/maximebf/debugbar/demo/bridge/cachecache', + '/libraries/vendor/maximebf/debugbar/demo/bridge', + '/libraries/vendor/maximebf/debugbar/demo', + '/libraries/vendor/maximebf/debugbar/build', + // From 4.1 to 4.2.0-beta1 + '/plugins/twofactorauth/yubikey/tmpl', + '/plugins/twofactorauth/yubikey', + '/plugins/twofactorauth/totp/tmpl', + '/plugins/twofactorauth/totp/postinstall', + '/plugins/twofactorauth/totp', + '/plugins/twofactorauth', + '/libraries/vendor/nyholm/psr7/doc', + // From 4.2.0-beta1 to 4.2.0-beta2 + '/layouts/plugins/user/profile/fields', + '/layouts/plugins/user/profile', + ); + + $status['files_checked'] = $files; + $status['folders_checked'] = $folders; + + foreach ($files as $file) { + if ($fileExists = File::exists(JPATH_ROOT . $file)) { + $status['files_exist'][] = $file; + + if ($dryRun === false) { + if (File::delete(JPATH_ROOT . $file)) { + $status['files_deleted'][] = $file; + } else { + $status['files_errors'][] = Text::sprintf('FILES_JOOMLA_ERROR_FILE_FOLDER', $file); + } + } + } + } + + $this->moveRemainingTemplateFiles(); + + foreach ($folders as $folder) { + if ($folderExists = Folder::exists(JPATH_ROOT . $folder)) { + $status['folders_exist'][] = $folder; + + if ($dryRun === false) { + if (Folder::delete(JPATH_ROOT . $folder)) { + $status['folders_deleted'][] = $folder; + } else { + $status['folders_errors'][] = Text::sprintf('FILES_JOOMLA_ERROR_FILE_FOLDER', $folder); + } + } + } + } + + $this->fixFilenameCasing(); + + /* + * Needed for updates from 3.10 + * If com_search doesn't exist then assume we can delete the search package manifest (included in the update packages) + * We deliberately check for the presence of the files in case people have previously uninstalled their search extension + * but an update has put the files back. In that case it exists even if they don't believe in it! + */ + if ( + !File::exists(JPATH_ROOT . '/administrator/components/com_search/search.php') + && File::exists(JPATH_ROOT . '/administrator/manifests/packages/pkg_search.xml') + ) { + File::delete(JPATH_ROOT . '/administrator/manifests/packages/pkg_search.xml'); + } + + if ($suppressOutput === false && count($status['folders_errors'])) { + echo implode('
', $status['folders_errors']); + } + + if ($suppressOutput === false && count($status['files_errors'])) { + echo implode('
', $status['files_errors']); + } + + return $status; + } + + /** + * Method to create assets for newly installed components + * + * @param Installer $installer The class calling this method + * + * @return boolean + * + * @since 3.2 + */ + public function updateAssets($installer) + { + // List all components added since 4.0 + $newComponents = array( + // Components to be added here + ); + + foreach ($newComponents as $component) { + /** @var \Joomla\CMS\Table\Asset $asset */ + $asset = Table::getInstance('Asset'); + + if ($asset->loadByName($component)) { + continue; + } + + $asset->name = $component; + $asset->parent_id = 1; + $asset->rules = '{}'; + $asset->title = $component; + $asset->setLocation(1, 'last-child'); + + if (!$asset->store()) { + // Install failed, roll back changes + $installer->abort(Text::sprintf('JLIB_INSTALLER_ABORT_COMP_INSTALL_ROLLBACK', $asset->getError(true))); + + return false; + } + } + + return true; + } + + /** + * Converts the site's database tables to support UTF-8 Multibyte. + * + * @param boolean $doDbFixMsg Flag if message to be shown to check db fix + * + * @return void + * + * @since 3.5 + */ + public function convertTablesToUtf8mb4($doDbFixMsg = false) + { + $db = Factory::getDbo(); + + if ($db->getServerType() !== 'mysql') { + return; + } + + // Check if the #__utf8_conversion table exists + $db->setQuery('SHOW TABLES LIKE ' . $db->quote($db->getPrefix() . 'utf8_conversion')); + + try { + $rows = $db->loadRowList(0); + } catch (Exception $e) { + // Render the error message from the Exception object + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + if ($doDbFixMsg) { + // Show an error message telling to check database problems + Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_DATABASE_UPGRADE_FAILED'), 'error'); + } + + return; + } + + // Nothing to do if the table doesn't exist because the CMS has never been updated from a pre-4.0 version + if (count($rows) === 0) { + return; + } + + // Set required conversion status + $converted = 5; + + // Check conversion status in database + $db->setQuery( + 'SELECT ' . $db->quoteName('converted') + . ' FROM ' . $db->quoteName('#__utf8_conversion') + ); + + try { + $convertedDB = $db->loadResult(); + } catch (Exception $e) { + // Render the error message from the Exception object + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + if ($doDbFixMsg) { + // Show an error message telling to check database problems + Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_DATABASE_UPGRADE_FAILED'), 'error'); + } + + return; + } + + // If conversion status from DB is equal to required final status, try to drop the #__utf8_conversion table + if ($convertedDB === $converted) { + $this->dropUtf8ConversionTable(); + + return; + } + + // Perform the required conversions of core tables if not done already in a previous step + if ($convertedDB !== 99) { + $fileName1 = JPATH_ROOT . '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion.sql'; + + if (is_file($fileName1)) { + $fileContents1 = @file_get_contents($fileName1); + $queries1 = $db->splitSql($fileContents1); + + if (!empty($queries1)) { + foreach ($queries1 as $query1) { + try { + $db->setQuery($query1)->execute(); + } catch (Exception $e) { + $converted = $convertedDB; + + // Still render the error message from the Exception object + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + } + } + } + + // If no error before, perform the optional conversions of tables which might or might not exist + if ($converted === 5) { + $fileName2 = JPATH_ROOT . '/administrator/components/com_admin/sql/others/mysql/utf8mb4-conversion_optional.sql'; + + if (is_file($fileName2)) { + $fileContents2 = @file_get_contents($fileName2); + $queries2 = $db->splitSql($fileContents2); + + if (!empty($queries2)) { + foreach ($queries2 as $query2) { + // Get table name from query + if (preg_match('/^ALTER\s+TABLE\s+([^\s]+)\s+/i', $query2, $matches) === 1) { + $tableName = str_replace('`', '', $matches[1]); + $tableName = str_replace('#__', $db->getPrefix(), $tableName); + + // Check if the table exists and if yes, run the query + try { + $db->setQuery('SHOW TABLES LIKE ' . $db->quote($tableName)); + + $rows = $db->loadRowList(0); + + if (count($rows) > 0) { + $db->setQuery($query2)->execute(); + } + } catch (Exception $e) { + $converted = 99; + + // Still render the error message from the Exception object + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + } + } + } + } + + if ($doDbFixMsg && $converted !== 5) { + // Show an error message telling to check database problems + Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_DATABASE_UPGRADE_FAILED'), 'error'); + } + + // If the conversion was successful try to drop the #__utf8_conversion table + if ($converted === 5 && $this->dropUtf8ConversionTable()) { + // Table successfully dropped + return; + } + + // Set flag in database if the conversion status has changed. + if ($converted !== $convertedDB) { + $db->setQuery('UPDATE ' . $db->quoteName('#__utf8_conversion') + . ' SET ' . $db->quoteName('converted') . ' = ' . $converted . ';')->execute(); + } + } + + /** + * This method clean the Joomla Cache using the method `clean` from the com_cache model + * + * @return void + * + * @since 3.5.1 + */ + private function cleanJoomlaCache() + { + /** @var \Joomla\Component\Cache\Administrator\Model\CacheModel $model */ + $model = Factory::getApplication()->bootComponent('com_cache')->getMVCFactory() + ->createModel('Cache', 'Administrator', ['ignore_request' => true]); + + // Clean frontend cache + $model->clean(); + + // Clean admin cache + $model->setState('client_id', 1); + $model->clean(); + } + + /** + * This method drops the #__utf8_conversion table + * + * @return boolean True on success + * + * @since 4.0.0 + */ + private function dropUtf8ConversionTable() + { + $db = Factory::getDbo(); + + try { + $db->setQuery('DROP TABLE ' . $db->quoteName('#__utf8_conversion') . ';')->execute(); + } catch (Exception $e) { + return false; + } + + return true; + } + + /** + * Called after any type of action + * + * @param string $action Which action is happening (install|uninstall|discover_install|update) + * @param Installer $installer The class calling this method + * + * @return boolean True on success + * + * @since 4.0.0 + */ + public function postflight($action, $installer) + { + if ($action !== 'update') { + return true; + } + + if (empty($this->fromVersion) || version_compare($this->fromVersion, '4.0.0', 'ge')) { + return true; + } + + // Update UCM content types. + $this->updateContentTypes(); + + $db = Factory::getDbo(); + Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_menus/Table/'); + + $tableItem = new \Joomla\Component\Menus\Administrator\Table\MenuTable($db); + + $contactItems = $this->contactItems($tableItem); + $finderItems = $this->finderItems($tableItem); + + $menuItems = array_merge($contactItems, $finderItems); + + foreach ($menuItems as $menuItem) { + // Check an existing record + $keys = [ + 'menutype' => $menuItem['menutype'], + 'type' => $menuItem['type'], + 'title' => $menuItem['title'], + 'parent_id' => $menuItem['parent_id'], + 'client_id' => $menuItem['client_id'], + ]; + + if ($tableItem->load($keys)) { + continue; + } + + $newTableItem = new \Joomla\Component\Menus\Administrator\Table\MenuTable($db); + + // Bind the data. + if (!$newTableItem->bind($menuItem)) { + return false; + } + + $newTableItem->setLocation($menuItem['parent_id'], 'last-child'); + + // Check the data. + if (!$newTableItem->check()) { + return false; + } + + // Store the data. + if (!$newTableItem->store()) { + return false; + } + + // Rebuild the tree path. + if (!$newTableItem->rebuildPath($newTableItem->id)) { + return false; + } + } + + return true; + } + + /** + * Prepare the contact menu items + * + * @return array Menu items + * + * @since 4.0.0 + */ + private function contactItems(Table $tableItem): array + { + // Check for the Contact parent Id Menu Item + $keys = [ + 'menutype' => 'main', + 'type' => 'component', + 'title' => 'com_contact', + 'parent_id' => 1, + 'client_id' => 1, + ]; + + $contactMenuitem = $tableItem->load($keys); + + if (!$contactMenuitem) { + return []; + } + + $parentId = $tableItem->id; + $componentId = ExtensionHelper::getExtensionRecord('com_fields', 'component')->extension_id; + + // Add Contact Fields Menu Items. + $menuItems = [ + [ + 'menutype' => 'main', + 'title' => '-', + 'alias' => microtime(true), + 'note' => '', + 'path' => '', + 'link' => '#', + 'type' => 'separator', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ], + [ + 'menutype' => 'main', + 'title' => 'mod_menu_fields', + 'alias' => 'Contact Custom Fields', + 'note' => '', + 'path' => 'contact/Custom Fields', + 'link' => 'index.php?option=com_fields&context=com_contact.contact', + 'type' => 'component', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ], + [ + 'menutype' => 'main', + 'title' => 'mod_menu_fields_group', + 'alias' => 'Contact Custom Fields Group', + 'note' => '', + 'path' => 'contact/Custom Fields Group', + 'link' => 'index.php?option=com_fields&view=groups&context=com_contact.contact', + 'type' => 'component', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ] + ]; + + return $menuItems; + } + + /** + * Prepare the finder menu items + * + * @return array Menu items + * + * @since 4.0.0 + */ + private function finderItems(Table $tableItem): array + { + // Check for the Finder parent Id Menu Item + $keys = [ + 'menutype' => 'main', + 'type' => 'component', + 'title' => 'com_finder', + 'parent_id' => 1, + 'client_id' => 1, + ]; + + $finderMenuitem = $tableItem->load($keys); + + if (!$finderMenuitem) { + return []; + } + + $parentId = $tableItem->id; + $componentId = ExtensionHelper::getExtensionRecord('com_finder', 'component')->extension_id; + + // Add Finder Fields Menu Items. + $menuItems = [ + [ + 'menutype' => 'main', + 'title' => '-', + 'alias' => microtime(true), + 'note' => '', + 'path' => '', + 'link' => '#', + 'type' => 'separator', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ], + [ + 'menutype' => 'main', + 'title' => 'com_finder_index', + 'alias' => 'Smart-Search-Index', + 'note' => '', + 'path' => 'Smart Search/Index', + 'link' => 'index.php?option=com_finder&view=index', + 'type' => 'component', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ], + [ + 'menutype' => 'main', + 'title' => 'com_finder_maps', + 'alias' => 'Smart-Search-Maps', + 'note' => '', + 'path' => 'Smart Search/Maps', + 'link' => 'index.php?option=com_finder&view=maps', + 'type' => 'component', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ], + [ + 'menutype' => 'main', + 'title' => 'com_finder_filters', + 'alias' => 'Smart-Search-Filters', + 'note' => '', + 'path' => 'Smart Search/Filters', + 'link' => 'index.php?option=com_finder&view=filters', + 'type' => 'component', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ], + [ + 'menutype' => 'main', + 'title' => 'com_finder_searches', + 'alias' => 'Smart-Search-Searches', + 'note' => '', + 'path' => 'Smart Search/Searches', + 'link' => 'index.php?option=com_finder&view=searches', + 'type' => 'component', + 'published' => 1, + 'parent_id' => $parentId, + 'level' => 2, + 'component_id' => $componentId, + 'checked_out' => null, + 'checked_out_time' => null, + 'browserNav' => 0, + 'access' => 0, + 'img' => '', + 'template_style_id' => 0, + 'params' => '{}', + 'home' => 0, + 'language' => '*', + 'client_id' => 1, + 'publish_up' => null, + 'publish_down' => null, + ] + ]; + + return $menuItems; + } + + /** + * Updates content type table classes. + * + * @return void + * + * @since 4.0.0 + */ + private function updateContentTypes(): void + { + // Content types to update. + $contentTypes = [ + 'com_content.article', + 'com_contact.contact', + 'com_newsfeeds.newsfeed', + 'com_tags.tag', + 'com_banners.banner', + 'com_banners.client', + 'com_users.note', + 'com_content.category', + 'com_contact.category', + 'com_newsfeeds.category', + 'com_banners.category', + 'com_users.category', + 'com_users.user', + ]; + + // Get table definitions. + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('type_alias'), + $db->quoteName('table'), + ] + ) + ->from($db->quoteName('#__content_types')) + ->whereIn($db->quoteName('type_alias'), $contentTypes, ParameterType::STRING); + + $db->setQuery($query); + $contentTypes = $db->loadObjectList(); + + // Prepare the update query. + $query = $db->getQuery(true) + ->update($db->quoteName('#__content_types')) + ->set($db->quoteName('table') . ' = :table') + ->where($db->quoteName('type_alias') . ' = :typeAlias') + ->bind(':table', $table) + ->bind(':typeAlias', $typeAlias); + + $db->setQuery($query); + + foreach ($contentTypes as $contentType) { + list($component, $tableType) = explode('.', $contentType->type_alias); + + // Special case for core table classes. + if ($contentType->type_alias === 'com_users.users' || $tableType === 'category') { + $tablePrefix = 'Joomla\\CMS\Table\\'; + $tableType = ucfirst($tableType); + } else { + $tablePrefix = 'Joomla\\Component\\' . ucfirst(substr($component, 4)) . '\\Administrator\\Table\\'; + $tableType = ucfirst($tableType) . 'Table'; + } + + // Bind type alias. + $typeAlias = $contentType->type_alias; + + $table = json_decode($contentType->table); + + // Update table definitions. + $table->special->type = $tableType; + $table->special->prefix = $tablePrefix; + + // Some content types don't have this property. + if (!empty($table->common->prefix)) { + $table->common->prefix = 'Joomla\\CMS\\Table\\'; + } + + $table = json_encode($table); + + // Execute the query. + $db->execute(); + } + } + + /** + * Renames or removes incorrectly cased files. + * + * @return void + * + * @since 3.9.25 + */ + protected function fixFilenameCasing() + { + $files = array( + // 3.10 changes + '/libraries/src/Filesystem/Support/Stringcontroller.php' => '/libraries/src/Filesystem/Support/StringController.php', + '/libraries/src/Form/Rule/SubFormRule.php' => '/libraries/src/Form/Rule/SubformRule.php', + // 4.0.0 + '/media/vendor/skipto/js/skipTo.js' => '/media/vendor/skipto/js/skipto.js', + ); + + foreach ($files as $old => $expected) { + $oldRealpath = realpath(JPATH_ROOT . $old); + + // On Unix without incorrectly cased file. + if ($oldRealpath === false) { + continue; + } + + $oldBasename = basename($oldRealpath); + $newRealpath = realpath(JPATH_ROOT . $expected); + $newBasename = basename($newRealpath); + $expectedBasename = basename($expected); + + // On Windows or Unix with only the incorrectly cased file. + if ($newBasename !== $expectedBasename) { + // Rename the file. + File::move(JPATH_ROOT . $old, JPATH_ROOT . $old . '.tmp'); + File::move(JPATH_ROOT . $old . '.tmp', JPATH_ROOT . $expected); + + continue; + } + + // There might still be an incorrectly cased file on other OS than Windows. + if ($oldBasename === basename($old)) { + // Check if case-insensitive file system, eg on OSX. + if (fileinode($oldRealpath) === fileinode($newRealpath)) { + // Check deeper because even realpath or glob might not return the actual case. + if (!in_array($expectedBasename, scandir(dirname($newRealpath)))) { + // Rename the file. + File::move(JPATH_ROOT . $old, JPATH_ROOT . $old . '.tmp'); + File::move(JPATH_ROOT . $old . '.tmp', JPATH_ROOT . $expected); + } + } else { + // On Unix with both files: Delete the incorrectly cased file. + File::delete(JPATH_ROOT . $old); + } + } + } + } + + /** + * Move core template (s)css or js or image files which are left after deleting + * obsolete core files to the right place in media folder. + * + * @return void + * + * @since 4.1.0 + */ + protected function moveRemainingTemplateFiles() + { + $folders = [ + '/administrator/templates/atum/css' => '/media/templates/administrator/atum/css', + '/administrator/templates/atum/images' => '/media/templates/administrator/atum/images', + '/administrator/templates/atum/js' => '/media/templates/administrator/atum/js', + '/administrator/templates/atum/scss' => '/media/templates/administrator/atum/scss', + '/templates/cassiopeia/css' => '/media/templates/site/cassiopeia/css', + '/templates/cassiopeia/images' => '/media/templates/site/cassiopeia/images', + '/templates/cassiopeia/js' => '/media/templates/site/cassiopeia/js', + '/templates/cassiopeia/scss' => '/media/templates/site/cassiopeia/scss', + ]; + + foreach ($folders as $oldFolder => $newFolder) { + if (Folder::exists(JPATH_ROOT . $oldFolder)) { + $oldPath = realpath(JPATH_ROOT . $oldFolder); + $newPath = realpath(JPATH_ROOT . $newFolder); + $directory = new \RecursiveDirectoryIterator($oldPath); + $directory->setFlags(RecursiveDirectoryIterator::SKIP_DOTS); + $iterator = new \RecursiveIteratorIterator($directory); + + // Handle all files in this folder and all sub-folders + foreach ($iterator as $oldFile) { + if ($oldFile->isDir()) { + continue; + } + + $newFile = $newPath . substr($oldFile, strlen($oldPath)); + + // Create target folder and parent folders if they don't exist yet + if (is_dir(dirname($newFile)) || @mkdir(dirname($newFile), 0755, true)) { + File::move($oldFile, $newFile); + } + } + } + } + } + + /** + * Ensure the core templates are correctly moved to the new mode. + * + * @return void + * + * @since 4.1.0 + */ + protected function fixTemplateMode(): void + { + $db = Factory::getContainer()->get('DatabaseDriver'); + + array_map( + function ($template) use ($db) { + $clientId = $template === 'atum' ? 1 : 0; + $query = $db->getQuery(true) + ->update($db->quoteName('#__template_styles')) + ->set($db->quoteName('inheritable') . ' = 1') + ->where($db->quoteName('template') . ' = ' . $db->quote($template)) + ->where($db->quoteName('client_id') . ' = ' . $clientId); + + try { + $db->setQuery($query)->execute(); + } catch (Exception $e) { + echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; + + return; + } + }, + ['atum', 'cassiopeia'] + ); + } + + /** + * Add the user Auth Provider Column as it could be present from 3.10 already + * + * @return void + * + * @since 4.1.1 + */ + protected function addUserAuthProviderColumn(): void + { + $db = Factory::getContainer()->get('DatabaseDriver'); + + // Check if the column already exists + $fields = $db->getTableColumns('#__users'); + + // Column exists, skip + if (isset($fields['authProvider'])) { + return; + } + + $query = 'ALTER TABLE ' . $db->quoteName('#__users') + . ' ADD COLUMN ' . $db->quoteName('authProvider') . ' varchar(100) DEFAULT ' . $db->quote('') . ' NOT NULL'; + + // Add column + try { + $db->setQuery($query)->execute(); + } catch (Exception $e) { + echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; + + return; + } + } } diff --git a/administrator/components/com_admin/services/provider.php b/administrator/components/com_admin/services/provider.php index 9973ba6aefd3b..d17a3d0cc5faf 100644 --- a/administrator/components/com_admin/services/provider.php +++ b/administrator/components/com_admin/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Admin')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Admin')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Admin')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Admin')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new AdminComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new AdminComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_admin/src/Controller/DisplayController.php b/administrator/components/com_admin/src/Controller/DisplayController.php index 3857024e52e86..53d8c6ca35faa 100644 --- a/administrator/components/com_admin/src/Controller/DisplayController.php +++ b/administrator/components/com_admin/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', $this->default_view); - $format = $this->input->get('format', 'html'); + /** + * View method + * + * @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()}. + * + * @return static Supports chaining. + * + * @since 3.9 + */ + public function display($cachable = false, $urlparams = array()) + { + $viewName = $this->input->get('view', $this->default_view); + $format = $this->input->get('format', 'html'); - // Check CSRF token for sysinfo export views - if ($viewName === 'sysinfo' && ($format === 'text' || $format === 'json')) - { - // Check for request forgeries. - $this->checkToken('GET'); - } + // Check CSRF token for sysinfo export views + if ($viewName === 'sysinfo' && ($format === 'text' || $format === 'json')) { + // Check for request forgeries. + $this->checkToken('GET'); + } - return parent::display($cachable, $urlparams); - } + return parent::display($cachable, $urlparams); + } } diff --git a/administrator/components/com_admin/src/Dispatcher/Dispatcher.php b/administrator/components/com_admin/src/Dispatcher/Dispatcher.php index 082a22e9f51f1..1fab521ee51e7 100644 --- a/administrator/components/com_admin/src/Dispatcher/Dispatcher.php +++ b/administrator/components/com_admin/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ getRegistry()->register('system', new System); - $this->getRegistry()->register('phpsetting', new PhpSetting); - $this->getRegistry()->register('directory', new Directory); - $this->getRegistry()->register('configuration', new Configuration); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('system', new System()); + $this->getRegistry()->register('phpsetting', new PhpSetting()); + $this->getRegistry()->register('directory', new Directory()); + $this->getRegistry()->register('configuration', new Configuration()); + } } diff --git a/administrator/components/com_admin/src/Model/HelpModel.php b/administrator/components/com_admin/src/Model/HelpModel.php index cec67d3093dcb..d5db970af9333 100644 --- a/administrator/components/com_admin/src/Model/HelpModel.php +++ b/administrator/components/com_admin/src/Model/HelpModel.php @@ -1,4 +1,5 @@ help_search)) - { - $this->help_search = Factory::getApplication()->input->getString('helpsearch'); - } - - return $this->help_search; - } - - /** - * Method to get the page - * - * @return string The page - * - * @since 1.6 - */ - public function &getPage() - { - if (\is_null($this->page)) - { - $this->page = Help::createUrl(Factory::getApplication()->input->get('page', 'Start_Here')); - } - - return $this->page; - } - - /** - * Method to get the lang tag - * - * @return string lang iso tag - * - * @since 1.6 - */ - public function getLangTag() - { - if (\is_null($this->lang_tag)) - { - $this->lang_tag = Factory::getLanguage()->getTag(); - - if (!is_dir(JPATH_BASE . '/help/' . $this->lang_tag)) - { - // Use English as fallback - $this->lang_tag = 'en-GB'; - } - } - - return $this->lang_tag; - } - - /** - * Method to get the table of contents - * - * @return array Table of contents - */ - public function &getToc() - { - if (!\is_null($this->toc)) - { - return $this->toc; - } - - // Get vars - $lang_tag = $this->getLangTag(); - $help_search = $this->getHelpSearch(); - - // New style - Check for a TOC \JSON file - if (file_exists(JPATH_BASE . '/help/' . $lang_tag . '/toc.json')) - { - $data = json_decode(file_get_contents(JPATH_BASE . '/help/' . $lang_tag . '/toc.json')); - - // Loop through the data array - foreach ($data as $key => $value) - { - $this->toc[$key] = Text::_('COM_ADMIN_HELP_' . $value); - } - - // Sort the Table of Contents - asort($this->toc); - - return $this->toc; - } - - // Get Help files - $files = Folder::files(JPATH_BASE . '/help/' . $lang_tag, '\.xml$|\.html$'); - $this->toc = array(); - - foreach ($files as $file) - { - $buffer = file_get_contents(JPATH_BASE . '/help/' . $lang_tag . '/' . $file); - - if (!preg_match('#(.*?)#', $buffer, $m)) - { - continue; - } - - $title = trim($m[1]); - - if (!$title) - { - continue; - } - - // Translate the page title - $title = Text::_($title); - - // Strip the extension - $file = preg_replace('#\.xml$|\.html$#', '', $file); - - if ($help_search && StringHelper::strpos(StringHelper::strtolower(strip_tags($buffer)), StringHelper::strtolower($help_search)) === false) - { - continue; - } - - // Add an item in the Table of Contents - $this->toc[$file] = $title; - } - - // Sort the Table of Contents - asort($this->toc); - - return $this->toc; - } + /** + * The search string + * + * @var string + * @since 1.6 + */ + protected $help_search = null; + + /** + * The page to be viewed + * + * @var string + * @since 1.6 + */ + protected $page = null; + + /** + * The ISO language tag + * + * @var string + * @since 1.6 + */ + protected $lang_tag = null; + + /** + * Table of contents + * + * @var array + * @since 1.6 + */ + protected $toc = null; + + /** + * URL for the latest version check + * + * @var string + * @since 1.6 + */ + protected $latest_version_check = null; + + /** + * Method to get the help search string + * + * @return string Help search string + * + * @since 1.6 + */ + public function &getHelpSearch() + { + if (\is_null($this->help_search)) { + $this->help_search = Factory::getApplication()->input->getString('helpsearch'); + } + + return $this->help_search; + } + + /** + * Method to get the page + * + * @return string The page + * + * @since 1.6 + */ + public function &getPage() + { + if (\is_null($this->page)) { + $this->page = Help::createUrl(Factory::getApplication()->input->get('page', 'Start_Here')); + } + + return $this->page; + } + + /** + * Method to get the lang tag + * + * @return string lang iso tag + * + * @since 1.6 + */ + public function getLangTag() + { + if (\is_null($this->lang_tag)) { + $this->lang_tag = Factory::getLanguage()->getTag(); + + if (!is_dir(JPATH_BASE . '/help/' . $this->lang_tag)) { + // Use English as fallback + $this->lang_tag = 'en-GB'; + } + } + + return $this->lang_tag; + } + + /** + * Method to get the table of contents + * + * @return array Table of contents + */ + public function &getToc() + { + if (!\is_null($this->toc)) { + return $this->toc; + } + + // Get vars + $lang_tag = $this->getLangTag(); + $help_search = $this->getHelpSearch(); + + // New style - Check for a TOC \JSON file + if (file_exists(JPATH_BASE . '/help/' . $lang_tag . '/toc.json')) { + $data = json_decode(file_get_contents(JPATH_BASE . '/help/' . $lang_tag . '/toc.json')); + + // Loop through the data array + foreach ($data as $key => $value) { + $this->toc[$key] = Text::_('COM_ADMIN_HELP_' . $value); + } + + // Sort the Table of Contents + asort($this->toc); + + return $this->toc; + } + + // Get Help files + $files = Folder::files(JPATH_BASE . '/help/' . $lang_tag, '\.xml$|\.html$'); + $this->toc = array(); + + foreach ($files as $file) { + $buffer = file_get_contents(JPATH_BASE . '/help/' . $lang_tag . '/' . $file); + + if (!preg_match('#(.*?)#', $buffer, $m)) { + continue; + } + + $title = trim($m[1]); + + if (!$title) { + continue; + } + + // Translate the page title + $title = Text::_($title); + + // Strip the extension + $file = preg_replace('#\.xml$|\.html$#', '', $file); + + if ($help_search && StringHelper::strpos(StringHelper::strtolower(strip_tags($buffer)), StringHelper::strtolower($help_search)) === false) { + continue; + } + + // Add an item in the Table of Contents + $this->toc[$file] = $title; + } + + // Sort the Table of Contents + asort($this->toc); + + return $this->toc; + } } diff --git a/administrator/components/com_admin/src/Model/SysinfoModel.php b/administrator/components/com_admin/src/Model/SysinfoModel.php index de12194ee3963..f05357d7a8e27 100644 --- a/administrator/components/com_admin/src/Model/SysinfoModel.php +++ b/administrator/components/com_admin/src/Model/SysinfoModel.php @@ -1,4 +1,5 @@ [ - 'CONTEXT_DOCUMENT_ROOT', - 'Cookie', - 'DOCUMENT_ROOT', - 'extension_dir', - 'error_log', - 'Host', - 'HTTP_COOKIE', - 'HTTP_HOST', - 'HTTP_ORIGIN', - 'HTTP_REFERER', - 'HTTP Request', - 'include_path', - 'mysql.default_socket', - 'MYSQL_SOCKET', - 'MYSQL_INCLUDE', - 'MYSQL_LIBS', - 'mysqli.default_socket', - 'MYSQLI_SOCKET', - 'PATH', - 'Path to sendmail', - 'pdo_mysql.default_socket', - 'Referer', - 'REMOTE_ADDR', - 'SCRIPT_FILENAME', - 'sendmail_path', - 'SERVER_ADDR', - 'SERVER_ADMIN', - 'Server Administrator', - 'SERVER_NAME', - 'Server Root', - 'session.name', - 'session.save_path', - 'upload_tmp_dir', - 'User/Group', - 'open_basedir', - ], - 'other' => [ - 'db', - 'dbprefix', - 'fromname', - 'live_site', - 'log_path', - 'mailfrom', - 'memcached_server_host', - 'open_basedir', - 'Origin', - 'proxy_host', - 'proxy_user', - 'proxy_pass', - 'redis_server_host', - 'redis_server_auth', - 'secret', - 'sendmail', - 'session.save_path', - 'session_memcached_server_host', - 'session_redis_server_host', - 'session_redis_server_auth', - 'sitename', - 'smtphost', - 'tmp_path', - 'open_basedir', - ] - ]; - - /** - * System values that can be "safely" shared - * - * @var array - * - * @since 3.5 - */ - protected $safeData; - - /** - * Information about writable state of directories - * - * @var array - * @since 1.6 - */ - protected $directories = []; - - /** - * The current editor. - * - * @var string - * @since 1.6 - */ - protected $editor = null; - - /** - * Remove sections of data marked as private in the privateSettings - * - * @param array $dataArray Array with data that may contain private information - * @param string $dataType Type of data to search for a specific section in the privateSettings array - * - * @return array - * - * @since 3.5 - */ - protected function cleanPrivateData(array $dataArray, string $dataType = 'other'): array - { - $dataType = isset($this->privateSettings[$dataType]) ? $dataType : 'other'; - - $privateSettings = $this->privateSettings[$dataType]; - - if (!$privateSettings) - { - return $dataArray; - } - - foreach ($dataArray as $section => $values) - { - if (\is_array($values)) - { - $dataArray[$section] = $this->cleanPrivateData($values, $dataType); - } - - if (\in_array($section, $privateSettings, true)) - { - $dataArray[$section] = $this->cleanSectionPrivateData($values); - } - } - - return $dataArray; - } - - /** - * Obfuscate section values - * - * @param mixed $sectionValues Section data - * - * @return string|array - * - * @since 3.5 - */ - protected function cleanSectionPrivateData($sectionValues) - { - if (!\is_array($sectionValues)) - { - if (strstr($sectionValues, JPATH_ROOT)) - { - $sectionValues = 'xxxxxx'; - } - - return \strlen($sectionValues) ? 'xxxxxx' : ''; - } - - foreach ($sectionValues as $setting => $value) - { - $sectionValues[$setting] = \strlen($value) ? 'xxxxxx' : ''; - } - - return $sectionValues; - } - - /** - * Method to get the PHP settings - * - * @return array Some PHP settings - * - * @since 1.6 - */ - public function &getPhpSettings(): array - { - if (!empty($this->php_settings)) - { - return $this->php_settings; - } - - $this->php_settings = [ - 'memory_limit' => ini_get('memory_limit'), - 'upload_max_filesize' => ini_get('upload_max_filesize'), - 'post_max_size' => ini_get('post_max_size'), - 'display_errors' => ini_get('display_errors') == '1', - 'short_open_tag' => ini_get('short_open_tag') == '1', - 'file_uploads' => ini_get('file_uploads') == '1', - 'output_buffering' => (int) ini_get('output_buffering') !== 0, - 'open_basedir' => ini_get('open_basedir'), - 'session.save_path' => ini_get('session.save_path'), - 'session.auto_start' => ini_get('session.auto_start'), - 'disable_functions' => ini_get('disable_functions'), - 'xml' => \extension_loaded('xml'), - 'zlib' => \extension_loaded('zlib'), - 'zip' => \function_exists('zip_open') && \function_exists('zip_read'), - 'mbstring' => \extension_loaded('mbstring'), - 'fileinfo' => \extension_loaded('fileinfo'), - 'gd' => \extension_loaded('gd'), - 'iconv' => \function_exists('iconv'), - 'intl' => \function_exists('transliterator_transliterate'), - 'max_input_vars' => ini_get('max_input_vars'), - ]; - - return $this->php_settings; - } - - /** - * Method to get the config - * - * @return array config values - * - * @since 1.6 - */ - public function &getConfig(): array - { - if (!empty($this->config)) - { - return $this->config; - } - - $registry = new Registry(new \JConfig); - $this->config = $registry->toArray(); - $hidden = [ - 'host', 'user', 'password', 'ftp_user', 'ftp_pass', - 'smtpuser', 'smtppass', 'redis_server_auth', 'session_redis_server_auth', - 'proxy_user', 'proxy_pass', 'secret' - ]; - - foreach ($hidden as $key) - { - $this->config[$key] = 'xxxxxx'; - } - - return $this->config; - } - - /** - * Method to get the system information - * - * @return array System information values - * - * @since 1.6 - */ - public function &getInfo(): array - { - if (!empty($this->info)) - { - return $this->info; - } - - $db = $this->getDatabase(); - - $this->info = [ - 'php' => php_uname(), - 'dbserver' => $db->getServerType(), - 'dbversion' => $db->getVersion(), - 'dbcollation' => $db->getCollation(), - 'dbconnectioncollation' => $db->getConnectionCollation(), - 'dbconnectionencryption' => $db->getConnectionEncryption(), - 'dbconnencryptsupported' => $db->isConnectionEncryptionSupported(), - 'phpversion' => PHP_VERSION, - 'server' => $_SERVER['SERVER_SOFTWARE'] ?? getenv('SERVER_SOFTWARE'), - 'sapi_name' => PHP_SAPI, - 'version' => (new Version)->getLongVersion(), - 'useragent' => $_SERVER['HTTP_USER_AGENT'] ?? '', - ]; - - return $this->info; - } - - /** - * Check if the phpinfo function is enabled - * - * @return boolean True if enabled - * - * @since 3.4.1 - */ - public function phpinfoEnabled(): bool - { - return !\in_array('phpinfo', explode(',', ini_get('disable_functions'))); - } - - /** - * Method to get filter data from the model - * - * @param string $dataType Type of data to get safely - * @param bool $public If true no sensitive information will be removed - * - * @return array - * - * @since 3.5 - */ - public function getSafeData(string $dataType, bool $public = true): array - { - if (isset($this->safeData[$dataType])) - { - return $this->safeData[$dataType]; - } - - $methodName = 'get' . ucfirst($dataType); - - if (!method_exists($this, $methodName)) - { - return []; - } - - $data = $this->$methodName($public); - - $this->safeData[$dataType] = $this->cleanPrivateData($data, $dataType); - - return $this->safeData[$dataType]; - } - - /** - * Method to get the PHP info - * - * @return string PHP info - * - * @since 1.6 - */ - public function &getPHPInfo(): string - { - if (!$this->phpinfoEnabled()) - { - $this->php_info = Text::_('COM_ADMIN_PHPINFO_DISABLED'); - - return $this->php_info; - } - - if (!\is_null($this->php_info)) - { - return $this->php_info; - } - - ob_start(); - date_default_timezone_set('UTC'); - phpinfo(INFO_GENERAL | INFO_CONFIGURATION | INFO_MODULES); - $phpInfo = ob_get_contents(); - ob_end_clean(); - preg_match_all('#]*>(.*)#siU', $phpInfo, $output); - $output = preg_replace('#]*>#', '', $output[1][0]); - $output = preg_replace('#(\w),(\w)#', '\1, \2', $output); - $output = preg_replace('#
#', '', $output); - $output = str_replace('
', '', $output); - $output = preg_replace('#
(.*)#', '$1', $output); - $output = str_replace('
', '', $output); - $output = str_replace('', '', $output); - $this->php_info = $output; - - return $this->php_info; - } - - /** - * Get phpinfo() output as array - * - * @return array - * - * @since 3.5 - */ - public function getPhpInfoArray(): array - { - // Already cached - if (null !== $this->phpInfoArray) - { - return $this->phpInfoArray; - } - - $phpInfo = $this->getPHPInfo(); - - $this->phpInfoArray = $this->parsePhpInfo($phpInfo); - - return $this->phpInfoArray; - } - - /** - * Method to get a list of installed extensions - * - * @return array installed extensions - * - * @since 3.5 - */ - public function getExtensions(): array - { - $installed = []; - $db = Factory::getContainer()->get('DatabaseDriver'); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__extensions')); - $db->setQuery($query); - - try - { - $extensions = $db->loadObjectList(); - } - catch (\Exception $e) - { - try - { - Log::add(Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()), - 'warning' - ); - } - - return $installed; - } - - if (empty($extensions)) - { - return $installed; - } - - foreach ($extensions as $extension) - { - if (\strlen($extension->name) == 0) - { - continue; - } - - $installed[$extension->name] = [ - 'name' => $extension->name, - 'type' => $extension->type, - 'state' => $extension->enabled ? Text::_('JENABLED') : Text::_('JDISABLED'), - 'author' => 'unknown', - 'version' => 'unknown', - 'creationDate' => 'unknown', - 'authorUrl' => 'unknown', - ]; - - $manifest = new Registry($extension->manifest_cache); - - $extraData = [ - 'author' => $manifest->get('author', ''), - 'version' => $manifest->get('version', ''), - 'creationDate' => $manifest->get('creationDate', ''), - 'authorUrl' => $manifest->get('authorUrl', '') - ]; - - $installed[$extension->name] = array_merge($installed[$extension->name], $extraData); - } - - return $installed; - } - - /** - * Method to get the directory states - * - * @param bool $public If true no information is going to be removed - * - * @return array States of directories - * - * @throws \Exception - * @since 1.6 - */ - public function getDirectory(bool $public = false): array - { - if (!empty($this->directories)) - { - return $this->directories; - } - - $this->directories = []; - - $registry = Factory::getApplication()->getConfig(); - $cparams = ComponentHelper::getParams('com_media'); - - $this->addDirectory('administrator/components', JPATH_ADMINISTRATOR . '/components'); - $this->addDirectory('administrator/components/com_joomlaupdate', JPATH_ADMINISTRATOR . '/components/com_joomlaupdate'); - $this->addDirectory('administrator/language', JPATH_ADMINISTRATOR . '/language'); - - // List all admin languages - $admin_langs = new \DirectoryIterator(JPATH_ADMINISTRATOR . '/language'); - - foreach ($admin_langs as $folder) - { - if ($folder->isDot() || !$folder->isDir()) - { - continue; - } - - $this->addDirectory( - 'administrator/language/' . $folder->getFilename(), - JPATH_ADMINISTRATOR . '/language/' . $folder->getFilename() - ); - } - - // List all manifests folders - $manifests = new \DirectoryIterator(JPATH_ADMINISTRATOR . '/manifests'); - - foreach ($manifests as $folder) - { - if ($folder->isDot() || !$folder->isDir()) - { - continue; - } - - $this->addDirectory( - 'administrator/manifests/' . $folder->getFilename(), - JPATH_ADMINISTRATOR . '/manifests/' . $folder->getFilename() - ); - } - - $this->addDirectory('administrator/modules', JPATH_ADMINISTRATOR . '/modules'); - $this->addDirectory('administrator/templates', JPATH_THEMES); - - $this->addDirectory('components', JPATH_SITE . '/components'); - - $this->addDirectory($cparams->get('image_path'), JPATH_SITE . '/' . $cparams->get('image_path')); - - // List all images folders - $image_folders = new \DirectoryIterator(JPATH_SITE . '/' . $cparams->get('image_path')); - - foreach ($image_folders as $folder) - { - if ($folder->isDot() || !$folder->isDir()) - { - continue; - } - - $this->addDirectory( - 'images/' . $folder->getFilename(), - JPATH_SITE . '/' . $cparams->get('image_path') . '/' . $folder->getFilename() - ); - } - - $this->addDirectory('language', JPATH_SITE . '/language'); - - // List all site languages - $site_langs = new \DirectoryIterator(JPATH_SITE . '/language'); - - foreach ($site_langs as $folder) - { - if ($folder->isDot() || !$folder->isDir()) - { - continue; - } - - $this->addDirectory('language/' . $folder->getFilename(), JPATH_SITE . '/language/' . $folder->getFilename()); - } - - $this->addDirectory('libraries', JPATH_LIBRARIES); - - $this->addDirectory('media', JPATH_SITE . '/media'); - $this->addDirectory('modules', JPATH_SITE . '/modules'); - $this->addDirectory('plugins', JPATH_PLUGINS); - - $plugin_groups = new \DirectoryIterator(JPATH_SITE . '/plugins'); - - foreach ($plugin_groups as $folder) - { - if ($folder->isDot() || !$folder->isDir()) - { - continue; - } - - $this->addDirectory('plugins/' . $folder->getFilename(), JPATH_PLUGINS . '/' . $folder->getFilename()); - } - - $this->addDirectory('templates', JPATH_SITE . '/templates'); - $this->addDirectory('configuration.php', JPATH_CONFIGURATION . '/configuration.php'); - - // Is there a cache path in configuration.php? - if ($cache_path = trim($registry->get('cache_path', ''))) - { - // Frontend and backend use same directory for caching. - $this->addDirectory($cache_path, $cache_path, 'COM_ADMIN_CACHE_DIRECTORY'); - } - else - { - $this->addDirectory('administrator/cache', JPATH_CACHE, 'COM_ADMIN_CACHE_DIRECTORY'); - } - - $this->addDirectory('media/cache', JPATH_ROOT . '/media/cache', 'COM_ADMIN_MEDIA_CACHE_DIRECTORY'); - - if ($public) - { - $this->addDirectory( - 'log', - $registry->get('log_path', JPATH_ADMINISTRATOR . '/logs'), - 'COM_ADMIN_LOG_DIRECTORY' - ); - $this->addDirectory( - 'tmp', - $registry->get('tmp_path', JPATH_ROOT . '/tmp'), - 'COM_ADMIN_TEMP_DIRECTORY' - ); - } - else - { - $this->addDirectory( - $registry->get('log_path', JPATH_ADMINISTRATOR . '/logs'), - $registry->get('log_path', JPATH_ADMINISTRATOR . '/logs'), - 'COM_ADMIN_LOG_DIRECTORY' - ); - $this->addDirectory( - $registry->get('tmp_path', JPATH_ROOT . '/tmp'), - $registry->get('tmp_path', JPATH_ROOT . '/tmp'), - 'COM_ADMIN_TEMP_DIRECTORY' - ); - } - - return $this->directories; - } - - /** - * Method to add a directory - * - * @param string $name Directory Name - * @param string $path Directory path - * @param string $message Message - * - * @return void - * - * @since 1.6 - */ - private function addDirectory(string $name, string $path, string $message = ''): void - { - $this->directories[$name] = ['writable' => is_writable($path), 'message' => $message,]; - } - - /** - * Method to get the editor - * - * @return string The default editor - * - * @note Has to be removed (it is present in the config...) - * @since 1.6 - */ - public function &getEditor(): string - { - if (!is_null($this->editor)) - { - return $this->editor; - } - - $this->editor = Factory::getApplication()->get('editor'); - - return $this->editor; - } - - /** - * Parse phpinfo output into an array - * Source https://gist.github.com/sbmzhcn/6255314 - * - * @param string $html Output of phpinfo() - * - * @return array - * - * @since 3.5 - */ - protected function parsePhpInfo(string $html): array - { - $html = strip_tags($html, '

'); - $html = preg_replace('/]*>([^<]+)<\/th>/', '\1', $html); - $html = preg_replace('/]*>([^<]+)<\/td>/', '\1', $html); - $t = preg_split('/(]*>[^<]+<\/h2>)/', $html, -1, PREG_SPLIT_DELIM_CAPTURE); - $r = []; - $count = \count($t); - $p1 = '([^<]+)<\/info>'; - $p2 = '/' . $p1 . '\s*' . $p1 . '\s*' . $p1 . '/'; - $p3 = '/' . $p1 . '\s*' . $p1 . '/'; - - for ($i = 1; $i < $count; $i++) - { - if (preg_match('/]*>([^<]+)<\/h2>/', $t[$i], $matches)) - { - $name = trim($matches[1]); - $vals = explode("\n", $t[$i + 1]); - - foreach ($vals AS $val) - { - // 3cols - if (preg_match($p2, $val, $matches)) - { - $r[$name][trim($matches[1])] = [trim($matches[2]), trim($matches[3]),]; - } - // 2cols - elseif (preg_match($p3, $val, $matches)) - { - $r[$name][trim($matches[1])] = trim($matches[2]); - } - } - } - } - - return $r; - } + /** + * Some PHP settings + * + * @var array + * @since 1.6 + */ + protected $php_settings = []; + + /** + * Config values + * + * @var array + * @since 1.6 + */ + protected $config = []; + + /** + * Some system values + * + * @var array + * @since 1.6 + */ + protected $info = []; + + /** + * PHP info + * + * @var string + * @since 1.6 + */ + protected $php_info = null; + + /** + * Array containing the phpinfo() data. + * + * @var array + * + * @since 3.5 + */ + protected $phpInfoArray; + + /** + * Private/critical data that we don't want to share + * + * @var array + * + * @since 3.5 + */ + protected $privateSettings = [ + 'phpInfoArray' => [ + 'CONTEXT_DOCUMENT_ROOT', + 'Cookie', + 'DOCUMENT_ROOT', + 'extension_dir', + 'error_log', + 'Host', + 'HTTP_COOKIE', + 'HTTP_HOST', + 'HTTP_ORIGIN', + 'HTTP_REFERER', + 'HTTP Request', + 'include_path', + 'mysql.default_socket', + 'MYSQL_SOCKET', + 'MYSQL_INCLUDE', + 'MYSQL_LIBS', + 'mysqli.default_socket', + 'MYSQLI_SOCKET', + 'PATH', + 'Path to sendmail', + 'pdo_mysql.default_socket', + 'Referer', + 'REMOTE_ADDR', + 'SCRIPT_FILENAME', + 'sendmail_path', + 'SERVER_ADDR', + 'SERVER_ADMIN', + 'Server Administrator', + 'SERVER_NAME', + 'Server Root', + 'session.name', + 'session.save_path', + 'upload_tmp_dir', + 'User/Group', + 'open_basedir', + ], + 'other' => [ + 'db', + 'dbprefix', + 'fromname', + 'live_site', + 'log_path', + 'mailfrom', + 'memcached_server_host', + 'open_basedir', + 'Origin', + 'proxy_host', + 'proxy_user', + 'proxy_pass', + 'redis_server_host', + 'redis_server_auth', + 'secret', + 'sendmail', + 'session.save_path', + 'session_memcached_server_host', + 'session_redis_server_host', + 'session_redis_server_auth', + 'sitename', + 'smtphost', + 'tmp_path', + 'open_basedir', + ] + ]; + + /** + * System values that can be "safely" shared + * + * @var array + * + * @since 3.5 + */ + protected $safeData; + + /** + * Information about writable state of directories + * + * @var array + * @since 1.6 + */ + protected $directories = []; + + /** + * The current editor. + * + * @var string + * @since 1.6 + */ + protected $editor = null; + + /** + * Remove sections of data marked as private in the privateSettings + * + * @param array $dataArray Array with data that may contain private information + * @param string $dataType Type of data to search for a specific section in the privateSettings array + * + * @return array + * + * @since 3.5 + */ + protected function cleanPrivateData(array $dataArray, string $dataType = 'other'): array + { + $dataType = isset($this->privateSettings[$dataType]) ? $dataType : 'other'; + + $privateSettings = $this->privateSettings[$dataType]; + + if (!$privateSettings) { + return $dataArray; + } + + foreach ($dataArray as $section => $values) { + if (\is_array($values)) { + $dataArray[$section] = $this->cleanPrivateData($values, $dataType); + } + + if (\in_array($section, $privateSettings, true)) { + $dataArray[$section] = $this->cleanSectionPrivateData($values); + } + } + + return $dataArray; + } + + /** + * Obfuscate section values + * + * @param mixed $sectionValues Section data + * + * @return string|array + * + * @since 3.5 + */ + protected function cleanSectionPrivateData($sectionValues) + { + if (!\is_array($sectionValues)) { + if (strstr($sectionValues, JPATH_ROOT)) { + $sectionValues = 'xxxxxx'; + } + + return \strlen($sectionValues) ? 'xxxxxx' : ''; + } + + foreach ($sectionValues as $setting => $value) { + $sectionValues[$setting] = \strlen($value) ? 'xxxxxx' : ''; + } + + return $sectionValues; + } + + /** + * Method to get the PHP settings + * + * @return array Some PHP settings + * + * @since 1.6 + */ + public function &getPhpSettings(): array + { + if (!empty($this->php_settings)) { + return $this->php_settings; + } + + $this->php_settings = [ + 'memory_limit' => ini_get('memory_limit'), + 'upload_max_filesize' => ini_get('upload_max_filesize'), + 'post_max_size' => ini_get('post_max_size'), + 'display_errors' => ini_get('display_errors') == '1', + 'short_open_tag' => ini_get('short_open_tag') == '1', + 'file_uploads' => ini_get('file_uploads') == '1', + 'output_buffering' => (int) ini_get('output_buffering') !== 0, + 'open_basedir' => ini_get('open_basedir'), + 'session.save_path' => ini_get('session.save_path'), + 'session.auto_start' => ini_get('session.auto_start'), + 'disable_functions' => ini_get('disable_functions'), + 'xml' => \extension_loaded('xml'), + 'zlib' => \extension_loaded('zlib'), + 'zip' => \function_exists('zip_open') && \function_exists('zip_read'), + 'mbstring' => \extension_loaded('mbstring'), + 'fileinfo' => \extension_loaded('fileinfo'), + 'gd' => \extension_loaded('gd'), + 'iconv' => \function_exists('iconv'), + 'intl' => \function_exists('transliterator_transliterate'), + 'max_input_vars' => ini_get('max_input_vars'), + ]; + + return $this->php_settings; + } + + /** + * Method to get the config + * + * @return array config values + * + * @since 1.6 + */ + public function &getConfig(): array + { + if (!empty($this->config)) { + return $this->config; + } + + $registry = new Registry(new \JConfig()); + $this->config = $registry->toArray(); + $hidden = [ + 'host', 'user', 'password', 'ftp_user', 'ftp_pass', + 'smtpuser', 'smtppass', 'redis_server_auth', 'session_redis_server_auth', + 'proxy_user', 'proxy_pass', 'secret' + ]; + + foreach ($hidden as $key) { + $this->config[$key] = 'xxxxxx'; + } + + return $this->config; + } + + /** + * Method to get the system information + * + * @return array System information values + * + * @since 1.6 + */ + public function &getInfo(): array + { + if (!empty($this->info)) { + return $this->info; + } + + $db = $this->getDatabase(); + + $this->info = [ + 'php' => php_uname(), + 'dbserver' => $db->getServerType(), + 'dbversion' => $db->getVersion(), + 'dbcollation' => $db->getCollation(), + 'dbconnectioncollation' => $db->getConnectionCollation(), + 'dbconnectionencryption' => $db->getConnectionEncryption(), + 'dbconnencryptsupported' => $db->isConnectionEncryptionSupported(), + 'phpversion' => PHP_VERSION, + 'server' => $_SERVER['SERVER_SOFTWARE'] ?? getenv('SERVER_SOFTWARE'), + 'sapi_name' => PHP_SAPI, + 'version' => (new Version())->getLongVersion(), + 'useragent' => $_SERVER['HTTP_USER_AGENT'] ?? '', + ]; + + return $this->info; + } + + /** + * Check if the phpinfo function is enabled + * + * @return boolean True if enabled + * + * @since 3.4.1 + */ + public function phpinfoEnabled(): bool + { + return !\in_array('phpinfo', explode(',', ini_get('disable_functions'))); + } + + /** + * Method to get filter data from the model + * + * @param string $dataType Type of data to get safely + * @param bool $public If true no sensitive information will be removed + * + * @return array + * + * @since 3.5 + */ + public function getSafeData(string $dataType, bool $public = true): array + { + if (isset($this->safeData[$dataType])) { + return $this->safeData[$dataType]; + } + + $methodName = 'get' . ucfirst($dataType); + + if (!method_exists($this, $methodName)) { + return []; + } + + $data = $this->$methodName($public); + + $this->safeData[$dataType] = $this->cleanPrivateData($data, $dataType); + + return $this->safeData[$dataType]; + } + + /** + * Method to get the PHP info + * + * @return string PHP info + * + * @since 1.6 + */ + public function &getPHPInfo(): string + { + if (!$this->phpinfoEnabled()) { + $this->php_info = Text::_('COM_ADMIN_PHPINFO_DISABLED'); + + return $this->php_info; + } + + if (!\is_null($this->php_info)) { + return $this->php_info; + } + + ob_start(); + date_default_timezone_set('UTC'); + phpinfo(INFO_GENERAL | INFO_CONFIGURATION | INFO_MODULES); + $phpInfo = ob_get_contents(); + ob_end_clean(); + preg_match_all('#]*>(.*)#siU', $phpInfo, $output); + $output = preg_replace('#]*>#', '', $output[1][0]); + $output = preg_replace('#(\w),(\w)#', '\1, \2', $output); + $output = preg_replace('#
#', '', $output); + $output = str_replace('
', '', $output); + $output = preg_replace('#
(.*)#', '$1', $output); + $output = str_replace('
', '', $output); + $output = str_replace('', '', $output); + $this->php_info = $output; + + return $this->php_info; + } + + /** + * Get phpinfo() output as array + * + * @return array + * + * @since 3.5 + */ + public function getPhpInfoArray(): array + { + // Already cached + if (null !== $this->phpInfoArray) { + return $this->phpInfoArray; + } + + $phpInfo = $this->getPHPInfo(); + + $this->phpInfoArray = $this->parsePhpInfo($phpInfo); + + return $this->phpInfoArray; + } + + /** + * Method to get a list of installed extensions + * + * @return array installed extensions + * + * @since 3.5 + */ + public function getExtensions(): array + { + $installed = []; + $db = Factory::getContainer()->get('DatabaseDriver'); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__extensions')); + $db->setQuery($query); + + try { + $extensions = $db->loadObjectList(); + } catch (\Exception $e) { + try { + Log::add(Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage( + Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()), + 'warning' + ); + } + + return $installed; + } + + if (empty($extensions)) { + return $installed; + } + + foreach ($extensions as $extension) { + if (\strlen($extension->name) == 0) { + continue; + } + + $installed[$extension->name] = [ + 'name' => $extension->name, + 'type' => $extension->type, + 'state' => $extension->enabled ? Text::_('JENABLED') : Text::_('JDISABLED'), + 'author' => 'unknown', + 'version' => 'unknown', + 'creationDate' => 'unknown', + 'authorUrl' => 'unknown', + ]; + + $manifest = new Registry($extension->manifest_cache); + + $extraData = [ + 'author' => $manifest->get('author', ''), + 'version' => $manifest->get('version', ''), + 'creationDate' => $manifest->get('creationDate', ''), + 'authorUrl' => $manifest->get('authorUrl', '') + ]; + + $installed[$extension->name] = array_merge($installed[$extension->name], $extraData); + } + + return $installed; + } + + /** + * Method to get the directory states + * + * @param bool $public If true no information is going to be removed + * + * @return array States of directories + * + * @throws \Exception + * @since 1.6 + */ + public function getDirectory(bool $public = false): array + { + if (!empty($this->directories)) { + return $this->directories; + } + + $this->directories = []; + + $registry = Factory::getApplication()->getConfig(); + $cparams = ComponentHelper::getParams('com_media'); + + $this->addDirectory('administrator/components', JPATH_ADMINISTRATOR . '/components'); + $this->addDirectory('administrator/components/com_joomlaupdate', JPATH_ADMINISTRATOR . '/components/com_joomlaupdate'); + $this->addDirectory('administrator/language', JPATH_ADMINISTRATOR . '/language'); + + // List all admin languages + $admin_langs = new \DirectoryIterator(JPATH_ADMINISTRATOR . '/language'); + + foreach ($admin_langs as $folder) { + if ($folder->isDot() || !$folder->isDir()) { + continue; + } + + $this->addDirectory( + 'administrator/language/' . $folder->getFilename(), + JPATH_ADMINISTRATOR . '/language/' . $folder->getFilename() + ); + } + + // List all manifests folders + $manifests = new \DirectoryIterator(JPATH_ADMINISTRATOR . '/manifests'); + + foreach ($manifests as $folder) { + if ($folder->isDot() || !$folder->isDir()) { + continue; + } + + $this->addDirectory( + 'administrator/manifests/' . $folder->getFilename(), + JPATH_ADMINISTRATOR . '/manifests/' . $folder->getFilename() + ); + } + + $this->addDirectory('administrator/modules', JPATH_ADMINISTRATOR . '/modules'); + $this->addDirectory('administrator/templates', JPATH_THEMES); + + $this->addDirectory('components', JPATH_SITE . '/components'); + + $this->addDirectory($cparams->get('image_path'), JPATH_SITE . '/' . $cparams->get('image_path')); + + // List all images folders + $image_folders = new \DirectoryIterator(JPATH_SITE . '/' . $cparams->get('image_path')); + + foreach ($image_folders as $folder) { + if ($folder->isDot() || !$folder->isDir()) { + continue; + } + + $this->addDirectory( + 'images/' . $folder->getFilename(), + JPATH_SITE . '/' . $cparams->get('image_path') . '/' . $folder->getFilename() + ); + } + + $this->addDirectory('language', JPATH_SITE . '/language'); + + // List all site languages + $site_langs = new \DirectoryIterator(JPATH_SITE . '/language'); + + foreach ($site_langs as $folder) { + if ($folder->isDot() || !$folder->isDir()) { + continue; + } + + $this->addDirectory('language/' . $folder->getFilename(), JPATH_SITE . '/language/' . $folder->getFilename()); + } + + $this->addDirectory('libraries', JPATH_LIBRARIES); + + $this->addDirectory('media', JPATH_SITE . '/media'); + $this->addDirectory('modules', JPATH_SITE . '/modules'); + $this->addDirectory('plugins', JPATH_PLUGINS); + + $plugin_groups = new \DirectoryIterator(JPATH_SITE . '/plugins'); + + foreach ($plugin_groups as $folder) { + if ($folder->isDot() || !$folder->isDir()) { + continue; + } + + $this->addDirectory('plugins/' . $folder->getFilename(), JPATH_PLUGINS . '/' . $folder->getFilename()); + } + + $this->addDirectory('templates', JPATH_SITE . '/templates'); + $this->addDirectory('configuration.php', JPATH_CONFIGURATION . '/configuration.php'); + + // Is there a cache path in configuration.php? + if ($cache_path = trim($registry->get('cache_path', ''))) { + // Frontend and backend use same directory for caching. + $this->addDirectory($cache_path, $cache_path, 'COM_ADMIN_CACHE_DIRECTORY'); + } else { + $this->addDirectory('administrator/cache', JPATH_CACHE, 'COM_ADMIN_CACHE_DIRECTORY'); + } + + $this->addDirectory('media/cache', JPATH_ROOT . '/media/cache', 'COM_ADMIN_MEDIA_CACHE_DIRECTORY'); + + if ($public) { + $this->addDirectory( + 'log', + $registry->get('log_path', JPATH_ADMINISTRATOR . '/logs'), + 'COM_ADMIN_LOG_DIRECTORY' + ); + $this->addDirectory( + 'tmp', + $registry->get('tmp_path', JPATH_ROOT . '/tmp'), + 'COM_ADMIN_TEMP_DIRECTORY' + ); + } else { + $this->addDirectory( + $registry->get('log_path', JPATH_ADMINISTRATOR . '/logs'), + $registry->get('log_path', JPATH_ADMINISTRATOR . '/logs'), + 'COM_ADMIN_LOG_DIRECTORY' + ); + $this->addDirectory( + $registry->get('tmp_path', JPATH_ROOT . '/tmp'), + $registry->get('tmp_path', JPATH_ROOT . '/tmp'), + 'COM_ADMIN_TEMP_DIRECTORY' + ); + } + + return $this->directories; + } + + /** + * Method to add a directory + * + * @param string $name Directory Name + * @param string $path Directory path + * @param string $message Message + * + * @return void + * + * @since 1.6 + */ + private function addDirectory(string $name, string $path, string $message = ''): void + { + $this->directories[$name] = ['writable' => is_writable($path), 'message' => $message,]; + } + + /** + * Method to get the editor + * + * @return string The default editor + * + * @note Has to be removed (it is present in the config...) + * @since 1.6 + */ + public function &getEditor(): string + { + if (!is_null($this->editor)) { + return $this->editor; + } + + $this->editor = Factory::getApplication()->get('editor'); + + return $this->editor; + } + + /** + * Parse phpinfo output into an array + * Source https://gist.github.com/sbmzhcn/6255314 + * + * @param string $html Output of phpinfo() + * + * @return array + * + * @since 3.5 + */ + protected function parsePhpInfo(string $html): array + { + $html = strip_tags($html, '

'); + $html = preg_replace('/]*>([^<]+)<\/th>/', '\1', $html); + $html = preg_replace('/]*>([^<]+)<\/td>/', '\1', $html); + $t = preg_split('/(]*>[^<]+<\/h2>)/', $html, -1, PREG_SPLIT_DELIM_CAPTURE); + $r = []; + $count = \count($t); + $p1 = '([^<]+)<\/info>'; + $p2 = '/' . $p1 . '\s*' . $p1 . '\s*' . $p1 . '/'; + $p3 = '/' . $p1 . '\s*' . $p1 . '/'; + + for ($i = 1; $i < $count; $i++) { + if (preg_match('/]*>([^<]+)<\/h2>/', $t[$i], $matches)) { + $name = trim($matches[1]); + $vals = explode("\n", $t[$i + 1]); + + foreach ($vals as $val) { + // 3cols + if (preg_match($p2, $val, $matches)) { + $r[$name][trim($matches[1])] = [trim($matches[2]), trim($matches[3]),]; + } elseif (preg_match($p3, $val, $matches)) { + // 2cols + $r[$name][trim($matches[1])] = trim($matches[2]); + } + } + } + } + + return $r; + } } diff --git a/administrator/components/com_admin/src/Service/HTML/Directory.php b/administrator/components/com_admin/src/Service/HTML/Directory.php index f5de97a6ca568..0bed0f675e2a9 100644 --- a/administrator/components/com_admin/src/Service/HTML/Directory.php +++ b/administrator/components/com_admin/src/Service/HTML/Directory.php @@ -1,4 +1,5 @@ ' . Text::_('COM_ADMIN_WRITABLE') . ''; - } - - return '' . Text::_('COM_ADMIN_UNWRITABLE') . ''; - } - - /** - * Method to generate a message for a directory - * - * @param string $dir the directory - * @param boolean $message the message - * @param boolean $visible is the $dir visible? - * - * @return string html code - */ - public function message($dir, $message, $visible = true) - { - $output = $visible ? $dir : ''; - - if (empty($message)) - { - return $output; - } - - return $output . ' ' . Text::_($message) . ''; - } + /** + * Method to generate a (un)writable message for directory + * + * @param boolean $writable is the directory writable? + * + * @return string html code + */ + public function writable($writable) + { + if ($writable) { + return '' . Text::_('COM_ADMIN_WRITABLE') . ''; + } + + return '' . Text::_('COM_ADMIN_UNWRITABLE') . ''; + } + + /** + * Method to generate a message for a directory + * + * @param string $dir the directory + * @param boolean $message the message + * @param boolean $visible is the $dir visible? + * + * @return string html code + */ + public function message($dir, $message, $visible = true) + { + $output = $visible ? $dir : ''; + + if (empty($message)) { + return $output; + } + + return $output . ' ' . Text::_($message) . ''; + } } diff --git a/administrator/components/com_admin/src/Service/HTML/PhpSetting.php b/administrator/components/com_admin/src/Service/HTML/PhpSetting.php index 8d4a1cd602dcb..903c781d498ea 100644 --- a/administrator/components/com_admin/src/Service/HTML/PhpSetting.php +++ b/administrator/components/com_admin/src/Service/HTML/PhpSetting.php @@ -1,4 +1,5 @@ getModel(); - $this->helpSearch = $model->getHelpSearch(); - $this->page = $model->getPage(); - $this->toc = $model->getToc(); - $this->languageTag = $model->getLangTag(); + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + * + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var HelpModel $model */ + $model = $this->getModel(); + $this->helpSearch = $model->getHelpSearch(); + $this->page = $model->getPage(); + $this->toc = $model->getToc(); + $this->languageTag = $model->getLangTag(); - $this->addToolbar(); + $this->addToolbar(); - parent::display($tpl); - } + parent::display($tpl); + } - /** - * Setup the Toolbar - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar(): void - { - ToolbarHelper::title(Text::_('COM_ADMIN_HELP'), 'support help_header'); - } + /** + * Setup the Toolbar + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar(): void + { + ToolbarHelper::title(Text::_('COM_ADMIN_HELP'), 'support help_header'); + } } diff --git a/administrator/components/com_admin/src/View/Sysinfo/HtmlView.php b/administrator/components/com_admin/src/View/Sysinfo/HtmlView.php index 4e7dca1227313..0fadf0891c4d0 100644 --- a/administrator/components/com_admin/src/View/Sysinfo/HtmlView.php +++ b/administrator/components/com_admin/src/View/Sysinfo/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser()->authorise('core.admin')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + * + * @throws Exception + */ + public function display($tpl = null): void + { + // Access check. + if (!$this->getCurrentUser()->authorise('core.admin')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } - /** @var SysinfoModel $model */ - $model = $this->getModel(); - $this->phpSettings = $model->getPhpSettings(); - $this->config = $model->getConfig(); - $this->info = $model->getInfo(); - $this->phpInfo = $model->getPHPInfo(); - $this->directory = $model->getDirectory(); + /** @var SysinfoModel $model */ + $model = $this->getModel(); + $this->phpSettings = $model->getPhpSettings(); + $this->config = $model->getConfig(); + $this->info = $model->getInfo(); + $this->phpInfo = $model->getPHPInfo(); + $this->directory = $model->getDirectory(); - $this->addToolbar(); + $this->addToolbar(); - parent::display($tpl); - } + parent::display($tpl); + } - /** - * Setup the Toolbar - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar(): void - { - ToolbarHelper::title(Text::_('COM_ADMIN_SYSTEM_INFORMATION'), 'info-circle systeminfo'); - ToolbarHelper::link( - Route::_('index.php?option=com_admin&view=sysinfo&format=text&' . Session::getFormToken() . '=1'), - 'COM_ADMIN_DOWNLOAD_SYSTEM_INFORMATION_TEXT', - 'download' - ); - ToolbarHelper::link( - Route::_('index.php?option=com_admin&view=sysinfo&format=json&' . Session::getFormToken() . '=1'), - 'COM_ADMIN_DOWNLOAD_SYSTEM_INFORMATION_JSON', - 'download' - ); - ToolbarHelper::help('Site_System_Information'); - } + /** + * Setup the Toolbar + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar(): void + { + ToolbarHelper::title(Text::_('COM_ADMIN_SYSTEM_INFORMATION'), 'info-circle systeminfo'); + ToolbarHelper::link( + Route::_('index.php?option=com_admin&view=sysinfo&format=text&' . Session::getFormToken() . '=1'), + 'COM_ADMIN_DOWNLOAD_SYSTEM_INFORMATION_TEXT', + 'download' + ); + ToolbarHelper::link( + Route::_('index.php?option=com_admin&view=sysinfo&format=json&' . Session::getFormToken() . '=1'), + 'COM_ADMIN_DOWNLOAD_SYSTEM_INFORMATION_JSON', + 'download' + ); + ToolbarHelper::help('Site_System_Information'); + } } diff --git a/administrator/components/com_admin/src/View/Sysinfo/JsonView.php b/administrator/components/com_admin/src/View/Sysinfo/JsonView.php index 116a0875c9bcb..b3d921509b363 100644 --- a/administrator/components/com_admin/src/View/Sysinfo/JsonView.php +++ b/administrator/components/com_admin/src/View/Sysinfo/JsonView.php @@ -1,4 +1,5 @@ authorise('core.admin')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 3.5 + * + * @throws Exception + */ + public function display($tpl = null): void + { + // Access check. + if (!Factory::getUser()->authorise('core.admin')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } - header('MIME-Version: 1.0'); - header('Content-Disposition: attachment; filename="systeminfo-' . date('c') . '.json"'); - header('Content-Transfer-Encoding: binary'); + header('MIME-Version: 1.0'); + header('Content-Disposition: attachment; filename="systeminfo-' . date('c') . '.json"'); + header('Content-Transfer-Encoding: binary'); - $data = $this->getLayoutData(); + $data = $this->getLayoutData(); - echo json_encode($data, JSON_PRETTY_PRINT); + echo json_encode($data, JSON_PRETTY_PRINT); - Factory::getApplication()->close(); - } + Factory::getApplication()->close(); + } - /** - * Get the data for the view - * - * @return array - * - * @since 3.5 - */ - protected function getLayoutData(): array - { - /** @var SysinfoModel $model */ - $model = $this->getModel(); + /** + * Get the data for the view + * + * @return array + * + * @since 3.5 + */ + protected function getLayoutData(): array + { + /** @var SysinfoModel $model */ + $model = $this->getModel(); - return [ - 'info' => $model->getSafeData('info'), - 'phpSettings' => $model->getSafeData('phpSettings'), - 'config' => $model->getSafeData('config'), - 'directories' => $model->getSafeData('directory', true), - 'phpInfo' => $model->getSafeData('phpInfoArray'), - 'extensions' => $model->getSafeData('extensions') - ]; - } + return [ + 'info' => $model->getSafeData('info'), + 'phpSettings' => $model->getSafeData('phpSettings'), + 'config' => $model->getSafeData('config'), + 'directories' => $model->getSafeData('directory', true), + 'phpInfo' => $model->getSafeData('phpInfoArray'), + 'extensions' => $model->getSafeData('extensions') + ]; + } } diff --git a/administrator/components/com_admin/src/View/Sysinfo/TextView.php b/administrator/components/com_admin/src/View/Sysinfo/TextView.php index 6da1078e7c417..ec4d1d579d68d 100644 --- a/administrator/components/com_admin/src/View/Sysinfo/TextView.php +++ b/administrator/components/com_admin/src/View/Sysinfo/TextView.php @@ -1,4 +1,5 @@ authorise('core.admin')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - header('Content-Type: text/plain; charset=utf-8'); - header('Content-Description: File Transfer'); - header('Content-Disposition: attachment; filename="systeminfo-' . date('c') . '.txt"'); - header('Cache-Control: must-revalidate'); - - $data = $this->getLayoutData(); - - $lines = []; - - foreach ($data as $sectionName => $section) - { - $customRenderingMethod = 'render' . ucfirst($sectionName); - - if (method_exists($this, $customRenderingMethod)) - { - $lines[] = $this->$customRenderingMethod($section['title'], $section['data']); - } - else - { - $lines[] = $this->renderSection($section['title'], $section['data']); - } - } - - echo str_replace(JPATH_ROOT, 'xxxxxx', implode("\n\n", $lines)); - - Factory::getApplication()->close(); - } - - /** - * Get the data for the view - * - * @return array - * - * @since 3.5 - */ - protected function getLayoutData(): array - { - /** @var SysinfoModel $model */ - $model = $this->getModel(); - - return [ - 'info' => [ - 'title' => Text::_('COM_ADMIN_SYSTEM_INFORMATION', true), - 'data' => $model->getSafeData('info') - ], - 'phpSettings' => [ - 'title' => Text::_('COM_ADMIN_PHP_SETTINGS', true), - 'data' => $model->getSafeData('phpSettings') - ], - 'config' => [ - 'title' => Text::_('COM_ADMIN_CONFIGURATION_FILE', true), - 'data' => $model->getSafeData('config') - ], - 'directories' => [ - 'title' => Text::_('COM_ADMIN_DIRECTORY_PERMISSIONS', true), - 'data' => $model->getSafeData('directory', true) - ], - 'phpInfo' => [ - 'title' => Text::_('COM_ADMIN_PHP_INFORMATION', true), - 'data' => $model->getSafeData('phpInfoArray') - ], - 'extensions' => [ - 'title' => Text::_('COM_ADMIN_EXTENSIONS', true), - 'data' => $model->getSafeData('extensions') - ] - ]; - } - - /** - * Render a section - * - * @param string $sectionName Name of the section to render - * @param array $sectionData Data of the section to render - * @param integer $level Depth level for indentation - * - * @return string - * - * @since 3.5 - */ - protected function renderSection(string $sectionName, array $sectionData, int $level = 0): string - { - $lines = []; - - $margin = ($level > 0) ? str_repeat("\t", $level) : null; - - $lines[] = $margin . '============='; - $lines[] = $margin . $sectionName; - $lines[] = $margin . '============='; - $level++; - - foreach ($sectionData as $name => $value) - { - if (\is_array($value)) - { - if ($name == 'Directive') - { - continue; - } - - $lines[] = ''; - $lines[] = $this->renderSection($name, $value, $level); - } - else - { - if (\is_bool($value)) - { - $value = $value ? 'true' : 'false'; - } - - if (\is_int($name) && ($name == 0 || $name == 1)) - { - $name = ($name == 0 ? 'Local Value' : 'Master Value'); - } - - $lines[] = $margin . $name . ': ' . $value; - } - } - - return implode("\n", $lines); - } - - /** - * Specific rendering for directories - * - * @param string $sectionName Name of the section - * @param array $sectionData Directories information - * @param integer $level Starting level - * - * @return string - * - * @since 3.5 - */ - protected function renderDirectories(string $sectionName, array $sectionData, int $level = -1): string - { - foreach ($sectionData as $directory => $data) - { - $sectionData[$directory] = $data['writable'] ? ' writable' : ' NOT writable'; - } - - return $this->renderSection($sectionName, $sectionData, $level); - } + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + * + * @since 3.5 + * + * @throws Exception + */ + public function display($tpl = null): void + { + // Access check. + if (!Factory::getUser()->authorise('core.admin')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + header('Content-Type: text/plain; charset=utf-8'); + header('Content-Description: File Transfer'); + header('Content-Disposition: attachment; filename="systeminfo-' . date('c') . '.txt"'); + header('Cache-Control: must-revalidate'); + + $data = $this->getLayoutData(); + + $lines = []; + + foreach ($data as $sectionName => $section) { + $customRenderingMethod = 'render' . ucfirst($sectionName); + + if (method_exists($this, $customRenderingMethod)) { + $lines[] = $this->$customRenderingMethod($section['title'], $section['data']); + } else { + $lines[] = $this->renderSection($section['title'], $section['data']); + } + } + + echo str_replace(JPATH_ROOT, 'xxxxxx', implode("\n\n", $lines)); + + Factory::getApplication()->close(); + } + + /** + * Get the data for the view + * + * @return array + * + * @since 3.5 + */ + protected function getLayoutData(): array + { + /** @var SysinfoModel $model */ + $model = $this->getModel(); + + return [ + 'info' => [ + 'title' => Text::_('COM_ADMIN_SYSTEM_INFORMATION', true), + 'data' => $model->getSafeData('info') + ], + 'phpSettings' => [ + 'title' => Text::_('COM_ADMIN_PHP_SETTINGS', true), + 'data' => $model->getSafeData('phpSettings') + ], + 'config' => [ + 'title' => Text::_('COM_ADMIN_CONFIGURATION_FILE', true), + 'data' => $model->getSafeData('config') + ], + 'directories' => [ + 'title' => Text::_('COM_ADMIN_DIRECTORY_PERMISSIONS', true), + 'data' => $model->getSafeData('directory', true) + ], + 'phpInfo' => [ + 'title' => Text::_('COM_ADMIN_PHP_INFORMATION', true), + 'data' => $model->getSafeData('phpInfoArray') + ], + 'extensions' => [ + 'title' => Text::_('COM_ADMIN_EXTENSIONS', true), + 'data' => $model->getSafeData('extensions') + ] + ]; + } + + /** + * Render a section + * + * @param string $sectionName Name of the section to render + * @param array $sectionData Data of the section to render + * @param integer $level Depth level for indentation + * + * @return string + * + * @since 3.5 + */ + protected function renderSection(string $sectionName, array $sectionData, int $level = 0): string + { + $lines = []; + + $margin = ($level > 0) ? str_repeat("\t", $level) : null; + + $lines[] = $margin . '============='; + $lines[] = $margin . $sectionName; + $lines[] = $margin . '============='; + $level++; + + foreach ($sectionData as $name => $value) { + if (\is_array($value)) { + if ($name == 'Directive') { + continue; + } + + $lines[] = ''; + $lines[] = $this->renderSection($name, $value, $level); + } else { + if (\is_bool($value)) { + $value = $value ? 'true' : 'false'; + } + + if (\is_int($name) && ($name == 0 || $name == 1)) { + $name = ($name == 0 ? 'Local Value' : 'Master Value'); + } + + $lines[] = $margin . $name . ': ' . $value; + } + } + + return implode("\n", $lines); + } + + /** + * Specific rendering for directories + * + * @param string $sectionName Name of the section + * @param array $sectionData Directories information + * @param integer $level Starting level + * + * @return string + * + * @since 3.5 + */ + protected function renderDirectories(string $sectionName, array $sectionData, int $level = -1): string + { + foreach ($sectionData as $directory => $data) { + $sectionData[$directory] = $data['writable'] ? ' writable' : ' NOT writable'; + } + + return $this->renderSection($sectionName, $sectionData, $level); + } } diff --git a/administrator/components/com_admin/tmpl/help/default.php b/administrator/components/com_admin/tmpl/help/default.php index 1cdac79740241..1cfe5e816f6b3 100644 --- a/administrator/components/com_admin/tmpl/help/default.php +++ b/administrator/components/com_admin/tmpl/help/default.php @@ -1,4 +1,5 @@
-
- -
- -
-
- +
+ +
+ +
+
+
diff --git a/administrator/components/com_admin/tmpl/help/langforum.php b/administrator/components/com_admin/tmpl/help/langforum.php index 3e4b0782978ba..979c726533351 100644 --- a/administrator/components/com_admin/tmpl/help/langforum.php +++ b/administrator/components/com_admin/tmpl/help/langforum.php @@ -1,4 +1,5 @@
- 'site', 'recall' => true, 'breakpoint' => 768]); ?> + 'site', 'recall' => true, 'breakpoint' => 768]); ?> - - loadTemplate('system'); ?> - + + loadTemplate('system'); ?> + - - loadTemplate('phpsettings'); ?> - + + loadTemplate('phpsettings'); ?> + - - loadTemplate('config'); ?> - + + loadTemplate('config'); ?> + - - loadTemplate('directory'); ?> - + + loadTemplate('directory'); ?> + - - loadTemplate('phpinfo'); ?> - + + loadTemplate('phpinfo'); ?> + - +
diff --git a/administrator/components/com_admin/tmpl/sysinfo/default_config.php b/administrator/components/com_admin/tmpl/sysinfo/default_config.php index 49e3f14b38fec..9cc7f0d5dcba4 100644 --- a/administrator/components/com_admin/tmpl/sysinfo/default_config.php +++ b/administrator/components/com_admin/tmpl/sysinfo/default_config.php @@ -1,4 +1,5 @@
- - - - - - - - - - config as $key => $value) : ?> - - - - - - -
- -
- - - -
- - - -
+ + + + + + + + + + config as $key => $value) : ?> + + + + + + +
+ +
+ + + +
+ + + +
diff --git a/administrator/components/com_admin/tmpl/sysinfo/default_directory.php b/administrator/components/com_admin/tmpl/sysinfo/default_directory.php index a18fd59e19916..135c16eabc6e3 100644 --- a/administrator/components/com_admin/tmpl/sysinfo/default_directory.php +++ b/administrator/components/com_admin/tmpl/sysinfo/default_directory.php @@ -1,4 +1,5 @@
- - - - - - - - - - directory as $dir => $info) : ?> - - - - - - -
- -
- - - -
- - - -
+ + + + + + + + + + directory as $dir => $info) : ?> + + + + + + +
+ +
+ + + +
+ + + +
diff --git a/administrator/components/com_admin/tmpl/sysinfo/default_phpinfo.php b/administrator/components/com_admin/tmpl/sysinfo/default_phpinfo.php index b4c8a68faca16..18e0fb1f60155 100644 --- a/administrator/components/com_admin/tmpl/sysinfo/default_phpinfo.php +++ b/administrator/components/com_admin/tmpl/sysinfo/default_phpinfo.php @@ -1,4 +1,5 @@
- phpInfo; ?> + phpInfo; ?>
diff --git a/administrator/components/com_admin/tmpl/sysinfo/default_phpsettings.php b/administrator/components/com_admin/tmpl/sysinfo/default_phpsettings.php index 3ea85c31b4024..078555d28d371 100644 --- a/administrator/components/com_admin/tmpl/sysinfo/default_phpsettings.php +++ b/administrator/components/com_admin/tmpl/sysinfo/default_phpsettings.php @@ -1,4 +1,5 @@
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - -
- - - phpSettings['upload_max_filesize']); ?> -
- - - phpSettings['post_max_size']); ?> -
- - - phpSettings['memory_limit']); ?> -
- - - phpSettings['open_basedir']); ?> -
- - - phpSettings['display_errors']); ?> -
- - - phpSettings['short_open_tag']); ?> -
- - - phpSettings['file_uploads']); ?> -
- - - phpSettings['output_buffering']); ?> -
- - - phpSettings['session.save_path']); ?> -
- - - phpSettings['session.auto_start']; ?> -
- - - phpSettings['xml']); ?> -
- - - phpSettings['zlib']); ?> -
- - - phpSettings['zip']); ?> -
- - - phpSettings['disable_functions']); ?> -
- - - phpSettings['fileinfo']); ?> -
- - - phpSettings['mbstring']); ?> -
- - - phpSettings['gd']); ?> -
- - - phpSettings['iconv']); ?> -
- - - phpSettings['intl']); ?> -
- - - phpSettings['max_input_vars']; ?> -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + +
+ + + phpSettings['upload_max_filesize']); ?> +
+ + + phpSettings['post_max_size']); ?> +
+ + + phpSettings['memory_limit']); ?> +
+ + + phpSettings['open_basedir']); ?> +
+ + + phpSettings['display_errors']); ?> +
+ + + phpSettings['short_open_tag']); ?> +
+ + + phpSettings['file_uploads']); ?> +
+ + + phpSettings['output_buffering']); ?> +
+ + + phpSettings['session.save_path']); ?> +
+ + + phpSettings['session.auto_start']; ?> +
+ + + phpSettings['xml']); ?> +
+ + + phpSettings['zlib']); ?> +
+ + + phpSettings['zip']); ?> +
+ + + phpSettings['disable_functions']); ?> +
+ + + phpSettings['fileinfo']); ?> +
+ + + phpSettings['mbstring']); ?> +
+ + + phpSettings['gd']); ?> +
+ + + phpSettings['iconv']); ?> +
+ + + phpSettings['intl']); ?> +
+ + + phpSettings['max_input_vars']; ?> +
diff --git a/administrator/components/com_admin/tmpl/sysinfo/default_system.php b/administrator/components/com_admin/tmpl/sysinfo/default_system.php index 97ddce3adf5b6..0891b5c2ef82b 100644 --- a/administrator/components/com_admin/tmpl/sysinfo/default_system.php +++ b/administrator/components/com_admin/tmpl/sysinfo/default_system.php @@ -1,4 +1,5 @@
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - -
- - - info['php']; ?> -
- - - info['dbserver']; ?> -
- - - info['dbversion']; ?> -
- - - info['dbcollation']; ?> -
- - - info['dbconnectioncollation']; ?> -
- - - info['dbconnectionencryption'] ?: Text::_('JNONE'); ?> -
- - - info['dbconnencryptsupported'] ? Text::_('JYES') : Text::_('JNO'); ?> -
- - - info['phpversion']; ?> -
- - - info['server']); ?> -
- - - info['sapi_name']; ?> -
- - - info['version']; ?> -
- - - info['useragent'], ENT_COMPAT, 'UTF-8'); ?> -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + +
+ + + info['php']; ?> +
+ + + info['dbserver']; ?> +
+ + + info['dbversion']; ?> +
+ + + info['dbcollation']; ?> +
+ + + info['dbconnectioncollation']; ?> +
+ + + info['dbconnectionencryption'] ?: Text::_('JNONE'); ?> +
+ + + info['dbconnencryptsupported'] ? Text::_('JYES') : Text::_('JNO'); ?> +
+ + + info['phpversion']; ?> +
+ + + info['server']); ?> +
+ + + info['sapi_name']; ?> +
+ + + info['version']; ?> +
+ + + info['useragent'], ENT_COMPAT, 'UTF-8'); ?> +
diff --git a/administrator/components/com_ajax/ajax.php b/administrator/components/com_ajax/ajax.php index 40e35cd18f9d4..37e61ad93a326 100644 --- a/administrator/components/com_ajax/ajax.php +++ b/administrator/components/com_ajax/ajax.php @@ -1,4 +1,5 @@ filterForm) && !empty($data['view']->filterForm)) -{ - // Checks if a selector (e.g. client_id) exists. - if ($selectorField = $data['view']->filterForm->getField($selectorFieldName)) - { - $showSelector = $selectorField->getAttribute('filtermode', '') === 'selector' ? true : $showSelector; - - // Checks if a selector should be shown in the current layout. - if (isset($data['view']->layout)) - { - $showSelector = $selectorField->getAttribute('layout', 'default') != $data['view']->layout ? false : $showSelector; - } - - // Unset the selector field from active filters group. - unset($data['view']->activeFilters[$selectorFieldName]); - } - - // Checks if the filters button should exist. - $filters = $data['view']->filterForm->getGroup('filter'); - $showFilterButton = isset($filters['filter_search']) && count($filters) === 1 ? false : true; - - // Checks if it should show the be hidden. - $hideActiveFilters = empty($data['view']->activeFilters); - - // Check if the no results message should appear. - if (isset($data['view']->total) && (int) $data['view']->total === 0) - { - $noResults = $data['view']->filterForm->getFieldAttribute('search', 'noresults', '', 'filter'); - if (!empty($noResults)) - { - $noResultsText = Text::_($noResults); - } - } +if (isset($data['view']->filterForm) && !empty($data['view']->filterForm)) { + // Checks if a selector (e.g. client_id) exists. + if ($selectorField = $data['view']->filterForm->getField($selectorFieldName)) { + $showSelector = $selectorField->getAttribute('filtermode', '') === 'selector' ? true : $showSelector; + + // Checks if a selector should be shown in the current layout. + if (isset($data['view']->layout)) { + $showSelector = $selectorField->getAttribute('layout', 'default') != $data['view']->layout ? false : $showSelector; + } + + // Unset the selector field from active filters group. + unset($data['view']->activeFilters[$selectorFieldName]); + } + + // Checks if the filters button should exist. + $filters = $data['view']->filterForm->getGroup('filter'); + $showFilterButton = isset($filters['filter_search']) && count($filters) === 1 ? false : true; + + // Checks if it should show the be hidden. + $hideActiveFilters = empty($data['view']->activeFilters); + + // Check if the no results message should appear. + if (isset($data['view']->total) && (int) $data['view']->total === 0) { + $noResults = $data['view']->filterForm->getFieldAttribute('search', 'noresults', '', 'filter'); + if (!empty($noResults)) { + $noResultsText = Text::_($noResults); + } + } } // Set some basic options. $customOptions = array( - 'filtersHidden' => isset($data['options']['filtersHidden']) && $data['options']['filtersHidden'] ? $data['options']['filtersHidden'] : $hideActiveFilters, - 'filterButton' => isset($data['options']['filterButton']) && $data['options']['filterButton'] ? $data['options']['filterButton'] : $showFilterButton, - 'defaultLimit' => $data['options']['defaultLimit'] ?? Factory::getApplication()->get('list_limit', 20), - 'searchFieldSelector' => '#filter_search', - 'selectorFieldName' => $selectorFieldName, - 'showSelector' => $showSelector, - 'orderFieldSelector' => '#list_fullordering', - 'showNoResults' => !empty($noResultsText), - 'noResultsText' => !empty($noResultsText) ? $noResultsText : '', - 'formSelector' => !empty($data['options']['formSelector']) ? $data['options']['formSelector'] : '#adminForm', + 'filtersHidden' => isset($data['options']['filtersHidden']) && $data['options']['filtersHidden'] ? $data['options']['filtersHidden'] : $hideActiveFilters, + 'filterButton' => isset($data['options']['filterButton']) && $data['options']['filterButton'] ? $data['options']['filterButton'] : $showFilterButton, + 'defaultLimit' => $data['options']['defaultLimit'] ?? Factory::getApplication()->get('list_limit', 20), + 'searchFieldSelector' => '#filter_search', + 'selectorFieldName' => $selectorFieldName, + 'showSelector' => $showSelector, + 'orderFieldSelector' => '#list_fullordering', + 'showNoResults' => !empty($noResultsText), + 'noResultsText' => !empty($noResultsText) ? $noResultsText : '', + 'formSelector' => !empty($data['options']['formSelector']) ? $data['options']['formSelector'] : '#adminForm', ); // Merge custom options in the options array. @@ -85,44 +81,44 @@ HTMLHelper::_('searchtools.form', $data['options']['formSelector'], $data['options']); ?> - sublayout('noitems', $data); ?> + sublayout('noitems', $data); ?> diff --git a/administrator/components/com_associations/services/provider.php b/administrator/components/com_associations/services/provider.php index 4ad47fec2648b..071fec0053bf2 100644 --- a/administrator/components/com_associations/services/provider.php +++ b/administrator/components/com_associations/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Associations')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Associations')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Associations')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Associations')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_associations/src/Controller/AssociationController.php b/administrator/components/com_associations/src/Controller/AssociationController.php index e3e7f12860480..2590760b931de 100644 --- a/administrator/components/com_associations/src/Controller/AssociationController.php +++ b/administrator/components/com_associations/src/Controller/AssociationController.php @@ -1,4 +1,5 @@ input->get('itemtype', '', 'string'), 2); + /** + * Method to edit an existing record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key + * (sometimes required to avoid router collisions). + * + * @return FormController|boolean True if access level check and checkout passes, false otherwise. + * + * @since 3.7.0 + */ + public function edit($key = null, $urlVar = null) + { + list($extensionName, $typeName) = explode('.', $this->input->get('itemtype', '', 'string'), 2); - $id = $this->input->get('id', 0, 'int'); + $id = $this->input->get('id', 0, 'int'); - // Check if reference item can be edited. - if (!AssociationsHelper::allowEdit($extensionName, $typeName, $id)) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); - $this->setRedirect(Route::_('index.php?option=com_associations&view=associations', false)); + // Check if reference item can be edited. + if (!AssociationsHelper::allowEdit($extensionName, $typeName, $id)) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); + $this->setRedirect(Route::_('index.php?option=com_associations&view=associations', false)); - return false; - } + return false; + } - return parent::display(); - } + return parent::display(); + } - /** - * Method for canceling the edit action - * - * @param string $key The name of the primary key of the URL variable. - * - * @return void - * - * @since 3.7.0 - */ - public function cancel($key = null) - { - $this->checkToken(); + /** + * Method for canceling the edit action + * + * @param string $key The name of the primary key of the URL variable. + * + * @return void + * + * @since 3.7.0 + */ + public function cancel($key = null) + { + $this->checkToken(); - list($extensionName, $typeName) = explode('.', $this->input->get('itemtype', '', 'string'), 2); + list($extensionName, $typeName) = explode('.', $this->input->get('itemtype', '', 'string'), 2); - // Only check in, if component item type allows to check out. - if (AssociationsHelper::typeSupportsCheckout($extensionName, $typeName)) - { - $ids = array(); - $targetId = $this->input->get('target-id', '', 'string'); + // Only check in, if component item type allows to check out. + if (AssociationsHelper::typeSupportsCheckout($extensionName, $typeName)) { + $ids = array(); + $targetId = $this->input->get('target-id', '', 'string'); - if ($targetId !== '') - { - $ids = array_unique(explode(',', $targetId)); - } + if ($targetId !== '') { + $ids = array_unique(explode(',', $targetId)); + } - $ids[] = $this->input->get('id', 0, 'int'); + $ids[] = $this->input->get('id', 0, 'int'); - foreach ($ids as $key => $id) - { - AssociationsHelper::getItem($extensionName, $typeName, $id)->checkIn(); - } - } + foreach ($ids as $key => $id) { + AssociationsHelper::getItem($extensionName, $typeName, $id)->checkIn(); + } + } - $this->setRedirect(Route::_('index.php?option=com_associations&view=associations', false)); - } + $this->setRedirect(Route::_('index.php?option=com_associations&view=associations', false)); + } } diff --git a/administrator/components/com_associations/src/Controller/AssociationsController.php b/administrator/components/com_associations/src/Controller/AssociationsController.php index 22bf8c7f8bae3..f5b3916ffd1aa 100644 --- a/administrator/components/com_associations/src/Controller/AssociationsController.php +++ b/administrator/components/com_associations/src/Controller/AssociationsController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to purge the associations table. - * - * @return void - * - * @since 3.7.0 - */ - public function purge() - { - $this->checkToken(); - - $this->getModel('associations')->purge(); - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false)); - } - - /** - * Method to delete the orphans from the associations table. - * - * @return void - * - * @since 3.7.0 - */ - public function clean() - { - $this->checkToken(); - - $this->getModel('associations')->clean(); - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false)); - } - - /** - * Method to check in an item from the association item overview. - * - * @return void - * - * @since 3.7.1 - */ - public function checkin() - { - // Set the redirect so we can just stop processing when we find a condition we can't process - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false)); - - // Figure out if the item supports checking and check it in - list($extensionName, $typeName) = explode('.', $this->input->get('itemtype')); - - $extension = AssociationsHelper::getSupportedExtension($extensionName); - $types = $extension->get('types'); - - if (!\array_key_exists($typeName, $types)) - { - return; - } - - if (AssociationsHelper::typeSupportsCheckout($extensionName, $typeName) === false) - { - // How on earth we came to that point, eject internet - return; - } - - $cid = (array) $this->input->get('cid', array(), 'int'); - - if (empty($cid)) - { - // Seems we don't have an id to work with. - return; - } - - // We know the first element is the one we need because we don't allow multi selection of rows - $id = $cid[0]; - - if ($id === 0) - { - // Seems we don't have an id to work with. - return; - } - - if (AssociationsHelper::canCheckinItem($extensionName, $typeName, $id) === true) - { - $item = AssociationsHelper::getItem($extensionName, $typeName, $id); - - $item->checkIn($id); - - return; - } - - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list), - Text::_('COM_ASSOCIATIONS_YOU_ARE_NOT_ALLOWED_TO_CHECKIN_THIS_ITEM') - ); - } + /** + * The URL view list variable. + * + * @var string + * + * @since 3.7.0 + */ + protected $view_list = 'associations'; + + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config The array of possible config values. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel|boolean + * + * @since 3.7.0 + */ + public function getModel($name = 'Associations', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to purge the associations table. + * + * @return void + * + * @since 3.7.0 + */ + public function purge() + { + $this->checkToken(); + + $this->getModel('associations')->purge(); + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false)); + } + + /** + * Method to delete the orphans from the associations table. + * + * @return void + * + * @since 3.7.0 + */ + public function clean() + { + $this->checkToken(); + + $this->getModel('associations')->clean(); + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false)); + } + + /** + * Method to check in an item from the association item overview. + * + * @return void + * + * @since 3.7.1 + */ + public function checkin() + { + // Set the redirect so we can just stop processing when we find a condition we can't process + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false)); + + // Figure out if the item supports checking and check it in + list($extensionName, $typeName) = explode('.', $this->input->get('itemtype')); + + $extension = AssociationsHelper::getSupportedExtension($extensionName); + $types = $extension->get('types'); + + if (!\array_key_exists($typeName, $types)) { + return; + } + + if (AssociationsHelper::typeSupportsCheckout($extensionName, $typeName) === false) { + // How on earth we came to that point, eject internet + return; + } + + $cid = (array) $this->input->get('cid', array(), 'int'); + + if (empty($cid)) { + // Seems we don't have an id to work with. + return; + } + + // We know the first element is the one we need because we don't allow multi selection of rows + $id = $cid[0]; + + if ($id === 0) { + // Seems we don't have an id to work with. + return; + } + + if (AssociationsHelper::canCheckinItem($extensionName, $typeName, $id) === true) { + $item = AssociationsHelper::getItem($extensionName, $typeName, $id); + + $item->checkIn($id); + + return; + } + + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list), + Text::_('COM_ASSOCIATIONS_YOU_ARE_NOT_ALLOWED_TO_CHECKIN_THIS_ITEM') + ); + } } diff --git a/administrator/components/com_associations/src/Controller/DisplayController.php b/administrator/components/com_associations/src/Controller/DisplayController.php index e2a48320e1d91..d02944ad6d582 100644 --- a/administrator/components/com_associations/src/Controller/DisplayController.php +++ b/administrator/components/com_associations/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('itemtype', '', 'string'); - - if ($itemType !== '') - { - list($extensionName, $typeName) = explode('.', $itemType); - - if (!AssociationsHelper::hasSupport($extensionName)) - { - throw new \Exception( - Text::sprintf('COM_ASSOCIATIONS_COMPONENT_NOT_SUPPORTED', $this->app->getLanguage()->_($extensionName)), - 404 - ); - } - - if (!$this->app->getIdentity()->authorise('core.manage', $extensionName)) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } - } + /** + * Method to check component access permission + * + * @since 4.0.0 + * + * @return void + * + * @throws \Exception|NotAllowed + */ + protected function checkAccess() + { + parent::checkAccess(); + + // Check if user has permission to access the component item type. + $itemType = $this->input->get('itemtype', '', 'string'); + + if ($itemType !== '') { + list($extensionName, $typeName) = explode('.', $itemType); + + if (!AssociationsHelper::hasSupport($extensionName)) { + throw new \Exception( + Text::sprintf('COM_ASSOCIATIONS_COMPONENT_NOT_SUPPORTED', $this->app->getLanguage()->_($extensionName)), + 404 + ); + } + + if (!$this->app->getIdentity()->authorise('core.manage', $extensionName)) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } + } } diff --git a/administrator/components/com_associations/src/Field/ItemlanguageField.php b/administrator/components/com_associations/src/Field/ItemlanguageField.php index 0d63fe5951d4d..cd081551b606b 100644 --- a/administrator/components/com_associations/src/Field/ItemlanguageField.php +++ b/administrator/components/com_associations/src/Field/ItemlanguageField.php @@ -1,4 +1,5 @@ input; - - list($extensionName, $typeName) = explode('.', $input->get('itemtype', '', 'string'), 2); - - // Get the extension specific helper method - $helper = AssociationsHelper::getExtensionHelper($extensionName); - - $languageField = $helper->getTypeFieldName($typeName, 'language'); - $referenceId = $input->get('id', 0, 'int'); - $reference = ArrayHelper::fromObject(AssociationsHelper::getItem($extensionName, $typeName, $referenceId)); - $referenceLang = $reference[$languageField]; - - // Get item associations given ID and item type - $associations = AssociationsHelper::getAssociationList($extensionName, $typeName, $referenceId); - - // Check if user can create items in this component item type. - $canCreate = AssociationsHelper::allowAdd($extensionName, $typeName); - - // Gets existing languages. - $existingLanguages = LanguageHelper::getContentLanguages(array(0, 1), false); - - $options = array(); - - // Each option has the format "|", example: "en-GB|1" - foreach ($existingLanguages as $langCode => $language) - { - // If language code is equal to reference language we don't need it. - if ($language->lang_code == $referenceLang) - { - continue; - } - - $options[$langCode] = new \stdClass; - $options[$langCode]->text = $language->title; - - // If association exists in this language. - if (isset($associations[$language->lang_code])) - { - $itemId = (int) $associations[$language->lang_code]['id']; - $options[$langCode]->value = $language->lang_code . ':' . $itemId . ':edit'; - - // Check if user does have permission to edit the associated item. - $canEdit = AssociationsHelper::allowEdit($extensionName, $typeName, $itemId); - - // Check if item can be checked out - $canCheckout = AssociationsHelper::canCheckinItem($extensionName, $typeName, $itemId); - - // Disable language if user is not allowed to edit the item associated to it. - $options[$langCode]->disable = !($canEdit && $canCheckout); - } - else - { - // New item, id = 0 and disabled if user is not allowed to create new items. - $options[$langCode]->value = $language->lang_code . ':0:add'; - - // Disable language if user is not allowed to create items. - $options[$langCode]->disable = !$canCreate; - } - } - - return array_merge(parent::getOptions(), $options); - } + /** + * The form field type. + * + * @var string + * @since 3.7.0 + */ + protected $type = 'Itemlanguage'; + + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.7.0 + */ + protected function getOptions() + { + $input = Factory::getApplication()->input; + + list($extensionName, $typeName) = explode('.', $input->get('itemtype', '', 'string'), 2); + + // Get the extension specific helper method + $helper = AssociationsHelper::getExtensionHelper($extensionName); + + $languageField = $helper->getTypeFieldName($typeName, 'language'); + $referenceId = $input->get('id', 0, 'int'); + $reference = ArrayHelper::fromObject(AssociationsHelper::getItem($extensionName, $typeName, $referenceId)); + $referenceLang = $reference[$languageField]; + + // Get item associations given ID and item type + $associations = AssociationsHelper::getAssociationList($extensionName, $typeName, $referenceId); + + // Check if user can create items in this component item type. + $canCreate = AssociationsHelper::allowAdd($extensionName, $typeName); + + // Gets existing languages. + $existingLanguages = LanguageHelper::getContentLanguages(array(0, 1), false); + + $options = array(); + + // Each option has the format "|", example: "en-GB|1" + foreach ($existingLanguages as $langCode => $language) { + // If language code is equal to reference language we don't need it. + if ($language->lang_code == $referenceLang) { + continue; + } + + $options[$langCode] = new \stdClass(); + $options[$langCode]->text = $language->title; + + // If association exists in this language. + if (isset($associations[$language->lang_code])) { + $itemId = (int) $associations[$language->lang_code]['id']; + $options[$langCode]->value = $language->lang_code . ':' . $itemId . ':edit'; + + // Check if user does have permission to edit the associated item. + $canEdit = AssociationsHelper::allowEdit($extensionName, $typeName, $itemId); + + // Check if item can be checked out + $canCheckout = AssociationsHelper::canCheckinItem($extensionName, $typeName, $itemId); + + // Disable language if user is not allowed to edit the item associated to it. + $options[$langCode]->disable = !($canEdit && $canCheckout); + } else { + // New item, id = 0 and disabled if user is not allowed to create new items. + $options[$langCode]->value = $language->lang_code . ':0:add'; + + // Disable language if user is not allowed to create items. + $options[$langCode]->disable = !$canCreate; + } + } + + return array_merge(parent::getOptions(), $options); + } } diff --git a/administrator/components/com_associations/src/Field/ItemtypeField.php b/administrator/components/com_associations/src/Field/ItemtypeField.php index c31c081ead930..7c6dd51dcfc37 100644 --- a/administrator/components/com_associations/src/Field/ItemtypeField.php +++ b/administrator/components/com_associations/src/Field/ItemtypeField.php @@ -1,4 +1,5 @@ get('associationssupport') === true) - { - foreach ($extension->get('types') as $type) - { - $context = $extension->get('component') . '.' . $type->get('name'); - $options[$extension->get('title')][] = HTMLHelper::_('select.option', $context, $type->get('title')); - } - } - } - - // Sort by alpha order. - uksort($options, 'strnatcmp'); - - // Add options to parent array. - return array_merge(parent::getGroups(), $options); - } + /** + * The form field type. + * + * @var string + * + * @since 3.7.0 + */ + protected $type = 'Itemtype'; + + /** + * Method to get the field input markup. + * + * @return array The field option objects as a nested array in groups. + * + * @since 3.7.0 + * + * @throws \UnexpectedValueException + */ + protected function getGroups() + { + $options = array(); + $extensions = AssociationsHelper::getSupportedExtensions(); + + foreach ($extensions as $extension) { + if ($extension->get('associationssupport') === true) { + foreach ($extension->get('types') as $type) { + $context = $extension->get('component') . '.' . $type->get('name'); + $options[$extension->get('title')][] = HTMLHelper::_('select.option', $context, $type->get('title')); + } + } + } + + // Sort by alpha order. + uksort($options, 'strnatcmp'); + + // Add options to parent array. + return array_merge(parent::getGroups(), $options); + } } diff --git a/administrator/components/com_associations/src/Field/Modal/AssociationField.php b/administrator/components/com_associations/src/Field/Modal/AssociationField.php index f05d89046b30d..3a173aabd8c4a 100644 --- a/administrator/components/com_associations/src/Field/Modal/AssociationField.php +++ b/administrator/components/com_associations/src/Field/Modal/AssociationField.php @@ -1,4 +1,5 @@ value ?: ''; - - $doc = Factory::getApplication()->getDocument(); - $wa = $doc->getWebAssetManager(); - - $doc->addScriptOptions('admin_associations_modal', ['itemId' => $value]); - $wa->useScript('com_associations.admin-associations-modal'); - - // Setup variables for display. - $html = array(); - - $linkAssociations = 'index.php?option=com_associations&view=associations&layout=modal&tmpl=component' - . '&forcedItemType=' . Factory::getApplication()->input->get('itemtype', '', 'string') . '&function=jSelectAssociation_' . $this->id; - - $linkAssociations .= "&forcedLanguage=' + document.getElementById('target-association').getAttribute('data-language') + '"; - - $urlSelect = $linkAssociations . '&' . Session::getFormToken() . '=1'; - - // Select custom association button - $html[] = '' - . ' ' - . '' - . ''; - - // Clear association button - $html[] = '' - . ' ' . Text::_('JCLEAR') - . ''; - - $html[] = ''; - - // Select custom association modal - $html[] = HTMLHelper::_( - 'bootstrap.renderModal', - 'associationSelect' . $this->id . 'Modal', - array( - 'title' => Text::_('COM_ASSOCIATIONS_SELECT_TARGET'), - 'backdrop' => 'static', - 'url' => $urlSelect, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ) - ); - - return implode("\n", $html); - } + /** + * The form field type. + * + * @var string + * @since 3.7.0 + */ + protected $type = 'Modal_Association'; + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 3.7.0 + */ + protected function getInput() + { + // @todo USE Layouts here!!! + // The active item id field. + $value = (int) $this->value ?: ''; + + $doc = Factory::getApplication()->getDocument(); + $wa = $doc->getWebAssetManager(); + + $doc->addScriptOptions('admin_associations_modal', ['itemId' => $value]); + $wa->useScript('com_associations.admin-associations-modal'); + + // Setup variables for display. + $html = array(); + + $linkAssociations = 'index.php?option=com_associations&view=associations&layout=modal&tmpl=component' + . '&forcedItemType=' . Factory::getApplication()->input->get('itemtype', '', 'string') . '&function=jSelectAssociation_' . $this->id; + + $linkAssociations .= "&forcedLanguage=' + document.getElementById('target-association').getAttribute('data-language') + '"; + + $urlSelect = $linkAssociations . '&' . Session::getFormToken() . '=1'; + + // Select custom association button + $html[] = '' + . ' ' + . '' + . ''; + + // Clear association button + $html[] = '' + . ' ' . Text::_('JCLEAR') + . ''; + + $html[] = ''; + + // Select custom association modal + $html[] = HTMLHelper::_( + 'bootstrap.renderModal', + 'associationSelect' . $this->id . 'Modal', + array( + 'title' => Text::_('COM_ASSOCIATIONS_SELECT_TARGET'), + 'backdrop' => 'static', + 'url' => $urlSelect, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ) + ); + + return implode("\n", $html); + } } diff --git a/administrator/components/com_associations/src/Helper/AssociationsHelper.php b/administrator/components/com_associations/src/Helper/AssociationsHelper.php index 5bf5b1df72b5d..9a8804a743871 100644 --- a/administrator/components/com_associations/src/Helper/AssociationsHelper.php +++ b/administrator/components/com_associations/src/Helper/AssociationsHelper.php @@ -1,4 +1,5 @@ getAssociationList($typeName, $itemId); - - } - - /** - * Get the the instance of the extension helper class - * - * @param string $extensionName The extension name with com_ - * - * @return \Joomla\CMS\Association\AssociationExtensionHelper|null - * - * @since 3.7.0 - */ - public static function getExtensionHelper($extensionName) - { - if (!self::hasSupport($extensionName)) - { - return null; - } - - $support = self::$extensionsSupport[$extensionName]; - - return $support->get('helper'); - } - - /** - * Get item information - * - * @param string $extensionName The extension name with com_ - * @param string $typeName The item type - * @param int $itemId The id of item for which we need the associated items - * - * @return \Joomla\CMS\Table\Table|null - * - * @since 3.7.0 - */ - public static function getItem($extensionName, $typeName, $itemId) - { - if (!self::hasSupport($extensionName)) - { - return null; - } - - // Get the extension specific helper method - $helper = self::getExtensionHelper($extensionName); - - return $helper->getItem($typeName, $itemId); - } - - /** - * Check if extension supports associations - * - * @param string $extensionName The extension name with com_ - * - * @return boolean - * - * @since 3.7.0 - */ - public static function hasSupport($extensionName) - { - if (\is_null(self::$extensionsSupport)) - { - self::getSupportedExtensions(); - } - - return \in_array($extensionName, self::$supportedExtensionsList); - } - - /** - * Loads the helper for the given class. - * - * @param string $extensionName The extension name with com_ - * - * @return AssociationExtensionInterface|null - * - * @since 4.0.0 - */ - private static function loadHelper($extensionName) - { - $component = Factory::getApplication()->bootComponent($extensionName); - - if ($component instanceof AssociationServiceInterface) - { - return $component->getAssociationsExtension(); - } - - // Check if associations helper exists - if (!file_exists(JPATH_ADMINISTRATOR . '/components/' . $extensionName . '/helpers/associations.php')) - { - return null; - } - - require_once JPATH_ADMINISTRATOR . '/components/' . $extensionName . '/helpers/associations.php'; - - $componentAssociationsHelperClassName = self::getExtensionHelperClassName($extensionName); - - if (!class_exists($componentAssociationsHelperClassName, false)) - { - return null; - } - - // Create an instance of the helper class - return new $componentAssociationsHelperClassName; - } - - /** - * Get the extension specific helper class name - * - * @param string $extensionName The extension name with com_ - * - * @return string - * - * @since 3.7.0 - */ - private static function getExtensionHelperClassName($extensionName) - { - $realName = self::getExtensionRealName($extensionName); - - return ucfirst($realName) . 'AssociationsHelper'; - } - - /** - * Get the real extension name. This means without com_ - * - * @param string $extensionName The extension name with com_ - * - * @return string - * - * @since 3.7.0 - */ - private static function getExtensionRealName($extensionName) - { - return strpos($extensionName, 'com_') === false ? $extensionName : substr($extensionName, 4); - } - - /** - * Get the associated language edit links Html. - * - * @param string $extensionName Extension Name - * @param string $typeName ItemType - * @param integer $itemId Item id. - * @param string $itemLanguage Item language code. - * @param boolean $addLink True for adding edit links. False for just text. - * @param boolean $assocLanguages True for showing non associated content languages. False only languages with associations. - * - * @return string The language HTML - * - * @since 3.7.0 - */ - public static function getAssociationHtmlList($extensionName, $typeName, $itemId, $itemLanguage, $addLink = true, $assocLanguages = true) - { - // Get the associations list for this item. - $items = self::getAssociationList($extensionName, $typeName, $itemId); - - $titleFieldName = self::getTypeFieldName($extensionName, $typeName, 'title'); - - // Get all content languages. - $languages = LanguageHelper::getContentLanguages(array(0, 1), false); - $content_languages = array_column($languages, 'lang_code'); - - // Display warning if Content Language is trashed or deleted - foreach ($items as $item) - { - if (!\in_array($item['language'], $content_languages)) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item['language']), 'warning'); - } - } - - $canEditReference = self::allowEdit($extensionName, $typeName, $itemId); - $canCreate = self::allowAdd($extensionName, $typeName); - - // Create associated items list. - foreach ($languages as $langCode => $language) - { - // Don't do for the reference language. - if ($langCode == $itemLanguage) - { - continue; - } - - // Don't show languages with associations, if we don't want to show them. - if ($assocLanguages && isset($items[$langCode])) - { - unset($items[$langCode]); - continue; - } - - // Don't show languages without associations, if we don't want to show them. - if (!$assocLanguages && !isset($items[$langCode])) - { - continue; - } - - // Get html parameters. - if (isset($items[$langCode])) - { - $title = $items[$langCode][$titleFieldName]; - $additional = ''; - - if (isset($items[$langCode]['catid'])) - { - $db = Factory::getDbo(); - - // Get the category name - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__categories')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $items[$langCode]['catid'], ParameterType::INTEGER); - - $db->setQuery($query); - $categoryTitle = $db->loadResult(); - - $additional = '' . Text::sprintf('JCATEGORY_SPRINTF', $categoryTitle) . '
'; - } - elseif (isset($items[$langCode]['menutype'])) - { - $db = Factory::getDbo(); - - // Get the menutype name - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__menu_types')) - ->where($db->quoteName('menutype') . ' = :menutype') - ->bind(':menutype', $items[$langCode]['menutype']); - - $db->setQuery($query); - $menutypeTitle = $db->loadResult(); - - $additional = '' . Text::sprintf('COM_MENUS_MENU_SPRINTF', $menutypeTitle) . '
'; - } - - $labelClass = 'bg-secondary'; - $target = $langCode . ':' . $items[$langCode]['id'] . ':edit'; - $allow = $canEditReference - && self::allowEdit($extensionName, $typeName, $items[$langCode]['id']) - && self::canCheckinItem($extensionName, $typeName, $items[$langCode]['id']); - - $additional .= $addLink && $allow ? Text::_('COM_ASSOCIATIONS_EDIT_ASSOCIATION') : ''; - } - else - { - $items[$langCode] = array(); - - $title = Text::_('COM_ASSOCIATIONS_NO_ASSOCIATION'); - $additional = $addLink ? Text::_('COM_ASSOCIATIONS_ADD_NEW_ASSOCIATION') : ''; - $labelClass = 'bg-warning text-dark'; - $target = $langCode . ':0:add'; - $allow = $canCreate; - } - - // Generate item Html. - $options = array( - 'option' => 'com_associations', - 'view' => 'association', - 'layout' => 'edit', - 'itemtype' => $extensionName . '.' . $typeName, - 'task' => 'association.edit', - 'id' => $itemId, - 'target' => $target, - ); - - $url = Route::_('index.php?' . http_build_query($options)); - $url = $allow && $addLink ? $url : ''; - $text = $language->lang_code; - - $tooltip = '' . htmlspecialchars($language->title, ENT_QUOTES, 'UTF-8') . '
' - . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '

' . $additional; - $classes = 'badge ' . $labelClass; - - $items[$langCode]['link'] = '' . $text . '' - . '
' . $tooltip . '
'; - } - - return LayoutHelper::render('joomla.content.associations', $items); - } - - /** - * Get all extensions with associations support. - * - * @return array The extensions. - * - * @since 3.7.0 - */ - public static function getSupportedExtensions() - { - if (!\is_null(self::$extensionsSupport)) - { - return self::$extensionsSupport; - } - - self::$extensionsSupport = array(); - - $extensions = self::getEnabledExtensions(); - - foreach ($extensions as $extension) - { - $support = self::getSupportedExtension($extension->element); - - if ($support->get('associationssupport') === true) - { - self::$supportedExtensionsList[] = $extension->element; - } - - self::$extensionsSupport[$extension->element] = $support; - } - - return self::$extensionsSupport; - } - - /** - * Get item context based on the item key. - * - * @param string $extensionName The extension identifier. - * - * @return \Joomla\Registry\Registry The item properties. - * - * @since 3.7.0 - */ - public static function getSupportedExtension($extensionName) - { - $result = new Registry; - - $result->def('component', $extensionName); - $result->def('associationssupport', false); - $result->def('helper', null); - - $helper = self::loadHelper($extensionName); - - if (!$helper) - { - return $result; - } - - $result->set('helper', $helper); - - if ($helper->hasAssociationsSupport() === false) - { - return $result; - } - - $result->set('associationssupport', true); - - // Get the translated titles. - $languagePath = JPATH_ADMINISTRATOR . '/components/' . $extensionName; - $lang = Factory::getLanguage(); - - $lang->load($extensionName . '.sys', JPATH_ADMINISTRATOR); - $lang->load($extensionName . '.sys', $languagePath); - $lang->load($extensionName, JPATH_ADMINISTRATOR); - $lang->load($extensionName, $languagePath); - - $result->def('title', Text::_(strtoupper($extensionName))); - - // Get the supported types - $types = $helper->getItemTypes(); - $rTypes = array(); - - foreach ($types as $typeName) - { - $details = $helper->getType($typeName); - $context = 'component'; - $title = $helper->getTypeTitle($typeName); - $languageKey = $typeName; - - $typeNameExploded = explode('.', $typeName); - - if (array_pop($typeNameExploded) === 'category') - { - $languageKey = strtoupper($extensionName) . '_CATEGORIES'; - $context = 'category'; - } - - if ($lang->hasKey(strtoupper($extensionName . '_' . $title . 'S'))) - { - $languageKey = strtoupper($extensionName . '_' . $title . 'S'); - } - - $title = $lang->hasKey($languageKey) ? Text::_($languageKey) : Text::_('COM_ASSOCIATIONS_ITEMS'); - - $rType = new Registry; - - $rType->def('name', $typeName); - $rType->def('details', $details); - $rType->def('title', $title); - $rType->def('context', $context); - - $rTypes[$typeName] = $rType; - } - - $result->def('types', $rTypes); - - return $result; - } - - /** - * Get all installed and enabled extensions - * - * @return mixed - * - * @since 3.7.0 - */ - private static function getEnabledExtensions() - { - $db = Factory::getDbo(); - - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('component')) - ->where($db->quoteName('enabled') . ' = 1'); - - $db->setQuery($query); - - return $db->loadObjectList(); - } - - /** - * Get all the content languages. - * - * @return array Array of objects all content languages by language code. - * - * @since 3.7.0 - */ - public static function getContentLanguages() - { - return LanguageHelper::getContentLanguages(array(0, 1)); - } - - /** - * Get the associated items for an item - * - * @param string $extensionName The extension name with com_ - * @param string $typeName The item type - * @param int $itemId The id of item for which we need the associated items - * - * @return boolean - * - * @since 3.7.0 - */ - public static function allowEdit($extensionName, $typeName, $itemId) - { - if (!self::hasSupport($extensionName)) - { - return false; - } - - // Get the extension specific helper method - $helper = self::getExtensionHelper($extensionName); - - if (method_exists($helper, 'allowEdit')) - { - return $helper->allowEdit($typeName, $itemId); - } - - return Factory::getUser()->authorise('core.edit', $extensionName); - } - - /** - * Check if user is allowed to create items. - * - * @param string $extensionName The extension name with com_ - * @param string $typeName The item type - * - * @return boolean True on allowed. - * - * @since 3.7.0 - */ - public static function allowAdd($extensionName, $typeName) - { - if (!self::hasSupport($extensionName)) - { - return false; - } - - // Get the extension specific helper method - $helper = self::getExtensionHelper($extensionName); - - if (method_exists($helper, 'allowAdd')) - { - return $helper->allowAdd($typeName); - } - - return Factory::getUser()->authorise('core.create', $extensionName); - } - - /** - * Check if an item is checked out - * - * @param string $extensionName The extension name with com_ - * @param string $typeName The item type - * @param int $itemId The id of item for which we need the associated items - * - * @return boolean True if item is checked out. - * - * @since 3.7.0 - */ - public static function isCheckoutItem($extensionName, $typeName, $itemId) - { - if (!self::hasSupport($extensionName)) - { - return false; - } - - if (!self::typeSupportsCheckout($extensionName, $typeName)) - { - return false; - } - - // Get the extension specific helper method - $helper = self::getExtensionHelper($extensionName); - - if (method_exists($helper, 'isCheckoutItem')) - { - return $helper->isCheckoutItem($typeName, $itemId); - } - - $item = self::getItem($extensionName, $typeName, $itemId); - - $checkedOutFieldName = $helper->getTypeFieldName($typeName, 'checked_out'); - - return $item->{$checkedOutFieldName} != 0; - } - - /** - * Check if user can checkin an item. - * - * @param string $extensionName The extension name with com_ - * @param string $typeName The item type - * @param int $itemId The id of item for which we need the associated items - * - * @return boolean True on allowed. - * - * @since 3.7.0 - */ - public static function canCheckinItem($extensionName, $typeName, $itemId) - { - if (!self::hasSupport($extensionName)) - { - return false; - } - - if (!self::typeSupportsCheckout($extensionName, $typeName)) - { - return true; - } - - // Get the extension specific helper method - $helper = self::getExtensionHelper($extensionName); - - if (method_exists($helper, 'canCheckinItem')) - { - return $helper->canCheckinItem($typeName, $itemId); - } - - $item = self::getItem($extensionName, $typeName, $itemId); - - $checkedOutFieldName = $helper->getTypeFieldName($typeName, 'checked_out'); - - $userId = Factory::getUser()->id; - - return ($item->{$checkedOutFieldName} == $userId || $item->{$checkedOutFieldName} == 0); - } - - /** - * Check if the type supports checkout - * - * @param string $extensionName The extension name with com_ - * @param string $typeName The item type - * - * @return boolean True on allowed. - * - * @since 3.7.0 - */ - public static function typeSupportsCheckout($extensionName, $typeName) - { - if (!self::hasSupport($extensionName)) - { - return false; - } - - // Get the extension specific helper method - $helper = self::getExtensionHelper($extensionName); - - $support = $helper->getTypeSupport($typeName); - - return !empty($support['checkout']); - } - - /** - * Get a table field name for a type - * - * @param string $extensionName The extension name with com_ - * @param string $typeName The item type - * @param string $fieldName The item type - * - * @return boolean True on allowed. - * - * @since 3.7.0 - */ - public static function getTypeFieldName($extensionName, $typeName, $fieldName) - { - if (!self::hasSupport($extensionName)) - { - return false; - } - - // Get the extension specific helper method - $helper = self::getExtensionHelper($extensionName); - - return $helper->getTypeFieldName($typeName, $fieldName); - } - - /** - * Gets the language filter system plugin extension id. - * - * @return integer The language filter system plugin extension id. - * - * @since 3.7.2 - */ - public static function getLanguagefilterPluginId() - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) - ->where($db->quoteName('element') . ' = ' . $db->quote('languagefilter')); - $db->setQuery($query); - - try - { - $result = (int) $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - return $result; - } + /** + * Array of Registry objects of extensions + * + * @var array + * @since 3.7.0 + */ + public static $extensionsSupport = null; + + /** + * List of extensions name with support + * + * @var array + * @since 3.7.0 + */ + public static $supportedExtensionsList = array(); + + /** + * Get the associated items for an item + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * @param int $itemId The id of item for which we need the associated items + * + * @return array + * + * @since 3.7.0 + */ + public static function getAssociationList($extensionName, $typeName, $itemId) + { + if (!self::hasSupport($extensionName)) { + return array(); + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + return $helper->getAssociationList($typeName, $itemId); + } + + /** + * Get the the instance of the extension helper class + * + * @param string $extensionName The extension name with com_ + * + * @return \Joomla\CMS\Association\AssociationExtensionHelper|null + * + * @since 3.7.0 + */ + public static function getExtensionHelper($extensionName) + { + if (!self::hasSupport($extensionName)) { + return null; + } + + $support = self::$extensionsSupport[$extensionName]; + + return $support->get('helper'); + } + + /** + * Get item information + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * @param int $itemId The id of item for which we need the associated items + * + * @return \Joomla\CMS\Table\Table|null + * + * @since 3.7.0 + */ + public static function getItem($extensionName, $typeName, $itemId) + { + if (!self::hasSupport($extensionName)) { + return null; + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + return $helper->getItem($typeName, $itemId); + } + + /** + * Check if extension supports associations + * + * @param string $extensionName The extension name with com_ + * + * @return boolean + * + * @since 3.7.0 + */ + public static function hasSupport($extensionName) + { + if (\is_null(self::$extensionsSupport)) { + self::getSupportedExtensions(); + } + + return \in_array($extensionName, self::$supportedExtensionsList); + } + + /** + * Loads the helper for the given class. + * + * @param string $extensionName The extension name with com_ + * + * @return AssociationExtensionInterface|null + * + * @since 4.0.0 + */ + private static function loadHelper($extensionName) + { + $component = Factory::getApplication()->bootComponent($extensionName); + + if ($component instanceof AssociationServiceInterface) { + return $component->getAssociationsExtension(); + } + + // Check if associations helper exists + if (!file_exists(JPATH_ADMINISTRATOR . '/components/' . $extensionName . '/helpers/associations.php')) { + return null; + } + + require_once JPATH_ADMINISTRATOR . '/components/' . $extensionName . '/helpers/associations.php'; + + $componentAssociationsHelperClassName = self::getExtensionHelperClassName($extensionName); + + if (!class_exists($componentAssociationsHelperClassName, false)) { + return null; + } + + // Create an instance of the helper class + return new $componentAssociationsHelperClassName(); + } + + /** + * Get the extension specific helper class name + * + * @param string $extensionName The extension name with com_ + * + * @return string + * + * @since 3.7.0 + */ + private static function getExtensionHelperClassName($extensionName) + { + $realName = self::getExtensionRealName($extensionName); + + return ucfirst($realName) . 'AssociationsHelper'; + } + + /** + * Get the real extension name. This means without com_ + * + * @param string $extensionName The extension name with com_ + * + * @return string + * + * @since 3.7.0 + */ + private static function getExtensionRealName($extensionName) + { + return strpos($extensionName, 'com_') === false ? $extensionName : substr($extensionName, 4); + } + + /** + * Get the associated language edit links Html. + * + * @param string $extensionName Extension Name + * @param string $typeName ItemType + * @param integer $itemId Item id. + * @param string $itemLanguage Item language code. + * @param boolean $addLink True for adding edit links. False for just text. + * @param boolean $assocLanguages True for showing non associated content languages. False only languages with associations. + * + * @return string The language HTML + * + * @since 3.7.0 + */ + public static function getAssociationHtmlList($extensionName, $typeName, $itemId, $itemLanguage, $addLink = true, $assocLanguages = true) + { + // Get the associations list for this item. + $items = self::getAssociationList($extensionName, $typeName, $itemId); + + $titleFieldName = self::getTypeFieldName($extensionName, $typeName, 'title'); + + // Get all content languages. + $languages = LanguageHelper::getContentLanguages(array(0, 1), false); + $content_languages = array_column($languages, 'lang_code'); + + // Display warning if Content Language is trashed or deleted + foreach ($items as $item) { + if (!\in_array($item['language'], $content_languages)) { + Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item['language']), 'warning'); + } + } + + $canEditReference = self::allowEdit($extensionName, $typeName, $itemId); + $canCreate = self::allowAdd($extensionName, $typeName); + + // Create associated items list. + foreach ($languages as $langCode => $language) { + // Don't do for the reference language. + if ($langCode == $itemLanguage) { + continue; + } + + // Don't show languages with associations, if we don't want to show them. + if ($assocLanguages && isset($items[$langCode])) { + unset($items[$langCode]); + continue; + } + + // Don't show languages without associations, if we don't want to show them. + if (!$assocLanguages && !isset($items[$langCode])) { + continue; + } + + // Get html parameters. + if (isset($items[$langCode])) { + $title = $items[$langCode][$titleFieldName]; + $additional = ''; + + if (isset($items[$langCode]['catid'])) { + $db = Factory::getDbo(); + + // Get the category name + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $items[$langCode]['catid'], ParameterType::INTEGER); + + $db->setQuery($query); + $categoryTitle = $db->loadResult(); + + $additional = '' . Text::sprintf('JCATEGORY_SPRINTF', $categoryTitle) . '
'; + } elseif (isset($items[$langCode]['menutype'])) { + $db = Factory::getDbo(); + + // Get the menutype name + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__menu_types')) + ->where($db->quoteName('menutype') . ' = :menutype') + ->bind(':menutype', $items[$langCode]['menutype']); + + $db->setQuery($query); + $menutypeTitle = $db->loadResult(); + + $additional = '' . Text::sprintf('COM_MENUS_MENU_SPRINTF', $menutypeTitle) . '
'; + } + + $labelClass = 'bg-secondary'; + $target = $langCode . ':' . $items[$langCode]['id'] . ':edit'; + $allow = $canEditReference + && self::allowEdit($extensionName, $typeName, $items[$langCode]['id']) + && self::canCheckinItem($extensionName, $typeName, $items[$langCode]['id']); + + $additional .= $addLink && $allow ? Text::_('COM_ASSOCIATIONS_EDIT_ASSOCIATION') : ''; + } else { + $items[$langCode] = array(); + + $title = Text::_('COM_ASSOCIATIONS_NO_ASSOCIATION'); + $additional = $addLink ? Text::_('COM_ASSOCIATIONS_ADD_NEW_ASSOCIATION') : ''; + $labelClass = 'bg-warning text-dark'; + $target = $langCode . ':0:add'; + $allow = $canCreate; + } + + // Generate item Html. + $options = array( + 'option' => 'com_associations', + 'view' => 'association', + 'layout' => 'edit', + 'itemtype' => $extensionName . '.' . $typeName, + 'task' => 'association.edit', + 'id' => $itemId, + 'target' => $target, + ); + + $url = Route::_('index.php?' . http_build_query($options)); + $url = $allow && $addLink ? $url : ''; + $text = $language->lang_code; + + $tooltip = '' . htmlspecialchars($language->title, ENT_QUOTES, 'UTF-8') . '
' + . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . '

' . $additional; + $classes = 'badge ' . $labelClass; + + $items[$langCode]['link'] = '' . $text . '' + . '
' . $tooltip . '
'; + } + + return LayoutHelper::render('joomla.content.associations', $items); + } + + /** + * Get all extensions with associations support. + * + * @return array The extensions. + * + * @since 3.7.0 + */ + public static function getSupportedExtensions() + { + if (!\is_null(self::$extensionsSupport)) { + return self::$extensionsSupport; + } + + self::$extensionsSupport = array(); + + $extensions = self::getEnabledExtensions(); + + foreach ($extensions as $extension) { + $support = self::getSupportedExtension($extension->element); + + if ($support->get('associationssupport') === true) { + self::$supportedExtensionsList[] = $extension->element; + } + + self::$extensionsSupport[$extension->element] = $support; + } + + return self::$extensionsSupport; + } + + /** + * Get item context based on the item key. + * + * @param string $extensionName The extension identifier. + * + * @return \Joomla\Registry\Registry The item properties. + * + * @since 3.7.0 + */ + public static function getSupportedExtension($extensionName) + { + $result = new Registry(); + + $result->def('component', $extensionName); + $result->def('associationssupport', false); + $result->def('helper', null); + + $helper = self::loadHelper($extensionName); + + if (!$helper) { + return $result; + } + + $result->set('helper', $helper); + + if ($helper->hasAssociationsSupport() === false) { + return $result; + } + + $result->set('associationssupport', true); + + // Get the translated titles. + $languagePath = JPATH_ADMINISTRATOR . '/components/' . $extensionName; + $lang = Factory::getLanguage(); + + $lang->load($extensionName . '.sys', JPATH_ADMINISTRATOR); + $lang->load($extensionName . '.sys', $languagePath); + $lang->load($extensionName, JPATH_ADMINISTRATOR); + $lang->load($extensionName, $languagePath); + + $result->def('title', Text::_(strtoupper($extensionName))); + + // Get the supported types + $types = $helper->getItemTypes(); + $rTypes = array(); + + foreach ($types as $typeName) { + $details = $helper->getType($typeName); + $context = 'component'; + $title = $helper->getTypeTitle($typeName); + $languageKey = $typeName; + + $typeNameExploded = explode('.', $typeName); + + if (array_pop($typeNameExploded) === 'category') { + $languageKey = strtoupper($extensionName) . '_CATEGORIES'; + $context = 'category'; + } + + if ($lang->hasKey(strtoupper($extensionName . '_' . $title . 'S'))) { + $languageKey = strtoupper($extensionName . '_' . $title . 'S'); + } + + $title = $lang->hasKey($languageKey) ? Text::_($languageKey) : Text::_('COM_ASSOCIATIONS_ITEMS'); + + $rType = new Registry(); + + $rType->def('name', $typeName); + $rType->def('details', $details); + $rType->def('title', $title); + $rType->def('context', $context); + + $rTypes[$typeName] = $rType; + } + + $result->def('types', $rTypes); + + return $result; + } + + /** + * Get all installed and enabled extensions + * + * @return mixed + * + * @since 3.7.0 + */ + private static function getEnabledExtensions() + { + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('component')) + ->where($db->quoteName('enabled') . ' = 1'); + + $db->setQuery($query); + + return $db->loadObjectList(); + } + + /** + * Get all the content languages. + * + * @return array Array of objects all content languages by language code. + * + * @since 3.7.0 + */ + public static function getContentLanguages() + { + return LanguageHelper::getContentLanguages(array(0, 1)); + } + + /** + * Get the associated items for an item + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * @param int $itemId The id of item for which we need the associated items + * + * @return boolean + * + * @since 3.7.0 + */ + public static function allowEdit($extensionName, $typeName, $itemId) + { + if (!self::hasSupport($extensionName)) { + return false; + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + if (method_exists($helper, 'allowEdit')) { + return $helper->allowEdit($typeName, $itemId); + } + + return Factory::getUser()->authorise('core.edit', $extensionName); + } + + /** + * Check if user is allowed to create items. + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * + * @return boolean True on allowed. + * + * @since 3.7.0 + */ + public static function allowAdd($extensionName, $typeName) + { + if (!self::hasSupport($extensionName)) { + return false; + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + if (method_exists($helper, 'allowAdd')) { + return $helper->allowAdd($typeName); + } + + return Factory::getUser()->authorise('core.create', $extensionName); + } + + /** + * Check if an item is checked out + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * @param int $itemId The id of item for which we need the associated items + * + * @return boolean True if item is checked out. + * + * @since 3.7.0 + */ + public static function isCheckoutItem($extensionName, $typeName, $itemId) + { + if (!self::hasSupport($extensionName)) { + return false; + } + + if (!self::typeSupportsCheckout($extensionName, $typeName)) { + return false; + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + if (method_exists($helper, 'isCheckoutItem')) { + return $helper->isCheckoutItem($typeName, $itemId); + } + + $item = self::getItem($extensionName, $typeName, $itemId); + + $checkedOutFieldName = $helper->getTypeFieldName($typeName, 'checked_out'); + + return $item->{$checkedOutFieldName} != 0; + } + + /** + * Check if user can checkin an item. + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * @param int $itemId The id of item for which we need the associated items + * + * @return boolean True on allowed. + * + * @since 3.7.0 + */ + public static function canCheckinItem($extensionName, $typeName, $itemId) + { + if (!self::hasSupport($extensionName)) { + return false; + } + + if (!self::typeSupportsCheckout($extensionName, $typeName)) { + return true; + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + if (method_exists($helper, 'canCheckinItem')) { + return $helper->canCheckinItem($typeName, $itemId); + } + + $item = self::getItem($extensionName, $typeName, $itemId); + + $checkedOutFieldName = $helper->getTypeFieldName($typeName, 'checked_out'); + + $userId = Factory::getUser()->id; + + return ($item->{$checkedOutFieldName} == $userId || $item->{$checkedOutFieldName} == 0); + } + + /** + * Check if the type supports checkout + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * + * @return boolean True on allowed. + * + * @since 3.7.0 + */ + public static function typeSupportsCheckout($extensionName, $typeName) + { + if (!self::hasSupport($extensionName)) { + return false; + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + $support = $helper->getTypeSupport($typeName); + + return !empty($support['checkout']); + } + + /** + * Get a table field name for a type + * + * @param string $extensionName The extension name with com_ + * @param string $typeName The item type + * @param string $fieldName The item type + * + * @return boolean True on allowed. + * + * @since 3.7.0 + */ + public static function getTypeFieldName($extensionName, $typeName, $fieldName) + { + if (!self::hasSupport($extensionName)) { + return false; + } + + // Get the extension specific helper method + $helper = self::getExtensionHelper($extensionName); + + return $helper->getTypeFieldName($typeName, $fieldName); + } + + /** + * Gets the language filter system plugin extension id. + * + * @return integer The language filter system plugin extension id. + * + * @since 3.7.2 + */ + public static function getLanguagefilterPluginId() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ->where($db->quoteName('element') . ' = ' . $db->quote('languagefilter')); + $db->setQuery($query); + + try { + $result = (int) $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + return $result; + } } diff --git a/administrator/components/com_associations/src/Model/AssociationModel.php b/administrator/components/com_associations/src/Model/AssociationModel.php index 54796fdedad5a..053a52275e2d7 100644 --- a/administrator/components/com_associations/src/Model/AssociationModel.php +++ b/administrator/components/com_associations/src/Model/AssociationModel.php @@ -1,4 +1,5 @@ loadForm('com_associations.association', 'association', array('control' => 'jform', 'load_data' => $loadData)); + /** + * Method to get the record form. + * + * @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 \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure + * + * @since 3.7.0 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_associations.association', 'association', array('control' => 'jform', 'load_data' => $loadData)); - return !empty($form) ? $form : false; - } + return !empty($form) ? $form : false; + } } diff --git a/administrator/components/com_associations/src/Model/AssociationsModel.php b/administrator/components/com_associations/src/Model/AssociationsModel.php index d63dc5bcf1586..d220949c89ae1 100644 --- a/administrator/components/com_associations/src/Model/AssociationsModel.php +++ b/administrator/components/com_associations/src/Model/AssociationsModel.php @@ -1,4 +1,5 @@ input->get('forcedLanguage', '', 'cmd'); - $forcedItemType = $app->input->get('forcedItemType', '', 'string'); - - // Adjust the context to support modal layouts. - if ($layout = $app->input->get('layout')) - { - $this->context .= '.' . $layout; - } - - // Adjust the context to support forced languages. - if ($forcedLanguage) - { - $this->context .= '.' . $forcedLanguage; - } - - // Adjust the context to support forced component item types. - if ($forcedItemType) - { - $this->context .= '.' . $forcedItemType; - } - - $this->setState('itemtype', $this->getUserStateFromRequest($this->context . '.itemtype', 'itemtype', '', 'string')); - $this->setState('language', $this->getUserStateFromRequest($this->context . '.language', 'language', '', 'string')); - - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); - $this->setState('filter.category_id', $this->getUserStateFromRequest($this->context . '.filter.category_id', 'filter_category_id', '', 'cmd')); - $this->setState('filter.menutype', $this->getUserStateFromRequest($this->context . '.filter.menutype', 'filter_menutype', '', 'string')); - $this->setState('filter.access', $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access', '', 'string')); - $this->setState('filter.level', $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level', '', 'cmd')); - - // List state information. - parent::populateState($ordering, $direction); - - // Force a language. - if (!empty($forcedLanguage)) - { - $this->setState('language', $forcedLanguage); - } - - // Force a component item type. - if (!empty($forcedItemType)) - { - $this->setState('itemtype', $forcedItemType); - } - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 3.7.0 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('itemtype'); - $id .= ':' . $this->getState('language'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.category_id'); - $id .= ':' . $this->getState('filter.menutype'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.level'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery|boolean - * - * @since 3.7.0 - */ - protected function getListQuery() - { - $type = null; - - list($extensionName, $typeName) = explode('.', $this->state->get('itemtype'), 2); - - $extension = AssociationsHelper::getSupportedExtension($extensionName); - $types = $extension->get('types'); - - if (\array_key_exists($typeName, $types)) - { - $type = $types[$typeName]; - } - - if (\is_null($type)) - { - return false; - } - - // Create a new query object. - $user = Factory::getUser(); - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $details = $type->get('details'); - - if (!\array_key_exists('support', $details)) - { - return false; - } - - $support = $details['support']; - - if (!\array_key_exists('fields', $details)) - { - return false; - } - - $fields = $details['fields']; - - // Main query. - $query->select($db->quoteName($fields['id'], 'id')) - ->select($db->quoteName($fields['title'], 'title')) - ->select($db->quoteName($fields['alias'], 'alias')); - - if (!\array_key_exists('tables', $details)) - { - return false; - } - - $tables = $details['tables']; - - foreach ($tables as $key => $table) - { - $query->from($db->quoteName($table, $key)); - } - - if (!\array_key_exists('joins', $details)) - { - return false; - } - - $joins = $details['joins']; - - foreach ($joins as $join) - { - $query->join($join['type'], $db->quoteName($join['condition'])); - } - - // Join over the language. - $query->select($db->quoteName($fields['language'], 'language')) - ->select($db->quoteName('l.title', 'language_title')) - ->select($db->quoteName('l.image', 'language_image')) - ->join( - 'LEFT', - $db->quoteName('#__languages', 'l'), - $db->quoteName('l.lang_code') . ' = ' . $db->quoteName($fields['language']) - ); - $extensionNameItem = $extensionName . '.item'; - - // Join over the associations. - $query->select('COUNT(' . $db->quoteName('asso2.id') . ') > 1 AS ' . $db->quoteName('association')) - ->join( - 'LEFT', - $db->quoteName('#__associations', 'asso'), - $db->quoteName('asso.id') . ' = ' . $db->quoteName($fields['id']) - . ' AND ' . $db->quoteName('asso.context') . ' = :context' - ) - ->join( - 'LEFT', - $db->quoteName('#__associations', 'asso2'), - $db->quoteName('asso2.key') . ' = ' . $db->quoteName('asso.key') - ) - ->bind(':context', $extensionNameItem); - - // Prepare the group by clause. - $groupby = array( - $fields['id'], - $fields['title'], - $fields['alias'], - $fields['language'], - 'l.title', - 'l.image', - ); - - // Select author for ACL checks. - if (!empty($fields['created_user_id'])) - { - $query->select($db->quoteName($fields['created_user_id'], 'created_user_id')); - - $groupby[] = $fields['created_user_id']; - } - - // Select checked out data for check in checkins. - if (!empty($fields['checked_out']) && !empty($fields['checked_out_time'])) - { - $query->select($db->quoteName($fields['checked_out'], 'checked_out')) - ->select($db->quoteName($fields['checked_out_time'], 'checked_out_time')); - - // Join over the users. - $query->select($db->quoteName('u.name', 'editor')) - ->join( - 'LEFT', - $db->quoteName('#__users', 'u'), - $db->quoteName('u.id') . ' = ' . $db->quoteName($fields['checked_out']) - ); - - $groupby[] = 'u.name'; - $groupby[] = $fields['checked_out']; - $groupby[] = $fields['checked_out_time']; - } - - // If component item type supports ordering, select the ordering also. - if (!empty($fields['ordering'])) - { - $query->select($db->quoteName($fields['ordering'], 'ordering')); - - $groupby[] = $fields['ordering']; - } - - // If component item type supports state, select the item state also. - if (!empty($fields['state'])) - { - $query->select($db->quoteName($fields['state'], 'state')); - - $groupby[] = $fields['state']; - } - - // If component item type supports level, select the level also. - if (!empty($fields['level'])) - { - $query->select($db->quoteName($fields['level'], 'level')); - - $groupby[] = $fields['level']; - } - - // If component item type supports categories, select the category also. - if (!empty($fields['catid'])) - { - $query->select($db->quoteName($fields['catid'], 'catid')); - - // Join over the categories. - $query->select($db->quoteName('c.title', 'category_title')) - ->join( - 'LEFT', - $db->quoteName('#__categories', 'c'), - $db->quoteName('c.id') . ' = ' . $db->quoteName($fields['catid']) - ); - - $groupby[] = 'c.title'; - $groupby[] = $fields['catid']; - } - - // If component item type supports menu type, select the menu type also. - if (!empty($fields['menutype'])) - { - $query->select($db->quoteName($fields['menutype'], 'menutype')); - - // Join over the menu types. - $query->select($db->quoteName('mt.title', 'menutype_title')) - ->select($db->quoteName('mt.id', 'menutypeid')) - ->join( - 'LEFT', - $db->quoteName('#__menu_types', 'mt'), - $db->quoteName('mt.menutype') . ' = ' . $db->quoteName($fields['menutype']) - ); - - $groupby[] = 'mt.title'; - $groupby[] = 'mt.id'; - $groupby[] = $fields['menutype']; - } - - // If component item type supports access level, select the access level also. - if (\array_key_exists('acl', $support) && $support['acl'] == true && !empty($fields['access'])) - { - $query->select($db->quoteName($fields['access'], 'access')); - - // Join over the access levels. - $query->select($db->quoteName('ag.title', 'access_level')) - ->join( - 'LEFT', - $db->quoteName('#__viewlevels', 'ag'), - $db->quoteName('ag.id') . ' = ' . $db->quoteName($fields['access']) - ); - - $groupby[] = 'ag.title'; - $groupby[] = $fields['access']; - - // Implement View Level Access. - if (!$user->authorise('core.admin', $extensionName)) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName($fields['access']), $groups); - } - } - - // If component item type is menus we need to remove the root item and the administrator menu. - if ($extensionName === 'com_menus') - { - $query->where($db->quoteName($fields['id']) . ' > 1') - ->where($db->quoteName('a.client_id') . ' = 0'); - } - - // If component item type is category we need to remove all other component categories. - if ($typeName === 'category') - { - $query->where($db->quoteName('a.extension') . ' = :extensionname') - ->bind(':extensionname', $extensionName); - } - elseif ($typeNameExploded = explode('.', $typeName)) - { - if (\count($typeNameExploded) > 1 && array_pop($typeNameExploded) === 'category') - { - $section = implode('.', $typeNameExploded); - $extensionNameSection = $extensionName . '.' . $section; - $query->where($db->quoteName('a.extension') . ' = :extensionsection') - ->bind(':extensionsection', $extensionNameSection); - } - } - - // Filter on the language. - if ($language = $this->getState('language')) - { - $query->where($db->quoteName($fields['language']) . ' = :language') - ->bind(':language', $language); - } - - // Filter by item state. - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $state = (int) $state; - $query->where($db->quoteName($fields['state']) . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - elseif ($state === '') - { - $query->whereIn($db->quoteName($fields['state']), [0, 1]); - } - - // Filter on the category. - $baselevel = 1; - - if ($categoryId = $this->getState('filter.category_id')) - { - $categoryTable = Table::getInstance('Category', 'JTable'); - $categoryTable->load($categoryId); - $baselevel = (int) $categoryTable->level; - - $lft = (int) $categoryTable->lft; - $rgt = (int) $categoryTable->rgt; - $query->where($db->quoteName('c.lft') . ' >= :lft') - ->where($db->quoteName('c.rgt') . ' <= :rgt') - ->bind(':lft', $lft, ParameterType::INTEGER) - ->bind(':rgt', $rgt, ParameterType::INTEGER); - } - - // Filter on the level. - if ($level = $this->getState('filter.level')) - { - $queryLevel = ((int) $level + (int) $baselevel - 1); - $query->where($db->quoteName('a.level') . ' <= :alevel') - ->bind(':alevel', $queryLevel, ParameterType::INTEGER); - } - - // Filter by menu type. - if ($menutype = $this->getState('filter.menutype')) - { - $query->where($db->quoteName($fields['menutype']) . ' = :menutype2') - ->bind(':menutype2', $menutype); - } - - // Filter by access level. - if ($access = $this->getState('filter.access')) - { - $access = (int) $access; - $query->where($db->quoteName($fields['access']) . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - - // Filter by search in name. - if ($search = $this->getState('filter.search')) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName($fields['id']) . ' = :searchid') - ->bind(':searchid', $search, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where('(' . $db->quoteName($fields['title']) . ' LIKE :title' - . ' OR ' . $db->quoteName($fields['alias']) . ' LIKE :alias)' - ) - ->bind(':title', $search) - ->bind(':alias', $search); - } - } - - // Add the group by clause - $query->group($db->quoteName($groupby)); - - // Add the list ordering clause - $listOrdering = $this->state->get('list.ordering', 'id'); - $orderDirn = $this->state->get('list.direction', 'ASC'); - - $query->order($db->escape($listOrdering) . ' ' . $db->escape($orderDirn)); - - return $query; - } - - /** - * Delete associations from #__associations table. - * - * @param string $context The associations context. Empty for all. - * @param string $key The associations key. Empty for all. - * - * @return boolean True on success. - * - * @since 3.7.0 - */ - public function purge($context = '', $key = '') - { - $app = Factory::getApplication(); - $db = $this->getDatabase(); - $query = $db->getQuery(true)->delete($db->quoteName('#__associations')); - - // Filter by associations context. - if ($context) - { - $query->where($db->quoteName('context') . ' = :context') - ->bind(':context', $context); - } - - // Filter by key. - if ($key) - { - $query->where($db->quoteName('key') . ' = :key') - ->bind(':key', $key); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (ExecutionFailureException $e) - { - $app->enqueueMessage(Text::_('COM_ASSOCIATIONS_PURGE_FAILED'), 'error'); - - return false; - } - - $app->enqueueMessage( - Text::_((int) $db->getAffectedRows() > 0 ? 'COM_ASSOCIATIONS_PURGE_SUCCESS' : 'COM_ASSOCIATIONS_PURGE_NONE'), - 'message' - ); - - return true; - } - - /** - * Delete orphans from the #__associations table. - * - * @param string $context The associations context. Empty for all. - * @param string $key The associations key. Empty for all. - * - * @return boolean True on success - * - * @since 3.7.0 - */ - public function clean($context = '', $key = '') - { - $app = Factory::getApplication(); - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('key') . ', COUNT(*)') - ->from($db->quoteName('#__associations')) - ->group($db->quoteName('key')) - ->having('COUNT(*) = 1'); - - // Filter by associations context. - if ($context) - { - $query->where($db->quoteName('context') . ' = :context') - ->bind(':context', $context); - } - - // Filter by key. - if ($key) - { - $query->where($db->quoteName('key') . ' = :key') - ->bind(':key', $key); - } - - $db->setQuery($query); - - $assocKeys = $db->loadObjectList(); - - $count = 0; - - // We have orphans. Let's delete them. - foreach ($assocKeys as $value) - { - $query->clear() - ->delete($db->quoteName('#__associations')) - ->where($db->quoteName('key') . ' = :valuekey') - ->bind(':valuekey', $value->key); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (ExecutionFailureException $e) - { - $app->enqueueMessage(Text::_('COM_ASSOCIATIONS_DELETE_ORPHANS_FAILED'), 'error'); - - return false; - } - - $count += (int) $db->getAffectedRows(); - } - - $app->enqueueMessage( - Text::_($count > 0 ? 'COM_ASSOCIATIONS_DELETE_ORPHANS_SUCCESS' : 'COM_ASSOCIATIONS_DELETE_ORPHANS_NONE'), - 'message' - ); - - return true; - } + /** + * Override parent constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.7 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', + 'title', + 'ordering', + 'itemtype', + 'language', + 'association', + 'menutype', + 'menutype_title', + 'level', + 'state', + 'category_id', + 'category_title', + 'access', + 'access_level', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.7.0 + */ + protected function populateState($ordering = 'ordering', $direction = 'asc') + { + $app = Factory::getApplication(); + + $forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd'); + $forcedItemType = $app->input->get('forcedItemType', '', 'string'); + + // Adjust the context to support modal layouts. + if ($layout = $app->input->get('layout')) { + $this->context .= '.' . $layout; + } + + // Adjust the context to support forced languages. + if ($forcedLanguage) { + $this->context .= '.' . $forcedLanguage; + } + + // Adjust the context to support forced component item types. + if ($forcedItemType) { + $this->context .= '.' . $forcedItemType; + } + + $this->setState('itemtype', $this->getUserStateFromRequest($this->context . '.itemtype', 'itemtype', '', 'string')); + $this->setState('language', $this->getUserStateFromRequest($this->context . '.language', 'language', '', 'string')); + + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); + $this->setState('filter.category_id', $this->getUserStateFromRequest($this->context . '.filter.category_id', 'filter_category_id', '', 'cmd')); + $this->setState('filter.menutype', $this->getUserStateFromRequest($this->context . '.filter.menutype', 'filter_menutype', '', 'string')); + $this->setState('filter.access', $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access', '', 'string')); + $this->setState('filter.level', $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level', '', 'cmd')); + + // List state information. + parent::populateState($ordering, $direction); + + // Force a language. + if (!empty($forcedLanguage)) { + $this->setState('language', $forcedLanguage); + } + + // Force a component item type. + if (!empty($forcedItemType)) { + $this->setState('itemtype', $forcedItemType); + } + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 3.7.0 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('itemtype'); + $id .= ':' . $this->getState('language'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.category_id'); + $id .= ':' . $this->getState('filter.menutype'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.level'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery|boolean + * + * @since 3.7.0 + */ + protected function getListQuery() + { + $type = null; + + list($extensionName, $typeName) = explode('.', $this->state->get('itemtype'), 2); + + $extension = AssociationsHelper::getSupportedExtension($extensionName); + $types = $extension->get('types'); + + if (\array_key_exists($typeName, $types)) { + $type = $types[$typeName]; + } + + if (\is_null($type)) { + return false; + } + + // Create a new query object. + $user = Factory::getUser(); + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $details = $type->get('details'); + + if (!\array_key_exists('support', $details)) { + return false; + } + + $support = $details['support']; + + if (!\array_key_exists('fields', $details)) { + return false; + } + + $fields = $details['fields']; + + // Main query. + $query->select($db->quoteName($fields['id'], 'id')) + ->select($db->quoteName($fields['title'], 'title')) + ->select($db->quoteName($fields['alias'], 'alias')); + + if (!\array_key_exists('tables', $details)) { + return false; + } + + $tables = $details['tables']; + + foreach ($tables as $key => $table) { + $query->from($db->quoteName($table, $key)); + } + + if (!\array_key_exists('joins', $details)) { + return false; + } + + $joins = $details['joins']; + + foreach ($joins as $join) { + $query->join($join['type'], $db->quoteName($join['condition'])); + } + + // Join over the language. + $query->select($db->quoteName($fields['language'], 'language')) + ->select($db->quoteName('l.title', 'language_title')) + ->select($db->quoteName('l.image', 'language_image')) + ->join( + 'LEFT', + $db->quoteName('#__languages', 'l'), + $db->quoteName('l.lang_code') . ' = ' . $db->quoteName($fields['language']) + ); + $extensionNameItem = $extensionName . '.item'; + + // Join over the associations. + $query->select('COUNT(' . $db->quoteName('asso2.id') . ') > 1 AS ' . $db->quoteName('association')) + ->join( + 'LEFT', + $db->quoteName('#__associations', 'asso'), + $db->quoteName('asso.id') . ' = ' . $db->quoteName($fields['id']) + . ' AND ' . $db->quoteName('asso.context') . ' = :context' + ) + ->join( + 'LEFT', + $db->quoteName('#__associations', 'asso2'), + $db->quoteName('asso2.key') . ' = ' . $db->quoteName('asso.key') + ) + ->bind(':context', $extensionNameItem); + + // Prepare the group by clause. + $groupby = array( + $fields['id'], + $fields['title'], + $fields['alias'], + $fields['language'], + 'l.title', + 'l.image', + ); + + // Select author for ACL checks. + if (!empty($fields['created_user_id'])) { + $query->select($db->quoteName($fields['created_user_id'], 'created_user_id')); + + $groupby[] = $fields['created_user_id']; + } + + // Select checked out data for check in checkins. + if (!empty($fields['checked_out']) && !empty($fields['checked_out_time'])) { + $query->select($db->quoteName($fields['checked_out'], 'checked_out')) + ->select($db->quoteName($fields['checked_out_time'], 'checked_out_time')); + + // Join over the users. + $query->select($db->quoteName('u.name', 'editor')) + ->join( + 'LEFT', + $db->quoteName('#__users', 'u'), + $db->quoteName('u.id') . ' = ' . $db->quoteName($fields['checked_out']) + ); + + $groupby[] = 'u.name'; + $groupby[] = $fields['checked_out']; + $groupby[] = $fields['checked_out_time']; + } + + // If component item type supports ordering, select the ordering also. + if (!empty($fields['ordering'])) { + $query->select($db->quoteName($fields['ordering'], 'ordering')); + + $groupby[] = $fields['ordering']; + } + + // If component item type supports state, select the item state also. + if (!empty($fields['state'])) { + $query->select($db->quoteName($fields['state'], 'state')); + + $groupby[] = $fields['state']; + } + + // If component item type supports level, select the level also. + if (!empty($fields['level'])) { + $query->select($db->quoteName($fields['level'], 'level')); + + $groupby[] = $fields['level']; + } + + // If component item type supports categories, select the category also. + if (!empty($fields['catid'])) { + $query->select($db->quoteName($fields['catid'], 'catid')); + + // Join over the categories. + $query->select($db->quoteName('c.title', 'category_title')) + ->join( + 'LEFT', + $db->quoteName('#__categories', 'c'), + $db->quoteName('c.id') . ' = ' . $db->quoteName($fields['catid']) + ); + + $groupby[] = 'c.title'; + $groupby[] = $fields['catid']; + } + + // If component item type supports menu type, select the menu type also. + if (!empty($fields['menutype'])) { + $query->select($db->quoteName($fields['menutype'], 'menutype')); + + // Join over the menu types. + $query->select($db->quoteName('mt.title', 'menutype_title')) + ->select($db->quoteName('mt.id', 'menutypeid')) + ->join( + 'LEFT', + $db->quoteName('#__menu_types', 'mt'), + $db->quoteName('mt.menutype') . ' = ' . $db->quoteName($fields['menutype']) + ); + + $groupby[] = 'mt.title'; + $groupby[] = 'mt.id'; + $groupby[] = $fields['menutype']; + } + + // If component item type supports access level, select the access level also. + if (\array_key_exists('acl', $support) && $support['acl'] == true && !empty($fields['access'])) { + $query->select($db->quoteName($fields['access'], 'access')); + + // Join over the access levels. + $query->select($db->quoteName('ag.title', 'access_level')) + ->join( + 'LEFT', + $db->quoteName('#__viewlevels', 'ag'), + $db->quoteName('ag.id') . ' = ' . $db->quoteName($fields['access']) + ); + + $groupby[] = 'ag.title'; + $groupby[] = $fields['access']; + + // Implement View Level Access. + if (!$user->authorise('core.admin', $extensionName)) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName($fields['access']), $groups); + } + } + + // If component item type is menus we need to remove the root item and the administrator menu. + if ($extensionName === 'com_menus') { + $query->where($db->quoteName($fields['id']) . ' > 1') + ->where($db->quoteName('a.client_id') . ' = 0'); + } + + // If component item type is category we need to remove all other component categories. + if ($typeName === 'category') { + $query->where($db->quoteName('a.extension') . ' = :extensionname') + ->bind(':extensionname', $extensionName); + } elseif ($typeNameExploded = explode('.', $typeName)) { + if (\count($typeNameExploded) > 1 && array_pop($typeNameExploded) === 'category') { + $section = implode('.', $typeNameExploded); + $extensionNameSection = $extensionName . '.' . $section; + $query->where($db->quoteName('a.extension') . ' = :extensionsection') + ->bind(':extensionsection', $extensionNameSection); + } + } + + // Filter on the language. + if ($language = $this->getState('language')) { + $query->where($db->quoteName($fields['language']) . ' = :language') + ->bind(':language', $language); + } + + // Filter by item state. + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $state = (int) $state; + $query->where($db->quoteName($fields['state']) . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } elseif ($state === '') { + $query->whereIn($db->quoteName($fields['state']), [0, 1]); + } + + // Filter on the category. + $baselevel = 1; + + if ($categoryId = $this->getState('filter.category_id')) { + $categoryTable = Table::getInstance('Category', 'JTable'); + $categoryTable->load($categoryId); + $baselevel = (int) $categoryTable->level; + + $lft = (int) $categoryTable->lft; + $rgt = (int) $categoryTable->rgt; + $query->where($db->quoteName('c.lft') . ' >= :lft') + ->where($db->quoteName('c.rgt') . ' <= :rgt') + ->bind(':lft', $lft, ParameterType::INTEGER) + ->bind(':rgt', $rgt, ParameterType::INTEGER); + } + + // Filter on the level. + if ($level = $this->getState('filter.level')) { + $queryLevel = ((int) $level + (int) $baselevel - 1); + $query->where($db->quoteName('a.level') . ' <= :alevel') + ->bind(':alevel', $queryLevel, ParameterType::INTEGER); + } + + // Filter by menu type. + if ($menutype = $this->getState('filter.menutype')) { + $query->where($db->quoteName($fields['menutype']) . ' = :menutype2') + ->bind(':menutype2', $menutype); + } + + // Filter by access level. + if ($access = $this->getState('filter.access')) { + $access = (int) $access; + $query->where($db->quoteName($fields['access']) . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + + // Filter by search in name. + if ($search = $this->getState('filter.search')) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName($fields['id']) . ' = :searchid') + ->bind(':searchid', $search, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where('(' . $db->quoteName($fields['title']) . ' LIKE :title' + . ' OR ' . $db->quoteName($fields['alias']) . ' LIKE :alias)') + ->bind(':title', $search) + ->bind(':alias', $search); + } + } + + // Add the group by clause + $query->group($db->quoteName($groupby)); + + // Add the list ordering clause + $listOrdering = $this->state->get('list.ordering', 'id'); + $orderDirn = $this->state->get('list.direction', 'ASC'); + + $query->order($db->escape($listOrdering) . ' ' . $db->escape($orderDirn)); + + return $query; + } + + /** + * Delete associations from #__associations table. + * + * @param string $context The associations context. Empty for all. + * @param string $key The associations key. Empty for all. + * + * @return boolean True on success. + * + * @since 3.7.0 + */ + public function purge($context = '', $key = '') + { + $app = Factory::getApplication(); + $db = $this->getDatabase(); + $query = $db->getQuery(true)->delete($db->quoteName('#__associations')); + + // Filter by associations context. + if ($context) { + $query->where($db->quoteName('context') . ' = :context') + ->bind(':context', $context); + } + + // Filter by key. + if ($key) { + $query->where($db->quoteName('key') . ' = :key') + ->bind(':key', $key); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (ExecutionFailureException $e) { + $app->enqueueMessage(Text::_('COM_ASSOCIATIONS_PURGE_FAILED'), 'error'); + + return false; + } + + $app->enqueueMessage( + Text::_((int) $db->getAffectedRows() > 0 ? 'COM_ASSOCIATIONS_PURGE_SUCCESS' : 'COM_ASSOCIATIONS_PURGE_NONE'), + 'message' + ); + + return true; + } + + /** + * Delete orphans from the #__associations table. + * + * @param string $context The associations context. Empty for all. + * @param string $key The associations key. Empty for all. + * + * @return boolean True on success + * + * @since 3.7.0 + */ + public function clean($context = '', $key = '') + { + $app = Factory::getApplication(); + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('key') . ', COUNT(*)') + ->from($db->quoteName('#__associations')) + ->group($db->quoteName('key')) + ->having('COUNT(*) = 1'); + + // Filter by associations context. + if ($context) { + $query->where($db->quoteName('context') . ' = :context') + ->bind(':context', $context); + } + + // Filter by key. + if ($key) { + $query->where($db->quoteName('key') . ' = :key') + ->bind(':key', $key); + } + + $db->setQuery($query); + + $assocKeys = $db->loadObjectList(); + + $count = 0; + + // We have orphans. Let's delete them. + foreach ($assocKeys as $value) { + $query->clear() + ->delete($db->quoteName('#__associations')) + ->where($db->quoteName('key') . ' = :valuekey') + ->bind(':valuekey', $value->key); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (ExecutionFailureException $e) { + $app->enqueueMessage(Text::_('COM_ASSOCIATIONS_DELETE_ORPHANS_FAILED'), 'error'); + + return false; + } + + $count += (int) $db->getAffectedRows(); + } + + $app->enqueueMessage( + Text::_($count > 0 ? 'COM_ASSOCIATIONS_DELETE_ORPHANS_SUCCESS' : 'COM_ASSOCIATIONS_DELETE_ORPHANS_NONE'), + 'message' + ); + + return true; + } } diff --git a/administrator/components/com_associations/src/View/Association/HtmlView.php b/administrator/components/com_associations/src/View/Association/HtmlView.php index c54f8b80dcfeb..4fb58586283af 100644 --- a/administrator/components/com_associations/src/View/Association/HtmlView.php +++ b/administrator/components/com_associations/src/View/Association/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - - // Check for errors. - if (\count($errors = $model->getErrors())) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->app = Factory::getApplication(); - $this->form = $model->getForm(); - /** @var Input $input */ - $input = $this->app->input; - $this->referenceId = $input->get('id', 0, 'int'); - - [$extensionName, $typeName] = explode('.', $input->get('itemtype', '', 'string'), 2); - - /** @var Registry $extension */ - $extension = AssociationsHelper::getSupportedExtension($extensionName); - $types = $extension->get('types'); - - if (\array_key_exists($typeName, $types)) - { - $this->type = $types[$typeName]; - $this->typeSupports = []; - $details = $this->type->get('details'); - $this->save2copy = false; - - if (\array_key_exists('support', $details)) - { - $support = $details['support']; - $this->typeSupports = $support; - } - - if (!empty($this->typeSupports['save2copy'])) - { - $this->save2copy = true; - } - } - - $this->extensionName = $extensionName; - $this->typeName = $typeName; - $this->itemType = $extensionName . '.' . $typeName; - - $languageField = AssociationsHelper::getTypeFieldName($extensionName, $typeName, 'language'); - $referenceId = $input->get('id', 0, 'int'); - $reference = ArrayHelper::fromObject(AssociationsHelper::getItem($extensionName, $typeName, $referenceId)); - - $this->referenceLanguage = $reference[$languageField]; - $this->referenceTitle = AssociationsHelper::getTypeFieldName($extensionName, $typeName, 'title'); - $this->referenceTitleValue = $reference[$this->referenceTitle]; - - // Check for special case category - $typeNameExploded = explode('.', $typeName); - - if (array_pop($typeNameExploded) === 'category') - { - $this->typeName = 'category'; - - if ($typeNameExploded) - { - $extensionName .= '.' . implode('.', $typeNameExploded); - } - - $options = [ - 'option' => 'com_categories', - 'view' => 'category', - 'extension' => $extensionName, - 'tmpl' => 'component', - ]; - } - else - { - $options = [ - 'option' => $extensionName, - 'view' => $typeName, - 'extension' => $extensionName, - 'tmpl' => 'component', - ]; - } - - // Reference and target edit links. - $this->editUri = 'index.php?' . http_build_query($options); - - // Get target language. - $this->targetId = '0'; - $this->targetLanguage = ''; - $this->defaultTargetSrc = ''; - $this->targetAction = ''; - $this->targetTitle = ''; - - if ($target = $input->get('target', '', 'string')) - { - $matches = preg_split("#[\:]+#", $target); - $this->targetAction = $matches[2]; - $this->targetId = $matches[1]; - $this->targetLanguage = $matches[0]; - $this->targetTitle = AssociationsHelper::getTypeFieldName($extensionName, $typeName, 'title'); - $task = $typeName . '.' . $this->targetAction; - - /** - * Let's put the target src into a variable to use in the javascript code - * to avoid race conditions when the reference iframe loads. - */ - $this->document->addScriptOptions('targetSrc', Route::_($this->editUri . '&task=' . $task . '&id=' . (int) $this->targetId)); - $this->form->setValue('itemlanguage', '', $this->targetLanguage . ':' . $this->targetId . ':' . $this->targetAction); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.7.0 - * - * @throws \Exception - */ - protected function addToolbar(): void - { - // Hide main menu. - $this->app->input->set('hidemainmenu', 1); - - $helper = AssociationsHelper::getExtensionHelper($this->extensionName); - $title = $helper->getTypeTitle($this->typeName); - - $languageKey = strtoupper($this->extensionName . '_' . $title . 'S'); - - if ($this->typeName === 'category') - { - $languageKey = strtoupper($this->extensionName) . '_CATEGORIES'; - } - - ToolbarHelper::title( - Text::sprintf( - 'COM_ASSOCIATIONS_TITLE_EDIT', - Text::_($this->extensionName), - Text::_($languageKey) - ), - 'language assoc' - ); - - $bar = Toolbar::getInstance(); - - $bar->appendButton( - 'Custom', '', 'reference' - ); - - $bar->appendButton( - 'Custom', '', 'target' - ); - - if ($this->typeName === 'category' || $this->extensionName === 'com_menus' || $this->save2copy === true) - { - ToolbarHelper::custom('copy', 'copy.png', '', 'COM_ASSOCIATIONS_COPY_REFERENCE', false); - } - - ToolbarHelper::cancel('association.cancel', 'JTOOLBAR_CLOSE'); - ToolbarHelper::help('Multilingual_Associations:_Edit'); - } + /** + * An array of items + * + * @var array + * + * @since 3.7.0 + */ + protected $items = []; + + /** + * The pagination object + * + * @var Pagination + * + * @since 3.7.0 + */ + protected $pagination; + + /** + * The model state + * + * @var CMSObject + * + * @since 3.7.0 + */ + protected $state; + + /** + * Selected item type properties. + * + * @var Registry + * + * @since 3.7.0 + */ + protected $itemType; + + /** + * The application + * + * @var AdministratorApplication + * @since 3.7.0 + */ + protected $app; + + /** + * The ID of the reference language + * + * @var integer + * @since 3.7.0 + */ + protected $referenceId = 0; + + /** + * The type name + * + * @var string + * @since 3.7.0 + */ + protected $typeName = ''; + + /** + * The reference language + * + * @var string + * @since 3.7.0 + */ + protected $referenceLanguage = ''; + + /** + * The title of the reference language + * + * @var string + * @since 3.7.0 + */ + protected $referenceTitle = ''; + + /** + * The value of the reference title + * + * @var string + * @since 3.7.0 + */ + protected $referenceTitleValue = ''; + + /** + * The URL to the edit screen + * + * @var string + * @since 3.7.0 + */ + protected $editUri = ''; + + /** + * The ID of the target field + * + * @var string + * @since 3.7.0 + */ + protected $targetId = ''; + + /** + * The target language + * + * @var string + * @since 3.7.0 + */ + protected $targetLanguage = ''; + + /** + * The source of the target field + * + * @var string + * @since 3.7.0 + */ + protected $defaultTargetSrc = ''; + + /** + * The action to perform for the target field + * + * @var string + * @since 3.7.0 + */ + protected $targetAction = ''; + + /** + * The title of the target field + * + * @var string + * @since 3.7.0 + */ + protected $targetTitle = ''; + + /** + * The edit form + * + * @var Form + * @since 3.7.0 + */ + protected $form; + + /** + * Set if the option is set to save as copy + * + * @var boolean + * @since 3.7.0 + */ + private $save2copy = false; + + /** + * The type of language + * + * @var Registry + * @since 3.7.0 + */ + private $type; + + /** + * The supported types + * + * @var array + * @since 3.7.0 + */ + private $typeSupports = []; + + /** + * The extension name + * + * @var string + * @since 3.7.0 + */ + private $extensionName = ''; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 3.7.0 + * + * @throws \Exception + */ + public function display($tpl = null): void + { + /** @var AssociationModel $model */ + $model = $this->getModel(); + + // Check for errors. + if (\count($errors = $model->getErrors())) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->app = Factory::getApplication(); + $this->form = $model->getForm(); + /** @var Input $input */ + $input = $this->app->input; + $this->referenceId = $input->get('id', 0, 'int'); + + [$extensionName, $typeName] = explode('.', $input->get('itemtype', '', 'string'), 2); + + /** @var Registry $extension */ + $extension = AssociationsHelper::getSupportedExtension($extensionName); + $types = $extension->get('types'); + + if (\array_key_exists($typeName, $types)) { + $this->type = $types[$typeName]; + $this->typeSupports = []; + $details = $this->type->get('details'); + $this->save2copy = false; + + if (\array_key_exists('support', $details)) { + $support = $details['support']; + $this->typeSupports = $support; + } + + if (!empty($this->typeSupports['save2copy'])) { + $this->save2copy = true; + } + } + + $this->extensionName = $extensionName; + $this->typeName = $typeName; + $this->itemType = $extensionName . '.' . $typeName; + + $languageField = AssociationsHelper::getTypeFieldName($extensionName, $typeName, 'language'); + $referenceId = $input->get('id', 0, 'int'); + $reference = ArrayHelper::fromObject(AssociationsHelper::getItem($extensionName, $typeName, $referenceId)); + + $this->referenceLanguage = $reference[$languageField]; + $this->referenceTitle = AssociationsHelper::getTypeFieldName($extensionName, $typeName, 'title'); + $this->referenceTitleValue = $reference[$this->referenceTitle]; + + // Check for special case category + $typeNameExploded = explode('.', $typeName); + + if (array_pop($typeNameExploded) === 'category') { + $this->typeName = 'category'; + + if ($typeNameExploded) { + $extensionName .= '.' . implode('.', $typeNameExploded); + } + + $options = [ + 'option' => 'com_categories', + 'view' => 'category', + 'extension' => $extensionName, + 'tmpl' => 'component', + ]; + } else { + $options = [ + 'option' => $extensionName, + 'view' => $typeName, + 'extension' => $extensionName, + 'tmpl' => 'component', + ]; + } + + // Reference and target edit links. + $this->editUri = 'index.php?' . http_build_query($options); + + // Get target language. + $this->targetId = '0'; + $this->targetLanguage = ''; + $this->defaultTargetSrc = ''; + $this->targetAction = ''; + $this->targetTitle = ''; + + if ($target = $input->get('target', '', 'string')) { + $matches = preg_split("#[\:]+#", $target); + $this->targetAction = $matches[2]; + $this->targetId = $matches[1]; + $this->targetLanguage = $matches[0]; + $this->targetTitle = AssociationsHelper::getTypeFieldName($extensionName, $typeName, 'title'); + $task = $typeName . '.' . $this->targetAction; + + /** + * Let's put the target src into a variable to use in the javascript code + * to avoid race conditions when the reference iframe loads. + */ + $this->document->addScriptOptions('targetSrc', Route::_($this->editUri . '&task=' . $task . '&id=' . (int) $this->targetId)); + $this->form->setValue('itemlanguage', '', $this->targetLanguage . ':' . $this->targetId . ':' . $this->targetAction); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.7.0 + * + * @throws \Exception + */ + protected function addToolbar(): void + { + // Hide main menu. + $this->app->input->set('hidemainmenu', 1); + + $helper = AssociationsHelper::getExtensionHelper($this->extensionName); + $title = $helper->getTypeTitle($this->typeName); + + $languageKey = strtoupper($this->extensionName . '_' . $title . 'S'); + + if ($this->typeName === 'category') { + $languageKey = strtoupper($this->extensionName) . '_CATEGORIES'; + } + + ToolbarHelper::title( + Text::sprintf( + 'COM_ASSOCIATIONS_TITLE_EDIT', + Text::_($this->extensionName), + Text::_($languageKey) + ), + 'language assoc' + ); + + $bar = Toolbar::getInstance(); + + $bar->appendButton( + 'Custom', + '', + 'reference' + ); + + $bar->appendButton( + 'Custom', + '', + 'target' + ); + + if ($this->typeName === 'category' || $this->extensionName === 'com_menus' || $this->save2copy === true) { + ToolbarHelper::custom('copy', 'copy.png', '', 'COM_ASSOCIATIONS_COPY_REFERENCE', false); + } + + ToolbarHelper::cancel('association.cancel', 'JTOOLBAR_CLOSE'); + ToolbarHelper::help('Multilingual_Associations:_Edit'); + } } diff --git a/administrator/components/com_associations/src/View/Associations/HtmlView.php b/administrator/components/com_associations/src/View/Associations/HtmlView.php index 12dc60323e892..e3e14c96bfbbb 100644 --- a/administrator/components/com_associations/src/View/Associations/HtmlView.php +++ b/administrator/components/com_associations/src/View/Associations/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!Associations::isEnabled()) - { - $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . AssociationsHelper::getLanguagefilterPluginId()); - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_ASSOCIATIONS_ERROR_NO_ASSOC', $link), 'warning'); - } - elseif ($this->state->get('itemtype') != '' && $this->state->get('language') != '') - { - $type = null; - - list($extensionName, $typeName) = explode('.', $this->state->get('itemtype'), 2); - - $extension = AssociationsHelper::getSupportedExtension($extensionName); - - $types = $extension->get('types'); - - if (\array_key_exists($typeName, $types)) - { - $type = $types[$typeName]; - } - - $this->itemType = $type; - - if (\is_null($type)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_ASSOCIATIONS_ERROR_NO_TYPE'), 'warning'); - } - else - { - $this->extensionName = $extensionName; - $this->typeName = $typeName; - $this->typeSupports = array(); - $this->typeFields = array(); - - $details = $type->get('details'); - - if (\array_key_exists('support', $details)) - { - $support = $details['support']; - $this->typeSupports = $support; - } - - if (\array_key_exists('fields', $details)) - { - $fields = $details['fields']; - $this->typeFields = $fields; - } - - // Dynamic filter form. - // This selectors doesn't have to activate the filter bar. - unset($this->activeFilters['itemtype']); - unset($this->activeFilters['language']); - - // Remove filters options depending on selected type. - if (empty($support['state'])) - { - unset($this->activeFilters['state']); - $this->filterForm->removeField('state', 'filter'); - } - - if (empty($support['category'])) - { - unset($this->activeFilters['category_id']); - $this->filterForm->removeField('category_id', 'filter'); - } - - if ($extensionName !== 'com_menus') - { - unset($this->activeFilters['menutype']); - $this->filterForm->removeField('menutype', 'filter'); - } - - if (empty($support['level'])) - { - unset($this->activeFilters['level']); - $this->filterForm->removeField('level', 'filter'); - } - - if (empty($support['acl'])) - { - unset($this->activeFilters['access']); - $this->filterForm->removeField('access', 'filter'); - } - - // Add extension attribute to category filter. - if (empty($support['catid'])) - { - $this->filterForm->setFieldAttribute('category_id', 'extension', $extensionName, 'filter'); - - if ($this->getLayout() == 'modal') - { - // We need to change the category filter to only show categories tagged to All or to the forced language. - if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) - { - $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); - } - } - } - - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - - $linkParameters = array( - 'layout' => 'edit', - 'itemtype' => $extensionName . '.' . $typeName, - 'task' => 'association.edit', - ); - - $this->editUri = 'index.php?option=com_associations&view=association&' . http_build_query($linkParameters); - } - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new \Exception(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.7.0 - */ - protected function addToolbar() - { - $user = $this->getCurrentUser(); - - if (isset($this->typeName) && isset($this->extensionName)) - { - $helper = AssociationsHelper::getExtensionHelper($this->extensionName); - $title = $helper->getTypeTitle($this->typeName); - - $languageKey = strtoupper($this->extensionName . '_' . $title . 'S'); - - if ($this->typeName === 'category') - { - $languageKey = strtoupper($this->extensionName) . '_CATEGORIES'; - } - - ToolbarHelper::title( - Text::sprintf( - 'COM_ASSOCIATIONS_TITLE_LIST', Text::_($this->extensionName), Text::_($languageKey) - ), 'language assoc' - ); - } - else - { - ToolbarHelper::title(Text::_('COM_ASSOCIATIONS_TITLE_LIST_SELECT'), 'language assoc'); - } - - if ($user->authorise('core.admin', 'com_associations') || $user->authorise('core.options', 'com_associations')) - { - if (!isset($this->typeName)) - { - ToolbarHelper::custom('associations.purge', 'purge', '', 'COM_ASSOCIATIONS_PURGE', false, false); - ToolbarHelper::custom('associations.clean', 'refresh', '', 'COM_ASSOCIATIONS_DELETE_ORPHANS', false, false); - } - - ToolbarHelper::preferences('com_associations'); - } - - ToolbarHelper::help('Multilingual_Associations'); - } + /** + * An array of items + * + * @var array + * + * @since 3.7.0 + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 3.7.0 + */ + protected $pagination; + + /** + * The model state + * + * @var object + * + * @since 3.7.0 + */ + protected $state; + + /** + * Selected item type properties. + * + * @var \Joomla\Registry\Registry + * + * @since 3.7.0 + */ + public $itemType = null; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 3.7.0 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!Associations::isEnabled()) { + $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . AssociationsHelper::getLanguagefilterPluginId()); + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_ASSOCIATIONS_ERROR_NO_ASSOC', $link), 'warning'); + } elseif ($this->state->get('itemtype') != '' && $this->state->get('language') != '') { + $type = null; + + list($extensionName, $typeName) = explode('.', $this->state->get('itemtype'), 2); + + $extension = AssociationsHelper::getSupportedExtension($extensionName); + + $types = $extension->get('types'); + + if (\array_key_exists($typeName, $types)) { + $type = $types[$typeName]; + } + + $this->itemType = $type; + + if (\is_null($type)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_ASSOCIATIONS_ERROR_NO_TYPE'), 'warning'); + } else { + $this->extensionName = $extensionName; + $this->typeName = $typeName; + $this->typeSupports = array(); + $this->typeFields = array(); + + $details = $type->get('details'); + + if (\array_key_exists('support', $details)) { + $support = $details['support']; + $this->typeSupports = $support; + } + + if (\array_key_exists('fields', $details)) { + $fields = $details['fields']; + $this->typeFields = $fields; + } + + // Dynamic filter form. + // This selectors doesn't have to activate the filter bar. + unset($this->activeFilters['itemtype']); + unset($this->activeFilters['language']); + + // Remove filters options depending on selected type. + if (empty($support['state'])) { + unset($this->activeFilters['state']); + $this->filterForm->removeField('state', 'filter'); + } + + if (empty($support['category'])) { + unset($this->activeFilters['category_id']); + $this->filterForm->removeField('category_id', 'filter'); + } + + if ($extensionName !== 'com_menus') { + unset($this->activeFilters['menutype']); + $this->filterForm->removeField('menutype', 'filter'); + } + + if (empty($support['level'])) { + unset($this->activeFilters['level']); + $this->filterForm->removeField('level', 'filter'); + } + + if (empty($support['acl'])) { + unset($this->activeFilters['access']); + $this->filterForm->removeField('access', 'filter'); + } + + // Add extension attribute to category filter. + if (empty($support['catid'])) { + $this->filterForm->setFieldAttribute('category_id', 'extension', $extensionName, 'filter'); + + if ($this->getLayout() == 'modal') { + // We need to change the category filter to only show categories tagged to All or to the forced language. + if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) { + $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); + } + } + } + + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + + $linkParameters = array( + 'layout' => 'edit', + 'itemtype' => $extensionName . '.' . $typeName, + 'task' => 'association.edit', + ); + + $this->editUri = 'index.php?option=com_associations&view=association&' . http_build_query($linkParameters); + } + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new \Exception(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.7.0 + */ + protected function addToolbar() + { + $user = $this->getCurrentUser(); + + if (isset($this->typeName) && isset($this->extensionName)) { + $helper = AssociationsHelper::getExtensionHelper($this->extensionName); + $title = $helper->getTypeTitle($this->typeName); + + $languageKey = strtoupper($this->extensionName . '_' . $title . 'S'); + + if ($this->typeName === 'category') { + $languageKey = strtoupper($this->extensionName) . '_CATEGORIES'; + } + + ToolbarHelper::title( + Text::sprintf( + 'COM_ASSOCIATIONS_TITLE_LIST', + Text::_($this->extensionName), + Text::_($languageKey) + ), + 'language assoc' + ); + } else { + ToolbarHelper::title(Text::_('COM_ASSOCIATIONS_TITLE_LIST_SELECT'), 'language assoc'); + } + + if ($user->authorise('core.admin', 'com_associations') || $user->authorise('core.options', 'com_associations')) { + if (!isset($this->typeName)) { + ToolbarHelper::custom('associations.purge', 'purge', '', 'COM_ASSOCIATIONS_PURGE', false, false); + ToolbarHelper::custom('associations.clean', 'refresh', '', 'COM_ASSOCIATIONS_DELETE_ORPHANS', false, false); + } + + ToolbarHelper::preferences('com_associations'); + } + + ToolbarHelper::help('Multilingual_Associations'); + } } diff --git a/administrator/components/com_associations/tmpl/association/edit.php b/administrator/components/com_associations/tmpl/association/edit.php index 422799a43312a..0f7b5bc1b4a1a 100644 --- a/administrator/components/com_associations/tmpl/association/edit.php +++ b/administrator/components/com_associations/tmpl/association/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->usePreset('com_associations.sidebyside') - ->useScript('webcomponent.core-loader'); + ->useScript('form.validate') + ->usePreset('com_associations.sidebyside') + ->useScript('webcomponent.core-loader'); $options = [ - 'layout' => $this->app->input->get('layout', '', 'string'), - 'itemtype' => $this->itemType, - 'id' => $this->referenceId, + 'layout' => $this->app->input->get('layout', '', 'string'), + 'itemtype' => $this->itemType, + 'id' => $this->referenceId, ]; ?>
-
-
-
-

- -
-
-
-
-
-
-

-
-
-
- form->getLabel('itemlanguage'); ?> -
- form->getInput('itemlanguage'); ?> -
-
- form->getInput('modalassociation'); ?> -
-
- -
-
-
+
+
+
+

+ +
+
+
+
+
+
+

+
+
+
+ form->getLabel('itemlanguage'); ?> +
+ form->getInput('itemlanguage'); ?> +
+
+ form->getInput('modalassociation'); ?> +
+
+ +
+
+
- - - + + +
diff --git a/administrator/components/com_associations/tmpl/associations/default.php b/administrator/components/com_associations/tmpl/associations/default.php index fd7879f38d399..ec539fed43377 100644 --- a/administrator/components/com_associations/tmpl/associations/default.php +++ b/administrator/components/com_associations/tmpl/associations/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('com_associations.admin-associations-default') - ->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('table.columns') + ->useScript('multiselect'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); $canManageCheckin = Factory::getUser()->authorise('core.manage', 'com_checkin'); $iconStates = array( - -2 => 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - 2 => 'icon-folder', + -2 => 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + 2 => 'icon-folder', ); Text::script('COM_ASSOCIATIONS_PURGE_CONFIRM_PROMPT', true); ?>
-
-
-
- $this)); ?> - state->get('itemtype') == '' || $this->state->get('language') == '') : ?> -
- - -
- items)) : ?> -
- - -
- - - - - - typeSupports['state'])) : ?> - - - - - - - typeFields['menutype'])) : ?> - - - typeFields['access'])) : ?> - - - - - - - items as $i => $item) : - $canCheckin = true; - $canEdit = AssociationsHelper::allowEdit($this->extensionName, $this->typeName, $item->id); - $canCheckin = $canManageCheckin || AssociationsHelper::canCheckinItem($this->extensionName, $this->typeName, $item->id); - $isCheckout = AssociationsHelper::isCheckoutItem($this->extensionName, $this->typeName, $item->id); - ?> - - typeSupports['state'])) : ?> - - - - - - - typeFields['menutype'])) : ?> - - - typeFields['access'])) : ?> - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - -
- - -
- level)) : ?> - $item->level)); ?> - - - editor, $item->checked_out_time, 'associations.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - - typeFields['alias'])) : ?> -
- escape($item->alias)); ?> -
- - typeFields['catid'])) : ?> -
- escape($item->category_title); ?> -
- -
-
- - - extensionName, $this->typeName, (int) $item->id, $item->language, !$isCheckout, false); ?> - - extensionName, $this->typeName, (int) $item->id, $item->language, !$isCheckout, true); ?> - - escape($item->menutype_title); ?> - - escape($item->access_level); ?> - - id; ?> -
+
+
+
+ $this)); ?> + state->get('itemtype') == '' || $this->state->get('language') == '') : ?> +
+ + +
+ items)) : ?> +
+ + +
+ + + + + + typeSupports['state'])) : ?> + + + + + + + typeFields['menutype'])) : ?> + + + typeFields['access'])) : ?> + + + + + + + items as $i => $item) : + $canCheckin = true; + $canEdit = AssociationsHelper::allowEdit($this->extensionName, $this->typeName, $item->id); + $canCheckin = $canManageCheckin || AssociationsHelper::canCheckinItem($this->extensionName, $this->typeName, $item->id); + $isCheckout = AssociationsHelper::isCheckoutItem($this->extensionName, $this->typeName, $item->id); + ?> + + typeSupports['state'])) : ?> + + + + + + + typeFields['menutype'])) : ?> + + + typeFields['access'])) : ?> + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + +
+ + +
+ level)) : ?> + $item->level)); ?> + + + editor, $item->checked_out_time, 'associations.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + + typeFields['alias'])) : ?> +
+ escape($item->alias)); ?> +
+ + typeFields['catid'])) : ?> +
+ escape($item->category_title); ?> +
+ +
+
+ + + extensionName, $this->typeName, (int) $item->id, $item->language, !$isCheckout, false); ?> + + extensionName, $this->typeName, (int) $item->id, $item->language, !$isCheckout, true); ?> + + escape($item->menutype_title); ?> + + escape($item->access_level); ?> + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - -
-
-
+ + + +
+
+
diff --git a/administrator/components/com_associations/tmpl/associations/modal.php b/administrator/components/com_associations/tmpl/associations/modal.php index b46c16594488a..398e271296b1c 100644 --- a/administrator/components/com_associations/tmpl/associations/modal.php +++ b/administrator/components/com_associations/tmpl/associations/modal.php @@ -1,4 +1,5 @@ isClient('site')) -{ - Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); +if ($app->isClient('site')) { + Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); } /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('multiselect') - ->useScript('com_associations.admin-associations-modal'); + ->useScript('com_associations.admin-associations-modal'); $function = $app->input->getCmd('function', 'jSelectAssociation'); $listOrder = $this->escape($this->state->get('list.ordering')); @@ -35,136 +35,136 @@ $canManageCheckin = Factory::getUser()->authorise('core.manage', 'com_checkin'); $iconStates = array( - -2 => 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - 2 => 'icon-folder', + -2 => 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + 2 => 'icon-folder', ); $this->document->addScriptOptions('associations-modal', ['func' => $function]); ?>
-
- $this)); ?> - items)) : ?> -
- - -
- - - - - - typeSupports['state'])) : ?> - - - - - - typeFields['menutype'])) : ?> - - - typeSupports['acl'])) : ?> - - - - - - - items as $i => $item) : - $canEdit = AssociationsHelper::allowEdit($this->extensionName, $this->typeName, $item->id); - $canCheckin = $canManageCheckin || AssociationsHelper::canCheckinItem($this->extensionName, $this->typeName, $item->id); - $isCheckout = AssociationsHelper::isCheckoutItem($this->extensionName, $this->typeName, $item->id); - ?> - - typeSupports['state'])) : ?> - - - - - - typeFields['menutype'])) : ?> - - - typeSupports['acl'])) : ?> - - - - - - -
- , - , - -
- - - - - - - - - - - - - -
- - - - - level)) : ?> - $item->level)); ?> - - - - escape($item->title); ?> - - editor, $item->checked_out_time, 'associations.'); ?> - - escape($item->title); ?> - - - escape($item->title); ?> - - typeFields['alias'])) : ?> - - escape($item->alias)); ?> - - - typeFields['catid'])) : ?> -
- escape($item->category_title); ?> -
- -
- - - association) : ?> - extensionName, $this->typeName, (int) $item->id, $item->language, false, false); ?> - - - escape($item->menutype_title); ?> - - escape($item->access_level); ?> - - id; ?> -
+ + $this)); ?> + items)) : ?> +
+ + +
+ + + + + + typeSupports['state'])) : ?> + + + + + + typeFields['menutype'])) : ?> + + + typeSupports['acl'])) : ?> + + + + + + + items as $i => $item) : + $canEdit = AssociationsHelper::allowEdit($this->extensionName, $this->typeName, $item->id); + $canCheckin = $canManageCheckin || AssociationsHelper::canCheckinItem($this->extensionName, $this->typeName, $item->id); + $isCheckout = AssociationsHelper::isCheckoutItem($this->extensionName, $this->typeName, $item->id); + ?> + + typeSupports['state'])) : ?> + + + + + + typeFields['menutype'])) : ?> + + + typeSupports['acl'])) : ?> + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + +
+ + + + + level)) : ?> + $item->level)); ?> + + + + escape($item->title); ?> + + editor, $item->checked_out_time, 'associations.'); ?> + + escape($item->title); ?> + + + escape($item->title); ?> + + typeFields['alias'])) : ?> + + escape($item->alias)); ?> + + + typeFields['catid'])) : ?> +
+ escape($item->category_title); ?> +
+ +
+ + + association) : ?> + extensionName, $this->typeName, (int) $item->id, $item->language, false, false); ?> + + + escape($item->menutype_title); ?> + + escape($item->access_level); ?> + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - - -
+ + + + +
diff --git a/administrator/components/com_banners/helpers/banners.php b/administrator/components/com_banners/helpers/banners.php index 97747c9c6c728..cf15f619cc341 100644 --- a/administrator/components/com_banners/helpers/banners.php +++ b/administrator/components/com_banners/helpers/banners.php @@ -1,13 +1,16 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; + /** * Banners component helper. @@ -18,5 +21,4 @@ */ class BannersHelper extends \Joomla\Component\Banners\Administrator\Helper\BannersHelper { - } diff --git a/administrator/components/com_banners/services/provider.php b/administrator/components/com_banners/services/provider.php index 6f629fedef70b..02bb499b3d240 100644 --- a/administrator/components/com_banners/services/provider.php +++ b/administrator/components/com_banners/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Banners')); - $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Banners')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Banners')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Banners')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Banners')); + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Banners')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Banners')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Banners')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new BannersComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new BannersComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_banners/src/Controller/BannerController.php b/administrator/components/com_banners/src/Controller/BannerController.php index bd3f642f963c9..d424d91e96148 100644 --- a/administrator/components/com_banners/src/Controller/BannerController.php +++ b/administrator/components/com_banners/src/Controller/BannerController.php @@ -1,4 +1,5 @@ input->getInt('filter_category_id'); - $categoryId = ArrayHelper::getValue($data, 'catid', $filter, 'int'); - - if ($categoryId) - { - // If the category has been passed in the URL check it. - return $this->app->getIdentity()->authorise('core.create', $this->option . '.category.' . $categoryId); - } - - // In the absence of better information, revert to the component permissions. - return parent::allowAdd($data); - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - $categoryId = 0; - - if ($recordId) - { - $categoryId = (int) $this->getModel()->getItem($recordId)->catid; - } - - if ($categoryId) - { - // The category has been set. Check the category permissions. - return $this->app->getIdentity()->authorise('core.edit', $this->option . '.category.' . $categoryId); - } - - // Since there is no asset tracking, revert to the component permissions. - return parent::allowEdit($data, $key); - } - - /** - * Method to run batch operations. - * - * @param string $model The model - * - * @return boolean True on success. - * - * @since 2.5 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - $model = $this->getModel('Banner', '', array()); - - // Preset the redirect - $this->setRedirect(Route::_('index.php?option=com_banners&view=banners' . $this->getRedirectToListAppend(), false)); - - return parent::batch($model); - } + use VersionableControllerTrait; + + /** + * The prefix to use with controller messages. + * + * @var string + * @since 1.6 + */ + protected $text_prefix = 'COM_BANNERS_BANNER'; + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowAdd($data = array()) + { + $filter = $this->input->getInt('filter_category_id'); + $categoryId = ArrayHelper::getValue($data, 'catid', $filter, 'int'); + + if ($categoryId) { + // If the category has been passed in the URL check it. + return $this->app->getIdentity()->authorise('core.create', $this->option . '.category.' . $categoryId); + } + + // In the absence of better information, revert to the component permissions. + return parent::allowAdd($data); + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + $categoryId = 0; + + if ($recordId) { + $categoryId = (int) $this->getModel()->getItem($recordId)->catid; + } + + if ($categoryId) { + // The category has been set. Check the category permissions. + return $this->app->getIdentity()->authorise('core.edit', $this->option . '.category.' . $categoryId); + } + + // Since there is no asset tracking, revert to the component permissions. + return parent::allowEdit($data, $key); + } + + /** + * Method to run batch operations. + * + * @param string $model The model + * + * @return boolean True on success. + * + * @since 2.5 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + $model = $this->getModel('Banner', '', array()); + + // Preset the redirect + $this->setRedirect(Route::_('index.php?option=com_banners&view=banners' . $this->getRedirectToListAppend(), false)); + + return parent::batch($model); + } } diff --git a/administrator/components/com_banners/src/Controller/BannersController.php b/administrator/components/com_banners/src/Controller/BannersController.php index 8dcc0df9c05ef..ac71135b75a78 100644 --- a/administrator/components/com_banners/src/Controller/BannersController.php +++ b/administrator/components/com_banners/src/Controller/BannersController.php @@ -1,4 +1,5 @@ registerTask('sticky_unpublish', 'sticky_publish'); - } + $this->registerTask('sticky_unpublish', 'sticky_publish'); + } - /** - * 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 \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. - * - * @since 1.6 - */ - public function getModel($name = 'Banner', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * 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 \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6 + */ + public function getModel($name = 'Banner', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } - /** - * Stick items - * - * @return void - * - * @since 1.6 - */ - public function sticky_publish() - { - // Check for request forgeries. - $this->checkToken(); + /** + * Stick items + * + * @return void + * + * @since 1.6 + */ + public function sticky_publish() + { + // Check for request forgeries. + $this->checkToken(); - $ids = (array) $this->input->get('cid', array(), 'int'); - $values = array('sticky_publish' => 1, 'sticky_unpublish' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($values, $task, 0, 'int'); + $ids = (array) $this->input->get('cid', array(), 'int'); + $values = array('sticky_publish' => 1, 'sticky_unpublish' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($values, $task, 0, 'int'); - // Remove zero values resulting from input filter - $ids = array_filter($ids); + // Remove zero values resulting from input filter + $ids = array_filter($ids); - if (empty($ids)) - { - $this->app->enqueueMessage(Text::_('COM_BANNERS_NO_BANNERS_SELECTED'), 'warning'); - } - else - { - // Get the model. - /** @var \Joomla\Component\Banners\Administrator\Model\BannerModel $model */ - $model = $this->getModel(); + if (empty($ids)) { + $this->app->enqueueMessage(Text::_('COM_BANNERS_NO_BANNERS_SELECTED'), 'warning'); + } else { + // Get the model. + /** @var \Joomla\Component\Banners\Administrator\Model\BannerModel $model */ + $model = $this->getModel(); - // Change the state of the records. - if (!$model->stick($ids, $value)) - { - $this->app->enqueueMessage($model->getError(), 'warning'); - } - else - { - if ($value == 1) - { - $ntext = 'COM_BANNERS_N_BANNERS_STUCK'; - } - else - { - $ntext = 'COM_BANNERS_N_BANNERS_UNSTUCK'; - } + // Change the state of the records. + if (!$model->stick($ids, $value)) { + $this->app->enqueueMessage($model->getError(), 'warning'); + } else { + if ($value == 1) { + $ntext = 'COM_BANNERS_N_BANNERS_STUCK'; + } else { + $ntext = 'COM_BANNERS_N_BANNERS_UNSTUCK'; + } - $this->setMessage(Text::plural($ntext, \count($ids))); - } - } + $this->setMessage(Text::plural($ntext, \count($ids))); + } + } - $this->setRedirect('index.php?option=com_banners&view=banners'); - } + $this->setRedirect('index.php?option=com_banners&view=banners'); + } } diff --git a/administrator/components/com_banners/src/Controller/ClientController.php b/administrator/components/com_banners/src/Controller/ClientController.php index 9b28ec21f5827..b0404380e461a 100644 --- a/administrator/components/com_banners/src/Controller/ClientController.php +++ b/administrator/components/com_banners/src/Controller/ClientController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * 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 \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6 + */ + public function getModel($name = 'Client', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_banners/src/Controller/DisplayController.php b/administrator/components/com_banners/src/Controller/DisplayController.php index 41468a8b71fe4..2c5b25491313c 100644 --- a/administrator/components/com_banners/src/Controller/DisplayController.php +++ b/administrator/components/com_banners/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'banners'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); + $view = $this->input->get('view', 'banners'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); - // Check for edit form. - if ($view == 'banner' && $layout == 'edit' && !$this->checkEditId('com_banners.edit.banner', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } + // Check for edit form. + if ($view == 'banner' && $layout == 'edit' && !$this->checkEditId('com_banners.edit.banner', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_banners&view=banners', false)); + $this->setRedirect(Route::_('index.php?option=com_banners&view=banners', false)); - return false; - } - elseif ($view == 'client' && $layout == 'edit' && !$this->checkEditId('com_banners.edit.client', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } + return false; + } elseif ($view == 'client' && $layout == 'edit' && !$this->checkEditId('com_banners.edit.client', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_banners&view=clients', false)); + $this->setRedirect(Route::_('index.php?option=com_banners&view=clients', false)); - return false; - } + return false; + } - return parent::display(); - } + return parent::display(); + } } diff --git a/administrator/components/com_banners/src/Controller/TracksController.php b/administrator/components/com_banners/src/Controller/TracksController.php index fbca3db5c12b0..1d0df14cde2c0 100644 --- a/administrator/components/com_banners/src/Controller/TracksController.php +++ b/administrator/components/com_banners/src/Controller/TracksController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to remove a record. - * - * @return void - * - * @since 1.6 - */ - public function delete() - { - // Check for request forgeries. - $this->checkToken(); - - // Get the model. - /** @var \Joomla\Component\Banners\Administrator\Model\TracksModel $model */ - $model = $this->getModel(); - - // Load the filter state. - $model->setState('filter.type', $this->app->getUserState($this->context . '.filter.type')); - $model->setState('filter.begin', $this->app->getUserState($this->context . '.filter.begin')); - $model->setState('filter.end', $this->app->getUserState($this->context . '.filter.end')); - $model->setState('filter.category_id', $this->app->getUserState($this->context . '.filter.category_id')); - $model->setState('filter.client_id', $this->app->getUserState($this->context . '.filter.client_id')); - $model->setState('list.limit', 0); - $model->setState('list.start', 0); - - $count = $model->getTotal(); - - // Remove the items. - if (!$model->delete()) - { - $this->app->enqueueMessage($model->getError(), 'warning'); - } - elseif ($count > 0) - { - $this->setMessage(Text::plural('COM_BANNERS_TRACKS_N_ITEMS_DELETED', $count)); - } - else - { - $this->setMessage(Text::_('COM_BANNERS_TRACKS_NO_ITEMS_DELETED')); - } - - $this->setRedirect('index.php?option=com_banners&view=tracks'); - } - - /** - * Display method for the raw track data. - * - * @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()}. - * - * @return static This object to support chaining. - * - * @since 1.5 - * @todo This should be done as a view, not here! - */ - public function display($cachable = false, $urlparams = array()) - { - // Get the document object. - $vName = 'tracks'; - - // Get and render the view. - if ($view = $this->getView($vName, 'raw')) - { - // Check for request forgeries. - $this->checkToken('GET'); - - // Get the model for the view. - /** @var \Joomla\Component\Banners\Administrator\Model\TracksModel $model */ - $model = $this->getModel($vName); - - // Load the filter state. - $app = $this->app; - - $model->setState('filter.type', $app->getUserState($this->context . '.filter.type')); - $model->setState('filter.begin', $app->getUserState($this->context . '.filter.begin')); - $model->setState('filter.end', $app->getUserState($this->context . '.filter.end')); - $model->setState('filter.category_id', $app->getUserState($this->context . '.filter.category_id')); - $model->setState('filter.client_id', $app->getUserState($this->context . '.filter.client_id')); - $model->setState('list.limit', 0); - $model->setState('list.start', 0); - - $form = $this->input->get('jform', array(), 'array'); - - $model->setState('basename', $form['basename']); - $model->setState('compressed', $form['compressed']); - - // Create one year cookies. - $cookieLifeTime = time() + 365 * 86400; - $cookieDomain = $app->get('cookie_domain', ''); - $cookiePath = $app->get('cookie_path', '/'); - $isHttpsForced = $app->isHttpsForced(); - - $this->input->cookie->set( - ApplicationHelper::getHash($this->context . '.basename'), - $form['basename'], - $cookieLifeTime, - $cookiePath, - $cookieDomain, - $isHttpsForced, - true - ); - - $this->input->cookie->set( - ApplicationHelper::getHash($this->context . '.compressed'), - $form['compressed'], - $cookieLifeTime, - $cookiePath, - $cookieDomain, - $isHttpsForced, - true - ); - - // Push the model into the view (as default). - $view->setModel($model, true); - - // Push document object into the view. - $view->document = $this->app->getDocument(); - - $view->display(); - } - - return $this; - } + /** + * The prefix to use with controller messages. + * + * @var string + * @since 1.6 + */ + protected $context = 'com_banners.tracks'; + + /** + * 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 \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6 + */ + public function getModel($name = 'Tracks', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to remove a record. + * + * @return void + * + * @since 1.6 + */ + public function delete() + { + // Check for request forgeries. + $this->checkToken(); + + // Get the model. + /** @var \Joomla\Component\Banners\Administrator\Model\TracksModel $model */ + $model = $this->getModel(); + + // Load the filter state. + $model->setState('filter.type', $this->app->getUserState($this->context . '.filter.type')); + $model->setState('filter.begin', $this->app->getUserState($this->context . '.filter.begin')); + $model->setState('filter.end', $this->app->getUserState($this->context . '.filter.end')); + $model->setState('filter.category_id', $this->app->getUserState($this->context . '.filter.category_id')); + $model->setState('filter.client_id', $this->app->getUserState($this->context . '.filter.client_id')); + $model->setState('list.limit', 0); + $model->setState('list.start', 0); + + $count = $model->getTotal(); + + // Remove the items. + if (!$model->delete()) { + $this->app->enqueueMessage($model->getError(), 'warning'); + } elseif ($count > 0) { + $this->setMessage(Text::plural('COM_BANNERS_TRACKS_N_ITEMS_DELETED', $count)); + } else { + $this->setMessage(Text::_('COM_BANNERS_TRACKS_NO_ITEMS_DELETED')); + } + + $this->setRedirect('index.php?option=com_banners&view=tracks'); + } + + /** + * Display method for the raw track data. + * + * @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()}. + * + * @return static This object to support chaining. + * + * @since 1.5 + * @todo This should be done as a view, not here! + */ + public function display($cachable = false, $urlparams = array()) + { + // Get the document object. + $vName = 'tracks'; + + // Get and render the view. + if ($view = $this->getView($vName, 'raw')) { + // Check for request forgeries. + $this->checkToken('GET'); + + // Get the model for the view. + /** @var \Joomla\Component\Banners\Administrator\Model\TracksModel $model */ + $model = $this->getModel($vName); + + // Load the filter state. + $app = $this->app; + + $model->setState('filter.type', $app->getUserState($this->context . '.filter.type')); + $model->setState('filter.begin', $app->getUserState($this->context . '.filter.begin')); + $model->setState('filter.end', $app->getUserState($this->context . '.filter.end')); + $model->setState('filter.category_id', $app->getUserState($this->context . '.filter.category_id')); + $model->setState('filter.client_id', $app->getUserState($this->context . '.filter.client_id')); + $model->setState('list.limit', 0); + $model->setState('list.start', 0); + + $form = $this->input->get('jform', array(), 'array'); + + $model->setState('basename', $form['basename']); + $model->setState('compressed', $form['compressed']); + + // Create one year cookies. + $cookieLifeTime = time() + 365 * 86400; + $cookieDomain = $app->get('cookie_domain', ''); + $cookiePath = $app->get('cookie_path', '/'); + $isHttpsForced = $app->isHttpsForced(); + + $this->input->cookie->set( + ApplicationHelper::getHash($this->context . '.basename'), + $form['basename'], + $cookieLifeTime, + $cookiePath, + $cookieDomain, + $isHttpsForced, + true + ); + + $this->input->cookie->set( + ApplicationHelper::getHash($this->context . '.compressed'), + $form['compressed'], + $cookieLifeTime, + $cookiePath, + $cookieDomain, + $isHttpsForced, + true + ); + + // Push the model into the view (as default). + $view->setModel($model, true); + + // Push document object into the view. + $view->document = $this->app->getDocument(); + + $view->display(); + } + + return $this; + } } diff --git a/administrator/components/com_banners/src/Extension/BannersComponent.php b/administrator/components/com_banners/src/Extension/BannersComponent.php index 2a52518e079e1..95308a9d9c6bd 100644 --- a/administrator/components/com_banners/src/Extension/BannersComponent.php +++ b/administrator/components/com_banners/src/Extension/BannersComponent.php @@ -1,4 +1,5 @@ setDatabase($container->get(DatabaseInterface::class)); + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $banner = new Banner(); + $banner->setDatabase($container->get(DatabaseInterface::class)); - $this->getRegistry()->register('banner', $banner); - } + $this->getRegistry()->register('banner', $banner); + } - /** - * Returns the table for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getTableNameForSection(string $section = null) - { - return 'banners'; - } + /** + * Returns the table for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getTableNameForSection(string $section = null) + { + return 'banners'; + } } diff --git a/administrator/components/com_banners/src/Field/BannerclientField.php b/administrator/components/com_banners/src/Field/BannerclientField.php index 5c0f33863d2a9..c4832c82cf724 100644 --- a/administrator/components/com_banners/src/Field/BannerclientField.php +++ b/administrator/components/com_banners/src/Field/BannerclientField.php @@ -1,4 +1,5 @@ id . '\').value=\'0\';"'; + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + $onclick = ' onclick="document.getElementById(\'' . $this->id . '\').value=\'0\';"'; - return '
' - . '
'; - } + return '
' + . '
'; + } } diff --git a/administrator/components/com_banners/src/Field/ImpmadeField.php b/administrator/components/com_banners/src/Field/ImpmadeField.php index f385b8531e88a..236b112a3c5b4 100644 --- a/administrator/components/com_banners/src/Field/ImpmadeField.php +++ b/administrator/components/com_banners/src/Field/ImpmadeField.php @@ -1,4 +1,5 @@ id . '\').value=\'0\';"'; + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + $onclick = ' onclick="document.getElementById(\'' . $this->id . '\').value=\'0\';"'; - return '
' - . '
'; - } + return '
' + . '
'; + } } diff --git a/administrator/components/com_banners/src/Field/ImptotalField.php b/administrator/components/com_banners/src/Field/ImptotalField.php index 54550bbaa5749..3dae74ffefe61 100644 --- a/administrator/components/com_banners/src/Field/ImptotalField.php +++ b/administrator/components/com_banners/src/Field/ImptotalField.php @@ -1,4 +1,5 @@ id . '_unlimited\').checked=document.getElementById(\'' . $this->id - . '\').value==\'\';"'; - $onclick = ' onclick="if (document.getElementById(\'' . $this->id . '_unlimited\').checked) document.getElementById(\'' . $this->id - . '\').value=\'\';"'; - $value = empty($this->value) ? '' : $this->value; - $checked = empty($this->value) ? ' checked="checked"' : ''; + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + $class = ' class="form-control validate-numeric text_area"'; + $onchange = ' onchange="document.getElementById(\'' . $this->id . '_unlimited\').checked=document.getElementById(\'' . $this->id + . '\').value==\'\';"'; + $onclick = ' onclick="if (document.getElementById(\'' . $this->id . '_unlimited\').checked) document.getElementById(\'' . $this->id + . '\').value=\'\';"'; + $value = empty($this->value) ? '' : $this->value; + $checked = empty($this->value) ? ' checked="checked"' : ''; - return '' - . '
' - . '
'; - } + return '' + . '
' + . '
'; + } } diff --git a/administrator/components/com_banners/src/Helper/BannersHelper.php b/administrator/components/com_banners/src/Helper/BannersHelper.php index 824318b5beadd..7c2b4f1b8a496 100644 --- a/administrator/components/com_banners/src/Helper/BannersHelper.php +++ b/administrator/components/com_banners/src/Helper/BannersHelper.php @@ -1,4 +1,5 @@ getIdentity(); - - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__banners')) - ->where( - [ - $db->quoteName('reset') . ' <= :date', - $db->quoteName('reset') . ' IS NOT NULL', - ] - ) - ->bind(':date', $date) - ->extendWhere( - 'AND', - [ - $db->quoteName('checked_out') . ' IS NULL', - $db->quoteName('checked_out') . ' = :userId', - ], - 'OR' - ) - ->bind(':userId', $user->id, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $rows = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - foreach ($rows as $row) - { - $purchaseType = $row->purchase_type; - - if ($purchaseType < 0 && $row->cid) - { - /** @var \Joomla\Component\Banners\Administrator\Table\ClientTable $client */ - $client = Table::getInstance('ClientTable', '\\Joomla\\Component\\Banners\\Administrator\\Table\\'); - $client->load($row->cid); - $purchaseType = $client->purchase_type; - } - - if ($purchaseType < 0) - { - $params = ComponentHelper::getParams('com_banners'); - $purchaseType = $params->get('purchase_type'); - } - - switch ($purchaseType) - { - case 1: - $reset = null; - break; - case 2: - $date = Factory::getDate('+1 year ' . date('Y-m-d')); - $reset = $date->toSql(); - break; - case 3: - $date = Factory::getDate('+1 month ' . date('Y-m-d')); - $reset = $date->toSql(); - break; - case 4: - $date = Factory::getDate('+7 day ' . date('Y-m-d')); - $reset = $date->toSql(); - break; - case 5: - $date = Factory::getDate('+1 day ' . date('Y-m-d')); - $reset = $date->toSql(); - break; - } - - // Update the row ordering field. - $query = $db->getQuery(true) - ->update($db->quoteName('#__banners')) - ->set( - [ - $db->quoteName('reset') . ' = :reset', - $db->quoteName('impmade') . ' = 0', - $db->quoteName('clicks') . ' = 0', - ] - ) - ->where($db->quoteName('id') . ' = :id') - ->bind(':reset', $reset, $reset === null ? ParameterType::NULL : ParameterType::STRING) - ->bind(':id', $row->id, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - } - - return true; - } - - /** - * Get client list in text/value format for a select field - * - * @return array - */ - public static function getClientOptions() - { - $options = array(); - - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('id', 'value'), - $db->quoteName('name', 'text'), - ] - ) - ->from($db->quoteName('#__banner_clients', 'a')) - ->where($db->quoteName('a.state') . ' = 1') - ->order($db->quoteName('a.name')); - - // Get the options. - $db->setQuery($query); - - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - array_unshift($options, HTMLHelper::_('select.option', '0', Text::_('COM_BANNERS_NO_CLIENT'))); - - return $options; - } + /** + * Update / reset the banners + * + * @return boolean + * + * @since 1.6 + */ + public static function updateReset() + { + $db = Factory::getDbo(); + $date = Factory::getDate(); + $app = Factory::getApplication(); + $user = $app->getIdentity(); + + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__banners')) + ->where( + [ + $db->quoteName('reset') . ' <= :date', + $db->quoteName('reset') . ' IS NOT NULL', + ] + ) + ->bind(':date', $date) + ->extendWhere( + 'AND', + [ + $db->quoteName('checked_out') . ' IS NULL', + $db->quoteName('checked_out') . ' = :userId', + ], + 'OR' + ) + ->bind(':userId', $user->id, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $rows = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + foreach ($rows as $row) { + $purchaseType = $row->purchase_type; + + if ($purchaseType < 0 && $row->cid) { + /** @var \Joomla\Component\Banners\Administrator\Table\ClientTable $client */ + $client = Table::getInstance('ClientTable', '\\Joomla\\Component\\Banners\\Administrator\\Table\\'); + $client->load($row->cid); + $purchaseType = $client->purchase_type; + } + + if ($purchaseType < 0) { + $params = ComponentHelper::getParams('com_banners'); + $purchaseType = $params->get('purchase_type'); + } + + switch ($purchaseType) { + case 1: + $reset = null; + break; + case 2: + $date = Factory::getDate('+1 year ' . date('Y-m-d')); + $reset = $date->toSql(); + break; + case 3: + $date = Factory::getDate('+1 month ' . date('Y-m-d')); + $reset = $date->toSql(); + break; + case 4: + $date = Factory::getDate('+7 day ' . date('Y-m-d')); + $reset = $date->toSql(); + break; + case 5: + $date = Factory::getDate('+1 day ' . date('Y-m-d')); + $reset = $date->toSql(); + break; + } + + // Update the row ordering field. + $query = $db->getQuery(true) + ->update($db->quoteName('#__banners')) + ->set( + [ + $db->quoteName('reset') . ' = :reset', + $db->quoteName('impmade') . ' = 0', + $db->quoteName('clicks') . ' = 0', + ] + ) + ->where($db->quoteName('id') . ' = :id') + ->bind(':reset', $reset, $reset === null ? ParameterType::NULL : ParameterType::STRING) + ->bind(':id', $row->id, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + } + + return true; + } + + /** + * Get client list in text/value format for a select field + * + * @return array + */ + public static function getClientOptions() + { + $options = array(); + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('id', 'value'), + $db->quoteName('name', 'text'), + ] + ) + ->from($db->quoteName('#__banner_clients', 'a')) + ->where($db->quoteName('a.state') . ' = 1') + ->order($db->quoteName('a.name')); + + // Get the options. + $db->setQuery($query); + + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + array_unshift($options, HTMLHelper::_('select.option', '0', Text::_('COM_BANNERS_NO_CLIENT'))); + + return $options; + } } diff --git a/administrator/components/com_banners/src/Model/BannerModel.php b/administrator/components/com_banners/src/Model/BannerModel.php index 92e34088576cf..e4ee583f465a6 100644 --- a/administrator/components/com_banners/src/Model/BannerModel.php +++ b/administrator/components/com_banners/src/Model/BannerModel.php @@ -1,4 +1,5 @@ 'batchClient', - 'language_id' => 'batchLanguage' - ); - - /** - * Batch client changes for a group of banners. - * - * @param string $value The new value matching a client. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 2.5 - */ - protected function batchClient($value, $pks, $contexts) - { - // Set the variables - $user = Factory::getUser(); - - /** @var \Joomla\Component\Banners\Administrator\Table\BannerTable $table */ - $table = $this->getTable(); - - foreach ($pks as $pk) - { - if (!$user->authorise('core.edit', $contexts[$pk])) - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); - - return false; - } - - $table->reset(); - $table->load($pk); - $table->cid = (int) $value; - - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->state != -2) - { - return false; - } - - if (!empty($record->catid)) - { - return Factory::getUser()->authorise('core.delete', 'com_banners.category.' . (int) $record->catid); - } - - return parent::canDelete($record); - } - - /** - * A method to preprocess generating a new title in order to allow tables with alternative names - * for alias and title to use the batch move and copy methods - * - * @param integer $categoryId The target category id - * @param Table $table The JTable within which move or copy is taking place - * - * @return void - * - * @since 3.8.12 - */ - public function generateTitle($categoryId, $table) - { - // Alter the title & alias - $data = $this->generateNewTitle($categoryId, $table->alias, $table->name); - $table->name = $data['0']; - $table->alias = $data['1']; - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canEditState($record) - { - // Check against the category. - if (!empty($record->catid)) - { - return Factory::getUser()->authorise('core.edit.state', 'com_banners.category.' . (int) $record->catid); - } - - // Default to component settings if category not known. - return parent::canEditState($record); - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. [optional] - * @param boolean $loadData True if the form is to load its own data (default case), false if not. [optional] - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_banners.banner', 'banner', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Modify the form based on access controls. - if (!$this->canEditState((object) $data)) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('publish_up', 'disabled', 'true'); - $form->setFieldAttribute('publish_down', 'disabled', 'true'); - $form->setFieldAttribute('state', 'disabled', 'true'); - $form->setFieldAttribute('sticky', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('publish_up', 'filter', 'unset'); - $form->setFieldAttribute('publish_down', 'filter', 'unset'); - $form->setFieldAttribute('state', 'filter', 'unset'); - $form->setFieldAttribute('sticky', 'filter', 'unset'); - } - - // Don't allow to change the created_by user if not allowed to access com_users. - if (!Factory::getUser()->authorise('core.manage', 'com_users')) - { - $form->setFieldAttribute('created_by', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $app = Factory::getApplication(); - $data = $app->getUserState('com_banners.edit.banner.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Prime some default values. - if ($this->getState('banner.id') == 0) - { - $filters = (array) $app->getUserState('com_banners.banners.filter'); - $filterCatId = $filters['category_id'] ?? null; - - $data->set('catid', $app->input->getInt('catid', $filterCatId)); - } - } - - $this->preprocessData('com_banners.banner', $data); - - return $data; - } - - /** - * Method to stick records. - * - * @param array $pks The ids of the items to publish. - * @param integer $value The value of the published state - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function stick(&$pks, $value = 1) - { - /** @var \Joomla\Component\Banners\Administrator\Table\BannerTable $table */ - $table = $this->getTable(); - $pks = (array) $pks; - - // Access checks. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - if (!$this->canEditState($table)) - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); - } - } - } - - // Attempt to change the state of the records. - if (!$table->stick($pks, $value, Factory::getUser()->id)) - { - $this->setError($table->getError()); - - return false; - } - - return true; - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param Table $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - $db = $this->getDatabase(); - - return [ - $db->quoteName('catid') . ' = ' . (int) $table->catid, - $db->quoteName('state') . ' >= 0', - ]; - } - - /** - * Prepare and sanitise the table prior to saving. - * - * @param Table $table A Table object. - * - * @return void - * - * @since 1.6 - */ - protected function prepareTable($table) - { - $date = Factory::getDate(); - $user = Factory::getUser(); - - if (empty($table->id)) - { - // Set the values - $table->created = $date->toSql(); - $table->created_by = $user->id; - - // Set ordering to the last item if not set - if (empty($table->ordering)) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('MAX(' . $db->quoteName('ordering') . ')') - ->from($db->quoteName('#__banners')); - - $db->setQuery($query); - $max = $db->loadResult(); - - $table->ordering = $max + 1; - } - } - else - { - // Set the values - $table->modified = $date->toSql(); - $table->modified_by = $user->id; - } - - // Increment the content version number. - $table->version++; - } - - /** - * Allows preprocessing of the Form object. - * - * @param Form $form The form object - * @param array $data The data to be merged into the form object - * @param string $group The plugin group to be executed - * - * @return void - * - * @since 3.6.1 - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - if ($this->canCreateCategory()) - { - $form->setFieldAttribute('catid', 'allowAdd', 'true'); - - // Add a prefix for categories created on the fly. - $form->setFieldAttribute('catid', 'customPrefix', '#new#'); - } - - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $input = Factory::getApplication()->input; - - // Create new category, if needed. - $createCategory = true; - - // If category ID is provided, check if it's valid. - if (is_numeric($data['catid']) && $data['catid']) - { - $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_banners'); - } - - // Save New Category - if ($createCategory && $this->canCreateCategory()) - { - $category = [ - // Remove #new# prefix, if exists. - 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], - 'parent_id' => 1, - 'extension' => 'com_banners', - 'language' => $data['language'], - 'published' => 1, - ]; - - /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ - $categoryModel = Factory::getApplication()->bootComponent('com_categories') - ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); - - // Create new category. - if (!$categoryModel->save($category)) - { - $this->setError($categoryModel->getError()); - - return false; - } - - // Get the new category ID. - $data['catid'] = $categoryModel->getState('category.id'); - } - - // Alter the name for save as copy - if ($input->get('task') == 'save2copy') - { - /** @var \Joomla\Component\Banners\Administrator\Table\BannerTable $origTable */ - $origTable = clone $this->getTable(); - $origTable->load($input->getInt('id')); - - if ($data['name'] == $origTable->name) - { - list($name, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['name']); - $data['name'] = $name; - $data['alias'] = $alias; - } - else - { - if ($data['alias'] == $origTable->alias) - { - $data['alias'] = ''; - } - } - - $data['state'] = 0; - } - - return parent::save($data); - } - - /** - * Is the user allowed to create an on the fly category? - * - * @return boolean - * - * @since 3.6.1 - */ - private function canCreateCategory() - { - return Factory::getUser()->authorise('core.create', 'com_banners'); - } + use VersionableModelTrait; + + /** + * The prefix to use with controller messages. + * + * @var string + * @since 1.6 + */ + protected $text_prefix = 'COM_BANNERS_BANNER'; + + /** + * The type alias for this content type. + * + * @var string + * @since 3.2 + */ + public $typeAlias = 'com_banners.banner'; + + /** + * Batch copy/move command. If set to false, the batch copy/move command is not supported + * + * @var string + */ + protected $batch_copymove = 'category_id'; + + /** + * Allowed batch commands + * + * @var array + */ + protected $batch_commands = array( + 'client_id' => 'batchClient', + 'language_id' => 'batchLanguage' + ); + + /** + * Batch client changes for a group of banners. + * + * @param string $value The new value matching a client. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 2.5 + */ + protected function batchClient($value, $pks, $contexts) + { + // Set the variables + $user = Factory::getUser(); + + /** @var \Joomla\Component\Banners\Administrator\Table\BannerTable $table */ + $table = $this->getTable(); + + foreach ($pks as $pk) { + if (!$user->authorise('core.edit', $contexts[$pk])) { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); + + return false; + } + + $table->reset(); + $table->load($pk); + $table->cid = (int) $value; + + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->state != -2) { + return false; + } + + if (!empty($record->catid)) { + return Factory::getUser()->authorise('core.delete', 'com_banners.category.' . (int) $record->catid); + } + + return parent::canDelete($record); + } + + /** + * A method to preprocess generating a new title in order to allow tables with alternative names + * for alias and title to use the batch move and copy methods + * + * @param integer $categoryId The target category id + * @param Table $table The JTable within which move or copy is taking place + * + * @return void + * + * @since 3.8.12 + */ + public function generateTitle($categoryId, $table) + { + // Alter the title & alias + $data = $this->generateNewTitle($categoryId, $table->alias, $table->name); + $table->name = $data['0']; + $table->alias = $data['1']; + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canEditState($record) + { + // Check against the category. + if (!empty($record->catid)) { + return Factory::getUser()->authorise('core.edit.state', 'com_banners.category.' . (int) $record->catid); + } + + // Default to component settings if category not known. + return parent::canEditState($record); + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. [optional] + * @param boolean $loadData True if the form is to load its own data (default case), false if not. [optional] + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_banners.banner', 'banner', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Modify the form based on access controls. + if (!$this->canEditState((object) $data)) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('publish_up', 'disabled', 'true'); + $form->setFieldAttribute('publish_down', 'disabled', 'true'); + $form->setFieldAttribute('state', 'disabled', 'true'); + $form->setFieldAttribute('sticky', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('publish_up', 'filter', 'unset'); + $form->setFieldAttribute('publish_down', 'filter', 'unset'); + $form->setFieldAttribute('state', 'filter', 'unset'); + $form->setFieldAttribute('sticky', 'filter', 'unset'); + } + + // Don't allow to change the created_by user if not allowed to access com_users. + if (!Factory::getUser()->authorise('core.manage', 'com_users')) { + $form->setFieldAttribute('created_by', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $app = Factory::getApplication(); + $data = $app->getUserState('com_banners.edit.banner.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Prime some default values. + if ($this->getState('banner.id') == 0) { + $filters = (array) $app->getUserState('com_banners.banners.filter'); + $filterCatId = $filters['category_id'] ?? null; + + $data->set('catid', $app->input->getInt('catid', $filterCatId)); + } + } + + $this->preprocessData('com_banners.banner', $data); + + return $data; + } + + /** + * Method to stick records. + * + * @param array $pks The ids of the items to publish. + * @param integer $value The value of the published state + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function stick(&$pks, $value = 1) + { + /** @var \Joomla\Component\Banners\Administrator\Table\BannerTable $table */ + $table = $this->getTable(); + $pks = (array) $pks; + + // Access checks. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + if (!$this->canEditState($table)) { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); + } + } + } + + // Attempt to change the state of the records. + if (!$table->stick($pks, $value, Factory::getUser()->id)) { + $this->setError($table->getError()); + + return false; + } + + return true; + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param Table $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('catid') . ' = ' . (int) $table->catid, + $db->quoteName('state') . ' >= 0', + ]; + } + + /** + * Prepare and sanitise the table prior to saving. + * + * @param Table $table A Table object. + * + * @return void + * + * @since 1.6 + */ + protected function prepareTable($table) + { + $date = Factory::getDate(); + $user = Factory::getUser(); + + if (empty($table->id)) { + // Set the values + $table->created = $date->toSql(); + $table->created_by = $user->id; + + // Set ordering to the last item if not set + if (empty($table->ordering)) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('MAX(' . $db->quoteName('ordering') . ')') + ->from($db->quoteName('#__banners')); + + $db->setQuery($query); + $max = $db->loadResult(); + + $table->ordering = $max + 1; + } + } else { + // Set the values + $table->modified = $date->toSql(); + $table->modified_by = $user->id; + } + + // Increment the content version number. + $table->version++; + } + + /** + * Allows preprocessing of the Form object. + * + * @param Form $form The form object + * @param array $data The data to be merged into the form object + * @param string $group The plugin group to be executed + * + * @return void + * + * @since 3.6.1 + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + if ($this->canCreateCategory()) { + $form->setFieldAttribute('catid', 'allowAdd', 'true'); + + // Add a prefix for categories created on the fly. + $form->setFieldAttribute('catid', 'customPrefix', '#new#'); + } + + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $input = Factory::getApplication()->input; + + // Create new category, if needed. + $createCategory = true; + + // If category ID is provided, check if it's valid. + if (is_numeric($data['catid']) && $data['catid']) { + $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_banners'); + } + + // Save New Category + if ($createCategory && $this->canCreateCategory()) { + $category = [ + // Remove #new# prefix, if exists. + 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], + 'parent_id' => 1, + 'extension' => 'com_banners', + 'language' => $data['language'], + 'published' => 1, + ]; + + /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ + $categoryModel = Factory::getApplication()->bootComponent('com_categories') + ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); + + // Create new category. + if (!$categoryModel->save($category)) { + $this->setError($categoryModel->getError()); + + return false; + } + + // Get the new category ID. + $data['catid'] = $categoryModel->getState('category.id'); + } + + // Alter the name for save as copy + if ($input->get('task') == 'save2copy') { + /** @var \Joomla\Component\Banners\Administrator\Table\BannerTable $origTable */ + $origTable = clone $this->getTable(); + $origTable->load($input->getInt('id')); + + if ($data['name'] == $origTable->name) { + list($name, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['name']); + $data['name'] = $name; + $data['alias'] = $alias; + } else { + if ($data['alias'] == $origTable->alias) { + $data['alias'] = ''; + } + } + + $data['state'] = 0; + } + + return parent::save($data); + } + + /** + * Is the user allowed to create an on the fly category? + * + * @return boolean + * + * @since 3.6.1 + */ + private function canCreateCategory() + { + return Factory::getUser()->authorise('core.create', 'com_banners'); + } } diff --git a/administrator/components/com_banners/src/Model/BannersModel.php b/administrator/components/com_banners/src/Model/BannersModel.php index 03a7307861c01..c8def963266d9 100644 --- a/administrator/components/com_banners/src/Model/BannersModel.php +++ b/administrator/components/com_banners/src/Model/BannersModel.php @@ -1,4 +1,5 @@ cache['categoryorders'])) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select( - [ - 'MAX(' . $db->quoteName('ordering') . ') AS ' . $db->quoteName('max'), - $db->quoteName('catid'), - ] - ) - ->from($db->quoteName('#__banners')) - ->group($db->quoteName('catid')); - $db->setQuery($query); - $this->cache['categoryorders'] = $db->loadAssocList('catid', 0); - } + /** + * Method to get the maximum ordering value for each category. + * + * @return array + * + * @since 1.6 + */ + public function &getCategoryOrders() + { + if (!isset($this->cache['categoryorders'])) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + 'MAX(' . $db->quoteName('ordering') . ') AS ' . $db->quoteName('max'), + $db->quoteName('catid'), + ] + ) + ->from($db->quoteName('#__banners')) + ->group($db->quoteName('catid')); + $db->setQuery($query); + $this->cache['categoryorders'] = $db->loadAssocList('catid', 0); + } - return $this->cache['categoryorders']; - } + return $this->cache['categoryorders']; + } - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.name'), - $db->quoteName('a.alias'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.catid'), - $db->quoteName('a.clicks'), - $db->quoteName('a.metakey'), - $db->quoteName('a.sticky'), - $db->quoteName('a.impmade'), - $db->quoteName('a.imptotal'), - $db->quoteName('a.state'), - $db->quoteName('a.ordering'), - $db->quoteName('a.purchase_type'), - $db->quoteName('a.language'), - $db->quoteName('a.publish_up'), - $db->quoteName('a.publish_down'), - ] - ) - ) - ->select( - [ - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image', 'language_image'), - $db->quoteName('uc.name', 'editor'), - $db->quoteName('c.title', 'category_title'), - $db->quoteName('cl.name', 'client_name'), - $db->quoteName('cl.purchase_type', 'client_purchase_type'), - ] - ) - ->from($db->quoteName('#__banners', 'a')) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')) - ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) - ->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('a.cid')); + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.name'), + $db->quoteName('a.alias'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.catid'), + $db->quoteName('a.clicks'), + $db->quoteName('a.metakey'), + $db->quoteName('a.sticky'), + $db->quoteName('a.impmade'), + $db->quoteName('a.imptotal'), + $db->quoteName('a.state'), + $db->quoteName('a.ordering'), + $db->quoteName('a.purchase_type'), + $db->quoteName('a.language'), + $db->quoteName('a.publish_up'), + $db->quoteName('a.publish_down'), + ] + ) + ) + ->select( + [ + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image', 'language_image'), + $db->quoteName('uc.name', 'editor'), + $db->quoteName('c.title', 'category_title'), + $db->quoteName('cl.name', 'client_name'), + $db->quoteName('cl.purchase_type', 'client_purchase_type'), + ] + ) + ->from($db->quoteName('#__banners', 'a')) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')) + ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) + ->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('a.cid')); - // Filter by published state - $published = (string) $this->getState('filter.published'); + // Filter by published state + $published = (string) $this->getState('filter.published'); - if (is_numeric($published)) - { - $published = (int) $published; - $query->where($db->quoteName('a.state') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->where($db->quoteName('a.state') . ' IN (0, 1)'); - } + if (is_numeric($published)) { + $published = (int) $published; + $query->where($db->quoteName('a.state') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->where($db->quoteName('a.state') . ' IN (0, 1)'); + } - // Filter by category. - $categoryId = $this->getState('filter.category_id'); + // Filter by category. + $categoryId = $this->getState('filter.category_id'); - if (is_numeric($categoryId)) - { - $categoryId = (int) $categoryId; - $query->where($db->quoteName('a.catid') . ' = :categoryId') - ->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } + if (is_numeric($categoryId)) { + $categoryId = (int) $categoryId; + $query->where($db->quoteName('a.catid') . ' = :categoryId') + ->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } - // Filter by client. - $clientId = $this->getState('filter.client_id'); + // Filter by client. + $clientId = $this->getState('filter.client_id'); - if (is_numeric($clientId)) - { - $clientId = (int) $clientId; - $query->where($db->quoteName('a.cid') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - } + if (is_numeric($clientId)) { + $clientId = (int) $clientId; + $query->where($db->quoteName('a.cid') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + } - // Filter by search in title - if ($search = $this->getState('filter.search')) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :search') - ->bind(':search', $search, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where('(' . $db->quoteName('a.name') . ' LIKE :search1 OR ' . $db->quoteName('a.alias') . ' LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - } + // Filter by search in title + if ($search = $this->getState('filter.search')) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :search') + ->bind(':search', $search, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where('(' . $db->quoteName('a.name') . ' LIKE :search1 OR ' . $db->quoteName('a.alias') . ' LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } + } - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('a.language') . ' = :language') - ->bind(':language', $language); - } + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('a.language') . ' = :language') + ->bind(':language', $language); + } - // Filter on the level. - if ($level = (int) $this->getState('filter.level')) - { - $query->where($db->quoteName('c.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } + // Filter on the level. + if ($level = (int) $this->getState('filter.level')) { + $query->where($db->quoteName('c.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } - // Add the list ordering clause. - $orderCol = $this->state->get('list.ordering', 'a.name'); - $orderDirn = $this->state->get('list.direction', 'ASC'); + // Add the list ordering clause. + $orderCol = $this->state->get('list.ordering', 'a.name'); + $orderDirn = $this->state->get('list.direction', 'ASC'); - if ($orderCol === 'a.ordering' || $orderCol === 'category_title') - { - $ordering = [ - $db->quoteName('c.title') . ' ' . $db->escape($orderDirn), - $db->quoteName('a.ordering') . ' ' . $db->escape($orderDirn), - ]; - } - else - { - if ($orderCol === 'client_name') - { - $orderCol = 'cl.name'; - } + if ($orderCol === 'a.ordering' || $orderCol === 'category_title') { + $ordering = [ + $db->quoteName('c.title') . ' ' . $db->escape($orderDirn), + $db->quoteName('a.ordering') . ' ' . $db->escape($orderDirn), + ]; + } else { + if ($orderCol === 'client_name') { + $orderCol = 'cl.name'; + } - $ordering = $db->escape($orderCol) . ' ' . $db->escape($orderDirn); - } + $ordering = $db->escape($orderCol) . ' ' . $db->escape($orderDirn); + } - $query->order($ordering); + $query->order($ordering); - return $query; - } + return $query; + } - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.category_id'); - $id .= ':' . $this->getState('filter.client_id'); - $id .= ':' . $this->getState('filter.language'); - $id .= ':' . $this->getState('filter.level'); + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.category_id'); + $id .= ':' . $this->getState('filter.client_id'); + $id .= ':' . $this->getState('filter.language'); + $id .= ':' . $this->getState('filter.level'); - return parent::getStoreId($id); - } + return parent::getStoreId($id); + } - /** - * Returns a reference to the a Table object, always creating it. - * - * @param string $type The table type to instantiate - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 1.6 - */ - public function getTable($type = 'Banner', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } + /** + * Returns a reference to the a Table object, always creating it. + * + * @param string $type The table type to instantiate + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 1.6 + */ + public function getTable($type = 'Banner', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'a.name', $direction = 'asc') - { - // Load the parameters. - $this->setState('params', ComponentHelper::getParams('com_banners')); + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.name', $direction = 'asc') + { + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_banners')); - // List state information. - parent::populateState($ordering, $direction); - } + // List state information. + parent::populateState($ordering, $direction); + } } diff --git a/administrator/components/com_banners/src/Model/ClientModel.php b/administrator/components/com_banners/src/Model/ClientModel.php index bbe474d565744..87178304a3bb4 100644 --- a/administrator/components/com_banners/src/Model/ClientModel.php +++ b/administrator/components/com_banners/src/Model/ClientModel.php @@ -1,4 +1,5 @@ id) || $record->state != -2) - { - return false; - } - - if (!empty($record->catid)) - { - return Factory::getUser()->authorise('core.delete', 'com_banners.category.' . (int) $record->catid); - } - - return parent::canDelete($record); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. - * Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - - if (!empty($record->catid)) - { - return $user->authorise('core.edit.state', 'com_banners.category.' . (int) $record->catid); - } - - return $user->authorise('core.edit.state', 'com_banners'); - } - - /** - * Method to get the record form. - * - * @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 \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_banners.client', 'client', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_banners.edit.client.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_banners.client', $data); - - return $data; - } - - /** - * Prepare and sanitise the table prior to saving. - * - * @param Table $table A Table object. - * - * @return void - * - * @since 1.6 - */ - protected function prepareTable($table) - { - $table->name = htmlspecialchars_decode($table->name, ENT_QUOTES); - } + use VersionableModelTrait; + + /** + * The type alias for this content type. + * + * @var string + * @since 3.2 + */ + public $typeAlias = 'com_banners.client'; + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->state != -2) { + return false; + } + + if (!empty($record->catid)) { + return Factory::getUser()->authorise('core.delete', 'com_banners.category.' . (int) $record->catid); + } + + return parent::canDelete($record); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. + * Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + + if (!empty($record->catid)) { + return $user->authorise('core.edit.state', 'com_banners.category.' . (int) $record->catid); + } + + return $user->authorise('core.edit.state', 'com_banners'); + } + + /** + * Method to get the record form. + * + * @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 \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_banners.client', 'client', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_banners.edit.client.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_banners.client', $data); + + return $data; + } + + /** + * Prepare and sanitise the table prior to saving. + * + * @param Table $table A Table object. + * + * @return void + * + * @since 1.6 + */ + protected function prepareTable($table) + { + $table->name = htmlspecialchars_decode($table->name, ENT_QUOTES); + } } diff --git a/administrator/components/com_banners/src/Model/ClientsModel.php b/administrator/components/com_banners/src/Model/ClientsModel.php index fea2e7b870bf4..89234ba998155 100644 --- a/administrator/components/com_banners/src/Model/ClientsModel.php +++ b/administrator/components/com_banners/src/Model/ClientsModel.php @@ -1,4 +1,5 @@ setState('params', ComponentHelper::getParams('com_banners')); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.purchase_type'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $defaultPurchase = (int) ComponentHelper::getParams('com_banners')->get('purchase_type', 3); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.name'), - $db->quoteName('a.contact'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.state'), - $db->quoteName('a.metakey'), - $db->quoteName('a.purchase_type'), - ] - ) - ) - ->select( - [ - 'COUNT(' . $db->quoteName('b.id') . ') AS ' . $db->quoteName('nbanners'), - $db->quoteName('uc.name', 'editor'), - ] - ); - - $query->from($db->quoteName('#__banner_clients', 'a')); - - // Join over the banners for counting - $query->join('LEFT', $db->quoteName('#__banners', 'b'), $db->quoteName('a.id') . ' = ' . $db->quoteName('b.cid')); - - // Join over the users for the checked out user. - $query->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); - - // Filter by published state - $published = (string) $this->getState('filter.state'); - - if (is_numeric($published)) - { - $published = (int) $published; - $query->where($db->quoteName('a.state') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->where($db->quoteName('a.state') . ' IN (0, 1)'); - } - - $query->group( - [ - $db->quoteName('a.id'), - $db->quoteName('a.name'), - $db->quoteName('a.contact'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.state'), - $db->quoteName('a.metakey'), - $db->quoteName('a.purchase_type'), - $db->quoteName('uc.name'), - ] - ); - - // Filter by search in title - if ($search = trim($this->getState('filter.search', ''))) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :search') - ->bind(':search', $search, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', $search) . '%'; - $query->where($db->quoteName('a.name') . ' LIKE :search') - ->bind(':search', $search); - } - } - - // Filter by purchase type - if ($purchaseType = (int) $this->getState('filter.purchase_type')) - { - if ($defaultPurchase === $purchaseType) - { - $query->where('(' . $db->quoteName('a.purchase_type') . ' = :type OR ' . $db->quoteName('a.purchase_type') . ' = -1)'); - } - else - { - $query->where($db->quoteName('a.purchase_type') . ' = :type'); - } - - $query->bind(':type', $purchaseType, ParameterType::INTEGER); - } - - // Add the list ordering clause. - $query->order( - $db->quoteName($db->escape($this->getState('list.ordering', 'a.name'))) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) - ); - - return $query; - } - - /** - * Overrides the getItems method to attach additional metrics to the list. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 3.6 - */ - public function getItems() - { - // Get a storage key. - $store = $this->getStoreId('getItems'); - - // Try to load the data from internal storage. - if (!empty($this->cache[$store])) - { - return $this->cache[$store]; - } - - // Load the list items. - $items = parent::getItems(); - - // If empty or an error, just return. - if (empty($items)) - { - return array(); - } - - // Getting the following metric by joins is WAY TOO SLOW. - // Faster to do three queries for very large banner trees. - - // Get the clients in the list. - $db = $this->getDatabase(); - $clientIds = array_column($items, 'id'); - - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('cid'), - 'COUNT(' . $db->quoteName('cid') . ') AS ' . $db->quoteName('count_published'), - ] - ) - ->from($db->quoteName('#__banners')) - ->where($db->quoteName('state') . ' = :state') - ->whereIn($db->quoteName('cid'), $clientIds) - ->group($db->quoteName('cid')) - ->bind(':state', $state, ParameterType::INTEGER); - - $db->setQuery($query); - - // Get the published banners count. - try - { - $state = 1; - $countPublished = $db->loadAssocList('cid', 'count_published'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Get the unpublished banners count. - try - { - $state = 0; - $countUnpublished = $db->loadAssocList('cid', 'count_published'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Get the trashed banners count. - try - { - $state = -2; - $countTrashed = $db->loadAssocList('cid', 'count_published'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Get the archived banners count. - try - { - $state = 2; - $countArchived = $db->loadAssocList('cid', 'count_published'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Inject the values back into the array. - foreach ($items as $item) - { - $item->count_published = isset($countPublished[$item->id]) ? $countPublished[$item->id] : 0; - $item->count_unpublished = isset($countUnpublished[$item->id]) ? $countUnpublished[$item->id] : 0; - $item->count_trashed = isset($countTrashed[$item->id]) ? $countTrashed[$item->id] : 0; - $item->count_archived = isset($countArchived[$item->id]) ? $countArchived[$item->id] : 0; - } - - // Add the items to the internal cache. - $this->cache[$store] = $items; - - return $this->cache[$store]; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'contact', 'a.contact', + 'state', 'a.state', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'purchase_type', 'a.purchase_type' + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.name', $direction = 'asc') + { + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_banners')); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.purchase_type'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $defaultPurchase = (int) ComponentHelper::getParams('com_banners')->get('purchase_type', 3); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.name'), + $db->quoteName('a.contact'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.state'), + $db->quoteName('a.metakey'), + $db->quoteName('a.purchase_type'), + ] + ) + ) + ->select( + [ + 'COUNT(' . $db->quoteName('b.id') . ') AS ' . $db->quoteName('nbanners'), + $db->quoteName('uc.name', 'editor'), + ] + ); + + $query->from($db->quoteName('#__banner_clients', 'a')); + + // Join over the banners for counting + $query->join('LEFT', $db->quoteName('#__banners', 'b'), $db->quoteName('a.id') . ' = ' . $db->quoteName('b.cid')); + + // Join over the users for the checked out user. + $query->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); + + // Filter by published state + $published = (string) $this->getState('filter.state'); + + if (is_numeric($published)) { + $published = (int) $published; + $query->where($db->quoteName('a.state') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->where($db->quoteName('a.state') . ' IN (0, 1)'); + } + + $query->group( + [ + $db->quoteName('a.id'), + $db->quoteName('a.name'), + $db->quoteName('a.contact'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.state'), + $db->quoteName('a.metakey'), + $db->quoteName('a.purchase_type'), + $db->quoteName('uc.name'), + ] + ); + + // Filter by search in title + if ($search = trim($this->getState('filter.search', ''))) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :search') + ->bind(':search', $search, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', $search) . '%'; + $query->where($db->quoteName('a.name') . ' LIKE :search') + ->bind(':search', $search); + } + } + + // Filter by purchase type + if ($purchaseType = (int) $this->getState('filter.purchase_type')) { + if ($defaultPurchase === $purchaseType) { + $query->where('(' . $db->quoteName('a.purchase_type') . ' = :type OR ' . $db->quoteName('a.purchase_type') . ' = -1)'); + } else { + $query->where($db->quoteName('a.purchase_type') . ' = :type'); + } + + $query->bind(':type', $purchaseType, ParameterType::INTEGER); + } + + // Add the list ordering clause. + $query->order( + $db->quoteName($db->escape($this->getState('list.ordering', 'a.name'))) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) + ); + + return $query; + } + + /** + * Overrides the getItems method to attach additional metrics to the list. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 3.6 + */ + public function getItems() + { + // Get a storage key. + $store = $this->getStoreId('getItems'); + + // Try to load the data from internal storage. + if (!empty($this->cache[$store])) { + return $this->cache[$store]; + } + + // Load the list items. + $items = parent::getItems(); + + // If empty or an error, just return. + if (empty($items)) { + return array(); + } + + // Getting the following metric by joins is WAY TOO SLOW. + // Faster to do three queries for very large banner trees. + + // Get the clients in the list. + $db = $this->getDatabase(); + $clientIds = array_column($items, 'id'); + + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('cid'), + 'COUNT(' . $db->quoteName('cid') . ') AS ' . $db->quoteName('count_published'), + ] + ) + ->from($db->quoteName('#__banners')) + ->where($db->quoteName('state') . ' = :state') + ->whereIn($db->quoteName('cid'), $clientIds) + ->group($db->quoteName('cid')) + ->bind(':state', $state, ParameterType::INTEGER); + + $db->setQuery($query); + + // Get the published banners count. + try { + $state = 1; + $countPublished = $db->loadAssocList('cid', 'count_published'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Get the unpublished banners count. + try { + $state = 0; + $countUnpublished = $db->loadAssocList('cid', 'count_published'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Get the trashed banners count. + try { + $state = -2; + $countTrashed = $db->loadAssocList('cid', 'count_published'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Get the archived banners count. + try { + $state = 2; + $countArchived = $db->loadAssocList('cid', 'count_published'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Inject the values back into the array. + foreach ($items as $item) { + $item->count_published = isset($countPublished[$item->id]) ? $countPublished[$item->id] : 0; + $item->count_unpublished = isset($countUnpublished[$item->id]) ? $countUnpublished[$item->id] : 0; + $item->count_trashed = isset($countTrashed[$item->id]) ? $countTrashed[$item->id] : 0; + $item->count_archived = isset($countArchived[$item->id]) ? $countArchived[$item->id] : 0; + } + + // Add the items to the internal cache. + $this->cache[$store] = $items; + + return $this->cache[$store]; + } } diff --git a/administrator/components/com_banners/src/Model/DownloadModel.php b/administrator/components/com_banners/src/Model/DownloadModel.php index 5cd2d4ff61a11..c4502ddcad978 100644 --- a/administrator/components/com_banners/src/Model/DownloadModel.php +++ b/administrator/components/com_banners/src/Model/DownloadModel.php @@ -1,4 +1,5 @@ input; + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $input = Factory::getApplication()->input; - $this->setState('basename', $input->cookie->getString(ApplicationHelper::getHash($this->_context . '.basename'), '__SITE__')); - $this->setState('compressed', $input->cookie->getInt(ApplicationHelper::getHash($this->_context . '.compressed'), 1)); - } + $this->setState('basename', $input->cookie->getString(ApplicationHelper::getHash($this->_context . '.basename'), '__SITE__')); + $this->setState('compressed', $input->cookie->getInt(ApplicationHelper::getHash($this->_context . '.compressed'), 1)); + } - /** - * Method to get the record form. - * - * @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 \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_banners.download', 'download', array('control' => 'jform', 'load_data' => $loadData)); + /** + * Method to get the record form. + * + * @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 \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_banners.download', 'download', array('control' => 'jform', 'load_data' => $loadData)); - if (empty($form)) - { - return false; - } + if (empty($form)) { + return false; + } - return $form; - } + return $form; + } - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - $data = (object) array( - 'basename' => $this->getState('basename'), - 'compressed' => $this->getState('compressed'), - ); + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + $data = (object) array( + 'basename' => $this->getState('basename'), + 'compressed' => $this->getState('compressed'), + ); - $this->preprocessData('com_banners.download', $data); + $this->preprocessData('com_banners.download', $data); - return $data; - } + return $data; + } } diff --git a/administrator/components/com_banners/src/Model/TracksModel.php b/administrator/components/com_banners/src/Model/TracksModel.php index b9e56e73e6c42..0e25c6e409016 100644 --- a/administrator/components/com_banners/src/Model/TracksModel.php +++ b/administrator/components/com_banners/src/Model/TracksModel.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Banners\Administrator\Model; -\defined('_JEXEC') or die; +namespace Joomla\Component\Banners\Administrator\Model; use Joomla\Archive\Archive; use Joomla\CMS\Component\ComponentHelper; @@ -27,524 +27,464 @@ */ class TracksModel extends ListModel { - /** - * The base name - * - * @var string - * @since 1.6 - */ - protected $basename; - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * - * @since 1.6 - */ - public function __construct($config = array()) - { - if (empty($config['filter_fields'])) - { - $config['filter_fields'] = array( - 'b.name', 'banner_name', - 'cl.name', 'client_name', 'client_id', - 'c.title', 'category_title', 'category_id', - 'track_type', 'a.track_type', 'type', - 'count', 'a.count', - 'track_date', 'a.track_date', 'end', 'begin', - 'level', 'c.level', - ); - } - - parent::__construct($config); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'b.name', $direction = 'asc') - { - // Load the parameters. - $this->setState('params', ComponentHelper::getParams('com_banners')); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - [ - $db->quoteName('a.track_date'), - $db->quoteName('a.track_type'), - $db->quoteName('a.count'), - $db->quoteName('b.name', 'banner_name'), - $db->quoteName('cl.name', 'client_name'), - $db->quoteName('c.title', 'category_title'), - ] - ); - - // From tracks table. - $query->from($db->quoteName('#__banner_tracks', 'a')); - - // Join with the banners. - $query->join('LEFT', $db->quoteName('#__banners', 'b'), $db->quoteName('b.id') . ' = ' . $db->quoteName('a.banner_id')); - - // Join with the client. - $query->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('b.cid')); - - // Join with the category. - $query->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('b.catid')); - - // Filter by type. - - if ($type = (int) $this->getState('filter.type')) - { - $query->where($db->quoteName('a.track_type') . ' = :type') - ->bind(':type', $type, ParameterType::INTEGER); - } - - // Filter by client. - $clientId = $this->getState('filter.client_id'); - - if (is_numeric($clientId)) - { - $clientId = (int) $clientId; - $query->where($db->quoteName('b.cid') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - } - - // Filter by category. - $categoryId = $this->getState('filter.category_id'); - - if (is_numeric($categoryId)) - { - $categoryId = (int) $categoryId; - $query->where($db->quoteName('b.catid') . ' = :categoryId') - ->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } - - // Filter by begin date. - if ($begin = $this->getState('filter.begin')) - { - $query->where($db->quoteName('a.track_date') . ' >= :begin') - ->bind(':begin', $begin); - } - - // Filter by end date. - if ($end = $this->getState('filter.end')) - { - $query->where($db->quoteName('a.track_date') . ' <= :end') - ->bind(':end', $end); - } - - // Filter on the level. - if ($level = (int) $this->getState('filter.level')) - { - $query->where($db->quoteName('c.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } - - // Filter by search in banner name or client name. - if ($search = $this->getState('filter.search')) - { - $search = '%' . StringHelper::strtolower($search) . '%'; - $query->where('(LOWER(' . $db->quoteName('b.name') . ') LIKE :search1 OR LOWER(' . $db->quoteName('cl.name') . ') LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - - // Add the list ordering clause. - $query->order( - $db->quoteName($db->escape($this->getState('list.ordering', 'b.name'))) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) - ); - - return $query; - } - - /** - * Method to delete rows. - * - * @return boolean Returns true on success, false on failure. - */ - public function delete() - { - $user = Factory::getUser(); - $categoryId = (int) $this->getState('category_id'); - - // Access checks. - if ($categoryId) - { - $allow = $user->authorise('core.delete', 'com_banners.category.' . $categoryId); - } - else - { - $allow = $user->authorise('core.delete', 'com_banners'); - } - - if ($allow) - { - // Delete tracks from this banner - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__banner_tracks')); - - // Filter by type - if ($type = (int) $this->getState('filter.type')) - { - $query->where($db->quoteName('track_type') . ' = :type') - ->bind(':type', $type, ParameterType::INTEGER); - } - - // Filter by begin date - if ($begin = $this->getState('filter.begin')) - { - $query->where($db->quoteName('track_date') . ' >= :begin') - ->bind(':begin', $begin); - } - - // Filter by end date - if ($end = $this->getState('filter.end')) - { - $query->where($db->quoteName('track_date') . ' <= :end') - ->bind(':end', $end); - } - - $subQuery = $db->getQuery(true); - $subQuery->select($db->quoteName('id')) - ->from($db->quoteName('#__banners')); - - // Filter by client - if ($clientId = (int) $this->getState('filter.client_id')) - { - $subQuery->where($db->quoteName('cid') . ' = :clientId'); - $query->bind(':clientId', $clientId, ParameterType::INTEGER); - } - - // Filter by category - if ($categoryId) - { - $subQuery->where($db->quoteName('catid') . ' = :categoryId'); - $query->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } - - $query->where($db->quoteName('banner_id') . ' IN (' . $subQuery . ')'); - - $db->setQuery($query); - $this->setError((string) $query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - else - { - Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); - } - - return true; - } - - /** - * Get file name - * - * @return string The file name - * - * @since 1.6 - */ - public function getBaseName() - { - if (!isset($this->basename)) - { - $basename = str_replace('__SITE__', Factory::getApplication()->get('sitename'), $this->getState('basename')); - $categoryId = $this->getState('filter.category_id'); - - if (is_numeric($categoryId)) - { - if ($categoryId > 0) - { - $basename = str_replace('__CATID__', $categoryId, $basename); - } - else - { - $basename = str_replace('__CATID__', '', $basename); - } - - $categoryName = $this->getCategoryName(); - $basename = str_replace('__CATNAME__', $categoryName, $basename); - } - else - { - $basename = str_replace(array('__CATID__', '__CATNAME__'), '', $basename); - } - - $clientId = $this->getState('filter.client_id'); - - if (is_numeric($clientId)) - { - if ($clientId > 0) - { - $basename = str_replace('__CLIENTID__', $clientId, $basename); - } - else - { - $basename = str_replace('__CLIENTID__', '', $basename); - } - - $clientName = $this->getClientName(); - $basename = str_replace('__CLIENTNAME__', $clientName, $basename); - } - else - { - $basename = str_replace(array('__CLIENTID__', '__CLIENTNAME__'), '', $basename); - } - - $type = $this->getState('filter.type'); - - if ($type > 0) - { - $basename = str_replace('__TYPE__', $type, $basename); - $typeName = Text::_('COM_BANNERS_TYPE' . $type); - $basename = str_replace('__TYPENAME__', $typeName, $basename); - } - else - { - $basename = str_replace(array('__TYPE__', '__TYPENAME__'), '', $basename); - } - - $begin = $this->getState('filter.begin'); - - if (!empty($begin)) - { - $basename = str_replace('__BEGIN__', $begin, $basename); - } - else - { - $basename = str_replace('__BEGIN__', '', $basename); - } - - $end = $this->getState('filter.end'); - - if (!empty($end)) - { - $basename = str_replace('__END__', $end, $basename); - } - else - { - $basename = str_replace('__END__', '', $basename); - } - - $this->basename = $basename; - } - - return $this->basename; - } - - /** - * Get the category name. - * - * @return string The category name - * - * @since 1.6 - */ - protected function getCategoryName() - { - $categoryId = (int) $this->getState('filter.category_id'); - - if ($categoryId) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__categories')) - ->where($db->quoteName('id') . ' = :categoryId') - ->bind(':categoryId', $categoryId, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $name = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return $name; - } - - return Text::_('COM_BANNERS_NOCATEGORYNAME'); - } - - /** - * Get the client name - * - * @return string The client name. - * - * @since 1.6 - */ - protected function getClientName() - { - $clientId = (int) $this->getState('filter.client_id'); - - if ($clientId) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('name')) - ->from($db->quoteName('#__banner_clients')) - ->where($db->quoteName('id') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $name = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return $name; - } - - return Text::_('COM_BANNERS_NOCLIENTNAME'); - } - - /** - * Get the file type. - * - * @return string The file type - * - * @since 1.6 - */ - public function getFileType() - { - return $this->getState('compressed') ? 'zip' : 'csv'; - } - - /** - * Get the mime type. - * - * @return string The mime type. - * - * @since 1.6 - */ - public function getMimeType() - { - return $this->getState('compressed') ? 'application/zip' : 'text/csv'; - } - - /** - * Get the content - * - * @return string The content. - * - * @since 1.6 - */ - public function getContent() - { - if (!isset($this->content)) - { - $this->content = '"' . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_NAME')) . '","' - . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_CLIENT')) . '","' - . str_replace('"', '""', Text::_('JCATEGORY')) . '","' - . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_TYPE')) . '","' - . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_COUNT')) . '","' - . str_replace('"', '""', Text::_('JDATE')) . '"' . "\n"; - - foreach ($this->getItems() as $item) - { - $this->content .= '"' . str_replace('"', '""', $item->banner_name) . '","' - . str_replace('"', '""', $item->client_name) . '","' - . str_replace('"', '""', $item->category_title) . '","' - . str_replace('"', '""', ($item->track_type == 1 ? Text::_('COM_BANNERS_IMPRESSION') : Text::_('COM_BANNERS_CLICK'))) . '","' - . str_replace('"', '""', $item->count) . '","' - . str_replace('"', '""', $item->track_date) . '"' . "\n"; - } - - if ($this->getState('compressed')) - { - $app = Factory::getApplication(); - - $files = array( - 'track' => array( - 'name' => $this->getBaseName() . '.csv', - 'data' => $this->content, - 'time' => time() - ) - ); - $ziproot = $app->get('tmp_path') . '/' . uniqid('banners_tracks_') . '.zip'; - - // Run the packager - $delete = Folder::files($app->get('tmp_path') . '/', uniqid('banners_tracks_'), false, true); - - if (!empty($delete)) - { - if (!File::delete($delete)) - { - // File::delete throws an error - $this->setError(Text::_('COM_BANNERS_ERR_ZIP_DELETE_FAILURE')); - - return false; - } - } - - $archive = new Archive; - - if (!$packager = $archive->getAdapter('zip')) - { - $this->setError(Text::_('COM_BANNERS_ERR_ZIP_ADAPTER_FAILURE')); - - return false; - } - elseif (!$packager->create($ziproot, $files)) - { - $this->setError(Text::_('COM_BANNERS_ERR_ZIP_CREATE_FAILURE')); - - return false; - } - - $this->content = file_get_contents($ziproot); - - // Remove tmp zip file, it's no longer needed. - File::delete($ziproot); - } - } - - return $this->content; - } + /** + * The base name + * + * @var string + * @since 1.6 + */ + protected $basename; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'b.name', 'banner_name', + 'cl.name', 'client_name', 'client_id', + 'c.title', 'category_title', 'category_id', + 'track_type', 'a.track_type', 'type', + 'count', 'a.count', + 'track_date', 'a.track_date', 'end', 'begin', + 'level', 'c.level', + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'b.name', $direction = 'asc') + { + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_banners')); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + [ + $db->quoteName('a.track_date'), + $db->quoteName('a.track_type'), + $db->quoteName('a.count'), + $db->quoteName('b.name', 'banner_name'), + $db->quoteName('cl.name', 'client_name'), + $db->quoteName('c.title', 'category_title'), + ] + ); + + // From tracks table. + $query->from($db->quoteName('#__banner_tracks', 'a')); + + // Join with the banners. + $query->join('LEFT', $db->quoteName('#__banners', 'b'), $db->quoteName('b.id') . ' = ' . $db->quoteName('a.banner_id')); + + // Join with the client. + $query->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('b.cid')); + + // Join with the category. + $query->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('b.catid')); + + // Filter by type. + + if ($type = (int) $this->getState('filter.type')) { + $query->where($db->quoteName('a.track_type') . ' = :type') + ->bind(':type', $type, ParameterType::INTEGER); + } + + // Filter by client. + $clientId = $this->getState('filter.client_id'); + + if (is_numeric($clientId)) { + $clientId = (int) $clientId; + $query->where($db->quoteName('b.cid') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + } + + // Filter by category. + $categoryId = $this->getState('filter.category_id'); + + if (is_numeric($categoryId)) { + $categoryId = (int) $categoryId; + $query->where($db->quoteName('b.catid') . ' = :categoryId') + ->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } + + // Filter by begin date. + if ($begin = $this->getState('filter.begin')) { + $query->where($db->quoteName('a.track_date') . ' >= :begin') + ->bind(':begin', $begin); + } + + // Filter by end date. + if ($end = $this->getState('filter.end')) { + $query->where($db->quoteName('a.track_date') . ' <= :end') + ->bind(':end', $end); + } + + // Filter on the level. + if ($level = (int) $this->getState('filter.level')) { + $query->where($db->quoteName('c.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } + + // Filter by search in banner name or client name. + if ($search = $this->getState('filter.search')) { + $search = '%' . StringHelper::strtolower($search) . '%'; + $query->where('(LOWER(' . $db->quoteName('b.name') . ') LIKE :search1 OR LOWER(' . $db->quoteName('cl.name') . ') LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } + + // Add the list ordering clause. + $query->order( + $db->quoteName($db->escape($this->getState('list.ordering', 'b.name'))) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) + ); + + return $query; + } + + /** + * Method to delete rows. + * + * @return boolean Returns true on success, false on failure. + */ + public function delete() + { + $user = Factory::getUser(); + $categoryId = (int) $this->getState('category_id'); + + // Access checks. + if ($categoryId) { + $allow = $user->authorise('core.delete', 'com_banners.category.' . $categoryId); + } else { + $allow = $user->authorise('core.delete', 'com_banners'); + } + + if ($allow) { + // Delete tracks from this banner + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__banner_tracks')); + + // Filter by type + if ($type = (int) $this->getState('filter.type')) { + $query->where($db->quoteName('track_type') . ' = :type') + ->bind(':type', $type, ParameterType::INTEGER); + } + + // Filter by begin date + if ($begin = $this->getState('filter.begin')) { + $query->where($db->quoteName('track_date') . ' >= :begin') + ->bind(':begin', $begin); + } + + // Filter by end date + if ($end = $this->getState('filter.end')) { + $query->where($db->quoteName('track_date') . ' <= :end') + ->bind(':end', $end); + } + + $subQuery = $db->getQuery(true); + $subQuery->select($db->quoteName('id')) + ->from($db->quoteName('#__banners')); + + // Filter by client + if ($clientId = (int) $this->getState('filter.client_id')) { + $subQuery->where($db->quoteName('cid') . ' = :clientId'); + $query->bind(':clientId', $clientId, ParameterType::INTEGER); + } + + // Filter by category + if ($categoryId) { + $subQuery->where($db->quoteName('catid') . ' = :categoryId'); + $query->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } + + $query->where($db->quoteName('banner_id') . ' IN (' . $subQuery . ')'); + + $db->setQuery($query); + $this->setError((string) $query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } else { + Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); + } + + return true; + } + + /** + * Get file name + * + * @return string The file name + * + * @since 1.6 + */ + public function getBaseName() + { + if (!isset($this->basename)) { + $basename = str_replace('__SITE__', Factory::getApplication()->get('sitename'), $this->getState('basename')); + $categoryId = $this->getState('filter.category_id'); + + if (is_numeric($categoryId)) { + if ($categoryId > 0) { + $basename = str_replace('__CATID__', $categoryId, $basename); + } else { + $basename = str_replace('__CATID__', '', $basename); + } + + $categoryName = $this->getCategoryName(); + $basename = str_replace('__CATNAME__', $categoryName, $basename); + } else { + $basename = str_replace(array('__CATID__', '__CATNAME__'), '', $basename); + } + + $clientId = $this->getState('filter.client_id'); + + if (is_numeric($clientId)) { + if ($clientId > 0) { + $basename = str_replace('__CLIENTID__', $clientId, $basename); + } else { + $basename = str_replace('__CLIENTID__', '', $basename); + } + + $clientName = $this->getClientName(); + $basename = str_replace('__CLIENTNAME__', $clientName, $basename); + } else { + $basename = str_replace(array('__CLIENTID__', '__CLIENTNAME__'), '', $basename); + } + + $type = $this->getState('filter.type'); + + if ($type > 0) { + $basename = str_replace('__TYPE__', $type, $basename); + $typeName = Text::_('COM_BANNERS_TYPE' . $type); + $basename = str_replace('__TYPENAME__', $typeName, $basename); + } else { + $basename = str_replace(array('__TYPE__', '__TYPENAME__'), '', $basename); + } + + $begin = $this->getState('filter.begin'); + + if (!empty($begin)) { + $basename = str_replace('__BEGIN__', $begin, $basename); + } else { + $basename = str_replace('__BEGIN__', '', $basename); + } + + $end = $this->getState('filter.end'); + + if (!empty($end)) { + $basename = str_replace('__END__', $end, $basename); + } else { + $basename = str_replace('__END__', '', $basename); + } + + $this->basename = $basename; + } + + return $this->basename; + } + + /** + * Get the category name. + * + * @return string The category name + * + * @since 1.6 + */ + protected function getCategoryName() + { + $categoryId = (int) $this->getState('filter.category_id'); + + if ($categoryId) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('id') . ' = :categoryId') + ->bind(':categoryId', $categoryId, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $name = $db->loadResult(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return $name; + } + + return Text::_('COM_BANNERS_NOCATEGORYNAME'); + } + + /** + * Get the client name + * + * @return string The client name. + * + * @since 1.6 + */ + protected function getClientName() + { + $clientId = (int) $this->getState('filter.client_id'); + + if ($clientId) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('name')) + ->from($db->quoteName('#__banner_clients')) + ->where($db->quoteName('id') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $name = $db->loadResult(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return $name; + } + + return Text::_('COM_BANNERS_NOCLIENTNAME'); + } + + /** + * Get the file type. + * + * @return string The file type + * + * @since 1.6 + */ + public function getFileType() + { + return $this->getState('compressed') ? 'zip' : 'csv'; + } + + /** + * Get the mime type. + * + * @return string The mime type. + * + * @since 1.6 + */ + public function getMimeType() + { + return $this->getState('compressed') ? 'application/zip' : 'text/csv'; + } + + /** + * Get the content + * + * @return string The content. + * + * @since 1.6 + */ + public function getContent() + { + if (!isset($this->content)) { + $this->content = '"' . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_NAME')) . '","' + . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_CLIENT')) . '","' + . str_replace('"', '""', Text::_('JCATEGORY')) . '","' + . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_TYPE')) . '","' + . str_replace('"', '""', Text::_('COM_BANNERS_HEADING_COUNT')) . '","' + . str_replace('"', '""', Text::_('JDATE')) . '"' . "\n"; + + foreach ($this->getItems() as $item) { + $this->content .= '"' . str_replace('"', '""', $item->banner_name) . '","' + . str_replace('"', '""', $item->client_name) . '","' + . str_replace('"', '""', $item->category_title) . '","' + . str_replace('"', '""', ($item->track_type == 1 ? Text::_('COM_BANNERS_IMPRESSION') : Text::_('COM_BANNERS_CLICK'))) . '","' + . str_replace('"', '""', $item->count) . '","' + . str_replace('"', '""', $item->track_date) . '"' . "\n"; + } + + if ($this->getState('compressed')) { + $app = Factory::getApplication(); + + $files = array( + 'track' => array( + 'name' => $this->getBaseName() . '.csv', + 'data' => $this->content, + 'time' => time() + ) + ); + $ziproot = $app->get('tmp_path') . '/' . uniqid('banners_tracks_') . '.zip'; + + // Run the packager + $delete = Folder::files($app->get('tmp_path') . '/', uniqid('banners_tracks_'), false, true); + + if (!empty($delete)) { + if (!File::delete($delete)) { + // File::delete throws an error + $this->setError(Text::_('COM_BANNERS_ERR_ZIP_DELETE_FAILURE')); + + return false; + } + } + + $archive = new Archive(); + + if (!$packager = $archive->getAdapter('zip')) { + $this->setError(Text::_('COM_BANNERS_ERR_ZIP_ADAPTER_FAILURE')); + + return false; + } elseif (!$packager->create($ziproot, $files)) { + $this->setError(Text::_('COM_BANNERS_ERR_ZIP_CREATE_FAILURE')); + + return false; + } + + $this->content = file_get_contents($ziproot); + + // Remove tmp zip file, it's no longer needed. + File::delete($ziproot); + } + } + + return $this->content; + } } diff --git a/administrator/components/com_banners/src/Service/Html/Banner.php b/administrator/components/com_banners/src/Service/Html/Banner.php index 3573e949be327..2380a599e7faf 100644 --- a/administrator/components/com_banners/src/Service/Html/Banner.php +++ b/administrator/components/com_banners/src/Service/Html/Banner.php @@ -1,4 +1,5 @@ ', - Text::_('COM_BANNERS_BATCH_CLIENT_LABEL'), - '', - '' - ) - ); - } + /** + * Display a batch widget for the client selector. + * + * @return string The necessary HTML for the widget. + * + * @since 2.5 + */ + public function clients() + { + // Create the batch selector to change the client on a selection list. + return implode( + "\n", + array( + '', + '' + ) + ); + } - /** - * Method to get the field options. - * - * @return array The field option objects. - * - * @since 1.6 - */ - public function clientlist() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('id', 'value'), - $db->quoteName('name', 'text'), - ] - ) - ->from($db->quoteName('#__banner_clients')) - ->order($db->quoteName('name')); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 1.6 + */ + public function clientlist() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('id', 'value'), + $db->quoteName('name', 'text'), + ] + ) + ->from($db->quoteName('#__banner_clients')) + ->order($db->quoteName('name')); - // Get the options. - $db->setQuery($query); + // Get the options. + $db->setQuery($query); - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } - return $options; - } + return $options; + } - /** - * Returns a pinned state on a grid - * - * @param integer $value The state value. - * @param integer $i The row index - * @param boolean $enabled An optional setting for access control on the action. - * @param string $checkbox An optional prefix for checkboxes. - * - * @return string The Html code - * - * @see HTMLHelperJGrid::state - * @since 2.5.5 - */ - public function pinned($value, $i, $enabled = true, $checkbox = 'cb') - { - $states = array( - 1 => array( - 'sticky_unpublish', - 'COM_BANNERS_BANNERS_PINNED', - 'COM_BANNERS_BANNERS_HTML_PIN_BANNER', - 'COM_BANNERS_BANNERS_PINNED', - true, - 'publish', - 'publish' - ), - 0 => array( - 'sticky_publish', - 'COM_BANNERS_BANNERS_UNPINNED', - 'COM_BANNERS_BANNERS_HTML_UNPIN_BANNER', - 'COM_BANNERS_BANNERS_UNPINNED', - true, - 'unpublish', - 'unpublish' - ), - ); + /** + * Returns a pinned state on a grid + * + * @param integer $value The state value. + * @param integer $i The row index + * @param boolean $enabled An optional setting for access control on the action. + * @param string $checkbox An optional prefix for checkboxes. + * + * @return string The Html code + * + * @see HTMLHelperJGrid::state + * @since 2.5.5 + */ + public function pinned($value, $i, $enabled = true, $checkbox = 'cb') + { + $states = array( + 1 => array( + 'sticky_unpublish', + 'COM_BANNERS_BANNERS_PINNED', + 'COM_BANNERS_BANNERS_HTML_PIN_BANNER', + 'COM_BANNERS_BANNERS_PINNED', + true, + 'publish', + 'publish' + ), + 0 => array( + 'sticky_publish', + 'COM_BANNERS_BANNERS_UNPINNED', + 'COM_BANNERS_BANNERS_HTML_UNPIN_BANNER', + 'COM_BANNERS_BANNERS_UNPINNED', + true, + 'unpublish', + 'unpublish' + ), + ); - return HTMLHelper::_('jgrid.state', $states, $value, $i, 'banners.', $enabled, true, $checkbox); - } + return HTMLHelper::_('jgrid.state', $states, $value, $i, 'banners.', $enabled, true, $checkbox); + } } diff --git a/administrator/components/com_banners/src/Table/BannerTable.php b/administrator/components/com_banners/src/Table/BannerTable.php index dd23a61538042..24b6600492ed5 100644 --- a/administrator/components/com_banners/src/Table/BannerTable.php +++ b/administrator/components/com_banners/src/Table/BannerTable.php @@ -1,4 +1,5 @@ typeAlias = 'com_banners.banner'; - - parent::__construct('#__banners', 'id', $db); - - $this->created = Factory::getDate()->toSql(); - $this->setColumnAlias('published', 'state'); - } - - /** - * Increase click count - * - * @return void - */ - public function clicks() - { - $id = (int) $this->id; - $query = $this->_db->getQuery(true) - ->update($this->_db->quoteName('#__banners')) - ->set($this->_db->quoteName('clicks') . ' = ' . $this->_db->quoteName('clicks') . ' + 1') - ->where($this->_db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - - $this->_db->setQuery($query); - $this->_db->execute(); - } - - /** - * Overloaded check function - * - * @return boolean - * - * @see Table::check - * @since 1.5 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Set name - $this->name = htmlspecialchars_decode($this->name, ENT_QUOTES); - - // Set alias - if (trim($this->alias) == '') - { - $this->alias = $this->name; - } - - $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); - - if (trim(str_replace('-', '', $this->alias)) == '') - { - $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); - } - - // Check for a valid category. - if (!$this->catid = (int) $this->catid) - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_CATEGORY_REQUIRED')); - - return false; - } - - // Set created date if not set. - if (!(int) $this->created) - { - $this->created = Factory::getDate()->toSql(); - } - - // Set publish_up, publish_down to null if not set - if (!$this->publish_up) - { - $this->publish_up = null; - } - - if (!$this->publish_down) - { - $this->publish_down = null; - } - - // Check the publish down date is not earlier than publish up. - if (!\is_null($this->publish_down) && !\is_null($this->publish_up) && $this->publish_down < $this->publish_up) - { - $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); - - return false; - } - - // Set ordering - if ($this->state < 0) - { - // Set ordering to 0 if state is archived or trashed - $this->ordering = 0; - } - elseif (empty($this->ordering)) - { - // Set ordering to last if ordering was 0 - $this->ordering = self::getNextOrder($this->_db->quoteName('catid') . ' = ' . ((int) $this->catid) . ' AND ' . $this->_db->quoteName('state') . ' >= 0'); - } - - // Set modified to created if not set - if (!$this->modified) - { - $this->modified = $this->created; - } - - // Set modified_by to created_by if not set - if (empty($this->modified_by)) - { - $this->modified_by = $this->created_by; - } - - return true; - } - - /** - * Overloaded bind function - * - * @param mixed $array An associative array or object to bind to the \JTable instance. - * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. - * - * @return boolean True on success - * - * @since 1.5 - */ - public function bind($array, $ignore = array()) - { - if (isset($array['params']) && \is_array($array['params'])) - { - $registry = new Registry($array['params']); - - if ((int) $registry->get('width', 0) < 0) - { - $this->setError(Text::sprintf('JLIB_DATABASE_ERROR_NEGATIVE_NOT_PERMITTED', Text::_('COM_BANNERS_FIELD_WIDTH_LABEL'))); - - return false; - } - - if ((int) $registry->get('height', 0) < 0) - { - $this->setError(Text::sprintf('JLIB_DATABASE_ERROR_NEGATIVE_NOT_PERMITTED', Text::_('COM_BANNERS_FIELD_HEIGHT_LABEL'))); - - return false; - } - - // Converts the width and height to an absolute numeric value: - $width = abs((int) $registry->get('width', 0)); - $height = abs((int) $registry->get('height', 0)); - - // Sets the width and height to an empty string if = 0 - $registry->set('width', $width ?: ''); - $registry->set('height', $height ?: ''); - - $array['params'] = (string) $registry; - } - - if (isset($array['imptotal'])) - { - $array['imptotal'] = abs((int) $array['imptotal']); - } - - return parent::bind($array, $ignore); - } - - /** - * Method to store a row - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return boolean True on success, false on failure. - */ - public function store($updateNulls = true) - { - $db = $this->getDbo(); - - if (empty($this->id)) - { - $purchaseType = $this->purchase_type; - - if ($purchaseType < 0 && $this->cid) - { - $client = new ClientTable($db); - $client->load($this->cid); - $purchaseType = $client->purchase_type; - } - - if ($purchaseType < 0) - { - $purchaseType = ComponentHelper::getParams('com_banners')->get('purchase_type'); - } - - switch ($purchaseType) - { - case 1: - $this->reset = null; - break; - case 2: - $date = Factory::getDate('+1 year ' . date('Y-m-d')); - $this->reset = $date->toSql(); - break; - case 3: - $date = Factory::getDate('+1 month ' . date('Y-m-d')); - $this->reset = $date->toSql(); - break; - case 4: - $date = Factory::getDate('+7 day ' . date('Y-m-d')); - $this->reset = $date->toSql(); - break; - case 5: - $date = Factory::getDate('+1 day ' . date('Y-m-d')); - $this->reset = $date->toSql(); - break; - } - - // Store the row - parent::store($updateNulls); - } - else - { - // Get the old row - /** @var BannerTable $oldrow */ - $oldrow = Table::getInstance('BannerTable', __NAMESPACE__ . '\\', array('dbo' => $db)); - - if (!$oldrow->load($this->id) && $oldrow->getError()) - { - $this->setError($oldrow->getError()); - } - - // Verify that the alias is unique - /** @var BannerTable $table */ - $table = Table::getInstance('BannerTable', __NAMESPACE__ . '\\', array('dbo' => $db)); - - if ($table->load(array('alias' => $this->alias, 'catid' => $this->catid)) && ($table->id != $this->id || $this->id == 0)) - { - $this->setError(Text::_('COM_BANNERS_ERROR_UNIQUE_ALIAS')); - - return false; - } - - // Store the new row - parent::store($updateNulls); - - // Need to reorder ? - if ($oldrow->state >= 0 && ($this->state < 0 || $oldrow->catid != $this->catid)) - { - // Reorder the oldrow - $this->reorder($this->_db->quoteName('catid') . ' = ' . ((int) $oldrow->catid) . ' AND ' . $this->_db->quoteName('state') . ' >= 0'); - } - } - - return \count($this->getErrors()) == 0; - } - - /** - * Method to set the sticky state for a row or list of rows in the database - * table. The method respects checked out rows by other users and will attempt - * to checkin rows that it can after adjustments are made. - * - * @param mixed $pks An optional array of primary key values to update. If not set the instance property value is used. - * @param integer $state The sticky state. eg. [0 = unsticked, 1 = sticked] - * @param integer $userId The user id of the user performing the operation. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function stick($pks = null, $state = 1, $userId = 0) - { - $k = $this->_tbl_key; - - // Sanitize input. - $pks = ArrayHelper::toInteger($pks); - $userId = (int) $userId; - $state = (int) $state; - - // If there are no primary keys set check to see if the instance key is set. - if (empty($pks)) - { - if ($this->$k) - { - $pks = array($this->$k); - } - // Nothing to set publishing state on, return false. - else - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_NO_ROWS_SELECTED')); - - return false; - } - } - - // Get an instance of the table - /** @var BannerTable $table */ - $table = Table::getInstance('BannerTable', __NAMESPACE__ . '\\', array('dbo' => $this->_db)); - - // For all keys - foreach ($pks as $pk) - { - // Load the banner - if (!$table->load($pk)) - { - $this->setError($table->getError()); - } - - // Verify checkout - if (\is_null($table->checked_out) || $table->checked_out == $userId) - { - // Change the state - $table->sticky = $state; - $table->checked_out = null; - $table->checked_out_time = null; - - // Check the row - $table->check(); - - // Store the row - if (!$table->store()) - { - $this->setError($table->getError()); - } - } - } - - return \count($this->getErrors()) == 0; - } - - /** - * Get the type alias for the history table - * - * @return string The alias as described above - * - * @since 4.0.0 - */ - public function getTypeAlias() - { - return $this->typeAlias; - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Constructor + * + * @param DatabaseDriver $db Database connector object + * + * @since 1.5 + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = 'com_banners.banner'; + + parent::__construct('#__banners', 'id', $db); + + $this->created = Factory::getDate()->toSql(); + $this->setColumnAlias('published', 'state'); + } + + /** + * Increase click count + * + * @return void + */ + public function clicks() + { + $id = (int) $this->id; + $query = $this->_db->getQuery(true) + ->update($this->_db->quoteName('#__banners')) + ->set($this->_db->quoteName('clicks') . ' = ' . $this->_db->quoteName('clicks') . ' + 1') + ->where($this->_db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + $this->_db->setQuery($query); + $this->_db->execute(); + } + + /** + * Overloaded check function + * + * @return boolean + * + * @see Table::check + * @since 1.5 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Set name + $this->name = htmlspecialchars_decode($this->name, ENT_QUOTES); + + // Set alias + if (trim($this->alias) == '') { + $this->alias = $this->name; + } + + $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); + + if (trim(str_replace('-', '', $this->alias)) == '') { + $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); + } + + // Check for a valid category. + if (!$this->catid = (int) $this->catid) { + $this->setError(Text::_('JLIB_DATABASE_ERROR_CATEGORY_REQUIRED')); + + return false; + } + + // Set created date if not set. + if (!(int) $this->created) { + $this->created = Factory::getDate()->toSql(); + } + + // Set publish_up, publish_down to null if not set + if (!$this->publish_up) { + $this->publish_up = null; + } + + if (!$this->publish_down) { + $this->publish_down = null; + } + + // Check the publish down date is not earlier than publish up. + if (!\is_null($this->publish_down) && !\is_null($this->publish_up) && $this->publish_down < $this->publish_up) { + $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); + + return false; + } + + // Set ordering + if ($this->state < 0) { + // Set ordering to 0 if state is archived or trashed + $this->ordering = 0; + } elseif (empty($this->ordering)) { + // Set ordering to last if ordering was 0 + $this->ordering = self::getNextOrder($this->_db->quoteName('catid') . ' = ' . ((int) $this->catid) . ' AND ' . $this->_db->quoteName('state') . ' >= 0'); + } + + // Set modified to created if not set + if (!$this->modified) { + $this->modified = $this->created; + } + + // Set modified_by to created_by if not set + if (empty($this->modified_by)) { + $this->modified_by = $this->created_by; + } + + return true; + } + + /** + * Overloaded bind function + * + * @param mixed $array An associative array or object to bind to the \JTable instance. + * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return boolean True on success + * + * @since 1.5 + */ + public function bind($array, $ignore = array()) + { + if (isset($array['params']) && \is_array($array['params'])) { + $registry = new Registry($array['params']); + + if ((int) $registry->get('width', 0) < 0) { + $this->setError(Text::sprintf('JLIB_DATABASE_ERROR_NEGATIVE_NOT_PERMITTED', Text::_('COM_BANNERS_FIELD_WIDTH_LABEL'))); + + return false; + } + + if ((int) $registry->get('height', 0) < 0) { + $this->setError(Text::sprintf('JLIB_DATABASE_ERROR_NEGATIVE_NOT_PERMITTED', Text::_('COM_BANNERS_FIELD_HEIGHT_LABEL'))); + + return false; + } + + // Converts the width and height to an absolute numeric value: + $width = abs((int) $registry->get('width', 0)); + $height = abs((int) $registry->get('height', 0)); + + // Sets the width and height to an empty string if = 0 + $registry->set('width', $width ?: ''); + $registry->set('height', $height ?: ''); + + $array['params'] = (string) $registry; + } + + if (isset($array['imptotal'])) { + $array['imptotal'] = abs((int) $array['imptotal']); + } + + return parent::bind($array, $ignore); + } + + /** + * Method to store a row + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return boolean True on success, false on failure. + */ + public function store($updateNulls = true) + { + $db = $this->getDbo(); + + if (empty($this->id)) { + $purchaseType = $this->purchase_type; + + if ($purchaseType < 0 && $this->cid) { + $client = new ClientTable($db); + $client->load($this->cid); + $purchaseType = $client->purchase_type; + } + + if ($purchaseType < 0) { + $purchaseType = ComponentHelper::getParams('com_banners')->get('purchase_type'); + } + + switch ($purchaseType) { + case 1: + $this->reset = null; + break; + case 2: + $date = Factory::getDate('+1 year ' . date('Y-m-d')); + $this->reset = $date->toSql(); + break; + case 3: + $date = Factory::getDate('+1 month ' . date('Y-m-d')); + $this->reset = $date->toSql(); + break; + case 4: + $date = Factory::getDate('+7 day ' . date('Y-m-d')); + $this->reset = $date->toSql(); + break; + case 5: + $date = Factory::getDate('+1 day ' . date('Y-m-d')); + $this->reset = $date->toSql(); + break; + } + + // Store the row + parent::store($updateNulls); + } else { + // Get the old row + /** @var BannerTable $oldrow */ + $oldrow = Table::getInstance('BannerTable', __NAMESPACE__ . '\\', array('dbo' => $db)); + + if (!$oldrow->load($this->id) && $oldrow->getError()) { + $this->setError($oldrow->getError()); + } + + // Verify that the alias is unique + /** @var BannerTable $table */ + $table = Table::getInstance('BannerTable', __NAMESPACE__ . '\\', array('dbo' => $db)); + + if ($table->load(array('alias' => $this->alias, 'catid' => $this->catid)) && ($table->id != $this->id || $this->id == 0)) { + $this->setError(Text::_('COM_BANNERS_ERROR_UNIQUE_ALIAS')); + + return false; + } + + // Store the new row + parent::store($updateNulls); + + // Need to reorder ? + if ($oldrow->state >= 0 && ($this->state < 0 || $oldrow->catid != $this->catid)) { + // Reorder the oldrow + $this->reorder($this->_db->quoteName('catid') . ' = ' . ((int) $oldrow->catid) . ' AND ' . $this->_db->quoteName('state') . ' >= 0'); + } + } + + return \count($this->getErrors()) == 0; + } + + /** + * Method to set the sticky state for a row or list of rows in the database + * table. The method respects checked out rows by other users and will attempt + * to checkin rows that it can after adjustments are made. + * + * @param mixed $pks An optional array of primary key values to update. If not set the instance property value is used. + * @param integer $state The sticky state. eg. [0 = unsticked, 1 = sticked] + * @param integer $userId The user id of the user performing the operation. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function stick($pks = null, $state = 1, $userId = 0) + { + $k = $this->_tbl_key; + + // Sanitize input. + $pks = ArrayHelper::toInteger($pks); + $userId = (int) $userId; + $state = (int) $state; + + // If there are no primary keys set check to see if the instance key is set. + if (empty($pks)) { + if ($this->$k) { + $pks = array($this->$k); + } else { + // Nothing to set publishing state on, return false. + $this->setError(Text::_('JLIB_DATABASE_ERROR_NO_ROWS_SELECTED')); + + return false; + } + } + + // Get an instance of the table + /** @var BannerTable $table */ + $table = Table::getInstance('BannerTable', __NAMESPACE__ . '\\', array('dbo' => $this->_db)); + + // For all keys + foreach ($pks as $pk) { + // Load the banner + if (!$table->load($pk)) { + $this->setError($table->getError()); + } + + // Verify checkout + if (\is_null($table->checked_out) || $table->checked_out == $userId) { + // Change the state + $table->sticky = $state; + $table->checked_out = null; + $table->checked_out_time = null; + + // Check the row + $table->check(); + + // Store the row + if (!$table->store()) { + $this->setError($table->getError()); + } + } + } + + return \count($this->getErrors()) == 0; + } + + /** + * Get the type alias for the history table + * + * @return string The alias as described above + * + * @since 4.0.0 + */ + public function getTypeAlias() + { + return $this->typeAlias; + } } diff --git a/administrator/components/com_banners/src/Table/ClientTable.php b/administrator/components/com_banners/src/Table/ClientTable.php index 977dc5f656857..d6c74778e8ff6 100644 --- a/administrator/components/com_banners/src/Table/ClientTable.php +++ b/administrator/components/com_banners/src/Table/ClientTable.php @@ -1,4 +1,5 @@ typeAlias = 'com_banners.client'; + /** + * Constructor + * + * @param DatabaseDriver $db Database connector object + * + * @since 1.5 + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = 'com_banners.client'; - $this->setColumnAlias('published', 'state'); + $this->setColumnAlias('published', 'state'); - parent::__construct('#__banner_clients', 'id', $db); - } + parent::__construct('#__banner_clients', 'id', $db); + } - /** - * Get the type alias for the history table - * - * @return string The alias as described above - * - * @since 4.0.0 - */ - public function getTypeAlias() - { - return $this->typeAlias; - } + /** + * Get the type alias for the history table + * + * @return string The alias as described above + * + * @since 4.0.0 + */ + public function getTypeAlias() + { + return $this->typeAlias; + } - /** - * Overloaded check function - * - * @return boolean True if the object is ok - * - * @see Table::check() - * @since 4.0.0 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); + /** + * Overloaded check function + * + * @return boolean True if the object is ok + * + * @see Table::check() + * @since 4.0.0 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); - return false; - } + return false; + } - // Check for valid name - if (trim($this->name) === '') - { - $this->setError(Text::_('COM_BANNERS_WARNING_PROVIDE_VALID_NAME')); + // Check for valid name + if (trim($this->name) === '') { + $this->setError(Text::_('COM_BANNERS_WARNING_PROVIDE_VALID_NAME')); - return false; - } + return false; + } - // Check for valid contact - if (trim($this->contact) === '') - { - $this->setError(Text::_('COM_BANNERS_PROVIDE_VALID_CONTACT')); + // Check for valid contact + if (trim($this->contact) === '') { + $this->setError(Text::_('COM_BANNERS_PROVIDE_VALID_CONTACT')); - return false; - } + return false; + } - return true; - } + return true; + } } diff --git a/administrator/components/com_banners/src/View/Banner/HtmlView.php b/administrator/components/com_banners/src/View/Banner/HtmlView.php index a73f0d9f2415c..a5799dc0d45bb 100644 --- a/administrator/components/com_banners/src/View/Banner/HtmlView.php +++ b/administrator/components/com_banners/src/View/Banner/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->form = $model->getForm(); - $this->item = $model->getItem(); - $this->state = $model->getState(); - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - * @throws Exception - */ - protected function addToolbar(): void - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = $this->getCurrentUser(); - $userId = $user->id; - $isNew = ($this->item->id == 0); - $checkedOut = !(\is_null($this->item->checked_out) || $this->item->checked_out == $userId); - - // Since we don't track these assets at the item level, use the category id. - $canDo = ContentHelper::getActions('com_banners', 'category', $this->item->catid); - - ToolbarHelper::title($isNew ? Text::_('COM_BANNERS_MANAGER_BANNER_NEW') : Text::_('COM_BANNERS_MANAGER_BANNER_EDIT'), 'bookmark banners'); - - $toolbarButtons = []; - - // If not checked out, can save the item. - if (!$checkedOut && ($canDo->get('core.edit') || \count($user->getAuthorisedCategories('com_banners', 'core.create')) > 0)) - { - ToolbarHelper::apply('banner.apply'); - $toolbarButtons[] = ['save', 'banner.save']; - - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'banner.save2new']; - } - } - - // If an existing item, can save to a copy. - if (!$isNew && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'banner.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('banner.cancel'); - } - else - { - ToolbarHelper::cancel('banner.cancel', 'JTOOLBAR_CLOSE'); - - if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) - { - ToolbarHelper::versions('com_banners.banner', $this->item->id); - } - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Banners:_Edit'); - } + /** + * The Form object + * + * @var Form + * @since 1.5 + */ + protected $form; + + /** + * The active item + * + * @var object + * @since 1.5 + */ + protected $item; + + /** + * The model state + * + * @var object + * @since 1.5 + */ + protected $state; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.5 + * + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var BannerModel $model */ + $model = $this->getModel(); + $this->form = $model->getForm(); + $this->item = $model->getItem(); + $this->state = $model->getState(); + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + * @throws Exception + */ + protected function addToolbar(): void + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $userId = $user->id; + $isNew = ($this->item->id == 0); + $checkedOut = !(\is_null($this->item->checked_out) || $this->item->checked_out == $userId); + + // Since we don't track these assets at the item level, use the category id. + $canDo = ContentHelper::getActions('com_banners', 'category', $this->item->catid); + + ToolbarHelper::title($isNew ? Text::_('COM_BANNERS_MANAGER_BANNER_NEW') : Text::_('COM_BANNERS_MANAGER_BANNER_EDIT'), 'bookmark banners'); + + $toolbarButtons = []; + + // If not checked out, can save the item. + if (!$checkedOut && ($canDo->get('core.edit') || \count($user->getAuthorisedCategories('com_banners', 'core.create')) > 0)) { + ToolbarHelper::apply('banner.apply'); + $toolbarButtons[] = ['save', 'banner.save']; + + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'banner.save2new']; + } + } + + // If an existing item, can save to a copy. + if (!$isNew && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'banner.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('banner.cancel'); + } else { + ToolbarHelper::cancel('banner.cancel', 'JTOOLBAR_CLOSE'); + + if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) { + ToolbarHelper::versions('com_banners.banner', $this->item->id); + } + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Banners:_Edit'); + } } diff --git a/administrator/components/com_banners/src/View/Banners/HtmlView.php b/administrator/components/com_banners/src/View/Banners/HtmlView.php index 5019ffd9e592d..c1568c5978852 100644 --- a/administrator/components/com_banners/src/View/Banners/HtmlView.php +++ b/administrator/components/com_banners/src/View/Banners/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->categories = $model->getCategoryOrders(); - $this->items = $model->getItems(); - $this->pagination = $model->getPagination(); - $this->state = $model->getState(); - $this->filterForm = $model->getFilterForm(); - $this->activeFilters = $model->getActiveFilters(); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar(): void - { - $canDo = ContentHelper::getActions('com_banners', 'category', $this->state->get('filter.category_id')); - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_BANNERS_MANAGER_BANNERS'), 'bookmark banners'); - - if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_banners', 'core.create')) > 0) - { - $toolbar->addNew('banner.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || ($this->state->get('filter.published') == -2 && $canDo->get('core.delete')))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state')) - { - if ($this->state->get('filter.published') != 2) - { - $childBar->publish('banners.publish')->listCheck(true); - - $childBar->unpublish('banners.unpublish')->listCheck(true); - } - - if ($this->state->get('filter.published') != -1) - { - if ($this->state->get('filter.published') != 2) - { - $childBar->archive('banners.archive')->listCheck(true); - } - elseif ($this->state->get('filter.published') == 2) - { - $childBar->publish('publish')->task('banners.publish')->listCheck(true); - } - } - - $childBar->checkin('banners.checkin')->listCheck(true); - - if ($this->state->get('filter.published') != -2) - { - $childBar->trash('banners.trash')->listCheck(true); - } - } - - if ($this->state->get('filter.published') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('banners.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - // Add a batch button - if ($user->authorise('core.create', 'com_banners') - && $user->authorise('core.edit', 'com_banners') - && $user->authorise('core.edit.state', 'com_banners')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if ($user->authorise('core.admin', 'com_banners') || $user->authorise('core.options', 'com_banners')) - { - $toolbar->preferences('com_banners'); - } - - $toolbar->help('Banners'); - } + /** + * The search tools form + * + * @var Form + * @since 1.6 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 1.6 + */ + public $activeFilters = []; + + /** + * Category data + * + * @var array + * @since 1.6 + */ + protected $categories = []; + + /** + * An array of items + * + * @var array + * @since 1.6 + */ + protected $items = []; + + /** + * The pagination object + * + * @var Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state + * + * @var Registry + * @since 1.6 + */ + protected $state; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 1.6 + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var BannersModel $model */ + $model = $this->getModel(); + $this->categories = $model->getCategoryOrders(); + $this->items = $model->getItems(); + $this->pagination = $model->getPagination(); + $this->state = $model->getState(); + $this->filterForm = $model->getFilterForm(); + $this->activeFilters = $model->getActiveFilters(); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar(): void + { + $canDo = ContentHelper::getActions('com_banners', 'category', $this->state->get('filter.category_id')); + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_BANNERS_MANAGER_BANNERS'), 'bookmark banners'); + + if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_banners', 'core.create')) > 0) { + $toolbar->addNew('banner.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || ($this->state->get('filter.published') == -2 && $canDo->get('core.delete')))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state')) { + if ($this->state->get('filter.published') != 2) { + $childBar->publish('banners.publish')->listCheck(true); + + $childBar->unpublish('banners.unpublish')->listCheck(true); + } + + if ($this->state->get('filter.published') != -1) { + if ($this->state->get('filter.published') != 2) { + $childBar->archive('banners.archive')->listCheck(true); + } elseif ($this->state->get('filter.published') == 2) { + $childBar->publish('publish')->task('banners.publish')->listCheck(true); + } + } + + $childBar->checkin('banners.checkin')->listCheck(true); + + if ($this->state->get('filter.published') != -2) { + $childBar->trash('banners.trash')->listCheck(true); + } + } + + if ($this->state->get('filter.published') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('banners.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + // Add a batch button + if ( + $user->authorise('core.create', 'com_banners') + && $user->authorise('core.edit', 'com_banners') + && $user->authorise('core.edit.state', 'com_banners') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if ($user->authorise('core.admin', 'com_banners') || $user->authorise('core.options', 'com_banners')) { + $toolbar->preferences('com_banners'); + } + + $toolbar->help('Banners'); + } } diff --git a/administrator/components/com_banners/src/View/Client/HtmlView.php b/administrator/components/com_banners/src/View/Client/HtmlView.php index b7b820267800c..c2670583dd591 100644 --- a/administrator/components/com_banners/src/View/Client/HtmlView.php +++ b/administrator/components/com_banners/src/View/Client/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->form = $model->getForm(); - $this->item = $model->getItem(); - $this->state = $model->getState(); - $this->canDo = ContentHelper::getActions('com_banners'); - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - * - * @throws Exception - */ - protected function addToolbar(): void - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = $this->getCurrentUser(); - $isNew = ($this->item->id == 0); - $checkedOut = !(\is_null($this->item->checked_out) || $this->item->checked_out == $user->id); - $canDo = $this->canDo; - - ToolbarHelper::title( - $isNew ? Text::_('COM_BANNERS_MANAGER_CLIENT_NEW') : Text::_('COM_BANNERS_MANAGER_CLIENT_EDIT'), - 'bookmark banners-clients' - ); - - $toolbarButtons = []; - - // If not checked out, can save the item. - if (!$checkedOut && ($canDo->get('core.edit') || $canDo->get('core.create'))) - { - ToolbarHelper::apply('client.apply'); - $toolbarButtons[] = ['save', 'client.save']; - } - - if (!$checkedOut && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'client.save2new']; - } - - // If an existing item, can save to a copy. - if (!$isNew && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'client.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('client.cancel'); - } - else - { - ToolbarHelper::cancel('client.cancel', 'JTOOLBAR_CLOSE'); - - if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) - { - ToolbarHelper::versions('com_banners.client', $this->item->id); - } - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Banners:_New_or_Edit_Client'); - } + /** + * The Form object + * + * @var Form + * @since 1.5 + */ + protected $form; + + /** + * The active item + * + * @var CMSObject + * @since 1.5 + */ + protected $item; + + /** + * The model state + * + * @var CMSObject + * @since 1.5 + */ + protected $state; + + /** + * Object containing permissions for the item + * + * @var CMSObject + * @since 1.5 + */ + protected $canDo; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.5 + * + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var ClientModel $model */ + $model = $this->getModel(); + $this->form = $model->getForm(); + $this->item = $model->getItem(); + $this->state = $model->getState(); + $this->canDo = ContentHelper::getActions('com_banners'); + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + * + * @throws Exception + */ + protected function addToolbar(): void + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $isNew = ($this->item->id == 0); + $checkedOut = !(\is_null($this->item->checked_out) || $this->item->checked_out == $user->id); + $canDo = $this->canDo; + + ToolbarHelper::title( + $isNew ? Text::_('COM_BANNERS_MANAGER_CLIENT_NEW') : Text::_('COM_BANNERS_MANAGER_CLIENT_EDIT'), + 'bookmark banners-clients' + ); + + $toolbarButtons = []; + + // If not checked out, can save the item. + if (!$checkedOut && ($canDo->get('core.edit') || $canDo->get('core.create'))) { + ToolbarHelper::apply('client.apply'); + $toolbarButtons[] = ['save', 'client.save']; + } + + if (!$checkedOut && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'client.save2new']; + } + + // If an existing item, can save to a copy. + if (!$isNew && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'client.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('client.cancel'); + } else { + ToolbarHelper::cancel('client.cancel', 'JTOOLBAR_CLOSE'); + + if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) { + ToolbarHelper::versions('com_banners.client', $this->item->id); + } + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Banners:_New_or_Edit_Client'); + } } diff --git a/administrator/components/com_banners/src/View/Clients/HtmlView.php b/administrator/components/com_banners/src/View/Clients/HtmlView.php index b18edbf38b771..585816a6af862 100644 --- a/administrator/components/com_banners/src/View/Clients/HtmlView.php +++ b/administrator/components/com_banners/src/View/Clients/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->items = $model->getItems(); - $this->pagination = $model->getPagination(); - $this->state = $model->getState(); - $this->filterForm = $model->getFilterForm(); - $this->activeFilters = $model->getActiveFilters(); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar(): void - { - $canDo = ContentHelper::getActions('com_banners'); - - ToolbarHelper::title(Text::_('COM_BANNERS_MANAGER_CLIENTS'), 'bookmark banners-clients'); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('client.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $canDo->get('core.admin'))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('clients.publish')->listCheck(true); - $childBar->unpublish('clients.unpublish')->listCheck(true); - $childBar->archive('clients.archive')->listCheck(true); - - if ($canDo->get('core.admin')) - { - $childBar->checkin('clients.checkin')->listCheck(true); - } - - if (!$this->state->get('filter.state') == -2) - { - $childBar->trash('clients.trash')->listCheck(true); - } - } - - if (!$this->isEmptyState && $this->state->get('filter.state') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('clients.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences('com_banners'); - } - - $toolbar->help('Banners:_Clients'); - } + /** + * The search tools form + * + * @var Form + * @since 1.6 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 1.6 + */ + public $activeFilters = []; + + /** + * An array of items + * + * @var array + * @since 1.6 + */ + protected $items = []; + + /** + * The pagination object + * + * @var Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + * + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var ClientsModel $model */ + $model = $this->getModel(); + $this->items = $model->getItems(); + $this->pagination = $model->getPagination(); + $this->state = $model->getState(); + $this->filterForm = $model->getFilterForm(); + $this->activeFilters = $model->getActiveFilters(); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar(): void + { + $canDo = ContentHelper::getActions('com_banners'); + + ToolbarHelper::title(Text::_('COM_BANNERS_MANAGER_CLIENTS'), 'bookmark banners-clients'); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('client.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $canDo->get('core.admin'))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('clients.publish')->listCheck(true); + $childBar->unpublish('clients.unpublish')->listCheck(true); + $childBar->archive('clients.archive')->listCheck(true); + + if ($canDo->get('core.admin')) { + $childBar->checkin('clients.checkin')->listCheck(true); + } + + if (!$this->state->get('filter.state') == -2) { + $childBar->trash('clients.trash')->listCheck(true); + } + } + + if (!$this->isEmptyState && $this->state->get('filter.state') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('clients.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences('com_banners'); + } + + $toolbar->help('Banners:_Clients'); + } } diff --git a/administrator/components/com_banners/src/View/Download/HtmlView.php b/administrator/components/com_banners/src/View/Download/HtmlView.php index 6307ee8d06992..1b6ef86d72f74 100644 --- a/administrator/components/com_banners/src/View/Download/HtmlView.php +++ b/administrator/components/com_banners/src/View/Download/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->form = $model->getForm(); + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + * + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var DownloadModel $model */ + $model = $this->getModel(); + $this->form = $model->getForm(); - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } - parent::display($tpl); - } + parent::display($tpl); + } } diff --git a/administrator/components/com_banners/src/View/Tracks/HtmlView.php b/administrator/components/com_banners/src/View/Tracks/HtmlView.php index 5931f49a3dfc5..2fa559ab1495f 100644 --- a/administrator/components/com_banners/src/View/Tracks/HtmlView.php +++ b/administrator/components/com_banners/src/View/Tracks/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->items = $model->getItems(); - $this->pagination = $model->getPagination(); - $this->state = $model->getState(); - $this->filterForm = $model->getFilterForm(); - $this->activeFilters = $model->getActiveFilters(); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar(): void - { - $canDo = ContentHelper::getActions('com_banners', 'category', $this->state->get('filter.category_id')); - - ToolbarHelper::title(Text::_('COM_BANNERS_MANAGER_TRACKS'), 'bookmark banners-tracks'); - - $bar = Toolbar::getInstance('toolbar'); - - if (!$this->isEmptyState) - { - $bar->popupButton() - ->url(Route::_('index.php?option=com_banners&view=download&tmpl=component')) - ->text('JTOOLBAR_EXPORT') - ->selector('downloadModal') - ->icon('icon-download') - ->footer('' - . '' - ); - } - - if (!$this->isEmptyState && $canDo->get('core.delete')) - { - $bar->appendButton('Confirm', 'COM_BANNERS_DELETE_MSG', 'delete', 'COM_BANNERS_TRACKS_DELETE', 'tracks.delete', false); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_banners'); - } - - ToolbarHelper::help('Banners:_Tracks'); - } + /** + * The search tools form + * + * @var Form + * @since 1.6 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 1.6 + */ + public $activeFilters = []; + + /** + * An array of items + * + * @var array + * @since 1.6 + */ + protected $items = []; + + /** + * The pagination object + * + * @var Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var TracksModel $model */ + $model = $this->getModel(); + $this->items = $model->getItems(); + $this->pagination = $model->getPagination(); + $this->state = $model->getState(); + $this->filterForm = $model->getFilterForm(); + $this->activeFilters = $model->getActiveFilters(); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar(): void + { + $canDo = ContentHelper::getActions('com_banners', 'category', $this->state->get('filter.category_id')); + + ToolbarHelper::title(Text::_('COM_BANNERS_MANAGER_TRACKS'), 'bookmark banners-tracks'); + + $bar = Toolbar::getInstance('toolbar'); + + if (!$this->isEmptyState) { + $bar->popupButton() + ->url(Route::_('index.php?option=com_banners&view=download&tmpl=component')) + ->text('JTOOLBAR_EXPORT') + ->selector('downloadModal') + ->icon('icon-download') + ->footer('' + . ''); + } + + if (!$this->isEmptyState && $canDo->get('core.delete')) { + $bar->appendButton('Confirm', 'COM_BANNERS_DELETE_MSG', 'delete', 'COM_BANNERS_TRACKS_DELETE', 'tracks.delete', false); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_banners'); + } + + ToolbarHelper::help('Banners:_Tracks'); + } } diff --git a/administrator/components/com_banners/src/View/Tracks/RawView.php b/administrator/components/com_banners/src/View/Tracks/RawView.php index 17470ab692f79..3b7579f3c10df 100644 --- a/administrator/components/com_banners/src/View/Tracks/RawView.php +++ b/administrator/components/com_banners/src/View/Tracks/RawView.php @@ -1,4 +1,5 @@ getModel(); - $basename = $model->getBaseName(); - $fileType = $model->getFileType(); - $mimeType = $model->getMimeType(); - $content = $model->getContent(); + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + * + * @throws Exception + */ + public function display($tpl = null): void + { + /** @var TracksModel $model */ + $model = $this->getModel(); + $basename = $model->getBaseName(); + $fileType = $model->getFileType(); + $mimeType = $model->getMimeType(); + $content = $model->getContent(); - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } - $this->document->setMimeEncoding($mimeType); + $this->document->setMimeEncoding($mimeType); - /** @var CMSApplication $app */ - $app = Factory::getApplication(); - $app->setHeader( - 'Content-disposition', - 'attachment; filename="' . $basename . '.' . $fileType . '"; creation-date="' . Factory::getDate()->toRFC822() . '"', - true - ); - echo $content; - } + /** @var CMSApplication $app */ + $app = Factory::getApplication(); + $app->setHeader( + 'Content-disposition', + 'attachment; filename="' . $basename . '.' . $fileType . '"; creation-date="' . Factory::getDate()->toRFC822() . '"', + true + ); + echo $content; + } } diff --git a/administrator/components/com_banners/tmpl/banner/edit.php b/administrator/components/com_banners/tmpl/banner/edit.php index 3320b3121114b..d6fe9c69f9a88 100644 --- a/administrator/components/com_banners/tmpl/banner/edit.php +++ b/administrator/components/com_banners/tmpl/banner/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_banners.admin-banner-edit'); + ->useScript('form.validate') + ->useScript('com_banners.admin-banner-edit'); ?> diff --git a/administrator/components/com_banners/tmpl/banners/default.php b/administrator/components/com_banners/tmpl/banners/default.php index 22ec2c5d1310e..1f8a3d6fdd804 100644 --- a/administrator/components/com_banners/tmpl/banners/default.php +++ b/administrator/components/com_banners/tmpl/banners/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $userId = $user->get('id'); @@ -30,173 +31,173 @@ $listDirn = $this->escape($this->state->get('list.direction')); $saveOrder = $listOrder == 'a.ordering'; -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_banners&task=banners.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_banners&task=banners.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
-
-
-
- $this]); - ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items as $i => $item) : - $ordering = ($listOrder == 'ordering'); - $item->cat_link = Route::_('index.php?option=com_categories&extension=com_banners&task=edit&type=other&cid[]=' . $item->catid); - $canCreate = $user->authorise('core.create', 'com_banners.category.' . $item->catid); - $canEdit = $user->authorise('core.edit', 'com_banners.category.' . $item->catid); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_banners.category.' . $item->catid) && $canCheckin; - ?> - - - + + + + + + + + + + + + + +
- , - , - -
- - - - - - - - - - - - - - - - - - - -
- id, false, 'cid', 'cb', $item->name); ?> - - +
+
+ $this]); + ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items as $i => $item) : + $ordering = ($listOrder == 'ordering'); + $item->cat_link = Route::_('index.php?option=com_categories&extension=com_banners&task=edit&type=other&cid[]=' . $item->catid); + $canCreate = $user->authorise('core.create', 'com_banners.category.' . $item->catid); + $canEdit = $user->authorise('core.edit', 'com_banners.category.' . $item->catid); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_banners.category.' . $item->catid) && $canCheckin; + ?> + + + - - - - - - - - - - - - - -
+ , + , + +
+ + + + + + + + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->name); ?> + + - - - - - - - - state, $i, 'banners.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> - -
- checked_out) : ?> - editor, $item->checked_out_time, 'banners.', $canCheckin); ?> - - - - escape($item->name); ?> - - escape($item->name); ?> - -
- escape($item->alias)); ?> -
-
- escape($item->category_title); ?> -
-
-
- sticky, $i, $canChange); ?> - - client_name; ?> - - impmade, $item->imptotal ?: Text::_('COM_BANNERS_UNLIMITED')); ?> - - clicks; ?> - - impmade ? 100 * $item->clicks / $item->impmade : 0); ?> - - - - id; ?> -
+ if (!$canChange) { + $iconClass = ' inactive'; + } elseif (!$saveOrder) { + $iconClass = ' inactive" title="' . Text::_('JORDERINGDISABLED'); + } + ?> + + + + + + +
+ state, $i, 'banners.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> + +
+ checked_out) : ?> + editor, $item->checked_out_time, 'banners.', $canCheckin); ?> + + + + escape($item->name); ?> + + escape($item->name); ?> + +
+ escape($item->alias)); ?> +
+
+ escape($item->category_title); ?> +
+
+
+ sticky, $i, $canChange); ?> + + client_name; ?> + + impmade, $item->imptotal ?: Text::_('COM_BANNERS_UNLIMITED')); ?> + + clicks; ?> - + impmade ? 100 * $item->clicks / $item->impmade : 0); ?> + + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', 'com_banners') - && $user->authorise('core.edit', 'com_banners') - && $user->authorise('core.edit.state', 'com_banners')) : ?> - Text::_('COM_BANNERS_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer') - ], - $this->loadTemplate('batch_body') - ); ?> - - + + authorise('core.create', 'com_banners') + && $user->authorise('core.edit', 'com_banners') + && $user->authorise('core.edit.state', 'com_banners') +) : ?> + Text::_('COM_BANNERS_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer') + ], + $this->loadTemplate('batch_body') + ); ?> + + - - - -
-
-
+ + + + + +
diff --git a/administrator/components/com_banners/tmpl/banners/default_batch_body.php b/administrator/components/com_banners/tmpl/banners/default_batch_body.php index f53c0395e2d66..fbc990db6f439 100644 --- a/administrator/components/com_banners/tmpl/banners/default_batch_body.php +++ b/administrator/components/com_banners/tmpl/banners/default_batch_body.php @@ -1,4 +1,5 @@ -
- -
-
- -
-
- -
-
- -
-
-
-
- = 0) : ?> -
-
- 'com_banners']); ?> -
-
- -
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ = 0) : ?> +
+
+ 'com_banners']); ?> +
+
+ +
diff --git a/administrator/components/com_banners/tmpl/banners/default_batch_footer.php b/administrator/components/com_banners/tmpl/banners/default_batch_footer.php index 723541c31226c..d1282ec1246fe 100644 --- a/administrator/components/com_banners/tmpl/banners/default_batch_footer.php +++ b/administrator/components/com_banners/tmpl/banners/default_batch_footer.php @@ -1,4 +1,5 @@ diff --git a/administrator/components/com_banners/tmpl/banners/emptystate.php b/administrator/components/com_banners/tmpl/banners/emptystate.php index ec83587c839c2..b4b0db976c3a1 100644 --- a/administrator/components/com_banners/tmpl/banners/emptystate.php +++ b/administrator/components/com_banners/tmpl/banners/emptystate.php @@ -1,4 +1,5 @@ 'COM_BANNERS', - 'formURL' => 'index.php?option=com_banners&view=banners', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Banners', - 'icon' => 'icon-bookmark banners', + 'textPrefix' => 'COM_BANNERS', + 'formURL' => 'index.php?option=com_banners&view=banners', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Banners', + 'icon' => 'icon-bookmark banners', ]; $user = Factory::getApplication()->getIdentity(); -if ($user->authorise('core.create', 'com_banners') || count($user->getAuthorisedCategories('com_banners', 'core.create')) > 0) -{ - $displayData['createURL'] = 'index.php?option=com_banners&task=banner.add'; +if ($user->authorise('core.create', 'com_banners') || count($user->getAuthorisedCategories('com_banners', 'core.create')) > 0) { + $displayData['createURL'] = 'index.php?option=com_banners&task=banner.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_banners/tmpl/client/edit.php b/administrator/components/com_banners/tmpl/client/edit.php index 9b8466a530183..bd0b60e087fb0 100644 --- a/administrator/components/com_banners/tmpl/client/edit.php +++ b/administrator/components/com_banners/tmpl/client/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?>
- + -
- 'general', 'recall' => true, 'breakpoint' => 768]); ?> +
+ 'general', 'recall' => true, 'breakpoint' => 768]); ?> - item->id) ? Text::_('COM_BANNERS_NEW_CLIENT') : Text::_('COM_BANNERS_EDIT_CLIENT')); ?> -
-
- form->renderField('contact'); - echo $this->form->renderField('email'); - echo $this->form->renderField('purchase_type'); - echo $this->form->renderField('track_impressions'); - echo $this->form->renderField('track_clicks'); - echo $this->form->renderFieldset('extra'); - ?> -
-
- -
-
- + item->id) ? Text::_('COM_BANNERS_NEW_CLIENT') : Text::_('COM_BANNERS_EDIT_CLIENT')); ?> +
+
+ form->renderField('contact'); + echo $this->form->renderField('email'); + echo $this->form->renderField('purchase_type'); + echo $this->form->renderField('track_impressions'); + echo $this->form->renderField('track_clicks'); + echo $this->form->renderFieldset('extra'); + ?> +
+
+ +
+
+ - -
-
-
- -
- form->renderFieldset('metadata'); ?> -
-
-
-
- + +
+
+
+ +
+ form->renderFieldset('metadata'); ?> +
+
+
+
+ - -
+ +
- - + +
diff --git a/administrator/components/com_banners/tmpl/clients/default.php b/administrator/components/com_banners/tmpl/clients/default.php index 31da558bd75b3..8db12d77ee7f3 100644 --- a/administrator/components/com_banners/tmpl/clients/default.php +++ b/administrator/components/com_banners/tmpl/clients/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $purchaseTypes = [ - '1' => 'UNLIMITED', - '2' => 'YEARLY', - '3' => 'MONTHLY', - '4' => 'WEEKLY', - '5' => 'DAILY', + '1' => 'UNLIMITED', + '2' => 'YEARLY', + '3' => 'MONTHLY', + '4' => 'WEEKLY', + '5' => 'DAILY', ]; $user = Factory::getUser(); $userId = $user->get('id'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); -$params = $this->state->params ?? new CMSObject; +$params = $this->state->params ?? new CMSObject(); ?>
-
-
-
- $this]); - ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - - items as $i => $item) : - $canCreate = $user->authorise('core.create', 'com_banners'); - $canEdit = $user->authorise('core.edit', 'com_banners'); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_banners') && $canCheckin; - ?> - - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - - - - - - - - - -
- id, false, 'cid', 'cb', $item->name); ?> - - state, $i, 'clients.', $canChange); ?> - -
- checked_out) : ?> - editor, $item->checked_out_time, 'clients.', $canCheckin); ?> - - - - escape($item->name); ?> - - escape($item->name); ?> - -
-
- contact; ?> - - - count_published; ?> - - - - - count_unpublished; ?> - - - - - count_archived; ?> - - - - - count_trashed; ?> - - - - purchase_type < 0) : ?> - get('purchase_type')])); ?> - - purchase_type]); ?> - - - id; ?> -
+
+
+
+ $this]); + ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + + items as $i => $item) : + $canCreate = $user->authorise('core.create', 'com_banners'); + $canEdit = $user->authorise('core.edit', 'com_banners'); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_banners') && $canCheckin; + ?> + + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->name); ?> + + state, $i, 'clients.', $canChange); ?> + +
+ checked_out) : ?> + editor, $item->checked_out_time, 'clients.', $canCheckin); ?> + + + + escape($item->name); ?> + + escape($item->name); ?> + +
+
+ contact; ?> + + + count_published; ?> + + + + + count_unpublished; ?> + + + + + count_archived; ?> + + + + + count_trashed; ?> + + + + purchase_type < 0) : ?> + get('purchase_type')])); ?> + + purchase_type]); ?> + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
-
-
+ + + +
+
+
diff --git a/administrator/components/com_banners/tmpl/clients/emptystate.php b/administrator/components/com_banners/tmpl/clients/emptystate.php index 1feedff923aac..c7e79091d9a11 100644 --- a/administrator/components/com_banners/tmpl/clients/emptystate.php +++ b/administrator/components/com_banners/tmpl/clients/emptystate.php @@ -1,4 +1,5 @@ 'COM_BANNERS_CLIENT', - 'formURL' => 'index.php?option=com_banners&view=clients', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Banners:_Clients', - 'icon' => 'icon-bookmark banners', + 'textPrefix' => 'COM_BANNERS_CLIENT', + 'formURL' => 'index.php?option=com_banners&view=clients', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Banners:_Clients', + 'icon' => 'icon-bookmark banners', ]; -if (count(Factory::getApplication()->getIdentity()->getAuthorisedCategories('com_banners', 'core.create')) > 0) -{ - $displayData['createURL'] = 'index.php?option=com_banners&task=client.add'; +if (count(Factory::getApplication()->getIdentity()->getAuthorisedCategories('com_banners', 'core.create')) > 0) { + $displayData['createURL'] = 'index.php?option=com_banners&task=client.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_banners/tmpl/download/default.php b/administrator/components/com_banners/tmpl/download/default.php index dc433005a22b7..dacd8035fa53b 100644 --- a/administrator/components/com_banners/tmpl/download/default.php +++ b/administrator/components/com_banners/tmpl/download/default.php @@ -1,4 +1,5 @@
-
- - form->getFieldset() as $field) : ?> - form->renderField($field->fieldname); ?> - - - -
+
+ + form->getFieldset() as $field) : ?> + form->renderField($field->fieldname); ?> + + + +
diff --git a/administrator/components/com_banners/tmpl/tracks/default.php b/administrator/components/com_banners/tmpl/tracks/default.php index 0e0113ec16c77..04ff5e1f2a0be 100644 --- a/administrator/components/com_banners/tmpl/tracks/default.php +++ b/administrator/components/com_banners/tmpl/tracks/default.php @@ -1,4 +1,5 @@ escape($this->state->get('list.direction')); ?>
-
-
-
- $this]); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - items as $i => $item) : ?> - - - - - - - - - -
- , - , - -
- - - - - - - - - -
- banner_name; ?> -
- escape($item->category_title); ?> -
-
- client_name; ?> - - track_type == 1 ? Text::_('COM_BANNERS_IMPRESSION') : Text::_('COM_BANNERS_CLICK'); ?> - - count; ?> - - track_date, Text::_('DATE_FORMAT_LC5')); ?> -
+
+
+
+ $this]); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + items as $i => $item) : ?> + + + + + + + + + +
+ , + , + +
+ + + + + + + + + +
+ banner_name; ?> +
+ escape($item->category_title); ?> +
+
+ client_name; ?> + + track_type == 1 ? Text::_('COM_BANNERS_IMPRESSION') : Text::_('COM_BANNERS_CLICK'); ?> + + count; ?> + + track_date, Text::_('DATE_FORMAT_LC5')); ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
-
-
+ + + + +
+
+
diff --git a/administrator/components/com_banners/tmpl/tracks/emptystate.php b/administrator/components/com_banners/tmpl/tracks/emptystate.php index 4ad37d7cafcab..0fe1cd2f152d7 100644 --- a/administrator/components/com_banners/tmpl/tracks/emptystate.php +++ b/administrator/components/com_banners/tmpl/tracks/emptystate.php @@ -1,4 +1,5 @@ 'COM_BANNERS_TRACKS', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Banners:_Tracks', - 'icon' => 'icon-bookmark banners', + 'textPrefix' => 'COM_BANNERS_TRACKS', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Banners:_Tracks', + 'icon' => 'icon-bookmark banners', ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_cache/services/provider.php b/administrator/components/com_cache/services/provider.php index 09e3a1e2c42e0..01ca10a33e748 100644 --- a/administrator/components/com_cache/services/provider.php +++ b/administrator/components/com_cache/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Cache')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Cache')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Cache')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Cache')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_cache/src/Controller/DisplayController.php b/administrator/components/com_cache/src/Controller/DisplayController.php index ccbdcc9698348..afc321e25f8f5 100644 --- a/administrator/components/com_cache/src/Controller/DisplayController.php +++ b/administrator/components/com_cache/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ getModel('Cache'); - - $data = $model->getData(); - - $size = 0; - - if (!empty($data)) - { - foreach ($data as $d) - { - $size += $d->size; - } - } - - // Number bytes are returned in format xxx.xx MB - $bytes = HTMLHelper::_('number.bytes', $size, 'MB', 1); - - if (!empty($bytes)) - { - $result['amount'] = $bytes; - $result['sronly'] = Text::sprintf('COM_CACHE_QUICKICON_SRONLY', $bytes); - } - else - { - $result['amount'] = 0; - $result['sronly'] = Text::sprintf('COM_CACHE_QUICKICON_SRONLY_NOCACHE'); - } - - echo new JsonResponse($result); - } - - /** - * Method to delete a list of cache groups. - * - * @return void - */ - public function delete() - { - // Check for request forgeries - $this->checkToken(); - - $cid = (array) $this->input->post->get('cid', array(), 'string'); - - if (empty($cid)) - { - $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), 'warning'); - } - else - { - $result = $this->getModel('cache')->cleanlist($cid); - - if ($result !== array()) - { - $this->app->enqueueMessage(Text::sprintf('COM_CACHE_EXPIRED_ITEMS_DELETE_ERROR', implode(', ', $result)), 'error'); - } - else - { - $this->app->enqueueMessage(Text::_('COM_CACHE_EXPIRED_ITEMS_HAVE_BEEN_DELETED'), 'message'); - } - } - - $this->setRedirect('index.php?option=com_cache'); - } - - /** - * Method to delete all cache groups. - * - * @return void - * - * @since 3.6.0 - */ - public function deleteAll() - { - // Check for request forgeries - $this->checkToken(); - - /** @var \Joomla\Component\Cache\Administrator\Model\CacheModel $model */ - $model = $this->getModel('cache'); - $allCleared = true; - - $mCache = $model->getCache(); - - foreach ($mCache->getAll() as $cache) - { - if ($mCache->clean($cache->group) === false) - { - $this->app->enqueueMessage( - Text::sprintf( - 'COM_CACHE_EXPIRED_ITEMS_DELETE_ERROR', Text::_('JADMINISTRATOR') . ' > ' . $cache->group - ), 'error' - ); - $allCleared = false; - } - } - - if ($allCleared) - { - $this->app->enqueueMessage(Text::_('COM_CACHE_MSG_ALL_CACHE_GROUPS_CLEARED'), 'message'); - } - else - { - $this->app->enqueueMessage(Text::_('COM_CACHE_MSG_SOME_CACHE_GROUPS_CLEARED'), 'warning'); - } - - $this->app->triggerEvent('onAfterPurge', array()); - $this->setRedirect('index.php?option=com_cache&view=cache'); - } - - /** - * Purge the cache. - * - * @return void - */ - public function purge() - { - // Check for request forgeries - $this->checkToken(); - - if (!$this->getModel('cache')->purge()) - { - $this->app->enqueueMessage(Text::_('COM_CACHE_EXPIRED_ITEMS_PURGING_ERROR'), 'error'); - } - else - { - $this->app->enqueueMessage(Text::_('COM_CACHE_EXPIRED_ITEMS_HAVE_BEEN_PURGED'), 'message'); - } - - $this->setRedirect('index.php?option=com_cache&view=cache'); - } + /** + * The default view for the display method. + * + * @var string + * @since 4.0.0 + */ + protected $default_view = 'cache'; + + /** + * Method to get The Cache Size + * + * @since 4.0.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('Cache'); + + $data = $model->getData(); + + $size = 0; + + if (!empty($data)) { + foreach ($data as $d) { + $size += $d->size; + } + } + + // Number bytes are returned in format xxx.xx MB + $bytes = HTMLHelper::_('number.bytes', $size, 'MB', 1); + + if (!empty($bytes)) { + $result['amount'] = $bytes; + $result['sronly'] = Text::sprintf('COM_CACHE_QUICKICON_SRONLY', $bytes); + } else { + $result['amount'] = 0; + $result['sronly'] = Text::sprintf('COM_CACHE_QUICKICON_SRONLY_NOCACHE'); + } + + echo new JsonResponse($result); + } + + /** + * Method to delete a list of cache groups. + * + * @return void + */ + public function delete() + { + // Check for request forgeries + $this->checkToken(); + + $cid = (array) $this->input->post->get('cid', array(), 'string'); + + if (empty($cid)) { + $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), 'warning'); + } else { + $result = $this->getModel('cache')->cleanlist($cid); + + if ($result !== array()) { + $this->app->enqueueMessage(Text::sprintf('COM_CACHE_EXPIRED_ITEMS_DELETE_ERROR', implode(', ', $result)), 'error'); + } else { + $this->app->enqueueMessage(Text::_('COM_CACHE_EXPIRED_ITEMS_HAVE_BEEN_DELETED'), 'message'); + } + } + + $this->setRedirect('index.php?option=com_cache'); + } + + /** + * Method to delete all cache groups. + * + * @return void + * + * @since 3.6.0 + */ + public function deleteAll() + { + // Check for request forgeries + $this->checkToken(); + + /** @var \Joomla\Component\Cache\Administrator\Model\CacheModel $model */ + $model = $this->getModel('cache'); + $allCleared = true; + + $mCache = $model->getCache(); + + foreach ($mCache->getAll() as $cache) { + if ($mCache->clean($cache->group) === false) { + $this->app->enqueueMessage( + Text::sprintf( + 'COM_CACHE_EXPIRED_ITEMS_DELETE_ERROR', + Text::_('JADMINISTRATOR') . ' > ' . $cache->group + ), + 'error' + ); + $allCleared = false; + } + } + + if ($allCleared) { + $this->app->enqueueMessage(Text::_('COM_CACHE_MSG_ALL_CACHE_GROUPS_CLEARED'), 'message'); + } else { + $this->app->enqueueMessage(Text::_('COM_CACHE_MSG_SOME_CACHE_GROUPS_CLEARED'), 'warning'); + } + + $this->app->triggerEvent('onAfterPurge', array()); + $this->setRedirect('index.php?option=com_cache&view=cache'); + } + + /** + * Purge the cache. + * + * @return void + */ + public function purge() + { + // Check for request forgeries + $this->checkToken(); + + if (!$this->getModel('cache')->purge()) { + $this->app->enqueueMessage(Text::_('COM_CACHE_EXPIRED_ITEMS_PURGING_ERROR'), 'error'); + } else { + $this->app->enqueueMessage(Text::_('COM_CACHE_EXPIRED_ITEMS_HAVE_BEEN_PURGED'), 'message'); + } + + $this->setRedirect('index.php?option=com_cache&view=cache'); + } } diff --git a/administrator/components/com_cache/src/Model/CacheModel.php b/administrator/components/com_cache/src/Model/CacheModel.php index cdbcebf587dec..a6113be391f79 100644 --- a/administrator/components/com_cache/src/Model/CacheModel.php +++ b/administrator/components/com_cache/src/Model/CacheModel.php @@ -1,4 +1,5 @@ setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 3.5 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - - return parent::getStoreId($id); - } - - /** - * Method to get cache data - * - * @return array - */ - public function getData() - { - if (empty($this->_data)) - { - try - { - $cache = $this->getCache(); - $data = $cache->getAll(); - - if ($data && \count($data) > 0) - { - // Process filter by search term. - if ($search = $this->getState('filter.search')) - { - foreach ($data as $key => $cacheItem) - { - if (stripos($cacheItem->group, $search) === false) - { - unset($data[$key]); - } - } - } - - // Process ordering. - $listOrder = $this->getState('list.ordering', 'group'); - $listDirn = $this->getState('list.direction', 'ASC'); - - $this->_data = ArrayHelper::sortObjects($data, $listOrder, strtolower($listDirn) === 'desc' ? -1 : 1, true, true); - - // Process pagination. - $limit = (int) $this->getState('list.limit', 25); - - if ($limit !== 0) - { - $start = (int) $this->getState('list.start', 0); - - return \array_slice($this->_data, $start, $limit); - } - } - else - { - $this->_data = array(); - } - } - catch (CacheConnectingException $exception) - { - $this->setError(Text::_('COM_CACHE_ERROR_CACHE_CONNECTION_FAILED')); - $this->_data = array(); - } - catch (UnsupportedCacheException $exception) - { - $this->setError(Text::_('COM_CACHE_ERROR_CACHE_DRIVER_UNSUPPORTED')); - $this->_data = array(); - } - } - - return $this->_data; - } - - /** - * Method to get cache instance. - * - * @return CacheController - */ - public function getCache() - { - $app = Factory::getApplication(); - - $options = array( - 'defaultgroup' => '', - 'storage' => $app->get('cache_handler', ''), - 'caching' => true, - 'cachebase' => $app->get('cache_path', JPATH_CACHE) - ); - - return Cache::getInstance('', $options); - } - - /** - * Get the number of current Cache Groups. - * - * @return integer - */ - public function getTotal() - { - if (empty($this->_total)) - { - $this->_total = count($this->getData()); - } - - return $this->_total; - } - - /** - * Method to get a pagination object for the cache. - * - * @return Pagination - */ - public function getPagination() - { - if (empty($this->_pagination)) - { - $this->_pagination = new Pagination($this->getTotal(), $this->getState('list.start'), $this->getState('list.limit')); - } - - return $this->_pagination; - } - - /** - * Clean out a cache group as named by param. - * If no param is passed clean all cache groups. - * - * @param string $group Cache group name. - * - * @return boolean True on success, false otherwise - */ - public function clean($group = '') - { - try - { - $this->getCache()->clean($group); - } - catch (CacheConnectingException $exception) - { - return false; - } - catch (UnsupportedCacheException $exception) - { - return false; - } - - Factory::getApplication()->triggerEvent('onAfterPurge', array($group)); - - return true; - } - - /** - * Purge an array of cache groups. - * - * @param array $array Array of cache group names. - * - * @return array Array with errors, if they exist. - */ - public function cleanlist($array) - { - $errors = array(); - - foreach ($array as $group) - { - if (!$this->clean($group)) - { - $errors[] = $group; - } - } - - return $errors; - } - - /** - * Purge all cache items. - * - * @return boolean True if successful; false otherwise. - */ - public function purge() - { - try - { - Factory::getCache('')->gc(); - } - catch (CacheConnectingException $exception) - { - return false; - } - catch (UnsupportedCacheException $exception) - { - return false; - } - - Factory::getApplication()->triggerEvent('onAfterPurge', array()); - - return true; - } + /** + * An Array of CacheItems indexed by cache group ID + * + * @var array + */ + protected $_data = array(); + + /** + * Group total + * + * @var integer + */ + protected $_total = null; + + /** + * Pagination object + * + * @var object + */ + protected $_pagination = null; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 3.5 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'group', + 'count', + 'size', + 'client_id', + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering Field for ordering. + * @param string $direction Direction of ordering. + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'group', $direction = 'asc') + { + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 3.5 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + + return parent::getStoreId($id); + } + + /** + * Method to get cache data + * + * @return array + */ + public function getData() + { + if (empty($this->_data)) { + try { + $cache = $this->getCache(); + $data = $cache->getAll(); + + if ($data && \count($data) > 0) { + // Process filter by search term. + if ($search = $this->getState('filter.search')) { + foreach ($data as $key => $cacheItem) { + if (stripos($cacheItem->group, $search) === false) { + unset($data[$key]); + } + } + } + + // Process ordering. + $listOrder = $this->getState('list.ordering', 'group'); + $listDirn = $this->getState('list.direction', 'ASC'); + + $this->_data = ArrayHelper::sortObjects($data, $listOrder, strtolower($listDirn) === 'desc' ? -1 : 1, true, true); + + // Process pagination. + $limit = (int) $this->getState('list.limit', 25); + + if ($limit !== 0) { + $start = (int) $this->getState('list.start', 0); + + return \array_slice($this->_data, $start, $limit); + } + } else { + $this->_data = array(); + } + } catch (CacheConnectingException $exception) { + $this->setError(Text::_('COM_CACHE_ERROR_CACHE_CONNECTION_FAILED')); + $this->_data = array(); + } catch (UnsupportedCacheException $exception) { + $this->setError(Text::_('COM_CACHE_ERROR_CACHE_DRIVER_UNSUPPORTED')); + $this->_data = array(); + } + } + + return $this->_data; + } + + /** + * Method to get cache instance. + * + * @return CacheController + */ + public function getCache() + { + $app = Factory::getApplication(); + + $options = array( + 'defaultgroup' => '', + 'storage' => $app->get('cache_handler', ''), + 'caching' => true, + 'cachebase' => $app->get('cache_path', JPATH_CACHE) + ); + + return Cache::getInstance('', $options); + } + + /** + * Get the number of current Cache Groups. + * + * @return integer + */ + public function getTotal() + { + if (empty($this->_total)) { + $this->_total = count($this->getData()); + } + + return $this->_total; + } + + /** + * Method to get a pagination object for the cache. + * + * @return Pagination + */ + public function getPagination() + { + if (empty($this->_pagination)) { + $this->_pagination = new Pagination($this->getTotal(), $this->getState('list.start'), $this->getState('list.limit')); + } + + return $this->_pagination; + } + + /** + * Clean out a cache group as named by param. + * If no param is passed clean all cache groups. + * + * @param string $group Cache group name. + * + * @return boolean True on success, false otherwise + */ + public function clean($group = '') + { + try { + $this->getCache()->clean($group); + } catch (CacheConnectingException $exception) { + return false; + } catch (UnsupportedCacheException $exception) { + return false; + } + + Factory::getApplication()->triggerEvent('onAfterPurge', array($group)); + + return true; + } + + /** + * Purge an array of cache groups. + * + * @param array $array Array of cache group names. + * + * @return array Array with errors, if they exist. + */ + public function cleanlist($array) + { + $errors = array(); + + foreach ($array as $group) { + if (!$this->clean($group)) { + $errors[] = $group; + } + } + + return $errors; + } + + /** + * Purge all cache items. + * + * @return boolean True if successful; false otherwise. + */ + public function purge() + { + try { + Factory::getCache('')->gc(); + } catch (CacheConnectingException $exception) { + return false; + } catch (UnsupportedCacheException $exception) { + return false; + } + + Factory::getApplication()->triggerEvent('onAfterPurge', array()); + + return true; + } } diff --git a/administrator/components/com_categories/helpers/categories.php b/administrator/components/com_categories/helpers/categories.php index b3caa9bf86cd5..a83cf756e457d 100644 --- a/administrator/components/com_categories/helpers/categories.php +++ b/administrator/components/com_categories/helpers/categories.php @@ -1,13 +1,16 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; + /** * Categories helper. diff --git a/administrator/components/com_categories/layouts/joomla/form/field/categoryedit.php b/administrator/components/com_categories/layouts/joomla/form/field/categoryedit.php index e4d3b0d7cded9..a94ab5edcd4bb 100644 --- a/administrator/components/com_categories/layouts/joomla/form/field/categoryedit.php +++ b/administrator/components/com_categories/layouts/joomla/form/field/categoryedit.php @@ -1,4 +1,5 @@ value as array - if ($multiple && is_array($value)) - { - if (!count($value)) - { - $value[] = ''; - } - - foreach ($value as $val) - { - $html[] = ''; - } - } - else - { - $html[] = ''; - } -} -else -{ - // Create a regular list. - if (count($options) === 0) - { - // All Categories have been deleted, so we need a new category (This will create on save if selected). - $options[0] = new \stdClass; - $options[0]->value = 'Uncategorised'; - $options[0]->text = 'Uncategorised'; - $options[0]->level = '1'; - $options[0]->published = '1'; - $options[0]->lft = '1'; - } - - $html[] = HTMLHelper::_('select.genericlist', $options, $name, trim($attr), 'value', 'text', $value, $id); +if ($readonly) { + $html[] = HTMLHelper::_('select.genericlist', $options, '', trim($attr), 'value', 'text', $value, $id); + + // E.g. form field type tag sends $this->value as array + if ($multiple && is_array($value)) { + if (!count($value)) { + $value[] = ''; + } + + foreach ($value as $val) { + $html[] = ''; + } + } else { + $html[] = ''; + } +} else { + // Create a regular list. + if (count($options) === 0) { + // All Categories have been deleted, so we need a new category (This will create on save if selected). + $options[0] = new \stdClass(); + $options[0]->value = 'Uncategorised'; + $options[0]->text = 'Uncategorised'; + $options[0]->level = '1'; + $options[0]->published = '1'; + $options[0]->lft = '1'; + } + + $html[] = HTMLHelper::_('select.genericlist', $options, $name, trim($attr), 'value', 'text', $value, $id); } -if ($refreshPage === true) -{ - $attr2 .= ' data-refresh-catid="' . $refreshCatId . '" data-refresh-section="' . $refreshSection . '"'; - $attr2 .= ' onchange="Joomla.categoryHasChanged(this)"'; +if ($refreshPage === true) { + $attr2 .= ' data-refresh-catid="' . $refreshCatId . '" data-refresh-section="' . $refreshSection . '"'; + $attr2 .= ' onchange="Joomla.categoryHasChanged(this)"'; - Factory::getDocument()->getWebAssetManager() - ->registerAndUseScript('field.category-change', 'layouts/joomla/form/field/category-change.min.js', [], ['defer' => true], ['core']) - ->useScript('webcomponent.core-loader'); + Factory::getDocument()->getWebAssetManager() + ->registerAndUseScript('field.category-change', 'layouts/joomla/form/field/category-change.min.js', [], ['defer' => true], ['core']) + ->useScript('webcomponent.core-loader'); - // Pass the element id to the javascript - Factory::getDocument()->addScriptOptions('category-change', $id); -} -else -{ - $attr2 .= $onchange ? ' onchange="' . $onchange . '"' : ''; + // Pass the element id to the javascript + Factory::getDocument()->addScriptOptions('category-change', $id); +} else { + $attr2 .= $onchange ? ' onchange="' . $onchange . '"' : ''; } Text::script('JGLOBAL_SELECT_NO_RESULTS_MATCH'); Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); Factory::getDocument()->getWebAssetManager() - ->usePreset('choicesjs') - ->useScript('webcomponent.field-fancy-select'); + ->usePreset('choicesjs') + ->useScript('webcomponent.field-fancy-select'); ?> > diff --git a/administrator/components/com_categories/services/provider.php b/administrator/components/com_categories/services/provider.php index 2ef22e136fe12..043c4850d6425 100644 --- a/administrator/components/com_categories/services/provider.php +++ b/administrator/components/com_categories/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Categories')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Categories')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Categories')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Categories')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new CategoriesComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new CategoriesComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_categories/src/Controller/AjaxController.php b/administrator/components/com_categories/src/Controller/AjaxController.php index f0bdbe8e9b767..3a3398a0a0821 100644 --- a/administrator/components/com_categories/src/Controller/AjaxController.php +++ b/administrator/components/com_categories/src/Controller/AjaxController.php @@ -1,4 +1,5 @@ input->get('extension'); + /** + * Method to fetch associations of a category + * + * The method assumes that the following http parameters are passed in an Ajax Get request: + * token: the form token + * assocId: the id of the category whose associations are to be returned + * excludeLang: the association for this language is to be excluded + * + * @return void + * + * @since 3.9.0 + */ + public function fetchAssociations() + { + if (!Session::checkToken('get')) { + echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true); + } else { + $extension = $this->input->get('extension'); - $assocId = $this->input->getInt('assocId', 0); + $assocId = $this->input->getInt('assocId', 0); - if ($assocId == 0) - { - echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); + if ($assocId == 0) { + echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); - return; - } + return; + } - $excludeLang = $this->input->get('excludeLang', '', 'STRING'); + $excludeLang = $this->input->get('excludeLang', '', 'STRING'); - $associations = Associations::getAssociations($extension, '#__categories', 'com_categories.item', (int) $assocId, 'id', 'alias', ''); + $associations = Associations::getAssociations($extension, '#__categories', 'com_categories.item', (int) $assocId, 'id', 'alias', ''); - unset($associations[$excludeLang]); + unset($associations[$excludeLang]); - // Add the title to each of the associated records - Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_categories/tables'); - $categoryTable = Table::getInstance('Category', 'JTable'); + // Add the title to each of the associated records + Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_categories/tables'); + $categoryTable = Table::getInstance('Category', 'JTable'); - foreach ($associations as $lang => $association) - { - $categoryTable->load($association->id); - $associations[$lang]->title = $categoryTable->title; - } + foreach ($associations as $lang => $association) { + $categoryTable->load($association->id); + $associations[$lang]->title = $categoryTable->title; + } - $countContentLanguages = \count(LanguageHelper::getContentLanguages(array(0, 1), false)); + $countContentLanguages = \count(LanguageHelper::getContentLanguages(array(0, 1), false)); - if (\count($associations) == 0) - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); - } - elseif ($countContentLanguages > \count($associations) + 2) - { - $tags = implode(', ', array_keys($associations)); - $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); - } - else - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); - } + if (\count($associations) == 0) { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); + } elseif ($countContentLanguages > \count($associations) + 2) { + $tags = implode(', ', array_keys($associations)); + $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); + } else { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); + } - echo new JsonResponse($associations, $message); - } - } + echo new JsonResponse($associations, $message); + } + } } diff --git a/administrator/components/com_categories/src/Controller/CategoriesController.php b/administrator/components/com_categories/src/Controller/CategoriesController.php index ad46433211073..ca5c6ceab862e 100644 --- a/administrator/components/com_categories/src/Controller/CategoriesController.php +++ b/administrator/components/com_categories/src/Controller/CategoriesController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Outputs the JSON-encoded amount of published content categories - * - * @return void - * - * @since 4.0.0 - */ - public function getQuickiconContent() - { - $model = $this->getModel('Categories'); - $model->setState('filter.published', 1); - $model->setState('filter.extension', 'com_content'); - - $amount = (int) $model->getTotal(); - - $result = []; - - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_CATEGORIES_N_QUICKICON_SRONLY', $amount); - $result['name'] = Text::plural('COM_CATEGORIES_N_QUICKICON', $amount); - - echo new JsonResponse($result); - } - - /** - * Rebuild the nested set tree. - * - * @return boolean False on failure or error, true on success. - * - * @since 1.6 - */ - public function rebuild() - { - $this->checkToken(); - - $extension = $this->input->get('extension'); - $this->setRedirect(Route::_('index.php?option=com_categories&view=categories&extension=' . $extension, false)); - - /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $model */ - $model = $this->getModel(); - - if ($model->rebuild()) - { - // Rebuild succeeded. - $this->setMessage(Text::_('COM_CATEGORIES_REBUILD_SUCCESS')); - - return true; - } - - // Rebuild failed. - $this->setMessage(Text::_('COM_CATEGORIES_REBUILD_FAILURE')); - - return false; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $extension = $this->input->getCmd('extension', null); - - return '&extension=' . $extension; - } + /** + * Proxy for getModel + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config The array of possible config values. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6 + */ + public function getModel($name = 'Category', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Outputs the JSON-encoded amount of published content categories + * + * @return void + * + * @since 4.0.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('Categories'); + $model->setState('filter.published', 1); + $model->setState('filter.extension', 'com_content'); + + $amount = (int) $model->getTotal(); + + $result = []; + + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_CATEGORIES_N_QUICKICON_SRONLY', $amount); + $result['name'] = Text::plural('COM_CATEGORIES_N_QUICKICON', $amount); + + echo new JsonResponse($result); + } + + /** + * Rebuild the nested set tree. + * + * @return boolean False on failure or error, true on success. + * + * @since 1.6 + */ + public function rebuild() + { + $this->checkToken(); + + $extension = $this->input->get('extension'); + $this->setRedirect(Route::_('index.php?option=com_categories&view=categories&extension=' . $extension, false)); + + /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $model */ + $model = $this->getModel(); + + if ($model->rebuild()) { + // Rebuild succeeded. + $this->setMessage(Text::_('COM_CATEGORIES_REBUILD_SUCCESS')); + + return true; + } + + // Rebuild failed. + $this->setMessage(Text::_('COM_CATEGORIES_REBUILD_FAILURE')); + + return false; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $extension = $this->input->getCmd('extension', null); + + return '&extension=' . $extension; + } } diff --git a/administrator/components/com_categories/src/Controller/CategoryController.php b/administrator/components/com_categories/src/Controller/CategoryController.php index 0a7e746733da5..ab7fbe315e809 100644 --- a/administrator/components/com_categories/src/Controller/CategoryController.php +++ b/administrator/components/com_categories/src/Controller/CategoryController.php @@ -1,4 +1,5 @@ extension)) - { - $this->extension = $this->input->get('extension', 'com_content'); - } - } - - /** - * Method to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowAdd($data = array()) - { - $user = $this->app->getIdentity(); - - return ($user->authorise('core.create', $this->extension) || \count($user->getAuthorisedCategories($this->extension, 'core.create'))); - } - - /** - * Method to check if you can edit a record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'parent_id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - $user = $this->app->getIdentity(); - - // Check "edit" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit', $this->extension . '.category.' . $recordId)) - { - return true; - } - - // Check "edit own" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit.own', $this->extension . '.category.' . $recordId)) - { - // Need to do a lookup from the model to get the owner - $record = $this->getModel()->getItem($recordId); - - if (empty($record)) - { - return false; - } - - $ownerId = $record->created_user_id; - - // If the owner matches 'me' then do the test. - if ($ownerId == $user->id) - { - return true; - } - } - - return false; - } - - /** - * Override parent save method to store form data with right key as expected by edit category page - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean True if successful, false otherwise. - * - * @since 3.10.3 - */ - public function save($key = null, $urlVar = null) - { - $result = parent::save($key, $urlVar); - - $oldKey = $this->option . '.edit.category.data'; - $newKey = $this->option . '.edit.category.' . substr($this->extension, 4) . '.data'; - $this->app->setUserState($newKey, $this->app->getUserState($oldKey)); - - return $result; - } - - /** - * Override cancel method to clear form data for a failed edit action - * - * @param string $key The name of the primary key of the URL variable. - * - * @return boolean True if access level checks pass, false otherwise. - * - * @since 3.10.3 - */ - public function cancel($key = null) - { - $result = parent::cancel($key); - - $newKey = $this->option . '.edit.category.' . substr($this->extension, 4) . '.data'; - $this->app->setUserState($newKey, null); - - return $result; - } - - /** - * Method to run batch operations. - * - * @param object|null $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 1.6 - */ - public function batch($model = null) - { - $this->checkToken(); - - /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $model */ - $model = $this->getModel('Category'); - - // Preset the redirect - $this->setRedirect('index.php?option=com_categories&view=categories&extension=' . $this->extension); - - return parent::batch($model); - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer|null $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 1.6 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $append = parent::getRedirectToItemAppend($recordId); - - // In case extension is not passed in the URL, get it directly from category instead of default to com_content - if (!$this->input->exists('extension') && $recordId > 0) - { - $table = $this->getModel('Category')->getTable(); - - if ($table->load($recordId)) - { - $this->extension = $table->extension; - } - } - - $append .= '&extension=' . $this->extension; - - return $append; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 1.6 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - $append .= '&extension=' . $this->extension; - - return $append; - } - - /** - * Function that allows child controller access to model data after the data has been saved. - * - * @param \Joomla\CMS\MVC\Model\BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 3.1 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) - { - $item = $model->getItem(); - - if (isset($item->params) && \is_array($item->params)) - { - $registry = new Registry($item->params); - $item->params = (string) $registry; - } - - if (isset($item->metadata) && \is_array($item->metadata)) - { - $registry = new Registry($item->metadata); - $item->metadata = (string) $registry; - } - } + use VersionableControllerTrait; + + /** + * The extension for which the categories apply. + * + * @var string + * @since 1.6 + */ + protected $extension; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The Application for the dispatcher + * @param Input|null $input Input + * + * @since 1.6 + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, CMSApplication $app = null, Input $input = null) + { + parent::__construct($config, $factory, $app, $input); + + if (empty($this->extension)) { + $this->extension = $this->input->get('extension', 'com_content'); + } + } + + /** + * Method to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowAdd($data = array()) + { + $user = $this->app->getIdentity(); + + return ($user->authorise('core.create', $this->extension) || \count($user->getAuthorisedCategories($this->extension, 'core.create'))); + } + + /** + * Method to check if you can edit a record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'parent_id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + $user = $this->app->getIdentity(); + + // Check "edit" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit', $this->extension . '.category.' . $recordId)) { + return true; + } + + // Check "edit own" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit.own', $this->extension . '.category.' . $recordId)) { + // Need to do a lookup from the model to get the owner + $record = $this->getModel()->getItem($recordId); + + if (empty($record)) { + return false; + } + + $ownerId = $record->created_user_id; + + // If the owner matches 'me' then do the test. + if ($ownerId == $user->id) { + return true; + } + } + + return false; + } + + /** + * Override parent save method to store form data with right key as expected by edit category page + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 3.10.3 + */ + public function save($key = null, $urlVar = null) + { + $result = parent::save($key, $urlVar); + + $oldKey = $this->option . '.edit.category.data'; + $newKey = $this->option . '.edit.category.' . substr($this->extension, 4) . '.data'; + $this->app->setUserState($newKey, $this->app->getUserState($oldKey)); + + return $result; + } + + /** + * Override cancel method to clear form data for a failed edit action + * + * @param string $key The name of the primary key of the URL variable. + * + * @return boolean True if access level checks pass, false otherwise. + * + * @since 3.10.3 + */ + public function cancel($key = null) + { + $result = parent::cancel($key); + + $newKey = $this->option . '.edit.category.' . substr($this->extension, 4) . '.data'; + $this->app->setUserState($newKey, null); + + return $result; + } + + /** + * Method to run batch operations. + * + * @param object|null $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 1.6 + */ + public function batch($model = null) + { + $this->checkToken(); + + /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $model */ + $model = $this->getModel('Category'); + + // Preset the redirect + $this->setRedirect('index.php?option=com_categories&view=categories&extension=' . $this->extension); + + return parent::batch($model); + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer|null $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 1.6 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $append = parent::getRedirectToItemAppend($recordId); + + // In case extension is not passed in the URL, get it directly from category instead of default to com_content + if (!$this->input->exists('extension') && $recordId > 0) { + $table = $this->getModel('Category')->getTable(); + + if ($table->load($recordId)) { + $this->extension = $table->extension; + } + } + + $append .= '&extension=' . $this->extension; + + return $append; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 1.6 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + $append .= '&extension=' . $this->extension; + + return $append; + } + + /** + * Function that allows child controller access to model data after the data has been saved. + * + * @param \Joomla\CMS\MVC\Model\BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 3.1 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) + { + $item = $model->getItem(); + + if (isset($item->params) && \is_array($item->params)) { + $registry = new Registry($item->params); + $item->params = (string) $registry; + } + + if (isset($item->metadata) && \is_array($item->metadata)) { + $registry = new Registry($item->metadata); + $item->metadata = (string) $registry; + } + } } diff --git a/administrator/components/com_categories/src/Controller/DisplayController.php b/administrator/components/com_categories/src/Controller/DisplayController.php index 5f9ab4ae35db5..33f2bb00e4574 100644 --- a/administrator/components/com_categories/src/Controller/DisplayController.php +++ b/administrator/components/com_categories/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ extension)) - { - $this->extension = $this->input->get('extension', 'com_content'); - } - } - - /** - * Method to display a view. - * - * @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()}. - * - * @return static|boolean This object to support chaining. - * - * @since 1.5 - */ - public function display($cachable = false, $urlparams = array()) - { - // Get the document object. - $document = $this->app->getDocument(); - - // Set the default view name and format from the Request. - $vName = $this->input->get('view', 'categories'); - $vFormat = $document->getType(); - $lName = $this->input->get('layout', 'default', 'string'); - $id = $this->input->getInt('id'); - - // Check for edit form. - if ($vName == 'category' && $lName == 'edit' && !$this->checkEditId('com_categories.edit.category', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_categories&view=categories&extension=' . $this->extension, false)); - - return false; - } - - // Get and render the view. - if ($view = $this->getView($vName, $vFormat)) - { - // Get the model for the view. - $model = $this->getModel($vName, 'Administrator', array('name' => $vName . '.' . substr($this->extension, 4))); - - // Push the model into the view (as default). - $view->setModel($model, true); - $view->setLayout($lName); - - // Push document object into the view. - $view->document = $document; - $view->display(); - } - - return $this; - } + /** + * The default view. + * + * @var string + * @since 1.6 + */ + protected $default_view = 'categories'; + + /** + * The extension for which the categories apply. + * + * @var string + * @since 1.6 + */ + protected $extension; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The Application for the dispatcher + * @param Input|null $input Input + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // Guess the Text message prefix. Defaults to the option. + if (empty($this->extension)) { + $this->extension = $this->input->get('extension', 'com_content'); + } + } + + /** + * Method to display a view. + * + * @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()}. + * + * @return static|boolean This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = array()) + { + // Get the document object. + $document = $this->app->getDocument(); + + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'categories'); + $vFormat = $document->getType(); + $lName = $this->input->get('layout', 'default', 'string'); + $id = $this->input->getInt('id'); + + // Check for edit form. + if ($vName == 'category' && $lName == 'edit' && !$this->checkEditId('com_categories.edit.category', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_categories&view=categories&extension=' . $this->extension, false)); + + return false; + } + + // Get and render the view. + if ($view = $this->getView($vName, $vFormat)) { + // Get the model for the view. + $model = $this->getModel($vName, 'Administrator', array('name' => $vName . '.' . substr($this->extension, 4))); + + // Push the model into the view (as default). + $view->setModel($model, true); + $view->setLayout($lName); + + // Push document object into the view. + $view->document = $document; + $view->display(); + } + + return $this; + } } diff --git a/administrator/components/com_categories/src/Dispatcher/Dispatcher.php b/administrator/components/com_categories/src/Dispatcher/Dispatcher.php index d32c1c0ece757..4a7b59afa5969 100644 --- a/administrator/components/com_categories/src/Dispatcher/Dispatcher.php +++ b/administrator/components/com_categories/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ getApplication()->input->getCmd('extension'); + /** + * Categories have to check for extension permission + * + * @return void + */ + protected function checkAccess() + { + $extension = $this->getApplication()->input->getCmd('extension'); - $parts = explode('.', $extension); + $parts = explode('.', $extension); - // Check the user has permission to access this component if in the backend - if ($this->app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.manage', $parts[0])) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + // Check the user has permission to access this component if in the backend + if ($this->app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.manage', $parts[0])) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/administrator/components/com_categories/src/Extension/CategoriesComponent.php b/administrator/components/com_categories/src/Extension/CategoriesComponent.php index 8b2eb72f2b9dd..cca327dc05da1 100644 --- a/administrator/components/com_categories/src/Extension/CategoriesComponent.php +++ b/administrator/components/com_categories/src/Extension/CategoriesComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('categoriesadministrator', new AdministratorService); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('categoriesadministrator', new AdministratorService()); + } } diff --git a/administrator/components/com_categories/src/Field/CategoryeditField.php b/administrator/components/com_categories/src/Field/CategoryeditField.php index 4a8ccec8e8d22..b141f5e7eb4c1 100644 --- a/administrator/components/com_categories/src/Field/CategoryeditField.php +++ b/administrator/components/com_categories/src/Field/CategoryeditField.php @@ -1,4 +1,5 @@ tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string|null $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @see FormField::setup() - * @since 3.2 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $return = parent::setup($element, $value, $group); - - if ($return) - { - $this->allowAdd = isset($this->element['allowAdd']) ? (boolean) $this->element['allowAdd'] : false; - $this->customPrefix = (string) $this->element['customPrefix']; - } - - return $return; - } - - /** - * Method to get certain otherwise inaccessible properties from the form field object. - * - * @param string $name The property name for which to get the value. - * - * @return mixed The property value or null. - * - * @since 3.6 - */ - public function __get($name) - { - switch ($name) - { - case 'allowAdd': - return (bool) $this->$name; - case 'customPrefix': - return $this->$name; - } - - return parent::__get($name); - } - - /** - * Method to set certain otherwise inaccessible properties of the form field object. - * - * @param string $name The property name for which to set the value. - * @param mixed $value The value of the property. - * - * @return void - * - * @since 3.6 - */ - public function __set($name, $value) - { - $value = (string) $value; - - switch ($name) - { - case 'allowAdd': - $value = (string) $value; - $this->$name = ($value === 'true' || $value === $name || $value === '1'); - break; - case 'customPrefix': - $this->$name = (string) $value; - break; - default: - parent::__set($name, $value); - } - } - - /** - * Method to get a list of categories that respects access controls and can be used for - * either category assignment or parent category assignment in edit screens. - * Use the parent element to indicate that the field will be used for assigning parent categories. - * - * @return array The field option objects. - * - * @since 1.6 - */ - protected function getOptions() - { - $options = array(); - $published = $this->element['published'] ? explode(',', (string) $this->element['published']) : array(0, 1); - $name = (string) $this->element['name']; - - // Let's get the id for the current item, either category or content item. - $jinput = Factory::getApplication()->input; - - // Load the category options for a given extension. - - // For categories the old category is the category id or 0 for new category. - if ($this->element['parent'] || $jinput->get('option') == 'com_categories') - { - $oldCat = $jinput->get('id', 0); - $oldParent = $this->form->getValue($name, 0); - $extension = $this->element['extension'] ? (string) $this->element['extension'] : (string) $jinput->get('extension', 'com_content'); - } - else - // For items the old category is the category they are in when opened or 0 if new. - { - $oldCat = $this->form->getValue($name, 0); - $extension = $this->element['extension'] ? (string) $this->element['extension'] : (string) $jinput->get('option', 'com_content'); - } - - // Account for case that a submitted form has a multi-value category id field (e.g. a filtering form), just use the first category - $oldCat = \is_array($oldCat) - ? (int) reset($oldCat) - : (int) $oldCat; - - $db = $this->getDatabase(); - $user = Factory::getUser(); - - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('a.id', 'value'), - $db->quoteName('a.title', 'text'), - $db->quoteName('a.level'), - $db->quoteName('a.published'), - $db->quoteName('a.lft'), - $db->quoteName('a.language'), - ] - ) - ->from($db->quoteName('#__categories', 'a')); - - // Filter by the extension type - if ($this->element['parent'] == true || $jinput->get('option') == 'com_categories') - { - $query->where('(' . $db->quoteName('a.extension') . ' = :extension OR ' . $db->quoteName('a.parent_id') . ' = 0)') - ->bind(':extension', $extension); - } - else - { - $query->where($db->quoteName('a.extension') . ' = :extension') - ->bind(':extension', $extension); - } - - // Filter language - if (!empty($this->element['language'])) - { - if (strpos($this->element['language'], ',') !== false) - { - $language = explode(',', $this->element['language']); - } - else - { - $language = $this->element['language']; - } - - $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); - } - - // Filter on the published state - $state = ArrayHelper::toInteger($published); - $query->whereIn($db->quoteName('a.published'), $state); - - // Filter categories on User Access Level - // Filter by access level on categories. - if (!$user->authorise('core.admin')) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('a.access'), $groups); - } - - $query->order($db->quoteName('a.lft') . ' ASC'); - - // If parent isn't explicitly stated but we are in com_categories assume we want parents - if ($oldCat != 0 && ($this->element['parent'] == true || $jinput->get('option') == 'com_categories')) - { - // Prevent parenting to children of this item. - // To rearrange parents and children move the children up, not the parents down. - $query->join( - 'LEFT', - $db->quoteName('#__categories', 'p'), - $db->quoteName('p.id') . ' = :oldcat' - ) - ->bind(':oldcat', $oldCat, ParameterType::INTEGER) - ->where('NOT(' . $db->quoteName('a.lft') . ' >= ' . $db->quoteName('p.lft') - . ' AND ' . $db->quoteName('a.rgt') . ' <= ' . $db->quoteName('p.rgt') . ')' - ); - } - - // Get the options. - $db->setQuery($query); - - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - // Pad the option text with spaces using depth level as a multiplier. - for ($i = 0, $n = \count($options); $i < $n; $i++) - { - // Translate ROOT - if ($this->element['parent'] == true || $jinput->get('option') == 'com_categories') - { - if ($options[$i]->level == 0) - { - $options[$i]->text = Text::_('JGLOBAL_ROOT_PARENT'); - } - } - - if ($options[$i]->published == 1) - { - $options[$i]->text = str_repeat('- ', !$options[$i]->level ? 0 : $options[$i]->level - 1) . $options[$i]->text; - } - else - { - $options[$i]->text = str_repeat('- ', !$options[$i]->level ? 0 : $options[$i]->level - 1) . '[' . $options[$i]->text . ']'; - } - - // Displays language code if not set to All - if ($options[$i]->language !== '*') - { - $options[$i]->text = $options[$i]->text . ' (' . $options[$i]->language . ')'; - } - } - - // For new items we want a list of categories you are allowed to create in. - if ($oldCat == 0) - { - foreach ($options as $i => $option) - { - /* - * To take save or create in a category you need to have create rights for that category unless the item is already in that category. - * Unset the option if the user isn't authorised for it. In this field assets are always categories. - */ - if ($option->level != 0 && !$user->authorise('core.create', $extension . '.category.' . $option->value)) - { - unset($options[$i]); - } - } - } - // If you have an existing category id things are more complex. - else - { - /* - * If you are only allowed to edit in this category but not edit.state, you should not get any - * option to change the category parent for a category or the category for a content item, - * but you should be able to save in that category. - */ - foreach ($options as $i => $option) - { - $assetKey = $extension . '.category.' . $oldCat; - - if ($option->level != 0 && !isset($oldParent) && $option->value != $oldCat && !$user->authorise('core.edit.state', $assetKey)) - { - unset($options[$i]); - continue; - } - - if ($option->level != 0 && isset($oldParent) && $option->value != $oldParent && !$user->authorise('core.edit.state', $assetKey)) - { - unset($options[$i]); - continue; - } - - /* - * However, if you can edit.state you can also move this to another category for which you have - * create permission and you should also still be able to save in the current category. - */ - $assetKey = $extension . '.category.' . $option->value; - - if ($option->level != 0 && !isset($oldParent) && $option->value != $oldCat && !$user->authorise('core.create', $assetKey)) - { - unset($options[$i]); - continue; - } - - if ($option->level != 0 && isset($oldParent) && $option->value != $oldParent && !$user->authorise('core.create', $assetKey)) - { - unset($options[$i]); - } - } - } - - if ($oldCat != 0 && ($this->element['parent'] == true || $jinput->get('option') == 'com_categories') - && !isset($options[0]) - && isset($this->element['show_root'])) - { - $rowQuery = $db->getQuery(true) - ->select( - [ - $db->quoteName('a.id', 'value'), - $db->quoteName('a.title', 'text'), - $db->quoteName('a.level'), - $db->quoteName('a.parent_id'), - ] - ) - ->from($db->quoteName('#__categories', 'a')) - ->where($db->quoteName('a.id') . ' = :aid') - ->bind(':aid', $oldCat, ParameterType::INTEGER); - $db->setQuery($rowQuery); - $row = $db->loadObject(); - - if ($row->parent_id == '1') - { - $parent = new \stdClass; - $parent->text = Text::_('JGLOBAL_ROOT_PARENT'); - array_unshift($options, $parent); - } - - array_unshift($options, HTMLHelper::_('select.option', '0', Text::_('JGLOBAL_ROOT'))); - } - - // Merge any additional options in the XML definition. - return array_merge(parent::getOptions(), $options); - } - - /** - * Method to get the field input markup for a generic list. - * Use the multiple attribute to enable multiselect. - * - * @return string The field input markup. - * - * @since 3.6 - */ - protected function getInput() - { - $data = $this->getLayoutData(); - - $data['options'] = $this->getOptions(); - $data['allowCustom'] = $this->allowAdd; - $data['customPrefix'] = $this->customPrefix; - $data['refreshPage'] = (boolean) $this->element['refresh-enabled']; - $data['refreshCatId'] = (string) $this->element['refresh-cat-id']; - $data['refreshSection'] = (string) $this->element['refresh-section']; - - $renderer = $this->getRenderer($this->layout); - $renderer->setComponent('com_categories'); - $renderer->setClient(1); - - return $renderer->render($data); - } + /** + * To allow creation of new categories. + * + * @var integer + * @since 3.6 + */ + protected $allowAdd; + + /** + * Optional prefix for new categories. + * + * @var string + * @since 3.9.11 + */ + protected $customPrefix; + + /** + * A flexible category list that respects access controls + * + * @var string + * @since 1.6 + */ + public $type = 'CategoryEdit'; + + /** + * Name of the layout being used to render the field + * + * @var string + * @since 4.0.0 + */ + protected $layout = 'joomla.form.field.categoryedit'; + + /** + * Method to attach a JForm object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string|null $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @see FormField::setup() + * @since 3.2 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $return = parent::setup($element, $value, $group); + + if ($return) { + $this->allowAdd = isset($this->element['allowAdd']) ? (bool) $this->element['allowAdd'] : false; + $this->customPrefix = (string) $this->element['customPrefix']; + } + + return $return; + } + + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to get the value. + * + * @return mixed The property value or null. + * + * @since 3.6 + */ + public function __get($name) + { + switch ($name) { + case 'allowAdd': + return (bool) $this->$name; + case 'customPrefix': + return $this->$name; + } + + return parent::__get($name); + } + + /** + * Method to set certain otherwise inaccessible properties of the form field object. + * + * @param string $name The property name for which to set the value. + * @param mixed $value The value of the property. + * + * @return void + * + * @since 3.6 + */ + public function __set($name, $value) + { + $value = (string) $value; + + switch ($name) { + case 'allowAdd': + $value = (string) $value; + $this->$name = ($value === 'true' || $value === $name || $value === '1'); + break; + case 'customPrefix': + $this->$name = (string) $value; + break; + default: + parent::__set($name, $value); + } + } + + /** + * Method to get a list of categories that respects access controls and can be used for + * either category assignment or parent category assignment in edit screens. + * Use the parent element to indicate that the field will be used for assigning parent categories. + * + * @return array The field option objects. + * + * @since 1.6 + */ + protected function getOptions() + { + $options = array(); + $published = $this->element['published'] ? explode(',', (string) $this->element['published']) : array(0, 1); + $name = (string) $this->element['name']; + + // Let's get the id for the current item, either category or content item. + $jinput = Factory::getApplication()->input; + + // Load the category options for a given extension. + + // For categories the old category is the category id or 0 for new category. + if ($this->element['parent'] || $jinput->get('option') == 'com_categories') { + $oldCat = $jinput->get('id', 0); + $oldParent = $this->form->getValue($name, 0); + $extension = $this->element['extension'] ? (string) $this->element['extension'] : (string) $jinput->get('extension', 'com_content'); + } else // For items the old category is the category they are in when opened or 0 if new. + { + $oldCat = $this->form->getValue($name, 0); + $extension = $this->element['extension'] ? (string) $this->element['extension'] : (string) $jinput->get('option', 'com_content'); + } + + // Account for case that a submitted form has a multi-value category id field (e.g. a filtering form), just use the first category + $oldCat = \is_array($oldCat) + ? (int) reset($oldCat) + : (int) $oldCat; + + $db = $this->getDatabase(); + $user = Factory::getUser(); + + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('a.id', 'value'), + $db->quoteName('a.title', 'text'), + $db->quoteName('a.level'), + $db->quoteName('a.published'), + $db->quoteName('a.lft'), + $db->quoteName('a.language'), + ] + ) + ->from($db->quoteName('#__categories', 'a')); + + // Filter by the extension type + if ($this->element['parent'] == true || $jinput->get('option') == 'com_categories') { + $query->where('(' . $db->quoteName('a.extension') . ' = :extension OR ' . $db->quoteName('a.parent_id') . ' = 0)') + ->bind(':extension', $extension); + } else { + $query->where($db->quoteName('a.extension') . ' = :extension') + ->bind(':extension', $extension); + } + + // Filter language + if (!empty($this->element['language'])) { + if (strpos($this->element['language'], ',') !== false) { + $language = explode(',', $this->element['language']); + } else { + $language = $this->element['language']; + } + + $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); + } + + // Filter on the published state + $state = ArrayHelper::toInteger($published); + $query->whereIn($db->quoteName('a.published'), $state); + + // Filter categories on User Access Level + // Filter by access level on categories. + if (!$user->authorise('core.admin')) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('a.access'), $groups); + } + + $query->order($db->quoteName('a.lft') . ' ASC'); + + // If parent isn't explicitly stated but we are in com_categories assume we want parents + if ($oldCat != 0 && ($this->element['parent'] == true || $jinput->get('option') == 'com_categories')) { + // Prevent parenting to children of this item. + // To rearrange parents and children move the children up, not the parents down. + $query->join( + 'LEFT', + $db->quoteName('#__categories', 'p'), + $db->quoteName('p.id') . ' = :oldcat' + ) + ->bind(':oldcat', $oldCat, ParameterType::INTEGER) + ->where('NOT(' . $db->quoteName('a.lft') . ' >= ' . $db->quoteName('p.lft') + . ' AND ' . $db->quoteName('a.rgt') . ' <= ' . $db->quoteName('p.rgt') . ')'); + } + + // Get the options. + $db->setQuery($query); + + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + // Pad the option text with spaces using depth level as a multiplier. + for ($i = 0, $n = \count($options); $i < $n; $i++) { + // Translate ROOT + if ($this->element['parent'] == true || $jinput->get('option') == 'com_categories') { + if ($options[$i]->level == 0) { + $options[$i]->text = Text::_('JGLOBAL_ROOT_PARENT'); + } + } + + if ($options[$i]->published == 1) { + $options[$i]->text = str_repeat('- ', !$options[$i]->level ? 0 : $options[$i]->level - 1) . $options[$i]->text; + } else { + $options[$i]->text = str_repeat('- ', !$options[$i]->level ? 0 : $options[$i]->level - 1) . '[' . $options[$i]->text . ']'; + } + + // Displays language code if not set to All + if ($options[$i]->language !== '*') { + $options[$i]->text = $options[$i]->text . ' (' . $options[$i]->language . ')'; + } + } + + // For new items we want a list of categories you are allowed to create in. + if ($oldCat == 0) { + foreach ($options as $i => $option) { + /* + * To take save or create in a category you need to have create rights for that category unless the item is already in that category. + * Unset the option if the user isn't authorised for it. In this field assets are always categories. + */ + if ($option->level != 0 && !$user->authorise('core.create', $extension . '.category.' . $option->value)) { + unset($options[$i]); + } + } + } else { + // If you have an existing category id things are more complex. + /* + * If you are only allowed to edit in this category but not edit.state, you should not get any + * option to change the category parent for a category or the category for a content item, + * but you should be able to save in that category. + */ + foreach ($options as $i => $option) { + $assetKey = $extension . '.category.' . $oldCat; + + if ($option->level != 0 && !isset($oldParent) && $option->value != $oldCat && !$user->authorise('core.edit.state', $assetKey)) { + unset($options[$i]); + continue; + } + + if ($option->level != 0 && isset($oldParent) && $option->value != $oldParent && !$user->authorise('core.edit.state', $assetKey)) { + unset($options[$i]); + continue; + } + + /* + * However, if you can edit.state you can also move this to another category for which you have + * create permission and you should also still be able to save in the current category. + */ + $assetKey = $extension . '.category.' . $option->value; + + if ($option->level != 0 && !isset($oldParent) && $option->value != $oldCat && !$user->authorise('core.create', $assetKey)) { + unset($options[$i]); + continue; + } + + if ($option->level != 0 && isset($oldParent) && $option->value != $oldParent && !$user->authorise('core.create', $assetKey)) { + unset($options[$i]); + } + } + } + + if ( + $oldCat != 0 && ($this->element['parent'] == true || $jinput->get('option') == 'com_categories') + && !isset($options[0]) + && isset($this->element['show_root']) + ) { + $rowQuery = $db->getQuery(true) + ->select( + [ + $db->quoteName('a.id', 'value'), + $db->quoteName('a.title', 'text'), + $db->quoteName('a.level'), + $db->quoteName('a.parent_id'), + ] + ) + ->from($db->quoteName('#__categories', 'a')) + ->where($db->quoteName('a.id') . ' = :aid') + ->bind(':aid', $oldCat, ParameterType::INTEGER); + $db->setQuery($rowQuery); + $row = $db->loadObject(); + + if ($row->parent_id == '1') { + $parent = new \stdClass(); + $parent->text = Text::_('JGLOBAL_ROOT_PARENT'); + array_unshift($options, $parent); + } + + array_unshift($options, HTMLHelper::_('select.option', '0', Text::_('JGLOBAL_ROOT'))); + } + + // Merge any additional options in the XML definition. + return array_merge(parent::getOptions(), $options); + } + + /** + * Method to get the field input markup for a generic list. + * Use the multiple attribute to enable multiselect. + * + * @return string The field input markup. + * + * @since 3.6 + */ + protected function getInput() + { + $data = $this->getLayoutData(); + + $data['options'] = $this->getOptions(); + $data['allowCustom'] = $this->allowAdd; + $data['customPrefix'] = $this->customPrefix; + $data['refreshPage'] = (bool) $this->element['refresh-enabled']; + $data['refreshCatId'] = (string) $this->element['refresh-cat-id']; + $data['refreshSection'] = (string) $this->element['refresh-section']; + + $renderer = $this->getRenderer($this->layout); + $renderer->setComponent('com_categories'); + $renderer->setClient(1); + + return $renderer->render($data); + } } diff --git a/administrator/components/com_categories/src/Field/ComponentsCategoryField.php b/administrator/components/com_categories/src/Field/ComponentsCategoryField.php index 8fd9838dd588a..3efedd4cd8d37 100644 --- a/administrator/components/com_categories/src/Field/ComponentsCategoryField.php +++ b/administrator/components/com_categories/src/Field/ComponentsCategoryField.php @@ -1,4 +1,5 @@ getDatabase(); - $options = array(); + /** + * Method to get a list of options for a list input. + * + * @return array An array of JHtml options. + * + * @since 3.7.0 + */ + protected function getOptions() + { + // Initialise variable. + $db = $this->getDatabase(); + $options = array(); - $query = $db->getQuery(true); - $query->select('DISTINCT ' . $db->quoteName('extension')) - ->from($db->quoteName('#__categories')) - ->where($db->quoteName('extension') . ' != ' . $db->quote('system')); + $query = $db->getQuery(true); + $query->select('DISTINCT ' . $db->quoteName('extension')) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('extension') . ' != ' . $db->quote('system')); - $db->setQuery($query); - $categoryTypes = $db->loadColumn(); + $db->setQuery($query); + $categoryTypes = $db->loadColumn(); - foreach ($categoryTypes as $categoryType) - { - $option = new \stdClass; - $option->value = $categoryType; + foreach ($categoryTypes as $categoryType) { + $option = new \stdClass(); + $option->value = $categoryType; - // Extract the component name and optional section name - $parts = explode('.', $categoryType); - $component = $parts[0]; - $section = (\count($parts) > 1) ? $parts[1] : null; + // Extract the component name and optional section name + $parts = explode('.', $categoryType); + $component = $parts[0]; + $section = (\count($parts) > 1) ? $parts[1] : null; - // Load component language files - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_BASE) - || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); + // Load component language files + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_BASE) + || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); - // If the component section string exists, let's use it - if ($lang->hasKey($component_section_key = strtoupper($component . ($section ? "_$section" : '')))) - { - $option->text = Text::_($component_section_key); - } - else - // Else use the component title - { - $option->text = Text::_(strtoupper($component)); - } + // If the component section string exists, let's use it + if ($lang->hasKey($component_section_key = strtoupper($component . ($section ? "_$section" : '')))) { + $option->text = Text::_($component_section_key); + } else // Else use the component title + { + $option->text = Text::_(strtoupper($component)); + } - $options[] = $option; - } + $options[] = $option; + } - // Sort by name - $options = ArrayHelper::sortObjects($options, 'text', 1, true, true); + // Sort by name + $options = ArrayHelper::sortObjects($options, 'text', 1, true, true); - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $options); + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $options); - return $options; - } + return $options; + } } diff --git a/administrator/components/com_categories/src/Field/Modal/CategoryField.php b/administrator/components/com_categories/src/Field/Modal/CategoryField.php index 70c1f5ff18237..0685b050afcc4 100644 --- a/administrator/components/com_categories/src/Field/Modal/CategoryField.php +++ b/administrator/components/com_categories/src/Field/Modal/CategoryField.php @@ -1,4 +1,5 @@ element['extension']) - { - $extension = (string) $this->element['extension']; - } - else - { - $extension = (string) Factory::getApplication()->input->get('extension', 'com_content'); - } - - $allowNew = ((string) $this->element['new'] == 'true'); - $allowEdit = ((string) $this->element['edit'] == 'true'); - $allowClear = ((string) $this->element['clear'] != 'false'); - $allowSelect = ((string) $this->element['select'] != 'false'); - $allowPropagate = ((string) $this->element['propagate'] == 'true'); - - $languages = LanguageHelper::getContentLanguages(array(0, 1), false); - - // Load language. - Factory::getLanguage()->load('com_categories', JPATH_ADMINISTRATOR); - - // The active category id field. - $value = (int) $this->value ?: ''; - - // Create the modal id. - $modalId = 'Category_' . $this->id; - - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); - - // Add the modal field script to the document head. - $wa->useScript('field.modal-fields'); - - // Script to proxy the select modal function to the modal-fields.js file. - if ($allowSelect) - { - static $scriptSelect = null; - - if (is_null($scriptSelect)) - { - $scriptSelect = array(); - } - - if (!isset($scriptSelect[$this->id])) - { - $wa->addInlineScript(" + /** + * The form field type. + * + * @var string + * @since 1.6 + */ + protected $type = 'Modal_Category'; + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + if ($this->element['extension']) { + $extension = (string) $this->element['extension']; + } else { + $extension = (string) Factory::getApplication()->input->get('extension', 'com_content'); + } + + $allowNew = ((string) $this->element['new'] == 'true'); + $allowEdit = ((string) $this->element['edit'] == 'true'); + $allowClear = ((string) $this->element['clear'] != 'false'); + $allowSelect = ((string) $this->element['select'] != 'false'); + $allowPropagate = ((string) $this->element['propagate'] == 'true'); + + $languages = LanguageHelper::getContentLanguages(array(0, 1), false); + + // Load language. + Factory::getLanguage()->load('com_categories', JPATH_ADMINISTRATOR); + + // The active category id field. + $value = (int) $this->value ?: ''; + + // Create the modal id. + $modalId = 'Category_' . $this->id; + + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + + // Add the modal field script to the document head. + $wa->useScript('field.modal-fields'); + + // Script to proxy the select modal function to the modal-fields.js file. + if ($allowSelect) { + static $scriptSelect = null; + + if (is_null($scriptSelect)) { + $scriptSelect = array(); + } + + if (!isset($scriptSelect[$this->id])) { + $wa->addInlineScript( + " window.jSelectCategory_" . $this->id . " = function (id, title, object) { window.processModalSelect('Category', '" . $this->id . "', id, title, '', object); }", - [], - ['type' => 'module'] - ); - - Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); - - $scriptSelect[$this->id] = true; - } - } - - // Setup variables for display. - $linkCategories = 'index.php?option=com_categories&view=categories&layout=modal&tmpl=component&' . Session::getFormToken() . '=1' - . '&extension=' . $extension; - $linkCategory = 'index.php?option=com_categories&view=category&layout=modal&tmpl=component&' . Session::getFormToken() . '=1' - . '&extension=' . $extension; - $modalTitle = Text::_('COM_CATEGORIES_SELECT_A_CATEGORY'); - - if (isset($this->element['language'])) - { - $linkCategories .= '&forcedLanguage=' . $this->element['language']; - $linkCategory .= '&forcedLanguage=' . $this->element['language']; - $modalTitle .= ' — ' . $this->element['label']; - } - - $urlSelect = $linkCategories . '&function=jSelectCategory_' . $this->id; - $urlEdit = $linkCategory . '&task=category.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; - $urlNew = $linkCategory . '&task=category.add'; - - if ($value) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__categories')) - ->where($db->quoteName('id') . ' = :value') - ->bind(':value', $value, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $title = $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - - $title = empty($title) ? Text::_('COM_CATEGORIES_SELECT_A_CATEGORY') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); - - // The current category display field. - $html = ''; - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - $html .= ''; - - // Select category button. - if ($allowSelect) - { - $html .= '' - . ' ' . Text::_('JSELECT') - . ''; - } - - // New category button. - if ($allowNew) - { - $html .= '' - . ' ' . Text::_('JACTION_CREATE') - . ''; - } - - // Edit category button. - if ($allowEdit) - { - $html .= '' - . ' ' . Text::_('JACTION_EDIT') - . ''; - } - - // Clear category button. - if ($allowClear) - { - $html .= '' - . ' ' . Text::_('JCLEAR') - . ''; - } - - // Propagate category button - if ($allowPropagate && \count($languages) > 2) - { - // Strip off language tag at the end - $tagLength = (int) \strlen($this->element['language']); - $callbackFunctionStem = substr("jSelectCategory_" . $this->id, 0, -$tagLength); - - $html .= '' - . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') - . ''; - } - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - // Select category modal. - if ($allowSelect) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalSelect' . $modalId, - array( - 'title' => $modalTitle, - 'url' => $urlSelect, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ) - ); - } - - // New category modal. - if ($allowNew) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalNew' . $modalId, - array( - 'title' => Text::_('COM_CATEGORIES_NEW_CATEGORY'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlNew, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Edit category modal. - if ($allowEdit) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalEdit' . $modalId, - array( - 'title' => Text::_('COM_CATEGORIES_EDIT_CATEGORY'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlEdit, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Note: class='required' for client side validation - $class = $this->required ? ' class="required modal-value"' : ''; - - $html .= ''; - - return $html; - } - - /** - * Method to get the field label markup. - * - * @return string The field label markup. - * - * @since 3.7.0 - */ - protected function getLabel() - { - return str_replace($this->id, $this->id . '_name', parent::getLabel()); - } + [], + ['type' => 'module'] + ); + + Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); + + $scriptSelect[$this->id] = true; + } + } + + // Setup variables for display. + $linkCategories = 'index.php?option=com_categories&view=categories&layout=modal&tmpl=component&' . Session::getFormToken() . '=1' + . '&extension=' . $extension; + $linkCategory = 'index.php?option=com_categories&view=category&layout=modal&tmpl=component&' . Session::getFormToken() . '=1' + . '&extension=' . $extension; + $modalTitle = Text::_('COM_CATEGORIES_SELECT_A_CATEGORY'); + + if (isset($this->element['language'])) { + $linkCategories .= '&forcedLanguage=' . $this->element['language']; + $linkCategory .= '&forcedLanguage=' . $this->element['language']; + $modalTitle .= ' — ' . $this->element['label']; + } + + $urlSelect = $linkCategories . '&function=jSelectCategory_' . $this->id; + $urlEdit = $linkCategory . '&task=category.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; + $urlNew = $linkCategory . '&task=category.add'; + + if ($value) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('id') . ' = :value') + ->bind(':value', $value, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $title = $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + + $title = empty($title) ? Text::_('COM_CATEGORIES_SELECT_A_CATEGORY') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); + + // The current category display field. + $html = ''; + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + $html .= ''; + + // Select category button. + if ($allowSelect) { + $html .= '' + . ' ' . Text::_('JSELECT') + . ''; + } + + // New category button. + if ($allowNew) { + $html .= '' + . ' ' . Text::_('JACTION_CREATE') + . ''; + } + + // Edit category button. + if ($allowEdit) { + $html .= '' + . ' ' . Text::_('JACTION_EDIT') + . ''; + } + + // Clear category button. + if ($allowClear) { + $html .= '' + . ' ' . Text::_('JCLEAR') + . ''; + } + + // Propagate category button + if ($allowPropagate && \count($languages) > 2) { + // Strip off language tag at the end + $tagLength = (int) \strlen($this->element['language']); + $callbackFunctionStem = substr("jSelectCategory_" . $this->id, 0, -$tagLength); + + $html .= '' + . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') + . ''; + } + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + // Select category modal. + if ($allowSelect) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalSelect' . $modalId, + array( + 'title' => $modalTitle, + 'url' => $urlSelect, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ) + ); + } + + // New category modal. + if ($allowNew) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalNew' . $modalId, + array( + 'title' => Text::_('COM_CATEGORIES_NEW_CATEGORY'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlNew, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Edit category modal. + if ($allowEdit) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalEdit' . $modalId, + array( + 'title' => Text::_('COM_CATEGORIES_EDIT_CATEGORY'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlEdit, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Note: class='required' for client side validation + $class = $this->required ? ' class="required modal-value"' : ''; + + $html .= ''; + + return $html; + } + + /** + * Method to get the field label markup. + * + * @return string The field label markup. + * + * @since 3.7.0 + */ + protected function getLabel() + { + return str_replace($this->id, $this->id . '_name', parent::getLabel()); + } } diff --git a/administrator/components/com_categories/src/Helper/CategoriesHelper.php b/administrator/components/com_categories/src/Helper/CategoriesHelper.php index dfaacc58ae81c..753432eb44fb0 100644 --- a/administrator/components/com_categories/src/Helper/CategoriesHelper.php +++ b/administrator/components/com_categories/src/Helper/CategoriesHelper.php @@ -1,4 +1,5 @@ getAuthorisedViewLevels(); - - foreach ($langAssociations as $langAssociation) - { - // Include only published categories with user access - $arrId = explode(':', $langAssociation->id); - $assocId = (int) $arrId[0]; - $db = Factory::getDbo(); - - $query = $db->getQuery(true) - ->select($db->quoteName('published')) - ->from($db->quoteName('#__categories')) - ->whereIn($db->quoteName('access'), $groups) - ->where($db->quoteName('id') . ' = :associd') - ->bind(':associd', $assocId, ParameterType::INTEGER); - - $result = (int) $db->setQuery($query)->loadResult(); - - if ($result === 1) - { - $associations[$langAssociation->language] = $langAssociation->id; - } - } - - return $associations; - } - - /** - * Check if Category ID exists otherwise assign to ROOT category. - * - * @param mixed $catid Name or ID of category. - * @param string $extension Extension that triggers this function - * - * @return integer $catid Category ID. - */ - public static function validateCategoryId($catid, $extension) - { - $categoryTable = Table::getInstance('CategoryTable', '\\Joomla\\Component\\Categories\\Administrator\\Table\\'); - - $data = array(); - $data['id'] = $catid; - $data['extension'] = $extension; - - if (!$categoryTable->load($data)) - { - $catid = 0; - } - - return (int) $catid; - } - - /** - * Create new Category from within item view. - * - * @param array $data Array of data for new category. - * - * @return integer - */ - public static function createCategory($data) - { - $categoryModel = Factory::getApplication()->bootComponent('com_categories') - ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); - $categoryModel->save($data); - - $catid = $categoryModel->getState('category.id'); - - return $catid; - } + /** + * Gets a list of associations for a given item. + * + * @param integer $pk Content item key. + * @param string $extension Optional extension name. + * + * @return array of associations. + */ + public static function getAssociations($pk, $extension = 'com_content') + { + $langAssociations = Associations::getAssociations($extension, '#__categories', 'com_categories.item', $pk, 'id', 'alias', ''); + $associations = array(); + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + + foreach ($langAssociations as $langAssociation) { + // Include only published categories with user access + $arrId = explode(':', $langAssociation->id); + $assocId = (int) $arrId[0]; + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select($db->quoteName('published')) + ->from($db->quoteName('#__categories')) + ->whereIn($db->quoteName('access'), $groups) + ->where($db->quoteName('id') . ' = :associd') + ->bind(':associd', $assocId, ParameterType::INTEGER); + + $result = (int) $db->setQuery($query)->loadResult(); + + if ($result === 1) { + $associations[$langAssociation->language] = $langAssociation->id; + } + } + + return $associations; + } + + /** + * Check if Category ID exists otherwise assign to ROOT category. + * + * @param mixed $catid Name or ID of category. + * @param string $extension Extension that triggers this function + * + * @return integer $catid Category ID. + */ + public static function validateCategoryId($catid, $extension) + { + $categoryTable = Table::getInstance('CategoryTable', '\\Joomla\\Component\\Categories\\Administrator\\Table\\'); + + $data = array(); + $data['id'] = $catid; + $data['extension'] = $extension; + + if (!$categoryTable->load($data)) { + $catid = 0; + } + + return (int) $catid; + } + + /** + * Create new Category from within item view. + * + * @param array $data Array of data for new category. + * + * @return integer + */ + public static function createCategory($data) + { + $categoryModel = Factory::getApplication()->bootComponent('com_categories') + ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); + $categoryModel->save($data); + + $catid = $categoryModel->getState('category.id'); + + return $catid; + } } diff --git a/administrator/components/com_categories/src/Helper/CategoryAssociationHelper.php b/administrator/components/com_categories/src/Helper/CategoryAssociationHelper.php index 17ada8cf21648..b8357e0b910f4 100644 --- a/administrator/components/com_categories/src/Helper/CategoryAssociationHelper.php +++ b/administrator/components/com_categories/src/Helper/CategoryAssociationHelper.php @@ -1,4 +1,5 @@ $item) - { - if (class_exists($helperClassname) && \is_callable(array($helperClassname, 'getCategoryRoute'))) - { - $return[$tag] = $helperClassname::getCategoryRoute($item, $tag, $layout); - } - else - { - $viewLayout = $layout ? '&layout=' . $layout : ''; - - $return[$tag] = 'index.php?option=' . $extension . '&view=category&id=' . $item . $viewLayout; - } - } - } - - return $return; - } + /** + * Flag if associations are present for categories + * + * @var boolean + * @since 3.0 + */ + public static $category_association = true; + + /** + * Method to get the associations for a given category + * + * @param integer $id Id of the item + * @param string $extension Name of the component + * @param string|null $layout Category layout + * + * @return array Array of associations for the component categories + * + * @since 3.0 + */ + public static function getCategoryAssociations($id = 0, $extension = 'com_content', $layout = null) + { + $return = array(); + + if ($id) { + $helperClassname = ucfirst(substr($extension, 4)) . 'HelperRoute'; + + $associations = CategoriesHelper::getAssociations($id, $extension); + + foreach ($associations as $tag => $item) { + if (class_exists($helperClassname) && \is_callable(array($helperClassname, 'getCategoryRoute'))) { + $return[$tag] = $helperClassname::getCategoryRoute($item, $tag, $layout); + } else { + $viewLayout = $layout ? '&layout=' . $layout : ''; + + $return[$tag] = 'index.php?option=' . $extension . '&view=category&id=' . $item . $viewLayout; + } + } + } + + return $return; + } } diff --git a/administrator/components/com_categories/src/Model/CategoriesModel.php b/administrator/components/com_categories/src/Model/CategoriesModel.php index 5637b90d2446a..f0d1b84126eaa 100644 --- a/administrator/components/com_categories/src/Model/CategoriesModel.php +++ b/administrator/components/com_categories/src/Model/CategoriesModel.php @@ -1,4 +1,5 @@ input->get('forcedLanguage', '', 'cmd'); - - // Adjust the context to support modal layouts. - if ($layout = $app->input->get('layout')) - { - $this->context .= '.' . $layout; - } - - // Adjust the context to support forced languages. - if ($forcedLanguage) - { - $this->context .= '.' . $forcedLanguage; - } - - $extension = $app->getUserStateFromRequest($this->context . '.filter.extension', 'extension', 'com_content', 'cmd'); - - $this->setState('filter.extension', $extension); - $parts = explode('.', $extension); - - // Extract the component name - $this->setState('filter.component', $parts[0]); - - // Extract the optional section name - $this->setState('filter.section', (\count($parts) > 1) ? $parts[1] : null); - - // List state information. - parent::populateState($ordering, $direction); - - // Force a language. - if (!empty($forcedLanguage)) - { - $this->setState('filter.language', $forcedLanguage); - } - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.extension'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.language'); - $id .= ':' . $this->getState('filter.level'); - $id .= ':' . serialize($this->getState('filter.tag')); - - return parent::getStoreId($id); - } - - /** - * Method to get a database query to list categories. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.id, a.title, a.alias, a.note, a.published, a.access' . - ', a.checked_out, a.checked_out_time, a.created_user_id' . - ', a.path, a.parent_id, a.level, a.lft, a.rgt' . - ', a.language' - ) - ); - $query->from($db->quoteName('#__categories', 'a')); - - // Join over the language - $query->select( - [ - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image', 'language_image'), - ] - ) - ->join( - 'LEFT', - $db->quoteName('#__languages', 'l'), - $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language') - ); - - // Join over the users for the checked out user. - $query->select($db->quoteName('uc.name', 'editor')) - ->join( - 'LEFT', - $db->quoteName('#__users', 'uc'), - $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out') - ); - - // Join over the asset groups. - $query->select($db->quoteName('ag.title', 'access_level')) - ->join( - 'LEFT', - $db->quoteName('#__viewlevels', 'ag'), - $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access') - ); - - // Join over the users for the author. - $query->select($db->quoteName('ua.name', 'author_name')) - ->join( - 'LEFT', - $db->quoteName('#__users', 'ua'), - $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_user_id') - ); - - // Join over the associations. - $assoc = $this->getAssoc(); - - if ($assoc) - { - $query->select('COUNT(asso2.id)>1 as association') - ->join( - 'LEFT', - $db->quoteName('#__associations', 'asso'), - $db->quoteName('asso.id') . ' = ' . $db->quoteName('a.id') - . ' AND ' . $db->quoteName('asso.context') . ' = ' . $db->quote('com_categories.item') - ) - ->join( - 'LEFT', - $db->quoteName('#__associations', 'asso2'), - $db->quoteName('asso2.key') . ' = ' . $db->quoteName('asso.key') - ) - ->group('a.id, l.title, uc.name, ag.title, ua.name'); - } - - // Filter by extension - if ($extension = $this->getState('filter.extension')) - { - $query->where($db->quoteName('a.extension') . ' = :extension') - ->bind(':extension', $extension); - } - - // Filter on the level. - if ($level = (int) $this->getState('filter.level')) - { - $query->where($db->quoteName('a.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } - - // Filter by access level. - if ($access = (int) $this->getState('filter.access')) - { - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - - // Implement View Level Access - if (!$user->authorise('core.admin')) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('a.access'), $groups); - } - - // Filter by published state - $published = (string) $this->getState('filter.published'); - - if (is_numeric($published)) - { - $published = (int) $published; - $query->where($db->quoteName('a.published') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->whereIn($db->quoteName('a.published'), [0, 1]); - } - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :search') - ->bind(':search', $search, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.title') . ' LIKE :title', - $db->quoteName('a.alias') . ' LIKE :alias', - $db->quoteName('a.note') . ' LIKE :note', - ], - 'OR' - ) - ->bind(':title', $search) - ->bind(':alias', $search) - ->bind(':note', $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('a.language') . ' = :language') - ->bind(':language', $language); - } - - // Filter by a single or group of tags. - $tag = $this->getState('filter.tag'); - $typeAlias = $extension . '.category'; - - // Run simplified query when filtering by one tag. - if (\is_array($tag) && \count($tag) === 1) - { - $tag = $tag[0]; - } - - if ($tag && \is_array($tag)) - { - $tag = ArrayHelper::toInteger($tag); - - $subQuery = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('content_item_id')) - ->from($db->quoteName('#__contentitem_tag_map')) - ->where( - [ - $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', - $db->quoteName('type_alias') . ' = :typeAlias', - ] - ); - - $query->join( - 'INNER', - '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ) - ->bind(':typeAlias', $typeAlias); - } - elseif ($tag = (int) $tag) - { - $query->join( - 'INNER', - $db->quoteName('#__contentitem_tag_map', 'tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ) - ->where( - [ - $db->quoteName('tagmap.tag_id') . ' = :tag', - $db->quoteName('tagmap.type_alias') . ' = :typeAlias', - ] - ) - ->bind(':tag', $tag, ParameterType::INTEGER) - ->bind(':typeAlias', $typeAlias); - } - - // Add the list ordering clause - $listOrdering = $this->getState('list.ordering', 'a.lft'); - $listDirn = $db->escape($this->getState('list.direction', 'ASC')); - - if ($listOrdering == 'a.access') - { - $query->order('a.access ' . $listDirn . ', a.lft ' . $listDirn); - } - else - { - $query->order($db->escape($listOrdering) . ' ' . $listDirn); - } - - // Group by on Categories for \JOIN with component tables to count items - $query->group('a.id, + /** + * Does an association exist? Caches the result of getAssoc(). + * + * @var boolean|null + * @since 4.0.5 + */ + private $hasAssociation; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface|null $factory The factory. + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'alias', 'a.alias', + 'published', 'a.published', + 'access', 'a.access', 'access_level', + 'language', 'a.language', 'language_title', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'created_time', 'a.created_time', + 'created_user_id', 'a.created_user_id', + 'lft', 'a.lft', + 'rgt', 'a.rgt', + 'level', 'a.level', + 'path', 'a.path', + 'tag', + ); + } + + if (Associations::isEnabled()) { + $config['filter_fields'][] = 'association'; + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.lft', $direction = 'asc') + { + $app = Factory::getApplication(); + + $forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd'); + + // Adjust the context to support modal layouts. + if ($layout = $app->input->get('layout')) { + $this->context .= '.' . $layout; + } + + // Adjust the context to support forced languages. + if ($forcedLanguage) { + $this->context .= '.' . $forcedLanguage; + } + + $extension = $app->getUserStateFromRequest($this->context . '.filter.extension', 'extension', 'com_content', 'cmd'); + + $this->setState('filter.extension', $extension); + $parts = explode('.', $extension); + + // Extract the component name + $this->setState('filter.component', $parts[0]); + + // Extract the optional section name + $this->setState('filter.section', (\count($parts) > 1) ? $parts[1] : null); + + // List state information. + parent::populateState($ordering, $direction); + + // Force a language. + if (!empty($forcedLanguage)) { + $this->setState('filter.language', $forcedLanguage); + } + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.extension'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.language'); + $id .= ':' . $this->getState('filter.level'); + $id .= ':' . serialize($this->getState('filter.tag')); + + return parent::getStoreId($id); + } + + /** + * Method to get a database query to list categories. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.id, a.title, a.alias, a.note, a.published, a.access' . + ', a.checked_out, a.checked_out_time, a.created_user_id' . + ', a.path, a.parent_id, a.level, a.lft, a.rgt' . + ', a.language' + ) + ); + $query->from($db->quoteName('#__categories', 'a')); + + // Join over the language + $query->select( + [ + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image', 'language_image'), + ] + ) + ->join( + 'LEFT', + $db->quoteName('#__languages', 'l'), + $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language') + ); + + // Join over the users for the checked out user. + $query->select($db->quoteName('uc.name', 'editor')) + ->join( + 'LEFT', + $db->quoteName('#__users', 'uc'), + $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out') + ); + + // Join over the asset groups. + $query->select($db->quoteName('ag.title', 'access_level')) + ->join( + 'LEFT', + $db->quoteName('#__viewlevels', 'ag'), + $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access') + ); + + // Join over the users for the author. + $query->select($db->quoteName('ua.name', 'author_name')) + ->join( + 'LEFT', + $db->quoteName('#__users', 'ua'), + $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_user_id') + ); + + // Join over the associations. + $assoc = $this->getAssoc(); + + if ($assoc) { + $query->select('COUNT(asso2.id)>1 as association') + ->join( + 'LEFT', + $db->quoteName('#__associations', 'asso'), + $db->quoteName('asso.id') . ' = ' . $db->quoteName('a.id') + . ' AND ' . $db->quoteName('asso.context') . ' = ' . $db->quote('com_categories.item') + ) + ->join( + 'LEFT', + $db->quoteName('#__associations', 'asso2'), + $db->quoteName('asso2.key') . ' = ' . $db->quoteName('asso.key') + ) + ->group('a.id, l.title, uc.name, ag.title, ua.name'); + } + + // Filter by extension + if ($extension = $this->getState('filter.extension')) { + $query->where($db->quoteName('a.extension') . ' = :extension') + ->bind(':extension', $extension); + } + + // Filter on the level. + if ($level = (int) $this->getState('filter.level')) { + $query->where($db->quoteName('a.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } + + // Filter by access level. + if ($access = (int) $this->getState('filter.access')) { + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + + // Implement View Level Access + if (!$user->authorise('core.admin')) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('a.access'), $groups); + } + + // Filter by published state + $published = (string) $this->getState('filter.published'); + + if (is_numeric($published)) { + $published = (int) $published; + $query->where($db->quoteName('a.published') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->whereIn($db->quoteName('a.published'), [0, 1]); + } + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :search') + ->bind(':search', $search, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.title') . ' LIKE :title', + $db->quoteName('a.alias') . ' LIKE :alias', + $db->quoteName('a.note') . ' LIKE :note', + ], + 'OR' + ) + ->bind(':title', $search) + ->bind(':alias', $search) + ->bind(':note', $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('a.language') . ' = :language') + ->bind(':language', $language); + } + + // Filter by a single or group of tags. + $tag = $this->getState('filter.tag'); + $typeAlias = $extension . '.category'; + + // Run simplified query when filtering by one tag. + if (\is_array($tag) && \count($tag) === 1) { + $tag = $tag[0]; + } + + if ($tag && \is_array($tag)) { + $tag = ArrayHelper::toInteger($tag); + + $subQuery = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('content_item_id')) + ->from($db->quoteName('#__contentitem_tag_map')) + ->where( + [ + $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', + $db->quoteName('type_alias') . ' = :typeAlias', + ] + ); + + $query->join( + 'INNER', + '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ) + ->bind(':typeAlias', $typeAlias); + } elseif ($tag = (int) $tag) { + $query->join( + 'INNER', + $db->quoteName('#__contentitem_tag_map', 'tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ) + ->where( + [ + $db->quoteName('tagmap.tag_id') . ' = :tag', + $db->quoteName('tagmap.type_alias') . ' = :typeAlias', + ] + ) + ->bind(':tag', $tag, ParameterType::INTEGER) + ->bind(':typeAlias', $typeAlias); + } + + // Add the list ordering clause + $listOrdering = $this->getState('list.ordering', 'a.lft'); + $listDirn = $db->escape($this->getState('list.direction', 'ASC')); + + if ($listOrdering == 'a.access') { + $query->order('a.access ' . $listDirn . ', a.lft ' . $listDirn); + } else { + $query->order($db->escape($listOrdering) . ' ' . $listDirn); + } + + // Group by on Categories for \JOIN with component tables to count items + $query->group('a.id, a.title, a.alias, a.note, @@ -395,125 +369,118 @@ protected function getListQuery() l.image, uc.name, ag.title, - ua.name' - ); + ua.name'); - return $query; - } + return $query; + } - /** - * Method to determine if an association exists - * - * @return boolean True if the association exists - * - * @since 3.0 - */ - public function getAssoc() - { - if (!\is_null($this->hasAssociation)) - { - return $this->hasAssociation; - } + /** + * Method to determine if an association exists + * + * @return boolean True if the association exists + * + * @since 3.0 + */ + public function getAssoc() + { + if (!\is_null($this->hasAssociation)) { + return $this->hasAssociation; + } - $extension = $this->getState('filter.extension'); + $extension = $this->getState('filter.extension'); - $this->hasAssociation = Associations::isEnabled(); - $extension = explode('.', $extension); - $component = array_shift($extension); - $cname = str_replace('com_', '', $component); + $this->hasAssociation = Associations::isEnabled(); + $extension = explode('.', $extension); + $component = array_shift($extension); + $cname = str_replace('com_', '', $component); - if (!$this->hasAssociation || !$component || !$cname) - { - $this->hasAssociation = false; + if (!$this->hasAssociation || !$component || !$cname) { + $this->hasAssociation = false; - return $this->hasAssociation; - } + return $this->hasAssociation; + } - $componentObject = $this->bootComponent($component); + $componentObject = $this->bootComponent($component); - if ($componentObject instanceof AssociationServiceInterface && $componentObject instanceof CategoryServiceInterface) - { - $this->hasAssociation = true; + if ($componentObject instanceof AssociationServiceInterface && $componentObject instanceof CategoryServiceInterface) { + $this->hasAssociation = true; - return $this->hasAssociation; - } + return $this->hasAssociation; + } - $hname = $cname . 'HelperAssociation'; - \JLoader::register($hname, JPATH_SITE . '/components/' . $component . '/helpers/association.php'); + $hname = $cname . 'HelperAssociation'; + \JLoader::register($hname, JPATH_SITE . '/components/' . $component . '/helpers/association.php'); /* @codingStandardsIgnoreStart */ $this->hasAssociation = class_exists($hname) && !empty($hname::$category_association); /* @codingStandardsIgnoreEnd */ - return $this->hasAssociation; - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 3.0.1 - */ - public function getItems() - { - $items = parent::getItems(); - - if ($items != false) - { - $extension = $this->getState('filter.extension'); - - $this->countItems($items, $extension); - } - - return $items; - } - - /** - * Method to load the countItems method from the extensions - * - * @param \stdClass[] $items The category items - * @param string $extension The category extension - * - * @return void - * - * @since 3.5 - */ - public function countItems(&$items, $extension) - { - $parts = explode('.', $extension, 2); - $section = ''; - - if (\count($parts) > 1) - { - $section = $parts[1]; - } - - $component = Factory::getApplication()->bootComponent($parts[0]); - - if ($component instanceof CategoryServiceInterface) - { - $component->countItems($items, $section); - } - } - - /** - * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. - * - * @return DatabaseQuery - * - * @since 4.0.0 - */ - protected function getEmptyStateQuery() - { - $query = parent::getEmptyStateQuery(); - - // Get the extension from the filter - $extension = $this->getState('filter.extension'); - - $query->where($this->getDatabase()->quoteName('extension') . ' = :extension') - ->bind(':extension', $extension); - - return $query; - } + return $this->hasAssociation; + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 3.0.1 + */ + public function getItems() + { + $items = parent::getItems(); + + if ($items != false) { + $extension = $this->getState('filter.extension'); + + $this->countItems($items, $extension); + } + + return $items; + } + + /** + * Method to load the countItems method from the extensions + * + * @param \stdClass[] $items The category items + * @param string $extension The category extension + * + * @return void + * + * @since 3.5 + */ + public function countItems(&$items, $extension) + { + $parts = explode('.', $extension, 2); + $section = ''; + + if (\count($parts) > 1) { + $section = $parts[1]; + } + + $component = Factory::getApplication()->bootComponent($parts[0]); + + if ($component instanceof CategoryServiceInterface) { + $component->countItems($items, $section); + } + } + + /** + * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. + * + * @return DatabaseQuery + * + * @since 4.0.0 + */ + protected function getEmptyStateQuery() + { + $query = parent::getEmptyStateQuery(); + + // Get the extension from the filter + $extension = $this->getState('filter.extension'); + + $query->where($this->getDatabase()->quoteName('extension') . ' = :extension') + ->bind(':extension', $extension); + + return $query; + } } diff --git a/administrator/components/com_categories/src/Model/CategoryModel.php b/administrator/components/com_categories/src/Model/CategoryModel.php index 3cd6e49238f94..784440f71c07d 100644 --- a/administrator/components/com_categories/src/Model/CategoryModel.php +++ b/administrator/components/com_categories/src/Model/CategoryModel.php @@ -1,4 +1,5 @@ input->get('extension', 'com_content'); - $this->typeAlias = $extension . '.category'; - - // Add a new batch command - $this->batch_commands['flip_ordering'] = 'batchFlipordering'; - - parent::__construct($config, $factory); - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->published != -2) - { - return false; - } - - return Factory::getUser()->authorise('core.delete', $record->extension . '.category.' . (int) $record->id); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - - // Check for existing category. - if (!empty($record->id)) - { - return $user->authorise('core.edit.state', $record->extension . '.category.' . (int) $record->id); - } - - // New category, so check against the parent. - if (!empty($record->parent_id)) - { - return $user->authorise('core.edit.state', $record->extension . '.category.' . (int) $record->parent_id); - } - - // Default to component settings if neither category nor parent known. - return $user->authorise('core.edit.state', $record->extension); - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $type The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return \Joomla\CMS\Table\Table A Table object - * - * @since 1.6 - */ - public function getTable($type = 'Category', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } - - /** - * Auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - $parentId = $app->input->getInt('parent_id'); - $this->setState('category.parent_id', $parentId); - - // Load the User state. - $pk = $app->input->getInt('id'); - $this->setState($this->getName() . '.id', $pk); - - $extension = $app->input->get('extension', 'com_content'); - $this->setState('category.extension', $extension); - $parts = explode('.', $extension); - - // Extract the component name - $this->setState('category.component', $parts[0]); - - // Extract the optional section name - $this->setState('category.section', (\count($parts) > 1) ? $parts[1] : null); - - // Load the parameters. - $params = ComponentHelper::getParams('com_categories'); - $this->setState('params', $params); - } - - /** - * Method to get a category. - * - * @param integer $pk An optional id of the object to get, otherwise the id from the model state is used. - * - * @return mixed Category data object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - if ($result = parent::getItem($pk)) - { - // Prime required properties. - if (empty($result->id)) - { - $result->parent_id = $this->getState('category.parent_id'); - $result->extension = $this->getState('category.extension'); - } - - // Convert the metadata field to an array. - $registry = new Registry($result->metadata); - $result->metadata = $registry->toArray(); - - if (!empty($result->id)) - { - $result->tags = new TagsHelper; - $result->tags->getTagIds($result->id, $result->extension . '.category'); - } - } - - $assoc = $this->getAssoc(); - - if ($assoc) - { - if ($result->id != null) - { - $result->associations = ArrayHelper::toInteger(CategoriesHelper::getAssociations($result->id, $result->extension)); - } - else - { - $result->associations = array(); - } - } - - return $result; - } - - /** - * Method to get the row form. - * - * @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 Form|boolean A JForm object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - $extension = $this->getState('category.extension'); - $jinput = Factory::getApplication()->input; - - // A workaround to get the extension into the model for save requests. - if (empty($extension) && isset($data['extension'])) - { - $extension = $data['extension']; - $parts = explode('.', $extension); - - $this->setState('category.extension', $extension); - $this->setState('category.component', $parts[0]); - $this->setState('category.section', @$parts[1]); - } - - // Get the form. - $form = $this->loadForm('com_categories.category' . $extension, 'category', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Modify the form based on Edit State access controls. - if (empty($data['extension'])) - { - $data['extension'] = $extension; - } - - $categoryId = $jinput->get('id'); - $parts = explode('.', $extension); - $assetKey = $categoryId ? $extension . '.category.' . $categoryId : $parts[0]; - - if (!Factory::getUser()->authorise('core.edit.state', $assetKey)) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('published', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - // Don't allow to change the created_user_id user if not allowed to access com_users. - if (!Factory::getUser()->authorise('core.manage', 'com_users')) - { - $form->setFieldAttribute('created_user_id', 'filter', 'unset'); - } - - return $form; - } - - /** - * A protected method to get the where clause for the reorder - * This ensures that the row will be moved relative to a row with the same extension - * - * @param Category $table Current table instance - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - $db = $this->getDatabase(); - - return [ - $db->quoteName('extension') . ' = ' . $db->quote($table->extension), - ]; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $app = Factory::getApplication(); - $data = $app->getUserState('com_categories.edit.' . $this->getName() . '.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Pre-select some filters (Status, Language, Access) in edit form if those have been selected in Category Manager - if (!$data->id) - { - // Check for which extension the Category Manager is used and get selected fields - $extension = substr($app->getUserState('com_categories.categories.filter.extension', ''), 4); - $filters = (array) $app->getUserState('com_categories.categories.' . $extension . '.filter'); - - $data->set( - 'published', - $app->input->getInt( - 'published', - ((isset($filters['published']) && $filters['published'] !== '') ? $filters['published'] : null) - ) - ); - $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); - $data->set( - 'access', - $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) - ); - } - } - - $this->preprocessData('com_categories.category', $data); - - return $data; - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @see JFormRule - * @see JFilterInput - * @since 3.9.23 - */ - public function validate($form, $data, $group = null) - { - if (!Factory::getUser()->authorise('core.admin', $data['extension'])) - { - if (isset($data['rules'])) - { - unset($data['rules']); - } - } - - return parent::validate($form, $data, $group); - } - - /** - * Method to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import. - * - * @return mixed - * - * @since 1.6 - * - * @throws \Exception if there is an error in the form event. - * - * @see \Joomla\CMS\Form\FormField - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $lang = Factory::getLanguage(); - $component = $this->getState('category.component'); - $section = $this->getState('category.section'); - $extension = Factory::getApplication()->input->get('extension', null); - - // Get the component form if it exists - $name = 'category' . ($section ? ('.' . $section) : ''); - - // Looking first in the component forms folder - $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/forms/$name.xml"); - - // Looking in the component models/forms folder (J! 3) - if (!file_exists($path)) - { - $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/models/forms/$name.xml"); - } - - // Old way: looking in the component folder - if (!file_exists($path)) - { - $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/$name.xml"); - } - - if (file_exists($path)) - { - $lang->load($component, JPATH_BASE); - $lang->load($component, JPATH_BASE . '/components/' . $component); - - if (!$form->loadFile($path, false)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - } - - $componentInterface = Factory::getApplication()->bootComponent($component); - - if ($componentInterface instanceof CategoryServiceInterface) - { - $componentInterface->prepareForm($form, $data); - } - else - { - // Try to find the component helper. - $eName = str_replace('com_', '', $component); - $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/helpers/category.php"); - - if (file_exists($path)) - { - $cName = ucfirst($eName) . ucfirst($section) . 'HelperCategory'; - - \JLoader::register($cName, $path); - - if (class_exists($cName) && \is_callable(array($cName, 'onPrepareForm'))) - { - $lang->load($component, JPATH_BASE, null, false, false) - || $lang->load($component, JPATH_BASE . '/components/' . $component, null, false, false) - || $lang->load($component, JPATH_BASE, $lang->getDefault(), false, false) - || $lang->load($component, JPATH_BASE . '/components/' . $component, $lang->getDefault(), false, false); - \call_user_func_array(array($cName, 'onPrepareForm'), array(&$form)); - - // Check for an error. - if ($form instanceof \Exception) - { - $this->setError($form->getMessage()); - - return false; - } - } - } - } - - // Set the access control rules field component value. - $form->setFieldAttribute('rules', 'component', $component); - $form->setFieldAttribute('rules', 'section', $name); - - // Association category items - if ($this->getAssoc()) - { - $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); - - if (\count($languages) > 1) - { - $addform = new \SimpleXMLElement('
'); - $fields = $addform->addChild('fields'); - $fields->addAttribute('name', 'associations'); - $fieldset = $fields->addChild('fieldset'); - $fieldset->addAttribute('name', 'item_associations'); - - foreach ($languages as $language) - { - $field = $fieldset->addChild('field'); - $field->addAttribute('name', $language->lang_code); - $field->addAttribute('type', 'modal_category'); - $field->addAttribute('language', $language->lang_code); - $field->addAttribute('label', $language->title); - $field->addAttribute('translate_label', 'false'); - $field->addAttribute('extension', $extension); - $field->addAttribute('select', 'true'); - $field->addAttribute('new', 'true'); - $field->addAttribute('edit', 'true'); - $field->addAttribute('clear', 'true'); - $field->addAttribute('propagate', 'true'); - } - - $form->load($addform, false); - } - } - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $table = $this->getTable(); - $input = Factory::getApplication()->input; - $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState($this->getName() . '.id'); - $isNew = true; - $context = $this->option . '.' . $this->name; - - if (!empty($data['tags']) && $data['tags'][0] != '') - { - $table->newTags = $data['tags']; - } - - // Include the plugins for the save events. - PluginHelper::importPlugin($this->events_map['save']); - - // Load the row if saving an existing category. - if ($pk > 0) - { - $table->load($pk); - $isNew = false; - } - - // Set the new parent id if parent id not matched OR while New/Save as Copy . - if ($table->parent_id != $data['parent_id'] || $data['id'] == 0) - { - $table->setLocation($data['parent_id'], 'last-child'); - } - - // Alter the title for save as copy - if ($input->get('task') == 'save2copy') - { - $origTable = clone $this->getTable(); - $origTable->load($input->getInt('id')); - - if ($data['title'] == $origTable->title) - { - [$title, $alias] = $this->generateNewTitle($data['parent_id'], $data['alias'], $data['title']); - $data['title'] = $title; - $data['alias'] = $alias; - } - else - { - if ($data['alias'] == $origTable->alias) - { - $data['alias'] = ''; - } - } - - $data['published'] = 0; - } - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Bind the rules. - if (isset($data['rules'])) - { - $rules = new Rules($data['rules']); - $table->setRules($rules); - } - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, $isNew, $data)); - - if (\in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - // Store the data. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - $assoc = $this->getAssoc(); - - if ($assoc) - { - // Adding self to the association - $associations = $data['associations'] ?? array(); - - // Unset any invalid associations - $associations = ArrayHelper::toInteger($associations); - - foreach ($associations as $tag => $id) - { - if (!$id) - { - unset($associations[$tag]); - } - } - - // Detecting all item menus - $allLanguage = $table->language == '*'; - - if ($allLanguage && !empty($associations)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CATEGORIES_ERROR_ALL_LANGUAGE_ASSOCIATED'), 'notice'); - } - - // Get associationskey for edited item - $db = $this->getDatabase(); - $id = (int) $table->id; - $query = $db->getQuery(true) - ->select($db->quoteName('key')) - ->from($db->quoteName('#__associations')) - ->where($db->quoteName('context') . ' = :associationscontext') - ->where($db->quoteName('id') . ' = :id') - ->bind(':associationscontext', $this->associationsContext) - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $oldKey = $db->loadResult(); - - if ($associations || $oldKey !== null) - { - $where = []; - - // Deleting old associations for the associated items - $query = $db->getQuery(true) - ->delete($db->quoteName('#__associations')) - ->where($db->quoteName('context') . ' = :associationscontext') - ->bind(':associationscontext', $this->associationsContext); - - if ($associations) - { - $where[] = $db->quoteName('id') . ' IN (' . implode(',', $query->bindArray(array_values($associations))) . ')'; - } - - if ($oldKey !== null) - { - $where[] = $db->quoteName('key') . ' = :oldKey'; - $query->bind(':oldKey', $oldKey); - } - - $query->extendWhere('AND', $where, 'OR'); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Adding self to the association - if (!$allLanguage) - { - $associations[$table->language] = (int) $table->id; - } - - if (\count($associations) > 1) - { - // Adding new association for these items - $key = md5(json_encode($associations)); - $query->clear() - ->insert($db->quoteName('#__associations')) - ->columns( - [ - $db->quoteName('id'), - $db->quoteName('context'), - $db->quoteName('key'), - ] - ); - - foreach ($associations as $id) - { - $id = (int) $id; - - $query->values( - implode( - ',', - $query->bindArray( - [$id, $this->associationsContext, $key], - [ParameterType::INTEGER, ParameterType::STRING, ParameterType::STRING] - ) - ) - ); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - } - - // Trigger the after save event. - Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, $isNew, $data)); - - // Rebuild the path for the category: - if (!$table->rebuildPath($table->id)) - { - $this->setError($table->getError()); - - return false; - } - - // Rebuild the paths of the category's children: - if (!$table->rebuild($table->id, $table->lft, $table->level, $table->path)) - { - $this->setError($table->getError()); - - return false; - } - - $this->setState($this->getName() . '.id', $table->id); - - if (Factory::getApplication()->input->get('task') == 'editAssociations') - { - return $this->redirectToAssociations($data); - } - - // Clear the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to change the published state of one or more records. - * - * @param array $pks A list of the primary keys to change. - * @param integer $value The value of the published state. - * - * @return boolean True on success. - * - * @since 2.5 - */ - public function publish(&$pks, $value = 1) - { - if (parent::publish($pks, $value)) - { - $extension = Factory::getApplication()->input->get('extension'); - - // Include the content plugins for the change of category state event. - PluginHelper::importPlugin('content'); - - // Trigger the onCategoryChangeState event. - Factory::getApplication()->triggerEvent('onCategoryChangeState', array($extension, $pks, $value)); - - return true; - } - } - - /** - * Method rebuild the entire nested set tree. - * - * @return boolean False on failure or error, true otherwise. - * - * @since 1.6 - */ - public function rebuild() - { - // Get an instance of the table object. - $table = $this->getTable(); - - if (!$table->rebuild()) - { - $this->setError($table->getError()); - - return false; - } - - // Clear the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to save the reordered nested set tree. - * First we save the new order values in the lft values of the changed ids. - * Then we invoke the table rebuild to implement the new ordering. - * - * @param array $idArray An array of primary key ids. - * @param integer $lftArray The lft value - * - * @return boolean False on failure or error, True otherwise - * - * @since 1.6 - */ - public function saveorder($idArray = null, $lftArray = null) - { - // Get an instance of the table object. - $table = $this->getTable(); - - if (!$table->saveorder($idArray, $lftArray)) - { - $this->setError($table->getError()); - - return false; - } - - // Clear the cache - $this->cleanCache(); - - return true; - } - - /** - * Batch flip category ordering. - * - * @param integer $value The new category. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return mixed An array of new IDs on success, boolean false on failure. - * - * @since 3.6.3 - */ - protected function batchFlipordering($value, $pks, $contexts) - { - $successful = array(); - - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - /** - * For each category get the max ordering value - * Re-order with max - ordering - */ - foreach ($pks as $id) - { - $query->select('MAX(' . $db->quoteName('ordering') . ')') - ->from($db->quoteName('#__content')) - ->where($db->quoteName('catid') . ' = :catid') - ->bind(':catid', $id, ParameterType::INTEGER); - - $db->setQuery($query); - - $max = (int) $db->loadResult(); - $max++; - - $query->clear(); - - $query->update($db->quoteName('#__content')) - ->set($db->quoteName('ordering') . ' = :max - ' . $db->quoteName('ordering')) - ->where($db->quoteName('catid') . ' = :catid') - ->bind(':max', $max, ParameterType::INTEGER) - ->bind(':catid', $id, ParameterType::INTEGER); - - $db->setQuery($query); - - if ($db->execute()) - { - $successful[] = $id; - } - } - - return empty($successful) ? false : $successful; - } - - /** - * Batch copy categories to a new category. - * - * @param integer $value The new category. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return mixed An array of new IDs on success, boolean false on failure. - * - * @since 1.6 - */ - protected function batchCopy($value, $pks, $contexts) - { - $type = new UCMType; - $this->type = $type->getTypeByAlias($this->typeAlias); - - // $value comes as {parent_id}.{extension} - $parts = explode('.', $value); - $parentId = (int) ArrayHelper::getValue($parts, 0, 1); - - $db = $this->getDatabase(); - $extension = Factory::getApplication()->input->get('extension', '', 'word'); - $newIds = array(); - - // Check that the parent exists - if ($parentId) - { - if (!$this->table->load($parentId)) - { - if ($error = $this->table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Non-fatal error - $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); - $parentId = 0; - } - } - - // Check that user has create permission for parent category - if ($parentId == $this->table->getRootId()) - { - $canCreate = $this->user->authorise('core.create', $extension); - } - else - { - $canCreate = $this->user->authorise('core.create', $extension . '.category.' . $parentId); - } - - if (!$canCreate) - { - // Error since user cannot create in parent category - $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_CREATE')); - - return false; - } - } - - // If the parent is 0, set it to the ID of the root item in the tree - if (empty($parentId)) - { - if (!$parentId = $this->table->getRootId()) - { - $this->setError($this->table->getError()); - - return false; - } - // Make sure we can create in root - elseif (!$this->user->authorise('core.create', $extension)) - { - $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_CREATE')); - - return false; - } - } - - // We need to log the parent ID - $parents = array(); - - // Calculate the emergency stop count as a precaution against a runaway loop bug - $query = $db->getQuery(true) - ->select('COUNT(' . $db->quoteName('id') . ')') - ->from($db->quoteName('#__categories')); - $db->setQuery($query); - - try - { - $count = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Parent exists so let's proceed - while (!empty($pks) && $count > 0) - { - // Pop the first id off the stack - $pk = array_shift($pks); - - $this->table->reset(); - - // Check that the row actually exists - if (!$this->table->load($pk)) - { - if ($error = $this->table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Not fatal error - $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); - continue; - } - } - - // Copy is a bit tricky, because we also need to copy the children - $lft = (int) $this->table->lft; - $rgt = (int) $this->table->rgt; - $query->clear() - ->select($db->quoteName('id')) - ->from($db->quoteName('#__categories')) - ->where($db->quoteName('lft') . ' > :lft') - ->where($db->quoteName('rgt') . ' < :rgt') - ->bind(':lft', $lft, ParameterType::INTEGER) - ->bind(':rgt', $rgt, ParameterType::INTEGER); - $db->setQuery($query); - $childIds = $db->loadColumn(); - - // Add child ID's to the array only if they aren't already there. - foreach ($childIds as $childId) - { - if (!\in_array($childId, $pks)) - { - $pks[] = $childId; - } - } - - // Make a copy of the old ID, Parent ID and Asset ID - $oldId = $this->table->id; - $oldParentId = $this->table->parent_id; - $oldAssetId = $this->table->asset_id; - - // Reset the id because we are making a copy. - $this->table->id = 0; - - // If we a copying children, the Old ID will turn up in the parents list - // otherwise it's a new top level item - $this->table->parent_id = $parents[$oldParentId] ?? $parentId; - - // Set the new location in the tree for the node. - $this->table->setLocation($this->table->parent_id, 'last-child'); - - // @TODO: Deal with ordering? - // $this->table->ordering = 1; - $this->table->level = null; - $this->table->asset_id = null; - $this->table->lft = null; - $this->table->rgt = null; - - // Alter the title & alias - [$title, $alias] = $this->generateNewTitle($this->table->parent_id, $this->table->alias, $this->table->title); - $this->table->title = $title; - $this->table->alias = $alias; - - // Unpublish because we are making a copy - $this->table->published = 0; - - // Store the row. - if (!$this->table->store()) - { - $this->setError($this->table->getError()); - - return false; - } - - // Get the new item ID - $newId = $this->table->get('id'); - - // Add the new ID to the array - $newIds[$pk] = $newId; - - // Copy rules - $query->clear() - ->update($db->quoteName('#__assets', 't')) - ->join('INNER', - $db->quoteName('#__assets', 's'), - $db->quoteName('s.id') . ' = :oldid' - ) - ->bind(':oldid', $oldAssetId, ParameterType::INTEGER) - ->set($db->quoteName('t.rules') . ' = ' . $db->quoteName('s.rules')) - ->where($db->quoteName('t.id') . ' = :assetid') - ->bind(':assetid', $this->table->asset_id, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - - // Now we log the old 'parent' to the new 'parent' - $parents[$oldId] = $this->table->id; - $count--; - } - - // Rebuild the hierarchy. - if (!$this->table->rebuild()) - { - $this->setError($this->table->getError()); - - return false; - } - - // Rebuild the tree path. - if (!$this->table->rebuildPath($this->table->id)) - { - $this->setError($this->table->getError()); - - return false; - } - - return $newIds; - } - - /** - * Batch move categories to a new category. - * - * @param integer $value The new category ID. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True on success. - * - * @since 1.6 - */ - protected function batchMove($value, $pks, $contexts) - { - $parentId = (int) $value; - $type = new UCMType; - $this->type = $type->getTypeByAlias($this->typeAlias); - - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $extension = Factory::getApplication()->input->get('extension', '', 'word'); - - // Check that the parent exists. - if ($parentId) - { - if (!$this->table->load($parentId)) - { - if ($error = $this->table->getError()) - { - // Fatal error. - $this->setError($error); - - return false; - } - else - { - // Non-fatal error. - $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); - $parentId = 0; - } - } - - // Check that user has create permission for parent category. - if ($parentId == $this->table->getRootId()) - { - $canCreate = $this->user->authorise('core.create', $extension); - } - else - { - $canCreate = $this->user->authorise('core.create', $extension . '.category.' . $parentId); - } - - if (!$canCreate) - { - // Error since user cannot create in parent category - $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_CREATE')); - - return false; - } - - // Check that user has edit permission for every category being moved - // Note that the entire batch operation fails if any category lacks edit permission - foreach ($pks as $pk) - { - if (!$this->user->authorise('core.edit', $extension . '.category.' . $pk)) - { - // Error since user cannot edit this category - $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_EDIT')); - - return false; - } - } - } - - // We are going to store all the children and just move the category - $children = array(); - - // Parent exists so let's proceed - foreach ($pks as $pk) - { - // Check that the row actually exists - if (!$this->table->load($pk)) - { - if ($error = $this->table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Not fatal error - $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); - continue; - } - } - - // Set the new location in the tree for the node. - $this->table->setLocation($parentId, 'last-child'); - - // Check if we are moving to a different parent - if ($parentId != $this->table->parent_id) - { - $lft = (int) $this->table->lft; - $rgt = (int) $this->table->rgt; - - // Add the child node ids to the children array. - $query->clear() - ->select($db->quoteName('id')) - ->from($db->quoteName('#__categories')) - ->where($db->quoteName('lft') . ' BETWEEN :lft AND :rgt') - ->bind(':lft', $lft, ParameterType::INTEGER) - ->bind(':rgt', $rgt, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $children = array_merge($children, (array) $db->loadColumn()); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - // Store the row. - if (!$this->table->store()) - { - $this->setError($this->table->getError()); - - return false; - } - - // Rebuild the tree path. - if (!$this->table->rebuildPath()) - { - $this->setError($this->table->getError()); - - return false; - } - } - - // Process the child rows - if (!empty($children)) - { - // Remove any duplicates and sanitize ids. - $children = array_unique($children); - $children = ArrayHelper::toInteger($children); - } - - return true; - } - - /** - * Custom clean the cache of com_content and content modules - * - * @param string $group Cache group name. - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - $extension = Factory::getApplication()->input->get('extension'); - - switch ($extension) - { - case 'com_content': - parent::cleanCache('com_content'); - parent::cleanCache('mod_articles_archive'); - parent::cleanCache('mod_articles_categories'); - parent::cleanCache('mod_articles_category'); - parent::cleanCache('mod_articles_latest'); - parent::cleanCache('mod_articles_news'); - parent::cleanCache('mod_articles_popular'); - break; - default: - parent::cleanCache($extension); - break; - } - } - - /** - * Method to change the title & alias. - * - * @param integer $parentId The id of the parent. - * @param string $alias The alias. - * @param string $title The title. - * - * @return array Contains the modified title and alias. - * - * @since 1.7 - */ - protected function generateNewTitle($parentId, $alias, $title) - { - // Alter the title & alias - $table = $this->getTable(); - - while ($table->load(array('alias' => $alias, 'parent_id' => $parentId))) - { - $title = StringHelper::increment($title); - $alias = StringHelper::increment($alias, 'dash'); - } - - return array($title, $alias); - } - - /** - * Method to determine if a category association is available. - * - * @return boolean True if a category association is available; false otherwise. - */ - public function getAssoc() - { - if (!\is_null($this->hasAssociation)) - { - return $this->hasAssociation; - } - - $extension = $this->getState('category.extension', ''); - - $this->hasAssociation = Associations::isEnabled(); - $extension = explode('.', $extension); - $component = array_shift($extension); - $cname = str_replace('com_', '', $component); - - if (!$this->hasAssociation || !$component || !$cname) - { - $this->hasAssociation = false; - - return $this->hasAssociation; - } - - $componentObject = $this->bootComponent($component); - - if ($componentObject instanceof AssociationServiceInterface && $componentObject instanceof CategoryServiceInterface) - { - $this->hasAssociation = true; - - return $this->hasAssociation; - } - - $hname = $cname . 'HelperAssociation'; - \JLoader::register($hname, JPATH_SITE . '/components/' . $component . '/helpers/association.php'); - - $this->hasAssociation = class_exists($hname) && !empty($hname::$category_association); - - return $this->hasAssociation; - } + use VersionableModelTrait; + + /** + * The prefix to use with controller messages. + * + * @var string + * @since 1.6 + */ + protected $text_prefix = 'COM_CATEGORIES'; + + /** + * The type alias for this content type. Used for content version history. + * + * @var string + * @since 3.2 + */ + public $typeAlias = null; + + /** + * The context used for the associations table + * + * @var string + * @since 3.4.4 + */ + protected $associationsContext = 'com_categories.item'; + + /** + * Does an association exist? Caches the result of getAssoc(). + * + * @var boolean|null + * @since 3.10.4 + */ + private $hasAssociation; + + /** + * Override parent constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface|null $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + $extension = Factory::getApplication()->input->get('extension', 'com_content'); + $this->typeAlias = $extension . '.category'; + + // Add a new batch command + $this->batch_commands['flip_ordering'] = 'batchFlipordering'; + + parent::__construct($config, $factory); + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->published != -2) { + return false; + } + + return Factory::getUser()->authorise('core.delete', $record->extension . '.category.' . (int) $record->id); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + + // Check for existing category. + if (!empty($record->id)) { + return $user->authorise('core.edit.state', $record->extension . '.category.' . (int) $record->id); + } + + // New category, so check against the parent. + if (!empty($record->parent_id)) { + return $user->authorise('core.edit.state', $record->extension . '.category.' . (int) $record->parent_id); + } + + // Default to component settings if neither category nor parent known. + return $user->authorise('core.edit.state', $record->extension); + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $type The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\Table\Table A Table object + * + * @since 1.6 + */ + public function getTable($type = 'Category', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } + + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + $parentId = $app->input->getInt('parent_id'); + $this->setState('category.parent_id', $parentId); + + // Load the User state. + $pk = $app->input->getInt('id'); + $this->setState($this->getName() . '.id', $pk); + + $extension = $app->input->get('extension', 'com_content'); + $this->setState('category.extension', $extension); + $parts = explode('.', $extension); + + // Extract the component name + $this->setState('category.component', $parts[0]); + + // Extract the optional section name + $this->setState('category.section', (\count($parts) > 1) ? $parts[1] : null); + + // Load the parameters. + $params = ComponentHelper::getParams('com_categories'); + $this->setState('params', $params); + } + + /** + * Method to get a category. + * + * @param integer $pk An optional id of the object to get, otherwise the id from the model state is used. + * + * @return mixed Category data object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + if ($result = parent::getItem($pk)) { + // Prime required properties. + if (empty($result->id)) { + $result->parent_id = $this->getState('category.parent_id'); + $result->extension = $this->getState('category.extension'); + } + + // Convert the metadata field to an array. + $registry = new Registry($result->metadata); + $result->metadata = $registry->toArray(); + + if (!empty($result->id)) { + $result->tags = new TagsHelper(); + $result->tags->getTagIds($result->id, $result->extension . '.category'); + } + } + + $assoc = $this->getAssoc(); + + if ($assoc) { + if ($result->id != null) { + $result->associations = ArrayHelper::toInteger(CategoriesHelper::getAssociations($result->id, $result->extension)); + } else { + $result->associations = array(); + } + } + + return $result; + } + + /** + * Method to get the row form. + * + * @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 Form|boolean A JForm object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + $extension = $this->getState('category.extension'); + $jinput = Factory::getApplication()->input; + + // A workaround to get the extension into the model for save requests. + if (empty($extension) && isset($data['extension'])) { + $extension = $data['extension']; + $parts = explode('.', $extension); + + $this->setState('category.extension', $extension); + $this->setState('category.component', $parts[0]); + $this->setState('category.section', @$parts[1]); + } + + // Get the form. + $form = $this->loadForm('com_categories.category' . $extension, 'category', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Modify the form based on Edit State access controls. + if (empty($data['extension'])) { + $data['extension'] = $extension; + } + + $categoryId = $jinput->get('id'); + $parts = explode('.', $extension); + $assetKey = $categoryId ? $extension . '.category.' . $categoryId : $parts[0]; + + if (!Factory::getUser()->authorise('core.edit.state', $assetKey)) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('published', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + // Don't allow to change the created_user_id user if not allowed to access com_users. + if (!Factory::getUser()->authorise('core.manage', 'com_users')) { + $form->setFieldAttribute('created_user_id', 'filter', 'unset'); + } + + return $form; + } + + /** + * A protected method to get the where clause for the reorder + * This ensures that the row will be moved relative to a row with the same extension + * + * @param Category $table Current table instance + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('extension') . ' = ' . $db->quote($table->extension), + ]; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $app = Factory::getApplication(); + $data = $app->getUserState('com_categories.edit.' . $this->getName() . '.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Pre-select some filters (Status, Language, Access) in edit form if those have been selected in Category Manager + if (!$data->id) { + // Check for which extension the Category Manager is used and get selected fields + $extension = substr($app->getUserState('com_categories.categories.filter.extension', ''), 4); + $filters = (array) $app->getUserState('com_categories.categories.' . $extension . '.filter'); + + $data->set( + 'published', + $app->input->getInt( + 'published', + ((isset($filters['published']) && $filters['published'] !== '') ? $filters['published'] : null) + ) + ); + $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); + $data->set( + 'access', + $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) + ); + } + } + + $this->preprocessData('com_categories.category', $data); + + return $data; + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @see JFormRule + * @see JFilterInput + * @since 3.9.23 + */ + public function validate($form, $data, $group = null) + { + if (!Factory::getUser()->authorise('core.admin', $data['extension'])) { + if (isset($data['rules'])) { + unset($data['rules']); + } + } + + return parent::validate($form, $data, $group); + } + + /** + * Method to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import. + * + * @return mixed + * + * @since 1.6 + * + * @throws \Exception if there is an error in the form event. + * + * @see \Joomla\CMS\Form\FormField + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $lang = Factory::getLanguage(); + $component = $this->getState('category.component'); + $section = $this->getState('category.section'); + $extension = Factory::getApplication()->input->get('extension', null); + + // Get the component form if it exists + $name = 'category' . ($section ? ('.' . $section) : ''); + + // Looking first in the component forms folder + $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/forms/$name.xml"); + + // Looking in the component models/forms folder (J! 3) + if (!file_exists($path)) { + $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/models/forms/$name.xml"); + } + + // Old way: looking in the component folder + if (!file_exists($path)) { + $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/$name.xml"); + } + + if (file_exists($path)) { + $lang->load($component, JPATH_BASE); + $lang->load($component, JPATH_BASE . '/components/' . $component); + + if (!$form->loadFile($path, false)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + } + + $componentInterface = Factory::getApplication()->bootComponent($component); + + if ($componentInterface instanceof CategoryServiceInterface) { + $componentInterface->prepareForm($form, $data); + } else { + // Try to find the component helper. + $eName = str_replace('com_', '', $component); + $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/helpers/category.php"); + + if (file_exists($path)) { + $cName = ucfirst($eName) . ucfirst($section) . 'HelperCategory'; + + \JLoader::register($cName, $path); + + if (class_exists($cName) && \is_callable(array($cName, 'onPrepareForm'))) { + $lang->load($component, JPATH_BASE, null, false, false) + || $lang->load($component, JPATH_BASE . '/components/' . $component, null, false, false) + || $lang->load($component, JPATH_BASE, $lang->getDefault(), false, false) + || $lang->load($component, JPATH_BASE . '/components/' . $component, $lang->getDefault(), false, false); + \call_user_func_array(array($cName, 'onPrepareForm'), array(&$form)); + + // Check for an error. + if ($form instanceof \Exception) { + $this->setError($form->getMessage()); + + return false; + } + } + } + } + + // Set the access control rules field component value. + $form->setFieldAttribute('rules', 'component', $component); + $form->setFieldAttribute('rules', 'section', $name); + + // Association category items + if ($this->getAssoc()) { + $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); + + if (\count($languages) > 1) { + $addform = new \SimpleXMLElement(''); + $fields = $addform->addChild('fields'); + $fields->addAttribute('name', 'associations'); + $fieldset = $fields->addChild('fieldset'); + $fieldset->addAttribute('name', 'item_associations'); + + foreach ($languages as $language) { + $field = $fieldset->addChild('field'); + $field->addAttribute('name', $language->lang_code); + $field->addAttribute('type', 'modal_category'); + $field->addAttribute('language', $language->lang_code); + $field->addAttribute('label', $language->title); + $field->addAttribute('translate_label', 'false'); + $field->addAttribute('extension', $extension); + $field->addAttribute('select', 'true'); + $field->addAttribute('new', 'true'); + $field->addAttribute('edit', 'true'); + $field->addAttribute('clear', 'true'); + $field->addAttribute('propagate', 'true'); + } + + $form->load($addform, false); + } + } + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $table = $this->getTable(); + $input = Factory::getApplication()->input; + $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState($this->getName() . '.id'); + $isNew = true; + $context = $this->option . '.' . $this->name; + + if (!empty($data['tags']) && $data['tags'][0] != '') { + $table->newTags = $data['tags']; + } + + // Include the plugins for the save events. + PluginHelper::importPlugin($this->events_map['save']); + + // Load the row if saving an existing category. + if ($pk > 0) { + $table->load($pk); + $isNew = false; + } + + // Set the new parent id if parent id not matched OR while New/Save as Copy . + if ($table->parent_id != $data['parent_id'] || $data['id'] == 0) { + $table->setLocation($data['parent_id'], 'last-child'); + } + + // Alter the title for save as copy + if ($input->get('task') == 'save2copy') { + $origTable = clone $this->getTable(); + $origTable->load($input->getInt('id')); + + if ($data['title'] == $origTable->title) { + [$title, $alias] = $this->generateNewTitle($data['parent_id'], $data['alias'], $data['title']); + $data['title'] = $title; + $data['alias'] = $alias; + } else { + if ($data['alias'] == $origTable->alias) { + $data['alias'] = ''; + } + } + + $data['published'] = 0; + } + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Bind the rules. + if (isset($data['rules'])) { + $rules = new Rules($data['rules']); + $table->setRules($rules); + } + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, $isNew, $data)); + + if (\in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + // Store the data. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + $assoc = $this->getAssoc(); + + if ($assoc) { + // Adding self to the association + $associations = $data['associations'] ?? array(); + + // Unset any invalid associations + $associations = ArrayHelper::toInteger($associations); + + foreach ($associations as $tag => $id) { + if (!$id) { + unset($associations[$tag]); + } + } + + // Detecting all item menus + $allLanguage = $table->language == '*'; + + if ($allLanguage && !empty($associations)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_CATEGORIES_ERROR_ALL_LANGUAGE_ASSOCIATED'), 'notice'); + } + + // Get associationskey for edited item + $db = $this->getDatabase(); + $id = (int) $table->id; + $query = $db->getQuery(true) + ->select($db->quoteName('key')) + ->from($db->quoteName('#__associations')) + ->where($db->quoteName('context') . ' = :associationscontext') + ->where($db->quoteName('id') . ' = :id') + ->bind(':associationscontext', $this->associationsContext) + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $oldKey = $db->loadResult(); + + if ($associations || $oldKey !== null) { + $where = []; + + // Deleting old associations for the associated items + $query = $db->getQuery(true) + ->delete($db->quoteName('#__associations')) + ->where($db->quoteName('context') . ' = :associationscontext') + ->bind(':associationscontext', $this->associationsContext); + + if ($associations) { + $where[] = $db->quoteName('id') . ' IN (' . implode(',', $query->bindArray(array_values($associations))) . ')'; + } + + if ($oldKey !== null) { + $where[] = $db->quoteName('key') . ' = :oldKey'; + $query->bind(':oldKey', $oldKey); + } + + $query->extendWhere('AND', $where, 'OR'); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Adding self to the association + if (!$allLanguage) { + $associations[$table->language] = (int) $table->id; + } + + if (\count($associations) > 1) { + // Adding new association for these items + $key = md5(json_encode($associations)); + $query->clear() + ->insert($db->quoteName('#__associations')) + ->columns( + [ + $db->quoteName('id'), + $db->quoteName('context'), + $db->quoteName('key'), + ] + ); + + foreach ($associations as $id) { + $id = (int) $id; + + $query->values( + implode( + ',', + $query->bindArray( + [$id, $this->associationsContext, $key], + [ParameterType::INTEGER, ParameterType::STRING, ParameterType::STRING] + ) + ) + ); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + } + + // Trigger the after save event. + Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, $isNew, $data)); + + // Rebuild the path for the category: + if (!$table->rebuildPath($table->id)) { + $this->setError($table->getError()); + + return false; + } + + // Rebuild the paths of the category's children: + if (!$table->rebuild($table->id, $table->lft, $table->level, $table->path)) { + $this->setError($table->getError()); + + return false; + } + + $this->setState($this->getName() . '.id', $table->id); + + if (Factory::getApplication()->input->get('task') == 'editAssociations') { + return $this->redirectToAssociations($data); + } + + // Clear the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to change the published state of one or more records. + * + * @param array $pks A list of the primary keys to change. + * @param integer $value The value of the published state. + * + * @return boolean True on success. + * + * @since 2.5 + */ + public function publish(&$pks, $value = 1) + { + if (parent::publish($pks, $value)) { + $extension = Factory::getApplication()->input->get('extension'); + + // Include the content plugins for the change of category state event. + PluginHelper::importPlugin('content'); + + // Trigger the onCategoryChangeState event. + Factory::getApplication()->triggerEvent('onCategoryChangeState', array($extension, $pks, $value)); + + return true; + } + } + + /** + * Method rebuild the entire nested set tree. + * + * @return boolean False on failure or error, true otherwise. + * + * @since 1.6 + */ + public function rebuild() + { + // Get an instance of the table object. + $table = $this->getTable(); + + if (!$table->rebuild()) { + $this->setError($table->getError()); + + return false; + } + + // Clear the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to save the reordered nested set tree. + * First we save the new order values in the lft values of the changed ids. + * Then we invoke the table rebuild to implement the new ordering. + * + * @param array $idArray An array of primary key ids. + * @param integer $lftArray The lft value + * + * @return boolean False on failure or error, True otherwise + * + * @since 1.6 + */ + public function saveorder($idArray = null, $lftArray = null) + { + // Get an instance of the table object. + $table = $this->getTable(); + + if (!$table->saveorder($idArray, $lftArray)) { + $this->setError($table->getError()); + + return false; + } + + // Clear the cache + $this->cleanCache(); + + return true; + } + + /** + * Batch flip category ordering. + * + * @param integer $value The new category. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return mixed An array of new IDs on success, boolean false on failure. + * + * @since 3.6.3 + */ + protected function batchFlipordering($value, $pks, $contexts) + { + $successful = array(); + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + /** + * For each category get the max ordering value + * Re-order with max - ordering + */ + foreach ($pks as $id) { + $query->select('MAX(' . $db->quoteName('ordering') . ')') + ->from($db->quoteName('#__content')) + ->where($db->quoteName('catid') . ' = :catid') + ->bind(':catid', $id, ParameterType::INTEGER); + + $db->setQuery($query); + + $max = (int) $db->loadResult(); + $max++; + + $query->clear(); + + $query->update($db->quoteName('#__content')) + ->set($db->quoteName('ordering') . ' = :max - ' . $db->quoteName('ordering')) + ->where($db->quoteName('catid') . ' = :catid') + ->bind(':max', $max, ParameterType::INTEGER) + ->bind(':catid', $id, ParameterType::INTEGER); + + $db->setQuery($query); + + if ($db->execute()) { + $successful[] = $id; + } + } + + return empty($successful) ? false : $successful; + } + + /** + * Batch copy categories to a new category. + * + * @param integer $value The new category. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return mixed An array of new IDs on success, boolean false on failure. + * + * @since 1.6 + */ + protected function batchCopy($value, $pks, $contexts) + { + $type = new UCMType(); + $this->type = $type->getTypeByAlias($this->typeAlias); + + // $value comes as {parent_id}.{extension} + $parts = explode('.', $value); + $parentId = (int) ArrayHelper::getValue($parts, 0, 1); + + $db = $this->getDatabase(); + $extension = Factory::getApplication()->input->get('extension', '', 'word'); + $newIds = array(); + + // Check that the parent exists + if ($parentId) { + if (!$this->table->load($parentId)) { + if ($error = $this->table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Non-fatal error + $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); + $parentId = 0; + } + } + + // Check that user has create permission for parent category + if ($parentId == $this->table->getRootId()) { + $canCreate = $this->user->authorise('core.create', $extension); + } else { + $canCreate = $this->user->authorise('core.create', $extension . '.category.' . $parentId); + } + + if (!$canCreate) { + // Error since user cannot create in parent category + $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_CREATE')); + + return false; + } + } + + // If the parent is 0, set it to the ID of the root item in the tree + if (empty($parentId)) { + if (!$parentId = $this->table->getRootId()) { + $this->setError($this->table->getError()); + + return false; + } elseif (!$this->user->authorise('core.create', $extension)) { + // Make sure we can create in root + $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_CREATE')); + + return false; + } + } + + // We need to log the parent ID + $parents = array(); + + // Calculate the emergency stop count as a precaution against a runaway loop bug + $query = $db->getQuery(true) + ->select('COUNT(' . $db->quoteName('id') . ')') + ->from($db->quoteName('#__categories')); + $db->setQuery($query); + + try { + $count = $db->loadResult(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Parent exists so let's proceed + while (!empty($pks) && $count > 0) { + // Pop the first id off the stack + $pk = array_shift($pks); + + $this->table->reset(); + + // Check that the row actually exists + if (!$this->table->load($pk)) { + if ($error = $this->table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Not fatal error + $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); + continue; + } + } + + // Copy is a bit tricky, because we also need to copy the children + $lft = (int) $this->table->lft; + $rgt = (int) $this->table->rgt; + $query->clear() + ->select($db->quoteName('id')) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('lft') . ' > :lft') + ->where($db->quoteName('rgt') . ' < :rgt') + ->bind(':lft', $lft, ParameterType::INTEGER) + ->bind(':rgt', $rgt, ParameterType::INTEGER); + $db->setQuery($query); + $childIds = $db->loadColumn(); + + // Add child ID's to the array only if they aren't already there. + foreach ($childIds as $childId) { + if (!\in_array($childId, $pks)) { + $pks[] = $childId; + } + } + + // Make a copy of the old ID, Parent ID and Asset ID + $oldId = $this->table->id; + $oldParentId = $this->table->parent_id; + $oldAssetId = $this->table->asset_id; + + // Reset the id because we are making a copy. + $this->table->id = 0; + + // If we a copying children, the Old ID will turn up in the parents list + // otherwise it's a new top level item + $this->table->parent_id = $parents[$oldParentId] ?? $parentId; + + // Set the new location in the tree for the node. + $this->table->setLocation($this->table->parent_id, 'last-child'); + + // @TODO: Deal with ordering? + // $this->table->ordering = 1; + $this->table->level = null; + $this->table->asset_id = null; + $this->table->lft = null; + $this->table->rgt = null; + + // Alter the title & alias + [$title, $alias] = $this->generateNewTitle($this->table->parent_id, $this->table->alias, $this->table->title); + $this->table->title = $title; + $this->table->alias = $alias; + + // Unpublish because we are making a copy + $this->table->published = 0; + + // Store the row. + if (!$this->table->store()) { + $this->setError($this->table->getError()); + + return false; + } + + // Get the new item ID + $newId = $this->table->get('id'); + + // Add the new ID to the array + $newIds[$pk] = $newId; + + // Copy rules + $query->clear() + ->update($db->quoteName('#__assets', 't')) + ->join( + 'INNER', + $db->quoteName('#__assets', 's'), + $db->quoteName('s.id') . ' = :oldid' + ) + ->bind(':oldid', $oldAssetId, ParameterType::INTEGER) + ->set($db->quoteName('t.rules') . ' = ' . $db->quoteName('s.rules')) + ->where($db->quoteName('t.id') . ' = :assetid') + ->bind(':assetid', $this->table->asset_id, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + + // Now we log the old 'parent' to the new 'parent' + $parents[$oldId] = $this->table->id; + $count--; + } + + // Rebuild the hierarchy. + if (!$this->table->rebuild()) { + $this->setError($this->table->getError()); + + return false; + } + + // Rebuild the tree path. + if (!$this->table->rebuildPath($this->table->id)) { + $this->setError($this->table->getError()); + + return false; + } + + return $newIds; + } + + /** + * Batch move categories to a new category. + * + * @param integer $value The new category ID. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True on success. + * + * @since 1.6 + */ + protected function batchMove($value, $pks, $contexts) + { + $parentId = (int) $value; + $type = new UCMType(); + $this->type = $type->getTypeByAlias($this->typeAlias); + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $extension = Factory::getApplication()->input->get('extension', '', 'word'); + + // Check that the parent exists. + if ($parentId) { + if (!$this->table->load($parentId)) { + if ($error = $this->table->getError()) { + // Fatal error. + $this->setError($error); + + return false; + } else { + // Non-fatal error. + $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); + $parentId = 0; + } + } + + // Check that user has create permission for parent category. + if ($parentId == $this->table->getRootId()) { + $canCreate = $this->user->authorise('core.create', $extension); + } else { + $canCreate = $this->user->authorise('core.create', $extension . '.category.' . $parentId); + } + + if (!$canCreate) { + // Error since user cannot create in parent category + $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_CREATE')); + + return false; + } + + // Check that user has edit permission for every category being moved + // Note that the entire batch operation fails if any category lacks edit permission + foreach ($pks as $pk) { + if (!$this->user->authorise('core.edit', $extension . '.category.' . $pk)) { + // Error since user cannot edit this category + $this->setError(Text::_('COM_CATEGORIES_BATCH_CANNOT_EDIT')); + + return false; + } + } + } + + // We are going to store all the children and just move the category + $children = array(); + + // Parent exists so let's proceed + foreach ($pks as $pk) { + // Check that the row actually exists + if (!$this->table->load($pk)) { + if ($error = $this->table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Not fatal error + $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); + continue; + } + } + + // Set the new location in the tree for the node. + $this->table->setLocation($parentId, 'last-child'); + + // Check if we are moving to a different parent + if ($parentId != $this->table->parent_id) { + $lft = (int) $this->table->lft; + $rgt = (int) $this->table->rgt; + + // Add the child node ids to the children array. + $query->clear() + ->select($db->quoteName('id')) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('lft') . ' BETWEEN :lft AND :rgt') + ->bind(':lft', $lft, ParameterType::INTEGER) + ->bind(':rgt', $rgt, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $children = array_merge($children, (array) $db->loadColumn()); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + // Store the row. + if (!$this->table->store()) { + $this->setError($this->table->getError()); + + return false; + } + + // Rebuild the tree path. + if (!$this->table->rebuildPath()) { + $this->setError($this->table->getError()); + + return false; + } + } + + // Process the child rows + if (!empty($children)) { + // Remove any duplicates and sanitize ids. + $children = array_unique($children); + $children = ArrayHelper::toInteger($children); + } + + return true; + } + + /** + * Custom clean the cache of com_content and content modules + * + * @param string $group Cache group name. + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + $extension = Factory::getApplication()->input->get('extension'); + + switch ($extension) { + case 'com_content': + parent::cleanCache('com_content'); + parent::cleanCache('mod_articles_archive'); + parent::cleanCache('mod_articles_categories'); + parent::cleanCache('mod_articles_category'); + parent::cleanCache('mod_articles_latest'); + parent::cleanCache('mod_articles_news'); + parent::cleanCache('mod_articles_popular'); + break; + default: + parent::cleanCache($extension); + break; + } + } + + /** + * Method to change the title & alias. + * + * @param integer $parentId The id of the parent. + * @param string $alias The alias. + * @param string $title The title. + * + * @return array Contains the modified title and alias. + * + * @since 1.7 + */ + protected function generateNewTitle($parentId, $alias, $title) + { + // Alter the title & alias + $table = $this->getTable(); + + while ($table->load(array('alias' => $alias, 'parent_id' => $parentId))) { + $title = StringHelper::increment($title); + $alias = StringHelper::increment($alias, 'dash'); + } + + return array($title, $alias); + } + + /** + * Method to determine if a category association is available. + * + * @return boolean True if a category association is available; false otherwise. + */ + public function getAssoc() + { + if (!\is_null($this->hasAssociation)) { + return $this->hasAssociation; + } + + $extension = $this->getState('category.extension', ''); + + $this->hasAssociation = Associations::isEnabled(); + $extension = explode('.', $extension); + $component = array_shift($extension); + $cname = str_replace('com_', '', $component); + + if (!$this->hasAssociation || !$component || !$cname) { + $this->hasAssociation = false; + + return $this->hasAssociation; + } + + $componentObject = $this->bootComponent($component); + + if ($componentObject instanceof AssociationServiceInterface && $componentObject instanceof CategoryServiceInterface) { + $this->hasAssociation = true; + + return $this->hasAssociation; + } + + $hname = $cname . 'HelperAssociation'; + \JLoader::register($hname, JPATH_SITE . '/components/' . $component . '/helpers/association.php'); + + $this->hasAssociation = class_exists($hname) && !empty($hname::$category_association); + + return $this->hasAssociation; + } } diff --git a/administrator/components/com_categories/src/Service/HTML/AdministratorService.php b/administrator/components/com_categories/src/Service/HTML/AdministratorService.php index 0a48842e8485e..c5c58226fc199 100644 --- a/administrator/components/com_categories/src/Service/HTML/AdministratorService.php +++ b/administrator/components/com_categories/src/Service/HTML/AdministratorService.php @@ -1,4 +1,5 @@ getQuery(true) - ->select( - [ - $db->quoteName('c.id'), - $db->quoteName('c.title'), - $db->quoteName('l.sef', 'lang_sef'), - $db->quoteName('l.lang_code'), - $db->quoteName('l.image'), - $db->quoteName('l.title', 'language_title'), - ] - ) - ->from($db->quoteName('#__categories', 'c')) - ->whereIn($db->quoteName('c.id'), array_values($associations)) - ->where($db->quoteName('c.id') . ' != :catid') - ->bind(':catid', $catid, ParameterType::INTEGER) - ->join( - 'LEFT', - $db->quoteName('#__languages', 'l'), - $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code') - ); - $db->setQuery($query); + // Get the associated categories + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('c.id'), + $db->quoteName('c.title'), + $db->quoteName('l.sef', 'lang_sef'), + $db->quoteName('l.lang_code'), + $db->quoteName('l.image'), + $db->quoteName('l.title', 'language_title'), + ] + ) + ->from($db->quoteName('#__categories', 'c')) + ->whereIn($db->quoteName('c.id'), array_values($associations)) + ->where($db->quoteName('c.id') . ' != :catid') + ->bind(':catid', $catid, ParameterType::INTEGER) + ->join( + 'LEFT', + $db->quoteName('#__languages', 'l'), + $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code') + ); + $db->setQuery($query); - try - { - $items = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500, $e); - } + try { + $items = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500, $e); + } - if ($items) - { - $languages = LanguageHelper::getContentLanguages(array(0, 1)); - $content_languages = array_column($languages, 'lang_code'); + if ($items) { + $languages = LanguageHelper::getContentLanguages(array(0, 1)); + $content_languages = array_column($languages, 'lang_code'); - foreach ($items as &$item) - { - if (in_array($item->lang_code, $content_languages)) - { - $text = $item->lang_code; - $url = Route::_('index.php?option=com_categories&task=category.edit&id=' . (int) $item->id . '&extension=' . $extension); - $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
' - . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8'); - $classes = 'badge bg-secondary'; + foreach ($items as &$item) { + if (in_array($item->lang_code, $content_languages)) { + $text = $item->lang_code; + $url = Route::_('index.php?option=com_categories&task=category.edit&id=' . (int) $item->id . '&extension=' . $extension); + $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
' + . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8'); + $classes = 'badge bg-secondary'; - $item->link = '' . $text . '' - . ''; - } - else - { - // Display warning if Content Language is trashed or deleted - Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); - } - } - } + $item->link = '' . $text . '' + . ''; + } else { + // Display warning if Content Language is trashed or deleted + Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); + } + } + } - $html = LayoutHelper::render('joomla.content.associations', $items); - } + $html = LayoutHelper::render('joomla.content.associations', $items); + } - return $html; - } + return $html; + } } diff --git a/administrator/components/com_categories/src/Table/CategoryTable.php b/administrator/components/com_categories/src/Table/CategoryTable.php index dc63e189a81da..6c48bef1ed572 100644 --- a/administrator/components/com_categories/src/Table/CategoryTable.php +++ b/administrator/components/com_categories/src/Table/CategoryTable.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->assoc = $this->get('Assoc'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Written this way because we only want to call IsEmptyState if no items, to prevent always calling it when not needed. - if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Preprocess the list of items to find ordering divisions. - foreach ($this->items as &$item) - { - $this->ordering[$item->parent_id][] = $item->id; - } - - // We don't need toolbar in the modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - else - { - // In article associations modal we need to remove language filter if forcing a language. - if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) - { - // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. - $languageXml = new \SimpleXMLElement(''); - $this->filterForm->setField($languageXml, 'filter', true); - - // Also, unset the active language filter so the search tools is not open by default with this filter. - unset($this->activeFilters['language']); - } - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @throws \Exception - * @since 1.6 - */ - protected function addToolbar() - { - $categoryId = $this->state->get('filter.category_id'); - $component = $this->state->get('filter.component'); - $section = $this->state->get('filter.section'); - $canDo = ContentHelper::getActions($component, 'category', $categoryId); - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - // Avoid nonsense situation. - if ($component == 'com_categories') - { - return; - } - - // Need to load the menu language file as mod_menu hasn't been loaded yet. - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_BASE) - || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); - - // If a component categories title string is present, let's use it. - if ($lang->hasKey($component_title_key = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORIES_TITLE')) - { - $title = Text::_($component_title_key); - } - elseif ($lang->hasKey($component_section_key = strtoupper($component . ($section ? "_$section" : '')))) - // Else if the component section string exists, let's use it. - { - $title = Text::sprintf('COM_CATEGORIES_CATEGORIES_TITLE', $this->escape(Text::_($component_section_key))); - } - else - // Else use the base title - { - $title = Text::_('COM_CATEGORIES_CATEGORIES_BASE_TITLE'); - } - - // Load specific css component - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = $this->document->getWebAssetManager(); - $wa->getRegistry()->addExtensionRegistryFile($component); - - if ($wa->assetExists('style', $component . '.admin-categories')) - { - $wa->useStyle($component . '.admin-categories'); - } - else - { - $wa->registerAndUseStyle($component . '.admin-categories', $component . '/administrator/categories.css'); - } - - // Prepare the toolbar. - ToolbarHelper::title($title, 'folder categories ' . substr($component, 4) . ($section ? "-$section" : '') . '-categories'); - - if ($canDo->get('core.create') || count($user->getAuthorisedCategories($component, 'core.create')) > 0) - { - $toolbar->addNew('category.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('categories.publish')->listCheck(true); - - $childBar->unpublish('categories.unpublish')->listCheck(true); - - $childBar->archive('categories.archive')->listCheck(true); - } - - if ($user->authorise('core.admin')) - { - $childBar->checkin('categories.checkin')->listCheck(true); - } - - if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) - { - $childBar->trash('categories.trash')->listCheck(true); - } - - // Add a batch button - if ($canDo->get('core.create') - && $canDo->get('core.edit') - && $canDo->get('core.edit.state')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if (!$this->isEmptyState && $canDo->get('core.admin')) - { - $toolbar->standardButton('refresh') - ->text('JTOOLBAR_REBUILD') - ->task('categories.rebuild'); - } - - if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete', $component)) - { - $toolbar->delete('categories.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences($component); - } - - // Get the component form if it exists for the help key/url - $name = 'category' . ($section ? ('.' . $section) : ''); - - // Looking first in the component forms folder - $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/forms/$name.xml"); - - // Looking in the component models/forms folder (J! 3) - if (!file_exists($path)) - { - $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/models/forms/$name.xml"); - } - - $ref_key = ''; - $url = ''; - - // Look first in form for help key and url - if (file_exists($path)) - { - if (!$xml = simplexml_load_file($path)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - $ref_key = (string) $xml->listhelp['key']; - $url = (string) $xml->listhelp['url']; - } - - if (!$ref_key) - { - // Compute the ref_key if it does exist in the component - $languageKey = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORIES_HELP_KEY'; - - if ($lang->hasKey($languageKey)) - { - $ref_key = $languageKey; - } - else - { - $languageKey = 'JHELP_COMPONENTS_' . strtoupper(substr($component, 4) . ($section ? "_$section" : '')) . '_CATEGORIES'; - - if ($lang->hasKey($languageKey)) - { - $ref_key = $languageKey; - } - } - } - - /* - * Get help for the categories view for the component by - * -remotely searching in a URL defined in the category form - * -remotely searching in a language defined dedicated URL: *component*_HELP_URL - * -locally searching in a component help file if helpURL param exists in the component and is set to '' - * -remotely searching in a component URL if helpURL param exists in the component and is NOT set to '' - */ - if (!$url) - { - if ($lang->hasKey($lang_help_url = strtoupper($component) . '_HELP_URL')) - { - $debug = $lang->setDebug(false); - $url = Text::_($lang_help_url); - $lang->setDebug($debug); - } - } - - ToolbarHelper::help($ref_key, ComponentHelper::getParams($component)->exists('helpURL'), $url); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var object + */ + protected $state; + + /** + * Flag if an association exists + * + * @var boolean + */ + protected $assoc; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view + * + * @param string|null $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @throws GenericDataException + * + * @return void + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->assoc = $this->get('Assoc'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Written this way because we only want to call IsEmptyState if no items, to prevent always calling it when not needed. + if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Preprocess the list of items to find ordering divisions. + foreach ($this->items as &$item) { + $this->ordering[$item->parent_id][] = $item->id; + } + + // We don't need toolbar in the modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } else { + // In article associations modal we need to remove language filter if forcing a language. + if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) { + // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. + $languageXml = new \SimpleXMLElement(''); + $this->filterForm->setField($languageXml, 'filter', true); + + // Also, unset the active language filter so the search tools is not open by default with this filter. + unset($this->activeFilters['language']); + } + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @throws \Exception + * @since 1.6 + */ + protected function addToolbar() + { + $categoryId = $this->state->get('filter.category_id'); + $component = $this->state->get('filter.component'); + $section = $this->state->get('filter.section'); + $canDo = ContentHelper::getActions($component, 'category', $categoryId); + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + // Avoid nonsense situation. + if ($component == 'com_categories') { + return; + } + + // Need to load the menu language file as mod_menu hasn't been loaded yet. + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_BASE) + || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); + + // If a component categories title string is present, let's use it. + if ($lang->hasKey($component_title_key = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORIES_TITLE')) { + $title = Text::_($component_title_key); + } elseif ($lang->hasKey($component_section_key = strtoupper($component . ($section ? "_$section" : '')))) { + // Else if the component section string exists, let's use it. + $title = Text::sprintf('COM_CATEGORIES_CATEGORIES_TITLE', $this->escape(Text::_($component_section_key))); + } else // Else use the base title + { + $title = Text::_('COM_CATEGORIES_CATEGORIES_BASE_TITLE'); + } + + // Load specific css component + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = $this->document->getWebAssetManager(); + $wa->getRegistry()->addExtensionRegistryFile($component); + + if ($wa->assetExists('style', $component . '.admin-categories')) { + $wa->useStyle($component . '.admin-categories'); + } else { + $wa->registerAndUseStyle($component . '.admin-categories', $component . '/administrator/categories.css'); + } + + // Prepare the toolbar. + ToolbarHelper::title($title, 'folder categories ' . substr($component, 4) . ($section ? "-$section" : '') . '-categories'); + + if ($canDo->get('core.create') || count($user->getAuthorisedCategories($component, 'core.create')) > 0) { + $toolbar->addNew('category.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state')) { + $childBar->publish('categories.publish')->listCheck(true); + + $childBar->unpublish('categories.unpublish')->listCheck(true); + + $childBar->archive('categories.archive')->listCheck(true); + } + + if ($user->authorise('core.admin')) { + $childBar->checkin('categories.checkin')->listCheck(true); + } + + if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) { + $childBar->trash('categories.trash')->listCheck(true); + } + + // Add a batch button + if ( + $canDo->get('core.create') + && $canDo->get('core.edit') + && $canDo->get('core.edit.state') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if (!$this->isEmptyState && $canDo->get('core.admin')) { + $toolbar->standardButton('refresh') + ->text('JTOOLBAR_REBUILD') + ->task('categories.rebuild'); + } + + if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete', $component)) { + $toolbar->delete('categories.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences($component); + } + + // Get the component form if it exists for the help key/url + $name = 'category' . ($section ? ('.' . $section) : ''); + + // Looking first in the component forms folder + $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/forms/$name.xml"); + + // Looking in the component models/forms folder (J! 3) + if (!file_exists($path)) { + $path = Path::clean(JPATH_ADMINISTRATOR . "/components/$component/models/forms/$name.xml"); + } + + $ref_key = ''; + $url = ''; + + // Look first in form for help key and url + if (file_exists($path)) { + if (!$xml = simplexml_load_file($path)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + $ref_key = (string) $xml->listhelp['key']; + $url = (string) $xml->listhelp['url']; + } + + if (!$ref_key) { + // Compute the ref_key if it does exist in the component + $languageKey = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORIES_HELP_KEY'; + + if ($lang->hasKey($languageKey)) { + $ref_key = $languageKey; + } else { + $languageKey = 'JHELP_COMPONENTS_' . strtoupper(substr($component, 4) . ($section ? "_$section" : '')) . '_CATEGORIES'; + + if ($lang->hasKey($languageKey)) { + $ref_key = $languageKey; + } + } + } + + /* + * Get help for the categories view for the component by + * -remotely searching in a URL defined in the category form + * -remotely searching in a language defined dedicated URL: *component*_HELP_URL + * -locally searching in a component help file if helpURL param exists in the component and is set to '' + * -remotely searching in a component URL if helpURL param exists in the component and is NOT set to '' + */ + if (!$url) { + if ($lang->hasKey($lang_help_url = strtoupper($component) . '_HELP_URL')) { + $debug = $lang->setDebug(false); + $url = Text::_($lang_help_url); + $lang->setDebug($debug); + } + } + + ToolbarHelper::help($ref_key, ComponentHelper::getParams($component)->exists('helpURL'), $url); + } } diff --git a/administrator/components/com_categories/src/View/Category/HtmlView.php b/administrator/components/com_categories/src/View/Category/HtmlView.php index d0a31e2b49143..a9cee9e400b4a 100644 --- a/administrator/components/com_categories/src/View/Category/HtmlView.php +++ b/administrator/components/com_categories/src/View/Category/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - $section = $this->state->get('category.section') ? $this->state->get('category.section') . '.' : ''; - $this->canDo = ContentHelper::getActions($this->state->get('category.component'), $section . 'category', $this->item->id); - $this->assoc = $this->get('Assoc'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check if we have a content type for this alias - if (!empty(TagsHelper::getTypes('objectList', array($this->state->get('category.extension') . '.category'), true))) - { - $this->checkTags = true; - } - - Factory::getApplication()->input->set('hidemainmenu', true); - - // If we are forcing a language in modal (used for associations). - if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) - { - // Set the language field to the forcedLanguage and disable changing it. - $this->form->setValue('language', null, $forcedLanguage); - $this->form->setFieldAttribute('language', 'readonly', 'true'); - - // Only allow to select categories with All language or with the forced language. - $this->form->setFieldAttribute('parent_id', 'language', '*,' . $forcedLanguage); - - // Only allow to select tags with All language or with the forced language. - $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $extension = Factory::getApplication()->input->get('extension'); - $user = $this->getCurrentUser(); - $userId = $user->id; - - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); - - // Avoid nonsense situation. - if ($extension == 'com_categories') - { - return; - } - - // The extension can be in the form com_foo.section - $parts = explode('.', $extension); - $component = $parts[0]; - $section = (count($parts) > 1) ? $parts[1] : null; - $componentParams = ComponentHelper::getParams($component); - - // Need to load the menu language file as mod_menu hasn't been loaded yet. - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_BASE) - || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); - - // Get the results for each action. - $canDo = $this->canDo; - - // If a component categories title string is present, let's use it. - if ($lang->hasKey($component_title_key = $component . ($section ? "_$section" : '') . '_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE')) - { - $title = Text::_($component_title_key); - } - // Else if the component section string exists, let's use it. - elseif ($lang->hasKey($component_section_key = $component . ($section ? "_$section" : ''))) - { - $title = Text::sprintf('COM_CATEGORIES_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT') - . '_TITLE', $this->escape(Text::_($component_section_key)) - ); - } - // Else use the base title - else - { - $title = Text::_('COM_CATEGORIES_CATEGORY_BASE_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE'); - } - - // Load specific css component - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = $this->document->getWebAssetManager(); - $wa->getRegistry()->addExtensionRegistryFile($component); - - if ($wa->assetExists('style', $component . '.admin-categories')) - { - $wa->useStyle($component . '.admin-categories'); - } - else - { - $wa->registerAndUseStyle($component . '.admin-categories', $component . '/administrator/categories.css'); - } - - // Prepare the toolbar. - ToolbarHelper::title( - $title, - 'folder category-' . ($isNew ? 'add' : 'edit') - . ' ' . substr($component, 4) . ($section ? "-$section" : '') . '-category-' . ($isNew ? 'add' : 'edit') - ); - - // For new records, check the create permission. - if ($isNew && (count($user->getAuthorisedCategories($component, 'core.create')) > 0)) - { - ToolbarHelper::apply('category.apply'); - ToolbarHelper::saveGroup( - [ - ['save', 'category.save'], - ['save2new', 'category.save2new'] - ], - 'btn-success' - ); - - ToolbarHelper::cancel('category.cancel'); - } - - // If not checked out, can save the item. - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_user_id == $userId); - - $toolbarButtons = []; - - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable) - { - ToolbarHelper::apply('category.apply'); - - $toolbarButtons[] = ['save', 'category.save']; - - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'category.save2new']; - } - } - - // If an existing item, can save to a copy. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'category.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel('category.cancel', 'JTOOLBAR_CLOSE'); - - if (ComponentHelper::isEnabled('com_contenthistory') && $componentParams->get('save_history', 0) && $itemEditable) - { - $typeAlias = $extension . '.category'; - ToolbarHelper::versions($typeAlias, $this->item->id); - } - - if (Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) - { - ToolbarHelper::custom('category.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); - } - } - - ToolbarHelper::divider(); - - // Look first in form for help key - $ref_key = (string) $this->form->getXml()->help['key']; - - // Try with a language string - if (!$ref_key) - { - // Compute the ref_key if it does exist in the component - $languageKey = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT') . '_HELP_KEY'; - - if ($lang->hasKey($languageKey)) - { - $ref_key = $languageKey; - } - else - { - $languageKey = 'JHELP_COMPONENTS_' - . strtoupper(substr($component, 4) . ($section ? "_$section" : '')) - . '_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT'); - - if ($lang->hasKey($languageKey)) - { - $ref_key = $languageKey; - } - } - } - - /* - * Get help for the category/section view for the component by - * -remotely searching in a URL defined in the category form - * -remotely searching in a language defined dedicated URL: *component*_HELP_URL - * -locally searching in a component help file if helpURL param exists in the component and is set to '' - * -remotely searching in a component URL if helpURL param exists in the component and is NOT set to '' - */ - $url = (string) $this->form->getXml()->help['url']; - - if (!$url) - { - if ($lang->hasKey($lang_help_url = strtoupper($component) . '_HELP_URL')) - { - $debug = $lang->setDebug(false); - $url = Text::_($lang_help_url); - $lang->setDebug($debug); - } - } - - ToolbarHelper::help($ref_key, $componentParams->exists('helpURL'), $url, $component); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * Flag if an association exists + * + * @var boolean + */ + protected $assoc; + + /** + * The actions the user is authorised to perform + * + * @var CMSObject + */ + protected $canDo; + + /** + * Is there a content type associated with this category alias + * + * @var boolean + * @since 4.0.0 + */ + protected $checkTags = false; + + /** + * Display the view. + * + * @param string|null $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + $section = $this->state->get('category.section') ? $this->state->get('category.section') . '.' : ''; + $this->canDo = ContentHelper::getActions($this->state->get('category.component'), $section . 'category', $this->item->id); + $this->assoc = $this->get('Assoc'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check if we have a content type for this alias + if (!empty(TagsHelper::getTypes('objectList', array($this->state->get('category.extension') . '.category'), true))) { + $this->checkTags = true; + } + + Factory::getApplication()->input->set('hidemainmenu', true); + + // If we are forcing a language in modal (used for associations). + if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) { + // Set the language field to the forcedLanguage and disable changing it. + $this->form->setValue('language', null, $forcedLanguage); + $this->form->setFieldAttribute('language', 'readonly', 'true'); + + // Only allow to select categories with All language or with the forced language. + $this->form->setFieldAttribute('parent_id', 'language', '*,' . $forcedLanguage); + + // Only allow to select tags with All language or with the forced language. + $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $extension = Factory::getApplication()->input->get('extension'); + $user = $this->getCurrentUser(); + $userId = $user->id; + + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); + + // Avoid nonsense situation. + if ($extension == 'com_categories') { + return; + } + + // The extension can be in the form com_foo.section + $parts = explode('.', $extension); + $component = $parts[0]; + $section = (count($parts) > 1) ? $parts[1] : null; + $componentParams = ComponentHelper::getParams($component); + + // Need to load the menu language file as mod_menu hasn't been loaded yet. + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_BASE) + || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); + + // Get the results for each action. + $canDo = $this->canDo; + + // If a component categories title string is present, let's use it. + if ($lang->hasKey($component_title_key = $component . ($section ? "_$section" : '') . '_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE')) { + $title = Text::_($component_title_key); + } elseif ($lang->hasKey($component_section_key = $component . ($section ? "_$section" : ''))) { + // Else if the component section string exists, let's use it. + $title = Text::sprintf('COM_CATEGORIES_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT') + . '_TITLE', $this->escape(Text::_($component_section_key))); + } else { + // Else use the base title + $title = Text::_('COM_CATEGORIES_CATEGORY_BASE_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE'); + } + + // Load specific css component + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = $this->document->getWebAssetManager(); + $wa->getRegistry()->addExtensionRegistryFile($component); + + if ($wa->assetExists('style', $component . '.admin-categories')) { + $wa->useStyle($component . '.admin-categories'); + } else { + $wa->registerAndUseStyle($component . '.admin-categories', $component . '/administrator/categories.css'); + } + + // Prepare the toolbar. + ToolbarHelper::title( + $title, + 'folder category-' . ($isNew ? 'add' : 'edit') + . ' ' . substr($component, 4) . ($section ? "-$section" : '') . '-category-' . ($isNew ? 'add' : 'edit') + ); + + // For new records, check the create permission. + if ($isNew && (count($user->getAuthorisedCategories($component, 'core.create')) > 0)) { + ToolbarHelper::apply('category.apply'); + ToolbarHelper::saveGroup( + [ + ['save', 'category.save'], + ['save2new', 'category.save2new'] + ], + 'btn-success' + ); + + ToolbarHelper::cancel('category.cancel'); + } else { + // If not checked out, can save the item. + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_user_id == $userId); + + $toolbarButtons = []; + + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable) { + ToolbarHelper::apply('category.apply'); + + $toolbarButtons[] = ['save', 'category.save']; + + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'category.save2new']; + } + } + + // If an existing item, can save to a copy. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'category.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel('category.cancel', 'JTOOLBAR_CLOSE'); + + if (ComponentHelper::isEnabled('com_contenthistory') && $componentParams->get('save_history', 0) && $itemEditable) { + $typeAlias = $extension . '.category'; + ToolbarHelper::versions($typeAlias, $this->item->id); + } + + if (Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) { + ToolbarHelper::custom('category.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); + } + } + + ToolbarHelper::divider(); + + // Look first in form for help key + $ref_key = (string) $this->form->getXml()->help['key']; + + // Try with a language string + if (!$ref_key) { + // Compute the ref_key if it does exist in the component + $languageKey = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT') . '_HELP_KEY'; + + if ($lang->hasKey($languageKey)) { + $ref_key = $languageKey; + } else { + $languageKey = 'JHELP_COMPONENTS_' + . strtoupper(substr($component, 4) . ($section ? "_$section" : '')) + . '_CATEGORY_' . ($isNew ? 'ADD' : 'EDIT'); + + if ($lang->hasKey($languageKey)) { + $ref_key = $languageKey; + } + } + } + + /* + * Get help for the category/section view for the component by + * -remotely searching in a URL defined in the category form + * -remotely searching in a language defined dedicated URL: *component*_HELP_URL + * -locally searching in a component help file if helpURL param exists in the component and is set to '' + * -remotely searching in a component URL if helpURL param exists in the component and is NOT set to '' + */ + $url = (string) $this->form->getXml()->help['url']; + + if (!$url) { + if ($lang->hasKey($lang_help_url = strtoupper($component) . '_HELP_URL')) { + $debug = $lang->setDebug(false); + $url = Text::_($lang_help_url); + $lang->setDebug($debug); + } + } + + ToolbarHelper::help($ref_key, $componentParams->exists('helpURL'), $url, $component); + } } diff --git a/administrator/components/com_categories/tmpl/categories/default.php b/administrator/components/com_categories/tmpl/categories/default.php index e4ad8677615e6..9a05036baf514 100644 --- a/administrator/components/com_categories/tmpl/categories/default.php +++ b/administrator/components/com_categories/tmpl/categories/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $userId = $user->get('id'); @@ -33,281 +34,273 @@ $component = $parts[0]; $section = null; -if (count($parts) > 1) -{ - $section = $parts[1]; +if (count($parts) > 1) { + $section = $parts[1]; - $inflector = Inflector::getInstance(); + $inflector = Inflector::getInstance(); - if (!$inflector->isPlural($section)) - { - $section = $inflector->toPlural($section); - } + if (!$inflector->isPlural($section)) { + $section = $inflector->toPlural($section); + } } -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_categories&task=categories.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_categories&task=categories.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?> -
-
-
- $this)); - ?> - items)) : ?> -
- - -
- - - - - - - - - - items[0]) && property_exists($this->items[0], 'count_published')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_archived')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> - - - - assoc) : ?> - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="false"> - items as $i => $item) : ?> - authorise('core.edit', $extension . '.category.' . $item->id); - $canCheckin = $user->authorise('core.admin', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); - $canEditOwn = $user->authorise('core.edit.own', $extension . '.category.' . $item->id) && $item->created_user_id == $userId; - $canChange = $user->authorise('core.edit.state', $extension . '.category.' . $item->id) && $canCheckin; +
+
+
+ $this)); + ?> + items)) : ?> +
+ + +
+ +
- , - , - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ + + + + + + + items[0]) && property_exists($this->items[0], 'count_published')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_archived')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> + + + + assoc) : ?> + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="false"> + items as $i => $item) : ?> + authorise('core.edit', $extension . '.category.' . $item->id); + $canCheckin = $user->authorise('core.admin', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); + $canEditOwn = $user->authorise('core.edit.own', $extension . '.category.' . $item->id) && $item->created_user_id == $userId; + $canChange = $user->authorise('core.edit.state', $extension . '.category.' . $item->id) && $canCheckin; - // Get the parents of item for sorting - if ($item->level > 1) - { - $parentsStr = ''; - $_currentParentId = $item->parent_id; - $parentsStr = ' ' . $_currentParentId; - for ($i2 = 0; $i2 < $item->level; $i2++) - { - foreach ($this->ordering as $k => $v) - { - $v = implode('-', $v); - $v = '-' . $v . '-'; - if (strpos($v, '-' . $_currentParentId . '-') !== false) - { - $parentsStr .= ' ' . $k; - $_currentParentId = $k; - break; - } - } - } - } - else - { - $parentsStr = ''; - } - ?> - - - - - - items[0]) && property_exists($this->items[0], 'count_published')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_archived')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> - - + // Get the parents of item for sorting + if ($item->level > 1) { + $parentsStr = ''; + $_currentParentId = $item->parent_id; + $parentsStr = ' ' . $_currentParentId; + for ($i2 = 0; $i2 < $item->level; $i2++) { + foreach ($this->ordering as $k => $v) { + $v = implode('-', $v); + $v = '-' . $v . '-'; + if (strpos($v, '-' . $_currentParentId . '-') !== false) { + $parentsStr .= ' ' . $k; + $_currentParentId = $k; + break; + } + } + } + } else { + $parentsStr = ''; + } + ?> + + + + + + items[0]) && property_exists($this->items[0], 'count_published')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_archived')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> + + - - assoc) : ?> - - - - - - - - - -
+ , + , + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
- id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - published, $i, 'categories.', $canChange); ?> - - $item->level)); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'categories.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - -
- - - note)) : ?> - escape($item->alias)); ?> - - escape($item->alias), $this->escape($item->note)); ?> - - -
-
- - count_published; ?> - - - - - count_unpublished; ?> - - - - - count_archived; ?> - - - - - count_trashed; ?> - - -
+ id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + published, $i, 'categories.', $canChange); ?> + + $item->level)); ?> + + checked_out) : ?> + editor, $item->checked_out_time, 'categories.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + +
+ + + note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + + +
+
+ + count_published; ?> + + + + + count_unpublished; ?> + + + + + count_archived; ?> + + + + + count_trashed; ?> + + + - escape($item->access_level); ?> - - association) : ?> - id, $extension); ?> - - - - - id; ?> -
+ + escape($item->access_level); ?> + + assoc) : ?> + + association) : ?> + id, $extension); ?> + + + + + + + + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', $extension) - && $user->authorise('core.edit', $extension) - && $user->authorise('core.edit.state', $extension)) : ?> - Text::_('COM_CATEGORIES_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ), - $this->loadTemplate('batch_body') - ); ?> - - + + authorise('core.create', $extension) + && $user->authorise('core.edit', $extension) + && $user->authorise('core.edit.state', $extension) +) : ?> + Text::_('COM_CATEGORIES_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ), + $this->loadTemplate('batch_body') + ); ?> + + - - - - -
-
-
+ + + + + + + diff --git a/administrator/components/com_categories/tmpl/categories/default_batch_body.php b/administrator/components/com_categories/tmpl/categories/default_batch_body.php index 700addd6f6756..aa432d63f01c5 100644 --- a/administrator/components/com_categories/tmpl/categories/default_batch_body.php +++ b/administrator/components/com_categories/tmpl/categories/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\HTML\HTMLHelper; @@ -19,46 +21,46 @@ ?>
-
- -
-
- -
-
- -
-
- -
-
-
-
- = 0) : ?> -
-
- $extension, 'addRoot' => true]); ?> -
-
- -
-
- -
-
-
- -
-
-
- -
- -
-
-
-
- +
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ = 0) : ?> +
+
+ $extension, 'addRoot' => true]); ?> +
+
+ +
+
+ +
+
+
+ +
+
+
+ +
+ +
+
+
+
+
diff --git a/administrator/components/com_categories/tmpl/categories/default_batch_footer.php b/administrator/components/com_categories/tmpl/categories/default_batch_footer.php index 5f37efc761a37..3f29dd80ef2ed 100644 --- a/administrator/components/com_categories/tmpl/categories/default_batch_footer.php +++ b/administrator/components/com_categories/tmpl/categories/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/administrator/components/com_categories/tmpl/categories/emptystate.php b/administrator/components/com_categories/tmpl/categories/emptystate.php index 03b15bdbece0b..df600d521a58e 100644 --- a/administrator/components/com_categories/tmpl/categories/emptystate.php +++ b/administrator/components/com_categories/tmpl/categories/emptystate.php @@ -1,4 +1,5 @@ load($component, JPATH_ADMINISTRATOR . '/components/' . $component); // If a component categories title string is present, let's use it. -if ($lang->hasKey($component_title_key = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORIES_TITLE')) -{ - $title = Text::_($component_title_key); -} -// Else if the component section string exists, let's use it -elseif ($lang->hasKey($component_section_key = strtoupper($component . ($section ? "_$section" : '')))) +if ($lang->hasKey($component_title_key = strtoupper($component . ($section ? "_$section" : '')) . '_CATEGORIES_TITLE')) { + $title = Text::_($component_title_key); +} elseif ($lang->hasKey($component_section_key = strtoupper($component . ($section ? "_$section" : '')))) { + // Else if the component section string exists, let's use it + $title = Text::sprintf('COM_CATEGORIES_CATEGORIES_TITLE', $this->escape(Text::_($component_section_key))); +} else // Else use the base title { - $title = Text::sprintf('COM_CATEGORIES_CATEGORIES_TITLE', $this->escape(Text::_($component_section_key))); -} -else // Else use the base title -{ - $title = Text::_('COM_CATEGORIES_CATEGORIES_BASE_TITLE'); + $title = Text::_('COM_CATEGORIES_CATEGORIES_BASE_TITLE'); } $displayData = [ - 'textPrefix' => 'COM_CATEGORIES', - 'formURL' => 'index.php?option=com_categories&extension=' . $extension, - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Category', - 'title' => $title, - 'icon' => 'icon-folder categories content-categories', + 'textPrefix' => 'COM_CATEGORIES', + 'formURL' => 'index.php?option=com_categories&extension=' . $extension, + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Category', + 'title' => $title, + 'icon' => 'icon-folder categories content-categories', ]; -if (Factory::getApplication()->getIdentity()->authorise('core.create', $extension)) -{ - $displayData['createURL'] = 'index.php?option=com_categories&extension=' . $extension . '&task=category.add'; +if (Factory::getApplication()->getIdentity()->authorise('core.create', $extension)) { + $displayData['createURL'] = 'index.php?option=com_categories&extension=' . $extension . '&task=category.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_categories/tmpl/categories/modal.php b/administrator/components/com_categories/tmpl/categories/modal.php index 2500c0b44a004..819957f58ddc7 100644 --- a/administrator/components/com_categories/tmpl/categories/modal.php +++ b/administrator/components/com_categories/tmpl/categories/modal.php @@ -1,4 +1,5 @@ isClient('site')) -{ - Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); +if ($app->isClient('site')) { + Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); } HTMLHelper::_('behavior.core'); @@ -34,114 +34,106 @@ ?>
-
+ - $this)); ?> + $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - 2 => 'icon-folder', - ); - ?> - items as $i => $item) : ?> - language && Multilanguage::isEnabled()) - { - $tag = strlen($item->language); - if ($tag == 5) - { - $lang = substr($item->language, 0, 2); - } - elseif ($tag == 6) - { - $lang = substr($item->language, 0, 3); - } - else - { - $lang = ''; - } - } - elseif (!Multilanguage::isEnabled()) - { - $lang = ''; - } - ?> - - - - - - - - - -
- , - , - -
- - - - - - - - - -
- - - - - $item->level)); ?> - - escape($item->title); ?> -
- note)) : ?> - escape($item->alias)); ?> - - escape($item->alias), $this->escape($item->note)); ?> - -
-
- escape($item->access_level); ?> - - - - id; ?> -
+ items)) : ?> +
+ + +
+ + + + + + + + + + + + + + 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + 2 => 'icon-folder', + ); + ?> + items as $i => $item) : ?> + language && Multilanguage::isEnabled()) { + $tag = strlen($item->language); + if ($tag == 5) { + $lang = substr($item->language, 0, 2); + } elseif ($tag == 6) { + $lang = substr($item->language, 0, 3); + } else { + $lang = ''; + } + } elseif (!Multilanguage::isEnabled()) { + $lang = ''; + } + ?> + + + + + + + + + +
+ , + , + +
+ + + + + + + + + +
+ + + + + $item->level)); ?> + + escape($item->title); ?> +
+ note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + +
+
+ escape($item->access_level); ?> + + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - - - + + + + + -
+
diff --git a/administrator/components/com_categories/tmpl/category/edit.php b/administrator/components/com_categories/tmpl/category/edit.php index 448b4a18d5832..b20338fdbb7e4 100644 --- a/administrator/components/com_categories/tmpl/category/edit.php +++ b/administrator/components/com_categories/tmpl/category/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $app = Factory::getApplication(); $input = $app->input; @@ -34,14 +35,12 @@ $c = Factory::getApplication()->bootComponent($this->state->get('category.extension')); -if ($c instanceof WorkflowServiceInterface) -{ - $wcontext = $c->getCategoryWorkflowContext($this->state->get('category.section')); +if ($c instanceof WorkflowServiceInterface) { + $wcontext = $c->getCategoryWorkflowContext($this->state->get('category.section')); - if (!$c->isWorkflowActive($wcontext)) - { - $this->ignore_fieldsets[] = 'workflow'; - } + if (!$c->isWorkflowActive($wcontext)) { + $this->ignore_fieldsets[] = 'workflow'; + } } $this->useCoreUI = true; @@ -54,78 +53,77 @@
- - -
- 'general', 'recall' => true, 'breakpoint' => 768]); ?> - -
-
- form->getLabel('description'); ?> - form->getInput('description'); ?> -
-
- -
-
- - - - - - -
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
- - - - - -
- -
- -
-
- - - - - - canDo->get('core.admin')) : ?> - - -
- -
- form->getInput('rules'); ?> -
-
- - - - - - form->getInput('extension'); ?> - - - - -
+ + +
+ 'general', 'recall' => true, 'breakpoint' => 768]); ?> + +
+
+ form->getLabel('description'); ?> + form->getInput('description'); ?> +
+
+ +
+
+ + + + + + +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+ + + + + +
+ +
+ +
+
+ + + + + + canDo->get('core.admin')) : ?> + +
+ +
+ form->getInput('rules'); ?> +
+
+ + + + + + form->getInput('extension'); ?> + + + + +
diff --git a/administrator/components/com_categories/tmpl/category/modal.php b/administrator/components/com_categories/tmpl/category/modal.php index 2adebfb9bcee7..87526bec61712 100644 --- a/administrator/components/com_categories/tmpl/category/modal.php +++ b/administrator/components/com_categories/tmpl/category/modal.php @@ -1,4 +1,5 @@
- setLayout('edit'); ?> - loadTemplate(); ?> + setLayout('edit'); ?> + loadTemplate(); ?>
diff --git a/administrator/components/com_checkin/services/provider.php b/administrator/components/com_checkin/services/provider.php index e4ad52d3d4754..66b8f0fec1811 100644 --- a/administrator/components/com_checkin/services/provider.php +++ b/administrator/components/com_checkin/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Checkin')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Checkin')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Checkin')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Checkin')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_checkin/src/Controller/DisplayController.php b/administrator/components/com_checkin/src/Controller/DisplayController.php index 34c5512726681..0cffe3cfe2133 100644 --- a/administrator/components/com_checkin/src/Controller/DisplayController.php +++ b/administrator/components/com_checkin/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'string'); - - if (empty($ids)) - { - $this->app->enqueueMessage(Text::_('JLIB_HTML_PLEASE_MAKE_A_SELECTION_FROM_THE_LIST'), 'warning'); - } - else - { - // Get the model. - /** @var \Joomla\Component\Checkin\Administrator\Model\CheckinModel $model */ - $model = $this->getModel('Checkin'); - - // Checked in the items. - $this->setMessage(Text::plural('COM_CHECKIN_N_ITEMS_CHECKED_IN', $model->checkin($ids))); - } - - $this->setRedirect('index.php?option=com_checkin'); - } - - /** - * Provide the data for a badge in a menu item via JSON - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function getMenuBadgeData() - { - if (!$this->app->getIdentity()->authorise('core.manage', 'com_checkin')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } - - $model = $this->getModel('Checkin'); - - $amount = (int) count($model->getItems()); - - echo new JsonResponse($amount); - } - - /** - * Method to get the number of locked icons - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function getQuickiconContent() - { - if (!$this->app->getIdentity()->authorise('core.manage', 'com_checkin')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } - - $model = $this->getModel('Checkin'); - - $amount = (int) count($model->getItems()); - - $result = []; - - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_CHECKIN_N_QUICKICON_SRONLY', $amount); - - echo new JsonResponse($result); - } + /** + * The default view. + * + * @var string + * @since 1.6 + */ + protected $default_view = 'checkin'; + + /** + * Method to display a view. + * + * @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()}. + * + * @return static A \JControllerLegacy object to support chaining. + */ + public function display($cachable = false, $urlparams = array()) + { + return parent::display(); + } + + /** + * Check in a list of items. + * + * @return void + */ + public function checkin() + { + // Check for request forgeries + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'string'); + + if (empty($ids)) { + $this->app->enqueueMessage(Text::_('JLIB_HTML_PLEASE_MAKE_A_SELECTION_FROM_THE_LIST'), 'warning'); + } else { + // Get the model. + /** @var \Joomla\Component\Checkin\Administrator\Model\CheckinModel $model */ + $model = $this->getModel('Checkin'); + + // Checked in the items. + $this->setMessage(Text::plural('COM_CHECKIN_N_ITEMS_CHECKED_IN', $model->checkin($ids))); + } + + $this->setRedirect('index.php?option=com_checkin'); + } + + /** + * Provide the data for a badge in a menu item via JSON + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function getMenuBadgeData() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_checkin')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } + + $model = $this->getModel('Checkin'); + + $amount = (int) count($model->getItems()); + + echo new JsonResponse($amount); + } + + /** + * Method to get the number of locked icons + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function getQuickiconContent() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_checkin')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } + + $model = $this->getModel('Checkin'); + + $amount = (int) count($model->getItems()); + + $result = []; + + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_CHECKIN_N_QUICKICON_SRONLY', $amount); + + echo new JsonResponse($result); + } } diff --git a/administrator/components/com_checkin/src/Model/CheckinModel.php b/administrator/components/com_checkin/src/Model/CheckinModel.php index efd485d2a8911..f3f5b92fb4c41 100644 --- a/administrator/components/com_checkin/src/Model/CheckinModel.php +++ b/administrator/components/com_checkin/src/Model/CheckinModel.php @@ -1,4 +1,5 @@ getDatabase(); - - if (!is_array($ids)) - { - return 0; - } - - // This int will hold the checked item count. - $results = 0; - - $app = Factory::getApplication(); - - foreach ($ids as $tn) - { - // Make sure we get the right tables based on prefix. - if (stripos($tn, $app->get('dbprefix')) !== 0) - { - continue; - } - - $fields = $db->getTableColumns($tn, false); - - if (!(isset($fields['checked_out']) && isset($fields['checked_out_time']))) - { - continue; - } - - $query = $db->getQuery(true) - ->update($db->quoteName($tn)) - ->set($db->quoteName('checked_out') . ' = DEFAULT'); - - if ($fields['checked_out_time']->Null === 'YES') - { - $query->set($db->quoteName('checked_out_time') . ' = NULL'); - } - else - { - $nullDate = $db->getNullDate(); - - $query->set($db->quoteName('checked_out_time') . ' = :checkouttime') - ->bind(':checkouttime', $nullDate); - } - - if ($fields['checked_out']->Null === 'YES') - { - $query->where($db->quoteName('checked_out') . ' IS NOT NULL'); - } - else - { - $query->where($db->quoteName('checked_out') . ' > 0'); - } - - $db->setQuery($query); - - if ($db->execute()) - { - $results = $results + $db->getAffectedRows(); - $app->triggerEvent('onAfterCheckin', array($tn)); - } - } - - return $results; - } - - /** - * Get total of tables - * - * @return integer Total to check-in tables - * - * @since 1.6 - */ - public function getTotal() - { - if (!isset($this->total)) - { - $this->getItems(); - } - - return $this->total; - } - - /** - * Get tables - * - * @return array Checked in table names as keys and checked in item count as values. - * - * @since 1.6 - */ - public function getItems() - { - if (!isset($this->items)) - { - $db = $this->getDatabase(); - $tables = $db->getTableList(); - $prefix = Factory::getApplication()->get('dbprefix'); - - // This array will hold table name as key and checked in item count as value. - $results = array(); - - foreach ($tables as $tn) - { - // Make sure we get the right tables based on prefix. - if (stripos($tn, $prefix) !== 0) - { - continue; - } - - if ($this->getState('filter.search') && stripos($tn, $this->getState('filter.search')) === false) - { - continue; - } - - $fields = $db->getTableColumns($tn, false); - - if (!(isset($fields['checked_out']) && isset($fields['checked_out_time']))) - { - continue; - } - - $query = $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName($tn)); - - if ($fields['checked_out']->Null === 'YES') - { - $query->where($db->quoteName('checked_out') . ' IS NOT NULL'); - } - else - { - $query->where($db->quoteName('checked_out') . ' > 0'); - } - - $db->setQuery($query); - $count = $db->loadResult(); - - if ($count) - { - $results[$tn] = $count; - } - } - - $this->total = count($results); - - // Order items by table - if ($this->getState('list.ordering') == 'table') - { - if (strtolower($this->getState('list.direction')) == 'asc') - { - ksort($results); - } - else - { - krsort($results); - } - } - // Order items by number of items - else - { - if (strtolower($this->getState('list.direction')) == 'asc') - { - asort($results); - } - else - { - arsort($results); - } - } - - // Pagination - $limit = (int) $this->getState('list.limit'); - - if ($limit !== 0) - { - $this->items = array_slice($results, $this->getState('list.start'), $limit); - } - else - { - $this->items = $results; - } - } - - return $this->items; - } + /** + * Count of the total items checked out + * + * @var integer + */ + protected $total; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'table', + 'count', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note: Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'table', $direction = 'asc') + { + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Checks in requested tables + * + * @param array $ids An array of table names. Optional. + * + * @return mixed The database results or 0 + * + * @since 1.6 + */ + public function checkin($ids = array()) + { + $db = $this->getDatabase(); + + if (!is_array($ids)) { + return 0; + } + + // This int will hold the checked item count. + $results = 0; + + $app = Factory::getApplication(); + + foreach ($ids as $tn) { + // Make sure we get the right tables based on prefix. + if (stripos($tn, $app->get('dbprefix')) !== 0) { + continue; + } + + $fields = $db->getTableColumns($tn, false); + + if (!(isset($fields['checked_out']) && isset($fields['checked_out_time']))) { + continue; + } + + $query = $db->getQuery(true) + ->update($db->quoteName($tn)) + ->set($db->quoteName('checked_out') . ' = DEFAULT'); + + if ($fields['checked_out_time']->Null === 'YES') { + $query->set($db->quoteName('checked_out_time') . ' = NULL'); + } else { + $nullDate = $db->getNullDate(); + + $query->set($db->quoteName('checked_out_time') . ' = :checkouttime') + ->bind(':checkouttime', $nullDate); + } + + if ($fields['checked_out']->Null === 'YES') { + $query->where($db->quoteName('checked_out') . ' IS NOT NULL'); + } else { + $query->where($db->quoteName('checked_out') . ' > 0'); + } + + $db->setQuery($query); + + if ($db->execute()) { + $results = $results + $db->getAffectedRows(); + $app->triggerEvent('onAfterCheckin', array($tn)); + } + } + + return $results; + } + + /** + * Get total of tables + * + * @return integer Total to check-in tables + * + * @since 1.6 + */ + public function getTotal() + { + if (!isset($this->total)) { + $this->getItems(); + } + + return $this->total; + } + + /** + * Get tables + * + * @return array Checked in table names as keys and checked in item count as values. + * + * @since 1.6 + */ + public function getItems() + { + if (!isset($this->items)) { + $db = $this->getDatabase(); + $tables = $db->getTableList(); + $prefix = Factory::getApplication()->get('dbprefix'); + + // This array will hold table name as key and checked in item count as value. + $results = array(); + + foreach ($tables as $tn) { + // Make sure we get the right tables based on prefix. + if (stripos($tn, $prefix) !== 0) { + continue; + } + + if ($this->getState('filter.search') && stripos($tn, $this->getState('filter.search')) === false) { + continue; + } + + $fields = $db->getTableColumns($tn, false); + + if (!(isset($fields['checked_out']) && isset($fields['checked_out_time']))) { + continue; + } + + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName($tn)); + + if ($fields['checked_out']->Null === 'YES') { + $query->where($db->quoteName('checked_out') . ' IS NOT NULL'); + } else { + $query->where($db->quoteName('checked_out') . ' > 0'); + } + + $db->setQuery($query); + $count = $db->loadResult(); + + if ($count) { + $results[$tn] = $count; + } + } + + $this->total = count($results); + + // Order items by table + if ($this->getState('list.ordering') == 'table') { + if (strtolower($this->getState('list.direction')) == 'asc') { + ksort($results); + } else { + krsort($results); + } + } else { + // Order items by number of items + if (strtolower($this->getState('list.direction')) == 'asc') { + asort($results); + } else { + arsort($results); + } + } + + // Pagination + $limit = (int) $this->getState('list.limit'); + + if ($limit !== 0) { + $this->items = array_slice($results, $this->getState('list.start'), $limit); + } else { + $this->items = $results; + } + } + + return $this->items; + } } diff --git a/administrator/components/com_checkin/src/View/Checkin/HtmlView.php b/administrator/components/com_checkin/src/View/Checkin/HtmlView.php index 7bb8218f1611d..6c08abab488f2 100644 --- a/administrator/components/com_checkin/src/View/Checkin/HtmlView.php +++ b/administrator/components/com_checkin/src/View/Checkin/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->total = $this->get('Total'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!\count($this->items)) - { - $this->isEmptyState = true; - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - ToolbarHelper::title(Text::_('COM_CHECKIN_GLOBAL_CHECK_IN'), 'check-square'); - - if (!$this->isEmptyState) - { - ToolbarHelper::custom('checkin', 'checkin', '', 'JTOOLBAR_CHECKIN', true); - } - - if (Factory::getApplication()->getIdentity()->authorise('core.admin', 'com_checkin')) - { - ToolbarHelper::divider(); - ToolbarHelper::preferences('com_checkin'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Maintenance:_Global_Check-in'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->total = $this->get('Total'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!\count($this->items)) { + $this->isEmptyState = true; + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + ToolbarHelper::title(Text::_('COM_CHECKIN_GLOBAL_CHECK_IN'), 'check-square'); + + if (!$this->isEmptyState) { + ToolbarHelper::custom('checkin', 'checkin', '', 'JTOOLBAR_CHECKIN', true); + } + + if (Factory::getApplication()->getIdentity()->authorise('core.admin', 'com_checkin')) { + ToolbarHelper::divider(); + ToolbarHelper::preferences('com_checkin'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Maintenance:_Global_Check-in'); + } } diff --git a/administrator/components/com_checkin/tmpl/checkin/default.php b/administrator/components/com_checkin/tmpl/checkin/default.php index 930cec729630d..1718632ffaa34 100644 --- a/administrator/components/com_checkin/tmpl/checkin/default.php +++ b/administrator/components/com_checkin/tmpl/checkin/default.php @@ -1,4 +1,5 @@ escape($this->state->get('list.direction')); ?>
-
-
-
- $this)); ?> - total > 0) : ?> - - - - - - - - - - - - items as $table => $count) : ?> - - - - - - - - -
- , - , - -
- - - - - -
- - - - - -
+
+
+
+ $this)); ?> + total > 0) : ?> + + + + + + + + + + + + items as $table => $count) : ?> + + + + + + + + +
+ , + , + +
+ + + + + +
+ + + + + +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
-
-
+ + + + +
+
+
diff --git a/administrator/components/com_checkin/tmpl/checkin/emptystate.php b/administrator/components/com_checkin/tmpl/checkin/emptystate.php index 9a85834893264..640e87e608e7b 100644 --- a/administrator/components/com_checkin/tmpl/checkin/emptystate.php +++ b/administrator/components/com_checkin/tmpl/checkin/emptystate.php @@ -1,4 +1,5 @@ 'COM_CHECKIN', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Maintenance:_Global_Check-in', - 'icon' => 'icon-check-square', - 'title' => Text::_('COM_CHECKIN_GLOBAL_CHECK_IN'), + 'textPrefix' => 'COM_CHECKIN', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Maintenance:_Global_Check-in', + 'icon' => 'icon-check-square', + 'title' => Text::_('COM_CHECKIN_GLOBAL_CHECK_IN'), ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_config/services/provider.php b/administrator/components/com_config/services/provider.php index d5a8df8a3d035..f0b39f879ae5f 100644 --- a/administrator/components/com_config/services/provider.php +++ b/administrator/components/com_config/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Config')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Config')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Config')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Config')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Config')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Config')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new ConfigComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new ConfigComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_config/src/Controller/ApplicationController.php b/administrator/components/com_config/src/Controller/ApplicationController.php index 9fc6687c48cab..7cc6227cc033b 100644 --- a/administrator/components/com_config/src/Controller/ApplicationController.php +++ b/administrator/components/com_config/src/Controller/ApplicationController.php @@ -1,4 +1,5 @@ registerTask('apply', 'save'); - } - - /** - * Cancel operation. - * - * @return void - * - * @since 3.0.0 - */ - public function cancel() - { - $this->setRedirect(Route::_('index.php?option=com_cpanel')); - } - - /** - * Saves the form - * - * @return void|boolean Void on success. Boolean false on fail. - * - * @since 4.0.0 - */ - public function save() - { - // Check for request forgeries. - $this->checkToken(); - - // Check if the user is authorized to do this. - if (!$this->app->getIdentity()->authorise('core.admin')) - { - $this->setRedirect('index.php', Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - - return false; - } - - $this->app->setUserState('com_config.config.global.data', null); - - /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ - $model = $this->getModel('Application', 'Administrator'); - - $data = $this->input->post->get('jform', array(), 'array'); - - // Complete data array if needed - $oldData = $model->getData(); - $data = array_replace($oldData, $data); - - // Get request type - $saveFormat = $this->app->getDocument()->getType(); - - // Handle service requests - if ($saveFormat == 'json') - { - $form = $model->getForm(); - $return = $model->validate($form, $data); - - if ($return === false) - { - $this->app->setHeader('Status', 422, true); - - return false; - } - - return $model->save($return); - } - - // Must load after serving service-requests - $form = $model->getForm(); - - // Validate the posted data. - $return = $model->validate($form, $data); - - // Check for validation errors. - if ($return === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $this->app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Save the posted data in the session. - $this->app->setUserState('com_config.config.global.data', $data); - - // Redirect back to the edit screen. - $this->setRedirect(Route::_('index.php?option=com_config', false)); - - return false; - } - - // Validate database connection data. - $data = $return; - $return = $model->validateDbConnection($data); - - // Check for validation errors. - if ($return === false) - { - /* - * The validateDbConnection method enqueued all messages for us. - */ - - // Save the posted data in the session. - $this->app->setUserState('com_config.config.global.data', $data); - - // Redirect back to the edit screen. - $this->setRedirect(Route::_('index.php?option=com_config', false)); - - return false; - } - - // Save the validated data in the session. - $this->app->setUserState('com_config.config.global.data', $return); - - // Attempt to save the configuration. - $data = $return; - $return = $model->save($data); - - // Check the return value. - if ($return === false) - { - /* - * The save method enqueued all messages for us, so we just need to redirect back. - */ - - // Save failed, go back to the screen and display a notice. - $this->setRedirect(Route::_('index.php?option=com_config', false)); - - return false; - } - - // Set the success message. - $this->app->enqueueMessage(Text::_('COM_CONFIG_SAVE_SUCCESS'), 'message'); - - // Set the redirect based on the task. - switch ($this->input->getCmd('task')) - { - case 'apply': - $this->setRedirect(Route::_('index.php?option=com_config', false)); - break; - - case 'save': - default: - $this->setRedirect(Route::_('index.php', false)); - break; - } - } - - /** - * Method to remove root in global configuration. - * - * @return boolean - * - * @since 3.2 - */ - public function removeroot() - { - // Check for request forgeries. - if (!Session::checkToken('get')) - { - $this->setRedirect('index.php', Text::_('JINVALID_TOKEN'), 'error'); - - return false; - } - - // Check if the user is authorized to do this. - if (!$this->app->getIdentity()->authorise('core.admin')) - { - $this->setRedirect('index.php', Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - - return false; - } - - // Initialise model. - - /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ - $model = $this->getModel('Application', 'Administrator'); - - // Attempt to save the configuration and remove root. - try - { - $model->removeroot(); - } - catch (\RuntimeException $e) - { - // Save failed, go back to the screen and display a notice. - $this->setRedirect('index.php', Text::_('JERROR_SAVE_FAILED', $e->getMessage()), 'error'); - - return false; - } - - // Set the redirect based on the task. - $this->setRedirect(Route::_('index.php'), Text::_('COM_CONFIG_SAVE_SUCCESS')); - - return true; - } - - /** - * Method to send the test mail. - * - * @return void - * - * @since 3.5 - */ - public function sendtestmail() - { - // Send json mime type. - $this->app->mimeType = 'application/json'; - $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); - $this->app->sendHeaders(); - - // Check if user token is valid. - if (!Session::checkToken()) - { - $this->app->enqueueMessage(Text::_('JINVALID_TOKEN'), 'error'); - echo new JsonResponse; - $this->app->close(); - } - - // Check if the user is authorized to do this. - if (!$this->app->getIdentity()->authorise('core.admin')) - { - $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - echo new JsonResponse; - $this->app->close(); - } - - /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ - $model = $this->getModel('Application', 'Administrator'); - - echo new JsonResponse($model->sendTestMail()); - - $this->app->close(); - } - - /** - * Method to GET permission value and give it to the model for storing in the database. - * - * @return void - * - * @since 3.5 - */ - public function store() - { - // Send json mime type. - $this->app->mimeType = 'application/json'; - $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); - $this->app->sendHeaders(); - - // Check if user token is valid. - if (!Session::checkToken('get')) - { - $this->app->enqueueMessage(Text::_('JINVALID_TOKEN'), 'error'); - echo new JsonResponse; - $this->app->close(); - } - - /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ - $model = $this->getModel('Application', 'Administrator'); - echo new JsonResponse($model->storePermissions()); - $this->app->close(); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * 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 Application for the dispatcher + * @param Input $input Input + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // Map the apply task to the save method. + $this->registerTask('apply', 'save'); + } + + /** + * Cancel operation. + * + * @return void + * + * @since 3.0.0 + */ + public function cancel() + { + $this->setRedirect(Route::_('index.php?option=com_cpanel')); + } + + /** + * Saves the form + * + * @return void|boolean Void on success. Boolean false on fail. + * + * @since 4.0.0 + */ + public function save() + { + // Check for request forgeries. + $this->checkToken(); + + // Check if the user is authorized to do this. + if (!$this->app->getIdentity()->authorise('core.admin')) { + $this->setRedirect('index.php', Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + + return false; + } + + $this->app->setUserState('com_config.config.global.data', null); + + /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ + $model = $this->getModel('Application', 'Administrator'); + + $data = $this->input->post->get('jform', array(), 'array'); + + // Complete data array if needed + $oldData = $model->getData(); + $data = array_replace($oldData, $data); + + // Get request type + $saveFormat = $this->app->getDocument()->getType(); + + // Handle service requests + if ($saveFormat == 'json') { + $form = $model->getForm(); + $return = $model->validate($form, $data); + + if ($return === false) { + $this->app->setHeader('Status', 422, true); + + return false; + } + + return $model->save($return); + } + + // Must load after serving service-requests + $form = $model->getForm(); + + // Validate the posted data. + $return = $model->validate($form, $data); + + // Check for validation errors. + if ($return === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $this->app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Save the posted data in the session. + $this->app->setUserState('com_config.config.global.data', $data); + + // Redirect back to the edit screen. + $this->setRedirect(Route::_('index.php?option=com_config', false)); + + return false; + } + + // Validate database connection data. + $data = $return; + $return = $model->validateDbConnection($data); + + // Check for validation errors. + if ($return === false) { + /* + * The validateDbConnection method enqueued all messages for us. + */ + + // Save the posted data in the session. + $this->app->setUserState('com_config.config.global.data', $data); + + // Redirect back to the edit screen. + $this->setRedirect(Route::_('index.php?option=com_config', false)); + + return false; + } + + // Save the validated data in the session. + $this->app->setUserState('com_config.config.global.data', $return); + + // Attempt to save the configuration. + $data = $return; + $return = $model->save($data); + + // Check the return value. + if ($return === false) { + /* + * The save method enqueued all messages for us, so we just need to redirect back. + */ + + // Save failed, go back to the screen and display a notice. + $this->setRedirect(Route::_('index.php?option=com_config', false)); + + return false; + } + + // Set the success message. + $this->app->enqueueMessage(Text::_('COM_CONFIG_SAVE_SUCCESS'), 'message'); + + // Set the redirect based on the task. + switch ($this->input->getCmd('task')) { + case 'apply': + $this->setRedirect(Route::_('index.php?option=com_config', false)); + break; + + case 'save': + default: + $this->setRedirect(Route::_('index.php', false)); + break; + } + } + + /** + * Method to remove root in global configuration. + * + * @return boolean + * + * @since 3.2 + */ + public function removeroot() + { + // Check for request forgeries. + if (!Session::checkToken('get')) { + $this->setRedirect('index.php', Text::_('JINVALID_TOKEN'), 'error'); + + return false; + } + + // Check if the user is authorized to do this. + if (!$this->app->getIdentity()->authorise('core.admin')) { + $this->setRedirect('index.php', Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + + return false; + } + + // Initialise model. + + /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ + $model = $this->getModel('Application', 'Administrator'); + + // Attempt to save the configuration and remove root. + try { + $model->removeroot(); + } catch (\RuntimeException $e) { + // Save failed, go back to the screen and display a notice. + $this->setRedirect('index.php', Text::_('JERROR_SAVE_FAILED', $e->getMessage()), 'error'); + + return false; + } + + // Set the redirect based on the task. + $this->setRedirect(Route::_('index.php'), Text::_('COM_CONFIG_SAVE_SUCCESS')); + + return true; + } + + /** + * Method to send the test mail. + * + * @return void + * + * @since 3.5 + */ + public function sendtestmail() + { + // Send json mime type. + $this->app->mimeType = 'application/json'; + $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); + $this->app->sendHeaders(); + + // Check if user token is valid. + if (!Session::checkToken()) { + $this->app->enqueueMessage(Text::_('JINVALID_TOKEN'), 'error'); + echo new JsonResponse(); + $this->app->close(); + } + + // Check if the user is authorized to do this. + if (!$this->app->getIdentity()->authorise('core.admin')) { + $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + echo new JsonResponse(); + $this->app->close(); + } + + /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ + $model = $this->getModel('Application', 'Administrator'); + + echo new JsonResponse($model->sendTestMail()); + + $this->app->close(); + } + + /** + * Method to GET permission value and give it to the model for storing in the database. + * + * @return void + * + * @since 3.5 + */ + public function store() + { + // Send json mime type. + $this->app->mimeType = 'application/json'; + $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); + $this->app->sendHeaders(); + + // Check if user token is valid. + if (!Session::checkToken('get')) { + $this->app->enqueueMessage(Text::_('JINVALID_TOKEN'), 'error'); + echo new JsonResponse(); + $this->app->close(); + } + + /** @var \Joomla\Component\Config\Administrator\Model\ApplicationModel $model */ + $model = $this->getModel('Application', 'Administrator'); + echo new JsonResponse($model->storePermissions()); + $this->app->close(); + } } diff --git a/administrator/components/com_config/src/Controller/ComponentController.php b/administrator/components/com_config/src/Controller/ComponentController.php index a7718e366b545..791c1a1ef66ca 100644 --- a/administrator/components/com_config/src/Controller/ComponentController.php +++ b/administrator/components/com_config/src/Controller/ComponentController.php @@ -1,4 +1,5 @@ registerTask('apply', 'save'); - } - - /** - * Method to save component configuration. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean - * - * @since 3.2 - */ - public function save($key = null, $urlVar = null) - { - // Check for request forgeries. - $this->checkToken(); - - $data = $this->input->get('jform', [], 'ARRAY'); - $id = $this->input->get('id', null, 'INT'); - $option = $this->input->get('component'); - $user = $this->app->getIdentity(); - $context = "$this->option.edit.$this->context.$option"; - - /** @var \Joomla\Component\Config\Administrator\Model\ComponentModel $model */ - $model = $this->getModel('Component', 'Administrator'); - $model->setState('component.option', $option); - $form = $model->getForm(); - - // Make sure com_joomlaupdate and com_privacy can only be accessed by SuperUser - if (\in_array(strtolower($option), ['com_joomlaupdate', 'com_privacy'], true) && !$user->authorise('core.admin')) - { - $this->setRedirect(Route::_('index.php', false), Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - } - - // Check if the user is authorised to do this. - if (!$user->authorise('core.admin', $option) && !$user->authorise('core.options', $option)) - { - $this->setRedirect(Route::_('index.php', false), Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - } - - // Remove the permissions rules data if user isn't allowed to edit them. - if (!$user->authorise('core.admin', $option) && isset($data['params']) && isset($data['params']['rules'])) - { - unset($data['params']['rules']); - } - - $returnUri = $this->input->post->get('return', null, 'base64'); - - $redirect = ''; - - if (!empty($returnUri)) - { - $redirect = '&return=' . urlencode($returnUri); - } - - // Validate the posted data. - $return = $model->validate($form, $data); - - // Check for validation errors. - if ($return === false) - { - // Save the data in the session. - $this->app->setUserState($context . '.data', $data); - - // Redirect back to the edit screen. - $this->setRedirect( - Route::_('index.php?option=com_config&view=component&component=' . $option . $redirect, false), - $model->getError(), - 'error' - ); - - return false; - } - - // Attempt to save the configuration. - $data = [ - 'params' => $return, - 'id' => $id, - 'option' => $option, - ]; - - try - { - $model->save($data); - } - catch (\RuntimeException $e) - { - // Save the data in the session. - $this->app->setUserState($context . '.data', $data); - - // Save failed, go back to the screen and display a notice. - $this->setRedirect( - Route::_('index.php?option=com_config&view=component&component=' . $option . $redirect, false), - Text::_('JERROR_SAVE_FAILED', $e->getMessage()), - 'error' - ); - - return false; - } - - // Clear session data. - $this->app->setUserState($context . '.data', null); - - // Set the redirect based on the task. - switch ($this->input->get('task')) - { - case 'apply': - $this->setRedirect( - Route::_('index.php?option=com_config&view=component&component=' . $option . $redirect, false), - Text::_('COM_CONFIG_SAVE_SUCCESS'), - 'message' - ); - - break; - - case 'save': - $this->setMessage(Text::_('COM_CONFIG_SAVE_SUCCESS'), 'message'); - default: - $redirect = 'index.php?option=' . $option; - - if (!empty($returnUri)) - { - $redirect = base64_decode($returnUri); - } - - // Don't redirect to an external URL. - if (!Uri::isInternal($redirect)) - { - $redirect = Uri::base(); - } - - $this->setRedirect(Route::_($redirect, false)); - } - - return true; - } - - /** - * Method to cancel global configuration component. - * - * @param string $key The name of the primary key of the URL variable. - * - * @return boolean - * - * @since 3.2 - */ - public function cancel($key = null) - { - $component = $this->input->get('component'); - - // Clear session data. - $this->app->setUserState("$this->option.edit.$this->context.$component.data", null); - - // Calculate redirect URL - $returnUri = $this->input->post->get('return', null, 'base64'); - - $redirect = 'index.php?option=' . $component; - - if (!empty($returnUri)) - { - $redirect = base64_decode($returnUri); - } - - // Don't redirect to an external URL. - if (!Uri::isInternal($redirect)) - { - $redirect = Uri::base(); - } - - $this->setRedirect(Route::_($redirect, false)); - - return true; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * 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 Application for the dispatcher + * @param Input $input Input + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // Map the apply task to the save method. + $this->registerTask('apply', 'save'); + } + + /** + * Method to save component configuration. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean + * + * @since 3.2 + */ + public function save($key = null, $urlVar = null) + { + // Check for request forgeries. + $this->checkToken(); + + $data = $this->input->get('jform', [], 'ARRAY'); + $id = $this->input->get('id', null, 'INT'); + $option = $this->input->get('component'); + $user = $this->app->getIdentity(); + $context = "$this->option.edit.$this->context.$option"; + + /** @var \Joomla\Component\Config\Administrator\Model\ComponentModel $model */ + $model = $this->getModel('Component', 'Administrator'); + $model->setState('component.option', $option); + $form = $model->getForm(); + + // Make sure com_joomlaupdate and com_privacy can only be accessed by SuperUser + if (\in_array(strtolower($option), ['com_joomlaupdate', 'com_privacy'], true) && !$user->authorise('core.admin')) { + $this->setRedirect(Route::_('index.php', false), Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + } + + // Check if the user is authorised to do this. + if (!$user->authorise('core.admin', $option) && !$user->authorise('core.options', $option)) { + $this->setRedirect(Route::_('index.php', false), Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + } + + // Remove the permissions rules data if user isn't allowed to edit them. + if (!$user->authorise('core.admin', $option) && isset($data['params']) && isset($data['params']['rules'])) { + unset($data['params']['rules']); + } + + $returnUri = $this->input->post->get('return', null, 'base64'); + + $redirect = ''; + + if (!empty($returnUri)) { + $redirect = '&return=' . urlencode($returnUri); + } + + // Validate the posted data. + $return = $model->validate($form, $data); + + // Check for validation errors. + if ($return === false) { + // Save the data in the session. + $this->app->setUserState($context . '.data', $data); + + // Redirect back to the edit screen. + $this->setRedirect( + Route::_('index.php?option=com_config&view=component&component=' . $option . $redirect, false), + $model->getError(), + 'error' + ); + + return false; + } + + // Attempt to save the configuration. + $data = [ + 'params' => $return, + 'id' => $id, + 'option' => $option, + ]; + + try { + $model->save($data); + } catch (\RuntimeException $e) { + // Save the data in the session. + $this->app->setUserState($context . '.data', $data); + + // Save failed, go back to the screen and display a notice. + $this->setRedirect( + Route::_('index.php?option=com_config&view=component&component=' . $option . $redirect, false), + Text::_('JERROR_SAVE_FAILED', $e->getMessage()), + 'error' + ); + + return false; + } + + // Clear session data. + $this->app->setUserState($context . '.data', null); + + // Set the redirect based on the task. + switch ($this->input->get('task')) { + case 'apply': + $this->setRedirect( + Route::_('index.php?option=com_config&view=component&component=' . $option . $redirect, false), + Text::_('COM_CONFIG_SAVE_SUCCESS'), + 'message' + ); + + break; + + case 'save': + $this->setMessage(Text::_('COM_CONFIG_SAVE_SUCCESS'), 'message'); + + // No break + + default: + $redirect = 'index.php?option=' . $option; + + if (!empty($returnUri)) { + $redirect = base64_decode($returnUri); + } + + // Don't redirect to an external URL. + if (!Uri::isInternal($redirect)) { + $redirect = Uri::base(); + } + + $this->setRedirect(Route::_($redirect, false)); + } + + return true; + } + + /** + * Method to cancel global configuration component. + * + * @param string $key The name of the primary key of the URL variable. + * + * @return boolean + * + * @since 3.2 + */ + public function cancel($key = null) + { + $component = $this->input->get('component'); + + // Clear session data. + $this->app->setUserState("$this->option.edit.$this->context.$component.data", null); + + // Calculate redirect URL + $returnUri = $this->input->post->get('return', null, 'base64'); + + $redirect = 'index.php?option=' . $component; + + if (!empty($returnUri)) { + $redirect = base64_decode($returnUri); + } + + // Don't redirect to an external URL. + if (!Uri::isInternal($redirect)) { + $redirect = Uri::base(); + } + + $this->setRedirect(Route::_($redirect, false)); + + return true; + } } diff --git a/administrator/components/com_config/src/Controller/DisplayController.php b/administrator/components/com_config/src/Controller/DisplayController.php index 2ed6a6f030c14..5a93d0515676d 100644 --- a/administrator/components/com_config/src/Controller/DisplayController.php +++ b/administrator/components/com_config/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('component', ''); - - // Make sure com_joomlaupdate and com_privacy can only be accessed by SuperUser - if (in_array(strtolower($component), array('com_joomlaupdate', 'com_privacy')) - && !$this->app->getIdentity()->authorise('core.admin')) - { - $this->setRedirect(Route::_('index.php'), Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - } - - return parent::display($cachable, $urlparams); - } + /** + * The default view. + * + * @var string + * @since 1.6 + */ + protected $default_view = 'application'; + + + /** + * Typical view method for MVC based architecture + * + * This function is provide as a default implementation, in most cases + * 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 InputFilter::clean()}. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 3.0 + * @throws \Exception + */ + public function display($cachable = false, $urlparams = array()) + { + $component = $this->input->get('component', ''); + + // Make sure com_joomlaupdate and com_privacy can only be accessed by SuperUser + if ( + in_array(strtolower($component), array('com_joomlaupdate', 'com_privacy')) + && !$this->app->getIdentity()->authorise('core.admin') + ) { + $this->setRedirect(Route::_('index.php'), Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + } + + return parent::display($cachable, $urlparams); + } } diff --git a/administrator/components/com_config/src/Controller/RequestController.php b/administrator/components/com_config/src/Controller/RequestController.php index f3121774503e9..4b4c17f167ff3 100644 --- a/administrator/components/com_config/src/Controller/RequestController.php +++ b/administrator/components/com_config/src/Controller/RequestController.php @@ -1,4 +1,5 @@ input->getWord('option', 'com_config'); - - if ($this->app->isClient('administrator')) - { - $viewName = $this->input->getWord('view', 'application'); - } - else - { - $viewName = $this->input->getWord('view', 'config'); - } - - // Register the layout paths for the view - $paths = new \SplPriorityQueue; - - if ($this->app->isClient('administrator')) - { - $paths->insert(JPATH_ADMINISTRATOR . '/components/' . $componentFolder . '/view/' . $viewName . '/tmpl', 1); - } - else - { - $paths->insert(JPATH_BASE . '/components/' . $componentFolder . '/view/' . $viewName . '/tmpl', 1); - } - - $model = new \Joomla\Component\Config\Administrator\Model\ApplicationModel; - $component = $model->getState()->get('component.option'); - - // Access check. - if (!$this->app->getIdentity()->authorise('core.admin', $component) - && !$this->app->getIdentity()->authorise('core.options', $component)) - { - $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - - return false; - } - - try - { - $data = $model->getData(); - $user = $this->app->getIdentity(); - } - catch (\Exception $e) - { - $this->app->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - $this->userIsSuperAdmin = $user->authorise('core.admin'); - - // Required data - $requiredData = array( - 'sitename' => null, - 'offline' => null, - 'access' => null, - 'list_limit' => null, - 'MetaDesc' => null, - 'MetaRights' => null, - 'sef' => null, - 'sitename_pagetitles' => null, - 'debug' => null, - 'debug_lang' => null, - 'error_reporting' => null, - 'mailfrom' => null, - 'fromname' => null - ); - - $data = array_intersect_key($data, $requiredData); - - return json_encode($data); - } + /** + * Execute the controller. + * + * @return mixed A rendered view or false + * + * @since 3.2 + */ + public function getJson() + { + $componentFolder = $this->input->getWord('option', 'com_config'); + + if ($this->app->isClient('administrator')) { + $viewName = $this->input->getWord('view', 'application'); + } else { + $viewName = $this->input->getWord('view', 'config'); + } + + // Register the layout paths for the view + $paths = new \SplPriorityQueue(); + + if ($this->app->isClient('administrator')) { + $paths->insert(JPATH_ADMINISTRATOR . '/components/' . $componentFolder . '/view/' . $viewName . '/tmpl', 1); + } else { + $paths->insert(JPATH_BASE . '/components/' . $componentFolder . '/view/' . $viewName . '/tmpl', 1); + } + + $model = new \Joomla\Component\Config\Administrator\Model\ApplicationModel(); + $component = $model->getState()->get('component.option'); + + // Access check. + if ( + !$this->app->getIdentity()->authorise('core.admin', $component) + && !$this->app->getIdentity()->authorise('core.options', $component) + ) { + $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + + return false; + } + + try { + $data = $model->getData(); + $user = $this->app->getIdentity(); + } catch (\Exception $e) { + $this->app->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + $this->userIsSuperAdmin = $user->authorise('core.admin'); + + // Required data + $requiredData = array( + 'sitename' => null, + 'offline' => null, + 'access' => null, + 'list_limit' => null, + 'MetaDesc' => null, + 'MetaRights' => null, + 'sef' => null, + 'sitename_pagetitles' => null, + 'debug' => null, + 'debug_lang' => null, + 'error_reporting' => null, + 'mailfrom' => null, + 'fromname' => null + ); + + $data = array_intersect_key($data, $requiredData); + + return json_encode($data); + } } diff --git a/administrator/components/com_config/src/Extension/ConfigComponent.php b/administrator/components/com_config/src/Extension/ConfigComponent.php index f275a9d63321c..6a94cf65712a9 100644 --- a/administrator/components/com_config/src/Extension/ConfigComponent.php +++ b/administrator/components/com_config/src/Extension/ConfigComponent.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select('name AS text, element AS value') - ->from('#__extensions') - ->where('enabled >= 1') - ->where('type =' . $db->quote('component')); + /** + * Method to get a list of options for a list input. + * + * @return array An array of JHtml options. + * + * @since 3.7.0 + */ + protected function getOptions() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('name AS text, element AS value') + ->from('#__extensions') + ->where('enabled >= 1') + ->where('type =' . $db->quote('component')); - $items = $db->setQuery($query)->loadObjectList(); + $items = $db->setQuery($query)->loadObjectList(); - if ($items) - { - $lang = Factory::getLanguage(); + if ($items) { + $lang = Factory::getLanguage(); - foreach ($items as &$item) - { - // Load language - $extension = $item->value; + foreach ($items as &$item) { + // Load language + $extension = $item->value; - if (File::exists(JPATH_ADMINISTRATOR . '/components/' . $extension . '/config.xml')) - { - $source = JPATH_ADMINISTRATOR . '/components/' . $extension; - $lang->load("$extension.sys", JPATH_ADMINISTRATOR) - || $lang->load("$extension.sys", $source); + if (File::exists(JPATH_ADMINISTRATOR . '/components/' . $extension . '/config.xml')) { + $source = JPATH_ADMINISTRATOR . '/components/' . $extension; + $lang->load("$extension.sys", JPATH_ADMINISTRATOR) + || $lang->load("$extension.sys", $source); - // Translate component name - $item->text = Text::_($item->text); - } - else - { - $item = null; - } - } + // Translate component name + $item->text = Text::_($item->text); + } else { + $item = null; + } + } - // Sort by component name - $items = ArrayHelper::sortObjects(array_filter($items), 'text', 1, true, true); - } + // Sort by component name + $items = ArrayHelper::sortObjects(array_filter($items), 'text', 1, true, true); + } - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $items); + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $items); - return $options; - } + return $options; + } } diff --git a/administrator/components/com_config/src/Field/FiltersField.php b/administrator/components/com_config/src/Field/FiltersField.php index 052c548face8b..34f18dbef7bab 100644 --- a/administrator/components/com_config/src/Field/FiltersField.php +++ b/administrator/components/com_config/src/Field/FiltersField.php @@ -1,4 +1,5 @@ getWebAssetManager()->useScript('com_config.filters'); - - // Get the available user groups. - $groups = $this->getUserGroups(); - - // Build the form control. - $html = array(); - - // Open the table. - $html[] = ''; - - // The table heading. - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - - // The table body. - $html[] = ' '; - - foreach ($groups as $group) - { - if (!isset($this->value[$group->value])) - { - $this->value[$group->value] = array('filter_type' => 'BL', 'filter_tags' => '', 'filter_attributes' => ''); - } - - $group_filter = $this->value[$group->value]; - - $group_filter['filter_tags'] = !empty($group_filter['filter_tags']) ? $group_filter['filter_tags'] : ''; - $group_filter['filter_attributes'] = !empty($group_filter['filter_attributes']) ? $group_filter['filter_attributes'] : ''; - - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - } - - $html[] = ' '; - - // Close the table. - $html[] = '
'; - $html[] = ' ' . Text::_('JGLOBAL_FILTER_GROUPS_LABEL') . ''; - $html[] = ' '; - $html[] = ' ' . Text::_('JGLOBAL_FILTER_TYPE_LABEL') . ''; - $html[] = ' '; - $html[] = ' ' . Text::_('JGLOBAL_FILTER_TAGS_LABEL') . ''; - $html[] = ' '; - $html[] = ' ' . Text::_('JGLOBAL_FILTER_ATTRIBUTES_LABEL') . ''; - $html[] = '
'; - $html[] = ' ' . LayoutHelper::render('joomla.html.treeprefix', array('level' => $group->level + 1)) . $group->text; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = ' '; - $html[] = '
'; - - return implode("\n", $html); - } - - /** - * A helper to get the list of user groups. - * - * @return array - * - * @since 1.6 - */ - protected function getUserGroups() - { - // Get a database object. - $db = $this->getDatabase(); - - // Get the user groups from the database. - $query = $db->getQuery(true); - $query->select('a.id AS value, a.title AS text, COUNT(DISTINCT b.id) AS level, a.parent_id as parent'); - $query->from('#__usergroups AS a'); - $query->join('LEFT', '#__usergroups AS b on a.lft > b.lft AND a.rgt < b.rgt'); - $query->group('a.id, a.title, a.lft'); - $query->order('a.lft ASC'); - $db->setQuery($query); - $options = $db->loadObjectList(); - - return $options; - } + /** + * The form field type. + * + * @var string + * @since 1.6 + */ + public $type = 'Filters'; + + /** + * Method to get the field input markup. + * + * @todo: Add access check. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + // Add translation string for notification + Text::script('COM_CONFIG_TEXT_FILTERS_NOTE'); + + // Add Javascript + Factory::getDocument()->getWebAssetManager()->useScript('com_config.filters'); + + // Get the available user groups. + $groups = $this->getUserGroups(); + + // Build the form control. + $html = array(); + + // Open the table. + $html[] = ''; + + // The table heading. + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + + // The table body. + $html[] = ' '; + + foreach ($groups as $group) { + if (!isset($this->value[$group->value])) { + $this->value[$group->value] = array('filter_type' => 'BL', 'filter_tags' => '', 'filter_attributes' => ''); + } + + $group_filter = $this->value[$group->value]; + + $group_filter['filter_tags'] = !empty($group_filter['filter_tags']) ? $group_filter['filter_tags'] : ''; + $group_filter['filter_attributes'] = !empty($group_filter['filter_attributes']) ? $group_filter['filter_attributes'] : ''; + + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + } + + $html[] = ' '; + + // Close the table. + $html[] = '
'; + $html[] = ' ' . Text::_('JGLOBAL_FILTER_GROUPS_LABEL') . ''; + $html[] = ' '; + $html[] = ' ' . Text::_('JGLOBAL_FILTER_TYPE_LABEL') . ''; + $html[] = ' '; + $html[] = ' ' . Text::_('JGLOBAL_FILTER_TAGS_LABEL') . ''; + $html[] = ' '; + $html[] = ' ' . Text::_('JGLOBAL_FILTER_ATTRIBUTES_LABEL') . ''; + $html[] = '
'; + $html[] = ' ' . LayoutHelper::render('joomla.html.treeprefix', array('level' => $group->level + 1)) . $group->text; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = ' '; + $html[] = '
'; + + return implode("\n", $html); + } + + /** + * A helper to get the list of user groups. + * + * @return array + * + * @since 1.6 + */ + protected function getUserGroups() + { + // Get a database object. + $db = $this->getDatabase(); + + // Get the user groups from the database. + $query = $db->getQuery(true); + $query->select('a.id AS value, a.title AS text, COUNT(DISTINCT b.id) AS level, a.parent_id as parent'); + $query->from('#__usergroups AS a'); + $query->join('LEFT', '#__usergroups AS b on a.lft > b.lft AND a.rgt < b.rgt'); + $query->group('a.id, a.title, a.lft'); + $query->order('a.lft ASC'); + $db->setQuery($query); + $options = $db->loadObjectList(); + + return $options; + } } diff --git a/administrator/components/com_config/src/Helper/ConfigHelper.php b/administrator/components/com_config/src/Helper/ConfigHelper.php index ee5ed8b3d84f5..ac51bd0f83f03 100644 --- a/administrator/components/com_config/src/Helper/ConfigHelper.php +++ b/administrator/components/com_config/src/Helper/ConfigHelper.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('element') - ->from('#__extensions') - ->where('type = ' . $db->quote('component')) - ->where('enabled = 1'); - $db->setQuery($query); - $result = $db->loadColumn(); + /** + * Get an array of all enabled components. + * + * @return array + * + * @since 3.0 + */ + public static function getAllComponents() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('element') + ->from('#__extensions') + ->where('type = ' . $db->quote('component')) + ->where('enabled = 1'); + $db->setQuery($query); + $result = $db->loadColumn(); - return $result; - } + return $result; + } - /** - * Returns true if the component has configuration options. - * - * @param string $component Component name - * - * @return boolean - * - * @since 3.0 - */ - public static function hasComponentConfig($component) - { - return is_file(JPATH_ADMINISTRATOR . '/components/' . $component . '/config.xml'); - } + /** + * Returns true if the component has configuration options. + * + * @param string $component Component name + * + * @return boolean + * + * @since 3.0 + */ + public static function hasComponentConfig($component) + { + return is_file(JPATH_ADMINISTRATOR . '/components/' . $component . '/config.xml'); + } - /** - * Returns an array of all components with configuration options. - * Optionally return only those components for which the current user has 'core.manage' rights. - * - * @param boolean $authCheck True to restrict to components where current user has 'core.manage' rights. - * - * @return array - * - * @since 3.0 - */ - public static function getComponentsWithConfig($authCheck = true) - { - $result = array(); - $components = self::getAllComponents(); - $user = Factory::getUser(); + /** + * Returns an array of all components with configuration options. + * Optionally return only those components for which the current user has 'core.manage' rights. + * + * @param boolean $authCheck True to restrict to components where current user has 'core.manage' rights. + * + * @return array + * + * @since 3.0 + */ + public static function getComponentsWithConfig($authCheck = true) + { + $result = array(); + $components = self::getAllComponents(); + $user = Factory::getUser(); - // Remove com_config from the array as that may have weird side effects - $components = array_diff($components, array('com_config')); + // Remove com_config from the array as that may have weird side effects + $components = array_diff($components, array('com_config')); - foreach ($components as $component) - { - if (self::hasComponentConfig($component) && (!$authCheck || $user->authorise('core.manage', $component))) - { - self::loadLanguageForComponent($component); - $result[$component] = ApplicationHelper::stringURLSafe(Text::_($component)) . '_' . $component; - } - } + foreach ($components as $component) { + if (self::hasComponentConfig($component) && (!$authCheck || $user->authorise('core.manage', $component))) { + self::loadLanguageForComponent($component); + $result[$component] = ApplicationHelper::stringURLSafe(Text::_($component)) . '_' . $component; + } + } - asort($result); + asort($result); - return array_keys($result); - } + return array_keys($result); + } - /** - * Load the sys language for the given component. - * - * @param array $components Array of component names. - * - * @return void - * - * @since 3.0 - */ - public static function loadLanguageForComponents($components) - { - foreach ($components as $component) - { - self::loadLanguageForComponent($component); - } - } + /** + * Load the sys language for the given component. + * + * @param array $components Array of component names. + * + * @return void + * + * @since 3.0 + */ + public static function loadLanguageForComponents($components) + { + foreach ($components as $component) { + self::loadLanguageForComponent($component); + } + } - /** - * Load the sys language for the given component. - * - * @param string $component component name. - * - * @return void - * - * @since 3.5 - */ - public static function loadLanguageForComponent($component) - { - if (empty($component)) - { - return; - } + /** + * Load the sys language for the given component. + * + * @param string $component component name. + * + * @return void + * + * @since 3.5 + */ + public static function loadLanguageForComponent($component) + { + if (empty($component)) { + return; + } - $lang = Factory::getLanguage(); + $lang = Factory::getLanguage(); - // Load the core file then - // Load extension-local file. - $lang->load($component . '.sys', JPATH_BASE) - || $lang->load($component . '.sys', JPATH_ADMINISTRATOR . '/components/' . $component); - } + // Load the core file then + // Load extension-local file. + $lang->load($component . '.sys', JPATH_BASE) + || $lang->load($component . '.sys', JPATH_ADMINISTRATOR . '/components/' . $component); + } } diff --git a/administrator/components/com_config/src/Model/ApplicationModel.php b/administrator/components/com_config/src/Model/ApplicationModel.php index 77d70c8669785..81c546ff8be96 100644 --- a/administrator/components/com_config/src/Model/ApplicationModel.php +++ b/administrator/components/com_config/src/Model/ApplicationModel.php @@ -1,4 +1,5 @@ loadForm('com_config.application', 'application', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the configuration data. - * - * This method will load the global configuration data straight from - * JConfig. If configuration data has been saved in the session, that - * data will be merged into the original data, overwriting it. - * - * @return array An array containing all global config data. - * - * @since 1.6 - */ - public function getData() - { - // Get the config data. - $config = new \JConfig; - $data = ArrayHelper::fromObject($config); - - // Get the correct driver at runtime - $data['dbtype'] = $this->getDatabase()->getName(); - - // Prime the asset_id for the rules. - $data['asset_id'] = 1; - - // Get the text filter data - $params = ComponentHelper::getParams('com_config'); - $data['filters'] = ArrayHelper::fromObject($params->get('filters')); - - // If no filter data found, get from com_content (update of 1.6/1.7 site) - if (empty($data['filters'])) - { - $contentParams = ComponentHelper::getParams('com_content'); - $data['filters'] = ArrayHelper::fromObject($contentParams->get('filters')); - } - - // Check for data in the session. - $temp = Factory::getApplication()->getUserState('com_config.config.global.data'); - - // Merge in the session data. - if (!empty($temp)) - { - // $temp can sometimes be an object, and we need it to be an array - if (is_object($temp)) - { - $temp = ArrayHelper::fromObject($temp); - } - - $data = array_merge($temp, $data); - } - - // Correct error_reporting value, since we removed "development", the "maximum" should be set instead - // @TODO: This can be removed in 5.0 - if (!empty($data['error_reporting']) && $data['error_reporting'] === 'development') - { - $data['error_reporting'] = 'maximum'; - } - - return $data; - } - - /** - * Method to validate the db connection properties. - * - * @param array $data An array containing all global config data. - * - * @return array|boolean Array with the validated global config data or boolean false on a validation failure. - * - * @since 4.0.0 - */ - public function validateDbConnection($data) - { - // Validate database connection encryption options - if ((int) $data['dbencryption'] === 0) - { - // Reset unused options - if (!empty($data['dbsslkey'])) - { - $data['dbsslkey'] = ''; - } - - if (!empty($data['dbsslcert'])) - { - $data['dbsslcert'] = ''; - } - - if ((bool) $data['dbsslverifyservercert'] === true) - { - $data['dbsslverifyservercert'] = false; - } - - if (!empty($data['dbsslca'])) - { - $data['dbsslca'] = ''; - } - - if (!empty($data['dbsslcipher'])) - { - $data['dbsslcipher'] = ''; - } - } - else - { - // Check localhost - if (strtolower($data['host']) === 'localhost') - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_DATABASE_ENCRYPTION_LOCALHOST'), 'error'); - - return false; - } - - // Check CA file and folder depending on database type if server certificate verification - if ((bool) $data['dbsslverifyservercert'] === true) - { - if (empty($data['dbsslca'])) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_EMPTY', - Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CA_LABEL') - ), - 'error' - ); - - return false; - } - - if (!File::exists(Path::clean($data['dbsslca']))) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_BAD', - Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CA_LABEL') - ), - 'error' - ); - - return false; - } - } - else - { - // Reset unused option - if (!empty($data['dbsslca'])) - { - $data['dbsslca'] = ''; - } - } - - // Check key and certificate if two-way encryption - if ((int) $data['dbencryption'] === 2) - { - if (empty($data['dbsslkey'])) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_EMPTY', - Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_KEY_LABEL') - ), - 'error' - ); - - return false; - } - - if (!File::exists(Path::clean($data['dbsslkey']))) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_BAD', - Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_KEY_LABEL') - ), - 'error' - ); - - return false; - } - - if (empty($data['dbsslcert'])) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_EMPTY', - Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CERT_LABEL') - ), - 'error' - ); - - return false; - } - - if (!File::exists(Path::clean($data['dbsslcert']))) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_BAD', - Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CERT_LABEL') - ), - 'error' - ); - - return false; - } - } - else - { - // Reset unused options - if (!empty($data['dbsslkey'])) - { - $data['dbsslkey'] = ''; - } - - if (!empty($data['dbsslcert'])) - { - $data['dbsslcert'] = ''; - } - } - } - - return $data; - } - - /** - * Method to save the configuration data. - * - * @param array $data An array containing all global config data. - * - * @return boolean True on success, false on failure. - * - * @since 1.6 - */ - public function save($data) - { - $app = Factory::getApplication(); - - // Try to load the values from the configuration file - foreach ($this->protectedConfigurationFields as $fieldKey) - { - if (!isset($data[$fieldKey])) - { - $data[$fieldKey] = $app->get($fieldKey, ''); - } - } - - // Check that we aren't setting wrong database configuration - $options = array( - 'driver' => $data['dbtype'], - 'host' => $data['host'], - 'user' => $data['user'], - 'password' => $data['password'], - 'database' => $data['db'], - 'prefix' => $data['dbprefix'], - ); - - if ((int) $data['dbencryption'] !== 0) - { - $options['ssl'] = [ - 'enable' => true, - 'verify_server_cert' => (bool) $data['dbsslverifyservercert'], - ]; - - foreach (['cipher', 'ca', 'key', 'cert'] as $value) - { - $confVal = trim($data['dbssl' . $value]); - - if ($confVal !== '') - { - $options['ssl'][$value] = $confVal; - } - } - } - - try - { - $revisedDbo = DatabaseDriver::getInstance($options); - $revisedDbo->getVersion(); - } - catch (\Exception $e) - { - $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_DATABASE_NOT_AVAILABLE', $e->getCode(), $e->getMessage()), 'error'); - - return false; - } - - if ((int) $data['dbencryption'] !== 0 && empty($revisedDbo->getConnectionEncryption())) - { - if ($revisedDbo->isConnectionEncryptionSupported()) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_DATABASE_ENCRYPTION_CONN_NOT_ENCRYPT'), 'error'); - } - else - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_DATABASE_ENCRYPTION_SRV_NOT_SUPPORTS'), 'error'); - } - - return false; - } - - // Check if we can set the Force SSL option - if ((int) $data['force_ssl'] !== 0 && (int) $data['force_ssl'] !== (int) $app->get('force_ssl', '0')) - { - try - { - // Make an HTTPS request to check if the site is available in HTTPS. - $host = Uri::getInstance()->getHost(); - $options = new Registry; - $options->set('userAgent', 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:41.0) Gecko/20100101 Firefox/41.0'); - - // Do not check for valid server certificate here, leave this to the user, moreover disable using a proxy if any is configured. - $options->set('transport.curl', - array( - CURLOPT_SSL_VERIFYPEER => false, - CURLOPT_SSL_VERIFYHOST => false, - CURLOPT_PROXY => null, - CURLOPT_PROXYUSERPWD => null, - ) - ); - $response = HttpFactory::getHttp($options)->get('https://' . $host . Uri::root(true) . '/', array('Host' => $host), 10); - - // If available in HTTPS check also the status code. - if (!in_array($response->code, array(200, 503, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 401), true)) - { - throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_SSL_NOT_AVAILABLE_HTTP_CODE')); - } - } - catch (\RuntimeException $e) - { - $data['force_ssl'] = 0; - - // Also update the user state - $app->setUserState('com_config.config.global.data.force_ssl', 0); - - // Inform the user - $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_SSL_NOT_AVAILABLE', $e->getMessage()), 'warning'); - } - } - - // Save the rules - if (isset($data['rules'])) - { - $rules = new Rules($data['rules']); - - // Check that we aren't removing our Super User permission - // Need to get groups from database, since they might have changed - $myGroups = Access::getGroupsByUser(Factory::getUser()->get('id')); - $myRules = $rules->getData(); - $hasSuperAdmin = $myRules['core.admin']->allow($myGroups); - - if (!$hasSuperAdmin) - { - $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_REMOVING_SUPER_ADMIN'), 'error'); - - return false; - } - - $asset = Table::getInstance('asset'); - - if ($asset->loadByName('root.1')) - { - $asset->rules = (string) $rules; - - if (!$asset->check() || !$asset->store()) - { - $app->enqueueMessage($asset->getError(), 'error'); - - return false; - } - } - else - { - $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_ROOT_ASSET_NOT_FOUND'), 'error'); - - return false; - } - - unset($data['rules']); - } - - // Save the text filters - if (isset($data['filters'])) - { - $registry = new Registry(array('filters' => $data['filters'])); - - $extension = Table::getInstance('extension'); - - // Get extension_id - $extensionId = $extension->find(array('name' => 'com_config')); - - if ($extension->load((int) $extensionId)) - { - $extension->params = (string) $registry; - - if (!$extension->check() || !$extension->store()) - { - $app->enqueueMessage($extension->getError(), 'error'); - - return false; - } - } - else - { - $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIG_EXTENSION_NOT_FOUND'), 'error'); - - return false; - } - - unset($data['filters']); - } - - // Get the previous configuration. - $prev = new \JConfig; - $prev = ArrayHelper::fromObject($prev); - - // Merge the new data in. We do this to preserve values that were not in the form. - $data = array_merge($prev, $data); - - /* - * Perform miscellaneous options based on configuration settings/changes. - */ - - // Escape the offline message if present. - if (isset($data['offline_message'])) - { - $data['offline_message'] = OutputFilter::ampReplace($data['offline_message']); - } - - // Purge the database session table if we are changing to the database handler. - if ($prev['session_handler'] != 'database' && $data['session_handler'] == 'database') - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__session')) - ->where($db->quoteName('time') . ' < ' . (time() - 1)); - $db->setQuery($query); - $db->execute(); - } - - // Purge the database session table if we are disabling session metadata - if ($prev['session_metadata'] == 1 && $data['session_metadata'] == 0) - { - try - { - // If we are are using the session handler, purge the extra columns, otherwise truncate the whole session table - if ($data['session_handler'] === 'database') - { - $revisedDbo->setQuery( - $revisedDbo->getQuery(true) - ->update('#__session') - ->set( - [ - $revisedDbo->quoteName('client_id') . ' = 0', - $revisedDbo->quoteName('guest') . ' = NULL', - $revisedDbo->quoteName('userid') . ' = NULL', - $revisedDbo->quoteName('username') . ' = NULL', - ] - ) - )->execute(); - } - else - { - $revisedDbo->truncateTable('#__session'); - } - } - catch (\RuntimeException $e) - { - /* - * The database API logs errors on failures so we don't need to add any error handling mechanisms here. - * Also, this data won't be added or checked anymore once the configuration is saved, so it'll purge itself - * through normal garbage collection anyway or if not using the database handler someone can purge the - * table on their own. Either way, carry on Soldier! - */ - } - } - - // Ensure custom session file path exists or try to create it if changed - if (!empty($data['session_filesystem_path'])) - { - $currentPath = $prev['session_filesystem_path'] ?? null; - - if ($currentPath) - { - $currentPath = Path::clean($currentPath); - } - - $data['session_filesystem_path'] = Path::clean($data['session_filesystem_path']); - - if ($currentPath !== $data['session_filesystem_path']) - { - if (!Folder::exists($data['session_filesystem_path']) && !Folder::create($data['session_filesystem_path'])) - { - try - { - Log::add( - Text::sprintf( - 'COM_CONFIG_ERROR_CUSTOM_SESSION_FILESYSTEM_PATH_NOTWRITABLE_USING_DEFAULT', - $data['session_filesystem_path'] - ), - Log::WARNING, - 'jerror' - ); - } - catch (\RuntimeException $logException) - { - $app->enqueueMessage( - Text::sprintf( - 'COM_CONFIG_ERROR_CUSTOM_SESSION_FILESYSTEM_PATH_NOTWRITABLE_USING_DEFAULT', - $data['session_filesystem_path'] - ), - 'warning' - ); - } - - $data['session_filesystem_path'] = $currentPath; - } - } - } - - // Set the shared session configuration - if (isset($data['shared_session'])) - { - $currentShared = $prev['shared_session'] ?? '0'; - - // Has the user enabled shared sessions? - if ($data['shared_session'] == 1 && $currentShared == 0) - { - // Generate a random shared session name - $data['session_name'] = UserHelper::genRandomPassword(16); - } - - // Has the user disabled shared sessions? - if ($data['shared_session'] == 0 && $currentShared == 1) - { - // Remove the session name value - unset($data['session_name']); - } - } - - // Set the shared session configuration - if (isset($data['shared_session'])) - { - $currentShared = $prev['shared_session'] ?? '0'; - - // Has the user enabled shared sessions? - if ($data['shared_session'] == 1 && $currentShared == 0) - { - // Generate a random shared session name - $data['session_name'] = UserHelper::genRandomPassword(16); - } - - // Has the user disabled shared sessions? - if ($data['shared_session'] == 0 && $currentShared == 1) - { - // Remove the session name value - unset($data['session_name']); - } - } - - if (empty($data['cache_handler'])) - { - $data['caching'] = 0; - } - - /* - * Look for a custom cache_path - * First check if a path is given in the submitted data, then check if a path exists in the previous data, otherwise use the default - */ - if (!empty($data['cache_path'])) - { - $path = $data['cache_path']; - } - elseif (!empty($prev['cache_path'])) - { - $path = $prev['cache_path']; - } - else - { - $path = JPATH_CACHE; - } - - // Give a warning if the cache-folder can not be opened - if ($data['caching'] > 0 && $data['cache_handler'] == 'file' && @opendir($path) == false) - { - $error = true; - - // If a custom path is in use, try using the system default instead of disabling cache - if ($path !== JPATH_CACHE && @opendir(JPATH_CACHE) != false) - { - try - { - Log::add( - Text::sprintf('COM_CONFIG_ERROR_CUSTOM_CACHE_PATH_NOTWRITABLE_USING_DEFAULT', $path, JPATH_CACHE), - Log::WARNING, - 'jerror' - ); - } - catch (\RuntimeException $logException) - { - $app->enqueueMessage( - Text::sprintf('COM_CONFIG_ERROR_CUSTOM_CACHE_PATH_NOTWRITABLE_USING_DEFAULT', $path, JPATH_CACHE), - 'warning' - ); - } - - $path = JPATH_CACHE; - $error = false; - - $data['cache_path'] = ''; - } - - if ($error) - { - try - { - Log::add(Text::sprintf('COM_CONFIG_ERROR_CACHE_PATH_NOTWRITABLE', $path), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_CACHE_PATH_NOTWRITABLE', $path), 'warning'); - } - - $data['caching'] = 0; - } - } - - // Did the user remove their custom cache path? Don't save the variable to the config - if (empty($data['cache_path'])) - { - unset($data['cache_path']); - } - - // Clean the cache if disabled but previously enabled or changing cache handlers; these operations use the `$prev` data already in memory - if ((!$data['caching'] && $prev['caching']) || $data['cache_handler'] !== $prev['cache_handler']) - { - try - { - Factory::getCache()->clean(); - } - catch (CacheConnectingException $exception) - { - try - { - Log::add(Text::_('COM_CONFIG_ERROR_CACHE_CONNECTION_FAILED'), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $logException) - { - $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CACHE_CONNECTION_FAILED'), 'warning'); - } - } - catch (UnsupportedCacheException $exception) - { - try - { - Log::add(Text::_('COM_CONFIG_ERROR_CACHE_DRIVER_UNSUPPORTED'), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $logException) - { - $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CACHE_DRIVER_UNSUPPORTED'), 'warning'); - } - } - } - - /* - * Look for a custom tmp_path - * First check if a path is given in the submitted data, then check if a path exists in the previous data, otherwise use the default - */ - $defaultTmpPath = JPATH_ROOT . '/tmp'; - - if (!empty($data['tmp_path'])) - { - $path = $data['tmp_path']; - } - elseif (!empty($prev['tmp_path'])) - { - $path = $prev['tmp_path']; - } - else - { - $path = $defaultTmpPath; - } - - $path = Path::clean($path); - - // Give a warning if the tmp-folder is not valid or not writable - if (!is_dir($path) || !is_writable($path)) - { - $error = true; - - // If a custom path is in use, try using the system default tmp path - if ($path !== $defaultTmpPath && is_dir($defaultTmpPath) && is_writable($defaultTmpPath)) - { - try - { - Log::add( - Text::sprintf('COM_CONFIG_ERROR_CUSTOM_TEMP_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultTmpPath), - Log::WARNING, - 'jerror' - ); - } - catch (\RuntimeException $logException) - { - $app->enqueueMessage( - Text::sprintf('COM_CONFIG_ERROR_CUSTOM_TEMP_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultTmpPath), - 'warning' - ); - } - - $error = false; - - $data['tmp_path'] = $defaultTmpPath; - } - - if ($error) - { - try - { - Log::add(Text::sprintf('COM_CONFIG_ERROR_TMP_PATH_NOTWRITABLE', $path), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_TMP_PATH_NOTWRITABLE', $path), 'warning'); - } - } - } - - /* - * Look for a custom log_path - * First check if a path is given in the submitted data, then check if a path exists in the previous data, otherwise use the default - */ - $defaultLogPath = JPATH_ADMINISTRATOR . '/logs'; - - if (!empty($data['log_path'])) - { - $path = $data['log_path']; - } - elseif (!empty($prev['log_path'])) - { - $path = $prev['log_path']; - } - else - { - $path = $defaultLogPath; - } - - $path = Path::clean($path); - - // Give a warning if the log-folder is not valid or not writable - if (!is_dir($path) || !is_writable($path)) - { - $error = true; - - // If a custom path is in use, try using the system default log path - if ($path !== $defaultLogPath && is_dir($defaultLogPath) && is_writable($defaultLogPath)) - { - try - { - Log::add( - Text::sprintf('COM_CONFIG_ERROR_CUSTOM_LOG_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultLogPath), - Log::WARNING, - 'jerror' - ); - } - catch (\RuntimeException $logException) - { - $app->enqueueMessage( - Text::sprintf('COM_CONFIG_ERROR_CUSTOM_LOG_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultLogPath), - 'warning' - ); - } - - $error = false; - $data['log_path'] = $defaultLogPath; - } - - if ($error) - { - try - { - Log::add(Text::sprintf('COM_CONFIG_ERROR_LOG_PATH_NOTWRITABLE', $path), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_LOG_PATH_NOTWRITABLE', $path), 'warning'); - } - } - } - - // Create the new configuration object. - $config = new Registry($data); - - // Overwrite webservices cors settings - $app->set('cors', $data['cors']); - $app->set('cors_allow_origin', $data['cors_allow_origin']); - $app->set('cors_allow_headers', $data['cors_allow_headers']); - $app->set('cors_allow_methods', $data['cors_allow_methods']); - - // Clear cache of com_config component. - $this->cleanCache('_system'); - - $result = $app->triggerEvent('onApplicationBeforeSave', array($config)); - - // Store the data. - if (in_array(false, $result, true)) - { - throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_UNKNOWN_BEFORE_SAVING')); - } - - // Write the configuration file. - $result = $this->writeConfigFile($config); - - // Trigger the after save event. - $app->triggerEvent('onApplicationAfterSave', array($config)); - - return $result; - } - - /** - * Method to unset the root_user value from configuration data. - * - * This method will load the global configuration data straight from - * JConfig and remove the root_user value for security, then save the configuration. - * - * @return boolean True on success, false on failure. - * - * @since 1.6 - */ - public function removeroot() - { - $app = Factory::getApplication(); - - // Get the previous configuration. - $prev = new \JConfig; - $prev = ArrayHelper::fromObject($prev); - - // Create the new configuration object, and unset the root_user property - unset($prev['root_user']); - $config = new Registry($prev); - - $result = $app->triggerEvent('onApplicationBeforeSave', array($config)); - - // Store the data. - if (in_array(false, $result, true)) - { - throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_UNKNOWN_BEFORE_SAVING')); - } - - // Write the configuration file. - $result = $this->writeConfigFile($config); - - // Trigger the after save event. - $app->triggerEvent('onApplicationAfterSave', array($config)); - - return $result; - } - - /** - * Method to write the configuration to a file. - * - * @param Registry $config A Registry object containing all global config data. - * - * @return boolean True on success, false on failure. - * - * @since 2.5.4 - * @throws \RuntimeException - */ - private function writeConfigFile(Registry $config) - { - // Set the configuration file path. - $file = JPATH_CONFIGURATION . '/configuration.php'; - - $app = Factory::getApplication(); - - // Attempt to make the file writeable. - if (Path::isOwner($file) && !Path::setPermissions($file, '0644')) - { - $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTWRITABLE'), 'notice'); - } - - // Attempt to write the configuration file as a PHP class named JConfig. - $configuration = $config->toString('PHP', array('class' => 'JConfig', 'closingtag' => false)); - - if (!File::write($file, $configuration)) - { - throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_WRITE_FAILED')); - } - - // Attempt to make the file unwriteable. - if (Path::isOwner($file) && !Path::setPermissions($file, '0444')) - { - $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTUNWRITABLE'), 'notice'); - } - - return true; - } - - /** - * Method to store the permission values in the asset table. - * - * This method will get an array with permission key value pairs and transform it - * into json and update the asset table in the database. - * - * @param string $permission Need an array with Permissions (component, rule, value and title) - * - * @return array|bool A list of result data or false on failure. - * - * @since 3.5 - */ - public function storePermissions($permission = null) - { - $app = Factory::getApplication(); - $user = Factory::getUser(); - - if (is_null($permission)) - { - // Get data from input. - $permission = array( - 'component' => $app->input->Json->get('comp'), - 'action' => $app->input->Json->get('action'), - 'rule' => $app->input->Json->get('rule'), - 'value' => $app->input->Json->get('value'), - 'title' => $app->input->Json->get('title', '', 'RAW') - ); - } - - // We are creating a new item so we don't have an item id so don't allow. - if (substr($permission['component'], -6) === '.false') - { - $app->enqueueMessage(Text::_('JLIB_RULES_SAVE_BEFORE_CHANGE_PERMISSIONS'), 'error'); - - return false; - } - - // Check if the user is authorized to do this. - if (!$user->authorise('core.admin', $permission['component'])) - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - - return false; - } - - $permission['component'] = empty($permission['component']) ? 'root.1' : $permission['component']; - - // Current view is global config? - $isGlobalConfig = $permission['component'] === 'root.1'; - - // Check if changed group has Super User permissions. - $isSuperUserGroupBefore = Access::checkGroup($permission['rule'], 'core.admin'); - - // Check if current user belongs to changed group. - $currentUserBelongsToGroup = in_array((int) $permission['rule'], $user->groups) ? true : false; - - // Get current user groups tree. - $currentUserGroupsTree = Access::getGroupsByUser($user->id, true); - - // Check if current user belongs to changed group. - $currentUserSuperUser = $user->authorise('core.admin'); - - // If user is not Super User cannot change the permissions of a group it belongs to. - if (!$currentUserSuperUser && $currentUserBelongsToGroup) - { - $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_CHANGE_OWN_GROUPS'), 'error'); - - return false; - } - - // If user is not Super User cannot change the permissions of a group it belongs to. - if (!$currentUserSuperUser && in_array((int) $permission['rule'], $currentUserGroupsTree)) - { - $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_CHANGE_OWN_PARENT_GROUPS'), 'error'); - - return false; - } - - // If user is not Super User cannot change the permissions of a Super User Group. - if (!$currentUserSuperUser && $isSuperUserGroupBefore && !$currentUserBelongsToGroup) - { - $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_CHANGE_SUPER_USER'), 'error'); - - return false; - } - - // If user is not Super User cannot change the Super User permissions in any group it belongs to. - if ($isSuperUserGroupBefore && $currentUserBelongsToGroup && $permission['action'] === 'core.admin') - { - $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_DEMOTE_SELF'), 'error'); - - return false; - } - - try - { - /** @var Asset $asset */ - $asset = Table::getInstance('asset'); - $result = $asset->loadByName($permission['component']); - - if ($result === false) - { - $data = array($permission['action'] => array($permission['rule'] => $permission['value'])); - - $rules = new Rules($data); - $asset->rules = (string) $rules; - $asset->name = (string) $permission['component']; - $asset->title = (string) $permission['title']; - - // Get the parent asset id so we have a correct tree. - /** @var Asset $parentAsset */ - $parentAsset = Table::getInstance('Asset'); - - if (strpos($asset->name, '.') !== false) - { - $assetParts = explode('.', $asset->name); - $parentAsset->loadByName($assetParts[0]); - $parentAssetId = $parentAsset->id; - } - else - { - $parentAssetId = $parentAsset->getRootId(); - } - - /** - * @todo: incorrect ACL stored - * When changing a permission of an item that doesn't have a row in the asset table the row a new row is created. - * This works fine for item <-> component <-> global config scenario and component <-> global config scenario. - * But doesn't work properly for item <-> section(s) <-> component <-> global config scenario, - * because a wrong parent asset id (the component) is stored. - * Happens when there is no row in the asset table (ex: deleted or not created on update). - */ - - $asset->setLocation($parentAssetId, 'last-child'); - } - else - { - // Decode the rule settings. - $temp = json_decode($asset->rules, true); - - // Check if a new value is to be set. - if (isset($permission['value'])) - { - // Check if we already have an action entry. - if (!isset($temp[$permission['action']])) - { - $temp[$permission['action']] = array(); - } - - // Check if we already have a rule entry. - if (!isset($temp[$permission['action']][$permission['rule']])) - { - $temp[$permission['action']][$permission['rule']] = array(); - } - - // Set the new permission. - $temp[$permission['action']][$permission['rule']] = (int) $permission['value']; - - // Check if we have an inherited setting. - if ($permission['value'] === '') - { - unset($temp[$permission['action']][$permission['rule']]); - } - - // Check if we have any rules. - if (!$temp[$permission['action']]) - { - unset($temp[$permission['action']]); - } - } - else - { - // There is no value so remove the action as it's not needed. - unset($temp[$permission['action']]); - } - - $asset->rules = json_encode($temp, JSON_FORCE_OBJECT); - } - - if (!$asset->check() || !$asset->store()) - { - $app->enqueueMessage(Text::_('JLIB_UNKNOWN'), 'error'); - - return false; - } - } - catch (\Exception $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - // All checks done. - $result = array( - 'text' => '', - 'class' => '', - 'result' => true, - ); - - // Show the current effective calculated permission considering current group, path and cascade. - - try - { - // The database instance - $db = $this->getDatabase(); - - // Get the asset id by the name of the component. - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__assets')) - ->where($db->quoteName('name') . ' = :component') - ->bind(':component', $permission['component']); - - $db->setQuery($query); - - $assetId = (int) $db->loadResult(); - - // Fetch the parent asset id. - $parentAssetId = null; - - /** - * @todo: incorrect info - * When creating a new item (not saving) it uses the calculated permissions from the component (item <-> component <-> global config). - * But if we have a section too (item <-> section(s) <-> component <-> global config) this is not correct. - * Also, currently it uses the component permission, but should use the calculated permissions for a child of the component/section. - */ - - // If not in global config we need the parent_id asset to calculate permissions. - if (!$isGlobalConfig) - { - // In this case we need to get the component rules too. - $query->clear() - ->select($db->quoteName('parent_id')) - ->from($db->quoteName('#__assets')) - ->where($db->quoteName('id') . ' = :assetid') - ->bind(':assetid', $assetId, ParameterType::INTEGER); - - $db->setQuery($query); - - $parentAssetId = (int) $db->loadResult(); - } - - // Get the group parent id of the current group. - $rule = (int) $permission['rule']; - $query->clear() - ->select($db->quoteName('parent_id')) - ->from($db->quoteName('#__usergroups')) - ->where($db->quoteName('id') . ' = :rule') - ->bind(':rule', $rule, ParameterType::INTEGER); - - $db->setQuery($query); - - $parentGroupId = (int) $db->loadResult(); - - // Count the number of child groups of the current group. - $query->clear() - ->select('COUNT(' . $db->quoteName('id') . ')') - ->from($db->quoteName('#__usergroups')) - ->where($db->quoteName('parent_id') . ' = :rule') - ->bind(':rule', $rule, ParameterType::INTEGER); - - $db->setQuery($query); - - $totalChildGroups = (int) $db->loadResult(); - } - catch (\Exception $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - // Clear access statistics. - Access::clearStatics(); - - // After current group permission is changed we need to check again if the group has Super User permissions. - $isSuperUserGroupAfter = Access::checkGroup($permission['rule'], 'core.admin'); - - // Get the rule for just this asset (non-recursive) and get the actual setting for the action for this group. - $assetRule = Access::getAssetRules($assetId, false, false)->allow($permission['action'], $permission['rule']); - - // Get the group, group parent id, and group global config recursive calculated permission for the chosen action. - $inheritedGroupRule = Access::checkGroup($permission['rule'], $permission['action'], $assetId); - - if (!empty($parentAssetId)) - { - $inheritedGroupParentAssetRule = Access::checkGroup($permission['rule'], $permission['action'], $parentAssetId); - } - else - { - $inheritedGroupParentAssetRule = null; - } - - $inheritedParentGroupRule = !empty($parentGroupId) ? Access::checkGroup($parentGroupId, $permission['action'], $assetId) : null; - - // Current group is a Super User group, so calculated setting is "Allowed (Super User)". - if ($isSuperUserGroupAfter) - { - $result['class'] = 'badge bg-success'; - $result['text'] = '' . Text::_('JLIB_RULES_ALLOWED_ADMIN'); - } - // Not super user. - else - { - // First get the real recursive calculated setting and add (Inherited) to it. - - // If recursive calculated setting is "Denied" or null. Calculated permission is "Not Allowed (Inherited)". - if ($inheritedGroupRule === null || $inheritedGroupRule === false) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_INHERITED'); - } - // If recursive calculated setting is "Allowed". Calculated permission is "Allowed (Inherited)". - else - { - $result['class'] = 'badge bg-success'; - $result['text'] = Text::_('JLIB_RULES_ALLOWED_INHERITED'); - } - - // Second part: Overwrite the calculated permissions labels if there is an explicit permission in the current group. - - /** - * @todo: incorrect info - * If a component has a permission that doesn't exists in global config (ex: frontend editing in com_modules) by default - * we get "Not Allowed (Inherited)" when we should get "Not Allowed (Default)". - */ - - // If there is an explicit permission "Not Allowed". Calculated permission is "Not Allowed". - if ($assetRule === false) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED'); - } - // If there is an explicit permission is "Allowed". Calculated permission is "Allowed". - elseif ($assetRule === true) - { - $result['class'] = 'badge bg-success'; - $result['text'] = Text::_('JLIB_RULES_ALLOWED'); - } - - // Third part: Overwrite the calculated permissions labels for special cases. - - // Global configuration with "Not Set" permission. Calculated permission is "Not Allowed (Default)". - if (empty($parentGroupId) && $isGlobalConfig === true && $assetRule === null) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_DEFAULT'); - } - - /** - * Component/Item with explicit "Denied" permission at parent Asset (Category, Component or Global config) configuration. - * Or some parent group has an explicit "Denied". - * Calculated permission is "Not Allowed (Locked)". - */ - elseif ($inheritedGroupParentAssetRule === false || $inheritedParentGroupRule === false) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = '' . Text::_('JLIB_RULES_NOT_ALLOWED_LOCKED'); - } - } - - // If removed or added super user from group, we need to refresh the page to recalculate all settings. - if ($isSuperUserGroupBefore != $isSuperUserGroupAfter) - { - $app->enqueueMessage(Text::_('JLIB_RULES_NOTICE_RECALCULATE_GROUP_PERMISSIONS'), 'notice'); - } - - // If this group has child groups, we need to refresh the page to recalculate the child settings. - if ($totalChildGroups > 0) - { - $app->enqueueMessage(Text::_('JLIB_RULES_NOTICE_RECALCULATE_GROUP_CHILDS_PERMISSIONS'), 'notice'); - } - - return $result; - } - - /** - * Method to send a test mail which is called via an AJAX request - * - * @return boolean - * - * @since 3.5 - */ - public function sendTestMail() - { - // Set the new values to test with the current settings - $app = Factory::getApplication(); - $user = Factory::getUser(); - $input = $app->input->json; - $smtppass = $input->get('smtppass', null, 'RAW'); - - $app->set('smtpauth', $input->get('smtpauth')); - $app->set('smtpuser', $input->get('smtpuser', '', 'STRING')); - $app->set('smtphost', $input->get('smtphost')); - $app->set('smtpsecure', $input->get('smtpsecure')); - $app->set('smtpport', $input->get('smtpport')); - $app->set('mailfrom', $input->get('mailfrom', '', 'STRING')); - $app->set('fromname', $input->get('fromname', '', 'STRING')); - $app->set('mailer', $input->get('mailer')); - $app->set('mailonline', $input->get('mailonline')); - - // Use smtppass only if it was submitted - if ($smtppass !== null) - { - $app->set('smtppass', $smtppass); - } - - $mail = Factory::getMailer(); - - // Prepare email and try to send it - $mailer = new MailTemplate('com_config.test_mail', $user->getParam('language', $app->get('language')), $mail); - $mailer->addTemplateData( - array( - 'sitename' => $app->get('sitename'), - 'method' => Text::_('COM_CONFIG_SENDMAIL_METHOD_' . strtoupper($mail->Mailer)) - ) - ); - $mailer->addRecipient($app->get('mailfrom'), $app->get('fromname')); - - try - { - $mailSent = $mailer->send(); - } - catch (MailDisabledException | phpMailerException $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - if ($mailSent === true) - { - $methodName = Text::_('COM_CONFIG_SENDMAIL_METHOD_' . strtoupper($mail->Mailer)); - - // If JMail send the mail using PHP Mail as fallback. - if ($mail->Mailer !== $app->get('mailer')) - { - $app->enqueueMessage(Text::sprintf('COM_CONFIG_SENDMAIL_SUCCESS_FALLBACK', $app->get('mailfrom'), $methodName), 'warning'); - } - else - { - $app->enqueueMessage(Text::sprintf('COM_CONFIG_SENDMAIL_SUCCESS', $app->get('mailfrom'), $methodName), 'message'); - } - - return true; - } - - $app->enqueueMessage(Text::_('COM_CONFIG_SENDMAIL_ERROR'), 'error'); - - return false; - } + /** + * Array of protected password fields from the configuration.php + * + * @var array + * @since 3.9.23 + */ + private $protectedConfigurationFields = array('password', 'secret', 'smtppass', 'redis_server_auth', 'session_redis_server_auth'); + + /** + * Method to get a form object. + * + * @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 mixed A JForm object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_config.application', 'application', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the configuration data. + * + * This method will load the global configuration data straight from + * JConfig. If configuration data has been saved in the session, that + * data will be merged into the original data, overwriting it. + * + * @return array An array containing all global config data. + * + * @since 1.6 + */ + public function getData() + { + // Get the config data. + $config = new \JConfig(); + $data = ArrayHelper::fromObject($config); + + // Get the correct driver at runtime + $data['dbtype'] = $this->getDatabase()->getName(); + + // Prime the asset_id for the rules. + $data['asset_id'] = 1; + + // Get the text filter data + $params = ComponentHelper::getParams('com_config'); + $data['filters'] = ArrayHelper::fromObject($params->get('filters')); + + // If no filter data found, get from com_content (update of 1.6/1.7 site) + if (empty($data['filters'])) { + $contentParams = ComponentHelper::getParams('com_content'); + $data['filters'] = ArrayHelper::fromObject($contentParams->get('filters')); + } + + // Check for data in the session. + $temp = Factory::getApplication()->getUserState('com_config.config.global.data'); + + // Merge in the session data. + if (!empty($temp)) { + // $temp can sometimes be an object, and we need it to be an array + if (is_object($temp)) { + $temp = ArrayHelper::fromObject($temp); + } + + $data = array_merge($temp, $data); + } + + // Correct error_reporting value, since we removed "development", the "maximum" should be set instead + // @TODO: This can be removed in 5.0 + if (!empty($data['error_reporting']) && $data['error_reporting'] === 'development') { + $data['error_reporting'] = 'maximum'; + } + + return $data; + } + + /** + * Method to validate the db connection properties. + * + * @param array $data An array containing all global config data. + * + * @return array|boolean Array with the validated global config data or boolean false on a validation failure. + * + * @since 4.0.0 + */ + public function validateDbConnection($data) + { + // Validate database connection encryption options + if ((int) $data['dbencryption'] === 0) { + // Reset unused options + if (!empty($data['dbsslkey'])) { + $data['dbsslkey'] = ''; + } + + if (!empty($data['dbsslcert'])) { + $data['dbsslcert'] = ''; + } + + if ((bool) $data['dbsslverifyservercert'] === true) { + $data['dbsslverifyservercert'] = false; + } + + if (!empty($data['dbsslca'])) { + $data['dbsslca'] = ''; + } + + if (!empty($data['dbsslcipher'])) { + $data['dbsslcipher'] = ''; + } + } else { + // Check localhost + if (strtolower($data['host']) === 'localhost') { + Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_DATABASE_ENCRYPTION_LOCALHOST'), 'error'); + + return false; + } + + // Check CA file and folder depending on database type if server certificate verification + if ((bool) $data['dbsslverifyservercert'] === true) { + if (empty($data['dbsslca'])) { + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_EMPTY', + Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CA_LABEL') + ), + 'error' + ); + + return false; + } + + if (!File::exists(Path::clean($data['dbsslca']))) { + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_BAD', + Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CA_LABEL') + ), + 'error' + ); + + return false; + } + } else { + // Reset unused option + if (!empty($data['dbsslca'])) { + $data['dbsslca'] = ''; + } + } + + // Check key and certificate if two-way encryption + if ((int) $data['dbencryption'] === 2) { + if (empty($data['dbsslkey'])) { + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_EMPTY', + Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_KEY_LABEL') + ), + 'error' + ); + + return false; + } + + if (!File::exists(Path::clean($data['dbsslkey']))) { + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_BAD', + Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_KEY_LABEL') + ), + 'error' + ); + + return false; + } + + if (empty($data['dbsslcert'])) { + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_EMPTY', + Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CERT_LABEL') + ), + 'error' + ); + + return false; + } + + if (!File::exists(Path::clean($data['dbsslcert']))) { + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'COM_CONFIG_ERROR_DATABASE_ENCRYPTION_FILE_FIELD_BAD', + Text::_('COM_CONFIG_FIELD_DATABASE_ENCRYPTION_CERT_LABEL') + ), + 'error' + ); + + return false; + } + } else { + // Reset unused options + if (!empty($data['dbsslkey'])) { + $data['dbsslkey'] = ''; + } + + if (!empty($data['dbsslcert'])) { + $data['dbsslcert'] = ''; + } + } + } + + return $data; + } + + /** + * Method to save the configuration data. + * + * @param array $data An array containing all global config data. + * + * @return boolean True on success, false on failure. + * + * @since 1.6 + */ + public function save($data) + { + $app = Factory::getApplication(); + + // Try to load the values from the configuration file + foreach ($this->protectedConfigurationFields as $fieldKey) { + if (!isset($data[$fieldKey])) { + $data[$fieldKey] = $app->get($fieldKey, ''); + } + } + + // Check that we aren't setting wrong database configuration + $options = array( + 'driver' => $data['dbtype'], + 'host' => $data['host'], + 'user' => $data['user'], + 'password' => $data['password'], + 'database' => $data['db'], + 'prefix' => $data['dbprefix'], + ); + + if ((int) $data['dbencryption'] !== 0) { + $options['ssl'] = [ + 'enable' => true, + 'verify_server_cert' => (bool) $data['dbsslverifyservercert'], + ]; + + foreach (['cipher', 'ca', 'key', 'cert'] as $value) { + $confVal = trim($data['dbssl' . $value]); + + if ($confVal !== '') { + $options['ssl'][$value] = $confVal; + } + } + } + + try { + $revisedDbo = DatabaseDriver::getInstance($options); + $revisedDbo->getVersion(); + } catch (\Exception $e) { + $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_DATABASE_NOT_AVAILABLE', $e->getCode(), $e->getMessage()), 'error'); + + return false; + } + + if ((int) $data['dbencryption'] !== 0 && empty($revisedDbo->getConnectionEncryption())) { + if ($revisedDbo->isConnectionEncryptionSupported()) { + Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_DATABASE_ENCRYPTION_CONN_NOT_ENCRYPT'), 'error'); + } else { + Factory::getApplication()->enqueueMessage(Text::_('COM_CONFIG_ERROR_DATABASE_ENCRYPTION_SRV_NOT_SUPPORTS'), 'error'); + } + + return false; + } + + // Check if we can set the Force SSL option + if ((int) $data['force_ssl'] !== 0 && (int) $data['force_ssl'] !== (int) $app->get('force_ssl', '0')) { + try { + // Make an HTTPS request to check if the site is available in HTTPS. + $host = Uri::getInstance()->getHost(); + $options = new Registry(); + $options->set('userAgent', 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:41.0) Gecko/20100101 Firefox/41.0'); + + // Do not check for valid server certificate here, leave this to the user, moreover disable using a proxy if any is configured. + $options->set( + 'transport.curl', + array( + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_PROXY => null, + CURLOPT_PROXYUSERPWD => null, + ) + ); + $response = HttpFactory::getHttp($options)->get('https://' . $host . Uri::root(true) . '/', array('Host' => $host), 10); + + // If available in HTTPS check also the status code. + if (!in_array($response->code, array(200, 503, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 401), true)) { + throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_SSL_NOT_AVAILABLE_HTTP_CODE')); + } + } catch (\RuntimeException $e) { + $data['force_ssl'] = 0; + + // Also update the user state + $app->setUserState('com_config.config.global.data.force_ssl', 0); + + // Inform the user + $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_SSL_NOT_AVAILABLE', $e->getMessage()), 'warning'); + } + } + + // Save the rules + if (isset($data['rules'])) { + $rules = new Rules($data['rules']); + + // Check that we aren't removing our Super User permission + // Need to get groups from database, since they might have changed + $myGroups = Access::getGroupsByUser(Factory::getUser()->get('id')); + $myRules = $rules->getData(); + $hasSuperAdmin = $myRules['core.admin']->allow($myGroups); + + if (!$hasSuperAdmin) { + $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_REMOVING_SUPER_ADMIN'), 'error'); + + return false; + } + + $asset = Table::getInstance('asset'); + + if ($asset->loadByName('root.1')) { + $asset->rules = (string) $rules; + + if (!$asset->check() || !$asset->store()) { + $app->enqueueMessage($asset->getError(), 'error'); + + return false; + } + } else { + $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_ROOT_ASSET_NOT_FOUND'), 'error'); + + return false; + } + + unset($data['rules']); + } + + // Save the text filters + if (isset($data['filters'])) { + $registry = new Registry(array('filters' => $data['filters'])); + + $extension = Table::getInstance('extension'); + + // Get extension_id + $extensionId = $extension->find(array('name' => 'com_config')); + + if ($extension->load((int) $extensionId)) { + $extension->params = (string) $registry; + + if (!$extension->check() || !$extension->store()) { + $app->enqueueMessage($extension->getError(), 'error'); + + return false; + } + } else { + $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIG_EXTENSION_NOT_FOUND'), 'error'); + + return false; + } + + unset($data['filters']); + } + + // Get the previous configuration. + $prev = new \JConfig(); + $prev = ArrayHelper::fromObject($prev); + + // Merge the new data in. We do this to preserve values that were not in the form. + $data = array_merge($prev, $data); + + /* + * Perform miscellaneous options based on configuration settings/changes. + */ + + // Escape the offline message if present. + if (isset($data['offline_message'])) { + $data['offline_message'] = OutputFilter::ampReplace($data['offline_message']); + } + + // Purge the database session table if we are changing to the database handler. + if ($prev['session_handler'] != 'database' && $data['session_handler'] == 'database') { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__session')) + ->where($db->quoteName('time') . ' < ' . (time() - 1)); + $db->setQuery($query); + $db->execute(); + } + + // Purge the database session table if we are disabling session metadata + if ($prev['session_metadata'] == 1 && $data['session_metadata'] == 0) { + try { + // If we are are using the session handler, purge the extra columns, otherwise truncate the whole session table + if ($data['session_handler'] === 'database') { + $revisedDbo->setQuery( + $revisedDbo->getQuery(true) + ->update('#__session') + ->set( + [ + $revisedDbo->quoteName('client_id') . ' = 0', + $revisedDbo->quoteName('guest') . ' = NULL', + $revisedDbo->quoteName('userid') . ' = NULL', + $revisedDbo->quoteName('username') . ' = NULL', + ] + ) + )->execute(); + } else { + $revisedDbo->truncateTable('#__session'); + } + } catch (\RuntimeException $e) { + /* + * The database API logs errors on failures so we don't need to add any error handling mechanisms here. + * Also, this data won't be added or checked anymore once the configuration is saved, so it'll purge itself + * through normal garbage collection anyway or if not using the database handler someone can purge the + * table on their own. Either way, carry on Soldier! + */ + } + } + + // Ensure custom session file path exists or try to create it if changed + if (!empty($data['session_filesystem_path'])) { + $currentPath = $prev['session_filesystem_path'] ?? null; + + if ($currentPath) { + $currentPath = Path::clean($currentPath); + } + + $data['session_filesystem_path'] = Path::clean($data['session_filesystem_path']); + + if ($currentPath !== $data['session_filesystem_path']) { + if (!Folder::exists($data['session_filesystem_path']) && !Folder::create($data['session_filesystem_path'])) { + try { + Log::add( + Text::sprintf( + 'COM_CONFIG_ERROR_CUSTOM_SESSION_FILESYSTEM_PATH_NOTWRITABLE_USING_DEFAULT', + $data['session_filesystem_path'] + ), + Log::WARNING, + 'jerror' + ); + } catch (\RuntimeException $logException) { + $app->enqueueMessage( + Text::sprintf( + 'COM_CONFIG_ERROR_CUSTOM_SESSION_FILESYSTEM_PATH_NOTWRITABLE_USING_DEFAULT', + $data['session_filesystem_path'] + ), + 'warning' + ); + } + + $data['session_filesystem_path'] = $currentPath; + } + } + } + + // Set the shared session configuration + if (isset($data['shared_session'])) { + $currentShared = $prev['shared_session'] ?? '0'; + + // Has the user enabled shared sessions? + if ($data['shared_session'] == 1 && $currentShared == 0) { + // Generate a random shared session name + $data['session_name'] = UserHelper::genRandomPassword(16); + } + + // Has the user disabled shared sessions? + if ($data['shared_session'] == 0 && $currentShared == 1) { + // Remove the session name value + unset($data['session_name']); + } + } + + // Set the shared session configuration + if (isset($data['shared_session'])) { + $currentShared = $prev['shared_session'] ?? '0'; + + // Has the user enabled shared sessions? + if ($data['shared_session'] == 1 && $currentShared == 0) { + // Generate a random shared session name + $data['session_name'] = UserHelper::genRandomPassword(16); + } + + // Has the user disabled shared sessions? + if ($data['shared_session'] == 0 && $currentShared == 1) { + // Remove the session name value + unset($data['session_name']); + } + } + + if (empty($data['cache_handler'])) { + $data['caching'] = 0; + } + + /* + * Look for a custom cache_path + * First check if a path is given in the submitted data, then check if a path exists in the previous data, otherwise use the default + */ + if (!empty($data['cache_path'])) { + $path = $data['cache_path']; + } elseif (!empty($prev['cache_path'])) { + $path = $prev['cache_path']; + } else { + $path = JPATH_CACHE; + } + + // Give a warning if the cache-folder can not be opened + if ($data['caching'] > 0 && $data['cache_handler'] == 'file' && @opendir($path) == false) { + $error = true; + + // If a custom path is in use, try using the system default instead of disabling cache + if ($path !== JPATH_CACHE && @opendir(JPATH_CACHE) != false) { + try { + Log::add( + Text::sprintf('COM_CONFIG_ERROR_CUSTOM_CACHE_PATH_NOTWRITABLE_USING_DEFAULT', $path, JPATH_CACHE), + Log::WARNING, + 'jerror' + ); + } catch (\RuntimeException $logException) { + $app->enqueueMessage( + Text::sprintf('COM_CONFIG_ERROR_CUSTOM_CACHE_PATH_NOTWRITABLE_USING_DEFAULT', $path, JPATH_CACHE), + 'warning' + ); + } + + $path = JPATH_CACHE; + $error = false; + + $data['cache_path'] = ''; + } + + if ($error) { + try { + Log::add(Text::sprintf('COM_CONFIG_ERROR_CACHE_PATH_NOTWRITABLE', $path), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_CACHE_PATH_NOTWRITABLE', $path), 'warning'); + } + + $data['caching'] = 0; + } + } + + // Did the user remove their custom cache path? Don't save the variable to the config + if (empty($data['cache_path'])) { + unset($data['cache_path']); + } + + // Clean the cache if disabled but previously enabled or changing cache handlers; these operations use the `$prev` data already in memory + if ((!$data['caching'] && $prev['caching']) || $data['cache_handler'] !== $prev['cache_handler']) { + try { + Factory::getCache()->clean(); + } catch (CacheConnectingException $exception) { + try { + Log::add(Text::_('COM_CONFIG_ERROR_CACHE_CONNECTION_FAILED'), Log::WARNING, 'jerror'); + } catch (\RuntimeException $logException) { + $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CACHE_CONNECTION_FAILED'), 'warning'); + } + } catch (UnsupportedCacheException $exception) { + try { + Log::add(Text::_('COM_CONFIG_ERROR_CACHE_DRIVER_UNSUPPORTED'), Log::WARNING, 'jerror'); + } catch (\RuntimeException $logException) { + $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CACHE_DRIVER_UNSUPPORTED'), 'warning'); + } + } + } + + /* + * Look for a custom tmp_path + * First check if a path is given in the submitted data, then check if a path exists in the previous data, otherwise use the default + */ + $defaultTmpPath = JPATH_ROOT . '/tmp'; + + if (!empty($data['tmp_path'])) { + $path = $data['tmp_path']; + } elseif (!empty($prev['tmp_path'])) { + $path = $prev['tmp_path']; + } else { + $path = $defaultTmpPath; + } + + $path = Path::clean($path); + + // Give a warning if the tmp-folder is not valid or not writable + if (!is_dir($path) || !is_writable($path)) { + $error = true; + + // If a custom path is in use, try using the system default tmp path + if ($path !== $defaultTmpPath && is_dir($defaultTmpPath) && is_writable($defaultTmpPath)) { + try { + Log::add( + Text::sprintf('COM_CONFIG_ERROR_CUSTOM_TEMP_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultTmpPath), + Log::WARNING, + 'jerror' + ); + } catch (\RuntimeException $logException) { + $app->enqueueMessage( + Text::sprintf('COM_CONFIG_ERROR_CUSTOM_TEMP_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultTmpPath), + 'warning' + ); + } + + $error = false; + + $data['tmp_path'] = $defaultTmpPath; + } + + if ($error) { + try { + Log::add(Text::sprintf('COM_CONFIG_ERROR_TMP_PATH_NOTWRITABLE', $path), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_TMP_PATH_NOTWRITABLE', $path), 'warning'); + } + } + } + + /* + * Look for a custom log_path + * First check if a path is given in the submitted data, then check if a path exists in the previous data, otherwise use the default + */ + $defaultLogPath = JPATH_ADMINISTRATOR . '/logs'; + + if (!empty($data['log_path'])) { + $path = $data['log_path']; + } elseif (!empty($prev['log_path'])) { + $path = $prev['log_path']; + } else { + $path = $defaultLogPath; + } + + $path = Path::clean($path); + + // Give a warning if the log-folder is not valid or not writable + if (!is_dir($path) || !is_writable($path)) { + $error = true; + + // If a custom path is in use, try using the system default log path + if ($path !== $defaultLogPath && is_dir($defaultLogPath) && is_writable($defaultLogPath)) { + try { + Log::add( + Text::sprintf('COM_CONFIG_ERROR_CUSTOM_LOG_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultLogPath), + Log::WARNING, + 'jerror' + ); + } catch (\RuntimeException $logException) { + $app->enqueueMessage( + Text::sprintf('COM_CONFIG_ERROR_CUSTOM_LOG_PATH_NOTWRITABLE_USING_DEFAULT', $path, $defaultLogPath), + 'warning' + ); + } + + $error = false; + $data['log_path'] = $defaultLogPath; + } + + if ($error) { + try { + Log::add(Text::sprintf('COM_CONFIG_ERROR_LOG_PATH_NOTWRITABLE', $path), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + $app->enqueueMessage(Text::sprintf('COM_CONFIG_ERROR_LOG_PATH_NOTWRITABLE', $path), 'warning'); + } + } + } + + // Create the new configuration object. + $config = new Registry($data); + + // Overwrite webservices cors settings + $app->set('cors', $data['cors']); + $app->set('cors_allow_origin', $data['cors_allow_origin']); + $app->set('cors_allow_headers', $data['cors_allow_headers']); + $app->set('cors_allow_methods', $data['cors_allow_methods']); + + // Clear cache of com_config component. + $this->cleanCache('_system'); + + $result = $app->triggerEvent('onApplicationBeforeSave', array($config)); + + // Store the data. + if (in_array(false, $result, true)) { + throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_UNKNOWN_BEFORE_SAVING')); + } + + // Write the configuration file. + $result = $this->writeConfigFile($config); + + // Trigger the after save event. + $app->triggerEvent('onApplicationAfterSave', array($config)); + + return $result; + } + + /** + * Method to unset the root_user value from configuration data. + * + * This method will load the global configuration data straight from + * JConfig and remove the root_user value for security, then save the configuration. + * + * @return boolean True on success, false on failure. + * + * @since 1.6 + */ + public function removeroot() + { + $app = Factory::getApplication(); + + // Get the previous configuration. + $prev = new \JConfig(); + $prev = ArrayHelper::fromObject($prev); + + // Create the new configuration object, and unset the root_user property + unset($prev['root_user']); + $config = new Registry($prev); + + $result = $app->triggerEvent('onApplicationBeforeSave', array($config)); + + // Store the data. + if (in_array(false, $result, true)) { + throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_UNKNOWN_BEFORE_SAVING')); + } + + // Write the configuration file. + $result = $this->writeConfigFile($config); + + // Trigger the after save event. + $app->triggerEvent('onApplicationAfterSave', array($config)); + + return $result; + } + + /** + * Method to write the configuration to a file. + * + * @param Registry $config A Registry object containing all global config data. + * + * @return boolean True on success, false on failure. + * + * @since 2.5.4 + * @throws \RuntimeException + */ + private function writeConfigFile(Registry $config) + { + // Set the configuration file path. + $file = JPATH_CONFIGURATION . '/configuration.php'; + + $app = Factory::getApplication(); + + // Attempt to make the file writeable. + if (Path::isOwner($file) && !Path::setPermissions($file, '0644')) { + $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTWRITABLE'), 'notice'); + } + + // Attempt to write the configuration file as a PHP class named JConfig. + $configuration = $config->toString('PHP', array('class' => 'JConfig', 'closingtag' => false)); + + if (!File::write($file, $configuration)) { + throw new \RuntimeException(Text::_('COM_CONFIG_ERROR_WRITE_FAILED')); + } + + // Attempt to make the file unwriteable. + if (Path::isOwner($file) && !Path::setPermissions($file, '0444')) { + $app->enqueueMessage(Text::_('COM_CONFIG_ERROR_CONFIGURATION_PHP_NOTUNWRITABLE'), 'notice'); + } + + return true; + } + + /** + * Method to store the permission values in the asset table. + * + * This method will get an array with permission key value pairs and transform it + * into json and update the asset table in the database. + * + * @param string $permission Need an array with Permissions (component, rule, value and title) + * + * @return array|bool A list of result data or false on failure. + * + * @since 3.5 + */ + public function storePermissions($permission = null) + { + $app = Factory::getApplication(); + $user = Factory::getUser(); + + if (is_null($permission)) { + // Get data from input. + $permission = array( + 'component' => $app->input->Json->get('comp'), + 'action' => $app->input->Json->get('action'), + 'rule' => $app->input->Json->get('rule'), + 'value' => $app->input->Json->get('value'), + 'title' => $app->input->Json->get('title', '', 'RAW') + ); + } + + // We are creating a new item so we don't have an item id so don't allow. + if (substr($permission['component'], -6) === '.false') { + $app->enqueueMessage(Text::_('JLIB_RULES_SAVE_BEFORE_CHANGE_PERMISSIONS'), 'error'); + + return false; + } + + // Check if the user is authorized to do this. + if (!$user->authorise('core.admin', $permission['component'])) { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + + return false; + } + + $permission['component'] = empty($permission['component']) ? 'root.1' : $permission['component']; + + // Current view is global config? + $isGlobalConfig = $permission['component'] === 'root.1'; + + // Check if changed group has Super User permissions. + $isSuperUserGroupBefore = Access::checkGroup($permission['rule'], 'core.admin'); + + // Check if current user belongs to changed group. + $currentUserBelongsToGroup = in_array((int) $permission['rule'], $user->groups) ? true : false; + + // Get current user groups tree. + $currentUserGroupsTree = Access::getGroupsByUser($user->id, true); + + // Check if current user belongs to changed group. + $currentUserSuperUser = $user->authorise('core.admin'); + + // If user is not Super User cannot change the permissions of a group it belongs to. + if (!$currentUserSuperUser && $currentUserBelongsToGroup) { + $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_CHANGE_OWN_GROUPS'), 'error'); + + return false; + } + + // If user is not Super User cannot change the permissions of a group it belongs to. + if (!$currentUserSuperUser && in_array((int) $permission['rule'], $currentUserGroupsTree)) { + $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_CHANGE_OWN_PARENT_GROUPS'), 'error'); + + return false; + } + + // If user is not Super User cannot change the permissions of a Super User Group. + if (!$currentUserSuperUser && $isSuperUserGroupBefore && !$currentUserBelongsToGroup) { + $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_CHANGE_SUPER_USER'), 'error'); + + return false; + } + + // If user is not Super User cannot change the Super User permissions in any group it belongs to. + if ($isSuperUserGroupBefore && $currentUserBelongsToGroup && $permission['action'] === 'core.admin') { + $app->enqueueMessage(Text::_('JLIB_USER_ERROR_CANNOT_DEMOTE_SELF'), 'error'); + + return false; + } + + try { + /** @var Asset $asset */ + $asset = Table::getInstance('asset'); + $result = $asset->loadByName($permission['component']); + + if ($result === false) { + $data = array($permission['action'] => array($permission['rule'] => $permission['value'])); + + $rules = new Rules($data); + $asset->rules = (string) $rules; + $asset->name = (string) $permission['component']; + $asset->title = (string) $permission['title']; + + // Get the parent asset id so we have a correct tree. + /** @var Asset $parentAsset */ + $parentAsset = Table::getInstance('Asset'); + + if (strpos($asset->name, '.') !== false) { + $assetParts = explode('.', $asset->name); + $parentAsset->loadByName($assetParts[0]); + $parentAssetId = $parentAsset->id; + } else { + $parentAssetId = $parentAsset->getRootId(); + } + + /** + * @todo: incorrect ACL stored + * When changing a permission of an item that doesn't have a row in the asset table the row a new row is created. + * This works fine for item <-> component <-> global config scenario and component <-> global config scenario. + * But doesn't work properly for item <-> section(s) <-> component <-> global config scenario, + * because a wrong parent asset id (the component) is stored. + * Happens when there is no row in the asset table (ex: deleted or not created on update). + */ + + $asset->setLocation($parentAssetId, 'last-child'); + } else { + // Decode the rule settings. + $temp = json_decode($asset->rules, true); + + // Check if a new value is to be set. + if (isset($permission['value'])) { + // Check if we already have an action entry. + if (!isset($temp[$permission['action']])) { + $temp[$permission['action']] = array(); + } + + // Check if we already have a rule entry. + if (!isset($temp[$permission['action']][$permission['rule']])) { + $temp[$permission['action']][$permission['rule']] = array(); + } + + // Set the new permission. + $temp[$permission['action']][$permission['rule']] = (int) $permission['value']; + + // Check if we have an inherited setting. + if ($permission['value'] === '') { + unset($temp[$permission['action']][$permission['rule']]); + } + + // Check if we have any rules. + if (!$temp[$permission['action']]) { + unset($temp[$permission['action']]); + } + } else { + // There is no value so remove the action as it's not needed. + unset($temp[$permission['action']]); + } + + $asset->rules = json_encode($temp, JSON_FORCE_OBJECT); + } + + if (!$asset->check() || !$asset->store()) { + $app->enqueueMessage(Text::_('JLIB_UNKNOWN'), 'error'); + + return false; + } + } catch (\Exception $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + // All checks done. + $result = array( + 'text' => '', + 'class' => '', + 'result' => true, + ); + + // Show the current effective calculated permission considering current group, path and cascade. + + try { + // The database instance + $db = $this->getDatabase(); + + // Get the asset id by the name of the component. + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__assets')) + ->where($db->quoteName('name') . ' = :component') + ->bind(':component', $permission['component']); + + $db->setQuery($query); + + $assetId = (int) $db->loadResult(); + + // Fetch the parent asset id. + $parentAssetId = null; + + /** + * @todo: incorrect info + * When creating a new item (not saving) it uses the calculated permissions from the component (item <-> component <-> global config). + * But if we have a section too (item <-> section(s) <-> component <-> global config) this is not correct. + * Also, currently it uses the component permission, but should use the calculated permissions for a child of the component/section. + */ + + // If not in global config we need the parent_id asset to calculate permissions. + if (!$isGlobalConfig) { + // In this case we need to get the component rules too. + $query->clear() + ->select($db->quoteName('parent_id')) + ->from($db->quoteName('#__assets')) + ->where($db->quoteName('id') . ' = :assetid') + ->bind(':assetid', $assetId, ParameterType::INTEGER); + + $db->setQuery($query); + + $parentAssetId = (int) $db->loadResult(); + } + + // Get the group parent id of the current group. + $rule = (int) $permission['rule']; + $query->clear() + ->select($db->quoteName('parent_id')) + ->from($db->quoteName('#__usergroups')) + ->where($db->quoteName('id') . ' = :rule') + ->bind(':rule', $rule, ParameterType::INTEGER); + + $db->setQuery($query); + + $parentGroupId = (int) $db->loadResult(); + + // Count the number of child groups of the current group. + $query->clear() + ->select('COUNT(' . $db->quoteName('id') . ')') + ->from($db->quoteName('#__usergroups')) + ->where($db->quoteName('parent_id') . ' = :rule') + ->bind(':rule', $rule, ParameterType::INTEGER); + + $db->setQuery($query); + + $totalChildGroups = (int) $db->loadResult(); + } catch (\Exception $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + // Clear access statistics. + Access::clearStatics(); + + // After current group permission is changed we need to check again if the group has Super User permissions. + $isSuperUserGroupAfter = Access::checkGroup($permission['rule'], 'core.admin'); + + // Get the rule for just this asset (non-recursive) and get the actual setting for the action for this group. + $assetRule = Access::getAssetRules($assetId, false, false)->allow($permission['action'], $permission['rule']); + + // Get the group, group parent id, and group global config recursive calculated permission for the chosen action. + $inheritedGroupRule = Access::checkGroup($permission['rule'], $permission['action'], $assetId); + + if (!empty($parentAssetId)) { + $inheritedGroupParentAssetRule = Access::checkGroup($permission['rule'], $permission['action'], $parentAssetId); + } else { + $inheritedGroupParentAssetRule = null; + } + + $inheritedParentGroupRule = !empty($parentGroupId) ? Access::checkGroup($parentGroupId, $permission['action'], $assetId) : null; + + // Current group is a Super User group, so calculated setting is "Allowed (Super User)". + if ($isSuperUserGroupAfter) { + $result['class'] = 'badge bg-success'; + $result['text'] = '' . Text::_('JLIB_RULES_ALLOWED_ADMIN'); + } else { + // Not super user. + // First get the real recursive calculated setting and add (Inherited) to it. + + // If recursive calculated setting is "Denied" or null. Calculated permission is "Not Allowed (Inherited)". + if ($inheritedGroupRule === null || $inheritedGroupRule === false) { + $result['class'] = 'badge bg-danger'; + $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_INHERITED'); + } else { + // If recursive calculated setting is "Allowed". Calculated permission is "Allowed (Inherited)". + $result['class'] = 'badge bg-success'; + $result['text'] = Text::_('JLIB_RULES_ALLOWED_INHERITED'); + } + + // Second part: Overwrite the calculated permissions labels if there is an explicit permission in the current group. + + /** + * @todo: incorrect info + * If a component has a permission that doesn't exists in global config (ex: frontend editing in com_modules) by default + * we get "Not Allowed (Inherited)" when we should get "Not Allowed (Default)". + */ + + // If there is an explicit permission "Not Allowed". Calculated permission is "Not Allowed". + if ($assetRule === false) { + $result['class'] = 'badge bg-danger'; + $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED'); + } elseif ($assetRule === true) { + // If there is an explicit permission is "Allowed". Calculated permission is "Allowed". + $result['class'] = 'badge bg-success'; + $result['text'] = Text::_('JLIB_RULES_ALLOWED'); + } + + // Third part: Overwrite the calculated permissions labels for special cases. + + // Global configuration with "Not Set" permission. Calculated permission is "Not Allowed (Default)". + if (empty($parentGroupId) && $isGlobalConfig === true && $assetRule === null) { + $result['class'] = 'badge bg-danger'; + $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_DEFAULT'); + } elseif ($inheritedGroupParentAssetRule === false || $inheritedParentGroupRule === false) { + /** + * Component/Item with explicit "Denied" permission at parent Asset (Category, Component or Global config) configuration. + * Or some parent group has an explicit "Denied". + * Calculated permission is "Not Allowed (Locked)". + */ + $result['class'] = 'badge bg-danger'; + $result['text'] = '' . Text::_('JLIB_RULES_NOT_ALLOWED_LOCKED'); + } + } + + // If removed or added super user from group, we need to refresh the page to recalculate all settings. + if ($isSuperUserGroupBefore != $isSuperUserGroupAfter) { + $app->enqueueMessage(Text::_('JLIB_RULES_NOTICE_RECALCULATE_GROUP_PERMISSIONS'), 'notice'); + } + + // If this group has child groups, we need to refresh the page to recalculate the child settings. + if ($totalChildGroups > 0) { + $app->enqueueMessage(Text::_('JLIB_RULES_NOTICE_RECALCULATE_GROUP_CHILDS_PERMISSIONS'), 'notice'); + } + + return $result; + } + + /** + * Method to send a test mail which is called via an AJAX request + * + * @return boolean + * + * @since 3.5 + */ + public function sendTestMail() + { + // Set the new values to test with the current settings + $app = Factory::getApplication(); + $user = Factory::getUser(); + $input = $app->input->json; + $smtppass = $input->get('smtppass', null, 'RAW'); + + $app->set('smtpauth', $input->get('smtpauth')); + $app->set('smtpuser', $input->get('smtpuser', '', 'STRING')); + $app->set('smtphost', $input->get('smtphost')); + $app->set('smtpsecure', $input->get('smtpsecure')); + $app->set('smtpport', $input->get('smtpport')); + $app->set('mailfrom', $input->get('mailfrom', '', 'STRING')); + $app->set('fromname', $input->get('fromname', '', 'STRING')); + $app->set('mailer', $input->get('mailer')); + $app->set('mailonline', $input->get('mailonline')); + + // Use smtppass only if it was submitted + if ($smtppass !== null) { + $app->set('smtppass', $smtppass); + } + + $mail = Factory::getMailer(); + + // Prepare email and try to send it + $mailer = new MailTemplate('com_config.test_mail', $user->getParam('language', $app->get('language')), $mail); + $mailer->addTemplateData( + array( + 'sitename' => $app->get('sitename'), + 'method' => Text::_('COM_CONFIG_SENDMAIL_METHOD_' . strtoupper($mail->Mailer)) + ) + ); + $mailer->addRecipient($app->get('mailfrom'), $app->get('fromname')); + + try { + $mailSent = $mailer->send(); + } catch (MailDisabledException | phpMailerException $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + if ($mailSent === true) { + $methodName = Text::_('COM_CONFIG_SENDMAIL_METHOD_' . strtoupper($mail->Mailer)); + + // If JMail send the mail using PHP Mail as fallback. + if ($mail->Mailer !== $app->get('mailer')) { + $app->enqueueMessage(Text::sprintf('COM_CONFIG_SENDMAIL_SUCCESS_FALLBACK', $app->get('mailfrom'), $methodName), 'warning'); + } else { + $app->enqueueMessage(Text::sprintf('COM_CONFIG_SENDMAIL_SUCCESS', $app->get('mailfrom'), $methodName), 'message'); + } + + return true; + } + + $app->enqueueMessage(Text::_('COM_CONFIG_SENDMAIL_ERROR'), 'error'); + + return false; + } } diff --git a/administrator/components/com_config/src/Model/ComponentModel.php b/administrator/components/com_config/src/Model/ComponentModel.php index 34e97a703fb44..726cc9ac9ea4f 100644 --- a/administrator/components/com_config/src/Model/ComponentModel.php +++ b/administrator/components/com_config/src/Model/ComponentModel.php @@ -1,4 +1,5 @@ input; - - // Set the component (option) we are dealing with. - $component = $input->get('component'); - - $this->state->set('component.option', $component); - - // Set an alternative path for the configuration file. - if ($path = $input->getString('path')) - { - $path = Path::clean(JPATH_SITE . '/' . $path); - Path::check($path); - $this->state->set('component.path', $path); - } - } - - /** - * Method to get a form object. - * - * @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 mixed A JForm object on success, false on failure - * - * @since 3.2 - */ - public function getForm($data = array(), $loadData = true) - { - $state = $this->getState(); - $option = $state->get('component.option'); - - if ($path = $state->get('component.path')) - { - // Add the search path for the admin component config.xml file. - Form::addFormPath($path); - } - else - { - // Add the search path for the admin component config.xml file. - Form::addFormPath(JPATH_ADMINISTRATOR . '/components/' . $option); - } - - // Get the form. - $form = $this->loadForm( - 'com_config.component', - 'config', - array('control' => 'jform', 'load_data' => $loadData), - false, - '/config' - ); - - if (empty($form)) - { - return false; - } - - $lang = Factory::getLanguage(); - $lang->load($option, JPATH_BASE) - || $lang->load($option, JPATH_BASE . "/components/$option"); - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return array The default data is an empty array. - * - * @since 4.0.0 - */ - protected function loadFormData() - { - $option = $this->getState()->get('component.option'); - - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_config.edit.component.' . $option . '.data', []); - - if (empty($data)) - { - return $this->getComponent()->getParams()->toArray(); - } - - return $data; - } - - /** - * Get the component information. - * - * @return object - * - * @since 3.2 - */ - public function getComponent() - { - $state = $this->getState(); - $option = $state->get('component.option'); - - // Load common and local language files. - $lang = Factory::getLanguage(); - $lang->load($option, JPATH_BASE) - || $lang->load($option, JPATH_BASE . "/components/$option"); - - $result = ComponentHelper::getComponent($option); - - return $result; - } - - /** - * Method to save the configuration data. - * - * @param array $data An array containing all global config data. - * - * @return boolean True on success, false on failure. - * - * @since 3.2 - * @throws \RuntimeException - */ - public function save($data) - { - $table = Table::getInstance('extension'); - $context = $this->option . '.' . $this->name; - PluginHelper::importPlugin('extension'); - - // Check super user group. - if (isset($data['params']) && !Factory::getUser()->authorise('core.admin')) - { - $form = $this->getForm(array(), false); - - foreach ($form->getFieldsets() as $fieldset) - { - foreach ($form->getFieldset($fieldset->name) as $field) - { - if ($field->type === 'UserGroupList' && isset($data['params'][$field->fieldname]) - && (int) $field->getAttribute('checksuperusergroup', 0) === 1 - && Access::checkGroup($data['params'][$field->fieldname], 'core.admin')) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED')); - } - } - } - } - - // Save the rules. - if (isset($data['params']) && isset($data['params']['rules'])) - { - if (!Factory::getUser()->authorise('core.admin', $data['option'])) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED')); - } - - $rules = new Rules($data['params']['rules']); - $asset = Table::getInstance('asset'); - - if (!$asset->loadByName($data['option'])) - { - $root = Table::getInstance('asset'); - $root->loadByName('root.1'); - $asset->name = $data['option']; - $asset->title = $data['option']; - $asset->setLocation($root->id, 'last-child'); - } - - $asset->rules = (string) $rules; - - if (!$asset->check() || !$asset->store()) - { - throw new \RuntimeException($asset->getError()); - } - - // We don't need this anymore - unset($data['option']); - unset($data['params']['rules']); - } - - // Load the previous Data - if (!$table->load($data['id'])) - { - throw new \RuntimeException($table->getError()); - } - - unset($data['id']); - - // Bind the data. - if (!$table->bind($data)) - { - throw new \RuntimeException($table->getError()); - } - - // Check the data. - if (!$table->check()) - { - throw new \RuntimeException($table->getError()); - } - - $result = Factory::getApplication()->triggerEvent('onExtensionBeforeSave', array($context, $table, false)); - - // Store the data. - if (in_array(false, $result, true) || !$table->store()) - { - throw new \RuntimeException($table->getError()); - } - - Factory::getApplication()->triggerEvent('onExtensionAfterSave', array($context, $table, false)); - - // Clean the component cache. - $this->cleanCache('_system'); - - return true; - } + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 3.2 + */ + protected function populateState() + { + $input = Factory::getApplication()->input; + + // Set the component (option) we are dealing with. + $component = $input->get('component'); + + $this->state->set('component.option', $component); + + // Set an alternative path for the configuration file. + if ($path = $input->getString('path')) { + $path = Path::clean(JPATH_SITE . '/' . $path); + Path::check($path); + $this->state->set('component.path', $path); + } + } + + /** + * Method to get a form object. + * + * @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 mixed A JForm object on success, false on failure + * + * @since 3.2 + */ + public function getForm($data = array(), $loadData = true) + { + $state = $this->getState(); + $option = $state->get('component.option'); + + if ($path = $state->get('component.path')) { + // Add the search path for the admin component config.xml file. + Form::addFormPath($path); + } else { + // Add the search path for the admin component config.xml file. + Form::addFormPath(JPATH_ADMINISTRATOR . '/components/' . $option); + } + + // Get the form. + $form = $this->loadForm( + 'com_config.component', + 'config', + array('control' => 'jform', 'load_data' => $loadData), + false, + '/config' + ); + + if (empty($form)) { + return false; + } + + $lang = Factory::getLanguage(); + $lang->load($option, JPATH_BASE) + || $lang->load($option, JPATH_BASE . "/components/$option"); + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return array The default data is an empty array. + * + * @since 4.0.0 + */ + protected function loadFormData() + { + $option = $this->getState()->get('component.option'); + + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_config.edit.component.' . $option . '.data', []); + + if (empty($data)) { + return $this->getComponent()->getParams()->toArray(); + } + + return $data; + } + + /** + * Get the component information. + * + * @return object + * + * @since 3.2 + */ + public function getComponent() + { + $state = $this->getState(); + $option = $state->get('component.option'); + + // Load common and local language files. + $lang = Factory::getLanguage(); + $lang->load($option, JPATH_BASE) + || $lang->load($option, JPATH_BASE . "/components/$option"); + + $result = ComponentHelper::getComponent($option); + + return $result; + } + + /** + * Method to save the configuration data. + * + * @param array $data An array containing all global config data. + * + * @return boolean True on success, false on failure. + * + * @since 3.2 + * @throws \RuntimeException + */ + public function save($data) + { + $table = Table::getInstance('extension'); + $context = $this->option . '.' . $this->name; + PluginHelper::importPlugin('extension'); + + // Check super user group. + if (isset($data['params']) && !Factory::getUser()->authorise('core.admin')) { + $form = $this->getForm(array(), false); + + foreach ($form->getFieldsets() as $fieldset) { + foreach ($form->getFieldset($fieldset->name) as $field) { + if ( + $field->type === 'UserGroupList' && isset($data['params'][$field->fieldname]) + && (int) $field->getAttribute('checksuperusergroup', 0) === 1 + && Access::checkGroup($data['params'][$field->fieldname], 'core.admin') + ) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED')); + } + } + } + } + + // Save the rules. + if (isset($data['params']) && isset($data['params']['rules'])) { + if (!Factory::getUser()->authorise('core.admin', $data['option'])) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED')); + } + + $rules = new Rules($data['params']['rules']); + $asset = Table::getInstance('asset'); + + if (!$asset->loadByName($data['option'])) { + $root = Table::getInstance('asset'); + $root->loadByName('root.1'); + $asset->name = $data['option']; + $asset->title = $data['option']; + $asset->setLocation($root->id, 'last-child'); + } + + $asset->rules = (string) $rules; + + if (!$asset->check() || !$asset->store()) { + throw new \RuntimeException($asset->getError()); + } + + // We don't need this anymore + unset($data['option']); + unset($data['params']['rules']); + } + + // Load the previous Data + if (!$table->load($data['id'])) { + throw new \RuntimeException($table->getError()); + } + + unset($data['id']); + + // Bind the data. + if (!$table->bind($data)) { + throw new \RuntimeException($table->getError()); + } + + // Check the data. + if (!$table->check()) { + throw new \RuntimeException($table->getError()); + } + + $result = Factory::getApplication()->triggerEvent('onExtensionBeforeSave', array($context, $table, false)); + + // Store the data. + if (in_array(false, $result, true) || !$table->store()) { + throw new \RuntimeException($table->getError()); + } + + Factory::getApplication()->triggerEvent('onExtensionAfterSave', array($context, $table, false)); + + // Clean the component cache. + $this->cleanCache('_system'); + + return true; + } } diff --git a/administrator/components/com_config/src/View/Application/HtmlView.php b/administrator/components/com_config/src/View/Application/HtmlView.php index 4811211852143..750e4371ad217 100644 --- a/administrator/components/com_config/src/View/Application/HtmlView.php +++ b/administrator/components/com_config/src/View/Application/HtmlView.php @@ -1,4 +1,5 @@ get('form'); - $data = $this->get('data'); - $user = $this->getCurrentUser(); - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return; - } - - // Bind data - if ($form && $data) - { - $form->bind($data); - } - - // Get the params for com_users. - $usersParams = ComponentHelper::getParams('com_users'); - - // Get the params for com_media. - $mediaParams = ComponentHelper::getParams('com_media'); - - $this->form = &$form; - $this->data = &$data; - $this->usersParams = &$usersParams; - $this->mediaParams = &$mediaParams; - $this->components = ConfigHelper::getComponentsWithConfig(); - ConfigHelper::loadLanguageForComponents($this->components); - - $this->userIsSuperAdmin = $user->authorise('core.admin'); - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.2 - */ - protected function addToolbar() - { - ToolbarHelper::title(Text::_('COM_CONFIG_GLOBAL_CONFIGURATION'), 'cog config'); - ToolbarHelper::apply('application.apply'); - ToolbarHelper::divider(); - ToolbarHelper::save('application.save'); - ToolbarHelper::divider(); - ToolbarHelper::cancel('application.cancel', 'JTOOLBAR_CLOSE'); - ToolbarHelper::divider(); - ToolbarHelper::inlinehelp(); - ToolbarHelper::help('Site_Global_Configuration'); - } + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * @since 3.2 + */ + public $state; + + /** + * The form object + * + * @var \Joomla\CMS\Form\Form + * @since 3.2 + */ + public $form; + + /** + * The data to be displayed in the form + * + * @var array + * @since 3.2 + */ + public $data; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see \JViewLegacy::loadTemplate() + * @since 3.0 + */ + public function display($tpl = null) + { + try { + // Load Form and Data + $form = $this->get('form'); + $data = $this->get('data'); + $user = $this->getCurrentUser(); + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return; + } + + // Bind data + if ($form && $data) { + $form->bind($data); + } + + // Get the params for com_users. + $usersParams = ComponentHelper::getParams('com_users'); + + // Get the params for com_media. + $mediaParams = ComponentHelper::getParams('com_media'); + + $this->form = &$form; + $this->data = &$data; + $this->usersParams = &$usersParams; + $this->mediaParams = &$mediaParams; + $this->components = ConfigHelper::getComponentsWithConfig(); + ConfigHelper::loadLanguageForComponents($this->components); + + $this->userIsSuperAdmin = $user->authorise('core.admin'); + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.2 + */ + protected function addToolbar() + { + ToolbarHelper::title(Text::_('COM_CONFIG_GLOBAL_CONFIGURATION'), 'cog config'); + ToolbarHelper::apply('application.apply'); + ToolbarHelper::divider(); + ToolbarHelper::save('application.save'); + ToolbarHelper::divider(); + ToolbarHelper::cancel('application.cancel', 'JTOOLBAR_CLOSE'); + ToolbarHelper::divider(); + ToolbarHelper::inlinehelp(); + ToolbarHelper::help('Site_Global_Configuration'); + } } diff --git a/administrator/components/com_config/src/View/Component/HtmlView.php b/administrator/components/com_config/src/View/Component/HtmlView.php index b29e47b5031ad..20c3122bf3910 100644 --- a/administrator/components/com_config/src/View/Component/HtmlView.php +++ b/administrator/components/com_config/src/View/Component/HtmlView.php @@ -1,4 +1,5 @@ get('component'); - - if (!$component->enabled) - { - return; - } - - $form = $this->get('form'); - $user = $this->getCurrentUser(); - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return; - } - - $this->fieldsets = $form ? $form->getFieldsets() : null; - $this->formControl = $form ? $form->getFormControl() : null; - - // Don't show permissions fieldset if not authorised. - if (!$user->authorise('core.admin', $component->option) && isset($this->fieldsets['permissions'])) - { - unset($this->fieldsets['permissions']); - } - - $this->form = &$form; - $this->component = &$component; - - $this->components = ConfigHelper::getComponentsWithConfig(); - - $this->userIsSuperAdmin = $user->authorise('core.admin'); - $this->currentComponent = Factory::getApplication()->input->get('component'); - $this->return = Factory::getApplication()->input->get('return', '', 'base64'); - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.2 - */ - protected function addToolbar() - { - ToolbarHelper::title(Text::_($this->component->option . '_configuration'), 'cog config'); - ToolbarHelper::apply('component.apply'); - ToolbarHelper::divider(); - ToolbarHelper::save('component.save'); - ToolbarHelper::divider(); - ToolbarHelper::cancel('component.cancel', 'JTOOLBAR_CLOSE'); - ToolbarHelper::divider(); - - $inlinehelp = (string) $this->form->getXml()->config->inlinehelp['button'] == 'show' ?: false; - $targetClass = (string) $this->form->getXml()->config->inlinehelp['targetclass'] ?: 'hide-aware-inline-help'; - - if ($inlinehelp) - { - ToolbarHelper::inlinehelp($targetClass); - } - - $helpUrl = $this->form->getData()->get('helpURL'); - $helpKey = (string) $this->form->getXml()->config->help['key']; - - // Try with legacy language key - if (!$helpKey) - { - $language = Factory::getApplication()->getLanguage(); - $languageKey = 'JHELP_COMPONENTS_' . strtoupper($this->currentComponent) . '_OPTIONS'; - - if ($language->hasKey($languageKey)) - { - $helpKey = $languageKey; - } - } - - ToolbarHelper::help($helpKey, (boolean) $helpUrl, null, $this->currentComponent); - } + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * @since 3.2 + */ + public $state; + + /** + * The form object + * + * @var \Joomla\CMS\Form\Form + * @since 3.2 + */ + public $form; + + /** + * An object with the information for the component + * + * @var \Joomla\CMS\Component\ComponentRecord + * @since 3.2 + */ + public $component; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see \JViewLegacy::loadTemplate() + * @since 3.2 + */ + public function display($tpl = null) + { + try { + $component = $this->get('component'); + + if (!$component->enabled) { + return; + } + + $form = $this->get('form'); + $user = $this->getCurrentUser(); + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return; + } + + $this->fieldsets = $form ? $form->getFieldsets() : null; + $this->formControl = $form ? $form->getFormControl() : null; + + // Don't show permissions fieldset if not authorised. + if (!$user->authorise('core.admin', $component->option) && isset($this->fieldsets['permissions'])) { + unset($this->fieldsets['permissions']); + } + + $this->form = &$form; + $this->component = &$component; + + $this->components = ConfigHelper::getComponentsWithConfig(); + + $this->userIsSuperAdmin = $user->authorise('core.admin'); + $this->currentComponent = Factory::getApplication()->input->get('component'); + $this->return = Factory::getApplication()->input->get('return', '', 'base64'); + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.2 + */ + protected function addToolbar() + { + ToolbarHelper::title(Text::_($this->component->option . '_configuration'), 'cog config'); + ToolbarHelper::apply('component.apply'); + ToolbarHelper::divider(); + ToolbarHelper::save('component.save'); + ToolbarHelper::divider(); + ToolbarHelper::cancel('component.cancel', 'JTOOLBAR_CLOSE'); + ToolbarHelper::divider(); + + $inlinehelp = (string) $this->form->getXml()->config->inlinehelp['button'] == 'show' ?: false; + $targetClass = (string) $this->form->getXml()->config->inlinehelp['targetclass'] ?: 'hide-aware-inline-help'; + + if ($inlinehelp) { + ToolbarHelper::inlinehelp($targetClass); + } + + $helpUrl = $this->form->getData()->get('helpURL'); + $helpKey = (string) $this->form->getXml()->config->help['key']; + + // Try with legacy language key + if (!$helpKey) { + $language = Factory::getApplication()->getLanguage(); + $languageKey = 'JHELP_COMPONENTS_' . strtoupper($this->currentComponent) . '_OPTIONS'; + + if ($language->hasKey($languageKey)) { + $helpKey = $languageKey; + } + } + + ToolbarHelper::help($helpKey, (bool) $helpUrl, null, $this->currentComponent); + } } diff --git a/administrator/components/com_config/tmpl/application/default.php b/administrator/components/com_config/tmpl/application/default.php index 9dba06f4e808e..9cc769e0d13e1 100644 --- a/administrator/components/com_config/tmpl/application/default.php +++ b/administrator/components/com_config/tmpl/application/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); // Load JS message titles Text::script('ERROR'); @@ -26,56 +27,56 @@ ?>
-
- -
- 'page-site', 'recall' => true, 'breakpoint' => 768]); ?> - - loadTemplate('site'); ?> - loadTemplate('metadata'); ?> - loadTemplate('seo'); ?> - loadTemplate('cookie'); ?> - +
+ +
+ 'page-site', 'recall' => true, 'breakpoint' => 768]); ?> + + loadTemplate('site'); ?> + loadTemplate('metadata'); ?> + loadTemplate('seo'); ?> + loadTemplate('cookie'); ?> + - - loadTemplate('debug'); ?> - loadTemplate('cache'); ?> - loadTemplate('session'); ?> - + + loadTemplate('debug'); ?> + loadTemplate('cache'); ?> + loadTemplate('session'); ?> + - - loadTemplate('server'); ?> - loadTemplate('locale'); ?> - loadTemplate('webservices'); ?> - loadTemplate('proxy'); ?> - loadTemplate('database'); ?> - loadTemplate('mail'); ?> - + + loadTemplate('server'); ?> + loadTemplate('locale'); ?> + loadTemplate('webservices'); ?> + loadTemplate('proxy'); ?> + loadTemplate('database'); ?> + loadTemplate('mail'); ?> + - - loadTemplate('logging'); ?> - loadTemplate('logging_custom'); ?> - + + loadTemplate('logging'); ?> + loadTemplate('logging_custom'); ?> + - - loadTemplate('filters'); ?> - + + loadTemplate('filters'); ?> + - - loadTemplate('permissions'); ?> - - + + loadTemplate('permissions'); ?> + + - - -
-
+ + +
+
diff --git a/administrator/components/com_config/tmpl/application/default_cache.php b/administrator/components/com_config/tmpl/application/default_cache.php index 79d8bb004f498..621f93e66383d 100644 --- a/administrator/components/com_config/tmpl/application/default_cache.php +++ b/administrator/components/com_config/tmpl/application/default_cache.php @@ -1,4 +1,5 @@ document->getWebAssetManager() - ->useScript('webcomponent.field-send-test-mail'); + ->useScript('webcomponent.field-send-test-mail'); // Load JavaScript message titles Text::script('ERROR'); @@ -42,9 +43,9 @@ ?> - + - + diff --git a/administrator/components/com_config/tmpl/application/default_metadata.php b/administrator/components/com_config/tmpl/application/default_metadata.php index 912499a3b6a36..75f9eeb1fea93 100644 --- a/administrator/components/com_config/tmpl/application/default_metadata.php +++ b/administrator/components/com_config/tmpl/application/default_metadata.php @@ -1,4 +1,5 @@ diff --git a/administrator/components/com_config/tmpl/application/default_permissions.php b/administrator/components/com_config/tmpl/application/default_permissions.php index 83cd3c43bfe0b..929e62b1a3cfd 100644 --- a/administrator/components/com_config/tmpl/application/default_permissions.php +++ b/administrator/components/com_config/tmpl/application/default_permissions.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('form.validate') - ->useScript('keepalive'); + ->useScript('keepalive'); -if ($this->fieldsets) -{ - HTMLHelper::_('bootstrap.framework'); +if ($this->fieldsets) { + HTMLHelper::_('bootstrap.framework'); } $xml = $this->form->getXml(); ?>
-
- - - - -
- fieldsets) : ?> - - - true, 'breakpoint' => 768]); ?> - - fieldsets as $name => $fieldSet) : ?> - xpath('//fieldset[@name="' . $name . '"]/fieldset'); - $hasParent = $xml->xpath('//fieldset/fieldset[@name="' . $name . '"]'); - $isGrandchild = $xml->xpath('//fieldset/fieldset/fieldset[@name="' . $name . '"]'); - ?> - - - showon)) : ?> - useScript('showon'); ?> - showon, $this->formControl)) . '\''; ?> - - - label) ? 'COM_CONFIG_' . $name . '_FIELDSET_LABEL' : $fieldSet->label; ?> - - -
- label); ?> -
- - - - 1) : ?> -
-
- - - - - - - - - - - - -
- label); ?> -
- - - - - description)) : ?> -
- - description); ?> -
- - - - form->renderFieldset($name, $name === 'permissions' ? ['hiddenLabel' => true, 'class' => 'revert-controls'] : []); ?> - - - -
-
- - - - - - 1) : ?> -
- - - - - - - - -
- - -
- -
- - - - - - - +
+ + + + +
+ fieldsets) : ?> + + + true, 'breakpoint' => 768]); ?> + + fieldsets as $name => $fieldSet) : ?> + xpath('//fieldset[@name="' . $name . '"]/fieldset'); + $hasParent = $xml->xpath('//fieldset/fieldset[@name="' . $name . '"]'); + $isGrandchild = $xml->xpath('//fieldset/fieldset/fieldset[@name="' . $name . '"]'); + ?> + + + showon)) : ?> + useScript('showon'); ?> + showon, $this->formControl)) . '\''; ?> + + + label) ? 'COM_CONFIG_' . $name . '_FIELDSET_LABEL' : $fieldSet->label; ?> + + +
+ label); ?> +
+ + + 1) : ?> +
+
+ + + + + + + + + + + +
+ label); ?> +
+ + + + + description)) : ?> +
+ + description); ?> +
+ + + + form->renderFieldset($name, $name === 'permissions' ? ['hiddenLabel' => true, 'class' => 'revert-controls'] : []); ?> + + + +
+
+ + + + + 1) : ?> +
+ + + + + + + + +
+ + +
+ +
+ + + + + + +
diff --git a/administrator/components/com_config/tmpl/component/default_navigation.php b/administrator/components/com_config/tmpl/component/default_navigation.php index 7f8f9a68b12cb..2eb654443da86 100644 --- a/administrator/components/com_config/tmpl/component/default_navigation.php +++ b/administrator/components/com_config/tmpl/component/default_navigation.php @@ -1,4 +1,5 @@ diff --git a/administrator/components/com_contact/helpers/contact.php b/administrator/components/com_contact/helpers/contact.php index 262294130943f..8b26f64d3aa49 100644 --- a/administrator/components/com_contact/helpers/contact.php +++ b/administrator/components/com_contact/helpers/contact.php @@ -1,13 +1,16 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; + /** * Contact component helper. diff --git a/administrator/components/com_contact/services/provider.php b/administrator/components/com_contact/services/provider.php index d9d3d0c348f22..21f9104d289ff 100644 --- a/administrator/components/com_contact/services/provider.php +++ b/administrator/components/com_contact/services/provider.php @@ -1,4 +1,5 @@ set(AssociationExtensionInterface::class, new AssociationsHelper); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->set(AssociationExtensionInterface::class, new AssociationsHelper()); - $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Contact')); - $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Contact')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Contact')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Contact')); + $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Contact')); + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Contact')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Contact')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Contact')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new ContactComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new ContactComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); - $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); + $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_contact/src/Controller/AjaxController.php b/administrator/components/com_contact/src/Controller/AjaxController.php index 1aff104743200..4a4649f3ab51f 100644 --- a/administrator/components/com_contact/src/Controller/AjaxController.php +++ b/administrator/components/com_contact/src/Controller/AjaxController.php @@ -1,4 +1,5 @@ input->getInt('assocId', 0); + /** + * Method to fetch associations of a contact + * + * The method assumes that the following http parameters are passed in an Ajax Get request: + * token: the form token + * assocId: the id of the contact whose associations are to be returned + * excludeLang: the association for this language is to be excluded + * + * @return void + * + * @since 3.9.0 + */ + public function fetchAssociations() + { + if (!Session::checkToken('get')) { + echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true); + } else { + $assocId = $this->input->getInt('assocId', 0); - if ($assocId == 0) - { - echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); + if ($assocId == 0) { + echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); - return; - } + return; + } - $excludeLang = $this->input->get('excludeLang', '', 'STRING'); + $excludeLang = $this->input->get('excludeLang', '', 'STRING'); - $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', (int) $assocId); + $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', (int) $assocId); - unset($associations[$excludeLang]); + unset($associations[$excludeLang]); - // Add the title to each of the associated records - $contactTable = $this->factory->createTable('Contact', 'Administrator'); + // Add the title to each of the associated records + $contactTable = $this->factory->createTable('Contact', 'Administrator'); - foreach ($associations as $lang => $association) - { - $contactTable->load($association->id); - $associations[$lang]->title = $contactTable->name; - } + foreach ($associations as $lang => $association) { + $contactTable->load($association->id); + $associations[$lang]->title = $contactTable->name; + } - $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); + $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); - if (count($associations) == 0) - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); - } - elseif ($countContentLanguages > count($associations) + 2) - { - $tags = implode(', ', array_keys($associations)); - $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); - } - else - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); - } + if (count($associations) == 0) { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); + } elseif ($countContentLanguages > count($associations) + 2) { + $tags = implode(', ', array_keys($associations)); + $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); + } else { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); + } - echo new JsonResponse($associations, $message); - } - } + echo new JsonResponse($associations, $message); + } + } } diff --git a/administrator/components/com_contact/src/Controller/ContactController.php b/administrator/components/com_contact/src/Controller/ContactController.php index 0817e4245da84..c305e6a5af50f 100644 --- a/administrator/components/com_contact/src/Controller/ContactController.php +++ b/administrator/components/com_contact/src/Controller/ContactController.php @@ -1,4 +1,5 @@ input->getInt('filter_category_id'), 'int'); - - if ($categoryId) - { - // If the category has been passed in the URL check it. - return $this->app->getIdentity()->authorise('core.create', $this->option . '.category.' . $categoryId); - } - - // In the absence of better information, revert to the component permissions. - return parent::allowAdd($data); - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - - // Since there is no asset tracking, fallback to the component permissions. - if (!$recordId) - { - return parent::allowEdit($data, $key); - } - - // Get the item. - $item = $this->getModel()->getItem($recordId); - - // Since there is no item, return false. - if (empty($item)) - { - return false; - } - - $user = $this->app->getIdentity(); - - // Check if can edit own core.edit.own. - $canEditOwn = $user->authorise('core.edit.own', $this->option . '.category.' . (int) $item->catid) && $item->created_by == $user->id; - - // Check the category core.edit permissions. - return $canEditOwn || $user->authorise('core.edit', $this->option . '.category.' . (int) $item->catid); - } - - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 2.5 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - /** @var \Joomla\Component\Contact\Administrator\Model\ContactModel $model */ - $model = $this->getModel('Contact', 'Administrator', array()); - - // Preset the redirect - $this->setRedirect(Route::_('index.php?option=com_contact&view=contacts' . $this->getRedirectToListAppend(), false)); - - return parent::batch($model); - } - - /** - * Function that allows child controller access to model data - * after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 4.1.0 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = []) - { - if ($this->getTask() === 'save2menu') - { - $editState = []; - - $id = $model->getState('contact.id'); - - $link = 'index.php?option=com_contact&view=contact'; - $type = 'component'; - - $editState['id'] = $id; - $editState['link'] = $link; - $editState['title'] = $model->getItem($id)->name; - $editState['type'] = $type; - $editState['request']['id'] = $id; - - $this->app->setUserState( - 'com_menus.edit.item', - [ - 'data' => $editState, - 'type' => $type, - 'link' => $link, - ] - ); - - $this->setRedirect(Route::_('index.php?option=com_menus&view=item&client_id=0&menutype=mainmenu&layout=edit', false)); - } - } + use VersionableControllerTrait; + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowAdd($data = array()) + { + $categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('filter_category_id'), 'int'); + + if ($categoryId) { + // If the category has been passed in the URL check it. + return $this->app->getIdentity()->authorise('core.create', $this->option . '.category.' . $categoryId); + } + + // In the absence of better information, revert to the component permissions. + return parent::allowAdd($data); + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + + // Since there is no asset tracking, fallback to the component permissions. + if (!$recordId) { + return parent::allowEdit($data, $key); + } + + // Get the item. + $item = $this->getModel()->getItem($recordId); + + // Since there is no item, return false. + if (empty($item)) { + return false; + } + + $user = $this->app->getIdentity(); + + // Check if can edit own core.edit.own. + $canEditOwn = $user->authorise('core.edit.own', $this->option . '.category.' . (int) $item->catid) && $item->created_by == $user->id; + + // Check the category core.edit permissions. + return $canEditOwn || $user->authorise('core.edit', $this->option . '.category.' . (int) $item->catid); + } + + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 2.5 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + /** @var \Joomla\Component\Contact\Administrator\Model\ContactModel $model */ + $model = $this->getModel('Contact', 'Administrator', array()); + + // Preset the redirect + $this->setRedirect(Route::_('index.php?option=com_contact&view=contacts' . $this->getRedirectToListAppend(), false)); + + return parent::batch($model); + } + + /** + * Function that allows child controller access to model data + * after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 4.1.0 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = []) + { + if ($this->getTask() === 'save2menu') { + $editState = []; + + $id = $model->getState('contact.id'); + + $link = 'index.php?option=com_contact&view=contact'; + $type = 'component'; + + $editState['id'] = $id; + $editState['link'] = $link; + $editState['title'] = $model->getItem($id)->name; + $editState['type'] = $type; + $editState['request']['id'] = $id; + + $this->app->setUserState( + 'com_menus.edit.item', + [ + 'data' => $editState, + 'type' => $type, + 'link' => $link, + ] + ); + + $this->setRedirect(Route::_('index.php?option=com_menus&view=item&client_id=0&menutype=mainmenu&layout=edit', false)); + } + } } diff --git a/administrator/components/com_contact/src/Controller/ContactsController.php b/administrator/components/com_contact/src/Controller/ContactsController.php index 953b454c28863..87c6ea36ffaaa 100644 --- a/administrator/components/com_contact/src/Controller/ContactsController.php +++ b/administrator/components/com_contact/src/Controller/ContactsController.php @@ -1,4 +1,5 @@ registerTask('unfeatured', 'featured'); - } - - /** - * Method to toggle the featured setting of a list of contacts. - * - * @return void - * - * @since 1.6 - */ - public function featured() - { - // Check for request forgeries - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - $values = array('featured' => 1, 'unfeatured' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($values, $task, 0, 'int'); - - // Get the model. - /** @var \Joomla\Component\Contact\Administrator\Model\ContactModel $model */ - $model = $this->getModel(); - - // Access checks. - foreach ($ids as $i => $id) - { - // Remove zero value resulting from input filter - if ($id === 0) - { - unset($ids[$i]); - - continue; - } - - $item = $model->getItem($id); - - if (!$this->app->getIdentity()->authorise('core.edit.state', 'com_contact.category.' . (int) $item->catid)) - { - // Prune items that you can't change. - unset($ids[$i]); - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'notice'); - } - } - - if (empty($ids)) - { - $message = null; - - $this->app->enqueueMessage(Text::_('COM_CONTACT_NO_ITEM_SELECTED'), 'warning'); - } - else - { - // Publish the items. - if (!$model->featured($ids, $value)) - { - $this->app->enqueueMessage($model->getError(), 'warning'); - } - - if ($value == 1) - { - $message = Text::plural('COM_CONTACT_N_ITEMS_FEATURED', count($ids)); - } - else - { - $message = Text::plural('COM_CONTACT_N_ITEMS_UNFEATURED', count($ids)); - } - } - - $this->setRedirect('index.php?option=com_contact&view=contacts', $message); - } - - /** - * Proxy for getModel. - * - * @param string $name The name of the model. - * @param string $prefix The prefix for the PHP class name. - * @param array $config Array of configuration parameters. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel - * - * @since 1.6 - */ - public function getModel($name = 'Contact', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * 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 Application for the dispatcher + * @param Input $input Input + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('unfeatured', 'featured'); + } + + /** + * Method to toggle the featured setting of a list of contacts. + * + * @return void + * + * @since 1.6 + */ + public function featured() + { + // Check for request forgeries + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + $values = array('featured' => 1, 'unfeatured' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($values, $task, 0, 'int'); + + // Get the model. + /** @var \Joomla\Component\Contact\Administrator\Model\ContactModel $model */ + $model = $this->getModel(); + + // Access checks. + foreach ($ids as $i => $id) { + // Remove zero value resulting from input filter + if ($id === 0) { + unset($ids[$i]); + + continue; + } + + $item = $model->getItem($id); + + if (!$this->app->getIdentity()->authorise('core.edit.state', 'com_contact.category.' . (int) $item->catid)) { + // Prune items that you can't change. + unset($ids[$i]); + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'notice'); + } + } + + if (empty($ids)) { + $message = null; + + $this->app->enqueueMessage(Text::_('COM_CONTACT_NO_ITEM_SELECTED'), 'warning'); + } else { + // Publish the items. + if (!$model->featured($ids, $value)) { + $this->app->enqueueMessage($model->getError(), 'warning'); + } + + if ($value == 1) { + $message = Text::plural('COM_CONTACT_N_ITEMS_FEATURED', count($ids)); + } else { + $message = Text::plural('COM_CONTACT_N_ITEMS_UNFEATURED', count($ids)); + } + } + + $this->setRedirect('index.php?option=com_contact&view=contacts', $message); + } + + /** + * Proxy for getModel. + * + * @param string $name The name of the model. + * @param string $prefix The prefix for the PHP class name. + * @param array $config Array of configuration parameters. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel + * + * @since 1.6 + */ + public function getModel($name = 'Contact', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_contact/src/Controller/DisplayController.php b/administrator/components/com_contact/src/Controller/DisplayController.php index 81f436b15d2a6..abbc104d0a4fb 100644 --- a/administrator/components/com_contact/src/Controller/DisplayController.php +++ b/administrator/components/com_contact/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', $this->default_view); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); - - // Check for edit form. - if ($view == 'contact' && $layout == 'edit' && !$this->checkEditId('com_contact.edit.contact', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_contact&view=contacts', false)); - - return false; - } - - return parent::display(); - } + /** + * The default view. + * + * @var string + * @since 1.6 + */ + protected $default_view = 'contacts'; + + /** + * Method to display a view. + * + * @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()}. + * + * @return static |boolean This object to support chaining. False on failure. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = array()) + { + $view = $this->input->get('view', $this->default_view); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); + + // Check for edit form. + if ($view == 'contact' && $layout == 'edit' && !$this->checkEditId('com_contact.edit.contact', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_contact&view=contacts', false)); + + return false; + } + + return parent::display(); + } } diff --git a/administrator/components/com_contact/src/Extension/ContactComponent.php b/administrator/components/com_contact/src/Extension/ContactComponent.php index cfddee3de41a3..9bb600d4ab5a4 100644 --- a/administrator/components/com_contact/src/Extension/ContactComponent.php +++ b/administrator/components/com_contact/src/Extension/ContactComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('contactadministrator', new AdministratorService); - $this->getRegistry()->register('contacticon', new Icon($container->get(UserFactoryInterface::class))); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('contactadministrator', new AdministratorService()); + $this->getRegistry()->register('contacticon', new Icon($container->get(UserFactoryInterface::class))); + } - /** - * Returns a valid section for the given section. If it is not valid then null - * is returned. - * - * @param string $section The section to get the mapping for - * @param object $item The item - * - * @return string|null The new section - * - * @since 4.0.0 - */ - public function validateSection($section, $item = null) - { - if (Factory::getApplication()->isClient('site') && $section == 'contact' && $item instanceof Form) - { - // The contact form needs to be the mail section - $section = 'mail'; - } + /** + * Returns a valid section for the given section. If it is not valid then null + * is returned. + * + * @param string $section The section to get the mapping for + * @param object $item The item + * + * @return string|null The new section + * + * @since 4.0.0 + */ + public function validateSection($section, $item = null) + { + if (Factory::getApplication()->isClient('site') && $section == 'contact' && $item instanceof Form) { + // The contact form needs to be the mail section + $section = 'mail'; + } - if (Factory::getApplication()->isClient('site') && ($section === 'category' || $section === 'form')) - { - // The contact form needs to be the mail section - $section = 'contact'; - } + if (Factory::getApplication()->isClient('site') && ($section === 'category' || $section === 'form')) { + // The contact form needs to be the mail section + $section = 'contact'; + } - if ($section !== 'mail' && $section !== 'contact') - { - // We don't know other sections - return null; - } + if ($section !== 'mail' && $section !== 'contact') { + // We don't know other sections + return null; + } - return $section; - } + return $section; + } - /** - * Returns valid contexts - * - * @return array - * - * @since 4.0.0 - */ - public function getContexts(): array - { - Factory::getLanguage()->load('com_contact', JPATH_ADMINISTRATOR); + /** + * Returns valid contexts + * + * @return array + * + * @since 4.0.0 + */ + public function getContexts(): array + { + Factory::getLanguage()->load('com_contact', JPATH_ADMINISTRATOR); - $contexts = array( - 'com_contact.contact' => Text::_('COM_CONTACT_FIELDS_CONTEXT_CONTACT'), - 'com_contact.mail' => Text::_('COM_CONTACT_FIELDS_CONTEXT_MAIL'), - 'com_contact.categories' => Text::_('JCATEGORY') - ); + $contexts = array( + 'com_contact.contact' => Text::_('COM_CONTACT_FIELDS_CONTEXT_CONTACT'), + 'com_contact.mail' => Text::_('COM_CONTACT_FIELDS_CONTEXT_MAIL'), + 'com_contact.categories' => Text::_('JCATEGORY') + ); - return $contexts; - } + return $contexts; + } - /** - * Returns the table for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getTableNameForSection(string $section = null) - { - return ($section === 'category' ? 'categories' : 'contact_details'); - } + /** + * Returns the table for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getTableNameForSection(string $section = null) + { + return ($section === 'category' ? 'categories' : 'contact_details'); + } - /** - * Returns the state column for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getStateColumnForSection(string $section = null) - { - return 'published'; - } + /** + * Returns the state column for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getStateColumnForSection(string $section = null) + { + return 'published'; + } } diff --git a/administrator/components/com_contact/src/Field/Modal/ContactField.php b/administrator/components/com_contact/src/Field/Modal/ContactField.php index a67d64497d0df..5f1268e84a881 100644 --- a/administrator/components/com_contact/src/Field/Modal/ContactField.php +++ b/administrator/components/com_contact/src/Field/Modal/ContactField.php @@ -1,4 +1,5 @@ element['new'] == 'true'); - $allowEdit = ((string) $this->element['edit'] == 'true'); - $allowClear = ((string) $this->element['clear'] != 'false'); - $allowSelect = ((string) $this->element['select'] != 'false'); - $allowPropagate = ((string) $this->element['propagate'] == 'true'); - - $languages = LanguageHelper::getContentLanguages(array(0, 1), false); - - // Load language - Factory::getLanguage()->load('com_contact', JPATH_ADMINISTRATOR); - - // The active contact id field. - $value = (int) $this->value ?: ''; - - // Create the modal id. - $modalId = 'Contact_' . $this->id; - - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); - - // Add the modal field script to the document head. - $wa->useScript('field.modal-fields'); - - // Script to proxy the select modal function to the modal-fields.js file. - if ($allowSelect) - { - static $scriptSelect = null; - - if (is_null($scriptSelect)) - { - $scriptSelect = array(); - } - - if (!isset($scriptSelect[$this->id])) - { - $wa->addInlineScript(" + /** + * The form field type. + * + * @var string + * @since 1.6 + */ + protected $type = 'Modal_Contact'; + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + $allowNew = ((string) $this->element['new'] == 'true'); + $allowEdit = ((string) $this->element['edit'] == 'true'); + $allowClear = ((string) $this->element['clear'] != 'false'); + $allowSelect = ((string) $this->element['select'] != 'false'); + $allowPropagate = ((string) $this->element['propagate'] == 'true'); + + $languages = LanguageHelper::getContentLanguages(array(0, 1), false); + + // Load language + Factory::getLanguage()->load('com_contact', JPATH_ADMINISTRATOR); + + // The active contact id field. + $value = (int) $this->value ?: ''; + + // Create the modal id. + $modalId = 'Contact_' . $this->id; + + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + + // Add the modal field script to the document head. + $wa->useScript('field.modal-fields'); + + // Script to proxy the select modal function to the modal-fields.js file. + if ($allowSelect) { + static $scriptSelect = null; + + if (is_null($scriptSelect)) { + $scriptSelect = array(); + } + + if (!isset($scriptSelect[$this->id])) { + $wa->addInlineScript( + " window.jSelectContact_" . $this->id . " = function (id, title, object) { window.processModalSelect('Contact', '" . $this->id . "', id, title, '', object); }", - [], - ['type' => 'module'] - ); - - Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); - - $scriptSelect[$this->id] = true; - } - } - - // Setup variables for display. - $linkContacts = 'index.php?option=com_contact&view=contacts&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; - $linkContact = 'index.php?option=com_contact&view=contact&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; - $modalTitle = Text::_('COM_CONTACT_SELECT_A_CONTACT'); - - if (isset($this->element['language'])) - { - $linkContacts .= '&forcedLanguage=' . $this->element['language']; - $linkContact .= '&forcedLanguage=' . $this->element['language']; - $modalTitle .= ' — ' . $this->element['label']; - } - - $urlSelect = $linkContacts . '&function=jSelectContact_' . $this->id; - $urlEdit = $linkContact . '&task=contact.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; - $urlNew = $linkContact . '&task=contact.add'; - - if ($value) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('name')) - ->from($db->quoteName('#__contact_details')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $value, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $title = $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - - $title = empty($title) ? Text::_('COM_CONTACT_SELECT_A_CONTACT') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); - - // The current contact display field. - $html = ''; - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - $html .= ''; - - // Select contact button - if ($allowSelect) - { - $html .= '' - . ' ' . Text::_('JSELECT') - . ''; - } - - // New contact button - if ($allowNew) - { - $html .= '' - . ' ' . Text::_('JACTION_CREATE') - . ''; - } - - // Edit contact button - if ($allowEdit) - { - $html .= '' - . ' ' . Text::_('JACTION_EDIT') - . ''; - } - - // Clear contact button - if ($allowClear) - { - $html .= '' - . ' ' . Text::_('JCLEAR') - . ''; - } - - // Propagate contact button - if ($allowPropagate && count($languages) > 2) - { - // Strip off language tag at the end - $tagLength = (int) strlen($this->element['language']); - $callbackFunctionStem = substr("jSelectContact_" . $this->id, 0, -$tagLength); - - $html .= '' - . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') - . ''; - } - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - // Select contact modal - if ($allowSelect) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalSelect' . $modalId, - array( - 'title' => $modalTitle, - 'url' => $urlSelect, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ) - ); - } - - // New contact modal - if ($allowNew) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalNew' . $modalId, - array( - 'title' => Text::_('COM_CONTACT_NEW_CONTACT'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlNew, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Edit contact modal. - if ($allowEdit) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalEdit' . $modalId, - array( - 'title' => Text::_('COM_CONTACT_EDIT_CONTACT'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlEdit, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Note: class='required' for client side validation. - $class = $this->required ? ' class="required modal-value"' : ''; - - $html .= ''; - - return $html; - } - - /** - * Method to get the field label markup. - * - * @return string The field label markup. - * - * @since 3.4 - */ - protected function getLabel() - { - return str_replace($this->id, $this->id . '_name', parent::getLabel()); - } + [], + ['type' => 'module'] + ); + + Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); + + $scriptSelect[$this->id] = true; + } + } + + // Setup variables for display. + $linkContacts = 'index.php?option=com_contact&view=contacts&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; + $linkContact = 'index.php?option=com_contact&view=contact&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; + $modalTitle = Text::_('COM_CONTACT_SELECT_A_CONTACT'); + + if (isset($this->element['language'])) { + $linkContacts .= '&forcedLanguage=' . $this->element['language']; + $linkContact .= '&forcedLanguage=' . $this->element['language']; + $modalTitle .= ' — ' . $this->element['label']; + } + + $urlSelect = $linkContacts . '&function=jSelectContact_' . $this->id; + $urlEdit = $linkContact . '&task=contact.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; + $urlNew = $linkContact . '&task=contact.add'; + + if ($value) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('name')) + ->from($db->quoteName('#__contact_details')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $value, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $title = $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + + $title = empty($title) ? Text::_('COM_CONTACT_SELECT_A_CONTACT') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); + + // The current contact display field. + $html = ''; + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + $html .= ''; + + // Select contact button + if ($allowSelect) { + $html .= '' + . ' ' . Text::_('JSELECT') + . ''; + } + + // New contact button + if ($allowNew) { + $html .= '' + . ' ' . Text::_('JACTION_CREATE') + . ''; + } + + // Edit contact button + if ($allowEdit) { + $html .= '' + . ' ' . Text::_('JACTION_EDIT') + . ''; + } + + // Clear contact button + if ($allowClear) { + $html .= '' + . ' ' . Text::_('JCLEAR') + . ''; + } + + // Propagate contact button + if ($allowPropagate && count($languages) > 2) { + // Strip off language tag at the end + $tagLength = (int) strlen($this->element['language']); + $callbackFunctionStem = substr("jSelectContact_" . $this->id, 0, -$tagLength); + + $html .= '' + . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') + . ''; + } + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + // Select contact modal + if ($allowSelect) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalSelect' . $modalId, + array( + 'title' => $modalTitle, + 'url' => $urlSelect, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ) + ); + } + + // New contact modal + if ($allowNew) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalNew' . $modalId, + array( + 'title' => Text::_('COM_CONTACT_NEW_CONTACT'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlNew, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Edit contact modal. + if ($allowEdit) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalEdit' . $modalId, + array( + 'title' => Text::_('COM_CONTACT_EDIT_CONTACT'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlEdit, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Note: class='required' for client side validation. + $class = $this->required ? ' class="required modal-value"' : ''; + + $html .= ''; + + return $html; + } + + /** + * Method to get the field label markup. + * + * @return string The field label markup. + * + * @since 3.4 + */ + protected function getLabel() + { + return str_replace($this->id, $this->id . '_name', parent::getLabel()); + } } diff --git a/administrator/components/com_contact/src/Helper/AssociationsHelper.php b/administrator/components/com_contact/src/Helper/AssociationsHelper.php index 87551616a9836..f8e7c64f091d8 100644 --- a/administrator/components/com_contact/src/Helper/AssociationsHelper.php +++ b/administrator/components/com_contact/src/Helper/AssociationsHelper.php @@ -1,4 +1,5 @@ getType($typeName); - - $context = $this->extension . '.item'; - $catidField = 'catid'; - - if ($typeName === 'category') - { - $context = 'com_categories.item'; - $catidField = ''; - } - - // Get the associations. - $associations = Associations::getAssociations( - $this->extension, - $type['tables']['a'], - $context, - $id, - 'id', - 'alias', - $catidField - ); - - return $associations; - } - - /** - * Get item information - * - * @param string $typeName The item type - * @param int $id The id of item for which we need the associated items - * - * @return Table|null - * - * @since 3.7.0 - */ - public function getItem($typeName, $id) - { - if (empty($id)) - { - return null; - } - - $table = null; - - switch ($typeName) - { - case 'contact': - $table = Table::getInstance('ContactTable', 'Joomla\\Component\\Contact\\Administrator\\Table\\'); - break; - - case 'category': - $table = Table::getInstance('Category'); - break; - } - - if (empty($table)) - { - return null; - } - - $table->load($id); - - return $table; - } - - /** - * Get information about the type - * - * @param string $typeName The item type - * - * @return array Array of item types - * - * @since 3.7.0 - */ - public function getType($typeName = '') - { - $fields = $this->getFieldsTemplate(); - $tables = array(); - $joins = array(); - $support = $this->getSupportTemplate(); - $title = ''; - - if (in_array($typeName, $this->itemTypes)) - { - switch ($typeName) - { - case 'contact': - $fields['title'] = 'a.name'; - $fields['state'] = 'a.published'; - - $support['state'] = true; - $support['acl'] = true; - $support['checkout'] = true; - $support['category'] = true; - $support['save2copy'] = true; - - $tables = array( - 'a' => '#__contact_details' - ); - - $title = 'contact'; - break; - - case 'category': - $fields['created_user_id'] = 'a.created_user_id'; - $fields['ordering'] = 'a.lft'; - $fields['level'] = 'a.level'; - $fields['catid'] = ''; - $fields['state'] = 'a.published'; - - $support['state'] = true; - $support['acl'] = true; - $support['checkout'] = true; - $support['level'] = true; - - $tables = array( - 'a' => '#__categories' - ); - - $title = 'category'; - break; - } - } - - return array( - 'fields' => $fields, - 'support' => $support, - 'tables' => $tables, - 'joins' => $joins, - 'title' => $title - ); - } + /** + * The extension name + * + * @var array $extension + * + * @since 3.7.0 + */ + protected $extension = 'com_contact'; + + /** + * Array of item types + * + * @var array $itemTypes + * + * @since 3.7.0 + */ + protected $itemTypes = array('contact', 'category'); + + /** + * Has the extension association support + * + * @var boolean $associationsSupport + * + * @since 3.7.0 + */ + protected $associationsSupport = true; + + /** + * Method to get the associations for a given item. + * + * @param integer $id Id of the item + * @param string $view Name of the view + * + * @return array Array of associations for the item + * + * @since 4.0.0 + */ + public function getAssociationsForItem($id = 0, $view = null) + { + return AssociationHelper::getAssociations($id, $view); + } + + /** + * Get the associated items for an item + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return array + * + * @since 3.7.0 + */ + public function getAssociations($typeName, $id) + { + $type = $this->getType($typeName); + + $context = $this->extension . '.item'; + $catidField = 'catid'; + + if ($typeName === 'category') { + $context = 'com_categories.item'; + $catidField = ''; + } + + // Get the associations. + $associations = Associations::getAssociations( + $this->extension, + $type['tables']['a'], + $context, + $id, + 'id', + 'alias', + $catidField + ); + + return $associations; + } + + /** + * Get item information + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return Table|null + * + * @since 3.7.0 + */ + public function getItem($typeName, $id) + { + if (empty($id)) { + return null; + } + + $table = null; + + switch ($typeName) { + case 'contact': + $table = Table::getInstance('ContactTable', 'Joomla\\Component\\Contact\\Administrator\\Table\\'); + break; + + case 'category': + $table = Table::getInstance('Category'); + break; + } + + if (empty($table)) { + return null; + } + + $table->load($id); + + return $table; + } + + /** + * Get information about the type + * + * @param string $typeName The item type + * + * @return array Array of item types + * + * @since 3.7.0 + */ + public function getType($typeName = '') + { + $fields = $this->getFieldsTemplate(); + $tables = array(); + $joins = array(); + $support = $this->getSupportTemplate(); + $title = ''; + + if (in_array($typeName, $this->itemTypes)) { + switch ($typeName) { + case 'contact': + $fields['title'] = 'a.name'; + $fields['state'] = 'a.published'; + + $support['state'] = true; + $support['acl'] = true; + $support['checkout'] = true; + $support['category'] = true; + $support['save2copy'] = true; + + $tables = array( + 'a' => '#__contact_details' + ); + + $title = 'contact'; + break; + + case 'category': + $fields['created_user_id'] = 'a.created_user_id'; + $fields['ordering'] = 'a.lft'; + $fields['level'] = 'a.level'; + $fields['catid'] = ''; + $fields['state'] = 'a.published'; + + $support['state'] = true; + $support['acl'] = true; + $support['checkout'] = true; + $support['level'] = true; + + $tables = array( + 'a' => '#__categories' + ); + + $title = 'category'; + break; + } + } + + return array( + 'fields' => $fields, + 'support' => $support, + 'tables' => $tables, + 'joins' => $joins, + 'title' => $title + ); + } } diff --git a/administrator/components/com_contact/src/Helper/ContactHelper.php b/administrator/components/com_contact/src/Helper/ContactHelper.php index 8928555cc685d..a6857efffcd35 100644 --- a/administrator/components/com_contact/src/Helper/ContactHelper.php +++ b/administrator/components/com_contact/src/Helper/ContactHelper.php @@ -1,4 +1,5 @@ 'batchAccess', - 'language_id' => 'batchLanguage', - 'tag' => 'batchTag', - 'user_id' => 'batchUser', - ); - - /** - * Name of the form - * - * @var string - * @since 4.0.0 - */ - protected $formName = 'contact'; - - /** - * Batch change a linked user. - * - * @param integer $value The new value matching a User ID. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 2.5 - */ - protected function batchUser($value, $pks, $contexts) - { - foreach ($pks as $pk) - { - if ($this->user->authorise('core.edit', $contexts[$pk])) - { - $this->table->reset(); - $this->table->load($pk); - $this->table->user_id = (int) $value; - - if (!$this->table->store()) - { - $this->setError($this->table->getError()); - - return false; - } - } - else - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->published != -2) - { - return false; - } - - return Factory::getUser()->authorise('core.delete', 'com_contact.category.' . (int) $record->catid); - } - - /** - * Method to test whether a record can have its state edited. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canEditState($record) - { - // Check against the category. - if (!empty($record->catid)) - { - return Factory::getUser()->authorise('core.edit.state', 'com_contact.category.' . (int) $record->catid); - } - - // Default to component settings if category not known. - return parent::canEditState($record); - } - - /** - * Method to get the row form. - * - * @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 Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - Form::addFieldPath(JPATH_ADMINISTRATOR . '/components/com_users/models/fields'); - - // Get the form. - $form = $this->loadForm('com_contact.' . $this->formName, $this->formName, array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Modify the form based on access controls. - if (!$this->canEditState((object) $data)) - { - // Disable fields for display. - $form->setFieldAttribute('featured', 'disabled', 'true'); - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('published', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('featured', 'filter', 'unset'); - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - // Don't allow to change the created_by user if not allowed to access com_users. - if (!Factory::getUser()->authorise('core.manage', 'com_users')) - { - $form->setFieldAttribute('created_by', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - if ($item = parent::getItem($pk)) - { - // Convert the metadata field to an array. - $registry = new Registry($item->metadata); - $item->metadata = $registry->toArray(); - } - - // Load associated contact items - $assoc = Associations::isEnabled(); - - if ($assoc) - { - $item->associations = array(); - - if ($item->id != null) - { - $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $item->id); - - foreach ($associations as $tag => $association) - { - $item->associations[$tag] = $association->id; - } - } - } - - // Load item tags - if (!empty($item->id)) - { - $item->tags = new TagsHelper; - $item->tags->getTagIds($item->id, 'com_contact.contact'); - } - - return $item; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - $app = Factory::getApplication(); - - // Check the session for previously entered form data. - $data = $app->getUserState('com_contact.edit.contact.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Prime some default values. - if ($this->getState('contact.id') == 0) - { - $data->set('catid', $app->input->get('catid', $app->getUserState('com_contact.contacts.filter.category_id'), 'int')); - } - } - - $this->preprocessData('com_contact.contact', $data); - - return $data; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 3.0 - */ - public function save($data) - { - $input = Factory::getApplication()->input; - - // Create new category, if needed. - $createCategory = true; - - // If category ID is provided, check if it's valid. - if (is_numeric($data['catid']) && $data['catid']) - { - $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_contact'); - } - - // Save New Category - if ($createCategory && $this->canCreateCategory()) - { - $category = [ - // Remove #new# prefix, if exists. - 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], - 'parent_id' => 1, - 'extension' => 'com_contact', - 'language' => $data['language'], - 'published' => 1, - ]; - - /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ - $categoryModel = Factory::getApplication()->bootComponent('com_categories') - ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); - - // Create new category. - if (!$categoryModel->save($category)) - { - $this->setError($categoryModel->getError()); - - return false; - } - - // Get the Category ID. - $data['catid'] = $categoryModel->getState('category.id'); - } - - // Alter the name for save as copy - if ($input->get('task') == 'save2copy') - { - $origTable = clone $this->getTable(); - $origTable->load($input->getInt('id')); - - if ($data['name'] == $origTable->name) - { - list($name, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['name']); - $data['name'] = $name; - $data['alias'] = $alias; - } - else - { - if ($data['alias'] == $origTable->alias) - { - $data['alias'] = ''; - } - } - - $data['published'] = 0; - } - - $links = array('linka', 'linkb', 'linkc', 'linkd', 'linke'); - - foreach ($links as $link) - { - if (!empty($data['params'][$link])) - { - $data['params'][$link] = PunycodeHelper::urlToPunycode($data['params'][$link]); - } - } - - return parent::save($data); - } - - /** - * Prepare and sanitise the table prior to saving. - * - * @param \Joomla\CMS\Table\Table $table The Table object - * - * @return void - * - * @since 1.6 - */ - protected function prepareTable($table) - { - $date = Factory::getDate()->toSql(); - - $table->name = htmlspecialchars_decode($table->name, ENT_QUOTES); - - $table->generateAlias(); - - if (empty($table->id)) - { - // Set the values - $table->created = $date; - - // Set ordering to the last item if not set - if (empty($table->ordering)) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('MAX(ordering)') - ->from($db->quoteName('#__contact_details')); - $db->setQuery($query); - $max = $db->loadResult(); - - $table->ordering = $max + 1; - } - } - else - { - // Set the values - $table->modified = $date; - $table->modified_by = Factory::getUser()->id; - } - - // Increment the content version number. - $table->version++; - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param \Joomla\CMS\Table\Table $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - return [ - $this->getDatabase()->quoteName('catid') . ' = ' . (int) $table->catid, - ]; - } - - /** - * Preprocess the form. - * - * @param Form $form Form object. - * @param object $data Data object. - * @param string $group Group name. - * - * @return void - * - * @since 3.0.3 - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - if ($this->canCreateCategory()) - { - $form->setFieldAttribute('catid', 'allowAdd', 'true'); - - // Add a prefix for categories created on the fly. - $form->setFieldAttribute('catid', 'customPrefix', '#new#'); - } - - // Association contact items - if (Associations::isEnabled()) - { - $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); - - if (count($languages) > 1) - { - $addform = new \SimpleXMLElement('
'); - $fields = $addform->addChild('fields'); - $fields->addAttribute('name', 'associations'); - $fieldset = $fields->addChild('fieldset'); - $fieldset->addAttribute('name', 'item_associations'); - - foreach ($languages as $language) - { - $field = $fieldset->addChild('field'); - $field->addAttribute('name', $language->lang_code); - $field->addAttribute('type', 'modal_contact'); - $field->addAttribute('language', $language->lang_code); - $field->addAttribute('label', $language->title); - $field->addAttribute('translate_label', 'false'); - $field->addAttribute('select', 'true'); - $field->addAttribute('new', 'true'); - $field->addAttribute('edit', 'true'); - $field->addAttribute('clear', 'true'); - $field->addAttribute('propagate', 'true'); - } - - $form->load($addform, false); - } - } - - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to toggle the featured setting of contacts. - * - * @param array $pks The ids of the items to toggle. - * @param integer $value The value to toggle to. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function featured($pks, $value = 0) - { - // Sanitize the ids. - $pks = ArrayHelper::toInteger((array) $pks); - - if (empty($pks)) - { - $this->setError(Text::_('COM_CONTACT_NO_ITEM_SELECTED')); - - return false; - } - - $table = $this->getTable(); - - try - { - $db = $this->getDatabase(); - - $query = $db->getQuery(true); - $query->update($db->quoteName('#__contact_details')); - $query->set($db->quoteName('featured') . ' = :featured'); - $query->whereIn($db->quoteName('id'), $pks); - $query->bind(':featured', $value, ParameterType::INTEGER); - - $db->setQuery($query); - - $db->execute(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - $table->reorder(); - - // Clean component's cache - $this->cleanCache(); - - return true; - } - - /** - * Is the user allowed to create an on the fly category? - * - * @return boolean - * - * @since 3.6.1 - */ - private function canCreateCategory() - { - return Factory::getUser()->authorise('core.create', 'com_contact'); - } + use VersionableModelTrait; + + /** + * The type alias for this content type. + * + * @var string + * @since 3.2 + */ + public $typeAlias = 'com_contact.contact'; + + /** + * The context used for the associations table + * + * @var string + * @since 3.4.4 + */ + protected $associationsContext = 'com_contact.item'; + + /** + * Batch copy/move command. If set to false, the batch copy/move command is not supported + * + * @var string + */ + protected $batch_copymove = 'category_id'; + + /** + * Allowed batch commands + * + * @var array + */ + protected $batch_commands = array( + 'assetgroup_id' => 'batchAccess', + 'language_id' => 'batchLanguage', + 'tag' => 'batchTag', + 'user_id' => 'batchUser', + ); + + /** + * Name of the form + * + * @var string + * @since 4.0.0 + */ + protected $formName = 'contact'; + + /** + * Batch change a linked user. + * + * @param integer $value The new value matching a User ID. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 2.5 + */ + protected function batchUser($value, $pks, $contexts) + { + foreach ($pks as $pk) { + if ($this->user->authorise('core.edit', $contexts[$pk])) { + $this->table->reset(); + $this->table->load($pk); + $this->table->user_id = (int) $value; + + if (!$this->table->store()) { + $this->setError($this->table->getError()); + + return false; + } + } else { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->published != -2) { + return false; + } + + return Factory::getUser()->authorise('core.delete', 'com_contact.category.' . (int) $record->catid); + } + + /** + * Method to test whether a record can have its state edited. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canEditState($record) + { + // Check against the category. + if (!empty($record->catid)) { + return Factory::getUser()->authorise('core.edit.state', 'com_contact.category.' . (int) $record->catid); + } + + // Default to component settings if category not known. + return parent::canEditState($record); + } + + /** + * Method to get the row form. + * + * @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 Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + Form::addFieldPath(JPATH_ADMINISTRATOR . '/components/com_users/models/fields'); + + // Get the form. + $form = $this->loadForm('com_contact.' . $this->formName, $this->formName, array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Modify the form based on access controls. + if (!$this->canEditState((object) $data)) { + // Disable fields for display. + $form->setFieldAttribute('featured', 'disabled', 'true'); + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('published', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('featured', 'filter', 'unset'); + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + // Don't allow to change the created_by user if not allowed to access com_users. + if (!Factory::getUser()->authorise('core.manage', 'com_users')) { + $form->setFieldAttribute('created_by', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + if ($item = parent::getItem($pk)) { + // Convert the metadata field to an array. + $registry = new Registry($item->metadata); + $item->metadata = $registry->toArray(); + } + + // Load associated contact items + $assoc = Associations::isEnabled(); + + if ($assoc) { + $item->associations = array(); + + if ($item->id != null) { + $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $item->id); + + foreach ($associations as $tag => $association) { + $item->associations[$tag] = $association->id; + } + } + } + + // Load item tags + if (!empty($item->id)) { + $item->tags = new TagsHelper(); + $item->tags->getTagIds($item->id, 'com_contact.contact'); + } + + return $item; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + $app = Factory::getApplication(); + + // Check the session for previously entered form data. + $data = $app->getUserState('com_contact.edit.contact.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Prime some default values. + if ($this->getState('contact.id') == 0) { + $data->set('catid', $app->input->get('catid', $app->getUserState('com_contact.contacts.filter.category_id'), 'int')); + } + } + + $this->preprocessData('com_contact.contact', $data); + + return $data; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 3.0 + */ + public function save($data) + { + $input = Factory::getApplication()->input; + + // Create new category, if needed. + $createCategory = true; + + // If category ID is provided, check if it's valid. + if (is_numeric($data['catid']) && $data['catid']) { + $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_contact'); + } + + // Save New Category + if ($createCategory && $this->canCreateCategory()) { + $category = [ + // Remove #new# prefix, if exists. + 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], + 'parent_id' => 1, + 'extension' => 'com_contact', + 'language' => $data['language'], + 'published' => 1, + ]; + + /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ + $categoryModel = Factory::getApplication()->bootComponent('com_categories') + ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); + + // Create new category. + if (!$categoryModel->save($category)) { + $this->setError($categoryModel->getError()); + + return false; + } + + // Get the Category ID. + $data['catid'] = $categoryModel->getState('category.id'); + } + + // Alter the name for save as copy + if ($input->get('task') == 'save2copy') { + $origTable = clone $this->getTable(); + $origTable->load($input->getInt('id')); + + if ($data['name'] == $origTable->name) { + list($name, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['name']); + $data['name'] = $name; + $data['alias'] = $alias; + } else { + if ($data['alias'] == $origTable->alias) { + $data['alias'] = ''; + } + } + + $data['published'] = 0; + } + + $links = array('linka', 'linkb', 'linkc', 'linkd', 'linke'); + + foreach ($links as $link) { + if (!empty($data['params'][$link])) { + $data['params'][$link] = PunycodeHelper::urlToPunycode($data['params'][$link]); + } + } + + return parent::save($data); + } + + /** + * Prepare and sanitise the table prior to saving. + * + * @param \Joomla\CMS\Table\Table $table The Table object + * + * @return void + * + * @since 1.6 + */ + protected function prepareTable($table) + { + $date = Factory::getDate()->toSql(); + + $table->name = htmlspecialchars_decode($table->name, ENT_QUOTES); + + $table->generateAlias(); + + if (empty($table->id)) { + // Set the values + $table->created = $date; + + // Set ordering to the last item if not set + if (empty($table->ordering)) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('MAX(ordering)') + ->from($db->quoteName('#__contact_details')); + $db->setQuery($query); + $max = $db->loadResult(); + + $table->ordering = $max + 1; + } + } else { + // Set the values + $table->modified = $date; + $table->modified_by = Factory::getUser()->id; + } + + // Increment the content version number. + $table->version++; + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param \Joomla\CMS\Table\Table $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + return [ + $this->getDatabase()->quoteName('catid') . ' = ' . (int) $table->catid, + ]; + } + + /** + * Preprocess the form. + * + * @param Form $form Form object. + * @param object $data Data object. + * @param string $group Group name. + * + * @return void + * + * @since 3.0.3 + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + if ($this->canCreateCategory()) { + $form->setFieldAttribute('catid', 'allowAdd', 'true'); + + // Add a prefix for categories created on the fly. + $form->setFieldAttribute('catid', 'customPrefix', '#new#'); + } + + // Association contact items + if (Associations::isEnabled()) { + $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); + + if (count($languages) > 1) { + $addform = new \SimpleXMLElement(''); + $fields = $addform->addChild('fields'); + $fields->addAttribute('name', 'associations'); + $fieldset = $fields->addChild('fieldset'); + $fieldset->addAttribute('name', 'item_associations'); + + foreach ($languages as $language) { + $field = $fieldset->addChild('field'); + $field->addAttribute('name', $language->lang_code); + $field->addAttribute('type', 'modal_contact'); + $field->addAttribute('language', $language->lang_code); + $field->addAttribute('label', $language->title); + $field->addAttribute('translate_label', 'false'); + $field->addAttribute('select', 'true'); + $field->addAttribute('new', 'true'); + $field->addAttribute('edit', 'true'); + $field->addAttribute('clear', 'true'); + $field->addAttribute('propagate', 'true'); + } + + $form->load($addform, false); + } + } + + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to toggle the featured setting of contacts. + * + * @param array $pks The ids of the items to toggle. + * @param integer $value The value to toggle to. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function featured($pks, $value = 0) + { + // Sanitize the ids. + $pks = ArrayHelper::toInteger((array) $pks); + + if (empty($pks)) { + $this->setError(Text::_('COM_CONTACT_NO_ITEM_SELECTED')); + + return false; + } + + $table = $this->getTable(); + + try { + $db = $this->getDatabase(); + + $query = $db->getQuery(true); + $query->update($db->quoteName('#__contact_details')); + $query->set($db->quoteName('featured') . ' = :featured'); + $query->whereIn($db->quoteName('id'), $pks); + $query->bind(':featured', $value, ParameterType::INTEGER); + + $db->setQuery($query); + + $db->execute(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + $table->reorder(); + + // Clean component's cache + $this->cleanCache(); + + return true; + } + + /** + * Is the user allowed to create an on the fly category? + * + * @return boolean + * + * @since 3.6.1 + */ + private function canCreateCategory() + { + return Factory::getUser()->authorise('core.create', 'com_contact'); + } } diff --git a/administrator/components/com_contact/src/Model/ContactsModel.php b/administrator/components/com_contact/src/Model/ContactsModel.php index f4adaf4746963..d49442ffe5c11 100644 --- a/administrator/components/com_contact/src/Model/ContactsModel.php +++ b/administrator/components/com_contact/src/Model/ContactsModel.php @@ -1,4 +1,5 @@ input->get('forcedLanguage', '', 'cmd'); - - // Adjust the context to support modal layouts. - if ($layout = $app->input->get('layout')) - { - $this->context .= '.' . $layout; - } - - // Adjust the context to support forced languages. - if ($forcedLanguage) - { - $this->context .= '.' . $forcedLanguage; - } - - // List state information. - parent::populateState($ordering, $direction); - - // Force a language. - if (!empty($forcedLanguage)) - { - $this->setState('filter.language', $forcedLanguage); - } - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . serialize($this->getState('filter.category_id')); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.language'); - $id .= ':' . serialize($this->getState('filter.tag')); - $id .= ':' . $this->getState('filter.level'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - - // Select the required fields from the table. - $query->select( - $db->quoteName( - explode( - ', ', - $this->getState( - 'list.select', - 'a.id, a.name, a.alias, a.checked_out, a.checked_out_time, a.catid, a.user_id' . - ', a.published, a.access, a.created, a.created_by, a.ordering, a.featured, a.language' . - ', a.publish_up, a.publish_down' - ) - ) - ) - ); - $query->from($db->quoteName('#__contact_details', 'a')); - - // Join over the users for the linked user. - $query->select( - array( - $db->quoteName('ul.name', 'linked_user'), - $db->quoteName('ul.email') - ) - ) - ->join( - 'LEFT', - $db->quoteName('#__users', 'ul') . ' ON ' . $db->quoteName('ul.id') . ' = ' . $db->quoteName('a.user_id') - ); - - // Join over the language - $query->select($db->quoteName('l.title', 'language_title')) - ->select($db->quoteName('l.image', 'language_image')) - ->join( - 'LEFT', - $db->quoteName('#__languages', 'l') . ' ON ' . $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language') - ); - - // Join over the users for the checked out user. - $query->select($db->quoteName('uc.name', 'editor')) - ->join( - 'LEFT', - $db->quoteName('#__users', 'uc') . ' ON ' . $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out') - ); - - // Join over the asset groups. - $query->select($db->quoteName('ag.title', 'access_level')) - ->join( - 'LEFT', - $db->quoteName('#__viewlevels', 'ag') . ' ON ' . $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access') - ); - - // Join over the categories. - $query->select($db->quoteName('c.title', 'category_title')) - ->join( - 'LEFT', - $db->quoteName('#__categories', 'c') . ' ON ' . $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid') - ); - - // Join over the associations. - if (Associations::isEnabled()) - { - $subQuery = $db->getQuery(true) - ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') - ->from($db->quoteName('#__associations', 'asso1')) - ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) - ->where( - [ - $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), - $db->quoteName('asso1.context') . ' = ' . $db->quote('com_contact.item'), - ] - ); - - $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); - } - - // Filter by featured. - $featured = (string) $this->getState('filter.featured'); - - if (in_array($featured, ['0','1'])) - { - $query->where($db->quoteName('a.featured') . ' = ' . (int) $featured); - } - - // Filter by access level. - if ($access = $this->getState('filter.access')) - { - $query->where($db->quoteName('a.access') . ' = :access'); - $query->bind(':access', $access, ParameterType::INTEGER); - } - - // Implement View Level Access - if (!$user->authorise('core.admin')) - { - $query->whereIn($db->quoteName('a.access'), $user->getAuthorisedViewLevels()); - } - - // Filter by published state - $published = (string) $this->getState('filter.published'); - - if (is_numeric($published)) - { - $query->where($db->quoteName('a.published') . ' = :published'); - $query->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->where('(' . $db->quoteName('a.published') . ' = 0 OR ' . $db->quoteName('a.published') . ' = 1)'); - } - - // Filter by search in name. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $search = substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $search, ParameterType::INTEGER); - } - else - { - $search = '%' . trim($search) . '%'; - $query->where( - '(' . $db->quoteName('a.name') . ' LIKE :name OR ' . $db->quoteName('a.alias') . ' LIKE :alias)' - ); - $query->bind(':name', $search); - $query->bind(':alias', $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('a.language') . ' = :language'); - $query->bind(':language', $language); - } - - // Filter by a single or group of tags. - $tag = $this->getState('filter.tag'); - - // Run simplified query when filtering by one tag. - if (\is_array($tag) && \count($tag) === 1) - { - $tag = $tag[0]; - } - - if ($tag && \is_array($tag)) - { - $tag = ArrayHelper::toInteger($tag); - - $subQuery = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('content_item_id')) - ->from($db->quoteName('#__contentitem_tag_map')) - ->where( - [ - $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', - $db->quoteName('type_alias') . ' = ' . $db->quote('com_contact.contact'), - ] - ); - - $query->join( - 'INNER', - '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ); - } - elseif ($tag = (int) $tag) - { - $query->join( - 'INNER', - $db->quoteName('#__contentitem_tag_map', 'tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ) - ->where( - [ - $db->quoteName('tagmap.tag_id') . ' = :tag', - $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_contact.contact'), - ] - ) - ->bind(':tag', $tag, ParameterType::INTEGER); - } - - // Filter by categories and by level - $categoryId = $this->getState('filter.category_id', array()); - $level = $this->getState('filter.level'); - - if (!is_array($categoryId)) - { - $categoryId = $categoryId ? array($categoryId) : array(); - } - - // Case: Using both categories filter and by level filter - if (count($categoryId)) - { - $categoryId = ArrayHelper::toInteger($categoryId); - $categoryTable = Table::getInstance('Category', 'JTable'); - $subCatItemsWhere = array(); - - // @todo: Convert to prepared statement - foreach ($categoryId as $filter_catid) - { - $categoryTable->load($filter_catid); - $subCatItemsWhere[] = '(' . - ($level ? 'c.level <= ' . ((int) $level + (int) $categoryTable->level - 1) . ' AND ' : '') . - 'c.lft >= ' . (int) $categoryTable->lft . ' AND ' . - 'c.rgt <= ' . (int) $categoryTable->rgt . ')'; - } - - $query->where('(' . implode(' OR ', $subCatItemsWhere) . ')'); - } - - // Case: Using only the by level filter - elseif ($level) - { - $query->where($db->quoteName('c.level') . ' <= :level'); - $query->bind(':level', $level, ParameterType::INTEGER); - } - - // Add the list ordering clause. - $orderCol = $this->state->get('list.ordering', 'a.name'); - $orderDirn = $this->state->get('list.direction', 'asc'); - - if ($orderCol == 'a.ordering' || $orderCol == 'category_title') - { - $orderCol = $db->quoteName('c.title') . ' ' . $orderDirn . ', ' . $db->quoteName('a.ordering'); - } - - $query->order($db->escape($orderCol . ' ' . $orderDirn)); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'alias', 'a.alias', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'catid', 'a.catid', 'category_id', 'category_title', + 'user_id', 'a.user_id', + 'published', 'a.published', + 'access', 'a.access', 'access_level', + 'created', 'a.created', + 'created_by', 'a.created_by', + 'ordering', 'a.ordering', + 'featured', 'a.featured', + 'language', 'a.language', 'language_title', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'ul.name', 'linked_user', + 'tag', + 'level', 'c.level', + ); + + if (Associations::isEnabled()) { + $config['filter_fields'][] = 'association'; + } + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.name', $direction = 'asc') + { + $app = Factory::getApplication(); + + $forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd'); + + // Adjust the context to support modal layouts. + if ($layout = $app->input->get('layout')) { + $this->context .= '.' . $layout; + } + + // Adjust the context to support forced languages. + if ($forcedLanguage) { + $this->context .= '.' . $forcedLanguage; + } + + // List state information. + parent::populateState($ordering, $direction); + + // Force a language. + if (!empty($forcedLanguage)) { + $this->setState('filter.language', $forcedLanguage); + } + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . serialize($this->getState('filter.category_id')); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.language'); + $id .= ':' . serialize($this->getState('filter.tag')); + $id .= ':' . $this->getState('filter.level'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + + // Select the required fields from the table. + $query->select( + $db->quoteName( + explode( + ', ', + $this->getState( + 'list.select', + 'a.id, a.name, a.alias, a.checked_out, a.checked_out_time, a.catid, a.user_id' . + ', a.published, a.access, a.created, a.created_by, a.ordering, a.featured, a.language' . + ', a.publish_up, a.publish_down' + ) + ) + ) + ); + $query->from($db->quoteName('#__contact_details', 'a')); + + // Join over the users for the linked user. + $query->select( + array( + $db->quoteName('ul.name', 'linked_user'), + $db->quoteName('ul.email') + ) + ) + ->join( + 'LEFT', + $db->quoteName('#__users', 'ul') . ' ON ' . $db->quoteName('ul.id') . ' = ' . $db->quoteName('a.user_id') + ); + + // Join over the language + $query->select($db->quoteName('l.title', 'language_title')) + ->select($db->quoteName('l.image', 'language_image')) + ->join( + 'LEFT', + $db->quoteName('#__languages', 'l') . ' ON ' . $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language') + ); + + // Join over the users for the checked out user. + $query->select($db->quoteName('uc.name', 'editor')) + ->join( + 'LEFT', + $db->quoteName('#__users', 'uc') . ' ON ' . $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out') + ); + + // Join over the asset groups. + $query->select($db->quoteName('ag.title', 'access_level')) + ->join( + 'LEFT', + $db->quoteName('#__viewlevels', 'ag') . ' ON ' . $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access') + ); + + // Join over the categories. + $query->select($db->quoteName('c.title', 'category_title')) + ->join( + 'LEFT', + $db->quoteName('#__categories', 'c') . ' ON ' . $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid') + ); + + // Join over the associations. + if (Associations::isEnabled()) { + $subQuery = $db->getQuery(true) + ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') + ->from($db->quoteName('#__associations', 'asso1')) + ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) + ->where( + [ + $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), + $db->quoteName('asso1.context') . ' = ' . $db->quote('com_contact.item'), + ] + ); + + $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); + } + + // Filter by featured. + $featured = (string) $this->getState('filter.featured'); + + if (in_array($featured, ['0','1'])) { + $query->where($db->quoteName('a.featured') . ' = ' . (int) $featured); + } + + // Filter by access level. + if ($access = $this->getState('filter.access')) { + $query->where($db->quoteName('a.access') . ' = :access'); + $query->bind(':access', $access, ParameterType::INTEGER); + } + + // Implement View Level Access + if (!$user->authorise('core.admin')) { + $query->whereIn($db->quoteName('a.access'), $user->getAuthorisedViewLevels()); + } + + // Filter by published state + $published = (string) $this->getState('filter.published'); + + if (is_numeric($published)) { + $query->where($db->quoteName('a.published') . ' = :published'); + $query->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->where('(' . $db->quoteName('a.published') . ' = 0 OR ' . $db->quoteName('a.published') . ' = 1)'); + } + + // Filter by search in name. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $search = substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $search, ParameterType::INTEGER); + } else { + $search = '%' . trim($search) . '%'; + $query->where( + '(' . $db->quoteName('a.name') . ' LIKE :name OR ' . $db->quoteName('a.alias') . ' LIKE :alias)' + ); + $query->bind(':name', $search); + $query->bind(':alias', $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('a.language') . ' = :language'); + $query->bind(':language', $language); + } + + // Filter by a single or group of tags. + $tag = $this->getState('filter.tag'); + + // Run simplified query when filtering by one tag. + if (\is_array($tag) && \count($tag) === 1) { + $tag = $tag[0]; + } + + if ($tag && \is_array($tag)) { + $tag = ArrayHelper::toInteger($tag); + + $subQuery = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('content_item_id')) + ->from($db->quoteName('#__contentitem_tag_map')) + ->where( + [ + $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', + $db->quoteName('type_alias') . ' = ' . $db->quote('com_contact.contact'), + ] + ); + + $query->join( + 'INNER', + '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ); + } elseif ($tag = (int) $tag) { + $query->join( + 'INNER', + $db->quoteName('#__contentitem_tag_map', 'tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ) + ->where( + [ + $db->quoteName('tagmap.tag_id') . ' = :tag', + $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_contact.contact'), + ] + ) + ->bind(':tag', $tag, ParameterType::INTEGER); + } + + // Filter by categories and by level + $categoryId = $this->getState('filter.category_id', array()); + $level = $this->getState('filter.level'); + + if (!is_array($categoryId)) { + $categoryId = $categoryId ? array($categoryId) : array(); + } + + // Case: Using both categories filter and by level filter + if (count($categoryId)) { + $categoryId = ArrayHelper::toInteger($categoryId); + $categoryTable = Table::getInstance('Category', 'JTable'); + $subCatItemsWhere = array(); + + // @todo: Convert to prepared statement + foreach ($categoryId as $filter_catid) { + $categoryTable->load($filter_catid); + $subCatItemsWhere[] = '(' . + ($level ? 'c.level <= ' . ((int) $level + (int) $categoryTable->level - 1) . ' AND ' : '') . + 'c.lft >= ' . (int) $categoryTable->lft . ' AND ' . + 'c.rgt <= ' . (int) $categoryTable->rgt . ')'; + } + + $query->where('(' . implode(' OR ', $subCatItemsWhere) . ')'); + } elseif ($level) { + // Case: Using only the by level filter + $query->where($db->quoteName('c.level') . ' <= :level'); + $query->bind(':level', $level, ParameterType::INTEGER); + } + + // Add the list ordering clause. + $orderCol = $this->state->get('list.ordering', 'a.name'); + $orderDirn = $this->state->get('list.direction', 'asc'); + + if ($orderCol == 'a.ordering' || $orderCol == 'category_title') { + $orderCol = $db->quoteName('c.title') . ' ' . $orderDirn . ', ' . $db->quoteName('a.ordering'); + } + + $query->order($db->escape($orderCol . ' ' . $orderDirn)); + + return $query; + } } diff --git a/administrator/components/com_contact/src/Service/HTML/AdministratorService.php b/administrator/components/com_contact/src/Service/HTML/AdministratorService.php index 7255f69339d97..beb84949e984a 100644 --- a/administrator/components/com_contact/src/Service/HTML/AdministratorService.php +++ b/administrator/components/com_contact/src/Service/HTML/AdministratorService.php @@ -1,4 +1,5 @@ $associated) - { - $associations[$tag] = (int) $associated->id; - } - - // Get the associated contact items - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('c.id'), - $db->quoteName('c.name', 'title'), - $db->quoteName('l.sef', 'lang_sef'), - $db->quoteName('lang_code'), - $db->quoteName('cat.title', 'category_title'), - $db->quoteName('l.image'), - $db->quoteName('l.title', 'language_title'), - ] - ) - ->from($db->quoteName('#__contact_details', 'c')) - ->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('cat.id') . ' = ' . $db->quoteName('c.catid')) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code')) - ->whereIn($db->quoteName('c.id'), array_values($associations)) - ->where($db->quoteName('c.id') . ' != :id') - ->bind(':id', $contactid, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $items = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500, $e); - } - - if ($items) - { - $languages = LanguageHelper::getContentLanguages(array(0, 1)); - $content_languages = array_column($languages, 'lang_code'); - - foreach ($items as &$item) - { - if (in_array($item->lang_code, $content_languages)) - { - $text = $item->lang_code; - $url = Route::_('index.php?option=com_contact&task=contact.edit&id=' . (int) $item->id); - $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
' - . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
' . Text::sprintf('JCATEGORY_SPRINTF', $item->category_title); - $classes = 'badge bg-secondary'; - - $item->link = '' . $text . '' - . ''; - } - else - { - // Display warning if Content Language is trashed or deleted - Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); - } - } - } - - $html = LayoutHelper::render('joomla.content.associations', $items); - } - - return $html; - } - - /** - * Show the featured/not-featured icon. - * - * @param integer $value The featured value. - * @param integer $i Id of the item. - * @param boolean $canChange Whether the value can be changed or not. - * - * @return string The anchor tag to toggle featured/unfeatured contacts. - * - * @since 1.6 - */ - public function featured($value, $i, $canChange = true) - { - // Array of image, task, title, action - $states = array( - 0 => array('unfeatured', 'contacts.featured', 'COM_CONTACT_UNFEATURED', 'JGLOBAL_ITEM_FEATURE'), - 1 => array('featured', 'contacts.unfeatured', 'JFEATURED', 'JGLOBAL_ITEM_UNFEATURE'), - ); - $state = ArrayHelper::getValue($states, (int) $value, $states[1]); - $icon = $state[0] === 'featured' ? 'star featured' : 'circle'; - $onclick = 'onclick="return Joomla.listItemTask(\'cb' . $i . '\',\'' . $state[1] . '\')"'; - $tooltipText = Text::_($state[3]); - - if (!$canChange) - { - $onclick = 'disabled'; - $tooltipText = Text::_($state[2]); - } - - $html = '' - . ''; - - return $html; - } + /** + * Get the associated language flags + * + * @param integer $contactid The item id to search associations + * + * @return string The language HTML + * + * @throws \Exception + */ + public function association($contactid) + { + // Defaults + $html = ''; + + // Get the associations + if ($associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $contactid)) { + foreach ($associations as $tag => $associated) { + $associations[$tag] = (int) $associated->id; + } + + // Get the associated contact items + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('c.id'), + $db->quoteName('c.name', 'title'), + $db->quoteName('l.sef', 'lang_sef'), + $db->quoteName('lang_code'), + $db->quoteName('cat.title', 'category_title'), + $db->quoteName('l.image'), + $db->quoteName('l.title', 'language_title'), + ] + ) + ->from($db->quoteName('#__contact_details', 'c')) + ->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('cat.id') . ' = ' . $db->quoteName('c.catid')) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code')) + ->whereIn($db->quoteName('c.id'), array_values($associations)) + ->where($db->quoteName('c.id') . ' != :id') + ->bind(':id', $contactid, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $items = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500, $e); + } + + if ($items) { + $languages = LanguageHelper::getContentLanguages(array(0, 1)); + $content_languages = array_column($languages, 'lang_code'); + + foreach ($items as &$item) { + if (in_array($item->lang_code, $content_languages)) { + $text = $item->lang_code; + $url = Route::_('index.php?option=com_contact&task=contact.edit&id=' . (int) $item->id); + $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
' + . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
' . Text::sprintf('JCATEGORY_SPRINTF', $item->category_title); + $classes = 'badge bg-secondary'; + + $item->link = '' . $text . '' + . ''; + } else { + // Display warning if Content Language is trashed or deleted + Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); + } + } + } + + $html = LayoutHelper::render('joomla.content.associations', $items); + } + + return $html; + } + + /** + * Show the featured/not-featured icon. + * + * @param integer $value The featured value. + * @param integer $i Id of the item. + * @param boolean $canChange Whether the value can be changed or not. + * + * @return string The anchor tag to toggle featured/unfeatured contacts. + * + * @since 1.6 + */ + public function featured($value, $i, $canChange = true) + { + // Array of image, task, title, action + $states = array( + 0 => array('unfeatured', 'contacts.featured', 'COM_CONTACT_UNFEATURED', 'JGLOBAL_ITEM_FEATURE'), + 1 => array('featured', 'contacts.unfeatured', 'JFEATURED', 'JGLOBAL_ITEM_UNFEATURE'), + ); + $state = ArrayHelper::getValue($states, (int) $value, $states[1]); + $icon = $state[0] === 'featured' ? 'star featured' : 'circle'; + $onclick = 'onclick="return Joomla.listItemTask(\'cb' . $i . '\',\'' . $state[1] . '\')"'; + $tooltipText = Text::_($state[3]); + + if (!$canChange) { + $onclick = 'disabled'; + $tooltipText = Text::_($state[2]); + } + + $html = '' + . ''; + + return $html; + } } diff --git a/administrator/components/com_contact/src/Service/HTML/Icon.php b/administrator/components/com_contact/src/Service/HTML/Icon.php index 07bb27759e342..a3ebe1a55b1cd 100644 --- a/administrator/components/com_contact/src/Service/HTML/Icon.php +++ b/administrator/components/com_contact/src/Service/HTML/Icon.php @@ -1,4 +1,5 @@ userFactory = $userFactory; - } - - /** - * Method to generate a link to the create item page for the given category - * - * @param object $category The category information - * @param Registry $params The item parameters - * @param array $attribs Optional attributes for the link - * - * @return string The HTML markup for the create item link - * - * @since 4.0.0 - */ - public function create($category, $params, $attribs = array()) - { - $uri = Uri::getInstance(); - - $url = 'index.php?option=com_contact&task=contact.add&return=' . base64_encode($uri) . '&id=0&catid=' . $category->id; - - $text = ''; - - if ($params->get('show_icons')) - { - $text .= ''; - } - - $text .= Text::_('COM_CONTACT_NEW_CONTACT'); - - // Add the button classes to the attribs array - if (isset($attribs['class'])) - { - $attribs['class'] .= ' btn btn-primary'; - } - else - { - $attribs['class'] = 'btn btn-primary'; - } - - $button = HTMLHelper::_('link', Route::_($url), $text, $attribs); - - return $button; - } - - /** - * Display an edit icon for the contact. - * - * This icon will not display in a popup window, nor if the contact is trashed. - * Edit access checks must be performed in the calling code. - * - * @param object $contact The contact information - * @param Registry $params The item parameters - * @param array $attribs Optional attributes for the link - * @param boolean $legacy True to use legacy images, false to use icomoon based graphic - * - * @return string The HTML for the contact edit icon. - * - * @since 4.0.0 - */ - public function edit($contact, $params, $attribs = array(), $legacy = false) - { - $user = Factory::getUser(); - $uri = Uri::getInstance(); - - // Ignore if in a popup window. - if ($params && $params->get('popup')) - { - return ''; - } - - // Ignore if the state is negative (trashed). - if ($contact->published < 0) - { - return ''; - } - - // Show checked_out icon if the contact is checked out by a different user - if (property_exists($contact, 'checked_out') - && property_exists($contact, 'checked_out_time') - && !is_null($contact->checked_out) - && $contact->checked_out !== $user->get('id')) - { - $checkoutUser = $this->userFactory->loadUserById($contact->checked_out); - $date = HTMLHelper::_('date', $contact->checked_out_time); - $tooltip = Text::sprintf('COM_CONTACT_CHECKED_OUT_BY', $checkoutUser->name) - . '
' . $date; - - $text = LayoutHelper::render('joomla.content.icons.edit_lock', array('contact' => $contact, 'tooltip' => $tooltip, 'legacy' => $legacy)); - - $attribs['aria-describedby'] = 'editcontact-' . (int) $contact->id; - $output = HTMLHelper::_('link', '#', $text, $attribs); - - return $output; - } - - $contactUrl = RouteHelper::getContactRoute($contact->slug, $contact->catid, $contact->language); - $url = $contactUrl . '&task=contact.edit&id=' . $contact->id . '&return=' . base64_encode($uri); - - if ((int) $contact->published === 0) - { - $tooltip = Text::_('COM_CONTACT_EDIT_UNPUBLISHED_CONTACT'); - } - else - { - $tooltip = Text::_('COM_CONTACT_EDIT_PUBLISHED_CONTACT'); - } - - $nowDate = strtotime(Factory::getDate()); - $icon = $contact->published ? 'edit' : 'eye-slash'; - - if (($contact->publish_up !== null && strtotime($contact->publish_up) > $nowDate) - || ($contact->publish_down !== null && strtotime($contact->publish_down) < $nowDate)) - { - $icon = 'eye-slash'; - } - - $aria_described = 'editcontact-' . (int) $contact->id; - - $text = ''; - $text .= Text::_('JGLOBAL_EDIT'); - $text .= ''; - - $attribs['aria-describedby'] = $aria_described; - $output = HTMLHelper::_('link', Route::_($url), $text, $attribs); - - return $output; - } + /** + * The user factory + * + * @var UserFactoryInterface + * + * @since 4.2.0 + */ + private $userFactory; + + /** + * Service constructor + * + * @param UserFactoryInterface $userFactory The userFactory + * + * @since 4.0.0 + */ + public function __construct(UserFactoryInterface $userFactory) + { + $this->userFactory = $userFactory; + } + + /** + * Method to generate a link to the create item page for the given category + * + * @param object $category The category information + * @param Registry $params The item parameters + * @param array $attribs Optional attributes for the link + * + * @return string The HTML markup for the create item link + * + * @since 4.0.0 + */ + public function create($category, $params, $attribs = array()) + { + $uri = Uri::getInstance(); + + $url = 'index.php?option=com_contact&task=contact.add&return=' . base64_encode($uri) . '&id=0&catid=' . $category->id; + + $text = ''; + + if ($params->get('show_icons')) { + $text .= ''; + } + + $text .= Text::_('COM_CONTACT_NEW_CONTACT'); + + // Add the button classes to the attribs array + if (isset($attribs['class'])) { + $attribs['class'] .= ' btn btn-primary'; + } else { + $attribs['class'] = 'btn btn-primary'; + } + + $button = HTMLHelper::_('link', Route::_($url), $text, $attribs); + + return $button; + } + + /** + * Display an edit icon for the contact. + * + * This icon will not display in a popup window, nor if the contact is trashed. + * Edit access checks must be performed in the calling code. + * + * @param object $contact The contact information + * @param Registry $params The item parameters + * @param array $attribs Optional attributes for the link + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML for the contact edit icon. + * + * @since 4.0.0 + */ + public function edit($contact, $params, $attribs = array(), $legacy = false) + { + $user = Factory::getUser(); + $uri = Uri::getInstance(); + + // Ignore if in a popup window. + if ($params && $params->get('popup')) { + return ''; + } + + // Ignore if the state is negative (trashed). + if ($contact->published < 0) { + return ''; + } + + // Show checked_out icon if the contact is checked out by a different user + if ( + property_exists($contact, 'checked_out') + && property_exists($contact, 'checked_out_time') + && !is_null($contact->checked_out) + && $contact->checked_out !== $user->get('id') + ) { + $checkoutUser = $this->userFactory->loadUserById($contact->checked_out); + $date = HTMLHelper::_('date', $contact->checked_out_time); + $tooltip = Text::sprintf('COM_CONTACT_CHECKED_OUT_BY', $checkoutUser->name) + . '
' . $date; + + $text = LayoutHelper::render('joomla.content.icons.edit_lock', array('contact' => $contact, 'tooltip' => $tooltip, 'legacy' => $legacy)); + + $attribs['aria-describedby'] = 'editcontact-' . (int) $contact->id; + $output = HTMLHelper::_('link', '#', $text, $attribs); + + return $output; + } + + $contactUrl = RouteHelper::getContactRoute($contact->slug, $contact->catid, $contact->language); + $url = $contactUrl . '&task=contact.edit&id=' . $contact->id . '&return=' . base64_encode($uri); + + if ((int) $contact->published === 0) { + $tooltip = Text::_('COM_CONTACT_EDIT_UNPUBLISHED_CONTACT'); + } else { + $tooltip = Text::_('COM_CONTACT_EDIT_PUBLISHED_CONTACT'); + } + + $nowDate = strtotime(Factory::getDate()); + $icon = $contact->published ? 'edit' : 'eye-slash'; + + if ( + ($contact->publish_up !== null && strtotime($contact->publish_up) > $nowDate) + || ($contact->publish_down !== null && strtotime($contact->publish_down) < $nowDate) + ) { + $icon = 'eye-slash'; + } + + $aria_described = 'editcontact-' . (int) $contact->id; + + $text = ''; + $text .= Text::_('JGLOBAL_EDIT'); + $text .= ''; + + $attribs['aria-describedby'] = $aria_described; + $output = HTMLHelper::_('link', Route::_($url), $text, $attribs); + + return $output; + } } diff --git a/administrator/components/com_contact/src/Table/ContactTable.php b/administrator/components/com_contact/src/Table/ContactTable.php index 4748f0ed52217..697b9fd765cf9 100644 --- a/administrator/components/com_contact/src/Table/ContactTable.php +++ b/administrator/components/com_contact/src/Table/ContactTable.php @@ -1,4 +1,5 @@ typeAlias = 'com_contact.contact'; - - parent::__construct('#__contact_details', 'id', $db); - - $this->setColumnAlias('title', 'name'); - } - - /** - * Stores a contact. - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return boolean True on success, false on failure. - * - * @since 1.6 - */ - public function store($updateNulls = true) - { - $date = Factory::getDate()->toSql(); - $userId = Factory::getUser()->id; - - // Set created date if not set. - if (!(int) $this->created) - { - $this->created = $date; - } - - if ($this->id) - { - // Existing item - $this->modified_by = $userId; - $this->modified = $date; - } - else - { - // Field created_by field can be set by the user, so we don't touch it if it's set. - if (empty($this->created_by)) - { - $this->created_by = $userId; - } - - if (!(int) $this->modified) - { - $this->modified = $date; - } - - if (empty($this->modified_by)) - { - $this->modified_by = $userId; - } - } - - // Store utf8 email as punycode - if ($this->email_to !== null) - { - $this->email_to = PunycodeHelper::emailToPunycode($this->email_to); - } - - // Convert IDN urls to punycode - if ($this->webpage !== null) - { - $this->webpage = PunycodeHelper::urlToPunycode($this->webpage); - } - - // Verify that the alias is unique - $table = Table::getInstance('ContactTable', __NAMESPACE__ . '\\', array('dbo' => $this->getDbo())); - - if ($table->load(array('alias' => $this->alias, 'catid' => $this->catid)) && ($table->id != $this->id || $this->id == 0)) - { - $this->setError(Text::_('COM_CONTACT_ERROR_UNIQUE_ALIAS')); - - return false; - } - - return parent::store($updateNulls); - } - - /** - * Overloaded check function - * - * @return boolean True on success, false on failure - * - * @see \JTable::check - * @since 1.5 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - $this->default_con = (int) $this->default_con; - - if ($this->webpage !== null && InputFilter::checkAttribute(array('href', $this->webpage))) - { - $this->setError(Text::_('COM_CONTACT_WARNING_PROVIDE_VALID_URL')); - - return false; - } - - // Check for valid name - if (trim($this->name) == '') - { - $this->setError(Text::_('COM_CONTACT_WARNING_PROVIDE_VALID_NAME')); - - return false; - } - - // Generate a valid alias - $this->generateAlias(); - - // Check for a valid category. - if (!$this->catid = (int) $this->catid) - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_CATEGORY_REQUIRED')); - - return false; - } - - // Sanity check for user_id - if (!$this->user_id) - { - $this->user_id = 0; - } - - // Check the publish down date is not earlier than publish up. - if ((int) $this->publish_down > 0 && $this->publish_down < $this->publish_up) - { - $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); - - return false; - } - - if (!$this->id) - { - // Hits must be zero on a new item - $this->hits = 0; - } - - // Clean up description -- eliminate quotes and <> brackets - if (!empty($this->metadesc)) - { - // Only process if not empty - $badCharacters = array("\"", '<', '>'); - $this->metadesc = StringHelper::str_ireplace($badCharacters, '', $this->metadesc); - } - else - { - $this->metadesc = ''; - } - - if (empty($this->params)) - { - $this->params = '{}'; - } - - if (empty($this->metadata)) - { - $this->metadata = '{}'; - } - - // Set publish_up, publish_down to null if not set - if (!$this->publish_up) - { - $this->publish_up = null; - } - - if (!$this->publish_down) - { - $this->publish_down = null; - } - - if (!$this->modified) - { - $this->modified = $this->created; - } - - if (empty($this->modified_by)) - { - $this->modified_by = $this->created_by; - } - - return true; - } - - /** - * Generate a valid alias from title / date. - * Remains public to be able to check for duplicated alias before saving - * - * @return string - */ - public function generateAlias() - { - if (empty($this->alias)) - { - $this->alias = $this->name; - } - - $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); - - if (trim(str_replace('-', '', $this->alias)) == '') - { - $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); - } - - return $this->alias; - } - - - /** - * Get the type alias for the history table - * - * @return string The alias as described above - * - * @since 4.0.0 - */ - public function getTypeAlias() - { - return $this->typeAlias; - } + use TaggableTableTrait; + + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Ensure the params and metadata in json encoded in the bind method + * + * @var array + * @since 3.3 + */ + protected $_jsonEncode = array('params', 'metadata'); + + /** + * Constructor + * + * @param DatabaseDriver $db Database connector object + * + * @since 1.0 + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = 'com_contact.contact'; + + parent::__construct('#__contact_details', 'id', $db); + + $this->setColumnAlias('title', 'name'); + } + + /** + * Stores a contact. + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return boolean True on success, false on failure. + * + * @since 1.6 + */ + public function store($updateNulls = true) + { + $date = Factory::getDate()->toSql(); + $userId = Factory::getUser()->id; + + // Set created date if not set. + if (!(int) $this->created) { + $this->created = $date; + } + + if ($this->id) { + // Existing item + $this->modified_by = $userId; + $this->modified = $date; + } else { + // Field created_by field can be set by the user, so we don't touch it if it's set. + if (empty($this->created_by)) { + $this->created_by = $userId; + } + + if (!(int) $this->modified) { + $this->modified = $date; + } + + if (empty($this->modified_by)) { + $this->modified_by = $userId; + } + } + + // Store utf8 email as punycode + if ($this->email_to !== null) { + $this->email_to = PunycodeHelper::emailToPunycode($this->email_to); + } + + // Convert IDN urls to punycode + if ($this->webpage !== null) { + $this->webpage = PunycodeHelper::urlToPunycode($this->webpage); + } + + // Verify that the alias is unique + $table = Table::getInstance('ContactTable', __NAMESPACE__ . '\\', array('dbo' => $this->getDbo())); + + if ($table->load(array('alias' => $this->alias, 'catid' => $this->catid)) && ($table->id != $this->id || $this->id == 0)) { + $this->setError(Text::_('COM_CONTACT_ERROR_UNIQUE_ALIAS')); + + return false; + } + + return parent::store($updateNulls); + } + + /** + * Overloaded check function + * + * @return boolean True on success, false on failure + * + * @see \JTable::check + * @since 1.5 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + $this->default_con = (int) $this->default_con; + + if ($this->webpage !== null && InputFilter::checkAttribute(array('href', $this->webpage))) { + $this->setError(Text::_('COM_CONTACT_WARNING_PROVIDE_VALID_URL')); + + return false; + } + + // Check for valid name + if (trim($this->name) == '') { + $this->setError(Text::_('COM_CONTACT_WARNING_PROVIDE_VALID_NAME')); + + return false; + } + + // Generate a valid alias + $this->generateAlias(); + + // Check for a valid category. + if (!$this->catid = (int) $this->catid) { + $this->setError(Text::_('JLIB_DATABASE_ERROR_CATEGORY_REQUIRED')); + + return false; + } + + // Sanity check for user_id + if (!$this->user_id) { + $this->user_id = 0; + } + + // Check the publish down date is not earlier than publish up. + if ((int) $this->publish_down > 0 && $this->publish_down < $this->publish_up) { + $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); + + return false; + } + + if (!$this->id) { + // Hits must be zero on a new item + $this->hits = 0; + } + + // Clean up description -- eliminate quotes and <> brackets + if (!empty($this->metadesc)) { + // Only process if not empty + $badCharacters = array("\"", '<', '>'); + $this->metadesc = StringHelper::str_ireplace($badCharacters, '', $this->metadesc); + } else { + $this->metadesc = ''; + } + + if (empty($this->params)) { + $this->params = '{}'; + } + + if (empty($this->metadata)) { + $this->metadata = '{}'; + } + + // Set publish_up, publish_down to null if not set + if (!$this->publish_up) { + $this->publish_up = null; + } + + if (!$this->publish_down) { + $this->publish_down = null; + } + + if (!$this->modified) { + $this->modified = $this->created; + } + + if (empty($this->modified_by)) { + $this->modified_by = $this->created_by; + } + + return true; + } + + /** + * Generate a valid alias from title / date. + * Remains public to be able to check for duplicated alias before saving + * + * @return string + */ + public function generateAlias() + { + if (empty($this->alias)) { + $this->alias = $this->name; + } + + $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); + + if (trim(str_replace('-', '', $this->alias)) == '') { + $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); + } + + return $this->alias; + } + + + /** + * Get the type alias for the history table + * + * @return string The alias as described above + * + * @since 4.0.0 + */ + public function getTypeAlias() + { + return $this->typeAlias; + } } diff --git a/administrator/components/com_contact/src/View/Contact/HtmlView.php b/administrator/components/com_contact/src/View/Contact/HtmlView.php index c3b55bb0fa0a9..15e0f15dc22ee 100644 --- a/administrator/components/com_contact/src/View/Contact/HtmlView.php +++ b/administrator/components/com_contact/src/View/Contact/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // If we are forcing a language in modal (used for associations). - if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) - { - // Set the language field to the forcedLanguage and disable changing it. - $this->form->setValue('language', null, $forcedLanguage); - $this->form->setFieldAttribute('language', 'readonly', 'true'); - - // Only allow to select categories with All language or with the forced language. - $this->form->setFieldAttribute('catid', 'language', '*,' . $forcedLanguage); - - // Only allow to select tags with All language or with the forced language. - $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = $this->getCurrentUser(); - $userId = $user->id; - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); - - // Since we don't track these assets at the item level, use the category id. - $canDo = ContentHelper::getActions('com_contact', 'category', $this->item->catid); - - $toolbar = Toolbar::getInstance(); - - ToolbarHelper::title($isNew ? Text::_('COM_CONTACT_MANAGER_CONTACT_NEW') : Text::_('COM_CONTACT_MANAGER_CONTACT_EDIT'), 'address-book contact'); - - // Build the actions for new and existing records. - if ($isNew) - { - // For new records, check the create permission. - if (count($user->getAuthorisedCategories('com_contact', 'core.create')) > 0) - { - ToolbarHelper::apply('contact.apply'); - - $saveGroup = $toolbar->dropdownButton('save-group'); - - $saveGroup->configure( - function (Toolbar $childBar) use ($user) - { - $childBar->save('contact.save'); - - if ($user->authorise('core.create', 'com_menus.menu')) - { - $childBar->save('contact.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); - } - - $childBar->save2new('contact.save2new'); - } - ); - } - - ToolbarHelper::cancel('contact.cancel'); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); - - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable) - { - $toolbar->apply('contact.apply'); - } - - $saveGroup = $toolbar->dropdownButton('save-group'); - - $saveGroup->configure( - function (Toolbar $childBar) use ($checkedOut, $itemEditable, $canDo, $user) - { - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable) - { - $childBar->save('contact.save'); - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $childBar->save2new('contact.save2new'); - } - } - - // If checked out, we can still save2menu - if ($user->authorise('core.create', 'com_menus.menu')) - { - $childBar->save('contact.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); - } - - // If checked out, we can still save - if ($canDo->get('core.create')) - { - $childBar->save2copy('contact.save2copy'); - } - } - ); - - ToolbarHelper::cancel('contact.cancel', 'JTOOLBAR_CLOSE'); - - if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $itemEditable) - { - ToolbarHelper::versions('com_contact.contact', $this->item->id); - } - - if (Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) - { - ToolbarHelper::custom('contact.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); - } - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Contacts:_New_or_Edit'); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + // Initialise variables. + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // If we are forcing a language in modal (used for associations). + if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) { + // Set the language field to the forcedLanguage and disable changing it. + $this->form->setValue('language', null, $forcedLanguage); + $this->form->setFieldAttribute('language', 'readonly', 'true'); + + // Only allow to select categories with All language or with the forced language. + $this->form->setFieldAttribute('catid', 'language', '*,' . $forcedLanguage); + + // Only allow to select tags with All language or with the forced language. + $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $userId = $user->id; + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); + + // Since we don't track these assets at the item level, use the category id. + $canDo = ContentHelper::getActions('com_contact', 'category', $this->item->catid); + + $toolbar = Toolbar::getInstance(); + + ToolbarHelper::title($isNew ? Text::_('COM_CONTACT_MANAGER_CONTACT_NEW') : Text::_('COM_CONTACT_MANAGER_CONTACT_EDIT'), 'address-book contact'); + + // Build the actions for new and existing records. + if ($isNew) { + // For new records, check the create permission. + if (count($user->getAuthorisedCategories('com_contact', 'core.create')) > 0) { + ToolbarHelper::apply('contact.apply'); + + $saveGroup = $toolbar->dropdownButton('save-group'); + + $saveGroup->configure( + function (Toolbar $childBar) use ($user) { + $childBar->save('contact.save'); + + if ($user->authorise('core.create', 'com_menus.menu')) { + $childBar->save('contact.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); + } + + $childBar->save2new('contact.save2new'); + } + ); + } + + ToolbarHelper::cancel('contact.cancel'); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); + + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable) { + $toolbar->apply('contact.apply'); + } + + $saveGroup = $toolbar->dropdownButton('save-group'); + + $saveGroup->configure( + function (Toolbar $childBar) use ($checkedOut, $itemEditable, $canDo, $user) { + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable) { + $childBar->save('contact.save'); + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $childBar->save2new('contact.save2new'); + } + } + + // If checked out, we can still save2menu + if ($user->authorise('core.create', 'com_menus.menu')) { + $childBar->save('contact.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); + } + + // If checked out, we can still save + if ($canDo->get('core.create')) { + $childBar->save2copy('contact.save2copy'); + } + } + ); + + ToolbarHelper::cancel('contact.cancel', 'JTOOLBAR_CLOSE'); + + if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $itemEditable) { + ToolbarHelper::versions('com_contact.contact', $this->item->id); + } + + if (Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) { + ToolbarHelper::custom('contact.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); + } + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Contacts:_New_or_Edit'); + } } diff --git a/administrator/components/com_contact/src/View/Contacts/HtmlView.php b/administrator/components/com_contact/src/View/Contacts/HtmlView.php index a03a9a060d153..a7c190246e421 100644 --- a/administrator/components/com_contact/src/View/Contacts/HtmlView.php +++ b/administrator/components/com_contact/src/View/Contacts/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Preprocess the list of items to find ordering divisions. - // @todo: Complete the ordering stuff with nested sets - foreach ($this->items as &$item) - { - $item->order_up = true; - $item->order_dn = true; - } - - // We don't need toolbar in the modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - else - { - // In article associations modal we need to remove language filter if forcing a language. - // We also need to change the category filter to show show categories with All or the forced language. - if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) - { - // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. - $languageXml = new \SimpleXMLElement(''); - $this->filterForm->setField($languageXml, 'filter', true); - - // Also, unset the active language filter so the search tools is not open by default with this filter. - unset($this->activeFilters['language']); - - // One last changes needed is to change the category filter to just show categories with All language or with the forced language. - $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); - } - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_contact', 'category', $this->state->get('filter.category_id')); - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_CONTACT_MANAGER_CONTACTS'), 'address-book contact'); - - if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_contact', 'core.create')) > 0) - { - $toolbar->addNew('contact.add'); - } - - if (!$this->isEmptyState && $canDo->get('core.edit.state')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('contacts.publish')->listCheck(true); - - $childBar->unpublish('contacts.unpublish')->listCheck(true); - - $childBar->standardButton('featured') - ->text('JFEATURE') - ->task('contacts.featured') - ->listCheck(true); - $childBar->standardButton('unfeatured') - ->text('JUNFEATURE') - ->task('contacts.unfeatured') - ->listCheck(true); - - $childBar->archive('contacts.archive')->listCheck(true); - - if ($user->authorise('core.admin')) - { - $childBar->checkin('contacts.checkin')->listCheck(true); - } - - if ($this->state->get('filter.published') != -2) - { - $childBar->trash('contacts.trash')->listCheck(true); - } - - // Add a batch button - if ($user->authorise('core.create', 'com_contact') - && $user->authorise('core.edit', 'com_contact') - && $user->authorise('core.edit.state', 'com_contact')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('contacts.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($user->authorise('core.admin', 'com_contact') || $user->authorise('core.options', 'com_contact')) - { - $toolbar->preferences('com_contact'); - } - - $toolbar->help('Contacts'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Preprocess the list of items to find ordering divisions. + // @todo: Complete the ordering stuff with nested sets + foreach ($this->items as &$item) { + $item->order_up = true; + $item->order_dn = true; + } + + // We don't need toolbar in the modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } else { + // In article associations modal we need to remove language filter if forcing a language. + // We also need to change the category filter to show show categories with All or the forced language. + if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) { + // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. + $languageXml = new \SimpleXMLElement(''); + $this->filterForm->setField($languageXml, 'filter', true); + + // Also, unset the active language filter so the search tools is not open by default with this filter. + unset($this->activeFilters['language']); + + // One last changes needed is to change the category filter to just show categories with All language or with the forced language. + $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); + } + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_contact', 'category', $this->state->get('filter.category_id')); + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_CONTACT_MANAGER_CONTACTS'), 'address-book contact'); + + if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_contact', 'core.create')) > 0) { + $toolbar->addNew('contact.add'); + } + + if (!$this->isEmptyState && $canDo->get('core.edit.state')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('contacts.publish')->listCheck(true); + + $childBar->unpublish('contacts.unpublish')->listCheck(true); + + $childBar->standardButton('featured') + ->text('JFEATURE') + ->task('contacts.featured') + ->listCheck(true); + $childBar->standardButton('unfeatured') + ->text('JUNFEATURE') + ->task('contacts.unfeatured') + ->listCheck(true); + + $childBar->archive('contacts.archive')->listCheck(true); + + if ($user->authorise('core.admin')) { + $childBar->checkin('contacts.checkin')->listCheck(true); + } + + if ($this->state->get('filter.published') != -2) { + $childBar->trash('contacts.trash')->listCheck(true); + } + + // Add a batch button + if ( + $user->authorise('core.create', 'com_contact') + && $user->authorise('core.edit', 'com_contact') + && $user->authorise('core.edit.state', 'com_contact') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('contacts.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($user->authorise('core.admin', 'com_contact') || $user->authorise('core.options', 'com_contact')) { + $toolbar->preferences('com_contact'); + } + + $toolbar->help('Contacts'); + } } diff --git a/administrator/components/com_contact/tmpl/contact/edit.php b/administrator/components/com_contact/tmpl/contact/edit.php index 61f2b057ff60b..6798cc6e1cc2d 100644 --- a/administrator/components/com_contact/tmpl/contact/edit.php +++ b/administrator/components/com_contact/tmpl/contact/edit.php @@ -20,7 +20,7 @@ /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $app = Factory::getApplication(); $input = $app->input; @@ -39,96 +39,96 @@ - - -
- 'details', 'recall' => true, 'breakpoint' => 768)); ?> - - item->id) ? Text::_('COM_CONTACT_NEW_CONTACT') : Text::_('COM_CONTACT_EDIT_CONTACT')); ?> -
-
-
-
- form->renderField('user_id'); ?> - form->renderField('image'); ?> - form->renderField('con_position'); ?> - form->renderField('email_to'); ?> - form->renderField('address'); ?> - form->renderField('suburb'); ?> - form->renderField('state'); ?> - form->renderField('postcode'); ?> - form->renderField('country'); ?> -
-
- form->renderField('telephone'); ?> - form->renderField('mobile'); ?> - form->renderField('fax'); ?> - form->renderField('webpage'); ?> - form->renderField('sortname1'); ?> - form->renderField('sortname2'); ?> - form->renderField('sortname3'); ?> -
-
-
-
- -
-
- - - -
-
-
- form->getField('misc')->title; ?> -
- form->getLabel('misc'); ?> - form->getInput('misc'); ?> -
-
-
-
- - - - - -
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
- - - - -
- -
- -
-
- - - - - - -
- - - + + +
+ 'details', 'recall' => true, 'breakpoint' => 768)); ?> + + item->id) ? Text::_('COM_CONTACT_NEW_CONTACT') : Text::_('COM_CONTACT_EDIT_CONTACT')); ?> +
+
+
+
+ form->renderField('user_id'); ?> + form->renderField('image'); ?> + form->renderField('con_position'); ?> + form->renderField('email_to'); ?> + form->renderField('address'); ?> + form->renderField('suburb'); ?> + form->renderField('state'); ?> + form->renderField('postcode'); ?> + form->renderField('country'); ?> +
+
+ form->renderField('telephone'); ?> + form->renderField('mobile'); ?> + form->renderField('fax'); ?> + form->renderField('webpage'); ?> + form->renderField('sortname1'); ?> + form->renderField('sortname2'); ?> + form->renderField('sortname3'); ?> +
+
+
+
+ +
+
+ + + +
+
+
+ form->getField('misc')->title; ?> +
+ form->getLabel('misc'); ?> + form->getInput('misc'); ?> +
+
+
+
+ + + + + +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+ + + + +
+ +
+ +
+
+ + + + + + +
+ + + diff --git a/administrator/components/com_contact/tmpl/contact/modal.php b/administrator/components/com_contact/tmpl/contact/modal.php index cf70ffd379321..04740bf4c752d 100644 --- a/administrator/components/com_contact/tmpl/contact/modal.php +++ b/administrator/components/com_contact/tmpl/contact/modal.php @@ -1,4 +1,5 @@
- setLayout('edit'); ?> - loadTemplate(); ?> + setLayout('edit'); ?> + loadTemplate(); ?>
diff --git a/administrator/components/com_contact/tmpl/contacts/default.php b/administrator/components/com_contact/tmpl/contacts/default.php index e4f20893ee730..cae745e06d33d 100644 --- a/administrator/components/com_contact/tmpl/contacts/default.php +++ b/administrator/components/com_contact/tmpl/contacts/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $userId = $user->get('id'); @@ -30,180 +31,180 @@ $saveOrder = $listOrder == 'a.ordering'; $assoc = Associations::isEnabled(); -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_contact&task=contacts.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_contact&task=contacts.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
-
-
-
- $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items); - foreach ($this->items as $i => $item) : - $canCreate = $user->authorise('core.create', 'com_contact.category.' . $item->catid); - $canEdit = $user->authorise('core.edit', 'com_contact.category.' . $item->catid); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); - $canEditOwn = $user->authorise('core.edit.own', 'com_contact.category.' . $item->catid) && $item->created_by == $userId; - $canChange = $user->authorise('core.edit.state', 'com_contact.category.' . $item->catid) && $canCheckin; +
+
+
+ $this)); ?> + items)) : ?> +
+ + +
+ +
- , - , - -
- - - - - - - - - - - - - - - - - - - -
+ + + + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items); + foreach ($this->items as $i => $item) : + $canCreate = $user->authorise('core.create', 'com_contact.category.' . $item->catid); + $canEdit = $user->authorise('core.edit', 'com_contact.category.' . $item->catid); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); + $canEditOwn = $user->authorise('core.edit.own', 'com_contact.category.' . $item->catid) && $item->created_by == $userId; + $canChange = $user->authorise('core.edit.state', 'com_contact.category.' . $item->catid) && $canCheckin; - $item->cat_link = Route::_('index.php?option=com_categories&extension=com_contact&task=edit&type=other&id=' . $item->catid); - ?> - - - - - + $item->cat_link = Route::_('index.php?option=com_categories&extension=com_contact&task=edit&type=other&id=' . $item->catid); + ?> + + + + + - - - - - - - - - - - - - -
+ , + , + +
+ + + + + + + + + + + + + + + + + + + +
- id, false, 'cid', 'cb', $item->name); ?> - - - - - - - - - - featured, $i, $canChange); ?> - - published, $i, 'contacts.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> -
+ id, false, 'cid', 'cb', $item->name); ?> + + + + + + + + + + featured, $i, $canChange); ?> + + published, $i, 'contacts.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> + -
- checked_out) : ?> - editor, $item->checked_out_time, 'contacts.', $canCheckin); ?> - - - - escape($item->name); ?> - - escape($item->name); ?> - -
- escape($item->alias)); ?> -
-
- escape($item->category_title); ?> -
-
-
- linked_user)) : ?> - linked_user; ?> -
email; ?>
- -
- access_level; ?> - - association) : ?> - id); ?> - - - - - id; ?> -
+ +
+ checked_out) : ?> + editor, $item->checked_out_time, 'contacts.', $canCheckin); ?> + + + + escape($item->name); ?> + + escape($item->name); ?> + +
+ escape($item->alias)); ?> +
+
+ escape($item->category_title); ?> +
+
+ + + linked_user)) : ?> + linked_user; ?> +
email; ?>
+ + + + access_level; ?> + + + + association) : ?> + id); ?> + + + + + + + + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', 'com_contact') - && $user->authorise('core.edit', 'com_contact') - && $user->authorise('core.edit.state', 'com_contact')) : ?> - Text::_('COM_CONTACT_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ), - $this->loadTemplate('batch_body') - ); ?> - - - - - -
-
-
+ + authorise('core.create', 'com_contact') + && $user->authorise('core.edit', 'com_contact') + && $user->authorise('core.edit.state', 'com_contact') +) : ?> + Text::_('COM_CONTACT_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ), + $this->loadTemplate('batch_body') + ); ?> + + + + + + + +
diff --git a/administrator/components/com_contact/tmpl/contacts/default_batch_body.php b/administrator/components/com_contact/tmpl/contacts/default_batch_body.php index 4d5bcd3610f7e..101087e40c57d 100644 --- a/administrator/components/com_contact/tmpl/contacts/default_batch_body.php +++ b/administrator/components/com_contact/tmpl/contacts/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Multilanguage; @@ -16,37 +18,37 @@ ?>
-
- -
-
- -
-
- -
-
- -
-
-
-
- = 0) : ?> -
-
- 'com_contact']); ?> -
-
- -
-
- -
-
-
-
- $noUser]); ?> -
-
-
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ = 0) : ?> +
+
+ 'com_contact']); ?> +
+
+ +
+
+ +
+
+
+
+ $noUser]); ?> +
+
+
diff --git a/administrator/components/com_contact/tmpl/contacts/default_batch_footer.php b/administrator/components/com_contact/tmpl/contacts/default_batch_footer.php index 3b64d567c3d67..440ed84e4008b 100644 --- a/administrator/components/com_contact/tmpl/contacts/default_batch_footer.php +++ b/administrator/components/com_contact/tmpl/contacts/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/administrator/components/com_contact/tmpl/contacts/emptystate.php b/administrator/components/com_contact/tmpl/contacts/emptystate.php index cf18d79722ea1..5cfb9038c043d 100644 --- a/administrator/components/com_contact/tmpl/contacts/emptystate.php +++ b/administrator/components/com_contact/tmpl/contacts/emptystate.php @@ -1,4 +1,5 @@ 'COM_CONTACT', - 'formURL' => 'index.php?option=com_contact', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Contacts', - 'icon' => 'icon-address-book contact', + 'textPrefix' => 'COM_CONTACT', + 'formURL' => 'index.php?option=com_contact', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Contacts', + 'icon' => 'icon-address-book contact', ]; $user = Factory::getApplication()->getIdentity(); -if ($user->authorise('core.create', 'com_contact') || count($user->getAuthorisedCategories('com_contact', 'core.create')) > 0) -{ - $displayData['createURL'] = 'index.php?option=com_contact&task=contact.add'; +if ($user->authorise('core.create', 'com_contact') || count($user->getAuthorisedCategories('com_contact', 'core.create')) > 0) { + $displayData['createURL'] = 'index.php?option=com_contact&task=contact.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_contact/tmpl/contacts/modal.php b/administrator/components/com_contact/tmpl/contacts/modal.php index 72b644fe3ed06..6f5a11153c85c 100644 --- a/administrator/components/com_contact/tmpl/contacts/modal.php +++ b/administrator/components/com_contact/tmpl/contacts/modal.php @@ -1,4 +1,5 @@ isClient('site')) -{ - Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); +if ($app->isClient('site')) { + Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); } /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ @@ -36,128 +36,120 @@ $onclick = $this->escape($function); $multilang = Multilanguage::isEnabled(); -if (!empty($editor)) -{ - // This view is used also in com_menus. Load the xtd script only if the editor is set! - $this->document->addScriptOptions('xtd-contacts', array('editor' => $editor)); - $onclick = "jSelectContact"; +if (!empty($editor)) { + // This view is used also in com_menus. Load the xtd script only if the editor is set! + $this->document->addScriptOptions('xtd-contacts', array('editor' => $editor)); + $onclick = "jSelectContact"; } ?>
-
+ - $this)); ?> + $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - 2 => 'icon-folder', - ); - ?> - items as $i => $item) : ?> - language && $multilang) - { - $tag = strlen($item->language); - if ($tag == 5) - { - $lang = substr($item->language, 0, 2); - } - elseif ($tag == 6) - { - $lang = substr($item->language, 0, 3); - } - else { - $lang = ''; - } - } - elseif (!$multilang) - { - $lang = ''; - } - ?> - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - -
- - - - - - escape($item->name); ?> - -
- escape($item->category_title); ?> -
-
- linked_user)) : ?> - linked_user; ?> - - - escape($item->access_level); ?> - - - - id; ?> -
+ items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + 2 => 'icon-folder', + ); + ?> + items as $i => $item) : ?> + language && $multilang) { + $tag = strlen($item->language); + if ($tag == 5) { + $lang = substr($item->language, 0, 2); + } elseif ($tag == 6) { + $lang = substr($item->language, 0, 3); + } else { + $lang = ''; + } + } elseif (!$multilang) { + $lang = ''; + } + ?> + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + +
+ + + + + + escape($item->name); ?> + +
+ escape($item->category_title); ?> +
+
+ linked_user)) : ?> + linked_user; ?> + + + escape($item->access_level); ?> + + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - + + + -
+
diff --git a/administrator/components/com_content/helpers/content.php b/administrator/components/com_content/helpers/content.php index ffa37af26c2b6..97ee1a2113373 100644 --- a/administrator/components/com_content/helpers/content.php +++ b/administrator/components/com_content/helpers/content.php @@ -1,13 +1,16 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; + /** * Content component helper. diff --git a/administrator/components/com_content/services/provider.php b/administrator/components/com_content/services/provider.php index a3960c5b1cf93..72b91c17f3e80 100644 --- a/administrator/components/com_content/services/provider.php +++ b/administrator/components/com_content/services/provider.php @@ -1,4 +1,5 @@ set(AssociationExtensionInterface::class, new AssociationsHelper); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->set(AssociationExtensionInterface::class, new AssociationsHelper()); - $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Content')); - $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Content')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Content')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Content')); + $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Content')); + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Content')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Content')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Content')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new ContentComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new ContentComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); - $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); + $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_content/src/Controller/AjaxController.php b/administrator/components/com_content/src/Controller/AjaxController.php index ddbe1add4a159..36b7ce5510b2d 100644 --- a/administrator/components/com_content/src/Controller/AjaxController.php +++ b/administrator/components/com_content/src/Controller/AjaxController.php @@ -1,4 +1,5 @@ input->getInt('assocId', 0); + /** + * Method to fetch associations of an article + * + * The method assumes that the following http parameters are passed in an Ajax Get request: + * token: the form token + * assocId: the id of the article whose associations are to be returned + * excludeLang: the association for this language is to be excluded + * + * @return null + * + * @since 3.9.0 + */ + public function fetchAssociations() + { + if (!Session::checkToken('get')) { + echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true); + } else { + $assocId = $this->input->getInt('assocId', 0); - if ($assocId == 0) - { - echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); + if ($assocId == 0) { + echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); - return; - } + return; + } - $excludeLang = $this->input->get('excludeLang', '', 'STRING'); + $excludeLang = $this->input->get('excludeLang', '', 'STRING'); - $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', (int) $assocId); + $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', (int) $assocId); - unset($associations[$excludeLang]); + unset($associations[$excludeLang]); - // Add the title to each of the associated records - $contentTable = Table::getInstance('Content', 'JTable'); + // Add the title to each of the associated records + $contentTable = Table::getInstance('Content', 'JTable'); - foreach ($associations as $lang => $association) - { - $contentTable->load($association->id); - $associations[$lang]->title = $contentTable->title; - } + foreach ($associations as $lang => $association) { + $contentTable->load($association->id); + $associations[$lang]->title = $contentTable->title; + } - $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); + $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); - if (count($associations) == 0) - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); - } - elseif ($countContentLanguages > count($associations) + 2) - { - $tags = implode(', ', array_keys($associations)); - $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); - } - else - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); - } + if (count($associations) == 0) { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); + } elseif ($countContentLanguages > count($associations) + 2) { + $tags = implode(', ', array_keys($associations)); + $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); + } else { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); + } - echo new JsonResponse($associations, $message); - } - } + echo new JsonResponse($associations, $message); + } + } } diff --git a/administrator/components/com_content/src/Controller/ArticleController.php b/administrator/components/com_content/src/Controller/ArticleController.php index 4147cb5b7de04..586bf30031ab9 100644 --- a/administrator/components/com_content/src/Controller/ArticleController.php +++ b/administrator/components/com_content/src/Controller/ArticleController.php @@ -1,4 +1,5 @@ input->get('return') == 'featured') - { - $this->view_list = 'featured'; - $this->view_item = 'article&return=featured'; - } - } - - /** - * Function that allows child controller access to model data - * after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 4.0.0 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) - { - if ($this->getTask() === 'save2menu') - { - $editState = []; - - $id = $model->getState('article.id'); - - $link = 'index.php?option=com_content&view=article'; - $type = 'component'; - - $editState['id'] = $id; - $editState['link'] = $link; - $editState['title'] = $model->getItem($id)->title; - $editState['type'] = $type; - $editState['request']['id'] = $id; - - $this->app->setUserState('com_menus.edit.item', array( - 'data' => $editState, - 'type' => $type, - 'link' => $link) - ); - - $this->setRedirect(Route::_('index.php?option=com_menus&view=item&client_id=0&menutype=mainmenu&layout=edit', false)); - } - } - - /** - * Method override to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowAdd($data = array()) - { - $categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('filter_category_id'), 'int'); - - if ($categoryId) - { - // If the category has been passed in the data or URL check it. - return $this->app->getIdentity()->authorise('core.create', 'com_content.category.' . $categoryId); - } - - // In the absence of better information, revert to the component permissions. - return parent::allowAdd(); - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - $user = $this->app->getIdentity(); - - // Zero record (id:0), return component edit permission by calling parent controller method - if (!$recordId) - { - return parent::allowEdit($data, $key); - } - - // Check edit on the record asset (explicit or inherited) - if ($user->authorise('core.edit', 'com_content.article.' . $recordId)) - { - return true; - } - - // Check edit own on the record asset (explicit or inherited) - if ($user->authorise('core.edit.own', 'com_content.article.' . $recordId)) - { - // Existing record already has an owner, get it - $record = $this->getModel()->getItem($recordId); - - if (empty($record)) - { - return false; - } - - // Grant if current user is owner of the record - return $user->id == $record->created_by; - } - - return false; - } - - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 1.6 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - /** @var \Joomla\Component\Content\Administrator\Model\ArticleModel $model */ - $model = $this->getModel('Article', 'Administrator', array()); - - // Preset the redirect - $this->setRedirect(Route::_('index.php?option=com_content&view=articles' . $this->getRedirectToListAppend(), false)); - - return parent::batch($model); - } + use VersionableControllerTrait; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * 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 Application for the dispatcher + * @param Input $input Input + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // An article edit form can come from the articles or featured view. + // Adjust the redirect view on the value of 'return' in the request. + if ($this->input->get('return') == 'featured') { + $this->view_list = 'featured'; + $this->view_item = 'article&return=featured'; + } + } + + /** + * Function that allows child controller access to model data + * after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 4.0.0 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) + { + if ($this->getTask() === 'save2menu') { + $editState = []; + + $id = $model->getState('article.id'); + + $link = 'index.php?option=com_content&view=article'; + $type = 'component'; + + $editState['id'] = $id; + $editState['link'] = $link; + $editState['title'] = $model->getItem($id)->title; + $editState['type'] = $type; + $editState['request']['id'] = $id; + + $this->app->setUserState('com_menus.edit.item', array( + 'data' => $editState, + 'type' => $type, + 'link' => $link)); + + $this->setRedirect(Route::_('index.php?option=com_menus&view=item&client_id=0&menutype=mainmenu&layout=edit', false)); + } + } + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowAdd($data = array()) + { + $categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('filter_category_id'), 'int'); + + if ($categoryId) { + // If the category has been passed in the data or URL check it. + return $this->app->getIdentity()->authorise('core.create', 'com_content.category.' . $categoryId); + } + + // In the absence of better information, revert to the component permissions. + return parent::allowAdd(); + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + $user = $this->app->getIdentity(); + + // Zero record (id:0), return component edit permission by calling parent controller method + if (!$recordId) { + return parent::allowEdit($data, $key); + } + + // Check edit on the record asset (explicit or inherited) + if ($user->authorise('core.edit', 'com_content.article.' . $recordId)) { + return true; + } + + // Check edit own on the record asset (explicit or inherited) + if ($user->authorise('core.edit.own', 'com_content.article.' . $recordId)) { + // Existing record already has an owner, get it + $record = $this->getModel()->getItem($recordId); + + if (empty($record)) { + return false; + } + + // Grant if current user is owner of the record + return $user->id == $record->created_by; + } + + return false; + } + + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 1.6 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + /** @var \Joomla\Component\Content\Administrator\Model\ArticleModel $model */ + $model = $this->getModel('Article', 'Administrator', array()); + + // Preset the redirect + $this->setRedirect(Route::_('index.php?option=com_content&view=articles' . $this->getRedirectToListAppend(), false)); + + return parent::batch($model); + } } diff --git a/administrator/components/com_content/src/Controller/ArticlesController.php b/administrator/components/com_content/src/Controller/ArticlesController.php index b70165bedd53f..149f4900ba21d 100644 --- a/administrator/components/com_content/src/Controller/ArticlesController.php +++ b/administrator/components/com_content/src/Controller/ArticlesController.php @@ -1,4 +1,5 @@ input->get('view') == 'featured') - { - $this->view_list = 'featured'; - } - - $this->registerTask('unfeatured', 'featured'); - } - - /** - * Method to toggle the featured setting of a list of articles. - * - * @return void - * - * @since 1.6 - */ - public function featured() - { - // Check for request forgeries - $this->checkToken(); - - $user = $this->app->getIdentity(); - $ids = (array) $this->input->get('cid', array(), 'int'); - $values = array('featured' => 1, 'unfeatured' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($values, $task, 0, 'int'); - $redirectUrl = 'index.php?option=com_content&view=' . $this->view_list . $this->getRedirectToListAppend(); - - // Access checks. - foreach ($ids as $i => $id) - { - // Remove zero value resulting from input filter - if ($id === 0) - { - unset($ids[$i]); - - continue; - } - - if (!$user->authorise('core.edit.state', 'com_content.article.' . (int) $id)) - { - // Prune items that you can't change. - unset($ids[$i]); - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'notice'); - } - } - - if (empty($ids)) - { - $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), 'error'); - - $this->setRedirect(Route::_($redirectUrl, false)); - - return; - } - - // Get the model. - /** @var \Joomla\Component\Content\Administrator\Model\ArticleModel $model */ - $model = $this->getModel(); - - // Publish the items. - if (!$model->featured($ids, $value)) - { - $this->setRedirect(Route::_($redirectUrl, false), $model->getError(), 'error'); - - return; - } - - if ($value == 1) - { - $message = Text::plural('COM_CONTENT_N_ITEMS_FEATURED', count($ids)); - } - else - { - $message = Text::plural('COM_CONTENT_N_ITEMS_UNFEATURED', count($ids)); - } - - $this->setRedirect(Route::_($redirectUrl, false), $message); - } - - /** - * Proxy for getModel. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config The array of possible config values. Optional. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel - * - * @since 1.6 - */ - public function getModel($name = 'Article', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to get the JSON-encoded amount of published articles - * - * @return void - * - * @since 4.0.0 - */ - public function getQuickiconContent() - { - $model = $this->getModel('articles'); - - $model->setState('filter.published', 1); - - $amount = (int) $model->getTotal(); - - $result = []; - - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_CONTENT_N_QUICKICON_SRONLY', $amount); - $result['name'] = Text::plural('COM_CONTENT_N_QUICKICON', $amount); - - echo new JsonResponse($result); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * 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 Application for the dispatcher + * @param Input $input Input + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // Articles default form can come from the articles or featured view. + // Adjust the redirect view on the value of 'view' in the request. + if ($this->input->get('view') == 'featured') { + $this->view_list = 'featured'; + } + + $this->registerTask('unfeatured', 'featured'); + } + + /** + * Method to toggle the featured setting of a list of articles. + * + * @return void + * + * @since 1.6 + */ + public function featured() + { + // Check for request forgeries + $this->checkToken(); + + $user = $this->app->getIdentity(); + $ids = (array) $this->input->get('cid', array(), 'int'); + $values = array('featured' => 1, 'unfeatured' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($values, $task, 0, 'int'); + $redirectUrl = 'index.php?option=com_content&view=' . $this->view_list . $this->getRedirectToListAppend(); + + // Access checks. + foreach ($ids as $i => $id) { + // Remove zero value resulting from input filter + if ($id === 0) { + unset($ids[$i]); + + continue; + } + + if (!$user->authorise('core.edit.state', 'com_content.article.' . (int) $id)) { + // Prune items that you can't change. + unset($ids[$i]); + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'notice'); + } + } + + if (empty($ids)) { + $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), 'error'); + + $this->setRedirect(Route::_($redirectUrl, false)); + + return; + } + + // Get the model. + /** @var \Joomla\Component\Content\Administrator\Model\ArticleModel $model */ + $model = $this->getModel(); + + // Publish the items. + if (!$model->featured($ids, $value)) { + $this->setRedirect(Route::_($redirectUrl, false), $model->getError(), 'error'); + + return; + } + + if ($value == 1) { + $message = Text::plural('COM_CONTENT_N_ITEMS_FEATURED', count($ids)); + } else { + $message = Text::plural('COM_CONTENT_N_ITEMS_UNFEATURED', count($ids)); + } + + $this->setRedirect(Route::_($redirectUrl, false), $message); + } + + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config The array of possible config values. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel + * + * @since 1.6 + */ + public function getModel($name = 'Article', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to get the JSON-encoded amount of published articles + * + * @return void + * + * @since 4.0.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('articles'); + + $model->setState('filter.published', 1); + + $amount = (int) $model->getTotal(); + + $result = []; + + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_CONTENT_N_QUICKICON_SRONLY', $amount); + $result['name'] = Text::plural('COM_CONTENT_N_QUICKICON', $amount); + + echo new JsonResponse($result); + } } diff --git a/administrator/components/com_content/src/Controller/DisplayController.php b/administrator/components/com_content/src/Controller/DisplayController.php index a36e2ec75ee16..b53d767e2aca6 100644 --- a/administrator/components/com_content/src/Controller/DisplayController.php +++ b/administrator/components/com_content/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'articles'); - $layout = $this->input->get('layout', 'articles'); - $id = $this->input->getInt('id'); - - // Check for edit form. - if ($view == 'article' && $layout == 'edit' && !$this->checkEditId('com_content.edit.article', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_content&view=articles', false)); - - return false; - } - - return parent::display(); - } + /** + * The default view. + * + * @var string + * @since 1.6 + */ + protected $default_view = 'articles'; + + /** + * Method to display a view. + * + * @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()}. + * + * @return BaseController|boolean This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = array()) + { + $view = $this->input->get('view', 'articles'); + $layout = $this->input->get('layout', 'articles'); + $id = $this->input->getInt('id'); + + // Check for edit form. + if ($view == 'article' && $layout == 'edit' && !$this->checkEditId('com_content.edit.article', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_content&view=articles', false)); + + return false; + } + + return parent::display(); + } } diff --git a/administrator/components/com_content/src/Controller/FeaturedController.php b/administrator/components/com_content/src/Controller/FeaturedController.php index 4f47f55b3bca6..028ee9f511b83 100644 --- a/administrator/components/com_content/src/Controller/FeaturedController.php +++ b/administrator/components/com_content/src/Controller/FeaturedController.php @@ -1,4 +1,5 @@ checkToken(); + /** + * Removes an item. + * + * @return void + * + * @since 1.6 + */ + public function delete() + { + // Check for request forgeries + $this->checkToken(); - $user = $this->app->getIdentity(); - $ids = (array) $this->input->get('cid', array(), 'int'); + $user = $this->app->getIdentity(); + $ids = (array) $this->input->get('cid', array(), 'int'); - // Access checks. - foreach ($ids as $i => $id) - { - // Remove zero value resulting from input filter - if ($id === 0) - { - unset($ids[$i]); + // Access checks. + foreach ($ids as $i => $id) { + // Remove zero value resulting from input filter + if ($id === 0) { + unset($ids[$i]); - continue; - } + continue; + } - if (!$user->authorise('core.delete', 'com_content.article.' . (int) $id)) - { - // Prune items that you can't delete. - unset($ids[$i]); - $this->app->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'notice'); - } - } + if (!$user->authorise('core.delete', 'com_content.article.' . (int) $id)) { + // Prune items that you can't delete. + unset($ids[$i]); + $this->app->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'notice'); + } + } - if (empty($ids)) - { - $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), 'error'); - } - else - { - /** @var \Joomla\Component\Content\Administrator\Model\FeatureModel $model */ - $model = $this->getModel(); + if (empty($ids)) { + $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), 'error'); + } else { + /** @var \Joomla\Component\Content\Administrator\Model\FeatureModel $model */ + $model = $this->getModel(); - // Remove the items. - if (!$model->featured($ids, 0)) - { - $this->app->enqueueMessage($model->getError(), 'error'); - } - } + // Remove the items. + if (!$model->featured($ids, 0)) { + $this->app->enqueueMessage($model->getError(), 'error'); + } + } - $this->setRedirect('index.php?option=com_content&view=featured'); - } + $this->setRedirect('index.php?option=com_content&view=featured'); + } - /** - * Method to publish a list of articles. - * - * @return void - * - * @since 1.0 - */ - public function publish() - { - parent::publish(); + /** + * Method to publish a list of articles. + * + * @return void + * + * @since 1.0 + */ + public function publish() + { + parent::publish(); - $this->setRedirect('index.php?option=com_content&view=featured'); - } + $this->setRedirect('index.php?option=com_content&view=featured'); + } - /** - * 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 \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. - * - * @since 1.6 - */ - public function getModel($name = 'Feature', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * 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 \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6 + */ + public function getModel($name = 'Feature', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_content/src/Event/Model/FeatureEvent.php b/administrator/components/com_content/src/Event/Model/FeatureEvent.php index eddcede227c72..09f48206b6c09 100644 --- a/administrator/components/com_content/src/Event/Model/FeatureEvent.php +++ b/administrator/components/com_content/src/Event/Model/FeatureEvent.php @@ -1,4 +1,5 @@ name is required but has not been provided"); - } + /** + * Constructor. + * + * @param string $name The event name. + * @param array $arguments The event arguments. + * + * @throws BadMethodCallException + * + * @since 4.0.0 + */ + public function __construct($name, array $arguments = array()) + { + if (!isset($arguments['extension'])) { + throw new BadMethodCallException("Argument 'extension' of event $this->name is required but has not been provided"); + } - if (!isset($arguments['extension']) || !is_string($arguments['extension'])) - { - throw new BadMethodCallException("Argument 'extension' of event $this->name is not of type 'string'"); - } + if (!isset($arguments['extension']) || !is_string($arguments['extension'])) { + throw new BadMethodCallException("Argument 'extension' of event $this->name is not of type 'string'"); + } - if (strpos($arguments['extension'], '.') === false) - { - throw new BadMethodCallException("Argument 'extension' of event $this->name has wrong format. Valid format: 'component.section'"); - } + if (strpos($arguments['extension'], '.') === false) { + throw new BadMethodCallException("Argument 'extension' of event $this->name has wrong format. Valid format: 'component.section'"); + } - if (!\array_key_exists('extensionName', $arguments) || !\array_key_exists('section', $arguments)) - { - $parts = explode('.', $arguments['extension']); + if (!\array_key_exists('extensionName', $arguments) || !\array_key_exists('section', $arguments)) { + $parts = explode('.', $arguments['extension']); - $arguments['extensionName'] = $arguments['extensionName'] ?? $parts[0]; - $arguments['section'] = $arguments['section'] ?? $parts[1]; - } + $arguments['extensionName'] = $arguments['extensionName'] ?? $parts[0]; + $arguments['section'] = $arguments['section'] ?? $parts[1]; + } - if (!isset($arguments['pks']) || !is_array($arguments['pks'])) - { - throw new BadMethodCallException("Argument 'pks' of event $this->name is not of type 'array'"); - } + if (!isset($arguments['pks']) || !is_array($arguments['pks'])) { + throw new BadMethodCallException("Argument 'pks' of event $this->name is not of type 'array'"); + } - if (!isset($arguments['value']) || !is_numeric($arguments['value'])) - { - throw new BadMethodCallException("Argument 'value' of event $this->name is not of type 'numeric'"); - } + if (!isset($arguments['value']) || !is_numeric($arguments['value'])) { + throw new BadMethodCallException("Argument 'value' of event $this->name is not of type 'numeric'"); + } - $arguments['value'] = (int) $arguments['value']; + $arguments['value'] = (int) $arguments['value']; - if ($arguments['value'] !== 0 && $arguments['value'] !== 1) - { - throw new BadMethodCallException("Argument 'value' of event $this->name is not 0 or 1"); - } + if ($arguments['value'] !== 0 && $arguments['value'] !== 1) { + throw new BadMethodCallException("Argument 'value' of event $this->name is not 0 or 1"); + } - parent::__construct($name, $arguments); - } + parent::__construct($name, $arguments); + } - /** - * Set used parameter to true - * - * @param bool $value The value to set - * - * @return void - * - * @since 4.0.0 - */ - public function setAbort(string $reason) - { - $this->arguments['abort'] = true; - $this->arguments['abortReason'] = $reason; - } + /** + * Set used parameter to true + * + * @param bool $value The value to set + * + * @return void + * + * @since 4.0.0 + */ + public function setAbort(string $reason) + { + $this->arguments['abort'] = true; + $this->arguments['abortReason'] = $reason; + } } diff --git a/administrator/components/com_content/src/Extension/ContentComponent.php b/administrator/components/com_content/src/Extension/ContentComponent.php index 3d2a03733eab8..e93bea1884ddd 100644 --- a/administrator/components/com_content/src/Extension/ContentComponent.php +++ b/administrator/components/com_content/src/Extension/ContentComponent.php @@ -1,4 +1,5 @@ true, - 'core.state' => true, - ]; - - /** - * The trashed condition - * - * @since 4.0.0 - */ - const CONDITION_NAMES = [ - self::CONDITION_PUBLISHED => 'JPUBLISHED', - self::CONDITION_UNPUBLISHED => 'JUNPUBLISHED', - self::CONDITION_ARCHIVED => 'JARCHIVED', - self::CONDITION_TRASHED => 'JTRASHED', - ]; - - /** - * The archived condition - * - * @since 4.0.0 - */ - const CONDITION_ARCHIVED = 2; - - /** - * The published condition - * - * @since 4.0.0 - */ - const CONDITION_PUBLISHED = 1; - - /** - * The unpublished condition - * - * @since 4.0.0 - */ - const CONDITION_UNPUBLISHED = 0; - - /** - * The trashed condition - * - * @since 4.0.0 - */ - const CONDITION_TRASHED = -2; - - /** - * Booting the extension. This is the function to set up the environment of the extension like - * registering new class loaders, etc. - * - * If required, some initial set up can be done from services of the container, eg. - * registering HTML services. - * - * @param ContainerInterface $container The container - * - * @return void - * - * @since 4.0.0 - */ - public function boot(ContainerInterface $container) - { - $this->getRegistry()->register('contentadministrator', new AdministratorService); - $this->getRegistry()->register('contenticon', new Icon); - - // The layout joomla.content.icons does need a general icon service - $this->getRegistry()->register('icon', $this->getRegistry()->getService('contenticon')); - } - - /** - * Returns a valid section for the given section. If it is not valid then null - * is returned. - * - * @param string $section The section to get the mapping for - * @param object $item The item - * - * @return string|null The new section - * - * @since 4.0.0 - */ - public function validateSection($section, $item = null) - { - if (Factory::getApplication()->isClient('site')) - { - // On the front end we need to map some sections - switch ($section) - { - // Editing an article - case 'form': - - // Category list view - case 'featured': - case 'category': - $section = 'article'; - } - } - - if ($section != 'article') - { - // We don't know other sections - return null; - } - - return $section; - } - - /** - * Returns valid contexts - * - * @return array - * - * @since 4.0.0 - */ - public function getContexts(): array - { - Factory::getLanguage()->load('com_content', JPATH_ADMINISTRATOR); - - $contexts = array( - 'com_content.article' => Text::_('COM_CONTENT'), - 'com_content.categories' => Text::_('JCATEGORY') - ); - - return $contexts; - } - - /** - * Returns valid contexts - * - * @return array - * - * @since 4.0.0 - */ - public function getWorkflowContexts(): array - { - Factory::getLanguage()->load('com_content', JPATH_ADMINISTRATOR); - - $contexts = array( - 'com_content.article' => Text::_('COM_CONTENT') - ); - - return $contexts; - } - - /** - * Returns the workflow context based on the given category section - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - public function getCategoryWorkflowContext(?string $section = null): string - { - $context = $this->getWorkflowContexts(); - - return array_key_first($context); - } - - /** - * Returns the table for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getTableNameForSection(string $section = null) - { - return '#__content'; - } - - /** - * Returns a table name for the state association - * - * @param string $section An optional section to separate different areas in the component - * - * @return string - * - * @since 4.0.0 - */ - public function getWorkflowTableBySection(?string $section = null): string - { - return '#__content'; - } - - /** - * Returns the model name, based on the context - * - * @param string $context The context of the workflow - * - * @return string - */ - public function getModelName($context): string - { - $parts = explode('.', $context); - - if (count($parts) < 2) - { - return ''; - } - - array_shift($parts); - - $modelname = array_shift($parts); - - if ($modelname === 'article' && Factory::getApplication()->isClient('site')) - { - return 'Form'; - } - elseif ($modelname === 'featured' && Factory::getApplication()->isClient('administrator')) - { - return 'Article'; - } - - return ucfirst($modelname); - } - - /** - * Method to filter transitions by given id of state. - * - * @param array $transitions The Transitions to filter - * @param int $pk Id of the state - * - * @return array - * - * @since 4.0.0 - */ - public function filterTransitions(array $transitions, int $pk): array - { - return ContentHelper::filterTransitions($transitions, $pk); - } - - /** - * Adds Count Items for Category Manager. - * - * @param \stdClass[] $items The category objects - * @param string $section The section - * - * @return void - * - * @since 4.0.0 - */ - public function countItems(array $items, string $section) - { - $config = (object) array( - 'related_tbl' => 'content', - 'state_col' => 'state', - 'group_col' => 'catid', - 'relation_type' => 'category_or_group', - 'uses_workflows' => true, - 'workflows_component' => 'com_content' - ); - - LibraryContentHelper::countRelations($items, $config); - } - - /** - * Adds Count Items for Tag Manager. - * - * @param \stdClass[] $items The content objects - * @param string $extension The name of the active view. - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function countTagItems(array $items, string $extension) - { - $parts = explode('.', $extension); - $section = count($parts) > 1 ? $parts[1] : null; - - $config = (object) array( - 'related_tbl' => ($section === 'category' ? 'categories' : 'content'), - 'state_col' => ($section === 'category' ? 'published' : 'state'), - 'group_col' => 'tag_id', - 'extension' => $extension, - 'relation_type' => 'tag_assigments', - ); - - LibraryContentHelper::countRelations($items, $config); - } - - /** - * Prepares the category form - * - * @param Form $form The form to prepare - * @param array|object $data The form data - * - * @return void - */ - public function prepareForm(Form $form, $data) - { - ContentHelper::onPrepareForm($form, $data); - } + use AssociationServiceTrait; + use RouterServiceTrait; + use HTMLRegistryAwareTrait; + use WorkflowServiceTrait; + use CategoryServiceTrait, TagServiceTrait { + CategoryServiceTrait::getTableNameForSection insteadof TagServiceTrait; + CategoryServiceTrait::getStateColumnForSection insteadof TagServiceTrait; + } + + /** @var array Supported functionality */ + protected $supportedFunctionality = [ + 'core.featured' => true, + 'core.state' => true, + ]; + + /** + * The trashed condition + * + * @since 4.0.0 + */ + public const CONDITION_NAMES = [ + self::CONDITION_PUBLISHED => 'JPUBLISHED', + self::CONDITION_UNPUBLISHED => 'JUNPUBLISHED', + self::CONDITION_ARCHIVED => 'JARCHIVED', + self::CONDITION_TRASHED => 'JTRASHED', + ]; + + /** + * The archived condition + * + * @since 4.0.0 + */ + public const CONDITION_ARCHIVED = 2; + + /** + * The published condition + * + * @since 4.0.0 + */ + public const CONDITION_PUBLISHED = 1; + + /** + * The unpublished condition + * + * @since 4.0.0 + */ + public const CONDITION_UNPUBLISHED = 0; + + /** + * The trashed condition + * + * @since 4.0.0 + */ + public const CONDITION_TRASHED = -2; + + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('contentadministrator', new AdministratorService()); + $this->getRegistry()->register('contenticon', new Icon()); + + // The layout joomla.content.icons does need a general icon service + $this->getRegistry()->register('icon', $this->getRegistry()->getService('contenticon')); + } + + /** + * Returns a valid section for the given section. If it is not valid then null + * is returned. + * + * @param string $section The section to get the mapping for + * @param object $item The item + * + * @return string|null The new section + * + * @since 4.0.0 + */ + public function validateSection($section, $item = null) + { + if (Factory::getApplication()->isClient('site')) { + // On the front end we need to map some sections + switch ($section) { + // Editing an article + case 'form': + // Category list view + case 'featured': + case 'category': + $section = 'article'; + } + } + + if ($section != 'article') { + // We don't know other sections + return null; + } + + return $section; + } + + /** + * Returns valid contexts + * + * @return array + * + * @since 4.0.0 + */ + public function getContexts(): array + { + Factory::getLanguage()->load('com_content', JPATH_ADMINISTRATOR); + + $contexts = array( + 'com_content.article' => Text::_('COM_CONTENT'), + 'com_content.categories' => Text::_('JCATEGORY') + ); + + return $contexts; + } + + /** + * Returns valid contexts + * + * @return array + * + * @since 4.0.0 + */ + public function getWorkflowContexts(): array + { + Factory::getLanguage()->load('com_content', JPATH_ADMINISTRATOR); + + $contexts = array( + 'com_content.article' => Text::_('COM_CONTENT') + ); + + return $contexts; + } + + /** + * Returns the workflow context based on the given category section + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + public function getCategoryWorkflowContext(?string $section = null): string + { + $context = $this->getWorkflowContexts(); + + return array_key_first($context); + } + + /** + * Returns the table for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getTableNameForSection(string $section = null) + { + return '#__content'; + } + + /** + * Returns a table name for the state association + * + * @param string $section An optional section to separate different areas in the component + * + * @return string + * + * @since 4.0.0 + */ + public function getWorkflowTableBySection(?string $section = null): string + { + return '#__content'; + } + + /** + * Returns the model name, based on the context + * + * @param string $context The context of the workflow + * + * @return string + */ + public function getModelName($context): string + { + $parts = explode('.', $context); + + if (count($parts) < 2) { + return ''; + } + + array_shift($parts); + + $modelname = array_shift($parts); + + if ($modelname === 'article' && Factory::getApplication()->isClient('site')) { + return 'Form'; + } elseif ($modelname === 'featured' && Factory::getApplication()->isClient('administrator')) { + return 'Article'; + } + + return ucfirst($modelname); + } + + /** + * Method to filter transitions by given id of state. + * + * @param array $transitions The Transitions to filter + * @param int $pk Id of the state + * + * @return array + * + * @since 4.0.0 + */ + public function filterTransitions(array $transitions, int $pk): array + { + return ContentHelper::filterTransitions($transitions, $pk); + } + + /** + * Adds Count Items for Category Manager. + * + * @param \stdClass[] $items The category objects + * @param string $section The section + * + * @return void + * + * @since 4.0.0 + */ + public function countItems(array $items, string $section) + { + $config = (object) array( + 'related_tbl' => 'content', + 'state_col' => 'state', + 'group_col' => 'catid', + 'relation_type' => 'category_or_group', + 'uses_workflows' => true, + 'workflows_component' => 'com_content' + ); + + LibraryContentHelper::countRelations($items, $config); + } + + /** + * Adds Count Items for Tag Manager. + * + * @param \stdClass[] $items The content objects + * @param string $extension The name of the active view. + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function countTagItems(array $items, string $extension) + { + $parts = explode('.', $extension); + $section = count($parts) > 1 ? $parts[1] : null; + + $config = (object) array( + 'related_tbl' => ($section === 'category' ? 'categories' : 'content'), + 'state_col' => ($section === 'category' ? 'published' : 'state'), + 'group_col' => 'tag_id', + 'extension' => $extension, + 'relation_type' => 'tag_assigments', + ); + + LibraryContentHelper::countRelations($items, $config); + } + + /** + * Prepares the category form + * + * @param Form $form The form to prepare + * @param array|object $data The form data + * + * @return void + */ + public function prepareForm(Form $form, $data) + { + ContentHelper::onPrepareForm($form, $data); + } } diff --git a/administrator/components/com_content/src/Field/AssocField.php b/administrator/components/com_content/src/Field/AssocField.php index 7746abeaa8f1b..0e85fa362c0e4 100644 --- a/administrator/components/com_content/src/Field/AssocField.php +++ b/administrator/components/com_content/src/Field/AssocField.php @@ -1,4 +1,5 @@ ` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @see AssocField::setup() - * @since 4.0.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - if (!Associations::isEnabled()) - { - return false; - } + /** + * Method to attach a Form object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @see AssocField::setup() + * @since 4.0.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + if (!Associations::isEnabled()) { + return false; + } - return parent::setup($element, $value, $group); - } + return parent::setup($element, $value, $group); + } } diff --git a/administrator/components/com_content/src/Field/Modal/ArticleField.php b/administrator/components/com_content/src/Field/Modal/ArticleField.php index d37a1c3296a67..bf9f8dda4c6fe 100644 --- a/administrator/components/com_content/src/Field/Modal/ArticleField.php +++ b/administrator/components/com_content/src/Field/Modal/ArticleField.php @@ -1,4 +1,5 @@ element['new'] == 'true'); - $allowEdit = ((string) $this->element['edit'] == 'true'); - $allowClear = ((string) $this->element['clear'] != 'false'); - $allowSelect = ((string) $this->element['select'] != 'false'); - $allowPropagate = ((string) $this->element['propagate'] == 'true'); - - $languages = LanguageHelper::getContentLanguages(array(0, 1), false); - - // Load language - Factory::getLanguage()->load('com_content', JPATH_ADMINISTRATOR); - - // The active article id field. - $value = (int) $this->value ?: ''; - - // Create the modal id. - $modalId = 'Article_' . $this->id; - - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); - - // Add the modal field script to the document head. - $wa->useScript('field.modal-fields'); - - // Script to proxy the select modal function to the modal-fields.js file. - if ($allowSelect) - { - static $scriptSelect = null; - - if (is_null($scriptSelect)) - { - $scriptSelect = array(); - } - - if (!isset($scriptSelect[$this->id])) - { - $wa->addInlineScript(" + /** + * The form field type. + * + * @var string + * @since 1.6 + */ + protected $type = 'Modal_Article'; + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + $allowNew = ((string) $this->element['new'] == 'true'); + $allowEdit = ((string) $this->element['edit'] == 'true'); + $allowClear = ((string) $this->element['clear'] != 'false'); + $allowSelect = ((string) $this->element['select'] != 'false'); + $allowPropagate = ((string) $this->element['propagate'] == 'true'); + + $languages = LanguageHelper::getContentLanguages(array(0, 1), false); + + // Load language + Factory::getLanguage()->load('com_content', JPATH_ADMINISTRATOR); + + // The active article id field. + $value = (int) $this->value ?: ''; + + // Create the modal id. + $modalId = 'Article_' . $this->id; + + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + + // Add the modal field script to the document head. + $wa->useScript('field.modal-fields'); + + // Script to proxy the select modal function to the modal-fields.js file. + if ($allowSelect) { + static $scriptSelect = null; + + if (is_null($scriptSelect)) { + $scriptSelect = array(); + } + + if (!isset($scriptSelect[$this->id])) { + $wa->addInlineScript( + " window.jSelectArticle_" . $this->id . " = function (id, title, catid, object, url, language) { window.processModalSelect('Article', '" . $this->id . "', id, title, catid, object, url, language); }", - [], - ['type' => 'module'] - ); - - Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); - - $scriptSelect[$this->id] = true; - } - } - - // Setup variables for display. - $linkArticles = 'index.php?option=com_content&view=articles&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; - $linkArticle = 'index.php?option=com_content&view=article&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; - - if (isset($this->element['language'])) - { - $linkArticles .= '&forcedLanguage=' . $this->element['language']; - $linkArticle .= '&forcedLanguage=' . $this->element['language']; - $modalTitle = Text::_('COM_CONTENT_SELECT_AN_ARTICLE') . ' — ' . $this->element['label']; - } - else - { - $modalTitle = Text::_('COM_CONTENT_SELECT_AN_ARTICLE'); - } - - $urlSelect = $linkArticles . '&function=jSelectArticle_' . $this->id; - $urlEdit = $linkArticle . '&task=article.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; - $urlNew = $linkArticle . '&task=article.add'; - - if ($value) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__content')) - ->where($db->quoteName('id') . ' = :value') - ->bind(':value', $value, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $title = $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - - $title = empty($title) ? Text::_('COM_CONTENT_SELECT_AN_ARTICLE') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); - - // The current article display field. - $html = ''; - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - $html .= ''; - - // Select article button - if ($allowSelect) - { - $html .= '' - . ' ' . Text::_('JSELECT') - . ''; - } - - // New article button - if ($allowNew) - { - $html .= '' - . ' ' . Text::_('JACTION_CREATE') - . ''; - } - - // Edit article button - if ($allowEdit) - { - $html .= '' - . ' ' . Text::_('JACTION_EDIT') - . ''; - } - - // Clear article button - if ($allowClear) - { - $html .= '' - . ' ' . Text::_('JCLEAR') - . ''; - } - - // Propagate article button - if ($allowPropagate && count($languages) > 2) - { - // Strip off language tag at the end - $tagLength = (int) strlen($this->element['language']); - $callbackFunctionStem = substr("jSelectArticle_" . $this->id, 0, -$tagLength); - - $html .= '' - . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') - . ''; - } - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - // Select article modal - if ($allowSelect) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalSelect' . $modalId, - array( - 'title' => $modalTitle, - 'url' => $urlSelect, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ) - ); - } - - // New article modal - if ($allowNew) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalNew' . $modalId, - array( - 'title' => Text::_('COM_CONTENT_NEW_ARTICLE'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlNew, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Edit article modal - if ($allowEdit) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalEdit' . $modalId, - array( - 'title' => Text::_('COM_CONTENT_EDIT_ARTICLE'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlEdit, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Note: class='required' for client side validation. - $class = $this->required ? ' class="required modal-value"' : ''; - - $html .= ''; - - return $html; - } - - /** - * Method to get the field label markup. - * - * @return string The field label markup. - * - * @since 3.4 - */ - protected function getLabel() - { - return str_replace($this->id, $this->id . '_name', parent::getLabel()); - } + [], + ['type' => 'module'] + ); + + Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); + + $scriptSelect[$this->id] = true; + } + } + + // Setup variables for display. + $linkArticles = 'index.php?option=com_content&view=articles&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; + $linkArticle = 'index.php?option=com_content&view=article&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; + + if (isset($this->element['language'])) { + $linkArticles .= '&forcedLanguage=' . $this->element['language']; + $linkArticle .= '&forcedLanguage=' . $this->element['language']; + $modalTitle = Text::_('COM_CONTENT_SELECT_AN_ARTICLE') . ' — ' . $this->element['label']; + } else { + $modalTitle = Text::_('COM_CONTENT_SELECT_AN_ARTICLE'); + } + + $urlSelect = $linkArticles . '&function=jSelectArticle_' . $this->id; + $urlEdit = $linkArticle . '&task=article.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; + $urlNew = $linkArticle . '&task=article.add'; + + if ($value) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__content')) + ->where($db->quoteName('id') . ' = :value') + ->bind(':value', $value, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $title = $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + + $title = empty($title) ? Text::_('COM_CONTENT_SELECT_AN_ARTICLE') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); + + // The current article display field. + $html = ''; + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + $html .= ''; + + // Select article button + if ($allowSelect) { + $html .= '' + . ' ' . Text::_('JSELECT') + . ''; + } + + // New article button + if ($allowNew) { + $html .= '' + . ' ' . Text::_('JACTION_CREATE') + . ''; + } + + // Edit article button + if ($allowEdit) { + $html .= '' + . ' ' . Text::_('JACTION_EDIT') + . ''; + } + + // Clear article button + if ($allowClear) { + $html .= '' + . ' ' . Text::_('JCLEAR') + . ''; + } + + // Propagate article button + if ($allowPropagate && count($languages) > 2) { + // Strip off language tag at the end + $tagLength = (int) strlen($this->element['language']); + $callbackFunctionStem = substr("jSelectArticle_" . $this->id, 0, -$tagLength); + + $html .= '' + . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') + . ''; + } + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + // Select article modal + if ($allowSelect) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalSelect' . $modalId, + array( + 'title' => $modalTitle, + 'url' => $urlSelect, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ) + ); + } + + // New article modal + if ($allowNew) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalNew' . $modalId, + array( + 'title' => Text::_('COM_CONTENT_NEW_ARTICLE'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlNew, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Edit article modal + if ($allowEdit) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalEdit' . $modalId, + array( + 'title' => Text::_('COM_CONTENT_EDIT_ARTICLE'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlEdit, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Note: class='required' for client side validation. + $class = $this->required ? ' class="required modal-value"' : ''; + + $html .= ''; + + return $html; + } + + /** + * Method to get the field label markup. + * + * @return string The field label markup. + * + * @since 3.4 + */ + protected function getLabel() + { + return str_replace($this->id, $this->id . '_name', parent::getLabel()); + } } diff --git a/administrator/components/com_content/src/Field/VotelistField.php b/administrator/components/com_content/src/Field/VotelistField.php index 5aa280b44cd76..3a3a0739de162 100644 --- a/administrator/components/com_content/src/Field/VotelistField.php +++ b/administrator/components/com_content/src/Field/VotelistField.php @@ -1,4 +1,5 @@ ` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - // Requires vote plugin enabled - if (!PluginHelper::isEnabled('content', 'vote')) - { - return false; - } + /** + * Method to attach a form object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + // Requires vote plugin enabled + if (!PluginHelper::isEnabled('content', 'vote')) { + return false; + } - return parent::setup($element, $value, $group); - } + return parent::setup($element, $value, $group); + } } diff --git a/administrator/components/com_content/src/Field/VoteradioField.php b/administrator/components/com_content/src/Field/VoteradioField.php index c9e8bd696e9d3..caf521d86d30d 100644 --- a/administrator/components/com_content/src/Field/VoteradioField.php +++ b/administrator/components/com_content/src/Field/VoteradioField.php @@ -1,4 +1,5 @@ ` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - // Requires vote plugin enabled - if (!PluginHelper::isEnabled('content', 'vote')) - { - return false; - } + /** + * Method to attach a form object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + // Requires vote plugin enabled + if (!PluginHelper::isEnabled('content', 'vote')) { + return false; + } - return parent::setup($element, $value, $group); - } + return parent::setup($element, $value, $group); + } } diff --git a/administrator/components/com_content/src/Helper/AssociationsHelper.php b/administrator/components/com_content/src/Helper/AssociationsHelper.php index 4640ae4aafba7..314dd56d509f4 100644 --- a/administrator/components/com_content/src/Helper/AssociationsHelper.php +++ b/administrator/components/com_content/src/Helper/AssociationsHelper.php @@ -1,4 +1,5 @@ getType($typeName); - - $context = $this->extension . '.item'; - $catidField = 'catid'; - - if ($typeName === 'category') - { - $context = 'com_categories.item'; - $catidField = ''; - } - - // Get the associations. - $associations = Associations::getAssociations( - $this->extension, - $type['tables']['a'], - $context, - $id, - 'id', - 'alias', - $catidField - ); - - return $associations; - } - - /** - * Get item information - * - * @param string $typeName The item type - * @param int $id The id of item for which we need the associated items - * - * @return Table|null - * - * @since 3.7.0 - */ - public function getItem($typeName, $id) - { - if (empty($id)) - { - return null; - } - - $table = null; - - switch ($typeName) - { - case 'article': - $table = Table::getInstance('Content'); - break; - - case 'category': - $table = Table::getInstance('Category'); - break; - } - - if (is_null($table)) - { - return null; - } - - $table->load($id); - - return $table; - } - - /** - * Get information about the type - * - * @param string $typeName The item type - * - * @return array Array of item types - * - * @since 3.7.0 - */ - public function getType($typeName = '') - { - $fields = $this->getFieldsTemplate(); - $tables = array(); - $joins = array(); - $support = $this->getSupportTemplate(); - $title = ''; - - if (in_array($typeName, $this->itemTypes)) - { - switch ($typeName) - { - case 'article': - - $support['state'] = true; - $support['acl'] = true; - $support['checkout'] = true; - $support['category'] = true; - $support['save2copy'] = true; - - $tables = array( - 'a' => '#__content' - ); - - $title = 'article'; - break; - - case 'category': - $fields['created_user_id'] = 'a.created_user_id'; - $fields['ordering'] = 'a.lft'; - $fields['level'] = 'a.level'; - $fields['catid'] = ''; - $fields['state'] = 'a.published'; - - $support['state'] = true; - $support['acl'] = true; - $support['checkout'] = true; - $support['level'] = true; - - $tables = array( - 'a' => '#__categories' - ); - - $title = 'category'; - break; - } - } - - return array( - 'fields' => $fields, - 'support' => $support, - 'tables' => $tables, - 'joins' => $joins, - 'title' => $title - ); - } + /** + * The extension name + * + * @var array $extension + * + * @since 3.7.0 + */ + protected $extension = 'com_content'; + + /** + * Array of item types + * + * @var array $itemTypes + * + * @since 3.7.0 + */ + protected $itemTypes = array('article', 'category'); + + /** + * Has the extension association support + * + * @var boolean $associationsSupport + * + * @since 3.7.0 + */ + protected $associationsSupport = true; + + /** + * Method to get the associations for a given item. + * + * @param integer $id Id of the item + * @param string $view Name of the view + * + * @return array Array of associations for the item + * + * @since 4.0.0 + */ + public function getAssociationsForItem($id = 0, $view = null) + { + return AssociationHelper::getAssociations($id, $view); + } + + /** + * Get the associated items for an item + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return array + * + * @since 3.7.0 + */ + public function getAssociations($typeName, $id) + { + $type = $this->getType($typeName); + + $context = $this->extension . '.item'; + $catidField = 'catid'; + + if ($typeName === 'category') { + $context = 'com_categories.item'; + $catidField = ''; + } + + // Get the associations. + $associations = Associations::getAssociations( + $this->extension, + $type['tables']['a'], + $context, + $id, + 'id', + 'alias', + $catidField + ); + + return $associations; + } + + /** + * Get item information + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return Table|null + * + * @since 3.7.0 + */ + public function getItem($typeName, $id) + { + if (empty($id)) { + return null; + } + + $table = null; + + switch ($typeName) { + case 'article': + $table = Table::getInstance('Content'); + break; + + case 'category': + $table = Table::getInstance('Category'); + break; + } + + if (is_null($table)) { + return null; + } + + $table->load($id); + + return $table; + } + + /** + * Get information about the type + * + * @param string $typeName The item type + * + * @return array Array of item types + * + * @since 3.7.0 + */ + public function getType($typeName = '') + { + $fields = $this->getFieldsTemplate(); + $tables = array(); + $joins = array(); + $support = $this->getSupportTemplate(); + $title = ''; + + if (in_array($typeName, $this->itemTypes)) { + switch ($typeName) { + case 'article': + $support['state'] = true; + $support['acl'] = true; + $support['checkout'] = true; + $support['category'] = true; + $support['save2copy'] = true; + + $tables = array( + 'a' => '#__content' + ); + + $title = 'article'; + break; + + case 'category': + $fields['created_user_id'] = 'a.created_user_id'; + $fields['ordering'] = 'a.lft'; + $fields['level'] = 'a.level'; + $fields['catid'] = ''; + $fields['state'] = 'a.published'; + + $support['state'] = true; + $support['acl'] = true; + $support['checkout'] = true; + $support['level'] = true; + + $tables = array( + 'a' => '#__categories' + ); + + $title = 'category'; + break; + } + } + + return array( + 'fields' => $fields, + 'support' => $support, + 'tables' => $tables, + 'joins' => $joins, + 'title' => $title + ); + } } diff --git a/administrator/components/com_content/src/Helper/ContentHelper.php b/administrator/components/com_content/src/Helper/ContentHelper.php index 3d26fc915fc6d..dae43a804f228 100644 --- a/administrator/components/com_content/src/Helper/ContentHelper.php +++ b/administrator/components/com_content/src/Helper/ContentHelper.php @@ -1,4 +1,5 @@ getQuery(true); - - $query->select('id') - ->from($db->quoteName('#__content')) - ->where($db->quoteName('state') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $states = $db->loadResult(); - - return empty($states); - } - - /** - * Method to filter transitions by given id of state - * - * @param array $transitions Array of transitions - * @param int $pk Id of state - * @param int $workflowId Id of the workflow - * - * @return array - * - * @since 4.0.0 - */ - public static function filterTransitions(array $transitions, int $pk, int $workflowId = 0): array - { - return array_values( - array_filter( - $transitions, - function ($var) use ($pk, $workflowId) - { - return in_array($var['from_stage_id'], [-1, $pk]) && $workflowId == $var['workflow_id']; - } - ) - ); - } - - /** - * Prepares a form - * - * @param Form $form The form to change - * @param array|object $data The form data - * - * @return void - */ - public static function onPrepareForm(Form $form, $data) - { - if ($form->getName() != 'com_categories.categorycom_content') - { - return; - } - - $db = Factory::getDbo(); - - $data = (array) $data; - - // Make workflows translatable - Factory::getLanguage()->load('com_workflow', JPATH_ADMINISTRATOR); - - $form->setFieldAttribute('workflow_id', 'default', 'inherit'); - - $component = Factory::getApplication()->bootComponent('com_content'); - - if (!$component instanceof WorkflowServiceInterface - || !$component->isWorkflowActive('com_content.article')) - { - $form->removeField('workflow_id', 'params'); - - return; - } - - $query = $db->getQuery(true); - - $query->select($db->quoteName('title')) - ->from($db->quoteName('#__workflows')) - ->where( - [ - $db->quoteName('default') . ' = 1', - $db->quoteName('published') . ' = 1', - $db->quoteName('extension') . ' = ' . $db->quote('com_content.article'), - ] - ); - - $defaulttitle = $db->setQuery($query)->loadResult(); - - $option = Text::_('COM_WORKFLOW_INHERIT_WORKFLOW_NEW'); - - if (!empty($data['id'])) - { - $category = new Category($db); - - $categories = $category->getPath((int) $data['id']); - - // Remove the current category, because we search for inheritance from parent. - array_pop($categories); - - $option = Text::sprintf('COM_WORKFLOW_INHERIT_WORKFLOW', Text::_($defaulttitle)); - - if (!empty($categories)) - { - $categories = array_reverse($categories); - - $query = $db->getQuery(true); - - $query->select($db->quoteName('title')) - ->from($db->quoteName('#__workflows')) - ->where( - [ - $db->quoteName('id') . ' = :workflowId', - $db->quoteName('published') . ' = 1', - $db->quoteName('extension') . ' = ' . $db->quote('com_content.article'), - ] - ) - ->bind(':workflowId', $workflow_id, ParameterType::INTEGER); - - $db->setQuery($query); - - foreach ($categories as $cat) - { - $cat->params = new Registry($cat->params); - - $workflow_id = $cat->params->get('workflow_id'); - - if ($workflow_id == 'inherit') - { - continue; - } - elseif ($workflow_id == 'use_default') - { - break; - } - elseif ($workflow_id = (int) $workflow_id) - { - $title = $db->loadResult(); - - if (!is_null($title)) - { - $option = Text::sprintf('COM_WORKFLOW_INHERIT_WORKFLOW', Text::_($title)); - - break; - } - } - } - } - } + /** + * Check if state can be deleted + * + * @param int $id Id of state to delete + * + * @return boolean + * + * @since 4.0.0 + */ + public static function canDeleteState(int $id): bool + { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + $query->select('id') + ->from($db->quoteName('#__content')) + ->where($db->quoteName('state') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $states = $db->loadResult(); + + return empty($states); + } + + /** + * Method to filter transitions by given id of state + * + * @param array $transitions Array of transitions + * @param int $pk Id of state + * @param int $workflowId Id of the workflow + * + * @return array + * + * @since 4.0.0 + */ + public static function filterTransitions(array $transitions, int $pk, int $workflowId = 0): array + { + return array_values( + array_filter( + $transitions, + function ($var) use ($pk, $workflowId) { + return in_array($var['from_stage_id'], [-1, $pk]) && $workflowId == $var['workflow_id']; + } + ) + ); + } + + /** + * Prepares a form + * + * @param Form $form The form to change + * @param array|object $data The form data + * + * @return void + */ + public static function onPrepareForm(Form $form, $data) + { + if ($form->getName() != 'com_categories.categorycom_content') { + return; + } + + $db = Factory::getDbo(); + + $data = (array) $data; + + // Make workflows translatable + Factory::getLanguage()->load('com_workflow', JPATH_ADMINISTRATOR); + + $form->setFieldAttribute('workflow_id', 'default', 'inherit'); + + $component = Factory::getApplication()->bootComponent('com_content'); + + if ( + !$component instanceof WorkflowServiceInterface + || !$component->isWorkflowActive('com_content.article') + ) { + $form->removeField('workflow_id', 'params'); + + return; + } + + $query = $db->getQuery(true); + + $query->select($db->quoteName('title')) + ->from($db->quoteName('#__workflows')) + ->where( + [ + $db->quoteName('default') . ' = 1', + $db->quoteName('published') . ' = 1', + $db->quoteName('extension') . ' = ' . $db->quote('com_content.article'), + ] + ); + + $defaulttitle = $db->setQuery($query)->loadResult(); + + $option = Text::_('COM_WORKFLOW_INHERIT_WORKFLOW_NEW'); + + if (!empty($data['id'])) { + $category = new Category($db); + + $categories = $category->getPath((int) $data['id']); + + // Remove the current category, because we search for inheritance from parent. + array_pop($categories); + + $option = Text::sprintf('COM_WORKFLOW_INHERIT_WORKFLOW', Text::_($defaulttitle)); + + if (!empty($categories)) { + $categories = array_reverse($categories); + + $query = $db->getQuery(true); + + $query->select($db->quoteName('title')) + ->from($db->quoteName('#__workflows')) + ->where( + [ + $db->quoteName('id') . ' = :workflowId', + $db->quoteName('published') . ' = 1', + $db->quoteName('extension') . ' = ' . $db->quote('com_content.article'), + ] + ) + ->bind(':workflowId', $workflow_id, ParameterType::INTEGER); + + $db->setQuery($query); + + foreach ($categories as $cat) { + $cat->params = new Registry($cat->params); + + $workflow_id = $cat->params->get('workflow_id'); + + if ($workflow_id == 'inherit') { + continue; + } elseif ($workflow_id == 'use_default') { + break; + } elseif ($workflow_id = (int) $workflow_id) { + $title = $db->loadResult(); + + if (!is_null($title)) { + $option = Text::sprintf('COM_WORKFLOW_INHERIT_WORKFLOW', Text::_($title)); + + break; + } + } + } + } + } - $field = $form->getField('workflow_id', 'params'); + $field = $form->getField('workflow_id', 'params'); - $field->addOption($option, ['value' => 'inherit']); + $field->addOption($option, ['value' => 'inherit']); - $field->addOption(Text::sprintf('COM_WORKFLOW_USE_DEFAULT_WORKFLOW', Text::_($defaulttitle)), ['value' => 'use_default']); + $field->addOption(Text::sprintf('COM_WORKFLOW_USE_DEFAULT_WORKFLOW', Text::_($defaulttitle)), ['value' => 'use_default']); - $field->addOption('- ' . Text::_('COM_CONTENT_WORKFLOWS') . ' -', ['disabled' => 'true']); - } + $field->addOption('- ' . Text::_('COM_CONTENT_WORKFLOWS') . ' -', ['disabled' => 'true']); + } } diff --git a/administrator/components/com_content/src/Model/ArticleModel.php b/administrator/components/com_content/src/Model/ArticleModel.php index 99982f0875fd7..e47de4929cc27 100644 --- a/administrator/components/com_content/src/Model/ArticleModel.php +++ b/administrator/components/com_content/src/Model/ArticleModel.php @@ -1,4 +1,5 @@ 'content'], - $config['events_map'] - ); - - parent::__construct($config, $factory, $formFactory); - - // Set the featured status change events - $this->event_before_change_featured = $config['event_before_change_featured'] ?? $this->event_before_change_featured; - $this->event_before_change_featured = $this->event_before_change_featured ?? 'onContentBeforeChangeFeatured'; - $this->event_after_change_featured = $config['event_after_change_featured'] ?? $this->event_after_change_featured; - $this->event_after_change_featured = $this->event_after_change_featured ?? 'onContentAfterChangeFeatured'; - - $this->setUpWorkflow('com_content.article'); - } - - /** - * Function that can be overridden to do any data cleanup after batch copying data - * - * @param TableInterface $table The table object containing the newly created item - * @param integer $newId The id of the new item - * @param integer $oldId The original item id - * - * @return void - * - * @since 3.8.12 - */ - protected function cleanupPostBatchCopy(TableInterface $table, $newId, $oldId) - { - // Check if the article was featured and update the #__content_frontpage table - if ($table->featured == 1) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('featured_up'), - $db->quoteName('featured_down'), - ] - ) - ->from($db->quoteName('#__content_frontpage')) - ->where($db->quoteName('content_id') . ' = :oldId') - ->bind(':oldId', $oldId, ParameterType::INTEGER); - - $featured = $db->setQuery($query)->loadObject(); - - if ($featured) - { - $query = $db->getQuery(true) - ->insert($db->quoteName('#__content_frontpage')) - ->values(':newId, 0, :featuredUp, :featuredDown') - ->bind(':newId', $newId, ParameterType::INTEGER) - ->bind(':featuredUp', $featured->featured_up, $featured->featured_up ? ParameterType::STRING : ParameterType::NULL) - ->bind(':featuredDown', $featured->featured_down, $featured->featured_down ? ParameterType::STRING : ParameterType::NULL); - - $db->setQuery($query); - $db->execute(); - } - } - - $this->workflowCleanupBatchMove($oldId, $newId); - - $oldItem = $this->getTable(); - $oldItem->load($oldId); - $fields = FieldsHelper::getFields('com_content.article', $oldItem, true); - - $fieldsData = array(); - - if (!empty($fields)) - { - $fieldsData['com_fields'] = array(); - - foreach ($fields as $field) - { - $fieldsData['com_fields'][$field->name] = $field->rawvalue; - } - } - - Factory::getApplication()->triggerEvent('onContentAfterSave', array('com_content.article', &$this->table, false, $fieldsData)); - } - - /** - * Batch move categories to a new category. - * - * @param integer $value The new category ID. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True on success. - * - * @since 3.8.6 - */ - protected function batchMove($value, $pks, $contexts) - { - if (empty($this->batchSet)) - { - // Set some needed variables. - $this->user = Factory::getUser(); - $this->table = $this->getTable(); - $this->tableClassName = get_class($this->table); - $this->contentType = new UCMType; - $this->type = $this->contentType->getTypeByTable($this->tableClassName); - } - - $categoryId = (int) $value; - - if (!$this->checkCategoryId($categoryId)) - { - return false; - } - - PluginHelper::importPlugin('system'); - - // Parent exists so we proceed - foreach ($pks as $pk) - { - if (!$this->user->authorise('core.edit', $contexts[$pk])) - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); - - return false; - } - - // Check that the row actually exists - if (!$this->table->load($pk)) - { - if ($error = $this->table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Not fatal error - $this->setError(Text::sprintf('JLIB_APPLICATION_ERROR_BATCH_MOVE_ROW_NOT_FOUND', $pk)); - continue; - } - } - - $fields = FieldsHelper::getFields('com_content.article', $this->table, true); - - $fieldsData = array(); - - if (!empty($fields)) - { - $fieldsData['com_fields'] = array(); - - foreach ($fields as $field) - { - $fieldsData['com_fields'][$field->name] = $field->rawvalue; - } - } - - // Set the new category ID - $this->table->catid = $categoryId; - - // We don't want to modify tags - so remove the associated tags helper - if ($this->table instanceof TaggableTableInterface) - { - $this->table->clearTagsHelper(); - } - - // Check the row. - if (!$this->table->check()) - { - $this->setError($this->table->getError()); - - return false; - } - - // Store the row. - if (!$this->table->store()) - { - $this->setError($this->table->getError()); - - return false; - } - - // Run event for moved article - Factory::getApplication()->triggerEvent('onContentAfterSave', array('com_content.article', &$this->table, false, $fieldsData)); - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canDelete($record) - { - if (empty($record->id) || ($record->state != -2)) - { - return false; - } - - return Factory::getUser()->authorise('core.delete', 'com_content.article.' . (int) $record->id); - } - - /** - * Method to test whether a record can have its state edited. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - - // Check for existing article. - if (!empty($record->id)) - { - return $user->authorise('core.edit.state', 'com_content.article.' . (int) $record->id); - } - - // New article, so check against the category. - if (!empty($record->catid)) - { - return $user->authorise('core.edit.state', 'com_content.category.' . (int) $record->catid); - } - - // Default to component settings if neither article nor category known. - return parent::canEditState($record); - } - - /** - * Prepare and sanitise the table data prior to saving. - * - * @param \Joomla\CMS\Table\Table $table A Table object. - * - * @return void - * - * @since 1.6 - */ - protected function prepareTable($table) - { - // Set the publish date to now - if ($table->state == Workflow::CONDITION_PUBLISHED && (int) $table->publish_up == 0) - { - $table->publish_up = Factory::getDate()->toSql(); - } - - if ($table->state == Workflow::CONDITION_PUBLISHED && intval($table->publish_down) == 0) - { - $table->publish_down = null; - } - - // Increment the content version number. - $table->version++; - - // Reorder the articles within the category so the new article is first - if (empty($table->id)) - { - $table->reorder('catid = ' . (int) $table->catid . ' AND state >= 0'); - } - } - - /** - * Method to change the published state of one or more records. - * - * @param array &$pks A list of the primary keys to change. - * @param integer $value The value of the published state. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function publish(&$pks, $value = 1) - { - $this->workflowBeforeStageChange(); - - return parent::publish($pks, $value); - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - */ - public function getItem($pk = null) - { - if ($item = parent::getItem($pk)) - { - // Convert the params field to an array. - $registry = new Registry($item->attribs); - $item->attribs = $registry->toArray(); - - // Convert the metadata field to an array. - $registry = new Registry($item->metadata); - $item->metadata = $registry->toArray(); - - // Convert the images field to an array. - $registry = new Registry($item->images); - $item->images = $registry->toArray(); - - // Convert the urls field to an array. - $registry = new Registry($item->urls); - $item->urls = $registry->toArray(); - - $item->articletext = ($item->fulltext !== null && trim($item->fulltext) != '') ? $item->introtext . '
' . $item->fulltext : $item->introtext; - - if (!empty($item->id)) - { - $item->tags = new TagsHelper; - $item->tags->getTagIds($item->id, 'com_content.article'); - - $item->featured_up = null; - $item->featured_down = null; - - if ($item->featured) - { - // Get featured dates. - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('featured_up'), - $db->quoteName('featured_down'), - ] - ) - ->from($db->quoteName('#__content_frontpage')) - ->where($db->quoteName('content_id') . ' = :id') - ->bind(':id', $item->id, ParameterType::INTEGER); - - $featured = $db->setQuery($query)->loadObject(); - - if ($featured) - { - $item->featured_up = $featured->featured_up; - $item->featured_down = $featured->featured_down; - } - } - } - } - - // Load associated content items - $assoc = Associations::isEnabled(); - - if ($assoc) - { - $item->associations = array(); - - if ($item->id != null) - { - $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', $item->id); - - foreach ($associations as $tag => $association) - { - $item->associations[$tag] = $association->id; - } - } - } - - return $item; - } - - /** - * Method to get the record form. - * - * @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 Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - $app = Factory::getApplication(); - - // Get the form. - $form = $this->loadForm('com_content.article', 'article', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Object uses for checking edit state permission of article - $record = new \stdClass; - - // Get ID of the article from input, for frontend, we use a_id while backend uses id - $articleIdFromInput = $app->isClient('site') - ? $app->input->getInt('a_id', 0) - : $app->input->getInt('id', 0); - - // On edit article, we get ID of article from article.id state, but on save, we use data from input - $id = (int) $this->getState('article.id', $articleIdFromInput); - - $record->id = $id; - - // For new articles we load the potential state + associations - if ($id == 0 && $formField = $form->getField('catid')) - { - $assignedCatids = $data['catid'] ?? $form->getValue('catid'); - - $assignedCatids = is_array($assignedCatids) - ? (int) reset($assignedCatids) - : (int) $assignedCatids; - - // Try to get the category from the category field - if (empty($assignedCatids)) - { - $assignedCatids = $formField->getAttribute('default', null); - - if (!$assignedCatids) - { - // Choose the first category available - $catOptions = $formField->options; - - if ($catOptions && !empty($catOptions[0]->value)) - { - $assignedCatids = (int) $catOptions[0]->value; - } - } - } - - // Activate the reload of the form when category is changed - $form->setFieldAttribute('catid', 'refresh-enabled', true); - $form->setFieldAttribute('catid', 'refresh-cat-id', $assignedCatids); - $form->setFieldAttribute('catid', 'refresh-section', 'article'); - - // Store ID of the category uses for edit state permission check - $record->catid = $assignedCatids; - } - else - { - // Get the category which the article is being added to - if (!empty($data['catid'])) - { - $catId = (int) $data['catid']; - } - else - { - $catIds = $form->getValue('catid'); - - $catId = is_array($catIds) - ? (int) reset($catIds) - : (int) $catIds; - - if (!$catId) - { - $catId = (int) $form->getFieldAttribute('catid', 'default', 0); - } - } - - $record->catid = $catId; - } - - // Modify the form based on Edit State access controls. - if (!$this->canEditState($record)) - { - // Disable fields for display. - $form->setFieldAttribute('featured', 'disabled', 'true'); - $form->setFieldAttribute('featured_up', 'disabled', 'true'); - $form->setFieldAttribute('featured_down', 'disabled', 'true'); - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('publish_up', 'disabled', 'true'); - $form->setFieldAttribute('publish_down', 'disabled', 'true'); - $form->setFieldAttribute('state', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is an article you can edit. - $form->setFieldAttribute('featured', 'filter', 'unset'); - $form->setFieldAttribute('featured_up', 'filter', 'unset'); - $form->setFieldAttribute('featured_down', 'filter', 'unset'); - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('publish_up', 'filter', 'unset'); - $form->setFieldAttribute('publish_down', 'filter', 'unset'); - $form->setFieldAttribute('state', 'filter', 'unset'); - } - - // Don't allow to change the created_by user if not allowed to access com_users. - if (!Factory::getUser()->authorise('core.manage', 'com_users')) - { - $form->setFieldAttribute('created_by', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $app = Factory::getApplication(); - $data = $app->getUserState('com_content.edit.article.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Pre-select some filters (Status, Category, Language, Access) in edit form if those have been selected in Article Manager: Articles - if ($this->getState('article.id') == 0) - { - $filters = (array) $app->getUserState('com_content.articles.filter'); - $data->set( - 'state', - $app->input->getInt( - 'state', - ((isset($filters['published']) && $filters['published'] !== '') ? $filters['published'] : null) - ) - ); - $data->set('catid', $app->input->getInt('catid', (!empty($filters['category_id']) ? $filters['category_id'] : null))); - - if ($app->isClient('administrator')) - { - $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); - } - - $data->set('access', - $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) - ); - } - } - - // If there are params fieldsets in the form it will fail with a registry object - if (isset($data->params) && $data->params instanceof Registry) - { - $data->params = $data->params->toArray(); - } - - $this->preprocessData('com_content.article', $data); - - return $data; - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @see \Joomla\CMS\Form\FormRule - * @see JFilterInput - * @since 3.7.0 - */ - public function validate($form, $data, $group = null) - { - if (!Factory::getUser()->authorise('core.admin', 'com_content')) - { - if (isset($data['rules'])) - { - unset($data['rules']); - } - } - - return parent::validate($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $app = Factory::getApplication(); - $input = $app->input; - $filter = InputFilter::getInstance(); - - if (isset($data['metadata']) && isset($data['metadata']['author'])) - { - $data['metadata']['author'] = $filter->clean($data['metadata']['author'], 'TRIM'); - } - - if (isset($data['created_by_alias'])) - { - $data['created_by_alias'] = $filter->clean($data['created_by_alias'], 'TRIM'); - } - - if (isset($data['images']) && is_array($data['images'])) - { - $registry = new Registry($data['images']); - - $data['images'] = (string) $registry; - } - - $this->workflowBeforeSave(); - - // Create new category, if needed. - $createCategory = true; - - if (is_null($data['catid'])) - { - // When there is no catid passed don't try to create one - $createCategory = false; - } - - // If category ID is provided, check if it's valid. - if (is_numeric($data['catid']) && $data['catid']) - { - $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_content'); - } - - // Save New Category - if ($createCategory && $this->canCreateCategory()) - { - $category = [ - // Remove #new# prefix, if exists. - 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], - 'parent_id' => 1, - 'extension' => 'com_content', - 'language' => $data['language'], - 'published' => 1, - ]; - - /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ - $categoryModel = Factory::getApplication()->bootComponent('com_categories') - ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); - - // Create new category. - if (!$categoryModel->save($category)) - { - $this->setError($categoryModel->getError()); - - return false; - } - - // Get the Category ID. - $data['catid'] = $categoryModel->getState('category.id'); - } - - if (isset($data['urls']) && is_array($data['urls'])) - { - $check = $input->post->get('jform', array(), 'array'); - - foreach ($data['urls'] as $i => $url) - { - if ($url != false && ($i == 'urla' || $i == 'urlb' || $i == 'urlc')) - { - if (preg_match('~^#[a-zA-Z]{1}[a-zA-Z0-9-_:.]*$~', $check['urls'][$i]) == 1) - { - $data['urls'][$i] = $check['urls'][$i]; - } - else - { - $data['urls'][$i] = PunycodeHelper::urlToPunycode($url); - } - } - } - - unset($check); - - $registry = new Registry($data['urls']); - - $data['urls'] = (string) $registry; - } - - // Alter the title for save as copy - if ($input->get('task') == 'save2copy') - { - $origTable = $this->getTable(); - - if ($app->isClient('site')) - { - $origTable->load($input->getInt('a_id')); - - if ($origTable->title === $data['title']) - { - /** - * If title of article is not changed, set alias to original article alias so that Joomla! will generate - * new Title and Alias for the copied article - */ - $data['alias'] = $origTable->alias; - } - else - { - $data['alias'] = ''; - } - } - else - { - $origTable->load($input->getInt('id')); - } - - if ($data['title'] == $origTable->title) - { - list($title, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['title']); - $data['title'] = $title; - $data['alias'] = $alias; - } - elseif ($data['alias'] == $origTable->alias) - { - $data['alias'] = ''; - } - } - - // Automatic handling of alias for empty fields - if (in_array($input->get('task'), array('apply', 'save', 'save2new')) && (!isset($data['id']) || (int) $data['id'] == 0)) - { - if ($data['alias'] == null) - { - if ($app->get('unicodeslugs') == 1) - { - $data['alias'] = OutputFilter::stringUrlUnicodeSlug($data['title']); - } - else - { - $data['alias'] = OutputFilter::stringURLSafe($data['title']); - } - - $table = $this->getTable(); - - if ($table->load(array('alias' => $data['alias'], 'catid' => $data['catid']))) - { - $msg = Text::_('COM_CONTENT_SAVE_WARNING'); - } - - list($title, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['title']); - $data['alias'] = $alias; - - if (isset($msg)) - { - $app->enqueueMessage($msg, 'warning'); - } - } - } - - if (parent::save($data)) - { - // Check if featured is set and if not managed by workflow - if (isset($data['featured']) && !$this->bootComponent('com_content')->isFunctionalityUsed('core.featured', 'com_content.article')) - { - if (!$this->featured( - $this->getState($this->getName() . '.id'), - $data['featured'], - $data['featured_up'] ?? null, - $data['featured_down'] ?? null - )) - { - return false; - } - } - - $this->workflowAfterSave($data); - - return true; - } - - return false; - } - - /** - * Method to toggle the featured setting of articles. - * - * @param array $pks The ids of the items to toggle. - * @param integer $value The value to toggle to. - * @param string|Date $featuredUp The date which item featured up. - * @param string|Date $featuredDown The date which item featured down. - * - * @return boolean True on success. - */ - public function featured($pks, $value = 0, $featuredUp = null, $featuredDown = null) - { - // Sanitize the ids. - $pks = (array) $pks; - $pks = ArrayHelper::toInteger($pks); - $value = (int) $value; - $context = $this->option . '.' . $this->name; - - $this->workflowBeforeStageChange(); - - // Include the plugins for the change of state event. - PluginHelper::importPlugin($this->events_map['featured']); - - // Convert empty strings to null for the query. - if ($featuredUp === '') - { - $featuredUp = null; - } - - if ($featuredDown === '') - { - $featuredDown = null; - } - - if (empty($pks)) - { - $this->setError(Text::_('COM_CONTENT_NO_ITEM_SELECTED')); - - return false; - } - - $table = $this->getTable('Featured', 'Administrator'); - - // Trigger the before change state event. - $eventResult = Factory::getApplication()->getDispatcher()->dispatch( - $this->event_before_change_featured, - AbstractEvent::create( - $this->event_before_change_featured, - [ - 'eventClass' => 'Joomla\Component\Content\Administrator\Event\Model\FeatureEvent', - 'subject' => $this, - 'extension' => $context, - 'pks' => $pks, - 'value' => $value, - ] - ) - ); - - if ($eventResult->getArgument('abort', false)) - { - $this->setError(Text::_($eventResult->getArgument('abortReason'))); - - return false; - } - - try - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->update($db->quoteName('#__content')) - ->set($db->quoteName('featured') . ' = :featured') - ->whereIn($db->quoteName('id'), $pks) - ->bind(':featured', $value, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - if ($value === 0) - { - // Adjust the mapping table. - // Clear the existing features settings. - $query = $db->getQuery(true) - ->delete($db->quoteName('#__content_frontpage')) - ->whereIn($db->quoteName('content_id'), $pks); - $db->setQuery($query); - $db->execute(); - } - else - { - // First, we find out which of our new featured articles are already featured. - $query = $db->getQuery(true) - ->select($db->quoteName('content_id')) - ->from($db->quoteName('#__content_frontpage')) - ->whereIn($db->quoteName('content_id'), $pks); - $db->setQuery($query); - - $oldFeatured = $db->loadColumn(); - - // Update old featured articles - if (count($oldFeatured)) - { - $query = $db->getQuery(true) - ->update($db->quoteName('#__content_frontpage')) - ->set( - [ - $db->quoteName('featured_up') . ' = :featuredUp', - $db->quoteName('featured_down') . ' = :featuredDown', - ] - ) - ->whereIn($db->quoteName('content_id'), $oldFeatured) - ->bind(':featuredUp', $featuredUp, $featuredUp ? ParameterType::STRING : ParameterType::NULL) - ->bind(':featuredDown', $featuredDown, $featuredDown ? ParameterType::STRING : ParameterType::NULL); - $db->setQuery($query); - $db->execute(); - } - - // We diff the arrays to get a list of the articles that are newly featured - $newFeatured = array_diff($pks, $oldFeatured); - - // Featuring. - if ($newFeatured) - { - $query = $db->getQuery(true) - ->insert($db->quoteName('#__content_frontpage')) - ->columns( - [ - $db->quoteName('content_id'), - $db->quoteName('ordering'), - $db->quoteName('featured_up'), - $db->quoteName('featured_down'), - ] - ); - - $dataTypes = [ - ParameterType::INTEGER, - ParameterType::INTEGER, - $featuredUp ? ParameterType::STRING : ParameterType::NULL, - $featuredDown ? ParameterType::STRING : ParameterType::NULL, - ]; - - foreach ($newFeatured as $pk) - { - $query->values(implode(',', $query->bindArray([$pk, 0, $featuredUp, $featuredDown], $dataTypes))); - } - - $db->setQuery($query); - $db->execute(); - } - } - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - $table->reorder(); - - // Trigger the change state event. - Factory::getApplication()->getDispatcher()->dispatch( - $this->event_after_change_featured, - AbstractEvent::create( - $this->event_after_change_featured, - [ - 'eventClass' => 'Joomla\Component\Content\Administrator\Event\Model\FeatureEvent', - 'subject' => $this, - 'extension' => $context, - 'pks' => $pks, - 'value' => $value, - ] - ) - ); - - $this->cleanCache(); - - return true; - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param object $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - return [ - $this->getDatabase()->quoteName('catid') . ' = ' . (int) $table->catid, - ]; - } - - /** - * Allows preprocessing of the Form object. - * - * @param Form $form The form object - * @param array $data The data to be merged into the form object - * @param string $group The plugin group to be executed - * - * @return void - * - * @since 3.0 - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - if ($this->canCreateCategory()) - { - $form->setFieldAttribute('catid', 'allowAdd', 'true'); - - // Add a prefix for categories created on the fly. - $form->setFieldAttribute('catid', 'customPrefix', '#new#'); - } - - // Association content items - if (Associations::isEnabled()) - { - $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); - - if (count($languages) > 1) - { - $addform = new \SimpleXMLElement('
'); - $fields = $addform->addChild('fields'); - $fields->addAttribute('name', 'associations'); - $fieldset = $fields->addChild('fieldset'); - $fieldset->addAttribute('name', 'item_associations'); - - foreach ($languages as $language) - { - $field = $fieldset->addChild('field'); - $field->addAttribute('name', $language->lang_code); - $field->addAttribute('type', 'modal_article'); - $field->addAttribute('language', $language->lang_code); - $field->addAttribute('label', $language->title); - $field->addAttribute('translate_label', 'false'); - $field->addAttribute('select', 'true'); - $field->addAttribute('new', 'true'); - $field->addAttribute('edit', 'true'); - $field->addAttribute('clear', 'true'); - $field->addAttribute('propagate', 'true'); - } - - $form->load($addform, false); - } - } - - $this->workflowPreprocessForm($form, $data); - - parent::preprocessForm($form, $data, $group); - } - - /** - * Custom clean the cache of com_content and content modules - * - * @param string $group The cache group - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('com_content'); - parent::cleanCache('mod_articles_archive'); - parent::cleanCache('mod_articles_categories'); - parent::cleanCache('mod_articles_category'); - parent::cleanCache('mod_articles_latest'); - parent::cleanCache('mod_articles_news'); - parent::cleanCache('mod_articles_popular'); - } - - /** - * Void hit function for pagebreak when editing content from frontend - * - * @return void - * - * @since 3.6.0 - */ - public function hit() - { - } - - /** - * Is the user allowed to create an on the fly category? - * - * @return boolean - * - * @since 3.6.1 - */ - private function canCreateCategory() - { - return Factory::getUser()->authorise('core.create', 'com_content'); - } - - /** - * Delete #__content_frontpage items if the deleted articles was featured - * - * @param object $pks The primary key related to the contents that was deleted. - * - * @return boolean - * - * @since 3.7.0 - */ - public function delete(&$pks) - { - $return = parent::delete($pks); - - if ($return) - { - // Now check to see if this articles was featured if so delete it from the #__content_frontpage table - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__content_frontpage')) - ->whereIn($db->quoteName('content_id'), $pks); - $db->setQuery($query); - $db->execute(); - - $this->workflow->deleteAssociation($pks); - } - - return $return; - } + use WorkflowBehaviorTrait; + use VersionableModelTrait; + + /** + * The prefix to use with controller messages. + * + * @var string + * @since 1.6 + */ + protected $text_prefix = 'COM_CONTENT'; + + /** + * The type alias for this content type (for example, 'com_content.article'). + * + * @var string + * @since 3.2 + */ + public $typeAlias = 'com_content.article'; + + /** + * The context used for the associations table + * + * @var string + * @since 3.4.4 + */ + protected $associationsContext = 'com_content.item'; + + /** + * The event to trigger before changing featured status one or more items. + * + * @var string + * @since 4.0.0 + */ + protected $event_before_change_featured = null; + + /** + * The event to trigger after changing featured status one or more items. + * + * @var string + * @since 4.0.0 + */ + protected $event_after_change_featured = null; + + /** + * Constructor. + * + * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). + * @param MVCFactoryInterface $factory The factory. + * @param FormFactoryInterface $formFactory The form factory. + * + * @since 1.6 + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, FormFactoryInterface $formFactory = null) + { + $config['events_map'] = $config['events_map'] ?? []; + + $config['events_map'] = array_merge( + ['featured' => 'content'], + $config['events_map'] + ); + + parent::__construct($config, $factory, $formFactory); + + // Set the featured status change events + $this->event_before_change_featured = $config['event_before_change_featured'] ?? $this->event_before_change_featured; + $this->event_before_change_featured = $this->event_before_change_featured ?? 'onContentBeforeChangeFeatured'; + $this->event_after_change_featured = $config['event_after_change_featured'] ?? $this->event_after_change_featured; + $this->event_after_change_featured = $this->event_after_change_featured ?? 'onContentAfterChangeFeatured'; + + $this->setUpWorkflow('com_content.article'); + } + + /** + * Function that can be overridden to do any data cleanup after batch copying data + * + * @param TableInterface $table The table object containing the newly created item + * @param integer $newId The id of the new item + * @param integer $oldId The original item id + * + * @return void + * + * @since 3.8.12 + */ + protected function cleanupPostBatchCopy(TableInterface $table, $newId, $oldId) + { + // Check if the article was featured and update the #__content_frontpage table + if ($table->featured == 1) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('featured_up'), + $db->quoteName('featured_down'), + ] + ) + ->from($db->quoteName('#__content_frontpage')) + ->where($db->quoteName('content_id') . ' = :oldId') + ->bind(':oldId', $oldId, ParameterType::INTEGER); + + $featured = $db->setQuery($query)->loadObject(); + + if ($featured) { + $query = $db->getQuery(true) + ->insert($db->quoteName('#__content_frontpage')) + ->values(':newId, 0, :featuredUp, :featuredDown') + ->bind(':newId', $newId, ParameterType::INTEGER) + ->bind(':featuredUp', $featured->featured_up, $featured->featured_up ? ParameterType::STRING : ParameterType::NULL) + ->bind(':featuredDown', $featured->featured_down, $featured->featured_down ? ParameterType::STRING : ParameterType::NULL); + + $db->setQuery($query); + $db->execute(); + } + } + + $this->workflowCleanupBatchMove($oldId, $newId); + + $oldItem = $this->getTable(); + $oldItem->load($oldId); + $fields = FieldsHelper::getFields('com_content.article', $oldItem, true); + + $fieldsData = array(); + + if (!empty($fields)) { + $fieldsData['com_fields'] = array(); + + foreach ($fields as $field) { + $fieldsData['com_fields'][$field->name] = $field->rawvalue; + } + } + + Factory::getApplication()->triggerEvent('onContentAfterSave', array('com_content.article', &$this->table, false, $fieldsData)); + } + + /** + * Batch move categories to a new category. + * + * @param integer $value The new category ID. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True on success. + * + * @since 3.8.6 + */ + protected function batchMove($value, $pks, $contexts) + { + if (empty($this->batchSet)) { + // Set some needed variables. + $this->user = Factory::getUser(); + $this->table = $this->getTable(); + $this->tableClassName = get_class($this->table); + $this->contentType = new UCMType(); + $this->type = $this->contentType->getTypeByTable($this->tableClassName); + } + + $categoryId = (int) $value; + + if (!$this->checkCategoryId($categoryId)) { + return false; + } + + PluginHelper::importPlugin('system'); + + // Parent exists so we proceed + foreach ($pks as $pk) { + if (!$this->user->authorise('core.edit', $contexts[$pk])) { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); + + return false; + } + + // Check that the row actually exists + if (!$this->table->load($pk)) { + if ($error = $this->table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Not fatal error + $this->setError(Text::sprintf('JLIB_APPLICATION_ERROR_BATCH_MOVE_ROW_NOT_FOUND', $pk)); + continue; + } + } + + $fields = FieldsHelper::getFields('com_content.article', $this->table, true); + + $fieldsData = array(); + + if (!empty($fields)) { + $fieldsData['com_fields'] = array(); + + foreach ($fields as $field) { + $fieldsData['com_fields'][$field->name] = $field->rawvalue; + } + } + + // Set the new category ID + $this->table->catid = $categoryId; + + // We don't want to modify tags - so remove the associated tags helper + if ($this->table instanceof TaggableTableInterface) { + $this->table->clearTagsHelper(); + } + + // Check the row. + if (!$this->table->check()) { + $this->setError($this->table->getError()); + + return false; + } + + // Store the row. + if (!$this->table->store()) { + $this->setError($this->table->getError()); + + return false; + } + + // Run event for moved article + Factory::getApplication()->triggerEvent('onContentAfterSave', array('com_content.article', &$this->table, false, $fieldsData)); + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if (empty($record->id) || ($record->state != -2)) { + return false; + } + + return Factory::getUser()->authorise('core.delete', 'com_content.article.' . (int) $record->id); + } + + /** + * Method to test whether a record can have its state edited. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + + // Check for existing article. + if (!empty($record->id)) { + return $user->authorise('core.edit.state', 'com_content.article.' . (int) $record->id); + } + + // New article, so check against the category. + if (!empty($record->catid)) { + return $user->authorise('core.edit.state', 'com_content.category.' . (int) $record->catid); + } + + // Default to component settings if neither article nor category known. + return parent::canEditState($record); + } + + /** + * Prepare and sanitise the table data prior to saving. + * + * @param \Joomla\CMS\Table\Table $table A Table object. + * + * @return void + * + * @since 1.6 + */ + protected function prepareTable($table) + { + // Set the publish date to now + if ($table->state == Workflow::CONDITION_PUBLISHED && (int) $table->publish_up == 0) { + $table->publish_up = Factory::getDate()->toSql(); + } + + if ($table->state == Workflow::CONDITION_PUBLISHED && intval($table->publish_down) == 0) { + $table->publish_down = null; + } + + // Increment the content version number. + $table->version++; + + // Reorder the articles within the category so the new article is first + if (empty($table->id)) { + $table->reorder('catid = ' . (int) $table->catid . ' AND state >= 0'); + } + } + + /** + * Method to change the published state of one or more records. + * + * @param array &$pks A list of the primary keys to change. + * @param integer $value The value of the published state. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function publish(&$pks, $value = 1) + { + $this->workflowBeforeStageChange(); + + return parent::publish($pks, $value); + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + */ + public function getItem($pk = null) + { + if ($item = parent::getItem($pk)) { + // Convert the params field to an array. + $registry = new Registry($item->attribs); + $item->attribs = $registry->toArray(); + + // Convert the metadata field to an array. + $registry = new Registry($item->metadata); + $item->metadata = $registry->toArray(); + + // Convert the images field to an array. + $registry = new Registry($item->images); + $item->images = $registry->toArray(); + + // Convert the urls field to an array. + $registry = new Registry($item->urls); + $item->urls = $registry->toArray(); + + $item->articletext = ($item->fulltext !== null && trim($item->fulltext) != '') ? $item->introtext . '
' . $item->fulltext : $item->introtext; + + if (!empty($item->id)) { + $item->tags = new TagsHelper(); + $item->tags->getTagIds($item->id, 'com_content.article'); + + $item->featured_up = null; + $item->featured_down = null; + + if ($item->featured) { + // Get featured dates. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('featured_up'), + $db->quoteName('featured_down'), + ] + ) + ->from($db->quoteName('#__content_frontpage')) + ->where($db->quoteName('content_id') . ' = :id') + ->bind(':id', $item->id, ParameterType::INTEGER); + + $featured = $db->setQuery($query)->loadObject(); + + if ($featured) { + $item->featured_up = $featured->featured_up; + $item->featured_down = $featured->featured_down; + } + } + } + } + + // Load associated content items + $assoc = Associations::isEnabled(); + + if ($assoc) { + $item->associations = array(); + + if ($item->id != null) { + $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', $item->id); + + foreach ($associations as $tag => $association) { + $item->associations[$tag] = $association->id; + } + } + } + + return $item; + } + + /** + * Method to get the record form. + * + * @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 Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + $app = Factory::getApplication(); + + // Get the form. + $form = $this->loadForm('com_content.article', 'article', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Object uses for checking edit state permission of article + $record = new \stdClass(); + + // Get ID of the article from input, for frontend, we use a_id while backend uses id + $articleIdFromInput = $app->isClient('site') + ? $app->input->getInt('a_id', 0) + : $app->input->getInt('id', 0); + + // On edit article, we get ID of article from article.id state, but on save, we use data from input + $id = (int) $this->getState('article.id', $articleIdFromInput); + + $record->id = $id; + + // For new articles we load the potential state + associations + if ($id == 0 && $formField = $form->getField('catid')) { + $assignedCatids = $data['catid'] ?? $form->getValue('catid'); + + $assignedCatids = is_array($assignedCatids) + ? (int) reset($assignedCatids) + : (int) $assignedCatids; + + // Try to get the category from the category field + if (empty($assignedCatids)) { + $assignedCatids = $formField->getAttribute('default', null); + + if (!$assignedCatids) { + // Choose the first category available + $catOptions = $formField->options; + + if ($catOptions && !empty($catOptions[0]->value)) { + $assignedCatids = (int) $catOptions[0]->value; + } + } + } + + // Activate the reload of the form when category is changed + $form->setFieldAttribute('catid', 'refresh-enabled', true); + $form->setFieldAttribute('catid', 'refresh-cat-id', $assignedCatids); + $form->setFieldAttribute('catid', 'refresh-section', 'article'); + + // Store ID of the category uses for edit state permission check + $record->catid = $assignedCatids; + } else { + // Get the category which the article is being added to + if (!empty($data['catid'])) { + $catId = (int) $data['catid']; + } else { + $catIds = $form->getValue('catid'); + + $catId = is_array($catIds) + ? (int) reset($catIds) + : (int) $catIds; + + if (!$catId) { + $catId = (int) $form->getFieldAttribute('catid', 'default', 0); + } + } + + $record->catid = $catId; + } + + // Modify the form based on Edit State access controls. + if (!$this->canEditState($record)) { + // Disable fields for display. + $form->setFieldAttribute('featured', 'disabled', 'true'); + $form->setFieldAttribute('featured_up', 'disabled', 'true'); + $form->setFieldAttribute('featured_down', 'disabled', 'true'); + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('publish_up', 'disabled', 'true'); + $form->setFieldAttribute('publish_down', 'disabled', 'true'); + $form->setFieldAttribute('state', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is an article you can edit. + $form->setFieldAttribute('featured', 'filter', 'unset'); + $form->setFieldAttribute('featured_up', 'filter', 'unset'); + $form->setFieldAttribute('featured_down', 'filter', 'unset'); + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('publish_up', 'filter', 'unset'); + $form->setFieldAttribute('publish_down', 'filter', 'unset'); + $form->setFieldAttribute('state', 'filter', 'unset'); + } + + // Don't allow to change the created_by user if not allowed to access com_users. + if (!Factory::getUser()->authorise('core.manage', 'com_users')) { + $form->setFieldAttribute('created_by', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $app = Factory::getApplication(); + $data = $app->getUserState('com_content.edit.article.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Pre-select some filters (Status, Category, Language, Access) in edit form if those have been selected in Article Manager: Articles + if ($this->getState('article.id') == 0) { + $filters = (array) $app->getUserState('com_content.articles.filter'); + $data->set( + 'state', + $app->input->getInt( + 'state', + ((isset($filters['published']) && $filters['published'] !== '') ? $filters['published'] : null) + ) + ); + $data->set('catid', $app->input->getInt('catid', (!empty($filters['category_id']) ? $filters['category_id'] : null))); + + if ($app->isClient('administrator')) { + $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); + } + + $data->set( + 'access', + $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) + ); + } + } + + // If there are params fieldsets in the form it will fail with a registry object + if (isset($data->params) && $data->params instanceof Registry) { + $data->params = $data->params->toArray(); + } + + $this->preprocessData('com_content.article', $data); + + return $data; + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @see \Joomla\CMS\Form\FormRule + * @see JFilterInput + * @since 3.7.0 + */ + public function validate($form, $data, $group = null) + { + if (!Factory::getUser()->authorise('core.admin', 'com_content')) { + if (isset($data['rules'])) { + unset($data['rules']); + } + } + + return parent::validate($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $app = Factory::getApplication(); + $input = $app->input; + $filter = InputFilter::getInstance(); + + if (isset($data['metadata']) && isset($data['metadata']['author'])) { + $data['metadata']['author'] = $filter->clean($data['metadata']['author'], 'TRIM'); + } + + if (isset($data['created_by_alias'])) { + $data['created_by_alias'] = $filter->clean($data['created_by_alias'], 'TRIM'); + } + + if (isset($data['images']) && is_array($data['images'])) { + $registry = new Registry($data['images']); + + $data['images'] = (string) $registry; + } + + $this->workflowBeforeSave(); + + // Create new category, if needed. + $createCategory = true; + + if (is_null($data['catid'])) { + // When there is no catid passed don't try to create one + $createCategory = false; + } + + // If category ID is provided, check if it's valid. + if (is_numeric($data['catid']) && $data['catid']) { + $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_content'); + } + + // Save New Category + if ($createCategory && $this->canCreateCategory()) { + $category = [ + // Remove #new# prefix, if exists. + 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], + 'parent_id' => 1, + 'extension' => 'com_content', + 'language' => $data['language'], + 'published' => 1, + ]; + + /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ + $categoryModel = Factory::getApplication()->bootComponent('com_categories') + ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); + + // Create new category. + if (!$categoryModel->save($category)) { + $this->setError($categoryModel->getError()); + + return false; + } + + // Get the Category ID. + $data['catid'] = $categoryModel->getState('category.id'); + } + + if (isset($data['urls']) && is_array($data['urls'])) { + $check = $input->post->get('jform', array(), 'array'); + + foreach ($data['urls'] as $i => $url) { + if ($url != false && ($i == 'urla' || $i == 'urlb' || $i == 'urlc')) { + if (preg_match('~^#[a-zA-Z]{1}[a-zA-Z0-9-_:.]*$~', $check['urls'][$i]) == 1) { + $data['urls'][$i] = $check['urls'][$i]; + } else { + $data['urls'][$i] = PunycodeHelper::urlToPunycode($url); + } + } + } + + unset($check); + + $registry = new Registry($data['urls']); + + $data['urls'] = (string) $registry; + } + + // Alter the title for save as copy + if ($input->get('task') == 'save2copy') { + $origTable = $this->getTable(); + + if ($app->isClient('site')) { + $origTable->load($input->getInt('a_id')); + + if ($origTable->title === $data['title']) { + /** + * If title of article is not changed, set alias to original article alias so that Joomla! will generate + * new Title and Alias for the copied article + */ + $data['alias'] = $origTable->alias; + } else { + $data['alias'] = ''; + } + } else { + $origTable->load($input->getInt('id')); + } + + if ($data['title'] == $origTable->title) { + list($title, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['title']); + $data['title'] = $title; + $data['alias'] = $alias; + } elseif ($data['alias'] == $origTable->alias) { + $data['alias'] = ''; + } + } + + // Automatic handling of alias for empty fields + if (in_array($input->get('task'), array('apply', 'save', 'save2new')) && (!isset($data['id']) || (int) $data['id'] == 0)) { + if ($data['alias'] == null) { + if ($app->get('unicodeslugs') == 1) { + $data['alias'] = OutputFilter::stringUrlUnicodeSlug($data['title']); + } else { + $data['alias'] = OutputFilter::stringURLSafe($data['title']); + } + + $table = $this->getTable(); + + if ($table->load(array('alias' => $data['alias'], 'catid' => $data['catid']))) { + $msg = Text::_('COM_CONTENT_SAVE_WARNING'); + } + + list($title, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['title']); + $data['alias'] = $alias; + + if (isset($msg)) { + $app->enqueueMessage($msg, 'warning'); + } + } + } + + if (parent::save($data)) { + // Check if featured is set and if not managed by workflow + if (isset($data['featured']) && !$this->bootComponent('com_content')->isFunctionalityUsed('core.featured', 'com_content.article')) { + if ( + !$this->featured( + $this->getState($this->getName() . '.id'), + $data['featured'], + $data['featured_up'] ?? null, + $data['featured_down'] ?? null + ) + ) { + return false; + } + } + + $this->workflowAfterSave($data); + + return true; + } + + return false; + } + + /** + * Method to toggle the featured setting of articles. + * + * @param array $pks The ids of the items to toggle. + * @param integer $value The value to toggle to. + * @param string|Date $featuredUp The date which item featured up. + * @param string|Date $featuredDown The date which item featured down. + * + * @return boolean True on success. + */ + public function featured($pks, $value = 0, $featuredUp = null, $featuredDown = null) + { + // Sanitize the ids. + $pks = (array) $pks; + $pks = ArrayHelper::toInteger($pks); + $value = (int) $value; + $context = $this->option . '.' . $this->name; + + $this->workflowBeforeStageChange(); + + // Include the plugins for the change of state event. + PluginHelper::importPlugin($this->events_map['featured']); + + // Convert empty strings to null for the query. + if ($featuredUp === '') { + $featuredUp = null; + } + + if ($featuredDown === '') { + $featuredDown = null; + } + + if (empty($pks)) { + $this->setError(Text::_('COM_CONTENT_NO_ITEM_SELECTED')); + + return false; + } + + $table = $this->getTable('Featured', 'Administrator'); + + // Trigger the before change state event. + $eventResult = Factory::getApplication()->getDispatcher()->dispatch( + $this->event_before_change_featured, + AbstractEvent::create( + $this->event_before_change_featured, + [ + 'eventClass' => 'Joomla\Component\Content\Administrator\Event\Model\FeatureEvent', + 'subject' => $this, + 'extension' => $context, + 'pks' => $pks, + 'value' => $value, + ] + ) + ); + + if ($eventResult->getArgument('abort', false)) { + $this->setError(Text::_($eventResult->getArgument('abortReason'))); + + return false; + } + + try { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->update($db->quoteName('#__content')) + ->set($db->quoteName('featured') . ' = :featured') + ->whereIn($db->quoteName('id'), $pks) + ->bind(':featured', $value, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + if ($value === 0) { + // Adjust the mapping table. + // Clear the existing features settings. + $query = $db->getQuery(true) + ->delete($db->quoteName('#__content_frontpage')) + ->whereIn($db->quoteName('content_id'), $pks); + $db->setQuery($query); + $db->execute(); + } else { + // First, we find out which of our new featured articles are already featured. + $query = $db->getQuery(true) + ->select($db->quoteName('content_id')) + ->from($db->quoteName('#__content_frontpage')) + ->whereIn($db->quoteName('content_id'), $pks); + $db->setQuery($query); + + $oldFeatured = $db->loadColumn(); + + // Update old featured articles + if (count($oldFeatured)) { + $query = $db->getQuery(true) + ->update($db->quoteName('#__content_frontpage')) + ->set( + [ + $db->quoteName('featured_up') . ' = :featuredUp', + $db->quoteName('featured_down') . ' = :featuredDown', + ] + ) + ->whereIn($db->quoteName('content_id'), $oldFeatured) + ->bind(':featuredUp', $featuredUp, $featuredUp ? ParameterType::STRING : ParameterType::NULL) + ->bind(':featuredDown', $featuredDown, $featuredDown ? ParameterType::STRING : ParameterType::NULL); + $db->setQuery($query); + $db->execute(); + } + + // We diff the arrays to get a list of the articles that are newly featured + $newFeatured = array_diff($pks, $oldFeatured); + + // Featuring. + if ($newFeatured) { + $query = $db->getQuery(true) + ->insert($db->quoteName('#__content_frontpage')) + ->columns( + [ + $db->quoteName('content_id'), + $db->quoteName('ordering'), + $db->quoteName('featured_up'), + $db->quoteName('featured_down'), + ] + ); + + $dataTypes = [ + ParameterType::INTEGER, + ParameterType::INTEGER, + $featuredUp ? ParameterType::STRING : ParameterType::NULL, + $featuredDown ? ParameterType::STRING : ParameterType::NULL, + ]; + + foreach ($newFeatured as $pk) { + $query->values(implode(',', $query->bindArray([$pk, 0, $featuredUp, $featuredDown], $dataTypes))); + } + + $db->setQuery($query); + $db->execute(); + } + } + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + $table->reorder(); + + // Trigger the change state event. + Factory::getApplication()->getDispatcher()->dispatch( + $this->event_after_change_featured, + AbstractEvent::create( + $this->event_after_change_featured, + [ + 'eventClass' => 'Joomla\Component\Content\Administrator\Event\Model\FeatureEvent', + 'subject' => $this, + 'extension' => $context, + 'pks' => $pks, + 'value' => $value, + ] + ) + ); + + $this->cleanCache(); + + return true; + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param object $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + return [ + $this->getDatabase()->quoteName('catid') . ' = ' . (int) $table->catid, + ]; + } + + /** + * Allows preprocessing of the Form object. + * + * @param Form $form The form object + * @param array $data The data to be merged into the form object + * @param string $group The plugin group to be executed + * + * @return void + * + * @since 3.0 + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + if ($this->canCreateCategory()) { + $form->setFieldAttribute('catid', 'allowAdd', 'true'); + + // Add a prefix for categories created on the fly. + $form->setFieldAttribute('catid', 'customPrefix', '#new#'); + } + + // Association content items + if (Associations::isEnabled()) { + $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); + + if (count($languages) > 1) { + $addform = new \SimpleXMLElement(''); + $fields = $addform->addChild('fields'); + $fields->addAttribute('name', 'associations'); + $fieldset = $fields->addChild('fieldset'); + $fieldset->addAttribute('name', 'item_associations'); + + foreach ($languages as $language) { + $field = $fieldset->addChild('field'); + $field->addAttribute('name', $language->lang_code); + $field->addAttribute('type', 'modal_article'); + $field->addAttribute('language', $language->lang_code); + $field->addAttribute('label', $language->title); + $field->addAttribute('translate_label', 'false'); + $field->addAttribute('select', 'true'); + $field->addAttribute('new', 'true'); + $field->addAttribute('edit', 'true'); + $field->addAttribute('clear', 'true'); + $field->addAttribute('propagate', 'true'); + } + + $form->load($addform, false); + } + } + + $this->workflowPreprocessForm($form, $data); + + parent::preprocessForm($form, $data, $group); + } + + /** + * Custom clean the cache of com_content and content modules + * + * @param string $group The cache group + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('com_content'); + parent::cleanCache('mod_articles_archive'); + parent::cleanCache('mod_articles_categories'); + parent::cleanCache('mod_articles_category'); + parent::cleanCache('mod_articles_latest'); + parent::cleanCache('mod_articles_news'); + parent::cleanCache('mod_articles_popular'); + } + + /** + * Void hit function for pagebreak when editing content from frontend + * + * @return void + * + * @since 3.6.0 + */ + public function hit() + { + } + + /** + * Is the user allowed to create an on the fly category? + * + * @return boolean + * + * @since 3.6.1 + */ + private function canCreateCategory() + { + return Factory::getUser()->authorise('core.create', 'com_content'); + } + + /** + * Delete #__content_frontpage items if the deleted articles was featured + * + * @param object $pks The primary key related to the contents that was deleted. + * + * @return boolean + * + * @since 3.7.0 + */ + public function delete(&$pks) + { + $return = parent::delete($pks); + + if ($return) { + // Now check to see if this articles was featured if so delete it from the #__content_frontpage table + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__content_frontpage')) + ->whereIn($db->quoteName('content_id'), $pks); + $db->setQuery($query); + $db->execute(); + + $this->workflow->deleteAssociation($pks); + } + + return $return; + } } diff --git a/administrator/components/com_content/src/Model/ArticlesModel.php b/administrator/components/com_content/src/Model/ArticlesModel.php index 33cd5215e9120..d7b325ff152c5 100644 --- a/administrator/components/com_content/src/Model/ArticlesModel.php +++ b/administrator/components/com_content/src/Model/ArticlesModel.php @@ -1,4 +1,5 @@ get('workflow_enabled')) - { - $form->removeField('stage', 'filter'); - } - else - { - $ordering = $form->getField('fullordering', 'list'); - - $ordering->addOption('JSTAGE_ASC', ['value' => 'ws.title ASC']); - $ordering->addOption('JSTAGE_DESC', ['value' => 'ws.title DESC']); - } - - return $form; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'a.id', $direction = 'desc') - { - $app = Factory::getApplication(); - - $forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd'); - - // Adjust the context to support modal layouts. - if ($layout = $app->input->get('layout')) - { - $this->context .= '.' . $layout; - } - - // Adjust the context to support forced languages. - if ($forcedLanguage) - { - $this->context .= '.' . $forcedLanguage; - } - - $search = $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search'); - $this->setState('filter.search', $search); - - $featured = $this->getUserStateFromRequest($this->context . '.filter.featured', 'filter_featured', ''); - $this->setState('filter.featured', $featured); - - $published = $this->getUserStateFromRequest($this->context . '.filter.published', 'filter_published', ''); - $this->setState('filter.published', $published); - - $level = $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level'); - $this->setState('filter.level', $level); - - $language = $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', ''); - $this->setState('filter.language', $language); - - $formSubmitted = $app->input->post->get('form_submitted'); - - // Gets the value of a user state variable and sets it in the session - $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access'); - $this->getUserStateFromRequest($this->context . '.filter.author_id', 'filter_author_id'); - $this->getUserStateFromRequest($this->context . '.filter.category_id', 'filter_category_id'); - $this->getUserStateFromRequest($this->context . '.filter.tag', 'filter_tag', ''); - - if ($formSubmitted) - { - $access = $app->input->post->get('access'); - $this->setState('filter.access', $access); - - $authorId = $app->input->post->get('author_id'); - $this->setState('filter.author_id', $authorId); - - $categoryId = $app->input->post->get('category_id'); - $this->setState('filter.category_id', $categoryId); - - $tag = $app->input->post->get('tag'); - $this->setState('filter.tag', $tag); - } - - // List state information. - parent::populateState($ordering, $direction); - - // Force a language - if (!empty($forcedLanguage)) - { - $this->setState('filter.language', $forcedLanguage); - $this->setState('filter.forcedLanguage', $forcedLanguage); - } - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . serialize($this->getState('filter.access')); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . serialize($this->getState('filter.category_id')); - $id .= ':' . serialize($this->getState('filter.author_id')); - $id .= ':' . $this->getState('filter.language'); - $id .= ':' . serialize($this->getState('filter.tag')); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - - $params = ComponentHelper::getParams('com_content'); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.asset_id'), - $db->quoteName('a.title'), - $db->quoteName('a.alias'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.catid'), - $db->quoteName('a.state'), - $db->quoteName('a.access'), - $db->quoteName('a.created'), - $db->quoteName('a.created_by'), - $db->quoteName('a.created_by_alias'), - $db->quoteName('a.modified'), - $db->quoteName('a.ordering'), - $db->quoteName('a.featured'), - $db->quoteName('a.language'), - $db->quoteName('a.hits'), - $db->quoteName('a.publish_up'), - $db->quoteName('a.publish_down'), - $db->quoteName('a.introtext'), - $db->quoteName('a.fulltext'), - $db->quoteName('a.note'), - $db->quoteName('a.images'), - $db->quoteName('a.metakey'), - $db->quoteName('a.metadesc'), - $db->quoteName('a.metadata'), - $db->quoteName('a.version'), - ] - ) - ) - ->select( - [ - $db->quoteName('fp.featured_up'), - $db->quoteName('fp.featured_down'), - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image', 'language_image'), - $db->quoteName('uc.name', 'editor'), - $db->quoteName('ag.title', 'access_level'), - $db->quoteName('c.title', 'category_title'), - $db->quoteName('c.created_user_id', 'category_uid'), - $db->quoteName('c.level', 'category_level'), - $db->quoteName('c.published', 'category_published'), - $db->quoteName('parent.title', 'parent_category_title'), - $db->quoteName('parent.id', 'parent_category_id'), - $db->quoteName('parent.created_user_id', 'parent_category_uid'), - $db->quoteName('parent.level', 'parent_category_level'), - $db->quoteName('ua.name', 'author_name'), - $db->quoteName('wa.stage_id', 'stage_id'), - $db->quoteName('ws.title', 'stage_title'), - $db->quoteName('ws.workflow_id', 'workflow_id'), - $db->quoteName('w.title', 'workflow_title'), - ] - ) - ->from($db->quoteName('#__content', 'a')) - ->where($db->quoteName('wa.extension') . ' = ' . $db->quote('com_content.article')) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')) - ->join('LEFT', $db->quoteName('#__content_frontpage', 'fp'), $db->quoteName('fp.content_id') . ' = ' . $db->quoteName('a.id')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')) - ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) - ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) - ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')) - ->join('LEFT', $db->quoteName('#__users', 'ua'), $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_by')) - ->join('INNER', $db->quoteName('#__workflow_associations', 'wa'), $db->quoteName('wa.item_id') . ' = ' . $db->quoteName('a.id')) - ->join('INNER', $db->quoteName('#__workflow_stages', 'ws'), $db->quoteName('ws.id') . ' = ' . $db->quoteName('wa.stage_id')) - ->join('INNER', $db->quoteName('#__workflows', 'w'), $db->quoteName('w.id') . ' = ' . $db->quoteName('ws.workflow_id')); - - if (PluginHelper::isEnabled('content', 'vote')) - { - $query->select( - [ - 'COALESCE(NULLIF(ROUND(' . $db->quoteName('v.rating_sum') . ' / ' . $db->quoteName('v.rating_count') . ', 0), 0), 0)' - . ' AS ' . $db->quoteName('rating'), - 'COALESCE(NULLIF(' . $db->quoteName('v.rating_count') . ', 0), 0) AS ' . $db->quoteName('rating_count'), - ] - ) - ->join('LEFT', $db->quoteName('#__content_rating', 'v'), $db->quoteName('a.id') . ' = ' . $db->quoteName('v.content_id')); - } - - // Join over the associations. - if (Associations::isEnabled()) - { - $subQuery = $db->getQuery(true) - ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') - ->from($db->quoteName('#__associations', 'asso1')) - ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) - ->where( - [ - $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), - $db->quoteName('asso1.context') . ' = ' . $db->quote('com_content.item'), - ] - ); - - $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); - } - - // Filter by access level. - $access = $this->getState('filter.access'); - - if (is_numeric($access)) - { - $access = (int) $access; - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - elseif (is_array($access)) - { - $access = ArrayHelper::toInteger($access); - $query->whereIn($db->quoteName('a.access'), $access); - } - - // Filter by featured. - $featured = (string) $this->getState('filter.featured'); - - if (\in_array($featured, ['0','1'])) - { - $featured = (int) $featured; - $query->where($db->quoteName('a.featured') . ' = :featured') - ->bind(':featured', $featured, ParameterType::INTEGER); - } - - // Filter by access level on categories. - if (!$user->authorise('core.admin')) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('a.access'), $groups); - $query->whereIn($db->quoteName('c.access'), $groups); - } - - // Filter by published state - $workflowStage = (string) $this->getState('filter.stage'); - - if ($params->get('workflow_enabled') && is_numeric($workflowStage)) - { - $workflowStage = (int) $workflowStage; - $query->where($db->quoteName('wa.stage_id') . ' = :stage') - ->bind(':stage', $workflowStage, ParameterType::INTEGER); - } - - $published = (string) $this->getState('filter.published'); - - if ($published !== '*') - { - if (is_numeric($published)) - { - $state = (int) $published; - $query->where($db->quoteName('a.state') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - elseif (!is_numeric($workflowStage)) - { - $query->whereIn( - $db->quoteName('a.state'), - [ - ContentComponent::CONDITION_PUBLISHED, - ContentComponent::CONDITION_UNPUBLISHED, - ] - ); - } - } - - // Filter by categories and by level - $categoryId = $this->getState('filter.category_id', array()); - $level = (int) $this->getState('filter.level'); - - if (!is_array($categoryId)) - { - $categoryId = $categoryId ? array($categoryId) : array(); - } - - // Case: Using both categories filter and by level filter - if (count($categoryId)) - { - $categoryId = ArrayHelper::toInteger($categoryId); - $categoryTable = Table::getInstance('Category', 'JTable'); - $subCatItemsWhere = array(); - - foreach ($categoryId as $key => $filter_catid) - { - $categoryTable->load($filter_catid); - - // Because values to $query->bind() are passed by reference, using $query->bindArray() here instead to prevent overwriting. - $valuesToBind = [$categoryTable->lft, $categoryTable->rgt]; - - if ($level) - { - $valuesToBind[] = $level + $categoryTable->level - 1; - } - - // Bind values and get parameter names. - $bounded = $query->bindArray($valuesToBind); - - $categoryWhere = $db->quoteName('c.lft') . ' >= ' . $bounded[0] . ' AND ' . $db->quoteName('c.rgt') . ' <= ' . $bounded[1]; - - if ($level) - { - $categoryWhere .= ' AND ' . $db->quoteName('c.level') . ' <= ' . $bounded[2]; - } - - $subCatItemsWhere[] = '(' . $categoryWhere . ')'; - } - - $query->where('(' . implode(' OR ', $subCatItemsWhere) . ')'); - } - - // Case: Using only the by level filter - elseif ($level = (int) $level) - { - $query->where($db->quoteName('c.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } - - // Filter by author - $authorId = $this->getState('filter.author_id'); - - if (is_numeric($authorId)) - { - $authorId = (int) $authorId; - $type = $this->getState('filter.author_id.include', true) ? ' = ' : ' <> '; - $query->where($db->quoteName('a.created_by') . $type . ':authorId') - ->bind(':authorId', $authorId, ParameterType::INTEGER); - } - elseif (is_array($authorId)) - { - // Check to see if by_me is in the array - if (\in_array('by_me', $authorId)) - - // Replace by_me with the current user id in the array - { - $authorId['by_me'] = $user->id; - } - - $authorId = ArrayHelper::toInteger($authorId); - $query->whereIn($db->quoteName('a.created_by'), $authorId); - } - - // Filter by search in title. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :search') - ->bind(':search', $search, ParameterType::INTEGER); - } - elseif (stripos($search, 'author:') === 0) - { - $search = '%' . substr($search, 7) . '%'; - $query->where('(' . $db->quoteName('ua.name') . ' LIKE :search1 OR ' . $db->quoteName('ua.username') . ' LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - elseif (stripos($search, 'content:') === 0) - { - $search = '%' . substr($search, 8) . '%'; - $query->where('(' . $db->quoteName('a.introtext') . ' LIKE :search1 OR ' . $db->quoteName('a.fulltext') . ' LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where( - '(' . $db->quoteName('a.title') . ' LIKE :search1 OR ' . $db->quoteName('a.alias') . ' LIKE :search2' - . ' OR ' . $db->quoteName('a.note') . ' LIKE :search3)' - ) - ->bind([':search1', ':search2', ':search3'], $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('a.language') . ' = :language') - ->bind(':language', $language); - } - - // Filter by a single or group of tags. - $tag = $this->getState('filter.tag'); - - // Run simplified query when filtering by one tag. - if (\is_array($tag) && \count($tag) === 1) - { - $tag = $tag[0]; - } - - if ($tag && \is_array($tag)) - { - $tag = ArrayHelper::toInteger($tag); - - $subQuery = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('content_item_id')) - ->from($db->quoteName('#__contentitem_tag_map')) - ->where( - [ - $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', - $db->quoteName('type_alias') . ' = ' . $db->quote('com_content.article'), - ] - ); - - $query->join( - 'INNER', - '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ); - } - elseif ($tag = (int) $tag) - { - $query->join( - 'INNER', - $db->quoteName('#__contentitem_tag_map', 'tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ) - ->where( - [ - $db->quoteName('tagmap.tag_id') . ' = :tag', - $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_content.article'), - ] - ) - ->bind(':tag', $tag, ParameterType::INTEGER); - } - - // Add the list ordering clause. - $orderCol = $this->state->get('list.ordering', 'a.id'); - $orderDirn = $this->state->get('list.direction', 'DESC'); - - if ($orderCol === 'a.ordering' || $orderCol === 'category_title') - { - $ordering = [ - $db->quoteName('c.title') . ' ' . $db->escape($orderDirn), - $db->quoteName('a.ordering') . ' ' . $db->escape($orderDirn), - ]; - } - else - { - $ordering = $db->escape($orderCol) . ' ' . $db->escape($orderDirn); - } - - $query->order($ordering); - - return $query; - } - - /** - * Method to get all transitions at once for all articles - * - * @return array|boolean - * - * @since 4.0.0 - */ - public function getTransitions() - { - // Get a storage key. - $store = $this->getStoreId('getTransitions'); - - // Try to load the data from internal storage. - if (isset($this->cache[$store])) - { - return $this->cache[$store]; - } - - $db = $this->getDatabase(); - $user = Factory::getUser(); - - $items = $this->getItems(); - - if ($items === false) - { - return false; - } - - $stage_ids = ArrayHelper::getColumn($items, 'stage_id'); - $stage_ids = ArrayHelper::toInteger($stage_ids); - $stage_ids = array_values(array_unique(array_filter($stage_ids))); - - $workflow_ids = ArrayHelper::getColumn($items, 'workflow_id'); - $workflow_ids = ArrayHelper::toInteger($workflow_ids); - $workflow_ids = array_values(array_unique(array_filter($workflow_ids))); - - $this->cache[$store] = array(); - - try - { - if (count($stage_ids) || count($workflow_ids)) - { - Factory::getLanguage()->load('com_workflow', JPATH_ADMINISTRATOR); - - $query = $db->getQuery(true); - - $query ->select( - [ - $db->quoteName('t.id', 'value'), - $db->quoteName('t.title', 'text'), - $db->quoteName('t.from_stage_id'), - $db->quoteName('t.to_stage_id'), - $db->quoteName('s.id', 'stage_id'), - $db->quoteName('s.title', 'stage_title'), - $db->quoteName('t.workflow_id'), - ] - ) - ->from($db->quoteName('#__workflow_transitions', 't')) - ->innerJoin( - $db->quoteName('#__workflow_stages', 's'), - $db->quoteName('t.to_stage_id') . ' = ' . $db->quoteName('s.id') - ) - ->where( - [ - $db->quoteName('t.published') . ' = 1', - $db->quoteName('s.published') . ' = 1', - ] - ) - ->order($db->quoteName('t.ordering')); - - $where = []; - - if (count($stage_ids)) - { - $where[] = $db->quoteName('t.from_stage_id') . ' IN (' . implode(',', $query->bindArray($stage_ids)) . ')'; - } - - if (count($workflow_ids)) - { - $where[] = '(' . $db->quoteName('t.from_stage_id') . ' = -1 AND ' . $db->quoteName('t.workflow_id') . ' IN (' . implode(',', $query->bindArray($workflow_ids)) . '))'; - } - - $query->where('((' . implode(') OR (', $where) . '))'); - - $transitions = $db->setQuery($query)->loadAssocList(); - - foreach ($transitions as $key => $transition) - { - if (!$user->authorise('core.execute.transition', 'com_content.transition.' . (int) $transition['value'])) - { - unset($transitions[$key]); - } - - $transitions[$key]['text'] = Text::_($transition['text']); - } - - $this->cache[$store] = $transitions; - } - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return $this->cache[$store]; - } - - /** - * Method to get a list of articles. - * Overridden to add item type alias. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 4.0.0 - */ - public function getItems() - { - $items = parent::getItems(); - - foreach ($items as $item) - { - $item->typeAlias = 'com_content.article'; - - if (isset($item->metadata)) - { - $registry = new Registry($item->metadata); - $item->metadata = $registry->toArray(); - } - } - - return $items; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + * @see \Joomla\CMS\MVC\Controller\BaseController + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'alias', 'a.alias', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'catid', 'a.catid', 'category_title', + 'state', 'a.state', + 'access', 'a.access', 'access_level', + 'created', 'a.created', + 'modified', 'a.modified', + 'created_by', 'a.created_by', + 'created_by_alias', 'a.created_by_alias', + 'ordering', 'a.ordering', + 'featured', 'a.featured', + 'featured_up', 'fp.featured_up', + 'featured_down', 'fp.featured_down', + 'language', 'a.language', + 'hits', 'a.hits', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'published', 'a.published', + 'author_id', + 'category_id', + 'level', + 'tag', + 'rating_count', 'rating', + 'stage', 'wa.stage_id', + 'ws.title' + ); + + if (Associations::isEnabled()) { + $config['filter_fields'][] = 'association'; + } + } + + parent::__construct($config); + } + + /** + * Get the filter form + * + * @param array $data data + * @param boolean $loadData load current data + * + * @return \Joomla\CMS\Form\Form|null The Form object or null if the form can't be found + * + * @since 3.2 + */ + public function getFilterForm($data = array(), $loadData = true) + { + $form = parent::getFilterForm($data, $loadData); + + $params = ComponentHelper::getParams('com_content'); + + if (!$params->get('workflow_enabled')) { + $form->removeField('stage', 'filter'); + } else { + $ordering = $form->getField('fullordering', 'list'); + + $ordering->addOption('JSTAGE_ASC', ['value' => 'ws.title ASC']); + $ordering->addOption('JSTAGE_DESC', ['value' => 'ws.title DESC']); + } + + return $form; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.id', $direction = 'desc') + { + $app = Factory::getApplication(); + + $forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd'); + + // Adjust the context to support modal layouts. + if ($layout = $app->input->get('layout')) { + $this->context .= '.' . $layout; + } + + // Adjust the context to support forced languages. + if ($forcedLanguage) { + $this->context .= '.' . $forcedLanguage; + } + + $search = $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search'); + $this->setState('filter.search', $search); + + $featured = $this->getUserStateFromRequest($this->context . '.filter.featured', 'filter_featured', ''); + $this->setState('filter.featured', $featured); + + $published = $this->getUserStateFromRequest($this->context . '.filter.published', 'filter_published', ''); + $this->setState('filter.published', $published); + + $level = $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level'); + $this->setState('filter.level', $level); + + $language = $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', ''); + $this->setState('filter.language', $language); + + $formSubmitted = $app->input->post->get('form_submitted'); + + // Gets the value of a user state variable and sets it in the session + $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access'); + $this->getUserStateFromRequest($this->context . '.filter.author_id', 'filter_author_id'); + $this->getUserStateFromRequest($this->context . '.filter.category_id', 'filter_category_id'); + $this->getUserStateFromRequest($this->context . '.filter.tag', 'filter_tag', ''); + + if ($formSubmitted) { + $access = $app->input->post->get('access'); + $this->setState('filter.access', $access); + + $authorId = $app->input->post->get('author_id'); + $this->setState('filter.author_id', $authorId); + + $categoryId = $app->input->post->get('category_id'); + $this->setState('filter.category_id', $categoryId); + + $tag = $app->input->post->get('tag'); + $this->setState('filter.tag', $tag); + } + + // List state information. + parent::populateState($ordering, $direction); + + // Force a language + if (!empty($forcedLanguage)) { + $this->setState('filter.language', $forcedLanguage); + $this->setState('filter.forcedLanguage', $forcedLanguage); + } + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . serialize($this->getState('filter.access')); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . serialize($this->getState('filter.category_id')); + $id .= ':' . serialize($this->getState('filter.author_id')); + $id .= ':' . $this->getState('filter.language'); + $id .= ':' . serialize($this->getState('filter.tag')); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + + $params = ComponentHelper::getParams('com_content'); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.asset_id'), + $db->quoteName('a.title'), + $db->quoteName('a.alias'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.catid'), + $db->quoteName('a.state'), + $db->quoteName('a.access'), + $db->quoteName('a.created'), + $db->quoteName('a.created_by'), + $db->quoteName('a.created_by_alias'), + $db->quoteName('a.modified'), + $db->quoteName('a.ordering'), + $db->quoteName('a.featured'), + $db->quoteName('a.language'), + $db->quoteName('a.hits'), + $db->quoteName('a.publish_up'), + $db->quoteName('a.publish_down'), + $db->quoteName('a.introtext'), + $db->quoteName('a.fulltext'), + $db->quoteName('a.note'), + $db->quoteName('a.images'), + $db->quoteName('a.metakey'), + $db->quoteName('a.metadesc'), + $db->quoteName('a.metadata'), + $db->quoteName('a.version'), + ] + ) + ) + ->select( + [ + $db->quoteName('fp.featured_up'), + $db->quoteName('fp.featured_down'), + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image', 'language_image'), + $db->quoteName('uc.name', 'editor'), + $db->quoteName('ag.title', 'access_level'), + $db->quoteName('c.title', 'category_title'), + $db->quoteName('c.created_user_id', 'category_uid'), + $db->quoteName('c.level', 'category_level'), + $db->quoteName('c.published', 'category_published'), + $db->quoteName('parent.title', 'parent_category_title'), + $db->quoteName('parent.id', 'parent_category_id'), + $db->quoteName('parent.created_user_id', 'parent_category_uid'), + $db->quoteName('parent.level', 'parent_category_level'), + $db->quoteName('ua.name', 'author_name'), + $db->quoteName('wa.stage_id', 'stage_id'), + $db->quoteName('ws.title', 'stage_title'), + $db->quoteName('ws.workflow_id', 'workflow_id'), + $db->quoteName('w.title', 'workflow_title'), + ] + ) + ->from($db->quoteName('#__content', 'a')) + ->where($db->quoteName('wa.extension') . ' = ' . $db->quote('com_content.article')) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')) + ->join('LEFT', $db->quoteName('#__content_frontpage', 'fp'), $db->quoteName('fp.content_id') . ' = ' . $db->quoteName('a.id')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')) + ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) + ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) + ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')) + ->join('LEFT', $db->quoteName('#__users', 'ua'), $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_by')) + ->join('INNER', $db->quoteName('#__workflow_associations', 'wa'), $db->quoteName('wa.item_id') . ' = ' . $db->quoteName('a.id')) + ->join('INNER', $db->quoteName('#__workflow_stages', 'ws'), $db->quoteName('ws.id') . ' = ' . $db->quoteName('wa.stage_id')) + ->join('INNER', $db->quoteName('#__workflows', 'w'), $db->quoteName('w.id') . ' = ' . $db->quoteName('ws.workflow_id')); + + if (PluginHelper::isEnabled('content', 'vote')) { + $query->select( + [ + 'COALESCE(NULLIF(ROUND(' . $db->quoteName('v.rating_sum') . ' / ' . $db->quoteName('v.rating_count') . ', 0), 0), 0)' + . ' AS ' . $db->quoteName('rating'), + 'COALESCE(NULLIF(' . $db->quoteName('v.rating_count') . ', 0), 0) AS ' . $db->quoteName('rating_count'), + ] + ) + ->join('LEFT', $db->quoteName('#__content_rating', 'v'), $db->quoteName('a.id') . ' = ' . $db->quoteName('v.content_id')); + } + + // Join over the associations. + if (Associations::isEnabled()) { + $subQuery = $db->getQuery(true) + ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') + ->from($db->quoteName('#__associations', 'asso1')) + ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) + ->where( + [ + $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), + $db->quoteName('asso1.context') . ' = ' . $db->quote('com_content.item'), + ] + ); + + $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); + } + + // Filter by access level. + $access = $this->getState('filter.access'); + + if (is_numeric($access)) { + $access = (int) $access; + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } elseif (is_array($access)) { + $access = ArrayHelper::toInteger($access); + $query->whereIn($db->quoteName('a.access'), $access); + } + + // Filter by featured. + $featured = (string) $this->getState('filter.featured'); + + if (\in_array($featured, ['0','1'])) { + $featured = (int) $featured; + $query->where($db->quoteName('a.featured') . ' = :featured') + ->bind(':featured', $featured, ParameterType::INTEGER); + } + + // Filter by access level on categories. + if (!$user->authorise('core.admin')) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('a.access'), $groups); + $query->whereIn($db->quoteName('c.access'), $groups); + } + + // Filter by published state + $workflowStage = (string) $this->getState('filter.stage'); + + if ($params->get('workflow_enabled') && is_numeric($workflowStage)) { + $workflowStage = (int) $workflowStage; + $query->where($db->quoteName('wa.stage_id') . ' = :stage') + ->bind(':stage', $workflowStage, ParameterType::INTEGER); + } + + $published = (string) $this->getState('filter.published'); + + if ($published !== '*') { + if (is_numeric($published)) { + $state = (int) $published; + $query->where($db->quoteName('a.state') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } elseif (!is_numeric($workflowStage)) { + $query->whereIn( + $db->quoteName('a.state'), + [ + ContentComponent::CONDITION_PUBLISHED, + ContentComponent::CONDITION_UNPUBLISHED, + ] + ); + } + } + + // Filter by categories and by level + $categoryId = $this->getState('filter.category_id', array()); + $level = (int) $this->getState('filter.level'); + + if (!is_array($categoryId)) { + $categoryId = $categoryId ? array($categoryId) : array(); + } + + // Case: Using both categories filter and by level filter + if (count($categoryId)) { + $categoryId = ArrayHelper::toInteger($categoryId); + $categoryTable = Table::getInstance('Category', 'JTable'); + $subCatItemsWhere = array(); + + foreach ($categoryId as $key => $filter_catid) { + $categoryTable->load($filter_catid); + + // Because values to $query->bind() are passed by reference, using $query->bindArray() here instead to prevent overwriting. + $valuesToBind = [$categoryTable->lft, $categoryTable->rgt]; + + if ($level) { + $valuesToBind[] = $level + $categoryTable->level - 1; + } + + // Bind values and get parameter names. + $bounded = $query->bindArray($valuesToBind); + + $categoryWhere = $db->quoteName('c.lft') . ' >= ' . $bounded[0] . ' AND ' . $db->quoteName('c.rgt') . ' <= ' . $bounded[1]; + + if ($level) { + $categoryWhere .= ' AND ' . $db->quoteName('c.level') . ' <= ' . $bounded[2]; + } + + $subCatItemsWhere[] = '(' . $categoryWhere . ')'; + } + + $query->where('(' . implode(' OR ', $subCatItemsWhere) . ')'); + } elseif ($level = (int) $level) { + // Case: Using only the by level filter + $query->where($db->quoteName('c.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } + + // Filter by author + $authorId = $this->getState('filter.author_id'); + + if (is_numeric($authorId)) { + $authorId = (int) $authorId; + $type = $this->getState('filter.author_id.include', true) ? ' = ' : ' <> '; + $query->where($db->quoteName('a.created_by') . $type . ':authorId') + ->bind(':authorId', $authorId, ParameterType::INTEGER); + } elseif (is_array($authorId)) { + // Check to see if by_me is in the array + if (\in_array('by_me', $authorId)) { + // Replace by_me with the current user id in the array + $authorId['by_me'] = $user->id; + } + + $authorId = ArrayHelper::toInteger($authorId); + $query->whereIn($db->quoteName('a.created_by'), $authorId); + } + + // Filter by search in title. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :search') + ->bind(':search', $search, ParameterType::INTEGER); + } elseif (stripos($search, 'author:') === 0) { + $search = '%' . substr($search, 7) . '%'; + $query->where('(' . $db->quoteName('ua.name') . ' LIKE :search1 OR ' . $db->quoteName('ua.username') . ' LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } elseif (stripos($search, 'content:') === 0) { + $search = '%' . substr($search, 8) . '%'; + $query->where('(' . $db->quoteName('a.introtext') . ' LIKE :search1 OR ' . $db->quoteName('a.fulltext') . ' LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where( + '(' . $db->quoteName('a.title') . ' LIKE :search1 OR ' . $db->quoteName('a.alias') . ' LIKE :search2' + . ' OR ' . $db->quoteName('a.note') . ' LIKE :search3)' + ) + ->bind([':search1', ':search2', ':search3'], $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('a.language') . ' = :language') + ->bind(':language', $language); + } + + // Filter by a single or group of tags. + $tag = $this->getState('filter.tag'); + + // Run simplified query when filtering by one tag. + if (\is_array($tag) && \count($tag) === 1) { + $tag = $tag[0]; + } + + if ($tag && \is_array($tag)) { + $tag = ArrayHelper::toInteger($tag); + + $subQuery = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('content_item_id')) + ->from($db->quoteName('#__contentitem_tag_map')) + ->where( + [ + $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', + $db->quoteName('type_alias') . ' = ' . $db->quote('com_content.article'), + ] + ); + + $query->join( + 'INNER', + '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ); + } elseif ($tag = (int) $tag) { + $query->join( + 'INNER', + $db->quoteName('#__contentitem_tag_map', 'tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ) + ->where( + [ + $db->quoteName('tagmap.tag_id') . ' = :tag', + $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_content.article'), + ] + ) + ->bind(':tag', $tag, ParameterType::INTEGER); + } + + // Add the list ordering clause. + $orderCol = $this->state->get('list.ordering', 'a.id'); + $orderDirn = $this->state->get('list.direction', 'DESC'); + + if ($orderCol === 'a.ordering' || $orderCol === 'category_title') { + $ordering = [ + $db->quoteName('c.title') . ' ' . $db->escape($orderDirn), + $db->quoteName('a.ordering') . ' ' . $db->escape($orderDirn), + ]; + } else { + $ordering = $db->escape($orderCol) . ' ' . $db->escape($orderDirn); + } + + $query->order($ordering); + + return $query; + } + + /** + * Method to get all transitions at once for all articles + * + * @return array|boolean + * + * @since 4.0.0 + */ + public function getTransitions() + { + // Get a storage key. + $store = $this->getStoreId('getTransitions'); + + // Try to load the data from internal storage. + if (isset($this->cache[$store])) { + return $this->cache[$store]; + } + + $db = $this->getDatabase(); + $user = Factory::getUser(); + + $items = $this->getItems(); + + if ($items === false) { + return false; + } + + $stage_ids = ArrayHelper::getColumn($items, 'stage_id'); + $stage_ids = ArrayHelper::toInteger($stage_ids); + $stage_ids = array_values(array_unique(array_filter($stage_ids))); + + $workflow_ids = ArrayHelper::getColumn($items, 'workflow_id'); + $workflow_ids = ArrayHelper::toInteger($workflow_ids); + $workflow_ids = array_values(array_unique(array_filter($workflow_ids))); + + $this->cache[$store] = array(); + + try { + if (count($stage_ids) || count($workflow_ids)) { + Factory::getLanguage()->load('com_workflow', JPATH_ADMINISTRATOR); + + $query = $db->getQuery(true); + + $query ->select( + [ + $db->quoteName('t.id', 'value'), + $db->quoteName('t.title', 'text'), + $db->quoteName('t.from_stage_id'), + $db->quoteName('t.to_stage_id'), + $db->quoteName('s.id', 'stage_id'), + $db->quoteName('s.title', 'stage_title'), + $db->quoteName('t.workflow_id'), + ] + ) + ->from($db->quoteName('#__workflow_transitions', 't')) + ->innerJoin( + $db->quoteName('#__workflow_stages', 's'), + $db->quoteName('t.to_stage_id') . ' = ' . $db->quoteName('s.id') + ) + ->where( + [ + $db->quoteName('t.published') . ' = 1', + $db->quoteName('s.published') . ' = 1', + ] + ) + ->order($db->quoteName('t.ordering')); + + $where = []; + + if (count($stage_ids)) { + $where[] = $db->quoteName('t.from_stage_id') . ' IN (' . implode(',', $query->bindArray($stage_ids)) . ')'; + } + + if (count($workflow_ids)) { + $where[] = '(' . $db->quoteName('t.from_stage_id') . ' = -1 AND ' . $db->quoteName('t.workflow_id') . ' IN (' . implode(',', $query->bindArray($workflow_ids)) . '))'; + } + + $query->where('((' . implode(') OR (', $where) . '))'); + + $transitions = $db->setQuery($query)->loadAssocList(); + + foreach ($transitions as $key => $transition) { + if (!$user->authorise('core.execute.transition', 'com_content.transition.' . (int) $transition['value'])) { + unset($transitions[$key]); + } + + $transitions[$key]['text'] = Text::_($transition['text']); + } + + $this->cache[$store] = $transitions; + } + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return $this->cache[$store]; + } + + /** + * Method to get a list of articles. + * Overridden to add item type alias. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 4.0.0 + */ + public function getItems() + { + $items = parent::getItems(); + + foreach ($items as $item) { + $item->typeAlias = 'com_content.article'; + + if (isset($item->metadata)) { + $registry = new Registry($item->metadata); + $item->metadata = $registry->toArray(); + } + } + + return $items; + } } diff --git a/administrator/components/com_content/src/Model/FeatureModel.php b/administrator/components/com_content/src/Model/FeatureModel.php index 8eebd6ae4d32b..ecc1ffc04e6e6 100644 --- a/administrator/components/com_content/src/Model/FeatureModel.php +++ b/administrator/components/com_content/src/Model/FeatureModel.php @@ -1,4 +1,5 @@ setState('filter.featured', 1); - } + // Filter by featured articles. + $this->setState('filter.featured', 1); + } - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 4.0.0 - */ - protected function getListQuery() - { - $query = parent::getListQuery(); + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 4.0.0 + */ + protected function getListQuery() + { + $query = parent::getListQuery(); - $query->select($this->getDatabase()->quoteName('fp.ordering')); + $query->select($this->getDatabase()->quoteName('fp.ordering')); - return $query; - } + return $query; + } } diff --git a/administrator/components/com_content/src/Service/HTML/AdministratorService.php b/administrator/components/com_content/src/Service/HTML/AdministratorService.php index 82691d820b8e9..408f0a226e939 100644 --- a/administrator/components/com_content/src/Service/HTML/AdministratorService.php +++ b/administrator/components/com_content/src/Service/HTML/AdministratorService.php @@ -1,4 +1,5 @@ $associated) - { - $associations[$tag] = (int) $associated->id; - } + // Get the associations + if ($associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', $articleid)) { + foreach ($associations as $tag => $associated) { + $associations[$tag] = (int) $associated->id; + } - // Get the associated menu items - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select( - [ - 'c.*', - $db->quoteName('l.sef', 'lang_sef'), - $db->quoteName('l.lang_code'), - $db->quoteName('cat.title', 'category_title'), - $db->quoteName('l.image'), - $db->quoteName('l.title', 'language_title'), - ] - ) - ->from($db->quoteName('#__content', 'c')) - ->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('cat.id') . ' = ' . $db->quoteName('c.catid')) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code')) - ->whereIn($db->quoteName('c.id'), array_values($associations)) - ->where($db->quoteName('c.id') . ' != :articleId') - ->bind(':articleId', $articleid, ParameterType::INTEGER); + // Get the associated menu items + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + 'c.*', + $db->quoteName('l.sef', 'lang_sef'), + $db->quoteName('l.lang_code'), + $db->quoteName('cat.title', 'category_title'), + $db->quoteName('l.image'), + $db->quoteName('l.title', 'language_title'), + ] + ) + ->from($db->quoteName('#__content', 'c')) + ->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('cat.id') . ' = ' . $db->quoteName('c.catid')) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code')) + ->whereIn($db->quoteName('c.id'), array_values($associations)) + ->where($db->quoteName('c.id') . ' != :articleId') + ->bind(':articleId', $articleid, ParameterType::INTEGER); - $db->setQuery($query); + $db->setQuery($query); - try - { - $items = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500, $e); - } + try { + $items = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500, $e); + } - if ($items) - { - $languages = LanguageHelper::getContentLanguages(array(0, 1)); - $content_languages = array_column($languages, 'lang_code'); + if ($items) { + $languages = LanguageHelper::getContentLanguages(array(0, 1)); + $content_languages = array_column($languages, 'lang_code'); - foreach ($items as &$item) - { - if (in_array($item->lang_code, $content_languages)) - { - $text = $item->lang_code; - $url = Route::_('index.php?option=com_content&task=article.edit&id=' . (int) $item->id); - $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
' - . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
' . Text::sprintf('JCATEGORY_SPRINTF', $item->category_title); - $classes = 'badge bg-secondary'; + foreach ($items as &$item) { + if (in_array($item->lang_code, $content_languages)) { + $text = $item->lang_code; + $url = Route::_('index.php?option=com_content&task=article.edit&id=' . (int) $item->id); + $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
' + . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
' . Text::sprintf('JCATEGORY_SPRINTF', $item->category_title); + $classes = 'badge bg-secondary'; - $item->link = '' . $text . '' - . ''; - } - else - { - // Display warning if Content Language is trashed or deleted - Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); - } - } - } + $item->link = '' . $text . '' + . ''; + } else { + // Display warning if Content Language is trashed or deleted + Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); + } + } + } - $html = LayoutHelper::render('joomla.content.associations', $items); - } + $html = LayoutHelper::render('joomla.content.associations', $items); + } - return $html; - } + return $html; + } } diff --git a/administrator/components/com_content/src/Service/HTML/Icon.php b/administrator/components/com_content/src/Service/HTML/Icon.php index b70cbaf279d4d..c069d8c054ccd 100644 --- a/administrator/components/com_content/src/Service/HTML/Icon.php +++ b/administrator/components/com_content/src/Service/HTML/Icon.php @@ -1,4 +1,5 @@ id; - - $text = ''; - - if ($params->get('show_icons')) - { - $text .= ''; - } - - $text .= Text::_('COM_CONTENT_NEW_ARTICLE'); - - // Add the button classes to the attribs array - if (isset($attribs['class'])) - { - $attribs['class'] .= ' btn btn-primary'; - } - else - { - $attribs['class'] = 'btn btn-primary'; - } - - $button = HTMLHelper::_('link', Route::_($url), $text, $attribs); - - return $button; - } - - /** - * Display an edit icon for the article. - * - * This icon will not display in a popup window, nor if the article is trashed. - * Edit access checks must be performed in the calling code. - * - * @param object $article The article information - * @param Registry $params The item parameters - * @param array $attribs Optional attributes for the link - * @param boolean $legacy True to use legacy images, false to use icomoon based graphic - * - * @return string The HTML for the article edit icon. - * - * @since 4.0.0 - */ - public function edit($article, $params, $attribs = array(), $legacy = false) - { - $user = Factory::getUser(); - $uri = Uri::getInstance(); - - // Ignore if in a popup window. - if ($params && $params->get('popup')) - { - return ''; - } - - // Ignore if the state is negative (trashed). - if (!in_array($article->state, [Workflow::CONDITION_UNPUBLISHED, Workflow::CONDITION_PUBLISHED])) - { - return ''; - } - - // Show checked_out icon if the article is checked out by a different user - if (property_exists($article, 'checked_out') - && property_exists($article, 'checked_out_time') - && !is_null($article->checked_out) - && $article->checked_out != $user->get('id')) - { - $checkoutUser = Factory::getUser($article->checked_out); - $date = HTMLHelper::_('date', $article->checked_out_time); - $tooltip = Text::sprintf('COM_CONTENT_CHECKED_OUT_BY', $checkoutUser->name) - . '
' . $date; - - $text = LayoutHelper::render('joomla.content.icons.edit_lock', array('article' => $article, 'tooltip' => $tooltip, 'legacy' => $legacy)); - - $attribs['aria-describedby'] = 'editarticle-' . (int) $article->id; - $output = HTMLHelper::_('link', '#', $text, $attribs); - - return $output; - } - - $contentUrl = RouteHelper::getArticleRoute($article->slug, $article->catid, $article->language); - $url = $contentUrl . '&task=article.edit&a_id=' . $article->id . '&return=' . base64_encode($uri); - - if ($article->state == Workflow::CONDITION_UNPUBLISHED) - { - $tooltip = Text::_('COM_CONTENT_EDIT_UNPUBLISHED_ARTICLE'); - } - else - { - $tooltip = Text::_('COM_CONTENT_EDIT_PUBLISHED_ARTICLE'); - } - - $text = LayoutHelper::render('joomla.content.icons.edit', array('article' => $article, 'tooltip' => $tooltip, 'legacy' => $legacy)); - - $attribs['aria-describedby'] = 'editarticle-' . (int) $article->id; - $output = HTMLHelper::_('link', Route::_($url), $text, $attribs); - - return $output; - } - - /** - * Method to generate a link to print an article - * - * @param Registry $params The item parameters - * @param boolean $legacy True to use legacy images, false to use icomoon based graphic - * - * @return string The HTML markup for the popup link - * - * @since 4.0.0 - */ - public function print_screen($params, $legacy = false) - { - $text = LayoutHelper::render('joomla.content.icons.print_screen', array('params' => $params, 'legacy' => $legacy)); - - return ''; - } + /** + * Method to generate a link to the create item page for the given category + * + * @param object $category The category information + * @param Registry $params The item parameters + * @param array $attribs Optional attributes for the link + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML markup for the create item link + * + * @since 4.0.0 + */ + public function create($category, $params, $attribs = array(), $legacy = false) + { + $uri = Uri::getInstance(); + + $url = 'index.php?option=com_content&task=article.add&return=' . base64_encode($uri) . '&a_id=0&catid=' . $category->id; + + $text = ''; + + if ($params->get('show_icons')) { + $text .= ''; + } + + $text .= Text::_('COM_CONTENT_NEW_ARTICLE'); + + // Add the button classes to the attribs array + if (isset($attribs['class'])) { + $attribs['class'] .= ' btn btn-primary'; + } else { + $attribs['class'] = 'btn btn-primary'; + } + + $button = HTMLHelper::_('link', Route::_($url), $text, $attribs); + + return $button; + } + + /** + * Display an edit icon for the article. + * + * This icon will not display in a popup window, nor if the article is trashed. + * Edit access checks must be performed in the calling code. + * + * @param object $article The article information + * @param Registry $params The item parameters + * @param array $attribs Optional attributes for the link + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML for the article edit icon. + * + * @since 4.0.0 + */ + public function edit($article, $params, $attribs = array(), $legacy = false) + { + $user = Factory::getUser(); + $uri = Uri::getInstance(); + + // Ignore if in a popup window. + if ($params && $params->get('popup')) { + return ''; + } + + // Ignore if the state is negative (trashed). + if (!in_array($article->state, [Workflow::CONDITION_UNPUBLISHED, Workflow::CONDITION_PUBLISHED])) { + return ''; + } + + // Show checked_out icon if the article is checked out by a different user + if ( + property_exists($article, 'checked_out') + && property_exists($article, 'checked_out_time') + && !is_null($article->checked_out) + && $article->checked_out != $user->get('id') + ) { + $checkoutUser = Factory::getUser($article->checked_out); + $date = HTMLHelper::_('date', $article->checked_out_time); + $tooltip = Text::sprintf('COM_CONTENT_CHECKED_OUT_BY', $checkoutUser->name) + . '
' . $date; + + $text = LayoutHelper::render('joomla.content.icons.edit_lock', array('article' => $article, 'tooltip' => $tooltip, 'legacy' => $legacy)); + + $attribs['aria-describedby'] = 'editarticle-' . (int) $article->id; + $output = HTMLHelper::_('link', '#', $text, $attribs); + + return $output; + } + + $contentUrl = RouteHelper::getArticleRoute($article->slug, $article->catid, $article->language); + $url = $contentUrl . '&task=article.edit&a_id=' . $article->id . '&return=' . base64_encode($uri); + + if ($article->state == Workflow::CONDITION_UNPUBLISHED) { + $tooltip = Text::_('COM_CONTENT_EDIT_UNPUBLISHED_ARTICLE'); + } else { + $tooltip = Text::_('COM_CONTENT_EDIT_PUBLISHED_ARTICLE'); + } + + $text = LayoutHelper::render('joomla.content.icons.edit', array('article' => $article, 'tooltip' => $tooltip, 'legacy' => $legacy)); + + $attribs['aria-describedby'] = 'editarticle-' . (int) $article->id; + $output = HTMLHelper::_('link', Route::_($url), $text, $attribs); + + return $output; + } + + /** + * Method to generate a link to print an article + * + * @param Registry $params The item parameters + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML markup for the popup link + * + * @since 4.0.0 + */ + public function print_screen($params, $legacy = false) + { + $text = LayoutHelper::render('joomla.content.icons.print_screen', array('params' => $params, 'legacy' => $legacy)); + + return ''; + } } diff --git a/administrator/components/com_content/src/Table/ArticleTable.php b/administrator/components/com_content/src/Table/ArticleTable.php index 35d8f589f7254..ceec80ce8e7dc 100644 --- a/administrator/components/com_content/src/Table/ArticleTable.php +++ b/administrator/components/com_content/src/Table/ArticleTable.php @@ -1,4 +1,5 @@ getLayout() == 'pagebreak') - { - parent::display($tpl); - - return; - } - - $this->form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - $this->canDo = ContentHelper::getActions('com_content', 'article', $this->item->id); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // If we are forcing a language in modal (used for associations). - if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) - { - // Set the language field to the forcedLanguage and disable changing it. - $this->form->setValue('language', null, $forcedLanguage); - $this->form->setFieldAttribute('language', 'readonly', 'true'); - - // Only allow to select categories with All language or with the forced language. - $this->form->setFieldAttribute('catid', 'language', '*,' . $forcedLanguage); - - // Only allow to select tags with All language or with the forced language. - $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - * - * @throws \Exception - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - $user = $this->getCurrentUser(); - $userId = $user->id; - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); - - // Built the actions for new and existing records. - $canDo = $this->canDo; - - $toolbar = Toolbar::getInstance(); - - ToolbarHelper::title( - Text::_('COM_CONTENT_PAGE_' . ($checkedOut ? 'VIEW_ARTICLE' : ($isNew ? 'ADD_ARTICLE' : 'EDIT_ARTICLE'))), - 'pencil-alt article-add' - ); - - // For new records, check the create permission. - if ($isNew && (count($user->getAuthorisedCategories('com_content', 'core.create')) > 0)) - { - $toolbar->apply('article.apply'); - - $saveGroup = $toolbar->dropdownButton('save-group'); - - $saveGroup->configure( - function (Toolbar $childBar) use ($user) - { - $childBar->save('article.save'); - - if ($user->authorise('core.create', 'com_menus.menu')) - { - $childBar->save('article.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); - } - - $childBar->save2new('article.save2new'); - } - ); - - $toolbar->cancel('article.cancel', 'JTOOLBAR_CANCEL'); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); - - if (!$checkedOut && $itemEditable) - { - $toolbar->apply('article.apply'); - } - - $saveGroup = $toolbar->dropdownButton('save-group'); - - $saveGroup->configure( - function (Toolbar $childBar) use ($checkedOut, $itemEditable, $canDo, $user) - { - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable) - { - $childBar->save('article.save'); - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $childBar->save2new('article.save2new'); - } - } - - // If checked out, we can still save2menu - if ($user->authorise('core.create', 'com_menus.menu')) - { - $childBar->save('article.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); - } - - // If checked out, we can still save - if ($canDo->get('core.create')) - { - $childBar->save2copy('article.save2copy'); - } - } - ); - - $toolbar->cancel('article.cancel', 'JTOOLBAR_CLOSE'); - - if (!$isNew) - { - if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $itemEditable) - { - $toolbar->versions('com_content.article', $this->item->id); - } - - $url = RouteHelper::getArticleRoute($this->item->id . ':' . $this->item->alias, $this->item->catid, $this->item->language); - - $toolbar->preview(Route::link('site', $url, true), 'JGLOBAL_PREVIEW') - ->bodyHeight(80) - ->modalWidth(90); - - if (PluginHelper::isEnabled('system', 'jooa11y')) - { - $toolbar->jooa11y(Route::link('site', $url . '&jooa11y=1', true), 'JGLOBAL_JOOA11Y') - ->bodyHeight(80) - ->modalWidth(90); - } - - if (Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) - { - $toolbar->standardButton('contract') - ->text('JTOOLBAR_ASSOCIATIONS') - ->task('article.editAssociations'); - } - } - } - - $toolbar->divider(); - - ToolbarHelper::inlinehelp(); - - $toolbar->help('Articles:_Edit'); - } + /** + * The \JForm object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The model state + * + * @var object + */ + protected $state; + + /** + * The actions the user is authorised to perform + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $canDo; + + /** + * Pagebreak TOC alias + * + * @var string + */ + protected $eName; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + * + * @throws \Exception + */ + public function display($tpl = null) + { + if ($this->getLayout() == 'pagebreak') { + parent::display($tpl); + + return; + } + + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + $this->canDo = ContentHelper::getActions('com_content', 'article', $this->item->id); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // If we are forcing a language in modal (used for associations). + if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) { + // Set the language field to the forcedLanguage and disable changing it. + $this->form->setValue('language', null, $forcedLanguage); + $this->form->setFieldAttribute('language', 'readonly', 'true'); + + // Only allow to select categories with All language or with the forced language. + $this->form->setFieldAttribute('catid', 'language', '*,' . $forcedLanguage); + + // Only allow to select tags with All language or with the forced language. + $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + * + * @throws \Exception + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + $user = $this->getCurrentUser(); + $userId = $user->id; + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); + + // Built the actions for new and existing records. + $canDo = $this->canDo; + + $toolbar = Toolbar::getInstance(); + + ToolbarHelper::title( + Text::_('COM_CONTENT_PAGE_' . ($checkedOut ? 'VIEW_ARTICLE' : ($isNew ? 'ADD_ARTICLE' : 'EDIT_ARTICLE'))), + 'pencil-alt article-add' + ); + + // For new records, check the create permission. + if ($isNew && (count($user->getAuthorisedCategories('com_content', 'core.create')) > 0)) { + $toolbar->apply('article.apply'); + + $saveGroup = $toolbar->dropdownButton('save-group'); + + $saveGroup->configure( + function (Toolbar $childBar) use ($user) { + $childBar->save('article.save'); + + if ($user->authorise('core.create', 'com_menus.menu')) { + $childBar->save('article.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); + } + + $childBar->save2new('article.save2new'); + } + ); + + $toolbar->cancel('article.cancel', 'JTOOLBAR_CANCEL'); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); + + if (!$checkedOut && $itemEditable) { + $toolbar->apply('article.apply'); + } + + $saveGroup = $toolbar->dropdownButton('save-group'); + + $saveGroup->configure( + function (Toolbar $childBar) use ($checkedOut, $itemEditable, $canDo, $user) { + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable) { + $childBar->save('article.save'); + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $childBar->save2new('article.save2new'); + } + } + + // If checked out, we can still save2menu + if ($user->authorise('core.create', 'com_menus.menu')) { + $childBar->save('article.save2menu', 'JTOOLBAR_SAVE_TO_MENU'); + } + + // If checked out, we can still save + if ($canDo->get('core.create')) { + $childBar->save2copy('article.save2copy'); + } + } + ); + + $toolbar->cancel('article.cancel', 'JTOOLBAR_CLOSE'); + + if (!$isNew) { + if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $itemEditable) { + $toolbar->versions('com_content.article', $this->item->id); + } + + $url = RouteHelper::getArticleRoute($this->item->id . ':' . $this->item->alias, $this->item->catid, $this->item->language); + + $toolbar->preview(Route::link('site', $url, true), 'JGLOBAL_PREVIEW') + ->bodyHeight(80) + ->modalWidth(90); + + if (PluginHelper::isEnabled('system', 'jooa11y')) { + $toolbar->jooa11y(Route::link('site', $url . '&jooa11y=1', true), 'JGLOBAL_JOOA11Y') + ->bodyHeight(80) + ->modalWidth(90); + } + + if (Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) { + $toolbar->standardButton('contract') + ->text('JTOOLBAR_ASSOCIATIONS') + ->task('article.editAssociations'); + } + } + } + + $toolbar->divider(); + + ToolbarHelper::inlinehelp(); + + $toolbar->help('Articles:_Edit'); + } } diff --git a/administrator/components/com_content/src/View/Articles/HtmlView.php b/administrator/components/com_content/src/View/Articles/HtmlView.php index 8461c2980cb3d..aea5ba8d2fe43 100644 --- a/administrator/components/com_content/src/View/Articles/HtmlView.php +++ b/administrator/components/com_content/src/View/Articles/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->vote = PluginHelper::isEnabled('content', 'vote'); - $this->hits = ComponentHelper::getParams('com_content')->get('record_hits', 1); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - if (ComponentHelper::getParams('com_content')->get('workflow_enabled')) - { - PluginHelper::importPlugin('workflow'); - - $this->transitions = $this->get('Transitions'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors')) || $this->transitions === false) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // We don't need toolbar in the modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - else - { - // In article associations modal we need to remove language filter if forcing a language. - // We also need to change the category filter to show show categories with All or the forced language. - if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) - { - // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. - $languageXml = new \SimpleXMLElement(''); - $this->filterForm->setField($languageXml, 'filter', true); - - // Also, unset the active language filter so the search tools is not open by default with this filter. - unset($this->activeFilters['language']); - - // One last changes needed is to change the category filter to just show categories with All language or with the forced language. - $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); - } - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_content', 'category', $this->state->get('filter.category_id')); - $user = $this->getCurrentUser(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_CONTENT_ARTICLES_TITLE'), 'copy article'); - - if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) - { - $toolbar->addNew('article.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || \count($this->transitions))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if (\count($this->transitions)) - { - $childBar->separatorButton('transition-headline') - ->text('COM_CONTENT_RUN_TRANSITIONS') - ->buttonClass('text-center py-2 h3'); - - $cmd = "Joomla.submitbutton('articles.runTransition');"; - $messages = "{error: [Joomla.JText._('JLIB_HTML_PLEASE_MAKE_A_SELECTION_FROM_THE_LIST')]}"; - $alert = 'Joomla.renderMessages(' . $messages . ')'; - $cmd = 'if (document.adminForm.boxchecked.value == 0) { ' . $alert . ' } else { ' . $cmd . ' }'; - - foreach ($this->transitions as $transition) - { - $childBar->standardButton('transition') - ->text($transition['text']) - ->buttonClass('transition-' . (int) $transition['value']) - ->icon('icon-project-diagram') - ->onclick('document.adminForm.transition_id.value=' . (int) $transition['value'] . ';' . $cmd); - } - - $childBar->separatorButton('transition-separator'); - } - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('articles.publish')->listCheck(true); - - $childBar->unpublish('articles.unpublish')->listCheck(true); - - $childBar->standardButton('featured') - ->text('JFEATURE') - ->task('articles.featured') - ->listCheck(true); - - $childBar->standardButton('unfeatured') - ->text('JUNFEATURE') - ->task('articles.unfeatured') - ->listCheck(true); - - $childBar->archive('articles.archive')->listCheck(true); - - $childBar->checkin('articles.checkin')->listCheck(true); - - if ($this->state->get('filter.published') != ContentComponent::CONDITION_TRASHED) - { - $childBar->trash('articles.trash')->listCheck(true); - } - } - - // Add a batch button - if ($user->authorise('core.create', 'com_content') - && $user->authorise('core.edit', 'com_content') - && $user->authorise('core.execute.transition', 'com_content')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if (!$this->isEmptyState && $this->state->get('filter.published') == ContentComponent::CONDITION_TRASHED && $canDo->get('core.delete')) - { - $toolbar->delete('articles.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($user->authorise('core.admin', 'com_content') || $user->authorise('core.options', 'com_content')) - { - $toolbar->preferences('com_content'); - } - - $toolbar->help('Articles'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + */ + public $activeFilters; + + /** + * All transition, which can be executed of one if the items + * + * @var array + */ + protected $transitions = []; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->vote = PluginHelper::isEnabled('content', 'vote'); + $this->hits = ComponentHelper::getParams('com_content')->get('record_hits', 1); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + if (ComponentHelper::getParams('com_content')->get('workflow_enabled')) { + PluginHelper::importPlugin('workflow'); + + $this->transitions = $this->get('Transitions'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors')) || $this->transitions === false) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // We don't need toolbar in the modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } else { + // In article associations modal we need to remove language filter if forcing a language. + // We also need to change the category filter to show show categories with All or the forced language. + if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) { + // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. + $languageXml = new \SimpleXMLElement(''); + $this->filterForm->setField($languageXml, 'filter', true); + + // Also, unset the active language filter so the search tools is not open by default with this filter. + unset($this->activeFilters['language']); + + // One last changes needed is to change the category filter to just show categories with All language or with the forced language. + $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); + } + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_content', 'category', $this->state->get('filter.category_id')); + $user = $this->getCurrentUser(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_CONTENT_ARTICLES_TITLE'), 'copy article'); + + if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) { + $toolbar->addNew('article.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || \count($this->transitions))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if (\count($this->transitions)) { + $childBar->separatorButton('transition-headline') + ->text('COM_CONTENT_RUN_TRANSITIONS') + ->buttonClass('text-center py-2 h3'); + + $cmd = "Joomla.submitbutton('articles.runTransition');"; + $messages = "{error: [Joomla.JText._('JLIB_HTML_PLEASE_MAKE_A_SELECTION_FROM_THE_LIST')]}"; + $alert = 'Joomla.renderMessages(' . $messages . ')'; + $cmd = 'if (document.adminForm.boxchecked.value == 0) { ' . $alert . ' } else { ' . $cmd . ' }'; + + foreach ($this->transitions as $transition) { + $childBar->standardButton('transition') + ->text($transition['text']) + ->buttonClass('transition-' . (int) $transition['value']) + ->icon('icon-project-diagram') + ->onclick('document.adminForm.transition_id.value=' . (int) $transition['value'] . ';' . $cmd); + } + + $childBar->separatorButton('transition-separator'); + } + + if ($canDo->get('core.edit.state')) { + $childBar->publish('articles.publish')->listCheck(true); + + $childBar->unpublish('articles.unpublish')->listCheck(true); + + $childBar->standardButton('featured') + ->text('JFEATURE') + ->task('articles.featured') + ->listCheck(true); + + $childBar->standardButton('unfeatured') + ->text('JUNFEATURE') + ->task('articles.unfeatured') + ->listCheck(true); + + $childBar->archive('articles.archive')->listCheck(true); + + $childBar->checkin('articles.checkin')->listCheck(true); + + if ($this->state->get('filter.published') != ContentComponent::CONDITION_TRASHED) { + $childBar->trash('articles.trash')->listCheck(true); + } + } + + // Add a batch button + if ( + $user->authorise('core.create', 'com_content') + && $user->authorise('core.edit', 'com_content') + && $user->authorise('core.execute.transition', 'com_content') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if (!$this->isEmptyState && $this->state->get('filter.published') == ContentComponent::CONDITION_TRASHED && $canDo->get('core.delete')) { + $toolbar->delete('articles.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($user->authorise('core.admin', 'com_content') || $user->authorise('core.options', 'com_content')) { + $toolbar->preferences('com_content'); + } + + $toolbar->help('Articles'); + } } diff --git a/administrator/components/com_content/src/View/Featured/HtmlView.php b/administrator/components/com_content/src/View/Featured/HtmlView.php index ca034b3767112..e4aebfbbba9b6 100644 --- a/administrator/components/com_content/src/View/Featured/HtmlView.php +++ b/administrator/components/com_content/src/View/Featured/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->vote = PluginHelper::isEnabled('content', 'vote'); - $this->hits = ComponentHelper::getParams('com_content')->get('record_hits', 1); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - if (ComponentHelper::getParams('com_content')->get('workflow_enabled')) - { - PluginHelper::importPlugin('workflow'); - - $this->transitions = $this->get('Transitions'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_content', 'category', $this->state->get('filter.category_id')); - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_CONTENT_FEATURED_TITLE'), 'star featured'); - - if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) - { - $toolbar->addNew('article.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || \count($this->transitions))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if (\count($this->transitions)) - { - $childBar->separatorButton('transition-headline') - ->text('COM_CONTENT_RUN_TRANSITIONS') - ->buttonClass('text-center py-2 h3'); - - $cmd = "Joomla.submitbutton('articles.runTransition');"; - $messages = "{error: [Joomla.JText._('JLIB_HTML_PLEASE_MAKE_A_SELECTION_FROM_THE_LIST')]}"; - $alert = 'Joomla.renderMessages(' . $messages . ')'; - $cmd = 'if (document.adminForm.boxchecked.value == 0) { ' . $alert . ' } else { ' . $cmd . ' }'; - - foreach ($this->transitions as $transition) - { - $childBar->standardButton('transition') - ->text($transition['text']) - ->buttonClass('transition-' . (int) $transition['value']) - ->icon('icon-project-diagram') - ->onclick('document.adminForm.transition_id.value=' . (int) $transition['value'] . ';' . $cmd); - } - - $childBar->separatorButton('transition-separator'); - } - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('articles.publish')->listCheck(true); - - $childBar->unpublish('articles.unpublish')->listCheck(true); - - $childBar->standardButton('unfeatured') - ->text('JUNFEATURE') - ->task('articles.unfeatured') - ->listCheck(true); - - $childBar->archive('articles.archive')->listCheck(true); - - $childBar->checkin('articles.checkin')->listCheck(true); - - if (!$this->state->get('filter.published') == ContentComponent::CONDITION_TRASHED) - { - $childBar->trash('articles.trash')->listCheck(true); - } - } - } - - if (!$this->isEmptyState && $this->state->get('filter.published') == ContentComponent::CONDITION_TRASHED && $canDo->get('core.delete')) - { - $toolbar->delete('articles.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($user->authorise('core.admin', 'com_content') || $user->authorise('core.options', 'com_content')) - { - $toolbar->preferences('com_content'); - } - - ToolbarHelper::help('Articles:_Featured'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + */ + public $activeFilters; + + /** + * All transition, which can be executed of one if the items + * + * @var array + */ + protected $transitions = []; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->vote = PluginHelper::isEnabled('content', 'vote'); + $this->hits = ComponentHelper::getParams('com_content')->get('record_hits', 1); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + if (ComponentHelper::getParams('com_content')->get('workflow_enabled')) { + PluginHelper::importPlugin('workflow'); + + $this->transitions = $this->get('Transitions'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_content', 'category', $this->state->get('filter.category_id')); + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_CONTENT_FEATURED_TITLE'), 'star featured'); + + if ($canDo->get('core.create') || \count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) { + $toolbar->addNew('article.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || \count($this->transitions))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if (\count($this->transitions)) { + $childBar->separatorButton('transition-headline') + ->text('COM_CONTENT_RUN_TRANSITIONS') + ->buttonClass('text-center py-2 h3'); + + $cmd = "Joomla.submitbutton('articles.runTransition');"; + $messages = "{error: [Joomla.JText._('JLIB_HTML_PLEASE_MAKE_A_SELECTION_FROM_THE_LIST')]}"; + $alert = 'Joomla.renderMessages(' . $messages . ')'; + $cmd = 'if (document.adminForm.boxchecked.value == 0) { ' . $alert . ' } else { ' . $cmd . ' }'; + + foreach ($this->transitions as $transition) { + $childBar->standardButton('transition') + ->text($transition['text']) + ->buttonClass('transition-' . (int) $transition['value']) + ->icon('icon-project-diagram') + ->onclick('document.adminForm.transition_id.value=' . (int) $transition['value'] . ';' . $cmd); + } + + $childBar->separatorButton('transition-separator'); + } + + if ($canDo->get('core.edit.state')) { + $childBar->publish('articles.publish')->listCheck(true); + + $childBar->unpublish('articles.unpublish')->listCheck(true); + + $childBar->standardButton('unfeatured') + ->text('JUNFEATURE') + ->task('articles.unfeatured') + ->listCheck(true); + + $childBar->archive('articles.archive')->listCheck(true); + + $childBar->checkin('articles.checkin')->listCheck(true); + + if (!$this->state->get('filter.published') == ContentComponent::CONDITION_TRASHED) { + $childBar->trash('articles.trash')->listCheck(true); + } + } + } + + if (!$this->isEmptyState && $this->state->get('filter.published') == ContentComponent::CONDITION_TRASHED && $canDo->get('core.delete')) { + $toolbar->delete('articles.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($user->authorise('core.admin', 'com_content') || $user->authorise('core.options', 'com_content')) { + $toolbar->preferences('com_content'); + } + + ToolbarHelper::help('Articles:_Featured'); + } } diff --git a/administrator/components/com_content/tmpl/article/edit.php b/administrator/components/com_content/tmpl/article/edit.php index 29ce01e10f511..969455f185a16 100644 --- a/administrator/components/com_content/tmpl/article/edit.php +++ b/administrator/components/com_content/tmpl/article/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->getRegistry()->addExtensionRegistryFile('com_contenthistory'); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_contenthistory.admin-history-versions'); + ->useScript('form.validate') + ->useScript('com_contenthistory.admin-history-versions'); $this->configFieldsets = array('editorConfig'); $this->hiddenFieldsets = array('basic-limited'); @@ -42,15 +43,13 @@ $assoc = Associations::isEnabled(); $showArticleOptions = $params->get('show_article_options', 1); -if (!$assoc || !$showArticleOptions) -{ - $this->ignore_fieldsets[] = 'frontendassociations'; +if (!$assoc || !$showArticleOptions) { + $this->ignore_fieldsets[] = 'frontendassociations'; } -if (!$showArticleOptions) -{ - // Ignore fieldsets inside Options tab - $this->ignore_fieldsets = array_merge($this->ignore_fieldsets, ['attribs', 'basic', 'category', 'author', 'date', 'other']); +if (!$showArticleOptions) { + // Ignore fieldsets inside Options tab + $this->ignore_fieldsets = array_merge($this->ignore_fieldsets, ['attribs', 'basic', 'category', 'author', 'date', 'other']); } // In case of modal @@ -59,129 +58,129 @@ $tmpl = $isModal || $input->get('tmpl', '', 'cmd') === 'component' ? '&tmpl=component' : ''; ?> - - -
- 'general', 'recall' => true, 'breakpoint' => 768]); ?> - - -
-
-
-
- form->getLabel('articletext'); ?> - form->getInput('articletext'); ?> -
-
-
-
- -
-
- - - - - get('show_urls_images_backend') == 1) : ?> - -
-
- -
- form->getFieldsets()[$fieldset]->label); ?> -
- form->renderFieldset($fieldset); ?> -
-
- -
-
- -
- form->getFieldsets()[$fieldset]->label); ?> -
- form->renderFieldset($fieldset); ?> -
-
- -
-
- - - - - - - - get('show_publishing_options', 1) == 1) : ?> - -
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
- - - - get('show_associations_edit', 1) == 1) : ?> - -
- -
- -
-
- - - - - - canDo->get('core.admin') && $params->get('show_configure_edit_options', 1) == 1) : ?> - -
- -
- form->renderFieldset('editorConfig'); ?> -
-
- - - - canDo->get('core.admin') && $params->get('show_permissions', 1) == 1) : ?> - -
- -
- form->getInput('rules'); ?> -
-
- - - - - - - get('show_publishing_options', 1) == 0) : ?> - form->getInput('id'); ?> - - - - - - - -
+ + +
+ 'general', 'recall' => true, 'breakpoint' => 768]); ?> + + +
+
+
+
+ form->getLabel('articletext'); ?> + form->getInput('articletext'); ?> +
+
+
+
+ +
+
+ + + + + get('show_urls_images_backend') == 1) : ?> + +
+
+ +
+ form->getFieldsets()[$fieldset]->label); ?> +
+ form->renderFieldset($fieldset); ?> +
+
+ +
+
+ +
+ form->getFieldsets()[$fieldset]->label); ?> +
+ form->renderFieldset($fieldset); ?> +
+
+ +
+
+ + + + + + + + get('show_publishing_options', 1) == 1) : ?> + +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+ + + + get('show_associations_edit', 1) == 1) : ?> + +
+ +
+ +
+
+ + + + + + canDo->get('core.admin') && $params->get('show_configure_edit_options', 1) == 1) : ?> + +
+ +
+ form->renderFieldset('editorConfig'); ?> +
+
+ + + + canDo->get('core.admin') && $params->get('show_permissions', 1) == 1) : ?> + +
+ +
+ form->getInput('rules'); ?> +
+
+ + + + + + + get('show_publishing_options', 1) == 0) : ?> + form->getInput('id'); ?> + + + + + + + +
diff --git a/administrator/components/com_content/tmpl/article/modal.php b/administrator/components/com_content/tmpl/article/modal.php index 9147d5b5ed403..f6d9da1f17513 100644 --- a/administrator/components/com_content/tmpl/article/modal.php +++ b/administrator/components/com_content/tmpl/article/modal.php @@ -1,4 +1,5 @@
- setLayout('edit'); ?> - loadTemplate(); ?> + setLayout('edit'); ?> + loadTemplate(); ?>
diff --git a/administrator/components/com_content/tmpl/article/pagebreak.php b/administrator/components/com_content/tmpl/article/pagebreak.php index 53a8211f88bf3..4612825d783f3 100644 --- a/administrator/components/com_content/tmpl/article/pagebreak.php +++ b/administrator/components/com_content/tmpl/article/pagebreak.php @@ -1,4 +1,5 @@
-
-
-
- -
-
- -
-
-
-
- -
-
- -
-
- - - -
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ + + +
diff --git a/administrator/components/com_content/tmpl/articles/default.php b/administrator/components/com_content/tmpl/articles/default.php index cfbecfcc46a14..5645f3c3323ad 100644 --- a/administrator/components/com_content/tmpl/articles/default.php +++ b/administrator/components/com_content/tmpl/articles/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $app = Factory::getApplication(); $user = Factory::getUser(); @@ -36,27 +37,19 @@ $listDirn = $this->escape($this->state->get('list.direction')); $saveOrder = $listOrder == 'a.ordering'; -if (strpos($listOrder, 'publish_up') !== false) -{ - $orderingColumn = 'publish_up'; -} -elseif (strpos($listOrder, 'publish_down') !== false) -{ - $orderingColumn = 'publish_down'; -} -elseif (strpos($listOrder, 'modified') !== false) -{ - $orderingColumn = 'modified'; -} -else -{ - $orderingColumn = 'created'; +if (strpos($listOrder, 'publish_up') !== false) { + $orderingColumn = 'publish_up'; +} elseif (strpos($listOrder, 'publish_down') !== false) { + $orderingColumn = 'publish_down'; +} elseif (strpos($listOrder, 'modified') !== false) { + $orderingColumn = 'modified'; +} else { + $orderingColumn = 'created'; } -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_content&task=articles.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_content&task=articles.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } $workflow_enabled = ComponentHelper::getParams('com_content')->get('workflow_enabled'); @@ -64,9 +57,8 @@ $workflow_featured = false; if ($workflow_enabled) : - // @todo move the script to a file -$js = <<getRegistry()->addExtensionRegistryFile('com_workflow'); -$wa->useScript('com_workflow.admin-items-workflow-buttons') - ->addInlineScript($js, [], ['type' => 'module']); - -$workflow_state = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.state', 'com_content.article'); -$workflow_featured = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.featured', 'com_content.article'); + $wa->getRegistry()->addExtensionRegistryFile('com_workflow'); + $wa->useScript('com_workflow.admin-items-workflow-buttons') + ->addInlineScript($js, [], ['type' => 'module']); + $workflow_state = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.state', 'com_content.article'); + $workflow_featured = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.featured', 'com_content.article'); endif; $assoc = Associations::isEnabled(); ?>
-
-
-
- $this)); - ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - - - - - - hits) : ?> - - - vote) : ?> - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items as $i => $item) : - $item->max_ordering = 0; - $canEdit = $user->authorise('core.edit', 'com_content.article.' . $item->id); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); - $canEditOwn = $user->authorise('core.edit.own', 'com_content.article.' . $item->id) && $item->created_by == $userId; - $canChange = $user->authorise('core.edit.state', 'com_content.article.' . $item->id) && $canCheckin; - $canEditCat = $user->authorise('core.edit', 'com_content.category.' . $item->catid); - $canEditOwnCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->catid) && $item->category_uid == $userId; - $canEditParCat = $user->authorise('core.edit', 'com_content.category.' . $item->parent_category_id); - $canEditOwnParCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->parent_category_id) && $item->parent_category_uid == $userId; +
+
+
+ $this)); + ?> + items)) : ?> +
+ + +
+ +
- , - , - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ + + + + + + + + + + + + + + + + + + + + hits) : ?> + + + vote) : ?> + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items as $i => $item) : + $item->max_ordering = 0; + $canEdit = $user->authorise('core.edit', 'com_content.article.' . $item->id); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); + $canEditOwn = $user->authorise('core.edit.own', 'com_content.article.' . $item->id) && $item->created_by == $userId; + $canChange = $user->authorise('core.edit.state', 'com_content.article.' . $item->id) && $canCheckin; + $canEditCat = $user->authorise('core.edit', 'com_content.category.' . $item->catid); + $canEditOwnCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->catid) && $item->category_uid == $userId; + $canEditParCat = $user->authorise('core.edit', 'com_content.category.' . $item->parent_category_id); + $canEditOwnParCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->parent_category_id) && $item->parent_category_uid == $userId; - $transitions = ContentHelper::filterTransitions($this->transitions, (int) $item->stage_id, (int) $item->workflow_id); + $transitions = ContentHelper::filterTransitions($this->transitions, (int) $item->stage_id, (int) $item->workflow_id); - $transition_ids = ArrayHelper::getColumn($transitions, 'value'); - $transition_ids = ArrayHelper::toInteger($transition_ids); + $transition_ids = ArrayHelper::getColumn($transitions, 'value'); + $transition_ids = ArrayHelper::toInteger($transition_ids); - ?> - - - - - + + + + - - + + - + - - - - - - - - - - - hits) : ?> - - - vote) : ?> - - - - - - - -
+ , + , + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
- id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - $transitions, - 'title' => Text::_($item->stage_title), - 'tip_content' => Text::sprintf('JWORKFLOW', Text::_($item->workflow_title)), - 'id' => 'workflow-' . $item->id, - 'task' => 'articles.runTransition' - ]; + ?> +
+ id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + $transitions, + 'title' => Text::_($item->stage_title), + 'tip_content' => Text::sprintf('JWORKFLOW', Text::_($item->workflow_title)), + 'id' => 'workflow-' . $item->id, + 'task' => 'articles.runTransition' + ]; - echo (new TransitionButton($options)) - ->render(0, $i); - ?> -
- stage_title); ?> -
-
- 'articles.', - 'disabled' => $workflow_featured || !$canChange, - 'id' => 'featured-' . $item->id - ]; + echo (new TransitionButton($options)) + ->render(0, $i); + ?> +
+ stage_title); ?> +
+
+ 'articles.', + 'disabled' => $workflow_featured || !$canChange, + 'id' => 'featured-' . $item->id + ]; - echo (new FeaturedButton) - ->render((int) $item->featured, $i, $options, $item->featured_up, $item->featured_down); - ?> - - 'articles.', - 'disabled' => $workflow_state || !$canChange, - 'id' => 'state-' . $item->id, - 'category_published' => $item->category_published - ]; + echo (new FeaturedButton()) + ->render((int) $item->featured, $i, $options, $item->featured_up, $item->featured_down); + ?> + + 'articles.', + 'disabled' => $workflow_state || !$canChange, + 'id' => 'state-' . $item->id, + 'category_published' => $item->category_published + ]; - echo (new PublishedButton)->render((int) $item->state, $i, $options, $item->publish_up, $item->publish_down); - ?> - -
- checked_out) : ?> - editor, $item->checked_out_time, 'articles.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - -
- note)) : ?> - escape($item->alias)); ?> - - escape($item->alias), $this->escape($item->note)); ?> - -
-
- parent_category_id . '&extension=com_content'); - $CurrentCatUrl = Route::_('index.php?option=com_categories&task=category.edit&id=' . $item->catid . '&extension=com_content'); - $EditCatTxt = Text::_('COM_CONTENT_EDIT_CATEGORY'); - echo Text::_('JCATEGORY') . ': '; - if ($item->category_level != '1') : - if ($item->parent_category_level != '1') : - echo ' » '; - endif; - endif; - if (Factory::getLanguage()->isRtl()) - { - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - echo $this->escape($item->category_title); - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - if ($item->category_level != '1') : - echo ' « '; - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - echo $this->escape($item->parent_category_title); - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - endif; - } - else - { - if ($item->category_level != '1') : - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - echo $this->escape($item->parent_category_title); - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - echo ' » '; - endif; - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - echo $this->escape($item->category_title); - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - } - if ($item->category_published < '1') : - echo $item->category_published == '0' ? ' (' . Text::_('JUNPUBLISHED') . ')' : ' (' . Text::_('JTRASHED') . ')'; - endif; - ?> -
-
-
- escape($item->access_level); ?> - - created_by != 0) : ?> - - escape($item->author_name); ?> - - - - - created_by_alias) : ?> -
escape($item->created_by_alias)); ?>
- -
- association) : ?> - id); ?> - - - - - {$orderingColumn}; - echo $date > 0 ? HTMLHelper::_('date', $date, Text::_('DATE_FORMAT_LC4')) : '-'; - ?> - - - hits; ?> - - - - rating_count; ?> - - - - rating; ?> - - - id; ?> -
+ echo (new PublishedButton())->render((int) $item->state, $i, $options, $item->publish_up, $item->publish_down); + ?> + + +
+ checked_out) : ?> + editor, $item->checked_out_time, 'articles.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + +
+ note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + +
+
+ parent_category_id . '&extension=com_content'); + $CurrentCatUrl = Route::_('index.php?option=com_categories&task=category.edit&id=' . $item->catid . '&extension=com_content'); + $EditCatTxt = Text::_('COM_CONTENT_EDIT_CATEGORY'); + echo Text::_('JCATEGORY') . ': '; + if ($item->category_level != '1') : + if ($item->parent_category_level != '1') : + echo ' » '; + endif; + endif; + if (Factory::getLanguage()->isRtl()) { + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + echo $this->escape($item->category_title); + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + if ($item->category_level != '1') : + echo ' « '; + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + echo $this->escape($item->parent_category_title); + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + endif; + } else { + if ($item->category_level != '1') : + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + echo $this->escape($item->parent_category_title); + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + echo ' » '; + endif; + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + echo $this->escape($item->category_title); + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + } + if ($item->category_published < '1') : + echo $item->category_published == '0' ? ' (' . Text::_('JUNPUBLISHED') . ')' : ' (' . Text::_('JTRASHED') . ')'; + endif; + ?> +
+
+ + + escape($item->access_level); ?> + + + created_by != 0) : ?> + + escape($item->author_name); ?> + + + + + created_by_alias) : ?> +
escape($item->created_by_alias)); ?>
+ + + + + association) : ?> + id); ?> + + + + + + + + + + {$orderingColumn}; + echo $date > 0 ? HTMLHelper::_('date', $date, Text::_('DATE_FORMAT_LC4')) : '-'; + ?> + + hits) : ?> + + + hits; ?> + + + + vote) : ?> + + + rating_count; ?> + + + + + rating; ?> + + + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', 'com_content') - && $user->authorise('core.edit', 'com_content') - && $user->authorise('core.edit.state', 'com_content')) : ?> - Text::_('COM_CONTENT_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ), - $this->loadTemplate('batch_body') - ); ?> - - + + authorise('core.create', 'com_content') + && $user->authorise('core.edit', 'com_content') + && $user->authorise('core.edit.state', 'com_content') +) : ?> + Text::_('COM_CONTENT_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ), + $this->loadTemplate('batch_body') + ); ?> + + - - - + + + - - - -
-
-
+ + + + + +
diff --git a/administrator/components/com_content/tmpl/articles/default_batch_body.php b/administrator/components/com_content/tmpl/articles/default_batch_body.php index eaa3e08bbea6a..0f838accf462c 100644 --- a/administrator/components/com_content/tmpl/articles/default_batch_body.php +++ b/administrator/components/com_content/tmpl/articles/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Component\ComponentHelper; @@ -21,39 +23,39 @@ ?>
-
- -
-
- -
-
- -
-
- -
-
-
-
- = 0) : ?> -
-
- 'com_content']); ?> -
-
- -
-
- -
-
- authorise('core.admin', 'com_content') && $params->get('workflow_enabled')) : ?> -
-
- 'com_content']); ?> -
-
- -
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ = 0) : ?> +
+
+ 'com_content']); ?> +
+
+ +
+
+ +
+
+ authorise('core.admin', 'com_content') && $params->get('workflow_enabled')) : ?> +
+
+ 'com_content']); ?> +
+
+ +
diff --git a/administrator/components/com_content/tmpl/articles/default_batch_footer.php b/administrator/components/com_content/tmpl/articles/default_batch_footer.php index 3fe481e8ec80c..fd221e578f496 100644 --- a/administrator/components/com_content/tmpl/articles/default_batch_footer.php +++ b/administrator/components/com_content/tmpl/articles/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; @@ -16,8 +18,8 @@ ?> diff --git a/administrator/components/com_content/tmpl/articles/emptystate.php b/administrator/components/com_content/tmpl/articles/emptystate.php index 222805bc2f92f..331fd161984ba 100644 --- a/administrator/components/com_content/tmpl/articles/emptystate.php +++ b/administrator/components/com_content/tmpl/articles/emptystate.php @@ -1,4 +1,5 @@ 'COM_CONTENT', - 'formURL' => 'index.php?option=com_content&view=articles', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Adding_a_new_article', - 'icon' => 'icon-copy article', + 'textPrefix' => 'COM_CONTENT', + 'formURL' => 'index.php?option=com_content&view=articles', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Adding_a_new_article', + 'icon' => 'icon-copy article', ]; $user = Factory::getApplication()->getIdentity(); -if ($user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) -{ - $displayData['createURL'] = 'index.php?option=com_content&task=article.add'; +if ($user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) { + $displayData['createURL'] = 'index.php?option=com_content&task=article.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_content/tmpl/articles/modal.php b/administrator/components/com_content/tmpl/articles/modal.php index 2ed1cdc81c11d..d99d647c4641c 100644 --- a/administrator/components/com_content/tmpl/articles/modal.php +++ b/administrator/components/com_content/tmpl/articles/modal.php @@ -1,4 +1,5 @@ isClient('site')) -{ - Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); +if ($app->isClient('site')) { + Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); } /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('core') - ->useScript('multiselect') - ->useScript('com_content.admin-articles-modal'); + ->useScript('multiselect') + ->useScript('com_content.admin-articles-modal'); $function = $app->input->getCmd('function', 'jSelectArticle'); $editor = $app->input->getCmd('editor', ''); @@ -38,140 +38,132 @@ $onclick = $this->escape($function); $multilang = Multilanguage::isEnabled(); -if (!empty($editor)) -{ - // This view is used also in com_menus. Load the xtd script only if the editor is set! - $this->document->addScriptOptions('xtd-articles', array('editor' => $editor)); - $onclick = "jSelectArticle"; +if (!empty($editor)) { + // This view is used also in com_menus. Load the xtd script only if the editor is set! + $this->document->addScriptOptions('xtd-articles', array('editor' => $editor)); + $onclick = "jSelectArticle"; } ?>
-
+ - $this)); ?> + $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - ); - ?> - items as $i => $item) : ?> - language && $multilang) - { - $tag = strlen($item->language); - if ($tag == 5) - { - $lang = substr($item->language, 0, 2); - } - elseif ($tag == 6) - { - $lang = substr($item->language, 0, 3); - } - else { - $lang = ''; - } - } - elseif (!$multilang) - { - $lang = ''; - } - ?> - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - -
- - - - - escape($onclick) . '"' - . ' data-id="' . $item->id . '"' - . ' data-title="' . $this->escape($item->title) . '"' - . ' data-cat-id="' . $this->escape($item->catid) . '"' - . ' data-uri="' . $this->escape(RouteHelper::getArticleRoute($item->id, $item->catid, $item->language)) . '"' - . ' data-language="' . $this->escape($lang) . '"'; - ?> - > - escape($item->title); ?> - -
- note)) : ?> - escape($item->alias)); ?> - - escape($item->alias), $this->escape($item->note)); ?> - -
-
- escape($item->category_title); ?> -
-
- escape($item->access_level); ?> - - - - created, Text::_('DATE_FORMAT_LC4')); ?> - - id; ?> -
+ items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + ); + ?> + items as $i => $item) : ?> + language && $multilang) { + $tag = strlen($item->language); + if ($tag == 5) { + $lang = substr($item->language, 0, 2); + } elseif ($tag == 6) { + $lang = substr($item->language, 0, 3); + } else { + $lang = ''; + } + } elseif (!$multilang) { + $lang = ''; + } + ?> + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + +
+ + + + + escape($onclick) . '"' + . ' data-id="' . $item->id . '"' + . ' data-title="' . $this->escape($item->title) . '"' + . ' data-cat-id="' . $this->escape($item->catid) . '"' + . ' data-uri="' . $this->escape(RouteHelper::getArticleRoute($item->id, $item->catid, $item->language)) . '"' + . ' data-language="' . $this->escape($lang) . '"'; + ?> + > + escape($item->title); ?> + +
+ note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + +
+
+ escape($item->category_title); ?> +
+
+ escape($item->access_level); ?> + + + + created, Text::_('DATE_FORMAT_LC4')); ?> + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - - + + + + -
+
diff --git a/administrator/components/com_content/tmpl/featured/default.php b/administrator/components/com_content/tmpl/featured/default.php index e719e334c5d2c..d0f9714e2494b 100644 --- a/administrator/components/com_content/tmpl/featured/default.php +++ b/administrator/components/com_content/tmpl/featured/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $app = Factory::getApplication(); $user = Factory::getUser(); @@ -36,28 +37,20 @@ $listDirn = $this->escape($this->state->get('list.direction')); $saveOrder = $listOrder == 'fp.ordering'; -if (strpos($listOrder, 'publish_up') !== false) -{ - $orderingColumn = 'publish_up'; -} -elseif (strpos($listOrder, 'publish_down') !== false) -{ - $orderingColumn = 'publish_down'; -} -elseif (strpos($listOrder, 'modified') !== false) -{ - $orderingColumn = 'modified'; -} -else -{ - $orderingColumn = 'created'; +if (strpos($listOrder, 'publish_up') !== false) { + $orderingColumn = 'publish_up'; +} elseif (strpos($listOrder, 'publish_down') !== false) { + $orderingColumn = 'publish_down'; +} elseif (strpos($listOrder, 'modified') !== false) { + $orderingColumn = 'modified'; +} else { + $orderingColumn = 'created'; } -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_content&task=featured.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_content&task=featured.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } $workflow_enabled = ComponentHelper::getParams('com_content')->get('workflow_enabled'); @@ -65,9 +58,8 @@ $workflow_featured = false; if ($workflow_enabled) : - // @todo move the script to a file -$js = <<getRegistry()->addExtensionRegistryFile('com_workflow'); -$wa->useScript('com_workflow.admin-items-workflow-buttons') - ->addInlineScript($js, [], ['type' => 'module']); - -$workflow_state = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.state', 'com_content.article'); -$workflow_featured = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.featured', 'com_content.article'); + $wa->getRegistry()->addExtensionRegistryFile('com_workflow'); + $wa->useScript('com_workflow.admin-items-workflow-buttons') + ->addInlineScript($js, [], ['type' => 'module']); + $workflow_state = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.state', 'com_content.article'); + $workflow_featured = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.featured', 'com_content.article'); endif; $assoc = Associations::isEnabled(); @@ -95,332 +86,328 @@ ?>
-
-
-
- $this)); - ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - - - - - - hits) : ?> - - - vote) : ?> - - - - - - - class="js-draggable" data-url="" data-direction=""> - items); ?> - items as $i => $item) : - $item->max_ordering = 0; - $ordering = ($listOrder == 'fp.ordering'); - $assetId = 'com_content.article.' . $item->id; - $canCreate = $user->authorise('core.create', 'com_content.category.' . $item->catid); - $canEdit = $user->authorise('core.edit', 'com_content.article.' . $item->id); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_content.article.' . $item->id) && $canCheckin; - $canEditCat = $user->authorise('core.edit', 'com_content.category.' . $item->catid); - $canEditOwnCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->catid) && $item->category_uid == $userId; - $canEditParCat = $user->authorise('core.edit', 'com_content.category.' . $item->parent_category_id); - $canEditOwnParCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->parent_category_id) && $item->parent_category_uid == $userId; +
+
+
+ $this)); + ?> + items)) : ?> +
+ + +
+ +
- , - , - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ + + + + + + + + + + + + + + + + + + + + hits) : ?> + + + vote) : ?> + + + + + + + class="js-draggable" data-url="" data-direction=""> + items); ?> + items as $i => $item) : + $item->max_ordering = 0; + $ordering = ($listOrder == 'fp.ordering'); + $assetId = 'com_content.article.' . $item->id; + $canCreate = $user->authorise('core.create', 'com_content.category.' . $item->catid); + $canEdit = $user->authorise('core.edit', 'com_content.article.' . $item->id); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_content.article.' . $item->id) && $canCheckin; + $canEditCat = $user->authorise('core.edit', 'com_content.category.' . $item->catid); + $canEditOwnCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->catid) && $item->category_uid == $userId; + $canEditParCat = $user->authorise('core.edit', 'com_content.category.' . $item->parent_category_id); + $canEditOwnParCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->parent_category_id) && $item->parent_category_uid == $userId; - $transitions = ContentHelper::filterTransitions($this->transitions, (int) $item->stage_id, (int) $item->workflow_id); + $transitions = ContentHelper::filterTransitions($this->transitions, (int) $item->stage_id, (int) $item->workflow_id); - $transition_ids = ArrayHelper::getColumn($transitions, 'value'); - $transition_ids = ArrayHelper::toInteger($transition_ids); + $transition_ids = ArrayHelper::getColumn($transitions, 'value'); + $transition_ids = ArrayHelper::toInteger($transition_ids); - ?> - - - + + - - + + - - + + - + - - - - - - - - - - - hits) : ?> - - - vote) : ?> - - - - - - - -
+ , + , + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
- id, false, 'cid', 'cb', $item->title); ?> - - +
+ id, false, 'cid', 'cb', $item->title); ?> + + - - - - - - - - $transitions, - 'title' => Text::_($item->stage_title), - 'tip_content' => Text::sprintf('JWORKFLOW', Text::_($item->workflow_title)), - 'id' => 'workflow-' . $item->id, - 'task' => 'articles.runTransitions' - ]; + if (!$canChange) { + $iconClass = ' inactive'; + } elseif (!$saveOrder) { + $iconClass = ' inactive" title="' . Text::_('JORDERINGDISABLED'); + } + ?> + + + + + + + + $transitions, + 'title' => Text::_($item->stage_title), + 'tip_content' => Text::sprintf('JWORKFLOW', Text::_($item->workflow_title)), + 'id' => 'workflow-' . $item->id, + 'task' => 'articles.runTransitions' + ]; - echo (new TransitionButton($options)) - ->render(0, $i); - ?> -
- stage_title); ?> -
-
- 'articles.', - 'disabled' => $workflow_featured || !$canChange, - 'id' => 'featured-' . $item->id - ]; + echo (new TransitionButton($options)) + ->render(0, $i); + ?> +
+ stage_title); ?> +
+
+ 'articles.', + 'disabled' => $workflow_featured || !$canChange, + 'id' => 'featured-' . $item->id + ]; - echo (new FeaturedButton) - ->render((int) $item->featured, $i, $options, $item->featured_up, $item->featured_down); - ?> - - 'articles.', - 'disabled' => $workflow_state || !$canChange, - 'id' => 'state-' . $item->id, - 'category_published' => $item->category_published - ]; + echo (new FeaturedButton()) + ->render((int) $item->featured, $i, $options, $item->featured_up, $item->featured_down); + ?> + + 'articles.', + 'disabled' => $workflow_state || !$canChange, + 'id' => 'state-' . $item->id, + 'category_published' => $item->category_published + ]; - echo (new PublishedButton)->render((int) $item->state, $i, $options, $item->publish_up, $item->publish_down); - ?> - -
- checked_out) : ?> - editor, $item->checked_out_time, 'articles.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - -
- note)) : ?> - escape($item->alias)); ?> - - escape($item->alias), $this->escape($item->note)); ?> - -
-
- parent_category_id . '&extension=com_content'); - $CurrentCatUrl = Route::_('index.php?option=com_categories&task=category.edit&id=' . $item->catid . '&extension=com_content'); - $EditCatTxt = Text::_('COM_CONTENT_EDIT_CATEGORY'); - echo Text::_('JCATEGORY') . ': '; - if ($item->category_level != '1') : - if ($item->parent_category_level != '1') : - echo ' » '; - endif; - endif; - if (Factory::getLanguage()->isRtl()) - { - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - echo $this->escape($item->category_title); - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - if ($item->category_level != '1') : - echo ' « '; - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - echo $this->escape($item->parent_category_title); - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - endif; - } - else - { - if ($item->category_level != '1') : - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - echo $this->escape($item->parent_category_title); - if ($canEditParCat || $canEditOwnParCat) : - echo ''; - endif; - echo ' » '; - endif; - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - echo $this->escape($item->category_title); - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - } - if ($item->category_published < '1') : - echo $item->category_published == '0' ? ' (' . Text::_('JUNPUBLISHED') . ')' : ' (' . Text::_('JTRASHED') . ')'; - endif; - ?> -
-
-
- escape($item->access_level); ?> - - created_by != 0) : ?> - - escape($item->author_name); ?> - - - - - created_by_alias) : ?> -
escape($item->created_by_alias)); ?>
- -
- association) : ?> - id); ?> - - - - - {$orderingColumn}; - echo $date > 0 ? HTMLHelper::_('date', $date, Text::_('DATE_FORMAT_LC4')) : '-'; - ?> - - - hits; ?> - - - - rating_count; ?> - - - - rating; ?> - - - id; ?> -
+ echo (new PublishedButton())->render((int) $item->state, $i, $options, $item->publish_up, $item->publish_down); + ?> + + +
+ checked_out) : ?> + editor, $item->checked_out_time, 'articles.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + +
+ note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + +
+
+ parent_category_id . '&extension=com_content'); + $CurrentCatUrl = Route::_('index.php?option=com_categories&task=category.edit&id=' . $item->catid . '&extension=com_content'); + $EditCatTxt = Text::_('COM_CONTENT_EDIT_CATEGORY'); + echo Text::_('JCATEGORY') . ': '; + if ($item->category_level != '1') : + if ($item->parent_category_level != '1') : + echo ' » '; + endif; + endif; + if (Factory::getLanguage()->isRtl()) { + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + echo $this->escape($item->category_title); + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + if ($item->category_level != '1') : + echo ' « '; + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + echo $this->escape($item->parent_category_title); + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + endif; + } else { + if ($item->category_level != '1') : + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + echo $this->escape($item->parent_category_title); + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + echo ' » '; + endif; + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + echo $this->escape($item->category_title); + if ($canEditCat || $canEditOwnCat) : + echo ''; + endif; + } + if ($item->category_published < '1') : + echo $item->category_published == '0' ? ' (' . Text::_('JUNPUBLISHED') . ')' : ' (' . Text::_('JTRASHED') . ')'; + endif; + ?> +
+
+ + + escape($item->access_level); ?> + + + created_by != 0) : ?> + + escape($item->author_name); ?> + + + + + created_by_alias) : ?> +
escape($item->created_by_alias)); ?>
+ + + + + association) : ?> + id); ?> + + + + + + + + + + {$orderingColumn}; + echo $date > 0 ? HTMLHelper::_('date', $date, Text::_('DATE_FORMAT_LC4')) : '-'; + ?> + + hits) : ?> + + + hits; ?> + + + + vote) : ?> + + + rating_count; ?> + + + + + rating; ?> + + + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - Text::_('JTOOLBAR_CHANGE_STATUS'), - 'footer' => $this->loadTemplate('stage_footer'), - ), - $this->loadTemplate('stage_body') - ); ?> + Text::_('JTOOLBAR_CHANGE_STATUS'), + 'footer' => $this->loadTemplate('stage_footer'), + ), + $this->loadTemplate('stage_body') + ); ?> - + - - - + + + - - - - -
-
-
+ + + + + + +
diff --git a/administrator/components/com_content/tmpl/featured/default_stage_body.php b/administrator/components/com_content/tmpl/featured/default_stage_body.php index 3dadb99058ecc..d5f510e40a892 100644 --- a/administrator/components/com_content/tmpl/featured/default_stage_body.php +++ b/administrator/components/com_content/tmpl/featured/default_stage_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; @@ -13,11 +15,11 @@ ?>
-
-
-

-
-
-
-
+
+
+

+
+
+
+
diff --git a/administrator/components/com_content/tmpl/featured/default_stage_footer.php b/administrator/components/com_content/tmpl/featured/default_stage_footer.php index 886538176cfc0..32ec9abcfb51f 100644 --- a/administrator/components/com_content/tmpl/featured/default_stage_footer.php +++ b/administrator/components/com_content/tmpl/featured/default_stage_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; @@ -16,8 +18,8 @@ ?> diff --git a/administrator/components/com_content/tmpl/featured/emptystate.php b/administrator/components/com_content/tmpl/featured/emptystate.php index 99bc733f48366..83748e92eacc7 100644 --- a/administrator/components/com_content/tmpl/featured/emptystate.php +++ b/administrator/components/com_content/tmpl/featured/emptystate.php @@ -1,4 +1,5 @@ 'COM_CONTENT', - 'formURL' => 'index.php?option=com_content&view=featured', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Adding_a_new_article', + 'textPrefix' => 'COM_CONTENT', + 'formURL' => 'index.php?option=com_content&view=featured', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Adding_a_new_article', ]; $user = Factory::getApplication()->getIdentity(); -if ($user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) -{ - $displayData['createURL'] = 'index.php?option=com_content&task=article.add'; +if ($user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) { + $displayData['createURL'] = 'index.php?option=com_content&task=article.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_contenthistory/helpers/contenthistory.php b/administrator/components/com_contenthistory/helpers/contenthistory.php index 1c4b548e8b989..0858654998ac9 100644 --- a/administrator/components/com_contenthistory/helpers/contenthistory.php +++ b/administrator/components/com_contenthistory/helpers/contenthistory.php @@ -1,13 +1,16 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; + /** * Categories helper. diff --git a/administrator/components/com_contenthistory/services/provider.php b/administrator/components/com_contenthistory/services/provider.php index 5840a2fcce394..b3c3ca13fa596 100644 --- a/administrator/components/com_contenthistory/services/provider.php +++ b/administrator/components/com_contenthistory/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Contenthistory')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Contenthistory')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Contenthistory')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Contenthistory')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_contenthistory/src/Controller/DisplayController.php b/administrator/components/com_contenthistory/src/Controller/DisplayController.php index a7fbefd572536..1ace8ff5c6755 100644 --- a/administrator/components/com_contenthistory/src/Controller/DisplayController.php +++ b/administrator/components/com_contenthistory/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Proxy for getModel. + * + * @param string $name The name of the model + * @param string $prefix The prefix for the model + * @param array $config An additional array of parameters + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model + * + * @since 3.2 + */ + public function getModel($name = 'History', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } - /** - * Toggles the keep forever value for one or more history rows. If it was Yes, changes to No. If No, changes to Yes. - * - * @return void - * - * @since 3.2 - */ - public function keep() - { - $this->checkToken(); + /** + * Toggles the keep forever value for one or more history rows. If it was Yes, changes to No. If No, changes to Yes. + * + * @return void + * + * @since 3.2 + */ + public function keep() + { + $this->checkToken(); - // Get items to toggle keep forever from the request. - $cid = (array) $this->input->get('cid', array(), 'int'); + // Get items to toggle keep forever from the request. + $cid = (array) $this->input->get('cid', array(), 'int'); - // Remove zero values resulting from input filter - $cid = array_filter($cid); + // Remove zero values resulting from input filter + $cid = array_filter($cid); - if (empty($cid)) - { - $this->app->enqueueMessage(Text::_('COM_CONTENTHISTORY_NO_ITEM_SELECTED'), 'warning'); - } - else - { - // Get the model. - $model = $this->getModel(); + if (empty($cid)) { + $this->app->enqueueMessage(Text::_('COM_CONTENTHISTORY_NO_ITEM_SELECTED'), 'warning'); + } else { + // Get the model. + $model = $this->getModel(); - // Toggle keep forever status of the selected items. - if ($model->keep($cid)) - { - $this->setMessage(Text::plural('COM_CONTENTHISTORY_N_ITEMS_KEEP_TOGGLE', count($cid))); - } - else - { - $this->setMessage($model->getError(), 'error'); - } - } + // Toggle keep forever status of the selected items. + if ($model->keep($cid)) { + $this->setMessage(Text::plural('COM_CONTENTHISTORY_N_ITEMS_KEEP_TOGGLE', count($cid))); + } else { + $this->setMessage($model->getError(), 'error'); + } + } - $this->setRedirect( - Route::_( - 'index.php?option=com_contenthistory&view=history&layout=modal&tmpl=component&item_id=' - . $this->input->getCmd('item_id') . '&' . Session::getFormToken() . '=1', false - ) - ); - } + $this->setRedirect( + Route::_( + 'index.php?option=com_contenthistory&view=history&layout=modal&tmpl=component&item_id=' + . $this->input->getCmd('item_id') . '&' . Session::getFormToken() . '=1', + false + ) + ); + } - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - return '&layout=modal&tmpl=component&item_id=' . $this->input->get('item_id') . '&' . Session::getFormToken() . '=1'; - } + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + return '&layout=modal&tmpl=component&item_id=' . $this->input->get('item_id') . '&' . Session::getFormToken() . '=1'; + } } diff --git a/administrator/components/com_contenthistory/src/Controller/PreviewController.php b/administrator/components/com_contenthistory/src/Controller/PreviewController.php index 9c9b14b3bdf07..7eb701c32d455 100644 --- a/administrator/components/com_contenthistory/src/Controller/PreviewController.php +++ b/administrator/components/com_contenthistory/src/Controller/PreviewController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Proxy for getModel. + * + * @param string $name The name of the model + * @param string $prefix The prefix for the model + * @param array $config An additional array of parameters + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model + * + * @since 3.2 + */ + public function getModel($name = 'Preview', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_contenthistory/src/Dispatcher/Dispatcher.php b/administrator/components/com_contenthistory/src/Dispatcher/Dispatcher.php index 61d79a98c955b..0ad304d6c32ca 100644 --- a/administrator/components/com_contenthistory/src/Dispatcher/Dispatcher.php +++ b/administrator/components/com_contenthistory/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->getIdentity()->guest) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + /** + * Method to check component access permission + * + * @since 4.0.0 + * + * @return void + * + * @throws \Exception|NotAllowed + */ + protected function checkAccess() + { + // Check the user has permission to access this component if in the backend + if ($this->app->getIdentity()->guest) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/administrator/components/com_contenthistory/src/Helper/ContenthistoryHelper.php b/administrator/components/com_contenthistory/src/Helper/ContenthistoryHelper.php index 18ab0f48624a9..63052b94cdb3f 100644 --- a/administrator/components/com_contenthistory/src/Helper/ContenthistoryHelper.php +++ b/administrator/components/com_contenthistory/src/Helper/ContenthistoryHelper.php @@ -1,4 +1,5 @@ $value) - { - $result[$name] = $value; - - if (is_object($value)) - { - foreach ($value as $subName => $subValue) - { - $result[$subName] = $subValue; - } - } - } - - return $result; - } - - /** - * Method to decode JSON-encoded fields in a standard object. Used to unpack JSON strings in the content history data column. - * - * @param string $jsonString JSON String to convert to an object. - * - * @return \stdClass Object with any JSON-encoded fields unpacked. - * - * @since 3.2 - */ - public static function decodeFields($jsonString) - { - $object = json_decode($jsonString); - - if (is_object($object)) - { - foreach ($object as $name => $value) - { - if ($subObject = json_decode($value)) - { - $object->$name = $subObject; - } - } - } - - return $object; - } - - /** - * Method to get field labels for the fields in the JSON-encoded object. - * First we see if we can find translatable labels for the fields in the object. - * We translate any we can find and return an array in the format object->name => label. - * - * @param \stdClass $object Standard class object in the format name->value. - * @param ContentType $typesTable Table object with content history options. - * - * @return \stdClass Contains two associative arrays. - * $formValues->labels in the format name => label (for example, 'id' => 'Article ID'). - * $formValues->values in the format name => value (for example, 'state' => 'Published'. - * This translates the text from the selected option in the form. - * - * @since 3.2 - */ - public static function getFormValues($object, ContentType $typesTable) - { - $labels = array(); - $values = array(); - $expandedObjectArray = static::createObjectArray($object); - static::loadLanguageFiles($typesTable->type_alias); - - if ($formFile = static::getFormFile($typesTable)) - { - if ($xml = simplexml_load_file($formFile)) - { - // Now we need to get all of the labels from the form - $fieldArray = $xml->xpath('//field'); - $fieldArray = array_merge($fieldArray, $xml->xpath('//fields')); - - foreach ($fieldArray as $field) - { - if ($label = (string) $field->attributes()->label) - { - $labels[(string) $field->attributes()->name] = Text::_($label); - } - } - - // Get values for any list type fields - $listFieldArray = $xml->xpath('//field[@type="list" or @type="radio"]'); - - foreach ($listFieldArray as $field) - { - $name = (string) $field->attributes()->name; - - if (isset($expandedObjectArray[$name])) - { - $optionFieldArray = $field->xpath('option[@value="' . $expandedObjectArray[$name] . '"]'); - - $valueText = null; - - if (is_array($optionFieldArray) && count($optionFieldArray)) - { - $valueText = trim((string) $optionFieldArray[0]); - } - - $values[(string) $field->attributes()->name] = Text::_($valueText); - } - } - } - } - - $result = new \stdClass; - $result->labels = $labels; - $result->values = $values; - - return $result; - } - - /** - * Method to get the XML form file for this component. Used to get translated field names for history preview. - * - * @param ContentType $typesTable Table object with content history options. - * - * @return mixed \JModel object if successful, false if no model found. - * - * @since 3.2 - */ - public static function getFormFile(ContentType $typesTable) - { - // First, see if we have a file name in the $typesTable - $options = json_decode($typesTable->content_history_options); - - if (is_object($options) && isset($options->formFile) && File::exists(JPATH_ROOT . '/' . $options->formFile)) - { - $result = JPATH_ROOT . '/' . $options->formFile; - } - else - { - $aliasArray = explode('.', $typesTable->type_alias); - $component = ($aliasArray[1] == 'category') ? 'com_categories' : $aliasArray[0]; - $path = Folder::makeSafe(JPATH_ADMINISTRATOR . '/components/' . $component . '/models/forms/'); - array_shift($aliasArray); - $file = File::makeSafe(implode('.', $aliasArray) . '.xml'); - $result = File::exists($path . $file) ? $path . $file : false; - } - - return $result; - } - - /** - * Method to query the database using values from lookup objects. - * - * @param \stdClass $lookup The std object with the values needed to do the query. - * @param mixed $value The value used to find the matching title or name. Typically the id. - * - * @return mixed Value from database (for example, name or title) on success, false on failure. - * - * @since 3.2 - */ - public static function getLookupValue($lookup, $value) - { - $result = false; - - if (isset($lookup->sourceColumn) && isset($lookup->targetTable) && isset($lookup->targetColumn) && isset($lookup->displayColumn)) - { - $db = Factory::getDbo(); - $value = (int) $value; - $query = $db->getQuery(true); - $query->select($db->quoteName($lookup->displayColumn)) - ->from($db->quoteName($lookup->targetTable)) - ->where($db->quoteName($lookup->targetColumn) . ' = :value') - ->bind(':value', $value, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $result = $db->loadResult(); - } - catch (\Exception $e) - { - // Ignore any errors and just return false - return false; - } - } - - return $result; - } - - /** - * Method to remove fields from the object based on values entered in the #__content_types table. - * - * @param \stdClass $object Object to be passed to view layout file. - * @param ContentType $typeTable Table object with content history options. - * - * @return \stdClass object with hidden fields removed. - * - * @since 3.2 - */ - public static function hideFields($object, ContentType $typeTable) - { - if ($options = json_decode($typeTable->content_history_options)) - { - if (isset($options->hideFields) && is_array($options->hideFields)) - { - foreach ($options->hideFields as $field) - { - unset($object->$field); - } - } - } - - return $object; - } - - /** - * Method to load the language files for the component whose history is being viewed. - * - * @param string $typeAlias The type alias, for example 'com_content.article'. - * - * @return void - * - * @since 3.2 - */ - public static function loadLanguageFiles($typeAlias) - { - $aliasArray = explode('.', $typeAlias); - - if (is_array($aliasArray) && count($aliasArray) == 2) - { - $component = ($aliasArray[1] == 'category') ? 'com_categories' : $aliasArray[0]; - $lang = Factory::getLanguage(); - - /** - * Loading language file from the administrator/language directory then - * loading language file from the administrator/components/extension/language directory - */ - $lang->load($component, JPATH_ADMINISTRATOR) - || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); - - // Force loading of backend global language file - $lang->load('joomla', Path::clean(JPATH_ADMINISTRATOR)); - } - } - - /** - * Method to create object to pass to the layout. Format is as follows: - * field is std object with name, value. - * - * Value can be a std object with name, value pairs. - * - * @param \stdClass $object The std object from the JSON string. Can be nested 1 level deep. - * @param \stdClass $formValues Standard class of label and value in an associative array. - * - * @return \stdClass Object with translated labels where available - * - * @since 3.2 - */ - public static function mergeLabels($object, $formValues) - { - $result = new \stdClass; - - if ($object === null) - { - return $result; - } - - $labelsArray = $formValues->labels; - $valuesArray = $formValues->values; - - foreach ($object as $name => $value) - { - $result->$name = new \stdClass; - $result->$name->name = $name; - $result->$name->value = $valuesArray[$name] ?? $value; - $result->$name->label = $labelsArray[$name] ?? $name; - - if (is_object($value)) - { - $subObject = new \stdClass; - - foreach ($value as $subName => $subValue) - { - $subObject->$subName = new \stdClass; - $subObject->$subName->name = $subName; - $subObject->$subName->value = $valuesArray[$subName] ?? $subValue; - $subObject->$subName->label = $labelsArray[$subName] ?? $subName; - $result->$name->value = $subObject; - } - } - } - - return $result; - } - - /** - * Method to prepare the object for the preview and compare views. - * - * @param ContentHistory $table Table object loaded with data. - * - * @return \stdClass Object ready for the views. - * - * @since 3.2 - */ - public static function prepareData(ContentHistory $table) - { - $object = static::decodeFields($table->version_data); - $typesTable = Table::getInstance('ContentType', 'Joomla\\CMS\\Table\\'); - $typeAlias = explode('.', $table->item_id); - array_pop($typeAlias); - $typesTable->load(array('type_alias' => implode('.', $typeAlias))); - $formValues = static::getFormValues($object, $typesTable); - $object = static::mergeLabels($object, $formValues); - $object = static::hideFields($object, $typesTable); - $object = static::processLookupFields($object, $typesTable); - - return $object; - } - - /** - * Method to process any lookup values found in the content_history_options column for this table. - * This allows category title and user name to be displayed instead of the id column. - * - * @param \stdClass $object The std object from the JSON string. Can be nested 1 level deep. - * @param ContentType $typesTable Table object loaded with data. - * - * @return \stdClass Object with lookup values inserted. - * - * @since 3.2 - */ - public static function processLookupFields($object, ContentType $typesTable) - { - if ($options = json_decode($typesTable->content_history_options)) - { - if (isset($options->displayLookup) && is_array($options->displayLookup)) - { - foreach ($options->displayLookup as $lookup) - { - $sourceColumn = $lookup->sourceColumn ?? false; - $sourceValue = $object->$sourceColumn->value ?? false; - - if ($sourceColumn && $sourceValue && ($lookupValue = static::getLookupValue($lookup, $sourceValue))) - { - $object->$sourceColumn->value = $lookupValue; - } - } - } - } - - return $object; - } + /** + * Method to put all field names, including nested ones, in a single array for easy lookup. + * + * @param \stdClass $object Standard class object that may contain one level of nested objects. + * + * @return array Associative array of all field names, including ones in a nested object. + * + * @since 3.2 + */ + public static function createObjectArray($object) + { + $result = array(); + + if ($object === null) { + return $result; + } + + foreach ($object as $name => $value) { + $result[$name] = $value; + + if (is_object($value)) { + foreach ($value as $subName => $subValue) { + $result[$subName] = $subValue; + } + } + } + + return $result; + } + + /** + * Method to decode JSON-encoded fields in a standard object. Used to unpack JSON strings in the content history data column. + * + * @param string $jsonString JSON String to convert to an object. + * + * @return \stdClass Object with any JSON-encoded fields unpacked. + * + * @since 3.2 + */ + public static function decodeFields($jsonString) + { + $object = json_decode($jsonString); + + if (is_object($object)) { + foreach ($object as $name => $value) { + if ($subObject = json_decode($value)) { + $object->$name = $subObject; + } + } + } + + return $object; + } + + /** + * Method to get field labels for the fields in the JSON-encoded object. + * First we see if we can find translatable labels for the fields in the object. + * We translate any we can find and return an array in the format object->name => label. + * + * @param \stdClass $object Standard class object in the format name->value. + * @param ContentType $typesTable Table object with content history options. + * + * @return \stdClass Contains two associative arrays. + * $formValues->labels in the format name => label (for example, 'id' => 'Article ID'). + * $formValues->values in the format name => value (for example, 'state' => 'Published'. + * This translates the text from the selected option in the form. + * + * @since 3.2 + */ + public static function getFormValues($object, ContentType $typesTable) + { + $labels = array(); + $values = array(); + $expandedObjectArray = static::createObjectArray($object); + static::loadLanguageFiles($typesTable->type_alias); + + if ($formFile = static::getFormFile($typesTable)) { + if ($xml = simplexml_load_file($formFile)) { + // Now we need to get all of the labels from the form + $fieldArray = $xml->xpath('//field'); + $fieldArray = array_merge($fieldArray, $xml->xpath('//fields')); + + foreach ($fieldArray as $field) { + if ($label = (string) $field->attributes()->label) { + $labels[(string) $field->attributes()->name] = Text::_($label); + } + } + + // Get values for any list type fields + $listFieldArray = $xml->xpath('//field[@type="list" or @type="radio"]'); + + foreach ($listFieldArray as $field) { + $name = (string) $field->attributes()->name; + + if (isset($expandedObjectArray[$name])) { + $optionFieldArray = $field->xpath('option[@value="' . $expandedObjectArray[$name] . '"]'); + + $valueText = null; + + if (is_array($optionFieldArray) && count($optionFieldArray)) { + $valueText = trim((string) $optionFieldArray[0]); + } + + $values[(string) $field->attributes()->name] = Text::_($valueText); + } + } + } + } + + $result = new \stdClass(); + $result->labels = $labels; + $result->values = $values; + + return $result; + } + + /** + * Method to get the XML form file for this component. Used to get translated field names for history preview. + * + * @param ContentType $typesTable Table object with content history options. + * + * @return mixed \JModel object if successful, false if no model found. + * + * @since 3.2 + */ + public static function getFormFile(ContentType $typesTable) + { + // First, see if we have a file name in the $typesTable + $options = json_decode($typesTable->content_history_options); + + if (is_object($options) && isset($options->formFile) && File::exists(JPATH_ROOT . '/' . $options->formFile)) { + $result = JPATH_ROOT . '/' . $options->formFile; + } else { + $aliasArray = explode('.', $typesTable->type_alias); + $component = ($aliasArray[1] == 'category') ? 'com_categories' : $aliasArray[0]; + $path = Folder::makeSafe(JPATH_ADMINISTRATOR . '/components/' . $component . '/models/forms/'); + array_shift($aliasArray); + $file = File::makeSafe(implode('.', $aliasArray) . '.xml'); + $result = File::exists($path . $file) ? $path . $file : false; + } + + return $result; + } + + /** + * Method to query the database using values from lookup objects. + * + * @param \stdClass $lookup The std object with the values needed to do the query. + * @param mixed $value The value used to find the matching title or name. Typically the id. + * + * @return mixed Value from database (for example, name or title) on success, false on failure. + * + * @since 3.2 + */ + public static function getLookupValue($lookup, $value) + { + $result = false; + + if (isset($lookup->sourceColumn) && isset($lookup->targetTable) && isset($lookup->targetColumn) && isset($lookup->displayColumn)) { + $db = Factory::getDbo(); + $value = (int) $value; + $query = $db->getQuery(true); + $query->select($db->quoteName($lookup->displayColumn)) + ->from($db->quoteName($lookup->targetTable)) + ->where($db->quoteName($lookup->targetColumn) . ' = :value') + ->bind(':value', $value, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $result = $db->loadResult(); + } catch (\Exception $e) { + // Ignore any errors and just return false + return false; + } + } + + return $result; + } + + /** + * Method to remove fields from the object based on values entered in the #__content_types table. + * + * @param \stdClass $object Object to be passed to view layout file. + * @param ContentType $typeTable Table object with content history options. + * + * @return \stdClass object with hidden fields removed. + * + * @since 3.2 + */ + public static function hideFields($object, ContentType $typeTable) + { + if ($options = json_decode($typeTable->content_history_options)) { + if (isset($options->hideFields) && is_array($options->hideFields)) { + foreach ($options->hideFields as $field) { + unset($object->$field); + } + } + } + + return $object; + } + + /** + * Method to load the language files for the component whose history is being viewed. + * + * @param string $typeAlias The type alias, for example 'com_content.article'. + * + * @return void + * + * @since 3.2 + */ + public static function loadLanguageFiles($typeAlias) + { + $aliasArray = explode('.', $typeAlias); + + if (is_array($aliasArray) && count($aliasArray) == 2) { + $component = ($aliasArray[1] == 'category') ? 'com_categories' : $aliasArray[0]; + $lang = Factory::getLanguage(); + + /** + * Loading language file from the administrator/language directory then + * loading language file from the administrator/components/extension/language directory + */ + $lang->load($component, JPATH_ADMINISTRATOR) + || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); + + // Force loading of backend global language file + $lang->load('joomla', Path::clean(JPATH_ADMINISTRATOR)); + } + } + + /** + * Method to create object to pass to the layout. Format is as follows: + * field is std object with name, value. + * + * Value can be a std object with name, value pairs. + * + * @param \stdClass $object The std object from the JSON string. Can be nested 1 level deep. + * @param \stdClass $formValues Standard class of label and value in an associative array. + * + * @return \stdClass Object with translated labels where available + * + * @since 3.2 + */ + public static function mergeLabels($object, $formValues) + { + $result = new \stdClass(); + + if ($object === null) { + return $result; + } + + $labelsArray = $formValues->labels; + $valuesArray = $formValues->values; + + foreach ($object as $name => $value) { + $result->$name = new \stdClass(); + $result->$name->name = $name; + $result->$name->value = $valuesArray[$name] ?? $value; + $result->$name->label = $labelsArray[$name] ?? $name; + + if (is_object($value)) { + $subObject = new \stdClass(); + + foreach ($value as $subName => $subValue) { + $subObject->$subName = new \stdClass(); + $subObject->$subName->name = $subName; + $subObject->$subName->value = $valuesArray[$subName] ?? $subValue; + $subObject->$subName->label = $labelsArray[$subName] ?? $subName; + $result->$name->value = $subObject; + } + } + } + + return $result; + } + + /** + * Method to prepare the object for the preview and compare views. + * + * @param ContentHistory $table Table object loaded with data. + * + * @return \stdClass Object ready for the views. + * + * @since 3.2 + */ + public static function prepareData(ContentHistory $table) + { + $object = static::decodeFields($table->version_data); + $typesTable = Table::getInstance('ContentType', 'Joomla\\CMS\\Table\\'); + $typeAlias = explode('.', $table->item_id); + array_pop($typeAlias); + $typesTable->load(array('type_alias' => implode('.', $typeAlias))); + $formValues = static::getFormValues($object, $typesTable); + $object = static::mergeLabels($object, $formValues); + $object = static::hideFields($object, $typesTable); + $object = static::processLookupFields($object, $typesTable); + + return $object; + } + + /** + * Method to process any lookup values found in the content_history_options column for this table. + * This allows category title and user name to be displayed instead of the id column. + * + * @param \stdClass $object The std object from the JSON string. Can be nested 1 level deep. + * @param ContentType $typesTable Table object loaded with data. + * + * @return \stdClass Object with lookup values inserted. + * + * @since 3.2 + */ + public static function processLookupFields($object, ContentType $typesTable) + { + if ($options = json_decode($typesTable->content_history_options)) { + if (isset($options->displayLookup) && is_array($options->displayLookup)) { + foreach ($options->displayLookup as $lookup) { + $sourceColumn = $lookup->sourceColumn ?? false; + $sourceValue = $object->$sourceColumn->value ?? false; + + if ($sourceColumn && $sourceValue && ($lookupValue = static::getLookupValue($lookup, $sourceValue))) { + $object->$sourceColumn->value = $lookupValue; + } + } + } + } + + return $object; + } } diff --git a/administrator/components/com_contenthistory/src/Model/CompareModel.php b/administrator/components/com_contenthistory/src/Model/CompareModel.php index f9536f923ec05..4347401a2f8d3 100644 --- a/administrator/components/com_contenthistory/src/Model/CompareModel.php +++ b/administrator/components/com_contenthistory/src/Model/CompareModel.php @@ -1,4 +1,5 @@ input; - - /** @var ContentHistory $table1 */ - $table1 = $this->getTable('ContentHistory'); - - /** @var ContentHistory $table2 */ - $table2 = $this->getTable('ContentHistory'); - - $id1 = $input->getInt('id1'); - $id2 = $input->getInt('id2'); - - if (!$id1 || \is_array($id1) || !$id2 || \is_array($id2)) - { - $this->setError(Text::_('COM_CONTENTHISTORY_ERROR_INVALID_ID')); - - return false; - } - - $result = array(); - - if (!$table1->load($id1) || !$table2->load($id2)) - { - $this->setError(Text::_('COM_CONTENTHISTORY_ERROR_VERSION_NOT_FOUND')); - - // Assume a failure to load the content means broken data, abort mission - return false; - } - - // Get the first history record's content type record so we can check ACL - /** @var ContentType $contentTypeTable */ - $contentTypeTable = $this->getTable('ContentType'); - $typeAlias = explode('.', $table1->item_id); - array_pop($typeAlias); - $typeAlias = implode('.', $typeAlias); - - if (!$contentTypeTable->load(array('type_alias' => $typeAlias))) - { - $this->setError(Text::_('COM_CONTENTHISTORY_ERROR_FAILED_LOADING_CONTENT_TYPE')); - - // Assume a failure to load the content type means broken data, abort mission - return false; - } - - $user = Factory::getUser(); - - // Access check - if (!$user->authorise('core.edit', $table1->item_id) && !$this->canEdit($table1)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - $nullDate = $this->getDatabase()->getNullDate(); - - foreach (array($table1, $table2) as $table) - { - $object = new \stdClass; - $object->data = ContenthistoryHelper::prepareData($table); - $object->version_note = $table->version_note; - - // Let's use custom calendars when present - $object->save_date = HTMLHelper::_('date', $table->save_date, Text::_('DATE_FORMAT_LC6')); - - $dateProperties = array ( - 'modified_time', - 'created_time', - 'modified', - 'created', - 'checked_out_time', - 'publish_up', - 'publish_down', - ); - - foreach ($dateProperties as $dateProperty) - { - if (property_exists($object->data, $dateProperty) - && $object->data->$dateProperty->value !== null - && $object->data->$dateProperty->value !== $nullDate) - { - $object->data->$dateProperty->value = HTMLHelper::_( - 'date', - $object->data->$dateProperty->value, - Text::_('DATE_FORMAT_LC6') - ); - } - } - - $result[] = $object; - } - - return $result; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $type The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 3.2 - */ - public function getTable($type = 'Contenthistory', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) - { - return Table::getInstance($type, $prefix, $config); - } - - /** - * Method to test whether a record is editable - * - * @param ContentHistory $record A Table object. - * - * @return boolean True if allowed to edit the record. Defaults to the permission set in the component. - * - * @since 3.6 - */ - protected function canEdit($record) - { - $result = false; - - if (!empty($record->item_id)) - { - /** - * Make sure user has edit privileges for this content item. Note that we use edit permissions - * for the content item, not delete permissions for the content history row. - */ - $user = Factory::getUser(); - $result = $user->authorise('core.edit', $record->item_id); - - // Finally try session (this catches edit.own case too) - if (!$result) - { - /** @var ContentType $contentTypeTable */ - $contentTypeTable = $this->getTable('ContentType'); - - $typeAlias = explode('.', $record->item_id); - $id = array_pop($typeAlias); - $typeAlias = implode('.', $typeAlias); - $contentTypeTable->load(array('type_alias' => $typeAlias)); - $typeEditables = (array) Factory::getApplication()->getUserState(str_replace('.', '.edit.', $contentTypeTable->type_alias) . '.id'); - $result = in_array((int) $id, $typeEditables); - } - } - - return $result; - } + /** + * Method to get a version history row. + * + * @return array|boolean On success, array of populated tables. False on failure. + * + * @since 3.2 + * + * @throws NotAllowed Thrown if not authorised to edit an item + */ + public function getItems() + { + $input = Factory::getApplication()->input; + + /** @var ContentHistory $table1 */ + $table1 = $this->getTable('ContentHistory'); + + /** @var ContentHistory $table2 */ + $table2 = $this->getTable('ContentHistory'); + + $id1 = $input->getInt('id1'); + $id2 = $input->getInt('id2'); + + if (!$id1 || \is_array($id1) || !$id2 || \is_array($id2)) { + $this->setError(Text::_('COM_CONTENTHISTORY_ERROR_INVALID_ID')); + + return false; + } + + $result = array(); + + if (!$table1->load($id1) || !$table2->load($id2)) { + $this->setError(Text::_('COM_CONTENTHISTORY_ERROR_VERSION_NOT_FOUND')); + + // Assume a failure to load the content means broken data, abort mission + return false; + } + + // Get the first history record's content type record so we can check ACL + /** @var ContentType $contentTypeTable */ + $contentTypeTable = $this->getTable('ContentType'); + $typeAlias = explode('.', $table1->item_id); + array_pop($typeAlias); + $typeAlias = implode('.', $typeAlias); + + if (!$contentTypeTable->load(array('type_alias' => $typeAlias))) { + $this->setError(Text::_('COM_CONTENTHISTORY_ERROR_FAILED_LOADING_CONTENT_TYPE')); + + // Assume a failure to load the content type means broken data, abort mission + return false; + } + + $user = Factory::getUser(); + + // Access check + if (!$user->authorise('core.edit', $table1->item_id) && !$this->canEdit($table1)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $nullDate = $this->getDatabase()->getNullDate(); + + foreach (array($table1, $table2) as $table) { + $object = new \stdClass(); + $object->data = ContenthistoryHelper::prepareData($table); + $object->version_note = $table->version_note; + + // Let's use custom calendars when present + $object->save_date = HTMLHelper::_('date', $table->save_date, Text::_('DATE_FORMAT_LC6')); + + $dateProperties = array ( + 'modified_time', + 'created_time', + 'modified', + 'created', + 'checked_out_time', + 'publish_up', + 'publish_down', + ); + + foreach ($dateProperties as $dateProperty) { + if ( + property_exists($object->data, $dateProperty) + && $object->data->$dateProperty->value !== null + && $object->data->$dateProperty->value !== $nullDate + ) { + $object->data->$dateProperty->value = HTMLHelper::_( + 'date', + $object->data->$dateProperty->value, + Text::_('DATE_FORMAT_LC6') + ); + } + } + + $result[] = $object; + } + + return $result; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $type The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 3.2 + */ + public function getTable($type = 'Contenthistory', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) + { + return Table::getInstance($type, $prefix, $config); + } + + /** + * Method to test whether a record is editable + * + * @param ContentHistory $record A Table object. + * + * @return boolean True if allowed to edit the record. Defaults to the permission set in the component. + * + * @since 3.6 + */ + protected function canEdit($record) + { + $result = false; + + if (!empty($record->item_id)) { + /** + * Make sure user has edit privileges for this content item. Note that we use edit permissions + * for the content item, not delete permissions for the content history row. + */ + $user = Factory::getUser(); + $result = $user->authorise('core.edit', $record->item_id); + + // Finally try session (this catches edit.own case too) + if (!$result) { + /** @var ContentType $contentTypeTable */ + $contentTypeTable = $this->getTable('ContentType'); + + $typeAlias = explode('.', $record->item_id); + $id = array_pop($typeAlias); + $typeAlias = implode('.', $typeAlias); + $contentTypeTable->load(array('type_alias' => $typeAlias)); + $typeEditables = (array) Factory::getApplication()->getUserState(str_replace('.', '.edit.', $contentTypeTable->type_alias) . '.id'); + $result = in_array((int) $id, $typeEditables); + } + } + + return $result; + } } diff --git a/administrator/components/com_contenthistory/src/Model/HistoryModel.php b/administrator/components/com_contenthistory/src/Model/HistoryModel.php index ca2976b273d9f..d197182b3beea 100644 --- a/administrator/components/com_contenthistory/src/Model/HistoryModel.php +++ b/administrator/components/com_contenthistory/src/Model/HistoryModel.php @@ -1,4 +1,5 @@ item_id)) - { - return false; - } - - /** - * Make sure user has edit privileges for this content item. Note that we use edit permissions - * for the content item, not delete permissions for the content history row. - */ - $user = Factory::getUser(); - - if ($user->authorise('core.edit', $record->item_id)) - { - return true; - } - - // Finally try session (this catches edit.own case too) - /** @var ContentType $contentTypeTable */ - $contentTypeTable = $this->getTable('ContentType'); - - $typeAlias = explode('.', $record->item_id); - $id = array_pop($typeAlias); - $typeAlias = implode('.', $typeAlias); - $contentTypeTable->load(array('type_alias' => $typeAlias)); - $typeEditables = (array) Factory::getApplication()->getUserState(str_replace('.', '.edit.', $contentTypeTable->type_alias) . '.id'); - $result = in_array((int) $id, $typeEditables); - - return $result; - } - - /** - * Method to test whether a history record can be deleted. Note that we check whether we have edit permissions - * for the content item row. - * - * @param ContentHistory $record A Table object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. - * - * @since 3.6 - */ - protected function canDelete($record) - { - return $this->canEdit($record); - } - - /** - * Method to delete one or more records from content history table. - * - * @param array $pks An array of record primary keys. - * - * @return boolean True if successful, false if an error occurs. - * - * @since 3.2 - */ - public function delete(&$pks) - { - $pks = (array) $pks; - $table = $this->getTable(); - - // Iterate the items to delete each one. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - if ((int) $table->keep_forever === 1) - { - unset($pks[$i]); - continue; - } - - if ($this->canEdit($table)) - { - if (!$table->delete($pk)) - { - $this->setError($table->getError()); - - return false; - } - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - $error = $this->getError(); - - if ($error) - { - try - { - Log::add($error, Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage($error, 'warning'); - } - - return false; - } - else - { - try - { - Log::add(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 'warning'); - } - - return false; - } - } - } - else - { - $this->setError($table->getError()); - - return false; - } - } - - // Clear the component's cache - $this->cleanCache(); - - return true; - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 3.4.5 - * - * @throws NotAllowed Thrown if not authorised to edit an item - */ - public function getItems() - { - $items = parent::getItems(); - $user = Factory::getUser(); - - if ($items === false) - { - return false; - } - - // This should be an array with at least one element - if (!is_array($items) || !isset($items[0])) - { - return $items; - } - - // Access check - if (!$user->authorise('core.edit', $items[0]->item_id) && !$this->canEdit($items[0])) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - return $items; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $type The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 3.2 - */ - public function getTable($type = 'ContentHistory', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) - { - return Table::getInstance($type, $prefix, $config); - } - /** - * Method to toggle on and off the keep forever value for one or more records from content history table. - * - * @param array $pks An array of record primary keys. - * - * @return boolean True if successful, false if an error occurs. - * - * @since 3.2 - */ - public function keep(&$pks) - { - $pks = (array) $pks; - $table = $this->getTable(); - - // Iterate the items to delete each one. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - if ($this->canEdit($table)) - { - $table->keep_forever = $table->keep_forever ? 0 : 1; - - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - $error = $this->getError(); - - if ($error) - { - try - { - Log::add($error, Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage($error, 'warning'); - } - - return false; - } - else - { - try - { - Log::add(Text::_('COM_CONTENTHISTORY_ERROR_KEEP_NOT_PERMITTED'), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_CONTENTHISTORY_ERROR_KEEP_NOT_PERMITTED'), 'warning'); - } - - return false; - } - } - } - else - { - $this->setError($table->getError()); - - return false; - } - } - - // Clear the component's cache - $this->cleanCache(); - - return true; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 3.2 - */ - protected function populateState($ordering = 'h.save_date', $direction = 'DESC') - { - $input = Factory::getApplication()->input; - $itemId = $input->get('item_id', '', 'string'); - - $this->setState('item_id', $itemId); - $this->setState('sha1_hash', $this->getSha1Hash()); - - // Load the parameters. - $params = ComponentHelper::getParams('com_contenthistory'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 3.2 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $itemId = $this->getState('item_id'); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('h.version_id'), - $db->quoteName('h.item_id'), - $db->quoteName('h.version_note'), - $db->quoteName('h.save_date'), - $db->quoteName('h.editor_user_id'), - $db->quoteName('h.character_count'), - $db->quoteName('h.sha1_hash'), - $db->quoteName('h.version_data'), - $db->quoteName('h.keep_forever'), - ] - ) - ) - ->from($db->quoteName('#__history', 'h')) - ->where($db->quoteName('h.item_id') . ' = :itemid') - ->bind(':itemid', $itemId, ParameterType::STRING) - - // Join over the users for the editor - ->select($db->quoteName('uc.name', 'editor')) - ->join('LEFT', - $db->quoteName('#__users', 'uc'), - $db->quoteName('uc.id') . ' = ' . $db->quoteName('h.editor_user_id') - ); - - // Add the list ordering clause. - $orderCol = $this->state->get('list.ordering'); - $orderDirn = $this->state->get('list.direction'); - $query->order($db->quoteName($orderCol) . $orderDirn); - - return $query; - } - - /** - * Get the sha1 hash value for the current item being edited. - * - * @return string sha1 hash of row data - * - * @since 3.2 - */ - protected function getSha1Hash() - { - $result = false; - $item_id = Factory::getApplication()->input->getCmd('item_id', ''); - $typeAlias = explode('.', $item_id); - Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/' . $typeAlias[0] . '/tables'); - $typeTable = $this->getTable('ContentType'); - $typeTable->load(['type_alias' => $typeAlias[0] . '.' . $typeAlias[1]]); - $contentTable = $typeTable->getContentTable(); - - if ($contentTable && $contentTable->load($typeAlias[2])) - { - $helper = new CMSHelper; - - $dataObject = $helper->getDataObject($contentTable); - $result = $this->getTable('ContentHistory')->getSha1(json_encode($dataObject), $typeTable); - } - - return $result; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'version_id', + 'h.version_id', + 'version_note', + 'h.version_note', + 'save_date', + 'h.save_date', + 'editor_user_id', + 'h.editor_user_id', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to test whether a record is editable + * + * @param ContentHistory $record A Table object. + * + * @return boolean True if allowed to edit the record. Defaults to the permission set in the component. + * + * @since 3.2 + */ + protected function canEdit($record) + { + if (empty($record->item_id)) { + return false; + } + + /** + * Make sure user has edit privileges for this content item. Note that we use edit permissions + * for the content item, not delete permissions for the content history row. + */ + $user = Factory::getUser(); + + if ($user->authorise('core.edit', $record->item_id)) { + return true; + } + + // Finally try session (this catches edit.own case too) + /** @var ContentType $contentTypeTable */ + $contentTypeTable = $this->getTable('ContentType'); + + $typeAlias = explode('.', $record->item_id); + $id = array_pop($typeAlias); + $typeAlias = implode('.', $typeAlias); + $contentTypeTable->load(array('type_alias' => $typeAlias)); + $typeEditables = (array) Factory::getApplication()->getUserState(str_replace('.', '.edit.', $contentTypeTable->type_alias) . '.id'); + $result = in_array((int) $id, $typeEditables); + + return $result; + } + + /** + * Method to test whether a history record can be deleted. Note that we check whether we have edit permissions + * for the content item row. + * + * @param ContentHistory $record A Table object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 3.6 + */ + protected function canDelete($record) + { + return $this->canEdit($record); + } + + /** + * Method to delete one or more records from content history table. + * + * @param array $pks An array of record primary keys. + * + * @return boolean True if successful, false if an error occurs. + * + * @since 3.2 + */ + public function delete(&$pks) + { + $pks = (array) $pks; + $table = $this->getTable(); + + // Iterate the items to delete each one. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + if ((int) $table->keep_forever === 1) { + unset($pks[$i]); + continue; + } + + if ($this->canEdit($table)) { + if (!$table->delete($pk)) { + $this->setError($table->getError()); + + return false; + } + } else { + // Prune items that you can't change. + unset($pks[$i]); + $error = $this->getError(); + + if ($error) { + try { + Log::add($error, Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage($error, 'warning'); + } + + return false; + } else { + try { + Log::add(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 'warning'); + } + + return false; + } + } + } else { + $this->setError($table->getError()); + + return false; + } + } + + // Clear the component's cache + $this->cleanCache(); + + return true; + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 3.4.5 + * + * @throws NotAllowed Thrown if not authorised to edit an item + */ + public function getItems() + { + $items = parent::getItems(); + $user = Factory::getUser(); + + if ($items === false) { + return false; + } + + // This should be an array with at least one element + if (!is_array($items) || !isset($items[0])) { + return $items; + } + + // Access check + if (!$user->authorise('core.edit', $items[0]->item_id) && !$this->canEdit($items[0])) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + return $items; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $type The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 3.2 + */ + public function getTable($type = 'ContentHistory', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) + { + return Table::getInstance($type, $prefix, $config); + } + /** + * Method to toggle on and off the keep forever value for one or more records from content history table. + * + * @param array $pks An array of record primary keys. + * + * @return boolean True if successful, false if an error occurs. + * + * @since 3.2 + */ + public function keep(&$pks) + { + $pks = (array) $pks; + $table = $this->getTable(); + + // Iterate the items to delete each one. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + if ($this->canEdit($table)) { + $table->keep_forever = $table->keep_forever ? 0 : 1; + + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + } else { + // Prune items that you can't change. + unset($pks[$i]); + $error = $this->getError(); + + if ($error) { + try { + Log::add($error, Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage($error, 'warning'); + } + + return false; + } else { + try { + Log::add(Text::_('COM_CONTENTHISTORY_ERROR_KEEP_NOT_PERMITTED'), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_('COM_CONTENTHISTORY_ERROR_KEEP_NOT_PERMITTED'), 'warning'); + } + + return false; + } + } + } else { + $this->setError($table->getError()); + + return false; + } + } + + // Clear the component's cache + $this->cleanCache(); + + return true; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.2 + */ + protected function populateState($ordering = 'h.save_date', $direction = 'DESC') + { + $input = Factory::getApplication()->input; + $itemId = $input->get('item_id', '', 'string'); + + $this->setState('item_id', $itemId); + $this->setState('sha1_hash', $this->getSha1Hash()); + + // Load the parameters. + $params = ComponentHelper::getParams('com_contenthistory'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 3.2 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $itemId = $this->getState('item_id'); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('h.version_id'), + $db->quoteName('h.item_id'), + $db->quoteName('h.version_note'), + $db->quoteName('h.save_date'), + $db->quoteName('h.editor_user_id'), + $db->quoteName('h.character_count'), + $db->quoteName('h.sha1_hash'), + $db->quoteName('h.version_data'), + $db->quoteName('h.keep_forever'), + ] + ) + ) + ->from($db->quoteName('#__history', 'h')) + ->where($db->quoteName('h.item_id') . ' = :itemid') + ->bind(':itemid', $itemId, ParameterType::STRING) + + // Join over the users for the editor + ->select($db->quoteName('uc.name', 'editor')) + ->join( + 'LEFT', + $db->quoteName('#__users', 'uc'), + $db->quoteName('uc.id') . ' = ' . $db->quoteName('h.editor_user_id') + ); + + // Add the list ordering clause. + $orderCol = $this->state->get('list.ordering'); + $orderDirn = $this->state->get('list.direction'); + $query->order($db->quoteName($orderCol) . $orderDirn); + + return $query; + } + + /** + * Get the sha1 hash value for the current item being edited. + * + * @return string sha1 hash of row data + * + * @since 3.2 + */ + protected function getSha1Hash() + { + $result = false; + $item_id = Factory::getApplication()->input->getCmd('item_id', ''); + $typeAlias = explode('.', $item_id); + Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/' . $typeAlias[0] . '/tables'); + $typeTable = $this->getTable('ContentType'); + $typeTable->load(['type_alias' => $typeAlias[0] . '.' . $typeAlias[1]]); + $contentTable = $typeTable->getContentTable(); + + if ($contentTable && $contentTable->load($typeAlias[2])) { + $helper = new CMSHelper(); + + $dataObject = $helper->getDataObject($contentTable); + $result = $this->getTable('ContentHistory')->getSha1(json_encode($dataObject), $typeTable); + } + + return $result; + } } diff --git a/administrator/components/com_contenthistory/src/Model/PreviewModel.php b/administrator/components/com_contenthistory/src/Model/PreviewModel.php index 7b10ed902f3db..ea420eba5ec0e 100644 --- a/administrator/components/com_contenthistory/src/Model/PreviewModel.php +++ b/administrator/components/com_contenthistory/src/Model/PreviewModel.php @@ -1,4 +1,5 @@ getTable('ContentHistory'); - $versionId = Factory::getApplication()->input->getInt('version_id'); - - if (!$versionId || \is_array($versionId) || !$table->load($versionId)) - { - return false; - } - - $user = Factory::getUser(); - - // Access check - if (!$user->authorise('core.edit', $table->item_id) && !$this->canEdit($table)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - $result = new \stdClass; - $result->version_note = $table->version_note; - $result->data = ContenthistoryHelper::prepareData($table); - - // Let's use custom calendars when present - $result->save_date = HTMLHelper::_('date', $table->save_date, Text::_('DATE_FORMAT_LC6')); - - $dateProperties = array ( - 'modified_time', - 'created_time', - 'modified', - 'created', - 'checked_out_time', - 'publish_up', - 'publish_down', - ); - - $nullDate = $this->getDatabase()->getNullDate(); - - foreach ($dateProperties as $dateProperty) - { - if (property_exists($result->data, $dateProperty) - && $result->data->$dateProperty->value !== null - && $result->data->$dateProperty->value !== $nullDate) - { - $result->data->$dateProperty->value = HTMLHelper::_( - 'date', - $result->data->$dateProperty->value, - Text::_('DATE_FORMAT_LC6') - ); - } - } - - return $result; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $type The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 3.2 - */ - public function getTable($type = 'ContentHistory', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) - { - return Table::getInstance($type, $prefix, $config); - } - - /** - * Method to test whether a record is editable - * - * @param ContentHistory $record A Table object. - * - * @return boolean True if allowed to edit the record. Defaults to the permission set in the component. - * - * @since 3.6 - */ - protected function canEdit($record) - { - $result = false; - - if (!empty($record->item_id)) - { - /** - * Make sure user has edit privileges for this content item. Note that we use edit permissions - * for the content item, not delete permissions for the content history row. - */ - $user = Factory::getUser(); - $result = $user->authorise('core.edit', $record->item_id); - - // Finally try session (this catches edit.own case too) - if (!$result) - { - /** @var ContentType $contentTypeTable */ - $contentTypeTable = $this->getTable('ContentType'); - - $typeAlias = explode('.', $record->item_id); - $id = array_pop($typeAlias); - $typeAlias = implode('.', $typeAlias); - $typeEditables = (array) Factory::getApplication()->getUserState(str_replace('.', '.edit.', $contentTypeTable->type_alias) . '.id'); - $result = in_array((int) $id, $typeEditables); - } - } - - return $result; - } + /** + * Method to get a version history row. + * + * @param integer $pk The id of the item + * + * @return \stdClass|boolean On success, standard object with row data. False on failure. + * + * @since 3.2 + * + * @throws NotAllowed Thrown if not authorised to edit an item + */ + public function getItem($pk = null) + { + /** @var ContentHistory $table */ + $table = $this->getTable('ContentHistory'); + $versionId = Factory::getApplication()->input->getInt('version_id'); + + if (!$versionId || \is_array($versionId) || !$table->load($versionId)) { + return false; + } + + $user = Factory::getUser(); + + // Access check + if (!$user->authorise('core.edit', $table->item_id) && !$this->canEdit($table)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $result = new \stdClass(); + $result->version_note = $table->version_note; + $result->data = ContenthistoryHelper::prepareData($table); + + // Let's use custom calendars when present + $result->save_date = HTMLHelper::_('date', $table->save_date, Text::_('DATE_FORMAT_LC6')); + + $dateProperties = array ( + 'modified_time', + 'created_time', + 'modified', + 'created', + 'checked_out_time', + 'publish_up', + 'publish_down', + ); + + $nullDate = $this->getDatabase()->getNullDate(); + + foreach ($dateProperties as $dateProperty) { + if ( + property_exists($result->data, $dateProperty) + && $result->data->$dateProperty->value !== null + && $result->data->$dateProperty->value !== $nullDate + ) { + $result->data->$dateProperty->value = HTMLHelper::_( + 'date', + $result->data->$dateProperty->value, + Text::_('DATE_FORMAT_LC6') + ); + } + } + + return $result; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $type The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 3.2 + */ + public function getTable($type = 'ContentHistory', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) + { + return Table::getInstance($type, $prefix, $config); + } + + /** + * Method to test whether a record is editable + * + * @param ContentHistory $record A Table object. + * + * @return boolean True if allowed to edit the record. Defaults to the permission set in the component. + * + * @since 3.6 + */ + protected function canEdit($record) + { + $result = false; + + if (!empty($record->item_id)) { + /** + * Make sure user has edit privileges for this content item. Note that we use edit permissions + * for the content item, not delete permissions for the content history row. + */ + $user = Factory::getUser(); + $result = $user->authorise('core.edit', $record->item_id); + + // Finally try session (this catches edit.own case too) + if (!$result) { + /** @var ContentType $contentTypeTable */ + $contentTypeTable = $this->getTable('ContentType'); + + $typeAlias = explode('.', $record->item_id); + $id = array_pop($typeAlias); + $typeAlias = implode('.', $typeAlias); + $typeEditables = (array) Factory::getApplication()->getUserState(str_replace('.', '.edit.', $contentTypeTable->type_alias) . '.id'); + $result = in_array((int) $id, $typeEditables); + } + } + + return $result; + } } diff --git a/administrator/components/com_contenthistory/src/View/Compare/HtmlView.php b/administrator/components/com_contenthistory/src/View/Compare/HtmlView.php index 8759e6357c9d6..a91d2945f7502 100644 --- a/administrator/components/com_contenthistory/src/View/Compare/HtmlView.php +++ b/administrator/components/com_contenthistory/src/View/Compare/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->items = $this->get('Items'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - parent::display($tpl); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 3.2 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + parent::display($tpl); + } } diff --git a/administrator/components/com_contenthistory/src/View/History/HtmlView.php b/administrator/components/com_contenthistory/src/View/History/HtmlView.php index 77bbc7eb2c289..beba710648a34 100644 --- a/administrator/components/com_contenthistory/src/View/History/HtmlView.php +++ b/administrator/components/com_contenthistory/src/View/History/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->toolbar = $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page toolbar. - * - * @return Toolbar - * - * @since 4.0.0 - */ - protected function addToolbar(): Toolbar - { - /** @var Toolbar $toolbar */ - $toolbar = Factory::getContainer()->get(ToolbarFactoryInterface::class)->createToolbar('toolbar'); - - // Cache a session token for reuse throughout. - $token = Session::getFormToken(); - - // Clean up input to ensure a clean url. - $aliasArray = explode('.', $this->state->item_id); - $option = $aliasArray[1] == 'category' - ? 'com_categories&extension=' . implode('.', array_slice($aliasArray, 0, count($aliasArray) - 2)) - : $aliasArray[0]; - $filter = InputFilter::getInstance(); - $task = $filter->clean($aliasArray[1], 'cmd') . '.loadhistory'; - - // Build the final urls. - $loadUrl = Route::_('index.php?option=' . $filter->clean($option, 'cmd') . '&task=' . $task . '&' . $token . '=1'); - $previewUrl = Route::_('index.php?option=com_contenthistory&view=preview&layout=preview&tmpl=component&' . $token . '=1'); - $compareUrl = Route::_('index.php?option=com_contenthistory&view=compare&layout=compare&tmpl=component&' . $token . '=1'); - - $toolbar->basicButton('load') - ->attributes(['data-url' => $loadUrl]) - ->icon('icon-upload') - ->buttonClass('btn btn-success') - ->text('COM_CONTENTHISTORY_BUTTON_LOAD') - ->listCheck(true); - - $toolbar->basicButton('preview') - ->attributes(['data-url' => $previewUrl]) - ->icon('icon-search') - ->text('COM_CONTENTHISTORY_BUTTON_PREVIEW') - ->listCheck(true); - - $toolbar->basicButton('compare') - ->attributes(['data-url' => $compareUrl]) - ->icon('icon-search-plus') - ->text('COM_CONTENTHISTORY_BUTTON_COMPARE') - ->listCheck(true); - - $toolbar->basicButton('keep') - ->task('history.keep') - ->buttonClass('btn btn-inverse') - ->icon('icon-lock') - ->text('COM_CONTENTHISTORY_BUTTON_KEEP') - ->listCheck(true); - - $toolbar->basicButton('delete') - ->task('history.delete') - ->buttonClass('btn btn-danger') - ->icon('icon-times') - ->text('COM_CONTENTHISTORY_BUTTON_DELETE') - ->listCheck(true); - - return $toolbar; - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The model state + * + * @var Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 3.2 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->toolbar = $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page toolbar. + * + * @return Toolbar + * + * @since 4.0.0 + */ + protected function addToolbar(): Toolbar + { + /** @var Toolbar $toolbar */ + $toolbar = Factory::getContainer()->get(ToolbarFactoryInterface::class)->createToolbar('toolbar'); + + // Cache a session token for reuse throughout. + $token = Session::getFormToken(); + + // Clean up input to ensure a clean url. + $aliasArray = explode('.', $this->state->item_id); + $option = $aliasArray[1] == 'category' + ? 'com_categories&extension=' . implode('.', array_slice($aliasArray, 0, count($aliasArray) - 2)) + : $aliasArray[0]; + $filter = InputFilter::getInstance(); + $task = $filter->clean($aliasArray[1], 'cmd') . '.loadhistory'; + + // Build the final urls. + $loadUrl = Route::_('index.php?option=' . $filter->clean($option, 'cmd') . '&task=' . $task . '&' . $token . '=1'); + $previewUrl = Route::_('index.php?option=com_contenthistory&view=preview&layout=preview&tmpl=component&' . $token . '=1'); + $compareUrl = Route::_('index.php?option=com_contenthistory&view=compare&layout=compare&tmpl=component&' . $token . '=1'); + + $toolbar->basicButton('load') + ->attributes(['data-url' => $loadUrl]) + ->icon('icon-upload') + ->buttonClass('btn btn-success') + ->text('COM_CONTENTHISTORY_BUTTON_LOAD') + ->listCheck(true); + + $toolbar->basicButton('preview') + ->attributes(['data-url' => $previewUrl]) + ->icon('icon-search') + ->text('COM_CONTENTHISTORY_BUTTON_PREVIEW') + ->listCheck(true); + + $toolbar->basicButton('compare') + ->attributes(['data-url' => $compareUrl]) + ->icon('icon-search-plus') + ->text('COM_CONTENTHISTORY_BUTTON_COMPARE') + ->listCheck(true); + + $toolbar->basicButton('keep') + ->task('history.keep') + ->buttonClass('btn btn-inverse') + ->icon('icon-lock') + ->text('COM_CONTENTHISTORY_BUTTON_KEEP') + ->listCheck(true); + + $toolbar->basicButton('delete') + ->task('history.delete') + ->buttonClass('btn btn-danger') + ->icon('icon-times') + ->text('COM_CONTENTHISTORY_BUTTON_DELETE') + ->listCheck(true); + + return $toolbar; + } } diff --git a/administrator/components/com_contenthistory/src/View/Preview/HtmlView.php b/administrator/components/com_contenthistory/src/View/Preview/HtmlView.php index df61b6c29bac8..236a308df8172 100644 --- a/administrator/components/com_contenthistory/src/View/Preview/HtmlView.php +++ b/administrator/components/com_contenthistory/src/View/Preview/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->item = $this->get('Item'); + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 3.2 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->item = $this->get('Item'); - if (false === $this->item) - { - Factory::getLanguage()->load('com_content', JPATH_SITE, null, true); + if (false === $this->item) { + Factory::getLanguage()->load('com_content', JPATH_SITE, null, true); - throw new \Exception(Text::_('COM_CONTENT_ERROR_ARTICLE_NOT_FOUND'), 404); - } + throw new \Exception(Text::_('COM_CONTENT_ERROR_ARTICLE_NOT_FOUND'), 404); + } - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } - parent::display($tpl); - } + parent::display($tpl); + } } diff --git a/administrator/components/com_contenthistory/tmpl/compare/compare.php b/administrator/components/com_contenthistory/tmpl/compare/compare.php index 78e0a3fc2cd7f..c269fdde1fd74 100644 --- a/administrator/components/com_contenthistory/tmpl/compare/compare.php +++ b/administrator/components/com_contenthistory/tmpl/compare/compare.php @@ -1,4 +1,5 @@
-

+

- - - - - - - - - - - - $value) : ?> - value) && isset($object2->$name->value) && $value->value != $object2->$name->value) : ?> - value)) : ?> - - - - value as $subName => $subValue) : ?> - $name->value->$subName->value ?? ''; ?> - value || $newSubValue) : ?> - value != $newSubValue) : ?> - - - - - - - - - - - - - - $name->value = is_object($object2->$name->value) ? json_encode($object2->$name->value) : $object2->$name->value; ?> - - - - - - - -
- -
- label; ?> -
  label; ?>value, ENT_COMPAT, 'UTF-8'); ?> 
- label; ?> - value); ?>$name->value, ENT_COMPAT, 'UTF-8'); ?> 
+ + + + + + + + + + + + $value) : ?> + value) && isset($object2->$name->value) && $value->value != $object2->$name->value) : ?> + value)) : ?> + + + + value as $subName => $subValue) : ?> + $name->value->$subName->value ?? ''; ?> + value || $newSubValue) : ?> + value != $newSubValue) : ?> + + + + + + + + + + + + + + $name->value = is_object($object2->$name->value) ? json_encode($object2->$name->value) : $object2->$name->value; ?> + + + + + + + +
+ +
+ label; ?> +
  label; ?>value, ENT_COMPAT, 'UTF-8'); ?> 
+ label; ?> + value); ?>$name->value, ENT_COMPAT, 'UTF-8'); ?> 
diff --git a/administrator/components/com_contenthistory/tmpl/history/modal.php b/administrator/components/com_contenthistory/tmpl/history/modal.php index 3f6f7cbf96d08..00dd1848e8751 100644 --- a/administrator/components/com_contenthistory/tmpl/history/modal.php +++ b/administrator/components/com_contenthistory/tmpl/history/modal.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('multiselect') - ->useScript('com_contenthistory.admin-history-modal'); + ->useScript('com_contenthistory.admin-history-modal'); ?>
-
- toolbar->render(); ?> -
-
- - - - - - - - - - - - - - - items as $item) : ?> - - - - - - - - - - - -
- -
- - - - - - - - - - - -
- version_id, false, 'cid', 'cb', $item->save_date); ?> - - - save_date, Text::_('DATE_FORMAT_LC6')); ?> - - sha1_hash == $hash) : ?> - - - - version_note); ?> - - keep_forever) : ?> - - - - - - editor); ?> - - character_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')); ?> -
+
+ toolbar->render(); ?> +
+ + + + + + + + + + + + + + + + items as $item) : ?> + + + + + + + + + + + +
+ +
+ + + + + + + + + + + +
+ version_id, false, 'cid', 'cb', $item->save_date); ?> + + + save_date, Text::_('DATE_FORMAT_LC6')); ?> + + sha1_hash == $hash) : ?> + + + + version_note); ?> + + keep_forever) : ?> + + + + + + editor); ?> + + character_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')); ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - + + + -
+
diff --git a/administrator/components/com_contenthistory/tmpl/preview/preview.php b/administrator/components/com_contenthistory/tmpl/preview/preview.php index a97b9f0457739..f6b6cc5ab9743 100644 --- a/administrator/components/com_contenthistory/tmpl/preview/preview.php +++ b/administrator/components/com_contenthistory/tmpl/preview/preview.php @@ -1,4 +1,5 @@
-

- item->save_date); ?> -

- item->version_note) : ?> -

- item->version_note); ?> -

- +

+ item->save_date); ?> +

+ item->version_note) : ?> +

+ item->version_note); ?> +

+ - - - - - - - - - - item->data as $name => $value) : ?> - value)) : ?> - - - - value as $subName => $subValue) : ?> - - - - - - - - - - - - - - - -
- -
- label; ?> -
  label; ?>value; ?>
label; ?>value; ?>
+ + + + + + + + + + item->data as $name => $value) : ?> + value)) : ?> + + + + value as $subName => $subValue) : ?> + + + + + + + + + + + + + + + +
+ +
+ label; ?> +
  label; ?>value; ?>
label; ?>value; ?>
diff --git a/administrator/components/com_cpanel/services/provider.php b/administrator/components/com_cpanel/services/provider.php index 0b42c914f2c14..064bc2bdf3e80 100644 --- a/administrator/components/com_cpanel/services/provider.php +++ b/administrator/components/com_cpanel/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Cpanel')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Cpanel')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Cpanel')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Cpanel')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_cpanel/src/Controller/DisplayController.php b/administrator/components/com_cpanel/src/Controller/DisplayController.php index d49c4635b923a..2f32fcd59b026 100644 --- a/administrator/components/com_cpanel/src/Controller/DisplayController.php +++ b/administrator/components/com_cpanel/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->set('tmpl', 'cpanel'); + /** + * Typical view method for MVC based architecture + * + * This function is provide as a default implementation, in most cases + * 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()}. + * + * @return static An instance of the current object to support chaining. + * + * @since 3.0 + */ + public function display($cachable = false, $urlparams = array()) + { + /* + * Set the template - this will display cpanel.php + * from the selected admin template. + */ + $this->input->set('tmpl', 'cpanel'); - return parent::display($cachable, $urlparams); - } + return parent::display($cachable, $urlparams); + } - /** - * Method to add a module to a dashboard - * - * @since 4.0.0 - * - * @return void - */ - public function addModule() - { - $position = $this->input->get('position', 'cpanel'); - $function = $this->input->get('function'); + /** + * Method to add a module to a dashboard + * + * @since 4.0.0 + * + * @return void + */ + public function addModule() + { + $position = $this->input->get('position', 'cpanel'); + $function = $this->input->get('function'); - $appendLink = ''; + $appendLink = ''; - if ($function) - { - $appendLink .= '&function=' . $function; - } + if ($function) { + $appendLink .= '&function=' . $function; + } - if (substr($position, 0, 6) != 'cpanel') - { - $position = 'cpanel'; - } + if (substr($position, 0, 6) != 'cpanel') { + $position = 'cpanel'; + } - Factory::getApplication()->setUserState('com_modules.modules.filter.position', $position); - Factory::getApplication()->setUserState('com_modules.modules.client_id', '1'); + Factory::getApplication()->setUserState('com_modules.modules.filter.position', $position); + Factory::getApplication()->setUserState('com_modules.modules.client_id', '1'); - $this->setRedirect(Route::_('index.php?option=com_modules&view=select&tmpl=component&layout=modal' . $appendLink, false)); - } + $this->setRedirect(Route::_('index.php?option=com_modules&view=select&tmpl=component&layout=modal' . $appendLink, false)); + } } diff --git a/administrator/components/com_cpanel/src/Dispatcher/Dispatcher.php b/administrator/components/com_cpanel/src/Dispatcher/Dispatcher.php index 2dd0c8c74b23e..089f3159e4f85 100644 --- a/administrator/components/com_cpanel/src/Dispatcher/Dispatcher.php +++ b/administrator/components/com_cpanel/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ input->getCmd('dashboard', ''); - - $position = ApplicationHelper::stringURLSafe($dashboard); - - // Generate a title for the view cpanel - if (!empty($dashboard)) - { - $parts = explode('.', $dashboard); - $component = $parts[0]; - - if (strpos($component, 'com_') === false) - { - $component = 'com_' . $component; - } - - // Need to load the language file - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_BASE) - || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); - $lang->load($component); - - // Lookup dashboard attributes from component manifest file - $manifestFile = JPATH_ADMINISTRATOR . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml'; - - if (is_file($manifestFile)) - { - $manifest = simplexml_load_file($manifestFile); - - if ($dashboardManifests = $manifest->dashboards) - { - foreach ($dashboardManifests->children() as $dashboardManifest) - { - if ((string) $dashboardManifest === $dashboard) - { - $title = Text::_((string) $dashboardManifest->attributes()->title); - $icon = (string) $dashboardManifest->attributes()->icon; - - break; - } - } - } - } - - if (empty($title)) - { - // Try building a title - $prefix = strtoupper($component) . '_DASHBOARD'; - - $sectionkey = !empty($parts[1]) ? '_' . strtoupper($parts[1]) : ''; - $key = $prefix . $sectionkey . '_TITLE'; - $keyIcon = $prefix . $sectionkey . '_ICON'; - - // Search for a component title - if ($lang->hasKey($key)) - { - $title = Text::_($key); - } - else - { - // Try with a string from CPanel - $key = 'COM_CPANEL_DASHBOARD_' . $parts[0] . '_TITLE'; - - if ($lang->hasKey($key)) - { - $title = Text::_($key); - } - else - { - $title = Text::_('COM_CPANEL_DASHBOARD_BASE_TITLE'); - } - } - - // Define the icon - if (empty($parts[1])) - { - // Default core icons. - if ($parts[0] === 'components') - { - $icon = 'icon-puzzle-piece'; - } - elseif ($parts[0] === 'system') - { - $icon = 'icon-wrench'; - } - elseif ($parts[0] === 'help') - { - $icon = 'icon-info-circle'; - } - elseif ($lang->hasKey($keyIcon)) - { - $icon = Text::_($keyIcon); - } - else - { - $icon = 'icon-home'; - } - } - elseif ($lang->hasKey($keyIcon)) - { - $icon = Text::_($keyIcon); - } - } - } - else - { - // Home Dashboard - $title = Text::_('COM_CPANEL_DASHBOARD_BASE_TITLE'); - $icon = 'icon-home'; - } - - // Set toolbar items for the page - ToolbarHelper::title($title, $icon . ' cpanel'); - ToolbarHelper::help('screen.cpanel'); - - // Display the cpanel modules - $this->position = $position ? 'cpanel-' . $position : 'cpanel'; - $this->modules = ModuleHelper::getModules($this->position); - - $quickicons = $position ? 'icon-' . $position : 'icon'; - $this->quickicons = ModuleHelper::getModules($quickicons); - - parent::display($tpl); - } + /** + * Array of cpanel modules + * + * @var array + */ + protected $modules = null; + + /** + * Array of cpanel modules + * + * @var array + */ + protected $quickicons = null; + + /** + * Moduleposition to load + * + * @var string + */ + protected $position = null; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $dashboard = $app->input->getCmd('dashboard', ''); + + $position = ApplicationHelper::stringURLSafe($dashboard); + + // Generate a title for the view cpanel + if (!empty($dashboard)) { + $parts = explode('.', $dashboard); + $component = $parts[0]; + + if (strpos($component, 'com_') === false) { + $component = 'com_' . $component; + } + + // Need to load the language file + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_BASE) + || $lang->load($component, JPATH_ADMINISTRATOR . '/components/' . $component); + $lang->load($component); + + // Lookup dashboard attributes from component manifest file + $manifestFile = JPATH_ADMINISTRATOR . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml'; + + if (is_file($manifestFile)) { + $manifest = simplexml_load_file($manifestFile); + + if ($dashboardManifests = $manifest->dashboards) { + foreach ($dashboardManifests->children() as $dashboardManifest) { + if ((string) $dashboardManifest === $dashboard) { + $title = Text::_((string) $dashboardManifest->attributes()->title); + $icon = (string) $dashboardManifest->attributes()->icon; + + break; + } + } + } + } + + if (empty($title)) { + // Try building a title + $prefix = strtoupper($component) . '_DASHBOARD'; + + $sectionkey = !empty($parts[1]) ? '_' . strtoupper($parts[1]) : ''; + $key = $prefix . $sectionkey . '_TITLE'; + $keyIcon = $prefix . $sectionkey . '_ICON'; + + // Search for a component title + if ($lang->hasKey($key)) { + $title = Text::_($key); + } else { + // Try with a string from CPanel + $key = 'COM_CPANEL_DASHBOARD_' . $parts[0] . '_TITLE'; + + if ($lang->hasKey($key)) { + $title = Text::_($key); + } else { + $title = Text::_('COM_CPANEL_DASHBOARD_BASE_TITLE'); + } + } + + // Define the icon + if (empty($parts[1])) { + // Default core icons. + if ($parts[0] === 'components') { + $icon = 'icon-puzzle-piece'; + } elseif ($parts[0] === 'system') { + $icon = 'icon-wrench'; + } elseif ($parts[0] === 'help') { + $icon = 'icon-info-circle'; + } elseif ($lang->hasKey($keyIcon)) { + $icon = Text::_($keyIcon); + } else { + $icon = 'icon-home'; + } + } elseif ($lang->hasKey($keyIcon)) { + $icon = Text::_($keyIcon); + } + } + } else { + // Home Dashboard + $title = Text::_('COM_CPANEL_DASHBOARD_BASE_TITLE'); + $icon = 'icon-home'; + } + + // Set toolbar items for the page + ToolbarHelper::title($title, $icon . ' cpanel'); + ToolbarHelper::help('screen.cpanel'); + + // Display the cpanel modules + $this->position = $position ? 'cpanel-' . $position : 'cpanel'; + $this->modules = ModuleHelper::getModules($this->position); + + $quickicons = $position ? 'icon-' . $position : 'icon'; + $this->quickicons = ModuleHelper::getModules($quickicons); + + parent::display($tpl); + } } diff --git a/administrator/components/com_cpanel/tmpl/cpanel/default.php b/administrator/components/com_cpanel/tmpl/cpanel/default.php index b11b265e6138c..20d1219de7c63 100644 --- a/administrator/components/com_cpanel/tmpl/cpanel/default.php +++ b/administrator/components/com_cpanel/tmpl/cpanel/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('com_cpanel.admin-cpanel') - ->useScript('com_cpanel.admin-addmodule'); + ->useScript('com_cpanel.admin-addmodule'); $user = Factory::getUser(); // Set up the bootstrap modal that will be used for all module editors echo HTMLHelper::_( - 'bootstrap.renderModal', - 'moduleDashboardAddModal', - array( - 'title' => Text::_('COM_CPANEL_ADD_MODULE_MODAL_TITLE'), - 'backdrop' => 'static', - 'url' => Route::_('index.php?option=com_cpanel&task=addModule&function=jSelectModuleType&position=' . $this->escape($this->position)), - 'bodyHeight' => '70', - 'modalWidth' => '80', - 'footer' => '' - . '', - ) + 'bootstrap.renderModal', + 'moduleDashboardAddModal', + array( + 'title' => Text::_('COM_CPANEL_ADD_MODULE_MODAL_TITLE'), + 'backdrop' => 'static', + 'url' => Route::_('index.php?option=com_cpanel&task=addModule&function=jSelectModuleType&position=' . $this->escape($this->position)), + 'bodyHeight' => '70', + 'modalWidth' => '80', + 'footer' => '' + . '', + ) ); ?>
-
-
- quickicons) : - foreach ($this->quickicons as $iconmodule) - { - echo ModuleHelper::renderModule($iconmodule, array('style' => 'well')); - } - endif; - foreach ($this->modules as $module) - { - echo ModuleHelper::renderModule($module, array('style' => 'well')); - } - ?> - authorise('core.create', 'com_modules')) : ?> -
-
- -
-
- -
-
+
+
+ quickicons) : + foreach ($this->quickicons as $iconmodule) { + echo ModuleHelper::renderModule($iconmodule, array('style' => 'well')); + } + endif; + foreach ($this->modules as $module) { + echo ModuleHelper::renderModule($module, array('style' => 'well')); + } + ?> + authorise('core.create', 'com_modules')) : ?> +
+
+ +
+
+ +
+
diff --git a/administrator/components/com_fields/helpers/fields.php b/administrator/components/com_fields/helpers/fields.php index 81d9091d5f76a..bdb004a98a5b0 100644 --- a/administrator/components/com_fields/helpers/fields.php +++ b/administrator/components/com_fields/helpers/fields.php @@ -1,12 +1,16 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; + + /** * FieldsHelper diff --git a/administrator/components/com_fields/services/provider.php b/administrator/components/com_fields/services/provider.php index 1628402593709..bb7fc5dce97b2 100644 --- a/administrator/components/com_fields/services/provider.php +++ b/administrator/components/com_fields/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Fields')); - $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Fields')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Fields')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Fields')); + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Fields')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Fields')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new FieldsComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new FieldsComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_fields/src/Controller/DisplayController.php b/administrator/components/com_fields/src/Controller/DisplayController.php index 97099ec795850..34d351f15ff91 100644 --- a/administrator/components/com_fields/src/Controller/DisplayController.php +++ b/administrator/components/com_fields/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'fields'); - $id = $this->input->getInt('id'); + /** + * Typical view method for MVC based architecture + * + * This function is provide as a default implementation, in most cases + * you will need to override it in your own controllers. + * + * @param boolean $cachable If true, the view output will be cached + * @param array|bool $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()} + * + * @return BaseController|boolean A Controller object to support chaining. + * + * @since 3.7.0 + */ + public function display($cachable = false, $urlparams = false) + { + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'fields'); + $id = $this->input->getInt('id'); - // Check for edit form. - if ($vName == 'field' && !$this->checkEditId('com_fields.edit.field', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } + // Check for edit form. + if ($vName == 'field' && !$this->checkEditId('com_fields.edit.field', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_fields&view=fields&context=' . $this->input->get('context'), false)); + $this->setRedirect(Route::_('index.php?option=com_fields&view=fields&context=' . $this->input->get('context'), false)); - return false; - } + return false; + } - return parent::display($cachable, $urlparams); - } + return parent::display($cachable, $urlparams); + } } diff --git a/administrator/components/com_fields/src/Controller/FieldController.php b/administrator/components/com_fields/src/Controller/FieldController.php index b0681ddb67185..584022cbf05f9 100644 --- a/administrator/components/com_fields/src/Controller/FieldController.php +++ b/administrator/components/com_fields/src/Controller/FieldController.php @@ -1,4 +1,5 @@ internalContext = $this->app->getUserStateFromRequest('com_fields.fields.context', 'context', 'com_content.article', 'CMD'); - $parts = FieldsHelper::extract($this->internalContext); - $this->component = $parts ? $parts[0] : null; - } - - /** - * Method override to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 3.7.0 - */ - protected function allowAdd($data = array()) - { - return $this->app->getIdentity()->authorise('core.create', $this->component); - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 3.7.0 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - $user = $this->app->getIdentity(); - - // Zero record (id:0), return component edit permission by calling parent controller method - if (!$recordId) - { - return parent::allowEdit($data, $key); - } - - // Check edit on the record asset (explicit or inherited) - if ($user->authorise('core.edit', $this->component . '.field.' . $recordId)) - { - return true; - } - - // Check edit own on the record asset (explicit or inherited) - if ($user->authorise('core.edit.own', $this->component . '.field.' . $recordId)) - { - // Existing record already has an owner, get it - $record = $this->getModel()->getItem($recordId); - - if (empty($record)) - { - return false; - } - - // Grant if current user is owner of the record - return $user->id == $record->created_user_id; - } - - return false; - } - - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 3.7.0 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - $model = $this->getModel('Field'); - - // Preset the redirect - $this->setRedirect('index.php?option=com_fields&view=fields&context=' . $this->internalContext); - - return parent::batch($model); - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 3.7.0 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - return parent::getRedirectToItemAppend($recordId) . '&context=' . $this->internalContext; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 3.7.0 - */ - protected function getRedirectToListAppend() - { - return parent::getRedirectToListAppend() . '&context=' . $this->internalContext; - } - - /** - * Function that allows child controller access to model data after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 3.7.0 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) - { - $item = $model->getItem(); - - if (isset($item->params) && is_array($item->params)) - { - $registry = new Registry; - $registry->loadArray($item->params); - $item->params = (string) $registry; - } - } + /** + * @var string + */ + private $internalContext; + + /** + * @var string + */ + private $component; + + /** + * The prefix to use with controller messages. + * + * @var string + + * @since 3.7.0 + */ + protected $text_prefix = 'COM_FIELDS_FIELD'; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * 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 Application for the dispatcher + * @param Input $input Input + * + * @since 3.7.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->internalContext = $this->app->getUserStateFromRequest('com_fields.fields.context', 'context', 'com_content.article', 'CMD'); + $parts = FieldsHelper::extract($this->internalContext); + $this->component = $parts ? $parts[0] : null; + } + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 3.7.0 + */ + protected function allowAdd($data = array()) + { + return $this->app->getIdentity()->authorise('core.create', $this->component); + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 3.7.0 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + $user = $this->app->getIdentity(); + + // Zero record (id:0), return component edit permission by calling parent controller method + if (!$recordId) { + return parent::allowEdit($data, $key); + } + + // Check edit on the record asset (explicit or inherited) + if ($user->authorise('core.edit', $this->component . '.field.' . $recordId)) { + return true; + } + + // Check edit own on the record asset (explicit or inherited) + if ($user->authorise('core.edit.own', $this->component . '.field.' . $recordId)) { + // Existing record already has an owner, get it + $record = $this->getModel()->getItem($recordId); + + if (empty($record)) { + return false; + } + + // Grant if current user is owner of the record + return $user->id == $record->created_user_id; + } + + return false; + } + + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 3.7.0 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + $model = $this->getModel('Field'); + + // Preset the redirect + $this->setRedirect('index.php?option=com_fields&view=fields&context=' . $this->internalContext); + + return parent::batch($model); + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 3.7.0 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + return parent::getRedirectToItemAppend($recordId) . '&context=' . $this->internalContext; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 3.7.0 + */ + protected function getRedirectToListAppend() + { + return parent::getRedirectToListAppend() . '&context=' . $this->internalContext; + } + + /** + * Function that allows child controller access to model data after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 3.7.0 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) + { + $item = $model->getItem(); + + if (isset($item->params) && is_array($item->params)) { + $registry = new Registry(); + $registry->loadArray($item->params); + $item->params = (string) $registry; + } + } } diff --git a/administrator/components/com_fields/src/Controller/FieldsController.php b/administrator/components/com_fields/src/Controller/FieldsController.php index 9d7ad0c849212..366c2a2d81225 100644 --- a/administrator/components/com_fields/src/Controller/FieldsController.php +++ b/administrator/components/com_fields/src/Controller/FieldsController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Proxy for getModel. + * + * @param string $name The name of the model. + * @param string $prefix The prefix for the PHP class name. + * @param array $config Array of configuration parameters. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel + * + * @since 3.7.0 + */ + public function getModel($name = 'Field', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_fields/src/Controller/GroupController.php b/administrator/components/com_fields/src/Controller/GroupController.php index 1c644954aeed5..ec5ea578d8a50 100644 --- a/administrator/components/com_fields/src/Controller/GroupController.php +++ b/administrator/components/com_fields/src/Controller/GroupController.php @@ -1,4 +1,5 @@ input->getCmd('context')); - - if ($parts) - { - $this->component = $parts[0]; - } - } - - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 3.7.0 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - $model = $this->getModel('Group'); - - // Preset the redirect - $this->setRedirect('index.php?option=com_fields&view=groups'); - - return parent::batch($model); - } - - /** - * Method override to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 3.7.0 - */ - protected function allowAdd($data = array()) - { - return $this->app->getIdentity()->authorise('core.create', $this->component); - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 3.7.0 - */ - protected function allowEdit($data = array(), $key = 'parent_id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - $user = $this->app->getIdentity(); - - // Zero record (parent_id:0), return component edit permission by calling parent controller method - if (!$recordId) - { - return parent::allowEdit($data, $key); - } - - // Check edit on the record asset (explicit or inherited) - if ($user->authorise('core.edit', $this->component . '.fieldgroup.' . $recordId)) - { - return true; - } - - // Check edit own on the record asset (explicit or inherited) - if ($user->authorise('core.edit.own', $this->component . '.fieldgroup.' . $recordId) || $user->authorise('core.edit.own', $this->component)) - { - // Existing record already has an owner, get it - $record = $this->getModel()->getItem($recordId); - - if (empty($record)) - { - return false; - } - - // Grant if current user is owner of the record - return $user->id == $record->created_by; - } - - return false; - } - - /** - * Function that allows child controller access to model data after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 3.7.0 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) - { - $item = $model->getItem(); - - if (isset($item->params) && is_array($item->params)) - { - $registry = new Registry; - $registry->loadArray($item->params); - $item->params = (string) $registry; - } - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $append = parent::getRedirectToItemAppend($recordId); - $append .= '&context=' . $this->input->get('context'); - - return $append; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - $append .= '&context=' . $this->input->get('context'); - - return $append; - } + /** + * The prefix to use with controller messages. + * + * @var string + + * @since 3.7.0 + */ + protected $text_prefix = 'COM_FIELDS_GROUP'; + + /** + * The component for which the group applies. + * + * @var string + * @since 3.7.0 + */ + private $component = ''; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * 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 Application for the dispatcher + * @param Input $input Input + * + * @since 3.7.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $parts = FieldsHelper::extract($this->input->getCmd('context')); + + if ($parts) { + $this->component = $parts[0]; + } + } + + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 3.7.0 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + $model = $this->getModel('Group'); + + // Preset the redirect + $this->setRedirect('index.php?option=com_fields&view=groups'); + + return parent::batch($model); + } + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 3.7.0 + */ + protected function allowAdd($data = array()) + { + return $this->app->getIdentity()->authorise('core.create', $this->component); + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 3.7.0 + */ + protected function allowEdit($data = array(), $key = 'parent_id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + $user = $this->app->getIdentity(); + + // Zero record (parent_id:0), return component edit permission by calling parent controller method + if (!$recordId) { + return parent::allowEdit($data, $key); + } + + // Check edit on the record asset (explicit or inherited) + if ($user->authorise('core.edit', $this->component . '.fieldgroup.' . $recordId)) { + return true; + } + + // Check edit own on the record asset (explicit or inherited) + if ($user->authorise('core.edit.own', $this->component . '.fieldgroup.' . $recordId) || $user->authorise('core.edit.own', $this->component)) { + // Existing record already has an owner, get it + $record = $this->getModel()->getItem($recordId); + + if (empty($record)) { + return false; + } + + // Grant if current user is owner of the record + return $user->id == $record->created_by; + } + + return false; + } + + /** + * Function that allows child controller access to model data after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 3.7.0 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) + { + $item = $model->getItem(); + + if (isset($item->params) && is_array($item->params)) { + $registry = new Registry(); + $registry->loadArray($item->params); + $item->params = (string) $registry; + } + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $append = parent::getRedirectToItemAppend($recordId); + $append .= '&context=' . $this->input->get('context'); + + return $append; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + $append .= '&context=' . $this->input->get('context'); + + return $append; + } } diff --git a/administrator/components/com_fields/src/Controller/GroupsController.php b/administrator/components/com_fields/src/Controller/GroupsController.php index 247217bb79571..7d0b0d4a06e41 100644 --- a/administrator/components/com_fields/src/Controller/GroupsController.php +++ b/administrator/components/com_fields/src/Controller/GroupsController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Proxy for getModel. + * + * @param string $name The name of the model. + * @param string $prefix The prefix for the PHP class name. + * @param array $config Array of configuration parameters. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel + * + * @since 3.7.0 + */ + public function getModel($name = 'Group', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_fields/src/Dispatcher/Dispatcher.php b/administrator/components/com_fields/src/Dispatcher/Dispatcher.php index ba4e8515ae1b3..e90dbd5efb511 100644 --- a/administrator/components/com_fields/src/Dispatcher/Dispatcher.php +++ b/administrator/components/com_fields/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->getUserStateFromRequest( - 'com_fields.groups.context', - 'context', - $this->app->getUserStateFromRequest('com_fields.fields.context', 'context', 'com_content.article', 'CMD'), - 'CMD' - ); + /** + * Method to check component access permission + * + * @since 4.0.0 + * + * @return void + */ + protected function checkAccess() + { + $context = $this->app->getUserStateFromRequest( + 'com_fields.groups.context', + 'context', + $this->app->getUserStateFromRequest('com_fields.fields.context', 'context', 'com_content.article', 'CMD'), + 'CMD' + ); - $parts = FieldsHelper::extract($context); + $parts = FieldsHelper::extract($context); - if (!$parts || !$this->app->getIdentity()->authorise('core.manage', $parts[0])) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + if (!$parts || !$this->app->getIdentity()->authorise('core.manage', $parts[0])) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/administrator/components/com_fields/src/Extension/FieldsComponent.php b/administrator/components/com_fields/src/Extension/FieldsComponent.php index 68fa6b3bc9fb2..3c8b6ce261939 100644 --- a/administrator/components/com_fields/src/Extension/FieldsComponent.php +++ b/administrator/components/com_fields/src/Extension/FieldsComponent.php @@ -1,4 +1,5 @@ getDatabase(); - - $query = $db->getQuery(true) - ->select('DISTINCT a.name AS text, a.element AS value') - ->from('#__extensions as a') - ->where('a.enabled >= 1') - ->where('a.type =' . $db->quote('component')); - - $items = $db->setQuery($query)->loadObjectList(); - - $options = []; - - if (count($items)) - { - $lang = Factory::getLanguage(); - - $components = []; - - // Search for components supporting Fieldgroups - suppose that these components support fields as well - foreach ($items as &$item) - { - $availableActions = Access::getActionsFromFile( - JPATH_ADMINISTRATOR . '/components/' . $item->value . '/access.xml', - "/access/section[@name='fieldgroup']/" - ); - - if (!empty($availableActions)) - { - // Load language - $source = JPATH_ADMINISTRATOR . '/components/' . $item->value; - $lang->load($item->value . 'sys', JPATH_ADMINISTRATOR) - || $lang->load($item->value . 'sys', $source); - - // Translate component name - $item->text = Text::_($item->text); - - $components[] = $item; - } - } - - if (empty($components)) - { - return []; - } - - foreach ($components as $component) - { - // Search for different contexts - $c = Factory::getApplication()->bootComponent($component->value); - - if ($c instanceof FieldsServiceInterface) - { - $contexts = $c->getContexts(); - - foreach ($contexts as $context) - { - $newOption = new \stdClass; - $newOption->value = strtolower($component->value . '.' . $context); - $newOption->text = $component->text . ' - ' . Text::_($context); - $options[] = $newOption; - } - } - else - { - $options[] = $component; - } - } - - // Sort by name - $items = ArrayHelper::sortObjects($options, 'text', 1, true, true); - } - - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $items); - - return $options; - } + /** + * The form field type. + * + * @var string + * @since 3.7.0 + */ + protected $type = 'ComponentsFieldgroup'; + + /** + * Method to get a list of options for a list input. + * + * @return array An array of JHtml options. + * + * @since 3.7.0 + */ + protected function getOptions() + { + // Initialise variable. + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select('DISTINCT a.name AS text, a.element AS value') + ->from('#__extensions as a') + ->where('a.enabled >= 1') + ->where('a.type =' . $db->quote('component')); + + $items = $db->setQuery($query)->loadObjectList(); + + $options = []; + + if (count($items)) { + $lang = Factory::getLanguage(); + + $components = []; + + // Search for components supporting Fieldgroups - suppose that these components support fields as well + foreach ($items as &$item) { + $availableActions = Access::getActionsFromFile( + JPATH_ADMINISTRATOR . '/components/' . $item->value . '/access.xml', + "/access/section[@name='fieldgroup']/" + ); + + if (!empty($availableActions)) { + // Load language + $source = JPATH_ADMINISTRATOR . '/components/' . $item->value; + $lang->load($item->value . 'sys', JPATH_ADMINISTRATOR) + || $lang->load($item->value . 'sys', $source); + + // Translate component name + $item->text = Text::_($item->text); + + $components[] = $item; + } + } + + if (empty($components)) { + return []; + } + + foreach ($components as $component) { + // Search for different contexts + $c = Factory::getApplication()->bootComponent($component->value); + + if ($c instanceof FieldsServiceInterface) { + $contexts = $c->getContexts(); + + foreach ($contexts as $context) { + $newOption = new \stdClass(); + $newOption->value = strtolower($component->value . '.' . $context); + $newOption->text = $component->text . ' - ' . Text::_($context); + $options[] = $newOption; + } + } else { + $options[] = $component; + } + } + + // Sort by name + $items = ArrayHelper::sortObjects($options, 'text', 1, true, true); + } + + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $items); + + return $options; + } } diff --git a/administrator/components/com_fields/src/Field/ComponentsFieldsField.php b/administrator/components/com_fields/src/Field/ComponentsFieldsField.php index b55336b2316e3..358af76a35fd9 100644 --- a/administrator/components/com_fields/src/Field/ComponentsFieldsField.php +++ b/administrator/components/com_fields/src/Field/ComponentsFieldsField.php @@ -1,4 +1,5 @@ getDatabase(); - - $query = $db->getQuery(true) - ->select('DISTINCT a.name AS text, a.element AS value') - ->from('#__extensions as a') - ->where('a.enabled >= 1') - ->where('a.type =' . $db->quote('component')); - - $items = $db->setQuery($query)->loadObjectList(); - - $options = []; - - if (count($items)) - { - $lang = Factory::getLanguage(); - - $components = []; - - // Search for components supporting Fieldgroups - suppose that these components support fields as well - foreach ($items as &$item) - { - $availableActions = Access::getActionsFromFile( - JPATH_ADMINISTRATOR . '/components/' . $item->value . '/access.xml', - "/access/section[@name='fieldgroup']/" - ); - - if (!empty($availableActions)) - { - // Load language - $source = JPATH_ADMINISTRATOR . '/components/' . $item->value; - $lang->load($item->value . 'sys', JPATH_ADMINISTRATOR) - || $lang->load($item->value . 'sys', $source); - - // Translate component name - $item->text = Text::_($item->text); - - $components[] = $item; - } - } - - if (empty($components)) - { - return []; - } - - foreach ($components as $component) - { - // Search for different contexts - $c = Factory::getApplication()->bootComponent($component->value); - - if ($c instanceof FieldsServiceInterface) - { - $contexts = $c->getContexts(); - - foreach ($contexts as $context) - { - $newOption = new \stdClass; - $newOption->value = strtolower($component->value . '.' . $context); - $newOption->text = $component->text . ' - ' . Text::_($context); - $options[] = $newOption; - } - } - else - { - $options[] = $component; - } - } - - // Sort by name - $items = ArrayHelper::sortObjects($options, 'text', 1, true, true); - } - - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $items); - - return $options; - } + /** + * The form field type. + * + * @var string + * @since 3.7.0 + */ + protected $type = 'ComponentsFields'; + + /** + * Method to get a list of options for a list input. + * + * @return array An array of JHtml options. + * + * @since 3.7.0 + */ + protected function getOptions() + { + // Initialise variable. + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select('DISTINCT a.name AS text, a.element AS value') + ->from('#__extensions as a') + ->where('a.enabled >= 1') + ->where('a.type =' . $db->quote('component')); + + $items = $db->setQuery($query)->loadObjectList(); + + $options = []; + + if (count($items)) { + $lang = Factory::getLanguage(); + + $components = []; + + // Search for components supporting Fieldgroups - suppose that these components support fields as well + foreach ($items as &$item) { + $availableActions = Access::getActionsFromFile( + JPATH_ADMINISTRATOR . '/components/' . $item->value . '/access.xml', + "/access/section[@name='fieldgroup']/" + ); + + if (!empty($availableActions)) { + // Load language + $source = JPATH_ADMINISTRATOR . '/components/' . $item->value; + $lang->load($item->value . 'sys', JPATH_ADMINISTRATOR) + || $lang->load($item->value . 'sys', $source); + + // Translate component name + $item->text = Text::_($item->text); + + $components[] = $item; + } + } + + if (empty($components)) { + return []; + } + + foreach ($components as $component) { + // Search for different contexts + $c = Factory::getApplication()->bootComponent($component->value); + + if ($c instanceof FieldsServiceInterface) { + $contexts = $c->getContexts(); + + foreach ($contexts as $context) { + $newOption = new \stdClass(); + $newOption->value = strtolower($component->value . '.' . $context); + $newOption->text = $component->text . ' - ' . Text::_($context); + $options[] = $newOption; + } + } else { + $options[] = $component; + } + } + + // Sort by name + $items = ArrayHelper::sortObjects($options, 'text', 1, true, true); + } + + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $items); + + return $options; + } } diff --git a/administrator/components/com_fields/src/Field/FieldLayoutField.php b/administrator/components/com_fields/src/Field/FieldLayoutField.php index bf45f58c8107c..74c191aecf2e4 100644 --- a/administrator/components/com_fields/src/Field/FieldLayoutField.php +++ b/administrator/components/com_fields/src/Field/FieldLayoutField.php @@ -1,4 +1,5 @@ form->getValue('context')); - $extension = $extension[0]; - - if ($extension) - { - // Get the database object and a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Build the query. - $query->select('element, name') - ->from('#__extensions') - ->where('client_id = 0') - ->where('type = ' . $db->quote('template')) - ->where('enabled = 1'); - - // Set the query and load the templates. - $db->setQuery($query); - $templates = $db->loadObjectList('element'); - - // Build the search paths for component layouts. - $component_path = Path::clean(JPATH_SITE . '/components/' . $extension . '/layouts/field'); - - // Prepare array of component layouts - $component_layouts = array(); - - // Prepare the grouped list - $groups = array(); - - // Add "Use Default" - $groups[]['items'][] = HTMLHelper::_('select.option', '', Text::_('JOPTION_USE_DEFAULT')); - - // Add the layout options from the component path. - if (is_dir($component_path) && ($component_layouts = Folder::files($component_path, '^[^_]*\.php$', false, true))) - { - // Create the group for the component - $groups['_'] = array(); - $groups['_']['id'] = $this->id . '__'; - $groups['_']['text'] = Text::sprintf('JOPTION_FROM_COMPONENT'); - $groups['_']['items'] = array(); - - foreach ($component_layouts as $i => $file) - { - // Add an option to the component group - $value = basename($file, '.php'); - $component_layouts[$i] = $value; - - if ($value === 'render') - { - continue; - } - - $groups['_']['items'][] = HTMLHelper::_('select.option', $value, $value); - } - } - - // Loop on all templates - if ($templates) - { - foreach ($templates as $template) - { - $files = array(); - $template_paths = array( - Path::clean(JPATH_SITE . '/templates/' . $template->element . '/html/layouts/' . $extension . '/field'), - Path::clean(JPATH_SITE . '/templates/' . $template->element . '/html/layouts/com_fields/field'), - Path::clean(JPATH_SITE . '/templates/' . $template->element . '/html/layouts/field'), - ); - - // Add the layout options from the template paths. - foreach ($template_paths as $template_path) - { - if (is_dir($template_path)) - { - $files = array_merge($files, Folder::files($template_path, '^[^_]*\.php$', false, true)); - } - } - - foreach ($files as $i => $file) - { - $value = basename($file, '.php'); - - // Remove the default "render.php" or layout files that exist in the component folder - if ($value === 'render' || in_array($value, $component_layouts)) - { - unset($files[$i]); - } - } - - if (count($files)) - { - // Create the group for the template - $groups[$template->name] = array(); - $groups[$template->name]['id'] = $this->id . '_' . $template->element; - $groups[$template->name]['text'] = Text::sprintf('JOPTION_FROM_TEMPLATE', $template->name); - $groups[$template->name]['items'] = array(); - - foreach ($files as $file) - { - // Add an option to the template group - $value = basename($file, '.php'); - $groups[$template->name]['items'][] = HTMLHelper::_('select.option', $value, $value); - } - } - } - } - - // Compute attributes for the grouped list - $attr = $this->element['size'] ? ' size="' . (int) $this->element['size'] . '"' : ''; - $attr .= $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; - - // Prepare HTML code - $html = array(); - - // Compute the current selected values - $selected = array($this->value); - - // Add a grouped list - $html[] = HTMLHelper::_( - 'select.groupedlist', $groups, $this->name, - array('id' => $this->id, 'group.id' => 'id', 'list.attr' => $attr, 'list.select' => $selected) - ); - - return implode($html); - } - - return ''; - } + /** + * The form field type. + * + * @var string + * @since 3.9.0 + */ + protected $type = 'FieldLayout'; + + /** + * Method to get the field input for a field layout field. + * + * @return string The field input. + * + * @since 3.9.0 + */ + protected function getInput() + { + $extension = explode('.', $this->form->getValue('context')); + $extension = $extension[0]; + + if ($extension) { + // Get the database object and a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Build the query. + $query->select('element, name') + ->from('#__extensions') + ->where('client_id = 0') + ->where('type = ' . $db->quote('template')) + ->where('enabled = 1'); + + // Set the query and load the templates. + $db->setQuery($query); + $templates = $db->loadObjectList('element'); + + // Build the search paths for component layouts. + $component_path = Path::clean(JPATH_SITE . '/components/' . $extension . '/layouts/field'); + + // Prepare array of component layouts + $component_layouts = array(); + + // Prepare the grouped list + $groups = array(); + + // Add "Use Default" + $groups[]['items'][] = HTMLHelper::_('select.option', '', Text::_('JOPTION_USE_DEFAULT')); + + // Add the layout options from the component path. + if (is_dir($component_path) && ($component_layouts = Folder::files($component_path, '^[^_]*\.php$', false, true))) { + // Create the group for the component + $groups['_'] = array(); + $groups['_']['id'] = $this->id . '__'; + $groups['_']['text'] = Text::sprintf('JOPTION_FROM_COMPONENT'); + $groups['_']['items'] = array(); + + foreach ($component_layouts as $i => $file) { + // Add an option to the component group + $value = basename($file, '.php'); + $component_layouts[$i] = $value; + + if ($value === 'render') { + continue; + } + + $groups['_']['items'][] = HTMLHelper::_('select.option', $value, $value); + } + } + + // Loop on all templates + if ($templates) { + foreach ($templates as $template) { + $files = array(); + $template_paths = array( + Path::clean(JPATH_SITE . '/templates/' . $template->element . '/html/layouts/' . $extension . '/field'), + Path::clean(JPATH_SITE . '/templates/' . $template->element . '/html/layouts/com_fields/field'), + Path::clean(JPATH_SITE . '/templates/' . $template->element . '/html/layouts/field'), + ); + + // Add the layout options from the template paths. + foreach ($template_paths as $template_path) { + if (is_dir($template_path)) { + $files = array_merge($files, Folder::files($template_path, '^[^_]*\.php$', false, true)); + } + } + + foreach ($files as $i => $file) { + $value = basename($file, '.php'); + + // Remove the default "render.php" or layout files that exist in the component folder + if ($value === 'render' || in_array($value, $component_layouts)) { + unset($files[$i]); + } + } + + if (count($files)) { + // Create the group for the template + $groups[$template->name] = array(); + $groups[$template->name]['id'] = $this->id . '_' . $template->element; + $groups[$template->name]['text'] = Text::sprintf('JOPTION_FROM_TEMPLATE', $template->name); + $groups[$template->name]['items'] = array(); + + foreach ($files as $file) { + // Add an option to the template group + $value = basename($file, '.php'); + $groups[$template->name]['items'][] = HTMLHelper::_('select.option', $value, $value); + } + } + } + } + + // Compute attributes for the grouped list + $attr = $this->element['size'] ? ' size="' . (int) $this->element['size'] . '"' : ''; + $attr .= $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; + + // Prepare HTML code + $html = array(); + + // Compute the current selected values + $selected = array($this->value); + + // Add a grouped list + $html[] = HTMLHelper::_( + 'select.groupedlist', + $groups, + $this->name, + array('id' => $this->id, 'group.id' => 'id', 'list.attr' => $attr, 'list.select' => $selected) + ); + + return implode($html); + } + + return ''; + } } diff --git a/administrator/components/com_fields/src/Field/FieldcontextsField.php b/administrator/components/com_fields/src/Field/FieldcontextsField.php index 3f223313bad9b..00d2d91c246ff 100644 --- a/administrator/components/com_fields/src/Field/FieldcontextsField.php +++ b/administrator/components/com_fields/src/Field/FieldcontextsField.php @@ -1,4 +1,5 @@ getOptions() ? parent::getInput() : ''; - } - - /** - * Method to get the field options. - * - * @return array The field option objects. - * - * @since 3.7.0 - */ - protected function getOptions() - { - $parts = explode('.', $this->value); - - $component = Factory::getApplication()->bootComponent($parts[0]); - - if ($component instanceof FieldsServiceInterface) - { - return $component->getContexts(); - } - - return []; - } + /** + * Type of the field + * + * @var string + */ + public $type = 'Fieldcontexts'; + + /** + * Method to get the field input markup for a generic list. + * Use the multiple attribute to enable multiselect. + * + * @return string The field input markup. + * + * @since 3.7.0 + */ + protected function getInput() + { + return $this->getOptions() ? parent::getInput() : ''; + } + + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.7.0 + */ + protected function getOptions() + { + $parts = explode('.', $this->value); + + $component = Factory::getApplication()->bootComponent($parts[0]); + + if ($component instanceof FieldsServiceInterface) { + return $component->getContexts(); + } + + return []; + } } diff --git a/administrator/components/com_fields/src/Field/FieldgroupsField.php b/administrator/components/com_fields/src/Field/FieldgroupsField.php index c8a8b24746f68..28cac368bc6ef 100644 --- a/administrator/components/com_fields/src/Field/FieldgroupsField.php +++ b/administrator/components/com_fields/src/Field/FieldgroupsField.php @@ -1,4 +1,5 @@ element['context']; - $states = $this->element['state'] ?: '0,1'; - $states = ArrayHelper::toInteger(explode(',', $states)); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.7.0 + */ + protected function getOptions() + { + $context = (string) $this->element['context']; + $states = $this->element['state'] ?: '0,1'; + $states = ArrayHelper::toInteger(explode(',', $states)); - $user = Factory::getUser(); - $viewlevels = ArrayHelper::toInteger($user->getAuthorisedViewLevels()); + $user = Factory::getUser(); + $viewlevels = ArrayHelper::toInteger($user->getAuthorisedViewLevels()); - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $query->select( - [ - $db->quoteName('title', 'text'), - $db->quoteName('id', 'value'), - $db->quoteName('state'), - ] - ); - $query->from($db->quoteName('#__fields_groups')); - $query->whereIn($db->quoteName('state'), $states); - $query->where($db->quoteName('context') . ' = :context'); - $query->whereIn($db->quoteName('access'), $viewlevels); - $query->order('ordering asc, id asc'); - $query->bind(':context', $context); + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('title', 'text'), + $db->quoteName('id', 'value'), + $db->quoteName('state'), + ] + ); + $query->from($db->quoteName('#__fields_groups')); + $query->whereIn($db->quoteName('state'), $states); + $query->where($db->quoteName('context') . ' = :context'); + $query->whereIn($db->quoteName('access'), $viewlevels); + $query->order('ordering asc, id asc'); + $query->bind(':context', $context); - $db->setQuery($query); - $options = $db->loadObjectList(); + $db->setQuery($query); + $options = $db->loadObjectList(); - foreach ($options AS $option) - { - if ($option->state == 0) - { - $option->text = '[' . $option->text . ']'; - } + foreach ($options as $option) { + if ($option->state == 0) { + $option->text = '[' . $option->text . ']'; + } - if ($option->state == 2) - { - $option->text = '{' . $option->text . '}'; - } - } + if ($option->state == 2) { + $option->text = '{' . $option->text . '}'; + } + } - return array_merge(parent::getOptions(), $options); - } + return array_merge(parent::getOptions(), $options); + } } diff --git a/administrator/components/com_fields/src/Field/SectionField.php b/administrator/components/com_fields/src/Field/SectionField.php index 2c3549c7b1a3e..651bcecafd826 100644 --- a/administrator/components/com_fields/src/Field/SectionField.php +++ b/administrator/components/com_fields/src/Field/SectionField.php @@ -1,4 +1,5 @@ ` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @since 3.7.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $return = parent::setup($element, $value, $group); + /** + * Method to attach a JForm object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 3.7.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $return = parent::setup($element, $value, $group); - // Onchange must always be the change context function - $this->onchange = 'Joomla.fieldsChangeContext(this.value);'; + // Onchange must always be the change context function + $this->onchange = 'Joomla.fieldsChangeContext(this.value);'; - return $return; - } + return $return; + } - /** - * Method to get the field input markup for a generic list. - * Use the multiple attribute to enable multiselect. - * - * @return string The field input markup. - * - * @since 3.7.0 - */ - protected function getInput() - { - Factory::getApplication()->getDocument()->getWebAssetManager() - ->useScript('com_fields.admin-field-changecontext'); + /** + * Method to get the field input markup for a generic list. + * Use the multiple attribute to enable multiselect. + * + * @return string The field input markup. + * + * @since 3.7.0 + */ + protected function getInput() + { + Factory::getApplication()->getDocument()->getWebAssetManager() + ->useScript('com_fields.admin-field-changecontext'); - return parent::getInput(); - } + return parent::getInput(); + } } diff --git a/administrator/components/com_fields/src/Field/SubfieldsField.php b/administrator/components/com_fields/src/Field/SubfieldsField.php index 1640d6cb1cb58..330fa46e25865 100644 --- a/administrator/components/com_fields/src/Field/SubfieldsField.php +++ b/administrator/components/com_fields/src/Field/SubfieldsField.php @@ -1,4 +1,5 @@ context])) - { - static::$customFieldsCache[$this->context] = FieldsHelper::getFields($this->context, null, false, null, true); - } - - // Iterate over the custom fields for this context - foreach (static::$customFieldsCache[$this->context] as $customField) - { - // Skip our own subform type. We won't have subform in subform. - if ($customField->type == 'subform') - { - continue; - } - - $options[] = HTMLHelper::_( - 'select.option', - $customField->id, - ($customField->title . ' (' . $customField->type . ')') - ); - } - - // Sorting the fields based on the text which is displayed - usort( - $options, - function ($a, $b) - { - return strcmp($a->text, $b->text); - } - ); - - if (count($options) == 0) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_FIELDS_NO_FIELDS_TO_CREATE_SUBFORM_FIELD_WARNING'), 'warning'); - } - - return $options; - } - - /** - * Method to attach a JForm object to the field. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $return = parent::setup($element, $value, $group); - - if ($return) - { - $this->context = (string) $this->element['context']; - } - - return $return; - } + /** + * The name of this Field type. + * + * @var string + * + * @since 4.0.0 + */ + public $type = 'Subfields'; + + /** + * Configuration option for this field type to could filter the displayed custom field instances + * by a given context. Default value empty string. If given empty string, displays all custom fields. + * + * @var string + * + * @since 4.0.0 + */ + protected $context = ''; + + /** + * Array to do a fast in-memory caching of all custom field items. Used to not bother the + * FieldsHelper with a call every time this field is being rendered. + * + * @var array + * + * @since 4.0.0 + */ + protected static $customFieldsCache = array(); + + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 4.0.0 + */ + protected function getOptions() + { + $options = parent::getOptions(); + + // Check whether we have a result for this context yet + if (!isset(static::$customFieldsCache[$this->context])) { + static::$customFieldsCache[$this->context] = FieldsHelper::getFields($this->context, null, false, null, true); + } + + // Iterate over the custom fields for this context + foreach (static::$customFieldsCache[$this->context] as $customField) { + // Skip our own subform type. We won't have subform in subform. + if ($customField->type == 'subform') { + continue; + } + + $options[] = HTMLHelper::_( + 'select.option', + $customField->id, + ($customField->title . ' (' . $customField->type . ')') + ); + } + + // Sorting the fields based on the text which is displayed + usort( + $options, + function ($a, $b) { + return strcmp($a->text, $b->text); + } + ); + + if (count($options) == 0) { + Factory::getApplication()->enqueueMessage(Text::_('COM_FIELDS_NO_FIELDS_TO_CREATE_SUBFORM_FIELD_WARNING'), 'warning'); + } + + return $options; + } + + /** + * Method to attach a JForm object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $return = parent::setup($element, $value, $group); + + if ($return) { + $this->context = (string) $this->element['context']; + } + + return $return; + } } diff --git a/administrator/components/com_fields/src/Field/TypeField.php b/administrator/components/com_fields/src/Field/TypeField.php index d48b1aefa780d..760a598d84694 100644 --- a/administrator/components/com_fields/src/Field/TypeField.php +++ b/administrator/components/com_fields/src/Field/TypeField.php @@ -1,4 +1,5 @@ ` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @since 3.7.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $return = parent::setup($element, $value, $group); + /** + * Method to attach a JForm object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 3.7.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $return = parent::setup($element, $value, $group); - $this->onchange = 'Joomla.typeHasChanged(this);'; + $this->onchange = 'Joomla.typeHasChanged(this);'; - return $return; - } + return $return; + } - /** - * Method to get the field options. - * - * @return array The field option objects. - * - * @since 3.7.0 - */ - protected function getOptions() - { - $options = parent::getOptions(); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.7.0 + */ + protected function getOptions() + { + $options = parent::getOptions(); - $fieldTypes = FieldsHelper::getFieldTypes(); + $fieldTypes = FieldsHelper::getFieldTypes(); - foreach ($fieldTypes as $fieldType) - { - $options[] = HTMLHelper::_('select.option', $fieldType['type'], $fieldType['label']); - } + foreach ($fieldTypes as $fieldType) { + $options[] = HTMLHelper::_('select.option', $fieldType['type'], $fieldType['label']); + } - // Sorting the fields based on the text which is displayed - usort( - $options, - function ($a, $b) - { - return strcmp($a->text, $b->text); - } - ); + // Sorting the fields based on the text which is displayed + usort( + $options, + function ($a, $b) { + return strcmp($a->text, $b->text); + } + ); - // Load scripts - Factory::getApplication()->getDocument()->getWebAssetManager() - ->useScript('com_fields.admin-field-typehaschanged') - ->useScript('webcomponent.core-loader'); + // Load scripts + Factory::getApplication()->getDocument()->getWebAssetManager() + ->useScript('com_fields.admin-field-typehaschanged') + ->useScript('webcomponent.core-loader'); - return $options; - } + return $options; + } } diff --git a/administrator/components/com_fields/src/Helper/FieldsHelper.php b/administrator/components/com_fields/src/Helper/FieldsHelper.php index e2f39866ab86b..d0ba28d4f5b13 100644 --- a/administrator/components/com_fields/src/Helper/FieldsHelper.php +++ b/administrator/components/com_fields/src/Helper/FieldsHelper.php @@ -1,4 +1,5 @@ bootComponent($parts[0]); - - if ($component instanceof FieldsServiceInterface) - { - $newSection = $component->validateSection($parts[1], $item); - } - - if ($newSection) - { - $parts[1] = $newSection; - } - - return $parts; - } - - /** - * Returns the fields for the given context. - * If the item is an object the returned fields do have an additional field - * "value" which represents the value for the given item. If the item has an - * assigned_cat_ids field, then additionally fields which belong to that - * category will be returned. - * Should the value being prepared to be shown in an HTML context then - * prepareValue must be set to true. No further escaping needs to be done. - * The values of the fields can be overridden by an associative array where the keys - * have to be a name and its corresponding value. - * - * @param string $context The context of the content passed to the helper - * @param null $item The item being edited in the form - * @param int|bool $prepareValue (if int is display event): 1 - AfterTitle, 2 - BeforeDisplay, 3 - AfterDisplay, 0 - OFF - * @param array|null $valuesToOverride The values to override - * @param bool $includeSubformFields Should I include fields marked as Only Use In Subform? - * - * @return array - * - * @throws \Exception - * @since 3.7.0 - */ - public static function getFields( - $context, $item = null, $prepareValue = false, array $valuesToOverride = null, bool $includeSubformFields = false - ) - { - if (self::$fieldsCache === null) - { - // Load the model - self::$fieldsCache = Factory::getApplication()->bootComponent('com_fields') - ->getMVCFactory()->createModel('Fields', 'Administrator', ['ignore_request' => true]); - - self::$fieldsCache->setState('filter.state', 1); - self::$fieldsCache->setState('list.limit', 0); - } - - if ($includeSubformFields) - { - self::$fieldsCache->setState('filter.only_use_in_subform', ''); - } - else - { - self::$fieldsCache->setState('filter.only_use_in_subform', 0); - } - - if (is_array($item)) - { - $item = (object) $item; - } - - if (Multilanguage::isEnabled() && isset($item->language) && $item->language != '*') - { - self::$fieldsCache->setState('filter.language', array('*', $item->language)); - } - - self::$fieldsCache->setState('filter.context', $context); - self::$fieldsCache->setState('filter.assigned_cat_ids', array()); - - /* - * If item has assigned_cat_ids parameter display only fields which - * belong to the category - */ - if ($item && (isset($item->catid) || isset($item->fieldscatid))) - { - $assignedCatIds = $item->catid ?? $item->fieldscatid; - - if (!is_array($assignedCatIds)) - { - $assignedCatIds = explode(',', $assignedCatIds); - } - - // Fields without any category assigned should show as well - $assignedCatIds[] = 0; - - self::$fieldsCache->setState('filter.assigned_cat_ids', $assignedCatIds); - } - - $fields = self::$fieldsCache->getItems(); - - if ($fields === false) - { - return array(); - } - - if ($item && isset($item->id)) - { - if (self::$fieldCache === null) - { - self::$fieldCache = Factory::getApplication()->bootComponent('com_fields') - ->getMVCFactory()->createModel('Field', 'Administrator', ['ignore_request' => true]); - } - - $fieldIds = array_map( - function ($f) - { - return $f->id; - }, - $fields - ); - - $fieldValues = self::$fieldCache->getFieldValues($fieldIds, $item->id); - - $new = array(); - - foreach ($fields as $key => $original) - { - /* - * Doing a clone, otherwise fields for different items will - * always reference to the same object - */ - $field = clone $original; - - if ($valuesToOverride && array_key_exists($field->name, $valuesToOverride)) - { - $field->value = $valuesToOverride[$field->name]; - } - elseif ($valuesToOverride && array_key_exists($field->id, $valuesToOverride)) - { - $field->value = $valuesToOverride[$field->id]; - } - elseif (array_key_exists($field->id, $fieldValues)) - { - $field->value = $fieldValues[$field->id]; - } - - if (!isset($field->value) || $field->value === '') - { - $field->value = $field->default_value; - } - - $field->rawvalue = $field->value; - - // If boolean prepare, if int, it is the event type: 1 - After Title, 2 - Before Display Content, 3 - After Display Content, 0 - Do not prepare - if ($prepareValue && (is_bool($prepareValue) || $prepareValue === (int) $field->params->get('display', '2'))) - { - PluginHelper::importPlugin('fields'); - - /* - * On before field prepare - * Event allow plugins to modify the output of the field before it is prepared - */ - Factory::getApplication()->triggerEvent('onCustomFieldsBeforePrepareField', array($context, $item, &$field)); - - // Gathering the value for the field - $value = Factory::getApplication()->triggerEvent('onCustomFieldsPrepareField', array($context, $item, &$field)); - - if (is_array($value)) - { - $value = implode(' ', $value); - } - - /* - * On after field render - * Event allows plugins to modify the output of the prepared field - */ - Factory::getApplication()->triggerEvent('onCustomFieldsAfterPrepareField', array($context, $item, $field, &$value)); - - // Assign the value - $field->value = $value; - } - - $new[$key] = $field; - } - - $fields = $new; - } - - return $fields; - } - - /** - * Renders the layout file and data on the context and does a fall back to - * Fields afterwards. - * - * @param string $context The context of the content passed to the helper - * @param string $layoutFile layoutFile - * @param array $displayData displayData - * - * @return NULL|string - * - * @since 3.7.0 - */ - public static function render($context, $layoutFile, $displayData) - { - $value = ''; - - /* - * Because the layout refreshes the paths before the render function is - * called, so there is no way to load the layout overrides in the order - * template -> context -> fields. - * If there is no override in the context then we need to call the - * layout from Fields. - */ - if ($parts = self::extract($context)) - { - // Trying to render the layout on the component from the context - $value = LayoutHelper::render($layoutFile, $displayData, null, array('component' => $parts[0], 'client' => 0)); - } - - if ($value == '') - { - // Trying to render the layout on Fields itself - $value = LayoutHelper::render($layoutFile, $displayData, null, array('component' => 'com_fields','client' => 0)); - } - - return $value; - } - - /** - * PrepareForm - * - * @param string $context The context of the content passed to the helper - * @param Form $form form - * @param object $data data. - * - * @return boolean - * - * @since 3.7.0 - */ - public static function prepareForm($context, Form $form, $data) - { - // Extracting the component and section - $parts = self::extract($context); - - if (! $parts) - { - return true; - } - - $context = $parts[0] . '.' . $parts[1]; - - // When no fields available return here - $fields = self::getFields($parts[0] . '.' . $parts[1], new CMSObject); - - if (! $fields) - { - return true; - } - - $component = $parts[0]; - $section = $parts[1]; - - $assignedCatids = $data->catid ?? $data->fieldscatid ?? $form->getValue('catid'); - - // Account for case that a submitted form has a multi-value category id field (e.g. a filtering form), just use the first category - $assignedCatids = is_array($assignedCatids) - ? (int) reset($assignedCatids) - : (int) $assignedCatids; - - if (!$assignedCatids && $formField = $form->getField('catid')) - { - $assignedCatids = $formField->getAttribute('default', null); - - if (!$assignedCatids) - { - // Choose the first category available - $catOptions = $formField->options; - - if ($catOptions && !empty($catOptions[0]->value)) - { - $assignedCatids = (int) $catOptions[0]->value; - } - } - - $data->fieldscatid = $assignedCatids; - } - - /* - * If there is a catid field we need to reload the page when the catid - * is changed - */ - if ($form->getField('catid') && $parts[0] != 'com_fields') - { - /* - * Setting some parameters for the category field - */ - $form->setFieldAttribute('catid', 'refresh-enabled', true); - $form->setFieldAttribute('catid', 'refresh-cat-id', $assignedCatids); - $form->setFieldAttribute('catid', 'refresh-section', $section); - } - - // Getting the fields - $fields = self::getFields($parts[0] . '.' . $parts[1], $data); - - if (!$fields) - { - return true; - } - - $fieldTypes = self::getFieldTypes(); - - // Creating the dom - $xml = new \DOMDocument('1.0', 'UTF-8'); - $fieldsNode = $xml->appendChild(new \DOMElement('form'))->appendChild(new \DOMElement('fields')); - $fieldsNode->setAttribute('name', 'com_fields'); - - // Organizing the fields according to their group - $fieldsPerGroup = array(0 => array()); - - foreach ($fields as $field) - { - if (!array_key_exists($field->type, $fieldTypes)) - { - // Field type is not available - continue; - } - - if (!array_key_exists($field->group_id, $fieldsPerGroup)) - { - $fieldsPerGroup[$field->group_id] = array(); - } - - if ($path = $fieldTypes[$field->type]['path']) - { - // Add the lookup path for the field - FormHelper::addFieldPath($path); - } - - if ($path = $fieldTypes[$field->type]['rules']) - { - // Add the lookup path for the rule - FormHelper::addRulePath($path); - } - - $fieldsPerGroup[$field->group_id][] = $field; - } - - $model = Factory::getApplication()->bootComponent('com_fields') - ->getMVCFactory()->createModel('Groups', 'Administrator', ['ignore_request' => true]); - $model->setState('filter.context', $context); - - /** - * $model->getItems() would only return existing groups, but we also - * have the 'default' group with id 0 which is not in the database, - * so we create it virtually here. - */ - $defaultGroup = new \stdClass; - $defaultGroup->id = 0; - $defaultGroup->title = ''; - $defaultGroup->description = ''; - $iterateGroups = array_merge(array($defaultGroup), $model->getItems()); - - // Looping through the groups - foreach ($iterateGroups as $group) - { - if (empty($fieldsPerGroup[$group->id])) - { - continue; - } - - // Defining the field set - /** @var \DOMElement $fieldset */ - $fieldset = $fieldsNode->appendChild(new \DOMElement('fieldset')); - $fieldset->setAttribute('name', 'fields-' . $group->id); - $fieldset->setAttribute('addfieldpath', '/administrator/components/' . $component . '/models/fields'); - $fieldset->setAttribute('addrulepath', '/administrator/components/' . $component . '/models/rules'); - - $label = $group->title; - $description = $group->description; - - if (!$label) - { - $key = strtoupper($component . '_FIELDS_' . $section . '_LABEL'); - - if (!Factory::getLanguage()->hasKey($key)) - { - $key = 'JGLOBAL_FIELDS'; - } - - $label = $key; - } - - if (!$description) - { - $key = strtoupper($component . '_FIELDS_' . $section . '_DESC'); - - if (Factory::getLanguage()->hasKey($key)) - { - $description = $key; - } - } - - $fieldset->setAttribute('label', $label); - $fieldset->setAttribute('description', strip_tags($description)); - - // Looping through the fields for that context - foreach ($fieldsPerGroup[$group->id] as $field) - { - try - { - Factory::getApplication()->triggerEvent('onCustomFieldsPrepareDom', array($field, $fieldset, $form)); - - /* - * If the field belongs to an assigned_cat_id but the assigned_cat_ids in the data - * is not known, set the required flag to false on any circumstance. - */ - if (!$assignedCatids && !empty($field->assigned_cat_ids) && $form->getField($field->name)) - { - $form->setFieldAttribute($field->name, 'required', 'false'); - } - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - - // When the field set is empty, then remove it - if (!$fieldset->hasChildNodes()) - { - $fieldsNode->removeChild($fieldset); - } - } - - // Loading the XML fields string into the form - $form->load($xml->saveXML()); - - $model = Factory::getApplication()->bootComponent('com_fields') - ->getMVCFactory()->createModel('Field', 'Administrator', ['ignore_request' => true]); - - if ((!isset($data->id) || !$data->id) && Factory::getApplication()->input->getCmd('controller') == 'modules' - && Factory::getApplication()->isClient('site')) - { - // Modules on front end editing don't have data and an id set - $data->id = Factory::getApplication()->input->getInt('id'); - } - - // Looping through the fields again to set the value - if (!isset($data->id) || !$data->id) - { - return true; - } - - foreach ($fields as $field) - { - $value = $model->getFieldValue($field->id, $data->id); - - if ($value === null) - { - continue; - } - - if (!is_array($value) && $value !== '') - { - // Function getField doesn't cache the fields, so we try to do it only when necessary - $formField = $form->getField($field->name, 'com_fields'); - - if ($formField && $formField->forceMultiple) - { - $value = (array) $value; - } - } - - // Setting the value on the field - $form->setValue($field->name, 'com_fields', $value); - } - - return true; - } - - /** - * Return a boolean if the actual logged in user can edit the given field value. - * - * @param \stdClass $field The field - * - * @return boolean - * - * @since 3.7.0 - */ - public static function canEditFieldValue($field) - { - $parts = self::extract($field->context); - - return Factory::getUser()->authorise('core.edit.value', $parts[0] . '.field.' . (int) $field->id); - } - - /** - * Return a boolean based on field (and field group) display / show_on settings - * - * @param \stdClass $field The field - * - * @return boolean - * - * @since 3.8.7 - */ - public static function displayFieldOnForm($field) - { - $app = Factory::getApplication(); - - // Detect if the field should be shown at all - if ($field->params->get('show_on') == 1 && $app->isClient('administrator')) - { - return false; - } - elseif ($field->params->get('show_on') == 2 && $app->isClient('site')) - { - return false; - } - - if (!self::canEditFieldValue($field)) - { - $fieldDisplayReadOnly = $field->params->get('display_readonly', '2'); - - if ($fieldDisplayReadOnly == '2') - { - // Inherit from field group display read-only setting - $groupModel = $app->bootComponent('com_fields') - ->getMVCFactory()->createModel('Group', 'Administrator', ['ignore_request' => true]); - $groupDisplayReadOnly = $groupModel->getItem($field->group_id)->params->get('display_readonly', '1'); - $fieldDisplayReadOnly = $groupDisplayReadOnly; - } - - if ($fieldDisplayReadOnly == '0') - { - // Do not display field on form when field is read-only - return false; - } - } - - // Display field on form - return true; - } - - /** - * Gets assigned categories ids for a field - * - * @param \stdClass[] $fieldId The field ID - * - * @return array Array with the assigned category ids - * - * @since 4.0.0 - */ - public static function getAssignedCategoriesIds($fieldId) - { - $fieldId = (int) $fieldId; - - if (!$fieldId) - { - return array(); - } - - $db = Factory::getDbo(); - $query = $db->getQuery(true); - - $query->select($db->quoteName('a.category_id')) - ->from($db->quoteName('#__fields_categories', 'a')) - ->where('a.field_id = ' . $fieldId); - - $db->setQuery($query); - - return $db->loadColumn(); - } - - /** - * Gets assigned categories titles for a field - * - * @param \stdClass[] $fieldId The field ID - * - * @return array Array with the assigned categories - * - * @since 3.7.0 - */ - public static function getAssignedCategoriesTitles($fieldId) - { - $fieldId = (int) $fieldId; - - if (!$fieldId) - { - return []; - } - - $db = Factory::getDbo(); - $query = $db->getQuery(true); - - $query->select($db->quoteName('c.title')) - ->from($db->quoteName('#__fields_categories', 'a')) - ->join('INNER', $db->quoteName('#__categories', 'c') . ' ON a.category_id = c.id') - ->where($db->quoteName('field_id') . ' = :fieldid') - ->bind(':fieldid', $fieldId, ParameterType::INTEGER); - - $db->setQuery($query); - - return $db->loadColumn(); - } - - /** - * Gets the fields system plugin extension id. - * - * @return integer The fields system plugin extension id. - * - * @since 3.7.0 - */ - public static function getFieldsPluginId() - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) - ->where($db->quoteName('element') . ' = ' . $db->quote('fields')); - $db->setQuery($query); - - try - { - $result = (int) $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - $result = 0; - } - - return $result; - } - - /** - * Loads the fields plugins and returns an array of field types from the plugins. - * - * The returned array contains arrays with the following keys: - * - label: The label of the field - * - type: The type of the field - * - path: The path of the folder where the field can be found - * - * @return array - * - * @since 3.7.0 - */ - public static function getFieldTypes() - { - PluginHelper::importPlugin('fields'); - $eventData = Factory::getApplication()->triggerEvent('onCustomFieldsGetTypes'); - - $data = array(); - - foreach ($eventData as $fields) - { - foreach ($fields as $fieldDescription) - { - if (!array_key_exists('path', $fieldDescription)) - { - $fieldDescription['path'] = null; - } - - if (!array_key_exists('rules', $fieldDescription)) - { - $fieldDescription['rules'] = null; - } - - $data[$fieldDescription['type']] = $fieldDescription; - } - } - - return $data; - } - - /** - * Clears the internal cache for the custom fields. - * - * @return void - * - * @since 3.8.0 - */ - public static function clearFieldsCache() - { - self::$fieldCache = null; - self::$fieldsCache = null; - } + /** + * @var FieldsModel + */ + private static $fieldsCache = null; + + /** + * @var FieldsModel + */ + private static $fieldCache = null; + + /** + * Extracts the component and section from the context string which has to + * be in the format component.context. + * + * @param string $contextString contextString + * @param object $item optional item object + * + * @return array|null + * + * @since 3.7.0 + */ + public static function extract($contextString, $item = null) + { + $parts = explode('.', $contextString, 2); + + if (count($parts) < 2) { + return null; + } + + $newSection = ''; + + $component = Factory::getApplication()->bootComponent($parts[0]); + + if ($component instanceof FieldsServiceInterface) { + $newSection = $component->validateSection($parts[1], $item); + } + + if ($newSection) { + $parts[1] = $newSection; + } + + return $parts; + } + + /** + * Returns the fields for the given context. + * If the item is an object the returned fields do have an additional field + * "value" which represents the value for the given item. If the item has an + * assigned_cat_ids field, then additionally fields which belong to that + * category will be returned. + * Should the value being prepared to be shown in an HTML context then + * prepareValue must be set to true. No further escaping needs to be done. + * The values of the fields can be overridden by an associative array where the keys + * have to be a name and its corresponding value. + * + * @param string $context The context of the content passed to the helper + * @param null $item The item being edited in the form + * @param int|bool $prepareValue (if int is display event): 1 - AfterTitle, 2 - BeforeDisplay, 3 - AfterDisplay, 0 - OFF + * @param array|null $valuesToOverride The values to override + * @param bool $includeSubformFields Should I include fields marked as Only Use In Subform? + * + * @return array + * + * @throws \Exception + * @since 3.7.0 + */ + public static function getFields( + $context, + $item = null, + $prepareValue = false, + array $valuesToOverride = null, + bool $includeSubformFields = false + ) { + if (self::$fieldsCache === null) { + // Load the model + self::$fieldsCache = Factory::getApplication()->bootComponent('com_fields') + ->getMVCFactory()->createModel('Fields', 'Administrator', ['ignore_request' => true]); + + self::$fieldsCache->setState('filter.state', 1); + self::$fieldsCache->setState('list.limit', 0); + } + + if ($includeSubformFields) { + self::$fieldsCache->setState('filter.only_use_in_subform', ''); + } else { + self::$fieldsCache->setState('filter.only_use_in_subform', 0); + } + + if (is_array($item)) { + $item = (object) $item; + } + + if (Multilanguage::isEnabled() && isset($item->language) && $item->language != '*') { + self::$fieldsCache->setState('filter.language', array('*', $item->language)); + } + + self::$fieldsCache->setState('filter.context', $context); + self::$fieldsCache->setState('filter.assigned_cat_ids', array()); + + /* + * If item has assigned_cat_ids parameter display only fields which + * belong to the category + */ + if ($item && (isset($item->catid) || isset($item->fieldscatid))) { + $assignedCatIds = $item->catid ?? $item->fieldscatid; + + if (!is_array($assignedCatIds)) { + $assignedCatIds = explode(',', $assignedCatIds); + } + + // Fields without any category assigned should show as well + $assignedCatIds[] = 0; + + self::$fieldsCache->setState('filter.assigned_cat_ids', $assignedCatIds); + } + + $fields = self::$fieldsCache->getItems(); + + if ($fields === false) { + return array(); + } + + if ($item && isset($item->id)) { + if (self::$fieldCache === null) { + self::$fieldCache = Factory::getApplication()->bootComponent('com_fields') + ->getMVCFactory()->createModel('Field', 'Administrator', ['ignore_request' => true]); + } + + $fieldIds = array_map( + function ($f) { + return $f->id; + }, + $fields + ); + + $fieldValues = self::$fieldCache->getFieldValues($fieldIds, $item->id); + + $new = array(); + + foreach ($fields as $key => $original) { + /* + * Doing a clone, otherwise fields for different items will + * always reference to the same object + */ + $field = clone $original; + + if ($valuesToOverride && array_key_exists($field->name, $valuesToOverride)) { + $field->value = $valuesToOverride[$field->name]; + } elseif ($valuesToOverride && array_key_exists($field->id, $valuesToOverride)) { + $field->value = $valuesToOverride[$field->id]; + } elseif (array_key_exists($field->id, $fieldValues)) { + $field->value = $fieldValues[$field->id]; + } + + if (!isset($field->value) || $field->value === '') { + $field->value = $field->default_value; + } + + $field->rawvalue = $field->value; + + // If boolean prepare, if int, it is the event type: 1 - After Title, 2 - Before Display Content, 3 - After Display Content, 0 - Do not prepare + if ($prepareValue && (is_bool($prepareValue) || $prepareValue === (int) $field->params->get('display', '2'))) { + PluginHelper::importPlugin('fields'); + + /* + * On before field prepare + * Event allow plugins to modify the output of the field before it is prepared + */ + Factory::getApplication()->triggerEvent('onCustomFieldsBeforePrepareField', array($context, $item, &$field)); + + // Gathering the value for the field + $value = Factory::getApplication()->triggerEvent('onCustomFieldsPrepareField', array($context, $item, &$field)); + + if (is_array($value)) { + $value = implode(' ', $value); + } + + /* + * On after field render + * Event allows plugins to modify the output of the prepared field + */ + Factory::getApplication()->triggerEvent('onCustomFieldsAfterPrepareField', array($context, $item, $field, &$value)); + + // Assign the value + $field->value = $value; + } + + $new[$key] = $field; + } + + $fields = $new; + } + + return $fields; + } + + /** + * Renders the layout file and data on the context and does a fall back to + * Fields afterwards. + * + * @param string $context The context of the content passed to the helper + * @param string $layoutFile layoutFile + * @param array $displayData displayData + * + * @return NULL|string + * + * @since 3.7.0 + */ + public static function render($context, $layoutFile, $displayData) + { + $value = ''; + + /* + * Because the layout refreshes the paths before the render function is + * called, so there is no way to load the layout overrides in the order + * template -> context -> fields. + * If there is no override in the context then we need to call the + * layout from Fields. + */ + if ($parts = self::extract($context)) { + // Trying to render the layout on the component from the context + $value = LayoutHelper::render($layoutFile, $displayData, null, array('component' => $parts[0], 'client' => 0)); + } + + if ($value == '') { + // Trying to render the layout on Fields itself + $value = LayoutHelper::render($layoutFile, $displayData, null, array('component' => 'com_fields','client' => 0)); + } + + return $value; + } + + /** + * PrepareForm + * + * @param string $context The context of the content passed to the helper + * @param Form $form form + * @param object $data data. + * + * @return boolean + * + * @since 3.7.0 + */ + public static function prepareForm($context, Form $form, $data) + { + // Extracting the component and section + $parts = self::extract($context); + + if (! $parts) { + return true; + } + + $context = $parts[0] . '.' . $parts[1]; + + // When no fields available return here + $fields = self::getFields($parts[0] . '.' . $parts[1], new CMSObject()); + + if (! $fields) { + return true; + } + + $component = $parts[0]; + $section = $parts[1]; + + $assignedCatids = $data->catid ?? $data->fieldscatid ?? $form->getValue('catid'); + + // Account for case that a submitted form has a multi-value category id field (e.g. a filtering form), just use the first category + $assignedCatids = is_array($assignedCatids) + ? (int) reset($assignedCatids) + : (int) $assignedCatids; + + if (!$assignedCatids && $formField = $form->getField('catid')) { + $assignedCatids = $formField->getAttribute('default', null); + + if (!$assignedCatids) { + // Choose the first category available + $catOptions = $formField->options; + + if ($catOptions && !empty($catOptions[0]->value)) { + $assignedCatids = (int) $catOptions[0]->value; + } + } + + $data->fieldscatid = $assignedCatids; + } + + /* + * If there is a catid field we need to reload the page when the catid + * is changed + */ + if ($form->getField('catid') && $parts[0] != 'com_fields') { + /* + * Setting some parameters for the category field + */ + $form->setFieldAttribute('catid', 'refresh-enabled', true); + $form->setFieldAttribute('catid', 'refresh-cat-id', $assignedCatids); + $form->setFieldAttribute('catid', 'refresh-section', $section); + } + + // Getting the fields + $fields = self::getFields($parts[0] . '.' . $parts[1], $data); + + if (!$fields) { + return true; + } + + $fieldTypes = self::getFieldTypes(); + + // Creating the dom + $xml = new \DOMDocument('1.0', 'UTF-8'); + $fieldsNode = $xml->appendChild(new \DOMElement('form'))->appendChild(new \DOMElement('fields')); + $fieldsNode->setAttribute('name', 'com_fields'); + + // Organizing the fields according to their group + $fieldsPerGroup = array(0 => array()); + + foreach ($fields as $field) { + if (!array_key_exists($field->type, $fieldTypes)) { + // Field type is not available + continue; + } + + if (!array_key_exists($field->group_id, $fieldsPerGroup)) { + $fieldsPerGroup[$field->group_id] = array(); + } + + if ($path = $fieldTypes[$field->type]['path']) { + // Add the lookup path for the field + FormHelper::addFieldPath($path); + } + + if ($path = $fieldTypes[$field->type]['rules']) { + // Add the lookup path for the rule + FormHelper::addRulePath($path); + } + + $fieldsPerGroup[$field->group_id][] = $field; + } + + $model = Factory::getApplication()->bootComponent('com_fields') + ->getMVCFactory()->createModel('Groups', 'Administrator', ['ignore_request' => true]); + $model->setState('filter.context', $context); + + /** + * $model->getItems() would only return existing groups, but we also + * have the 'default' group with id 0 which is not in the database, + * so we create it virtually here. + */ + $defaultGroup = new \stdClass(); + $defaultGroup->id = 0; + $defaultGroup->title = ''; + $defaultGroup->description = ''; + $iterateGroups = array_merge(array($defaultGroup), $model->getItems()); + + // Looping through the groups + foreach ($iterateGroups as $group) { + if (empty($fieldsPerGroup[$group->id])) { + continue; + } + + // Defining the field set + /** @var \DOMElement $fieldset */ + $fieldset = $fieldsNode->appendChild(new \DOMElement('fieldset')); + $fieldset->setAttribute('name', 'fields-' . $group->id); + $fieldset->setAttribute('addfieldpath', '/administrator/components/' . $component . '/models/fields'); + $fieldset->setAttribute('addrulepath', '/administrator/components/' . $component . '/models/rules'); + + $label = $group->title; + $description = $group->description; + + if (!$label) { + $key = strtoupper($component . '_FIELDS_' . $section . '_LABEL'); + + if (!Factory::getLanguage()->hasKey($key)) { + $key = 'JGLOBAL_FIELDS'; + } + + $label = $key; + } + + if (!$description) { + $key = strtoupper($component . '_FIELDS_' . $section . '_DESC'); + + if (Factory::getLanguage()->hasKey($key)) { + $description = $key; + } + } + + $fieldset->setAttribute('label', $label); + $fieldset->setAttribute('description', strip_tags($description)); + + // Looping through the fields for that context + foreach ($fieldsPerGroup[$group->id] as $field) { + try { + Factory::getApplication()->triggerEvent('onCustomFieldsPrepareDom', array($field, $fieldset, $form)); + + /* + * If the field belongs to an assigned_cat_id but the assigned_cat_ids in the data + * is not known, set the required flag to false on any circumstance. + */ + if (!$assignedCatids && !empty($field->assigned_cat_ids) && $form->getField($field->name)) { + $form->setFieldAttribute($field->name, 'required', 'false'); + } + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + + // When the field set is empty, then remove it + if (!$fieldset->hasChildNodes()) { + $fieldsNode->removeChild($fieldset); + } + } + + // Loading the XML fields string into the form + $form->load($xml->saveXML()); + + $model = Factory::getApplication()->bootComponent('com_fields') + ->getMVCFactory()->createModel('Field', 'Administrator', ['ignore_request' => true]); + + if ( + (!isset($data->id) || !$data->id) && Factory::getApplication()->input->getCmd('controller') == 'modules' + && Factory::getApplication()->isClient('site') + ) { + // Modules on front end editing don't have data and an id set + $data->id = Factory::getApplication()->input->getInt('id'); + } + + // Looping through the fields again to set the value + if (!isset($data->id) || !$data->id) { + return true; + } + + foreach ($fields as $field) { + $value = $model->getFieldValue($field->id, $data->id); + + if ($value === null) { + continue; + } + + if (!is_array($value) && $value !== '') { + // Function getField doesn't cache the fields, so we try to do it only when necessary + $formField = $form->getField($field->name, 'com_fields'); + + if ($formField && $formField->forceMultiple) { + $value = (array) $value; + } + } + + // Setting the value on the field + $form->setValue($field->name, 'com_fields', $value); + } + + return true; + } + + /** + * Return a boolean if the actual logged in user can edit the given field value. + * + * @param \stdClass $field The field + * + * @return boolean + * + * @since 3.7.0 + */ + public static function canEditFieldValue($field) + { + $parts = self::extract($field->context); + + return Factory::getUser()->authorise('core.edit.value', $parts[0] . '.field.' . (int) $field->id); + } + + /** + * Return a boolean based on field (and field group) display / show_on settings + * + * @param \stdClass $field The field + * + * @return boolean + * + * @since 3.8.7 + */ + public static function displayFieldOnForm($field) + { + $app = Factory::getApplication(); + + // Detect if the field should be shown at all + if ($field->params->get('show_on') == 1 && $app->isClient('administrator')) { + return false; + } elseif ($field->params->get('show_on') == 2 && $app->isClient('site')) { + return false; + } + + if (!self::canEditFieldValue($field)) { + $fieldDisplayReadOnly = $field->params->get('display_readonly', '2'); + + if ($fieldDisplayReadOnly == '2') { + // Inherit from field group display read-only setting + $groupModel = $app->bootComponent('com_fields') + ->getMVCFactory()->createModel('Group', 'Administrator', ['ignore_request' => true]); + $groupDisplayReadOnly = $groupModel->getItem($field->group_id)->params->get('display_readonly', '1'); + $fieldDisplayReadOnly = $groupDisplayReadOnly; + } + + if ($fieldDisplayReadOnly == '0') { + // Do not display field on form when field is read-only + return false; + } + } + + // Display field on form + return true; + } + + /** + * Gets assigned categories ids for a field + * + * @param \stdClass[] $fieldId The field ID + * + * @return array Array with the assigned category ids + * + * @since 4.0.0 + */ + public static function getAssignedCategoriesIds($fieldId) + { + $fieldId = (int) $fieldId; + + if (!$fieldId) { + return array(); + } + + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + $query->select($db->quoteName('a.category_id')) + ->from($db->quoteName('#__fields_categories', 'a')) + ->where('a.field_id = ' . $fieldId); + + $db->setQuery($query); + + return $db->loadColumn(); + } + + /** + * Gets assigned categories titles for a field + * + * @param \stdClass[] $fieldId The field ID + * + * @return array Array with the assigned categories + * + * @since 3.7.0 + */ + public static function getAssignedCategoriesTitles($fieldId) + { + $fieldId = (int) $fieldId; + + if (!$fieldId) { + return []; + } + + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + $query->select($db->quoteName('c.title')) + ->from($db->quoteName('#__fields_categories', 'a')) + ->join('INNER', $db->quoteName('#__categories', 'c') . ' ON a.category_id = c.id') + ->where($db->quoteName('field_id') . ' = :fieldid') + ->bind(':fieldid', $fieldId, ParameterType::INTEGER); + + $db->setQuery($query); + + return $db->loadColumn(); + } + + /** + * Gets the fields system plugin extension id. + * + * @return integer The fields system plugin extension id. + * + * @since 3.7.0 + */ + public static function getFieldsPluginId() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ->where($db->quoteName('element') . ' = ' . $db->quote('fields')); + $db->setQuery($query); + + try { + $result = (int) $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + $result = 0; + } + + return $result; + } + + /** + * Loads the fields plugins and returns an array of field types from the plugins. + * + * The returned array contains arrays with the following keys: + * - label: The label of the field + * - type: The type of the field + * - path: The path of the folder where the field can be found + * + * @return array + * + * @since 3.7.0 + */ + public static function getFieldTypes() + { + PluginHelper::importPlugin('fields'); + $eventData = Factory::getApplication()->triggerEvent('onCustomFieldsGetTypes'); + + $data = array(); + + foreach ($eventData as $fields) { + foreach ($fields as $fieldDescription) { + if (!array_key_exists('path', $fieldDescription)) { + $fieldDescription['path'] = null; + } + + if (!array_key_exists('rules', $fieldDescription)) { + $fieldDescription['rules'] = null; + } + + $data[$fieldDescription['type']] = $fieldDescription; + } + } + + return $data; + } + + /** + * Clears the internal cache for the custom fields. + * + * @return void + * + * @since 3.8.0 + */ + public static function clearFieldsCache() + { + self::$fieldCache = null; + self::$fieldsCache = null; + } } diff --git a/administrator/components/com_fields/src/Model/FieldModel.php b/administrator/components/com_fields/src/Model/FieldModel.php index d8af27d19629f..af2f28f4c7a59 100644 --- a/administrator/components/com_fields/src/Model/FieldModel.php +++ b/administrator/components/com_fields/src/Model/FieldModel.php @@ -1,4 +1,5 @@ 'batchAccess', - 'language_id' => 'batchLanguage' - ); - - /** - * @var array - * - * @since 3.7.0 - */ - private $valueCache = array(); - - /** - * Constructor - * - * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). - * @param MVCFactoryInterface $factory The factory. - * - * @since 3.7.0 - * @throws \Exception - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null) - { - parent::__construct($config, $factory); - - $this->typeAlias = Factory::getApplication()->input->getCmd('context', 'com_content.article') . '.field'; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success, False on error. - * - * @since 3.7.0 - */ - public function save($data) - { - $field = null; - - if (isset($data['id']) && $data['id']) - { - $field = $this->getItem($data['id']); - } - - if (!isset($data['label']) && isset($data['params']['label'])) - { - $data['label'] = $data['params']['label']; - - unset($data['params']['label']); - } - - // Alter the title for save as copy - $input = Factory::getApplication()->input; - - if ($input->get('task') == 'save2copy') - { - $origTable = clone $this->getTable(); - $origTable->load($input->getInt('id')); - - if ($data['title'] == $origTable->title) - { - list($title, $name) = $this->generateNewTitle($data['group_id'], $data['name'], $data['title']); - $data['title'] = $title; - $data['label'] = $title; - $data['name'] = $name; - } - else - { - if ($data['name'] == $origTable->name) - { - $data['name'] = ''; - } - } - - $data['state'] = 0; - } - - // Load the fields plugins, perhaps they want to do something - PluginHelper::importPlugin('fields'); - - $message = $this->checkDefaultValue($data); - - if ($message !== true) - { - $this->setError($message); - - return false; - } - - if (!parent::save($data)) - { - return false; - } - - // Save the assigned categories into #__fields_categories - $db = $this->getDatabase(); - $id = (int) $this->getState('field.id'); - - /** - * If the field is only used in subform, set Category to None automatically so that it will only be displayed - * as part of SubForm on add/edit item screen - */ - if (!empty($data['only_use_in_subform'])) - { - $cats = [-1]; - } - else - { - $cats = isset($data['assigned_cat_ids']) ? (array) $data['assigned_cat_ids'] : array(); - $cats = ArrayHelper::toInteger($cats); - } - - $assignedCatIds = array(); - - foreach ($cats as $cat) - { - // If we have found the 'JNONE' category, remove all other from the result and break. - if ($cat == '-1') - { - $assignedCatIds = array('-1'); - break; - } - - if ($cat) - { - $assignedCatIds[] = $cat; - } - } - - // First delete all assigned categories - $query = $db->getQuery(true); - $query->delete('#__fields_categories') - ->where($db->quoteName('field_id') . ' = :fieldid') - ->bind(':fieldid', $id, ParameterType::INTEGER); - - $db->setQuery($query); - $db->execute(); - - // Inset new assigned categories - $tupel = new \stdClass; - $tupel->field_id = $id; - - foreach ($assignedCatIds as $catId) - { - $tupel->category_id = $catId; - $db->insertObject('#__fields_categories', $tupel); - } - - /** - * If the options have changed, delete the values. This should only apply for list, checkboxes and radio - * custom field types, because when their options are being changed, their values might get invalid, because - * e.g. there is a value selected from a list, which is not part of the list anymore. Hence we need to delete - * all values that are not part of the options anymore. Note: The only field types with fieldparams+options - * are those above listed plus the subfields type. And we do explicitly not want the values to be deleted - * when the options of a subfields field are getting changed. - */ - if ($field && in_array($field->type, array('list', 'checkboxes', 'radio'), true) - && isset($data['fieldparams']['options']) && isset($field->fieldparams['options'])) - { - $oldParams = $this->getParams($field->fieldparams['options']); - $newParams = $this->getParams($data['fieldparams']['options']); - - if (is_object($oldParams) && is_object($newParams) && $oldParams != $newParams) - { - // Get new values. - $names = array_column((array) $newParams, 'value'); - - $fieldId = (int) $field->id; - $query = $db->getQuery(true); - $query->delete($db->quoteName('#__fields_values')) - ->where($db->quoteName('field_id') . ' = :fieldid') - ->bind(':fieldid', $fieldId, ParameterType::INTEGER); - - // If new values are set, delete only old values. Otherwise delete all values. - if ($names) - { - $query->whereNotIn($db->quoteName('value'), $names, ParameterType::STRING); - } - - $db->setQuery($query); - $db->execute(); - } - } - - FieldsHelper::clearFieldsCache(); - - return true; - } - - - /** - * Checks if the default value is valid for the given data. If a string is returned then - * it can be assumed that the default value is invalid. - * - * @param array $data The data. - * - * @return true|string true if valid, a string containing the exception message when not. - * - * @since 3.7.0 - */ - private function checkDefaultValue($data) - { - // Empty default values are correct - if (empty($data['default_value']) && $data['default_value'] !== '0') - { - return true; - } - - $types = FieldsHelper::getFieldTypes(); - - // Check if type exists - if (!array_key_exists($data['type'], $types)) - { - return true; - } - - $path = $types[$data['type']]['rules']; - - // Add the path for the rules of the plugin when available - if ($path) - { - // Add the lookup path for the rule - FormHelper::addRulePath($path); - } - - // Create the fields object - $obj = (object) $data; - $obj->params = new Registry($obj->params); - $obj->fieldparams = new Registry(!empty($obj->fieldparams) ? $obj->fieldparams : array()); - - // Prepare the dom - $dom = new \DOMDocument; - $node = $dom->appendChild(new \DOMElement('form')); - - // Trigger the event to create the field dom node - $form = new Form($data['context']); - $form->setDatabase($this->getDatabase()); - Factory::getApplication()->triggerEvent('onCustomFieldsPrepareDom', array($obj, $node, $form)); - - // Check if a node is created - if (!$node->firstChild) - { - return true; - } - - // Define the type either from the field or from the data - $type = $node->firstChild->getAttribute('validate') ? : $data['type']; - - // Load the rule - $rule = FormHelper::loadRuleType($type); - - // When no rule exists, we allow the default value - if (!$rule) - { - return true; - } - - if ($rule instanceof DatabaseAwareInterface) - { - try - { - $rule->setDatabase($this->getDatabase()); - } - catch (DatabaseNotFoundException $e) - { - @trigger_error(sprintf('Database must be set, this will not be caught anymore in 5.0.'), E_USER_DEPRECATED); - $rule->setDatabase(Factory::getContainer()->get(DatabaseInterface::class)); - } - } - - try - { - // Perform the check - $result = $rule->test(simplexml_import_dom($node->firstChild), $data['default_value']); - - // Check if the test succeeded - return $result === true ? : Text::_('COM_FIELDS_FIELD_INVALID_DEFAULT_VALUE'); - } - catch (\UnexpectedValueException $e) - { - return $e->getMessage(); - } - } - - /** - * Converts the unknown params into an object. - * - * @param mixed $params The params. - * - * @return \stdClass Object on success, false on failure. - * - * @since 3.7.0 - */ - private function getParams($params) - { - if (is_string($params)) - { - $params = json_decode($params); - } - - if (is_array($params)) - { - $params = (object) $params; - } - - return $params; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 3.7.0 - */ - public function getItem($pk = null) - { - $result = parent::getItem($pk); - - if ($result) - { - // Prime required properties. - if (empty($result->id)) - { - $result->context = Factory::getApplication()->input->getCmd('context', $this->getState('field.context')); - } - - if (property_exists($result, 'fieldparams') && $result->fieldparams !== null) - { - $registry = new Registry; - - if ($result->fieldparams) - { - $registry->loadString($result->fieldparams); - } - - $result->fieldparams = $registry->toArray(); - } - - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $fieldId = (int) $result->id; - $query->select($db->quoteName('category_id')) - ->from($db->quoteName('#__fields_categories')) - ->where($db->quoteName('field_id') . ' = :fieldid') - ->bind(':fieldid', $fieldId, ParameterType::INTEGER); - - $db->setQuery($query); - $result->assigned_cat_ids = $db->loadColumn() ?: array(0); - } - - return $result; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 3.7.0 - * @throws \Exception - */ - public function getTable($name = 'Field', $prefix = 'Administrator', $options = array()) - { - // Default to text type - $table = parent::getTable($name, $prefix, $options); - $table->type = 'text'; - - return $table; - } - - /** - * Method to change the title & name. - * - * @param integer $categoryId The id of the category. - * @param string $name The name. - * @param string $title The title. - * - * @return array Contains the modified title and name. - * - * @since 3.7.0 - */ - protected function generateNewTitle($categoryId, $name, $title) - { - // Alter the title & name - $table = $this->getTable(); - - while ($table->load(array('name' => $name))) - { - $title = StringHelper::increment($title); - $name = StringHelper::increment($name, 'dash'); - } - - return array( - $title, - $name, - ); - } - - /** - * Method to delete one or more records. - * - * @param array $pks An array of record primary keys. - * - * @return boolean True if successful, false if an error occurs. - * - * @since 3.7.0 - */ - public function delete(&$pks) - { - $success = parent::delete($pks); - - if ($success) - { - $pks = (array) $pks; - $pks = ArrayHelper::toInteger($pks); - $pks = array_filter($pks); - - if (!empty($pks)) - { - // Delete Values - $query = $this->getDatabase()->getQuery(true); - - $query->delete($query->quoteName('#__fields_values')) - ->whereIn($query->quoteName('field_id'), $pks); - - $this->getDatabase()->setQuery($query)->execute(); - - // Delete Assigned Categories - $query = $this->getDatabase()->getQuery(true); - - $query->delete($query->quoteName('#__fields_categories')) - ->whereIn($query->quoteName('field_id'), $pks); - - $this->getDatabase()->setQuery($query)->execute(); - } - } - - return $success; - } - - /** - * Abstract method for getting the form from the model. - * - * @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 Form|bool A Form object on success, false on failure - * - * @since 3.7.0 - */ - public function getForm($data = array(), $loadData = true) - { - $context = $this->getState('field.context'); - $jinput = Factory::getApplication()->input; - - // A workaround to get the context into the model for save requests. - if (empty($context) && isset($data['context'])) - { - $context = $data['context']; - $parts = FieldsHelper::extract($context); - - $this->setState('field.context', $context); - - if ($parts) - { - $this->setState('field.component', $parts[0]); - $this->setState('field.section', $parts[1]); - } - } - - if (isset($data['type'])) - { - // This is needed that the plugins can determine the type - $this->setState('field.type', $data['type']); - } - - // Load the fields plugin that they can add additional parameters to the form - PluginHelper::importPlugin('fields'); - - // Get the form. - $form = $this->loadForm( - 'com_fields.field.' . $context, 'field', - array( - 'control' => 'jform', - 'load_data' => true, - ) - ); - - if (empty($form)) - { - return false; - } - - // Modify the form based on Edit State access controls. - if (empty($data['context'])) - { - $data['context'] = $context; - } - - $fieldId = $jinput->get('id'); - $assetKey = $this->state->get('field.component') . '.field.' . $fieldId; - - if (!Factory::getUser()->authorise('core.edit.state', $assetKey)) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('state', 'disabled', 'true'); - - // Disable fields while saving. The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('state', 'filter', 'unset'); - } - - // Don't allow to change the created_user_id user if not allowed to access com_users. - if (!Factory::getUser()->authorise('core.manage', 'com_users')) - { - $form->setFieldAttribute('created_user_id', 'filter', 'unset'); - } - - // In case we are editing a field, field type cannot be changed, so some extra handling below is needed - if ($fieldId) - { - $fieldType = $form->getField('type'); - - if ($fieldType->value == 'subform') - { - // Only Use In subform should not be available for subform field type, so we remove it - $form->removeField('only_use_in_subform'); - } - else - { - // Field type could not be changed, so remove showon attribute to avoid js errors - $form->setFieldAttribute('only_use_in_subform', 'showon', ''); - } - } - - return $form; - } - - /** - * Setting the value for the given field id, context and item id. - * - * @param string $fieldId The field ID. - * @param string $itemId The ID of the item. - * @param string $value The value. - * - * @return boolean - * - * @since 3.7.0 - */ - public function setFieldValue($fieldId, $itemId, $value) - { - $field = $this->getItem($fieldId); - $params = $field->params; - - if (is_array($params)) - { - $params = new Registry($params); - } - - // Don't save the value when the user is not authorized to change it - if (!$field || !FieldsHelper::canEditFieldValue($field)) - { - return false; - } - - $needsDelete = false; - $needsInsert = false; - $needsUpdate = false; - - $oldValue = $this->getFieldValue($fieldId, $itemId); - $value = (array) $value; - - if ($oldValue === null) - { - // No records available, doing normal insert - $needsInsert = true; - } - elseif (count($value) == 1 && count((array) $oldValue) == 1) - { - // Only a single row value update can be done when not empty - $needsUpdate = is_array($value[0]) ? count($value[0]) : strlen($value[0]); - $needsDelete = !$needsUpdate; - } - else - { - // Multiple values, we need to purge the data and do a new - // insert - $needsDelete = true; - $needsInsert = true; - } - - if ($needsDelete) - { - $fieldId = (int) $fieldId; - - // Deleting the existing record as it is a reset - $query = $this->getDatabase()->getQuery(true); - - $query->delete($query->quoteName('#__fields_values')) - ->where($query->quoteName('field_id') . ' = :fieldid') - ->where($query->quoteName('item_id') . ' = :itemid') - ->bind(':fieldid', $fieldId, ParameterType::INTEGER) - ->bind(':itemid', $itemId); - - $this->getDatabase()->setQuery($query)->execute(); - } - - if ($needsInsert) - { - $newObj = new \stdClass; - - $newObj->field_id = (int) $fieldId; - $newObj->item_id = $itemId; - - foreach ($value as $v) - { - $newObj->value = $v; - - $this->getDatabase()->insertObject('#__fields_values', $newObj); - } - } - - if ($needsUpdate) - { - $updateObj = new \stdClass; - - $updateObj->field_id = (int) $fieldId; - $updateObj->item_id = $itemId; - $updateObj->value = reset($value); - - $this->getDatabase()->updateObject('#__fields_values', $updateObj, array('field_id', 'item_id')); - } - - $this->valueCache = array(); - FieldsHelper::clearFieldsCache(); - - return true; - } - - /** - * Returning the value for the given field id, context and item id. - * - * @param string $fieldId The field ID. - * @param string $itemId The ID of the item. - * - * @return NULL|string - * - * @since 3.7.0 - */ - public function getFieldValue($fieldId, $itemId) - { - $values = $this->getFieldValues(array($fieldId), $itemId); - - if (array_key_exists($fieldId, $values)) - { - return $values[$fieldId]; - } - - return null; - } - - /** - * Returning the values for the given field ids, context and item id. - * - * @param array $fieldIds The field Ids. - * @param string $itemId The ID of the item. - * - * @return NULL|array - * - * @since 3.7.0 - */ - public function getFieldValues(array $fieldIds, $itemId) - { - if (!$fieldIds) - { - return array(); - } - - // Create a unique key for the cache - $key = md5(serialize($fieldIds) . $itemId); - - // Fill the cache when it doesn't exist - if (!array_key_exists($key, $this->valueCache)) - { - // Create the query - $query = $this->getDatabase()->getQuery(true); - - $query->select($query->quoteName(['field_id', 'value'])) - ->from($query->quoteName('#__fields_values')) - ->whereIn($query->quoteName('field_id'), ArrayHelper::toInteger($fieldIds)) - ->where($query->quoteName('item_id') . ' = :itemid') - ->bind(':itemid', $itemId); - - // Fetch the row from the database - $rows = $this->getDatabase()->setQuery($query)->loadObjectList(); - - $data = array(); - - // Fill the data container from the database rows - foreach ($rows as $row) - { - // If there are multiple values for a field, create an array - if (array_key_exists($row->field_id, $data)) - { - // Transform it to an array - if (!is_array($data[$row->field_id])) - { - $data[$row->field_id] = array($data[$row->field_id]); - } - - // Set the value in the array - $data[$row->field_id][] = $row->value; - - // Go to the next row, otherwise the value gets overwritten in the data container - continue; - } - - // Set the value - $data[$row->field_id] = $row->value; - } - - // Assign it to the internal cache - $this->valueCache[$key] = $data; - } - - // Return the value from the cache - return $this->valueCache[$key]; - } - - /** - * Cleaning up the values for the given item on the context. - * - * @param string $context The context. - * @param string $itemId The Item ID. - * - * @return void - * - * @since 3.7.0 - */ - public function cleanupValues($context, $itemId) - { - // Delete with inner join is not possible so we need to do a subquery - $fieldsQuery = $this->getDatabase()->getQuery(true); - $fieldsQuery->select($fieldsQuery->quoteName('id')) - ->from($fieldsQuery->quoteName('#__fields')) - ->where($fieldsQuery->quoteName('context') . ' = :context'); - - $query = $this->getDatabase()->getQuery(true); - - $query->delete($query->quoteName('#__fields_values')) - ->where($query->quoteName('field_id') . ' IN (' . $fieldsQuery . ')') - ->where($query->quoteName('item_id') . ' = :itemid') - ->bind(':itemid', $itemId) - ->bind(':context', $context); - - $this->getDatabase()->setQuery($query)->execute(); - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission for the component. - * - * @since 3.7.0 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->state != -2) - { - return false; - } - - $parts = FieldsHelper::extract($record->context); - - return Factory::getUser()->authorise('core.delete', $parts[0] . '.field.' . (int) $record->id); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission for the - * component. - * - * @since 3.7.0 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - $parts = FieldsHelper::extract($record->context); - - // Check for existing field. - if (!empty($record->id)) - { - return $user->authorise('core.edit.state', $parts[0] . '.field.' . (int) $record->id); - } - - return $user->authorise('core.edit.state', $parts[0]); - } - - /** - * Stock method to auto-populate the model state. - * - * @return void - * - * @since 3.7.0 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - // Load the User state. - $pk = $app->input->getInt('id'); - $this->setState($this->getName() . '.id', $pk); - - $context = $app->input->get('context', 'com_content.article'); - $this->setState('field.context', $context); - $parts = FieldsHelper::extract($context); - - // Extract the component name - $this->setState('field.component', $parts[0]); - - // Extract the optional section name - $this->setState('field.section', (count($parts) > 1) ? $parts[1] : null); - - // Load the parameters. - $params = ComponentHelper::getParams('com_fields'); - $this->setState('params', $params); - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param Table $table A Table object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 3.7.0 - */ - protected function getReorderConditions($table) - { - $db = $this->getDatabase(); - - return [ - $db->quoteName('context') . ' = ' . $db->quote($table->context), - ]; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return array The default data is an empty array. - * - * @since 3.7.0 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $app = Factory::getApplication(); - $data = $app->getUserState('com_fields.edit.field.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Pre-select some filters (Status, Language, Access) in edit form - // if those have been selected in Category Manager - if (!$data->id) - { - // Check for which context the Category Manager is used and - // get selected fields - $filters = (array) $app->getUserState('com_fields.fields.filter'); - - $data->set('state', $app->input->getInt('state', ((isset($filters['state']) && $filters['state'] !== '') ? $filters['state'] : null))); - $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); - $data->set('group_id', $app->input->getString('group_id', (!empty($filters['group_id']) ? $filters['group_id'] : null))); - $data->set( - 'access', - $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) - ); - - // Set the type if available from the request - $data->set('type', $app->input->getWord('type', $this->state->get('field.type', $data->get('type')))); - } - - if ($data->label && !isset($data->params['label'])) - { - $data->params['label'] = $data->label; - } - } - - $this->preprocessData('com_fields.field', $data); - - return $data; - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @see JFormRule - * @see JFilterInput - * @since 3.9.23 - */ - public function validate($form, $data, $group = null) - { - if (!Factory::getUser()->authorise('core.admin', 'com_fields')) - { - if (isset($data['rules'])) - { - unset($data['rules']); - } - } - - return parent::validate($form, $data, $group); - } - - /** - * Method to allow derived classes to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 3.7.0 - * - * @throws \Exception if there is an error in the form event. - * - * @see \Joomla\CMS\Form\FormField - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $component = $this->state->get('field.component'); - $section = $this->state->get('field.section'); - $dataObject = $data; - - if (is_array($dataObject)) - { - $dataObject = (object) $dataObject; - } - - if (isset($dataObject->type)) - { - $form->setFieldAttribute('type', 'component', $component); - - // Not allowed to change the type of an existing record - if ($dataObject->id) - { - $form->setFieldAttribute('type', 'readonly', 'true'); - } - - // Allow to override the default value label and description through the plugin - $key = 'PLG_FIELDS_' . strtoupper($dataObject->type) . '_DEFAULT_VALUE_LABEL'; - - if (Factory::getLanguage()->hasKey($key)) - { - $form->setFieldAttribute('default_value', 'label', $key); - } - - $key = 'PLG_FIELDS_' . strtoupper($dataObject->type) . '_DEFAULT_VALUE_DESC'; - - if (Factory::getLanguage()->hasKey($key)) - { - $form->setFieldAttribute('default_value', 'description', $key); - } - - // Remove placeholder field on list fields - if ($dataObject->type == 'list') - { - $form->removeField('hint', 'params'); - } - } - - // Get the categories for this component (and optionally this section, if available) - $cat = ( - function () use ($component, $section) { - // Get the CategoryService for this component - $componentObject = $this->bootComponent($component); - - if (!$componentObject instanceof CategoryServiceInterface) - { - // No CategoryService -> no categories - return null; - } - - $cat = null; - - // Try to get the categories for this component and section - try - { - $cat = $componentObject->getCategory([], $section ?: ''); - } - catch (SectionNotFoundException $e) - { - // Not found for component and section -> Now try once more without the section, so only component - try - { - $cat = $componentObject->getCategory(); - } - catch (SectionNotFoundException $e) - { - // If we haven't found it now, return (no categories available for this component) - return null; - } - } - - // So we found categories for at least the component, return them - return $cat; - } - )(); - - // If we found categories, and if the root category has children, set them in the form - if ($cat && $cat->get('root')->hasChildren()) - { - $form->setFieldAttribute('assigned_cat_ids', 'extension', $cat->getExtension()); - } - else - { - // Else remove the field from the form - $form->removeField('assigned_cat_ids'); - } - - $form->setFieldAttribute('type', 'component', $component); - $form->setFieldAttribute('group_id', 'context', $this->state->get('field.context')); - $form->setFieldAttribute('rules', 'component', $component); - - // Looking in the component forms folder for a specific section forms file - $path = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/forms/fields/' . $section . '.xml'); - - if (!file_exists($path)) - { - // Looking in the component models/forms folder for a specific section forms file - $path = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/models/forms/fields/' . $section . '.xml'); - } - - if (file_exists($path)) - { - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_BASE); - $lang->load($component, JPATH_BASE . '/components/' . $component); - - if (!$form->loadFile($path, false)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - } - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } - - /** - * Clean the cache - * - * @param string $group The cache group - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 3.7.0 - */ - protected function cleanCache($group = null, $clientId = 0) - { - $context = Factory::getApplication()->input->get('context'); - - switch ($context) - { - case 'com_content': - parent::cleanCache('com_content'); - parent::cleanCache('mod_articles_archive'); - parent::cleanCache('mod_articles_categories'); - parent::cleanCache('mod_articles_category'); - parent::cleanCache('mod_articles_latest'); - parent::cleanCache('mod_articles_news'); - parent::cleanCache('mod_articles_popular'); - break; - default: - parent::cleanCache($context); - break; - } - } - - /** - * Batch copy fields to a new group. - * - * @param integer $value The new value matching a fields group. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return array|boolean new IDs if successful, false otherwise and internal error is set. - * - * @since 3.7.0 - */ - protected function batchCopy($value, $pks, $contexts) - { - // Set the variables - $user = Factory::getUser(); - $table = $this->getTable(); - $newIds = array(); - $component = $this->state->get('filter.component'); - $value = (int) $value; - - foreach ($pks as $pk) - { - if ($user->authorise('core.create', $component . '.fieldgroup.' . $value)) - { - $table->reset(); - $table->load($pk); - - $table->group_id = $value; - - // Reset the ID because we are making a copy - $table->id = 0; - - // Unpublish the new field - $table->state = 0; - - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Get the new item ID - $newId = $table->get('id'); - - // Add the new ID to the array - $newIds[$pk] = $newId; - } - else - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_CREATE')); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return $newIds; - } - - /** - * Batch move fields to a new group. - * - * @param integer $value The new value matching a fields group. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 3.7.0 - */ - protected function batchMove($value, $pks, $contexts) - { - // Set the variables - $user = Factory::getUser(); - $table = $this->getTable(); - $context = explode('.', Factory::getApplication()->getUserState('com_fields.fields.context')); - $value = (int) $value; - - foreach ($pks as $pk) - { - if ($user->authorise('core.edit', $context[0] . '.fieldgroup.' . $value)) - { - $table->reset(); - $table->load($pk); - - $table->group_id = $value; - - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - } - else - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } + /** + * @var null|string + * + * @since 3.7.0 + */ + public $typeAlias = null; + + /** + * @var string + * + * @since 3.7.0 + */ + protected $text_prefix = 'COM_FIELDS'; + + /** + * Batch copy/move command. If set to false, + * the batch copy/move command is not supported + * + * @var string + * @since 3.4 + */ + protected $batch_copymove = 'group_id'; + + /** + * Allowed batch commands + * + * @var array + */ + protected $batch_commands = array( + 'assetgroup_id' => 'batchAccess', + 'language_id' => 'batchLanguage' + ); + + /** + * @var array + * + * @since 3.7.0 + */ + private $valueCache = array(); + + /** + * Constructor + * + * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). + * @param MVCFactoryInterface $factory The factory. + * + * @since 3.7.0 + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + parent::__construct($config, $factory); + + $this->typeAlias = Factory::getApplication()->input->getCmd('context', 'com_content.article') . '.field'; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success, False on error. + * + * @since 3.7.0 + */ + public function save($data) + { + $field = null; + + if (isset($data['id']) && $data['id']) { + $field = $this->getItem($data['id']); + } + + if (!isset($data['label']) && isset($data['params']['label'])) { + $data['label'] = $data['params']['label']; + + unset($data['params']['label']); + } + + // Alter the title for save as copy + $input = Factory::getApplication()->input; + + if ($input->get('task') == 'save2copy') { + $origTable = clone $this->getTable(); + $origTable->load($input->getInt('id')); + + if ($data['title'] == $origTable->title) { + list($title, $name) = $this->generateNewTitle($data['group_id'], $data['name'], $data['title']); + $data['title'] = $title; + $data['label'] = $title; + $data['name'] = $name; + } else { + if ($data['name'] == $origTable->name) { + $data['name'] = ''; + } + } + + $data['state'] = 0; + } + + // Load the fields plugins, perhaps they want to do something + PluginHelper::importPlugin('fields'); + + $message = $this->checkDefaultValue($data); + + if ($message !== true) { + $this->setError($message); + + return false; + } + + if (!parent::save($data)) { + return false; + } + + // Save the assigned categories into #__fields_categories + $db = $this->getDatabase(); + $id = (int) $this->getState('field.id'); + + /** + * If the field is only used in subform, set Category to None automatically so that it will only be displayed + * as part of SubForm on add/edit item screen + */ + if (!empty($data['only_use_in_subform'])) { + $cats = [-1]; + } else { + $cats = isset($data['assigned_cat_ids']) ? (array) $data['assigned_cat_ids'] : array(); + $cats = ArrayHelper::toInteger($cats); + } + + $assignedCatIds = array(); + + foreach ($cats as $cat) { + // If we have found the 'JNONE' category, remove all other from the result and break. + if ($cat == '-1') { + $assignedCatIds = array('-1'); + break; + } + + if ($cat) { + $assignedCatIds[] = $cat; + } + } + + // First delete all assigned categories + $query = $db->getQuery(true); + $query->delete('#__fields_categories') + ->where($db->quoteName('field_id') . ' = :fieldid') + ->bind(':fieldid', $id, ParameterType::INTEGER); + + $db->setQuery($query); + $db->execute(); + + // Inset new assigned categories + $tupel = new \stdClass(); + $tupel->field_id = $id; + + foreach ($assignedCatIds as $catId) { + $tupel->category_id = $catId; + $db->insertObject('#__fields_categories', $tupel); + } + + /** + * If the options have changed, delete the values. This should only apply for list, checkboxes and radio + * custom field types, because when their options are being changed, their values might get invalid, because + * e.g. there is a value selected from a list, which is not part of the list anymore. Hence we need to delete + * all values that are not part of the options anymore. Note: The only field types with fieldparams+options + * are those above listed plus the subfields type. And we do explicitly not want the values to be deleted + * when the options of a subfields field are getting changed. + */ + if ( + $field && in_array($field->type, array('list', 'checkboxes', 'radio'), true) + && isset($data['fieldparams']['options']) && isset($field->fieldparams['options']) + ) { + $oldParams = $this->getParams($field->fieldparams['options']); + $newParams = $this->getParams($data['fieldparams']['options']); + + if (is_object($oldParams) && is_object($newParams) && $oldParams != $newParams) { + // Get new values. + $names = array_column((array) $newParams, 'value'); + + $fieldId = (int) $field->id; + $query = $db->getQuery(true); + $query->delete($db->quoteName('#__fields_values')) + ->where($db->quoteName('field_id') . ' = :fieldid') + ->bind(':fieldid', $fieldId, ParameterType::INTEGER); + + // If new values are set, delete only old values. Otherwise delete all values. + if ($names) { + $query->whereNotIn($db->quoteName('value'), $names, ParameterType::STRING); + } + + $db->setQuery($query); + $db->execute(); + } + } + + FieldsHelper::clearFieldsCache(); + + return true; + } + + + /** + * Checks if the default value is valid for the given data. If a string is returned then + * it can be assumed that the default value is invalid. + * + * @param array $data The data. + * + * @return true|string true if valid, a string containing the exception message when not. + * + * @since 3.7.0 + */ + private function checkDefaultValue($data) + { + // Empty default values are correct + if (empty($data['default_value']) && $data['default_value'] !== '0') { + return true; + } + + $types = FieldsHelper::getFieldTypes(); + + // Check if type exists + if (!array_key_exists($data['type'], $types)) { + return true; + } + + $path = $types[$data['type']]['rules']; + + // Add the path for the rules of the plugin when available + if ($path) { + // Add the lookup path for the rule + FormHelper::addRulePath($path); + } + + // Create the fields object + $obj = (object) $data; + $obj->params = new Registry($obj->params); + $obj->fieldparams = new Registry(!empty($obj->fieldparams) ? $obj->fieldparams : array()); + + // Prepare the dom + $dom = new \DOMDocument(); + $node = $dom->appendChild(new \DOMElement('form')); + + // Trigger the event to create the field dom node + $form = new Form($data['context']); + $form->setDatabase($this->getDatabase()); + Factory::getApplication()->triggerEvent('onCustomFieldsPrepareDom', array($obj, $node, $form)); + + // Check if a node is created + if (!$node->firstChild) { + return true; + } + + // Define the type either from the field or from the data + $type = $node->firstChild->getAttribute('validate') ? : $data['type']; + + // Load the rule + $rule = FormHelper::loadRuleType($type); + + // When no rule exists, we allow the default value + if (!$rule) { + return true; + } + + if ($rule instanceof DatabaseAwareInterface) { + try { + $rule->setDatabase($this->getDatabase()); + } catch (DatabaseNotFoundException $e) { + @trigger_error(sprintf('Database must be set, this will not be caught anymore in 5.0.'), E_USER_DEPRECATED); + $rule->setDatabase(Factory::getContainer()->get(DatabaseInterface::class)); + } + } + + try { + // Perform the check + $result = $rule->test(simplexml_import_dom($node->firstChild), $data['default_value']); + + // Check if the test succeeded + return $result === true ? : Text::_('COM_FIELDS_FIELD_INVALID_DEFAULT_VALUE'); + } catch (\UnexpectedValueException $e) { + return $e->getMessage(); + } + } + + /** + * Converts the unknown params into an object. + * + * @param mixed $params The params. + * + * @return \stdClass Object on success, false on failure. + * + * @since 3.7.0 + */ + private function getParams($params) + { + if (is_string($params)) { + $params = json_decode($params); + } + + if (is_array($params)) { + $params = (object) $params; + } + + return $params; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 3.7.0 + */ + public function getItem($pk = null) + { + $result = parent::getItem($pk); + + if ($result) { + // Prime required properties. + if (empty($result->id)) { + $result->context = Factory::getApplication()->input->getCmd('context', $this->getState('field.context')); + } + + if (property_exists($result, 'fieldparams') && $result->fieldparams !== null) { + $registry = new Registry(); + + if ($result->fieldparams) { + $registry->loadString($result->fieldparams); + } + + $result->fieldparams = $registry->toArray(); + } + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $fieldId = (int) $result->id; + $query->select($db->quoteName('category_id')) + ->from($db->quoteName('#__fields_categories')) + ->where($db->quoteName('field_id') . ' = :fieldid') + ->bind(':fieldid', $fieldId, ParameterType::INTEGER); + + $db->setQuery($query); + $result->assigned_cat_ids = $db->loadColumn() ?: array(0); + } + + return $result; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 3.7.0 + * @throws \Exception + */ + public function getTable($name = 'Field', $prefix = 'Administrator', $options = array()) + { + // Default to text type + $table = parent::getTable($name, $prefix, $options); + $table->type = 'text'; + + return $table; + } + + /** + * Method to change the title & name. + * + * @param integer $categoryId The id of the category. + * @param string $name The name. + * @param string $title The title. + * + * @return array Contains the modified title and name. + * + * @since 3.7.0 + */ + protected function generateNewTitle($categoryId, $name, $title) + { + // Alter the title & name + $table = $this->getTable(); + + while ($table->load(array('name' => $name))) { + $title = StringHelper::increment($title); + $name = StringHelper::increment($name, 'dash'); + } + + return array( + $title, + $name, + ); + } + + /** + * Method to delete one or more records. + * + * @param array $pks An array of record primary keys. + * + * @return boolean True if successful, false if an error occurs. + * + * @since 3.7.0 + */ + public function delete(&$pks) + { + $success = parent::delete($pks); + + if ($success) { + $pks = (array) $pks; + $pks = ArrayHelper::toInteger($pks); + $pks = array_filter($pks); + + if (!empty($pks)) { + // Delete Values + $query = $this->getDatabase()->getQuery(true); + + $query->delete($query->quoteName('#__fields_values')) + ->whereIn($query->quoteName('field_id'), $pks); + + $this->getDatabase()->setQuery($query)->execute(); + + // Delete Assigned Categories + $query = $this->getDatabase()->getQuery(true); + + $query->delete($query->quoteName('#__fields_categories')) + ->whereIn($query->quoteName('field_id'), $pks); + + $this->getDatabase()->setQuery($query)->execute(); + } + } + + return $success; + } + + /** + * Abstract method for getting the form from the model. + * + * @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 Form|bool A Form object on success, false on failure + * + * @since 3.7.0 + */ + public function getForm($data = array(), $loadData = true) + { + $context = $this->getState('field.context'); + $jinput = Factory::getApplication()->input; + + // A workaround to get the context into the model for save requests. + if (empty($context) && isset($data['context'])) { + $context = $data['context']; + $parts = FieldsHelper::extract($context); + + $this->setState('field.context', $context); + + if ($parts) { + $this->setState('field.component', $parts[0]); + $this->setState('field.section', $parts[1]); + } + } + + if (isset($data['type'])) { + // This is needed that the plugins can determine the type + $this->setState('field.type', $data['type']); + } + + // Load the fields plugin that they can add additional parameters to the form + PluginHelper::importPlugin('fields'); + + // Get the form. + $form = $this->loadForm( + 'com_fields.field.' . $context, + 'field', + array( + 'control' => 'jform', + 'load_data' => true, + ) + ); + + if (empty($form)) { + return false; + } + + // Modify the form based on Edit State access controls. + if (empty($data['context'])) { + $data['context'] = $context; + } + + $fieldId = $jinput->get('id'); + $assetKey = $this->state->get('field.component') . '.field.' . $fieldId; + + if (!Factory::getUser()->authorise('core.edit.state', $assetKey)) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('state', 'disabled', 'true'); + + // Disable fields while saving. The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('state', 'filter', 'unset'); + } + + // Don't allow to change the created_user_id user if not allowed to access com_users. + if (!Factory::getUser()->authorise('core.manage', 'com_users')) { + $form->setFieldAttribute('created_user_id', 'filter', 'unset'); + } + + // In case we are editing a field, field type cannot be changed, so some extra handling below is needed + if ($fieldId) { + $fieldType = $form->getField('type'); + + if ($fieldType->value == 'subform') { + // Only Use In subform should not be available for subform field type, so we remove it + $form->removeField('only_use_in_subform'); + } else { + // Field type could not be changed, so remove showon attribute to avoid js errors + $form->setFieldAttribute('only_use_in_subform', 'showon', ''); + } + } + + return $form; + } + + /** + * Setting the value for the given field id, context and item id. + * + * @param string $fieldId The field ID. + * @param string $itemId The ID of the item. + * @param string $value The value. + * + * @return boolean + * + * @since 3.7.0 + */ + public function setFieldValue($fieldId, $itemId, $value) + { + $field = $this->getItem($fieldId); + $params = $field->params; + + if (is_array($params)) { + $params = new Registry($params); + } + + // Don't save the value when the user is not authorized to change it + if (!$field || !FieldsHelper::canEditFieldValue($field)) { + return false; + } + + $needsDelete = false; + $needsInsert = false; + $needsUpdate = false; + + $oldValue = $this->getFieldValue($fieldId, $itemId); + $value = (array) $value; + + if ($oldValue === null) { + // No records available, doing normal insert + $needsInsert = true; + } elseif (count($value) == 1 && count((array) $oldValue) == 1) { + // Only a single row value update can be done when not empty + $needsUpdate = is_array($value[0]) ? count($value[0]) : strlen($value[0]); + $needsDelete = !$needsUpdate; + } else { + // Multiple values, we need to purge the data and do a new + // insert + $needsDelete = true; + $needsInsert = true; + } + + if ($needsDelete) { + $fieldId = (int) $fieldId; + + // Deleting the existing record as it is a reset + $query = $this->getDatabase()->getQuery(true); + + $query->delete($query->quoteName('#__fields_values')) + ->where($query->quoteName('field_id') . ' = :fieldid') + ->where($query->quoteName('item_id') . ' = :itemid') + ->bind(':fieldid', $fieldId, ParameterType::INTEGER) + ->bind(':itemid', $itemId); + + $this->getDatabase()->setQuery($query)->execute(); + } + + if ($needsInsert) { + $newObj = new \stdClass(); + + $newObj->field_id = (int) $fieldId; + $newObj->item_id = $itemId; + + foreach ($value as $v) { + $newObj->value = $v; + + $this->getDatabase()->insertObject('#__fields_values', $newObj); + } + } + + if ($needsUpdate) { + $updateObj = new \stdClass(); + + $updateObj->field_id = (int) $fieldId; + $updateObj->item_id = $itemId; + $updateObj->value = reset($value); + + $this->getDatabase()->updateObject('#__fields_values', $updateObj, array('field_id', 'item_id')); + } + + $this->valueCache = array(); + FieldsHelper::clearFieldsCache(); + + return true; + } + + /** + * Returning the value for the given field id, context and item id. + * + * @param string $fieldId The field ID. + * @param string $itemId The ID of the item. + * + * @return NULL|string + * + * @since 3.7.0 + */ + public function getFieldValue($fieldId, $itemId) + { + $values = $this->getFieldValues(array($fieldId), $itemId); + + if (array_key_exists($fieldId, $values)) { + return $values[$fieldId]; + } + + return null; + } + + /** + * Returning the values for the given field ids, context and item id. + * + * @param array $fieldIds The field Ids. + * @param string $itemId The ID of the item. + * + * @return NULL|array + * + * @since 3.7.0 + */ + public function getFieldValues(array $fieldIds, $itemId) + { + if (!$fieldIds) { + return array(); + } + + // Create a unique key for the cache + $key = md5(serialize($fieldIds) . $itemId); + + // Fill the cache when it doesn't exist + if (!array_key_exists($key, $this->valueCache)) { + // Create the query + $query = $this->getDatabase()->getQuery(true); + + $query->select($query->quoteName(['field_id', 'value'])) + ->from($query->quoteName('#__fields_values')) + ->whereIn($query->quoteName('field_id'), ArrayHelper::toInteger($fieldIds)) + ->where($query->quoteName('item_id') . ' = :itemid') + ->bind(':itemid', $itemId); + + // Fetch the row from the database + $rows = $this->getDatabase()->setQuery($query)->loadObjectList(); + + $data = array(); + + // Fill the data container from the database rows + foreach ($rows as $row) { + // If there are multiple values for a field, create an array + if (array_key_exists($row->field_id, $data)) { + // Transform it to an array + if (!is_array($data[$row->field_id])) { + $data[$row->field_id] = array($data[$row->field_id]); + } + + // Set the value in the array + $data[$row->field_id][] = $row->value; + + // Go to the next row, otherwise the value gets overwritten in the data container + continue; + } + + // Set the value + $data[$row->field_id] = $row->value; + } + + // Assign it to the internal cache + $this->valueCache[$key] = $data; + } + + // Return the value from the cache + return $this->valueCache[$key]; + } + + /** + * Cleaning up the values for the given item on the context. + * + * @param string $context The context. + * @param string $itemId The Item ID. + * + * @return void + * + * @since 3.7.0 + */ + public function cleanupValues($context, $itemId) + { + // Delete with inner join is not possible so we need to do a subquery + $fieldsQuery = $this->getDatabase()->getQuery(true); + $fieldsQuery->select($fieldsQuery->quoteName('id')) + ->from($fieldsQuery->quoteName('#__fields')) + ->where($fieldsQuery->quoteName('context') . ' = :context'); + + $query = $this->getDatabase()->getQuery(true); + + $query->delete($query->quoteName('#__fields_values')) + ->where($query->quoteName('field_id') . ' IN (' . $fieldsQuery . ')') + ->where($query->quoteName('item_id') . ' = :itemid') + ->bind(':itemid', $itemId) + ->bind(':context', $context); + + $this->getDatabase()->setQuery($query)->execute(); + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission for the component. + * + * @since 3.7.0 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->state != -2) { + return false; + } + + $parts = FieldsHelper::extract($record->context); + + return Factory::getUser()->authorise('core.delete', $parts[0] . '.field.' . (int) $record->id); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission for the + * component. + * + * @since 3.7.0 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + $parts = FieldsHelper::extract($record->context); + + // Check for existing field. + if (!empty($record->id)) { + return $user->authorise('core.edit.state', $parts[0] . '.field.' . (int) $record->id); + } + + return $user->authorise('core.edit.state', $parts[0]); + } + + /** + * Stock method to auto-populate the model state. + * + * @return void + * + * @since 3.7.0 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the User state. + $pk = $app->input->getInt('id'); + $this->setState($this->getName() . '.id', $pk); + + $context = $app->input->get('context', 'com_content.article'); + $this->setState('field.context', $context); + $parts = FieldsHelper::extract($context); + + // Extract the component name + $this->setState('field.component', $parts[0]); + + // Extract the optional section name + $this->setState('field.section', (count($parts) > 1) ? $parts[1] : null); + + // Load the parameters. + $params = ComponentHelper::getParams('com_fields'); + $this->setState('params', $params); + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param Table $table A Table object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 3.7.0 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('context') . ' = ' . $db->quote($table->context), + ]; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return array The default data is an empty array. + * + * @since 3.7.0 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $app = Factory::getApplication(); + $data = $app->getUserState('com_fields.edit.field.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Pre-select some filters (Status, Language, Access) in edit form + // if those have been selected in Category Manager + if (!$data->id) { + // Check for which context the Category Manager is used and + // get selected fields + $filters = (array) $app->getUserState('com_fields.fields.filter'); + + $data->set('state', $app->input->getInt('state', ((isset($filters['state']) && $filters['state'] !== '') ? $filters['state'] : null))); + $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); + $data->set('group_id', $app->input->getString('group_id', (!empty($filters['group_id']) ? $filters['group_id'] : null))); + $data->set( + 'access', + $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) + ); + + // Set the type if available from the request + $data->set('type', $app->input->getWord('type', $this->state->get('field.type', $data->get('type')))); + } + + if ($data->label && !isset($data->params['label'])) { + $data->params['label'] = $data->label; + } + } + + $this->preprocessData('com_fields.field', $data); + + return $data; + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @see JFormRule + * @see JFilterInput + * @since 3.9.23 + */ + public function validate($form, $data, $group = null) + { + if (!Factory::getUser()->authorise('core.admin', 'com_fields')) { + if (isset($data['rules'])) { + unset($data['rules']); + } + } + + return parent::validate($form, $data, $group); + } + + /** + * Method to allow derived classes to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 3.7.0 + * + * @throws \Exception if there is an error in the form event. + * + * @see \Joomla\CMS\Form\FormField + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $component = $this->state->get('field.component'); + $section = $this->state->get('field.section'); + $dataObject = $data; + + if (is_array($dataObject)) { + $dataObject = (object) $dataObject; + } + + if (isset($dataObject->type)) { + $form->setFieldAttribute('type', 'component', $component); + + // Not allowed to change the type of an existing record + if ($dataObject->id) { + $form->setFieldAttribute('type', 'readonly', 'true'); + } + + // Allow to override the default value label and description through the plugin + $key = 'PLG_FIELDS_' . strtoupper($dataObject->type) . '_DEFAULT_VALUE_LABEL'; + + if (Factory::getLanguage()->hasKey($key)) { + $form->setFieldAttribute('default_value', 'label', $key); + } + + $key = 'PLG_FIELDS_' . strtoupper($dataObject->type) . '_DEFAULT_VALUE_DESC'; + + if (Factory::getLanguage()->hasKey($key)) { + $form->setFieldAttribute('default_value', 'description', $key); + } + + // Remove placeholder field on list fields + if ($dataObject->type == 'list') { + $form->removeField('hint', 'params'); + } + } + + // Get the categories for this component (and optionally this section, if available) + $cat = ( + function () use ($component, $section) { + // Get the CategoryService for this component + $componentObject = $this->bootComponent($component); + + if (!$componentObject instanceof CategoryServiceInterface) { + // No CategoryService -> no categories + return null; + } + + $cat = null; + + // Try to get the categories for this component and section + try { + $cat = $componentObject->getCategory([], $section ?: ''); + } catch (SectionNotFoundException $e) { + // Not found for component and section -> Now try once more without the section, so only component + try { + $cat = $componentObject->getCategory(); + } catch (SectionNotFoundException $e) { + // If we haven't found it now, return (no categories available for this component) + return null; + } + } + + // So we found categories for at least the component, return them + return $cat; + } + )(); + + // If we found categories, and if the root category has children, set them in the form + if ($cat && $cat->get('root')->hasChildren()) { + $form->setFieldAttribute('assigned_cat_ids', 'extension', $cat->getExtension()); + } else { + // Else remove the field from the form + $form->removeField('assigned_cat_ids'); + } + + $form->setFieldAttribute('type', 'component', $component); + $form->setFieldAttribute('group_id', 'context', $this->state->get('field.context')); + $form->setFieldAttribute('rules', 'component', $component); + + // Looking in the component forms folder for a specific section forms file + $path = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/forms/fields/' . $section . '.xml'); + + if (!file_exists($path)) { + // Looking in the component models/forms folder for a specific section forms file + $path = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/models/forms/fields/' . $section . '.xml'); + } + + if (file_exists($path)) { + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_BASE); + $lang->load($component, JPATH_BASE . '/components/' . $component); + + if (!$form->loadFile($path, false)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + } + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } + + /** + * Clean the cache + * + * @param string $group The cache group + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 3.7.0 + */ + protected function cleanCache($group = null, $clientId = 0) + { + $context = Factory::getApplication()->input->get('context'); + + switch ($context) { + case 'com_content': + parent::cleanCache('com_content'); + parent::cleanCache('mod_articles_archive'); + parent::cleanCache('mod_articles_categories'); + parent::cleanCache('mod_articles_category'); + parent::cleanCache('mod_articles_latest'); + parent::cleanCache('mod_articles_news'); + parent::cleanCache('mod_articles_popular'); + break; + default: + parent::cleanCache($context); + break; + } + } + + /** + * Batch copy fields to a new group. + * + * @param integer $value The new value matching a fields group. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return array|boolean new IDs if successful, false otherwise and internal error is set. + * + * @since 3.7.0 + */ + protected function batchCopy($value, $pks, $contexts) + { + // Set the variables + $user = Factory::getUser(); + $table = $this->getTable(); + $newIds = array(); + $component = $this->state->get('filter.component'); + $value = (int) $value; + + foreach ($pks as $pk) { + if ($user->authorise('core.create', $component . '.fieldgroup.' . $value)) { + $table->reset(); + $table->load($pk); + + $table->group_id = $value; + + // Reset the ID because we are making a copy + $table->id = 0; + + // Unpublish the new field + $table->state = 0; + + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Get the new item ID + $newId = $table->get('id'); + + // Add the new ID to the array + $newIds[$pk] = $newId; + } else { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_CREATE')); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return $newIds; + } + + /** + * Batch move fields to a new group. + * + * @param integer $value The new value matching a fields group. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 3.7.0 + */ + protected function batchMove($value, $pks, $contexts) + { + // Set the variables + $user = Factory::getUser(); + $table = $this->getTable(); + $context = explode('.', Factory::getApplication()->getUserState('com_fields.fields.context')); + $value = (int) $value; + + foreach ($pks as $pk) { + if ($user->authorise('core.edit', $context[0] . '.fieldgroup.' . $value)) { + $table->reset(); + $table->load($pk); + + $table->group_id = $value; + + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + } else { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } } diff --git a/administrator/components/com_fields/src/Model/FieldsModel.php b/administrator/components/com_fields/src/Model/FieldsModel.php index 9bcb237868609..13edfa9255709 100644 --- a/administrator/components/com_fields/src/Model/FieldsModel.php +++ b/administrator/components/com_fields/src/Model/FieldsModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest($this->context . '.context', 'context', 'com_content.article', 'CMD'); - $this->setState('filter.context', $context); - - // Split context into component and optional section - $parts = FieldsHelper::extract($context); - - if ($parts) - { - $this->setState('filter.component', $parts[0]); - $this->setState('filter.section', $parts[1]); - } - } - - /** - * Method to get a store id based on the model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id An identifier string to generate the store id. - * - * @return string A store id. - * - * @since 3.7.0 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.context'); - $id .= ':' . serialize($this->getState('filter.assigned_cat_ids')); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.group_id'); - $id .= ':' . serialize($this->getState('filter.language')); - $id .= ':' . $this->getState('filter.only_use_in_subform'); - - return parent::getStoreId($id); - } - - /** - * Method to get a DatabaseQuery object for retrieving the data set from a database. - * - * @return \Joomla\Database\DatabaseQuery A DatabaseQuery object to retrieve the data set. - * - * @since 3.7.0 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - $app = Factory::getApplication(); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'DISTINCT a.id, a.title, a.name, a.checked_out, a.checked_out_time, a.note' . - ', a.state, a.access, a.created_time, a.created_user_id, a.ordering, a.language' . - ', a.fieldparams, a.params, a.type, a.default_value, a.context, a.group_id' . - ', a.label, a.description, a.required, a.only_use_in_subform' - ) - ); - $query->from('#__fields AS a'); - - // Join over the language - $query->select('l.title AS language_title, l.image AS language_image') - ->join('LEFT', $db->quoteName('#__languages') . ' AS l ON l.lang_code = a.language'); - - // Join over the users for the checked out user. - $query->select('uc.name AS editor')->join('LEFT', '#__users AS uc ON uc.id=a.checked_out'); - - // Join over the asset groups. - $query->select('ag.title AS access_level')->join('LEFT', '#__viewlevels AS ag ON ag.id = a.access'); - - // Join over the users for the author. - $query->select('ua.name AS author_name')->join('LEFT', '#__users AS ua ON ua.id = a.created_user_id'); - - // Join over the field groups. - $query->select('g.title AS group_title, g.access as group_access, g.state AS group_state, g.note as group_note'); - $query->join('LEFT', '#__fields_groups AS g ON g.id = a.group_id'); - - // Filter by context - if ($context = $this->getState('filter.context')) - { - $query->where($db->quoteName('a.context') . ' = :context') - ->bind(':context', $context); - } - - // Filter by access level. - if ($access = $this->getState('filter.access')) - { - if (is_array($access)) - { - $access = ArrayHelper::toInteger($access); - $query->whereIn($db->quoteName('a.access'), $access); - } - else - { - $access = (int) $access; - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - } - - if (($categories = $this->getState('filter.assigned_cat_ids')) && $context) - { - $categories = (array) $categories; - $categories = ArrayHelper::toInteger($categories); - $parts = FieldsHelper::extract($context); - - if ($parts) - { - // Get the categories for this component (and optionally this section, if available) - $cat = ( - function () use ($parts) { - // Get the CategoryService for this component - $componentObject = $this->bootComponent($parts[0]); - - if (!$componentObject instanceof CategoryServiceInterface) - { - // No CategoryService -> no categories - return null; - } - - $cat = null; - - // Try to get the categories for this component and section - try - { - $cat = $componentObject->getCategory([], $parts[1] ?: ''); - } - catch (SectionNotFoundException $e) - { - // Not found for component and section -> Now try once more without the section, so only component - try - { - $cat = $componentObject->getCategory(); - } - catch (SectionNotFoundException $e) - { - // If we haven't found it now, return (no categories available for this component) - return null; - } - } - - // So we found categories for at least the component, return them - return $cat; - } - )(); - - if ($cat) - { - foreach ($categories as $assignedCatIds) - { - // Check if we have the actual category - $parent = $cat->get($assignedCatIds); - - if ($parent) - { - $categories[] = (int) $parent->id; - - // Traverse the tree up to get all the fields which are attached to a parent - while ($parent->getParent() && $parent->getParent()->id != 'root') - { - $parent = $parent->getParent(); - $categories[] = (int) $parent->id; - } - } - } - } - } - - $categories = array_unique($categories); - - // Join over the assigned categories - $query->join('LEFT', $db->quoteName('#__fields_categories') . ' AS fc ON fc.field_id = a.id'); - - if (in_array('0', $categories)) - { - $query->where( - '(' . - $db->quoteName('fc.category_id') . ' IS NULL OR ' . - $db->quoteName('fc.category_id') . ' IN (' . implode(',', $query->bindArray(array_values($categories), ParameterType::INTEGER)) . ')' . - ')' - ); - } - else - { - $query->whereIn($db->quoteName('fc.category_id'), $categories); - } - } - - // Implement View Level Access - if (!$app->isClient('administrator') || !$user->authorise('core.admin')) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('a.access'), $groups); - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.group_id') . ' = 0', - $db->quoteName('g.access') . ' IN (' . implode(',', $query->bindArray($groups, ParameterType::INTEGER)) . ')' - ], - 'OR' - ); - } - - // Filter by state - $state = $this->getState('filter.state'); - - // Include group state only when not on on back end list - $includeGroupState = !$app->isClient('administrator') || - $app->input->get('option') != 'com_fields' || - $app->input->get('view') != 'fields'; - - if (is_numeric($state)) - { - $state = (int) $state; - $query->where($db->quoteName('a.state') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - - if ($includeGroupState) - { - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.group_id') . ' = 0', - $db->quoteName('g.state') . ' = :gstate', - ], - 'OR' - ) - ->bind(':gstate', $state, ParameterType::INTEGER); - } - } - elseif (!$state) - { - $query->whereIn($db->quoteName('a.state'), [0, 1]); - - if ($includeGroupState) - { - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.group_id') . ' = 0', - $db->quoteName('g.state') . ' IN (' . implode(',', $query->bindArray([0, 1], ParameterType::INTEGER)) . ')' - ], - 'OR' - ); - } - } - - $groupId = $this->getState('filter.group_id'); - - if (is_numeric($groupId)) - { - $groupId = (int) $groupId; - $query->where($db->quoteName('a.group_id') . ' = :groupid') - ->bind(':groupid', $groupId, ParameterType::INTEGER); - } - - $onlyUseInSubForm = $this->getState('filter.only_use_in_subform'); - - if (is_numeric($onlyUseInSubForm)) - { - $onlyUseInSubForm = (int) $onlyUseInSubForm; - $query->where($db->quoteName('a.only_use_in_subform') . ' = :only_use_in_subform') - ->bind(':only_use_in_subform', $onlyUseInSubForm, ParameterType::INTEGER); - } - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $search, ParameterType::INTEGER); - } - elseif (stripos($search, 'author:') === 0) - { - $search = '%' . substr($search, 7) . '%'; - $query->where( - '(' . - $db->quoteName('ua.name') . ' LIKE :name OR ' . - $db->quoteName('ua.username') . ' LIKE :username' . - ')' - ) - ->bind(':name', $search) - ->bind(':username', $search); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where( - '(' . - $db->quoteName('a.title') . ' LIKE :title OR ' . - $db->quoteName('a.name') . ' LIKE :sname OR ' . - $db->quoteName('a.note') . ' LIKE :note' . - ')' - ) - ->bind(':title', $search) - ->bind(':sname', $search) - ->bind(':note', $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $language = (array) $language; - - $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); - } - - // Add the list ordering clause - $listOrdering = $this->state->get('list.ordering', 'a.ordering'); - $orderDirn = $this->state->get('list.direction', 'ASC'); - - $query->order($db->escape($listOrdering) . ' ' . $db->escape($orderDirn)); - - return $query; - } - - /** - * Gets an array of objects from the results of database query. - * - * @param string $query The query. - * @param integer $limitstart Offset. - * @param integer $limit The number of records. - * - * @return array An array of results. - * - * @since 3.7.0 - * @throws \RuntimeException - */ - protected function _getList($query, $limitstart = 0, $limit = 0) - { - $result = parent::_getList($query, $limitstart, $limit); - - if (is_array($result)) - { - foreach ($result as $field) - { - $field->fieldparams = new Registry($field->fieldparams); - $field->params = new Registry($field->params); - } - } - - return $result; - } - - /** - * Get the filter form - * - * @param array $data data - * @param boolean $loadData load current data - * - * @return \Joomla\CMS\Form\Form|bool the Form object or false - * - * @since 3.7.0 - */ - public function getFilterForm($data = array(), $loadData = true) - { - $form = parent::getFilterForm($data, $loadData); - - if ($form) - { - $form->setValue('context', null, $this->getState('filter.context')); - $form->setFieldAttribute('group_id', 'context', $this->getState('filter.context'), 'filter'); - $form->setFieldAttribute('assigned_cat_ids', 'extension', $this->state->get('filter.component'), 'filter'); - } - - return $form; - } - - /** - * Get the groups for the batch method - * - * @return array An array of groups - * - * @since 3.7.0 - */ - public function getGroups() - { - $user = Factory::getUser(); - $viewlevels = ArrayHelper::toInteger($user->getAuthorisedViewLevels()); - $context = $this->state->get('filter.context'); - - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $query->select( - [ - $db->quoteName('title', 'text'), - $db->quoteName('id', 'value'), - $db->quoteName('state'), - ] - ); - $query->from($db->quoteName('#__fields_groups')); - $query->whereIn($db->quoteName('state'), [0, 1]); - $query->where($db->quoteName('context') . ' = :context'); - $query->whereIn($db->quoteName('access'), $viewlevels); - $query->bind(':context', $context); - - $db->setQuery($query); - - return $db->loadObjectList(); - } + /** + * Constructor + * + * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). + * @param MVCFactoryInterface $factory The factory. + * + * @since 3.7.0 + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'type', 'a.type', + 'name', 'a.name', + 'state', 'a.state', + 'access', 'a.access', + 'access_level', + 'only_use_in_subform', + 'language', 'a.language', + 'ordering', 'a.ordering', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'created_time', 'a.created_time', + 'created_user_id', 'a.created_user_id', + 'group_title', 'g.title', + 'category_id', 'a.category_id', + 'group_id', 'a.group_id', + 'assigned_cat_ids' + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.7.0 + */ + protected function populateState($ordering = null, $direction = null) + { + // List state information. + parent::populateState('a.ordering', 'asc'); + + $context = $this->getUserStateFromRequest($this->context . '.context', 'context', 'com_content.article', 'CMD'); + $this->setState('filter.context', $context); + + // Split context into component and optional section + $parts = FieldsHelper::extract($context); + + if ($parts) { + $this->setState('filter.component', $parts[0]); + $this->setState('filter.section', $parts[1]); + } + } + + /** + * Method to get a store id based on the model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id An identifier string to generate the store id. + * + * @return string A store id. + * + * @since 3.7.0 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.context'); + $id .= ':' . serialize($this->getState('filter.assigned_cat_ids')); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.group_id'); + $id .= ':' . serialize($this->getState('filter.language')); + $id .= ':' . $this->getState('filter.only_use_in_subform'); + + return parent::getStoreId($id); + } + + /** + * Method to get a DatabaseQuery object for retrieving the data set from a database. + * + * @return \Joomla\Database\DatabaseQuery A DatabaseQuery object to retrieve the data set. + * + * @since 3.7.0 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + $app = Factory::getApplication(); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'DISTINCT a.id, a.title, a.name, a.checked_out, a.checked_out_time, a.note' . + ', a.state, a.access, a.created_time, a.created_user_id, a.ordering, a.language' . + ', a.fieldparams, a.params, a.type, a.default_value, a.context, a.group_id' . + ', a.label, a.description, a.required, a.only_use_in_subform' + ) + ); + $query->from('#__fields AS a'); + + // Join over the language + $query->select('l.title AS language_title, l.image AS language_image') + ->join('LEFT', $db->quoteName('#__languages') . ' AS l ON l.lang_code = a.language'); + + // Join over the users for the checked out user. + $query->select('uc.name AS editor')->join('LEFT', '#__users AS uc ON uc.id=a.checked_out'); + + // Join over the asset groups. + $query->select('ag.title AS access_level')->join('LEFT', '#__viewlevels AS ag ON ag.id = a.access'); + + // Join over the users for the author. + $query->select('ua.name AS author_name')->join('LEFT', '#__users AS ua ON ua.id = a.created_user_id'); + + // Join over the field groups. + $query->select('g.title AS group_title, g.access as group_access, g.state AS group_state, g.note as group_note'); + $query->join('LEFT', '#__fields_groups AS g ON g.id = a.group_id'); + + // Filter by context + if ($context = $this->getState('filter.context')) { + $query->where($db->quoteName('a.context') . ' = :context') + ->bind(':context', $context); + } + + // Filter by access level. + if ($access = $this->getState('filter.access')) { + if (is_array($access)) { + $access = ArrayHelper::toInteger($access); + $query->whereIn($db->quoteName('a.access'), $access); + } else { + $access = (int) $access; + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + } + + if (($categories = $this->getState('filter.assigned_cat_ids')) && $context) { + $categories = (array) $categories; + $categories = ArrayHelper::toInteger($categories); + $parts = FieldsHelper::extract($context); + + if ($parts) { + // Get the categories for this component (and optionally this section, if available) + $cat = ( + function () use ($parts) { + // Get the CategoryService for this component + $componentObject = $this->bootComponent($parts[0]); + + if (!$componentObject instanceof CategoryServiceInterface) { + // No CategoryService -> no categories + return null; + } + + $cat = null; + + // Try to get the categories for this component and section + try { + $cat = $componentObject->getCategory([], $parts[1] ?: ''); + } catch (SectionNotFoundException $e) { + // Not found for component and section -> Now try once more without the section, so only component + try { + $cat = $componentObject->getCategory(); + } catch (SectionNotFoundException $e) { + // If we haven't found it now, return (no categories available for this component) + return null; + } + } + + // So we found categories for at least the component, return them + return $cat; + } + )(); + + if ($cat) { + foreach ($categories as $assignedCatIds) { + // Check if we have the actual category + $parent = $cat->get($assignedCatIds); + + if ($parent) { + $categories[] = (int) $parent->id; + + // Traverse the tree up to get all the fields which are attached to a parent + while ($parent->getParent() && $parent->getParent()->id != 'root') { + $parent = $parent->getParent(); + $categories[] = (int) $parent->id; + } + } + } + } + } + + $categories = array_unique($categories); + + // Join over the assigned categories + $query->join('LEFT', $db->quoteName('#__fields_categories') . ' AS fc ON fc.field_id = a.id'); + + if (in_array('0', $categories)) { + $query->where( + '(' . + $db->quoteName('fc.category_id') . ' IS NULL OR ' . + $db->quoteName('fc.category_id') . ' IN (' . implode(',', $query->bindArray(array_values($categories), ParameterType::INTEGER)) . ')' . + ')' + ); + } else { + $query->whereIn($db->quoteName('fc.category_id'), $categories); + } + } + + // Implement View Level Access + if (!$app->isClient('administrator') || !$user->authorise('core.admin')) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('a.access'), $groups); + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.group_id') . ' = 0', + $db->quoteName('g.access') . ' IN (' . implode(',', $query->bindArray($groups, ParameterType::INTEGER)) . ')' + ], + 'OR' + ); + } + + // Filter by state + $state = $this->getState('filter.state'); + + // Include group state only when not on on back end list + $includeGroupState = !$app->isClient('administrator') || + $app->input->get('option') != 'com_fields' || + $app->input->get('view') != 'fields'; + + if (is_numeric($state)) { + $state = (int) $state; + $query->where($db->quoteName('a.state') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + + if ($includeGroupState) { + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.group_id') . ' = 0', + $db->quoteName('g.state') . ' = :gstate', + ], + 'OR' + ) + ->bind(':gstate', $state, ParameterType::INTEGER); + } + } elseif (!$state) { + $query->whereIn($db->quoteName('a.state'), [0, 1]); + + if ($includeGroupState) { + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.group_id') . ' = 0', + $db->quoteName('g.state') . ' IN (' . implode(',', $query->bindArray([0, 1], ParameterType::INTEGER)) . ')' + ], + 'OR' + ); + } + } + + $groupId = $this->getState('filter.group_id'); + + if (is_numeric($groupId)) { + $groupId = (int) $groupId; + $query->where($db->quoteName('a.group_id') . ' = :groupid') + ->bind(':groupid', $groupId, ParameterType::INTEGER); + } + + $onlyUseInSubForm = $this->getState('filter.only_use_in_subform'); + + if (is_numeric($onlyUseInSubForm)) { + $onlyUseInSubForm = (int) $onlyUseInSubForm; + $query->where($db->quoteName('a.only_use_in_subform') . ' = :only_use_in_subform') + ->bind(':only_use_in_subform', $onlyUseInSubForm, ParameterType::INTEGER); + } + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $search, ParameterType::INTEGER); + } elseif (stripos($search, 'author:') === 0) { + $search = '%' . substr($search, 7) . '%'; + $query->where( + '(' . + $db->quoteName('ua.name') . ' LIKE :name OR ' . + $db->quoteName('ua.username') . ' LIKE :username' . + ')' + ) + ->bind(':name', $search) + ->bind(':username', $search); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where( + '(' . + $db->quoteName('a.title') . ' LIKE :title OR ' . + $db->quoteName('a.name') . ' LIKE :sname OR ' . + $db->quoteName('a.note') . ' LIKE :note' . + ')' + ) + ->bind(':title', $search) + ->bind(':sname', $search) + ->bind(':note', $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $language = (array) $language; + + $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); + } + + // Add the list ordering clause + $listOrdering = $this->state->get('list.ordering', 'a.ordering'); + $orderDirn = $this->state->get('list.direction', 'ASC'); + + $query->order($db->escape($listOrdering) . ' ' . $db->escape($orderDirn)); + + return $query; + } + + /** + * Gets an array of objects from the results of database query. + * + * @param string $query The query. + * @param integer $limitstart Offset. + * @param integer $limit The number of records. + * + * @return array An array of results. + * + * @since 3.7.0 + * @throws \RuntimeException + */ + protected function _getList($query, $limitstart = 0, $limit = 0) + { + $result = parent::_getList($query, $limitstart, $limit); + + if (is_array($result)) { + foreach ($result as $field) { + $field->fieldparams = new Registry($field->fieldparams); + $field->params = new Registry($field->params); + } + } + + return $result; + } + + /** + * Get the filter form + * + * @param array $data data + * @param boolean $loadData load current data + * + * @return \Joomla\CMS\Form\Form|bool the Form object or false + * + * @since 3.7.0 + */ + public function getFilterForm($data = array(), $loadData = true) + { + $form = parent::getFilterForm($data, $loadData); + + if ($form) { + $form->setValue('context', null, $this->getState('filter.context')); + $form->setFieldAttribute('group_id', 'context', $this->getState('filter.context'), 'filter'); + $form->setFieldAttribute('assigned_cat_ids', 'extension', $this->state->get('filter.component'), 'filter'); + } + + return $form; + } + + /** + * Get the groups for the batch method + * + * @return array An array of groups + * + * @since 3.7.0 + */ + public function getGroups() + { + $user = Factory::getUser(); + $viewlevels = ArrayHelper::toInteger($user->getAuthorisedViewLevels()); + $context = $this->state->get('filter.context'); + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('title', 'text'), + $db->quoteName('id', 'value'), + $db->quoteName('state'), + ] + ); + $query->from($db->quoteName('#__fields_groups')); + $query->whereIn($db->quoteName('state'), [0, 1]); + $query->where($db->quoteName('context') . ' = :context'); + $query->whereIn($db->quoteName('access'), $viewlevels); + $query->bind(':context', $context); + + $db->setQuery($query); + + return $db->loadObjectList(); + } } diff --git a/administrator/components/com_fields/src/Model/GroupModel.php b/administrator/components/com_fields/src/Model/GroupModel.php index 81c1c2b4947cc..58a529d5f464b 100644 --- a/administrator/components/com_fields/src/Model/GroupModel.php +++ b/administrator/components/com_fields/src/Model/GroupModel.php @@ -1,4 +1,5 @@ 'batchAccess', - 'language_id' => 'batchLanguage' - ); - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success, False on error. - * - * @since 3.7.0 - */ - public function save($data) - { - // Alter the title for save as copy - $input = Factory::getApplication()->input; - - // Save new group as unpublished - if ($input->get('task') == 'save2copy') - { - $data['state'] = 0; - } - - return parent::save($data); - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 3.7.0 - * @throws \Exception - */ - public function getTable($name = 'Group', $prefix = 'Administrator', $options = array()) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Abstract method for getting the form from the model. - * - * @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 mixed A Form object on success, false on failure - * - * @since 3.7.0 - */ - public function getForm($data = array(), $loadData = true) - { - $context = $this->getState('filter.context'); - $jinput = Factory::getApplication()->input; - - if (empty($context) && isset($data['context'])) - { - $context = $data['context']; - $this->setState('filter.context', $context); - } - - // Get the form. - $form = $this->loadForm( - 'com_fields.group.' . $context, 'group', - array( - 'control' => 'jform', - 'load_data' => $loadData, - ) - ); - - if (empty($form)) - { - return false; - } - - // Modify the form based on Edit State access controls. - if (empty($data['context'])) - { - $data['context'] = $context; - } - - $user = Factory::getUser(); - - if (!$user->authorise('core.edit.state', $context . '.fieldgroup.' . $jinput->get('id'))) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('state', 'disabled', 'true'); - - // Disable fields while saving. The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('state', 'filter', 'unset'); - } - - // Don't allow to change the created_by user if not allowed to access com_users. - if (!$user->authorise('core.manage', 'com_users')) - { - $form->setFieldAttribute('created_by', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission for the component. - * - * @since 3.7.0 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->state != -2) - { - return false; - } - - return Factory::getUser()->authorise('core.delete', $record->context . '.fieldgroup.' . (int) $record->id); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission for the - * component. - * - * @since 3.7.0 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - - // Check for existing fieldgroup. - if (!empty($record->id)) - { - return $user->authorise('core.edit.state', $record->context . '.fieldgroup.' . (int) $record->id); - } - - // Default to component settings. - return $user->authorise('core.edit.state', $record->context); - } - - /** - * Auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 3.7.0 - */ - protected function populateState() - { - parent::populateState(); - - $context = Factory::getApplication()->getUserStateFromRequest('com_fields.groups.context', 'context', 'com_fields', 'CMD'); - $this->setState('filter.context', $context); - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param Table $table A Table object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 3.7.0 - */ - protected function getReorderConditions($table) - { - $db = $this->getDatabase(); - - return [ - $db->quoteName('context') . ' = ' . $db->quote($table->context), - ]; - } - - /** - * Method to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @see \Joomla\CMS\Form\FormField - * @since 3.7.0 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - parent::preprocessForm($form, $data, $group); - - $parts = FieldsHelper::extract($this->state->get('filter.context')); - - // Extract the component name - $component = $parts[0]; - - // Extract the optional section name - $section = (count($parts) > 1) ? $parts[1] : null; - - if ($parts) - { - // Set the access control rules field component value. - $form->setFieldAttribute('rules', 'component', $component); - } - - if ($section !== null) - { - // Looking first in the component models/forms folder - $path = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/models/forms/fieldgroup/' . $section . '.xml'); - - if (file_exists($path)) - { - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_BASE); - $lang->load($component, JPATH_BASE . '/components/' . $component); - - if (!$form->loadFile($path, false)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - } - } - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @see JFormRule - * @see JFilterInput - * @since 3.9.23 - */ - public function validate($form, $data, $group = null) - { - if (!Factory::getUser()->authorise('core.admin', 'com_fields')) - { - if (isset($data['rules'])) - { - unset($data['rules']); - } - } - - return parent::validate($form, $data, $group); - } - - /** - * Method to get the data that should be injected in the form. - * - * @return array The default data is an empty array. - * - * @since 3.7.0 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $app = Factory::getApplication(); - $data = $app->getUserState('com_fields.edit.group.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Pre-select some filters (Status, Language, Access) in edit form if those have been selected in Field Group Manager - if (!$data->id) - { - // Check for which context the Field Group Manager is used and get selected fields - $context = substr($app->getUserState('com_fields.groups.filter.context', ''), 4); - $filters = (array) $app->getUserState('com_fields.groups.' . $context . '.filter'); - - $data->set( - 'state', - $app->input->getInt('state', (!empty($filters['state']) ? $filters['state'] : null)) - ); - $data->set( - 'language', - $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null)) - ); - $data->set( - 'access', - $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) - ); - } - } - - $this->preprocessData('com_fields.group', $data); - - return $data; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 3.7.0 - */ - public function getItem($pk = null) - { - if ($item = parent::getItem($pk)) - { - // Prime required properties. - if (empty($item->id)) - { - $item->context = $this->getState('filter.context'); - } - - if (property_exists($item, 'params')) - { - $item->params = new Registry($item->params); - } - } - - return $item; - } - - /** - * Clean the cache - * - * @param string $group The cache group - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 3.7.0 - */ - protected function cleanCache($group = null, $clientId = 0) - { - $context = Factory::getApplication()->input->get('context'); - - parent::cleanCache($context); - } + /** + * @var null|string + * + * @since 3.7.0 + */ + public $typeAlias = null; + + /** + * Allowed batch commands + * + * @var array + */ + protected $batch_commands = array( + 'assetgroup_id' => 'batchAccess', + 'language_id' => 'batchLanguage' + ); + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success, False on error. + * + * @since 3.7.0 + */ + public function save($data) + { + // Alter the title for save as copy + $input = Factory::getApplication()->input; + + // Save new group as unpublished + if ($input->get('task') == 'save2copy') { + $data['state'] = 0; + } + + return parent::save($data); + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 3.7.0 + * @throws \Exception + */ + public function getTable($name = 'Group', $prefix = 'Administrator', $options = array()) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Abstract method for getting the form from the model. + * + * @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 mixed A Form object on success, false on failure + * + * @since 3.7.0 + */ + public function getForm($data = array(), $loadData = true) + { + $context = $this->getState('filter.context'); + $jinput = Factory::getApplication()->input; + + if (empty($context) && isset($data['context'])) { + $context = $data['context']; + $this->setState('filter.context', $context); + } + + // Get the form. + $form = $this->loadForm( + 'com_fields.group.' . $context, + 'group', + array( + 'control' => 'jform', + 'load_data' => $loadData, + ) + ); + + if (empty($form)) { + return false; + } + + // Modify the form based on Edit State access controls. + if (empty($data['context'])) { + $data['context'] = $context; + } + + $user = Factory::getUser(); + + if (!$user->authorise('core.edit.state', $context . '.fieldgroup.' . $jinput->get('id'))) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('state', 'disabled', 'true'); + + // Disable fields while saving. The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('state', 'filter', 'unset'); + } + + // Don't allow to change the created_by user if not allowed to access com_users. + if (!$user->authorise('core.manage', 'com_users')) { + $form->setFieldAttribute('created_by', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission for the component. + * + * @since 3.7.0 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->state != -2) { + return false; + } + + return Factory::getUser()->authorise('core.delete', $record->context . '.fieldgroup.' . (int) $record->id); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission for the + * component. + * + * @since 3.7.0 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + + // Check for existing fieldgroup. + if (!empty($record->id)) { + return $user->authorise('core.edit.state', $record->context . '.fieldgroup.' . (int) $record->id); + } + + // Default to component settings. + return $user->authorise('core.edit.state', $record->context); + } + + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 3.7.0 + */ + protected function populateState() + { + parent::populateState(); + + $context = Factory::getApplication()->getUserStateFromRequest('com_fields.groups.context', 'context', 'com_fields', 'CMD'); + $this->setState('filter.context', $context); + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param Table $table A Table object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 3.7.0 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('context') . ' = ' . $db->quote($table->context), + ]; + } + + /** + * Method to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @see \Joomla\CMS\Form\FormField + * @since 3.7.0 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + parent::preprocessForm($form, $data, $group); + + $parts = FieldsHelper::extract($this->state->get('filter.context')); + + // Extract the component name + $component = $parts[0]; + + // Extract the optional section name + $section = (count($parts) > 1) ? $parts[1] : null; + + if ($parts) { + // Set the access control rules field component value. + $form->setFieldAttribute('rules', 'component', $component); + } + + if ($section !== null) { + // Looking first in the component models/forms folder + $path = Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component . '/models/forms/fieldgroup/' . $section . '.xml'); + + if (file_exists($path)) { + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_BASE); + $lang->load($component, JPATH_BASE . '/components/' . $component); + + if (!$form->loadFile($path, false)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + } + } + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @see JFormRule + * @see JFilterInput + * @since 3.9.23 + */ + public function validate($form, $data, $group = null) + { + if (!Factory::getUser()->authorise('core.admin', 'com_fields')) { + if (isset($data['rules'])) { + unset($data['rules']); + } + } + + return parent::validate($form, $data, $group); + } + + /** + * Method to get the data that should be injected in the form. + * + * @return array The default data is an empty array. + * + * @since 3.7.0 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $app = Factory::getApplication(); + $data = $app->getUserState('com_fields.edit.group.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Pre-select some filters (Status, Language, Access) in edit form if those have been selected in Field Group Manager + if (!$data->id) { + // Check for which context the Field Group Manager is used and get selected fields + $context = substr($app->getUserState('com_fields.groups.filter.context', ''), 4); + $filters = (array) $app->getUserState('com_fields.groups.' . $context . '.filter'); + + $data->set( + 'state', + $app->input->getInt('state', (!empty($filters['state']) ? $filters['state'] : null)) + ); + $data->set( + 'language', + $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null)) + ); + $data->set( + 'access', + $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) + ); + } + } + + $this->preprocessData('com_fields.group', $data); + + return $data; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 3.7.0 + */ + public function getItem($pk = null) + { + if ($item = parent::getItem($pk)) { + // Prime required properties. + if (empty($item->id)) { + $item->context = $this->getState('filter.context'); + } + + if (property_exists($item, 'params')) { + $item->params = new Registry($item->params); + } + } + + return $item; + } + + /** + * Clean the cache + * + * @param string $group The cache group + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 3.7.0 + */ + protected function cleanCache($group = null, $clientId = 0) + { + $context = Factory::getApplication()->input->get('context'); + + parent::cleanCache($context); + } } diff --git a/administrator/components/com_fields/src/Model/GroupsModel.php b/administrator/components/com_fields/src/Model/GroupsModel.php index 2669f0834171e..09b555d1d885d 100644 --- a/administrator/components/com_fields/src/Model/GroupsModel.php +++ b/administrator/components/com_fields/src/Model/GroupsModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest($this->context . '.context', 'context', 'com_content', 'CMD'); - $this->setState('filter.context', $context); - } - - /** - * Method to get a store id based on the model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id An identifier string to generate the store id. - * - * @return string A store id. - * - * @since 3.7.0 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.context'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . print_r($this->getState('filter.language'), true); - - return parent::getStoreId($id); - } - - /** - * Method to get a DatabaseQuery object for retrieving the data set from a database. - * - * @return \Joomla\Database\DatabaseQuery A DatabaseQuery object to retrieve the data set. - * - * @since 3.7.0 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - - // Select the required fields from the table. - $query->select($this->getState('list.select', 'a.*')); - $query->from('#__fields_groups AS a'); - - // Join over the language - $query->select('l.title AS language_title, l.image AS language_image') - ->join('LEFT', $db->quoteName('#__languages') . ' AS l ON l.lang_code = a.language'); - - // Join over the users for the checked out user. - $query->select('uc.name AS editor')->join('LEFT', '#__users AS uc ON uc.id=a.checked_out'); - - // Join over the asset groups. - $query->select('ag.title AS access_level')->join('LEFT', '#__viewlevels AS ag ON ag.id = a.access'); - - // Join over the users for the author. - $query->select('ua.name AS author_name')->join('LEFT', '#__users AS ua ON ua.id = a.created_by'); - - // Filter by context - if ($context = $this->getState('filter.context', 'com_fields')) - { - $query->where($db->quoteName('a.context') . ' = :context') - ->bind(':context', $context); - } - - // Filter by access level. - if ($access = $this->getState('filter.access')) - { - if (is_array($access)) - { - $access = ArrayHelper::toInteger($access); - $query->whereIn($db->quoteName('a.access'), $access); - } - else - { - $access = (int) $access; - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - } - - // Implement View Level Access - if (!$user->authorise('core.admin')) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('a.access'), $groups); - } - - // Filter by published state - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $state = (int) $state; - $query->where($db->quoteName('a.state') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - elseif (!$state) - { - $query->whereIn($db->quoteName('a.state'), [0, 1]); - } - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :search') - ->bind(':id', $search, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where($db->quoteName('a.title') . ' LIKE :search') - ->bind(':search', $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $language = (array) $language; - - $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); - } - - // Add the list ordering clause - $listOrdering = $this->getState('list.ordering', 'a.ordering'); - $listDirn = $db->escape($this->getState('list.direction', 'ASC')); - - $query->order($db->escape($listOrdering) . ' ' . $listDirn); - - return $query; - } - - /** - * Gets an array of objects from the results of database query. - * - * @param string $query The query. - * @param integer $limitstart Offset. - * @param integer $limit The number of records. - * - * @return array An array of results. - * - * @since 3.8.7 - * @throws \RuntimeException - */ - protected function _getList($query, $limitstart = 0, $limit = 0) - { - $result = parent::_getList($query, $limitstart, $limit); - - if (is_array($result)) - { - foreach ($result as $group) - { - $group->params = new Registry($group->params); - } - } - - return $result; - } + /** + * Context string for the model type. This is used to handle uniqueness + * when dealing with the getStoreId() method and caching data structures. + * + * @var string + * @since 3.7.0 + */ + protected $context = 'com_fields.groups'; + + /** + * Constructor + * + * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). + * @param MVCFactoryInterface $factory The factory. + * + * @since 3.7.0 + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'type', 'a.type', + 'state', 'a.state', + 'access', 'a.access', + 'access_level', + 'language', 'a.language', + 'ordering', 'a.ordering', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'created', 'a.created', + 'created_by', 'a.created_by', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.7.0 + */ + protected function populateState($ordering = null, $direction = null) + { + // List state information. + parent::populateState('a.ordering', 'asc'); + + $context = $this->getUserStateFromRequest($this->context . '.context', 'context', 'com_content', 'CMD'); + $this->setState('filter.context', $context); + } + + /** + * Method to get a store id based on the model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id An identifier string to generate the store id. + * + * @return string A store id. + * + * @since 3.7.0 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.context'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . print_r($this->getState('filter.language'), true); + + return parent::getStoreId($id); + } + + /** + * Method to get a DatabaseQuery object for retrieving the data set from a database. + * + * @return \Joomla\Database\DatabaseQuery A DatabaseQuery object to retrieve the data set. + * + * @since 3.7.0 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + + // Select the required fields from the table. + $query->select($this->getState('list.select', 'a.*')); + $query->from('#__fields_groups AS a'); + + // Join over the language + $query->select('l.title AS language_title, l.image AS language_image') + ->join('LEFT', $db->quoteName('#__languages') . ' AS l ON l.lang_code = a.language'); + + // Join over the users for the checked out user. + $query->select('uc.name AS editor')->join('LEFT', '#__users AS uc ON uc.id=a.checked_out'); + + // Join over the asset groups. + $query->select('ag.title AS access_level')->join('LEFT', '#__viewlevels AS ag ON ag.id = a.access'); + + // Join over the users for the author. + $query->select('ua.name AS author_name')->join('LEFT', '#__users AS ua ON ua.id = a.created_by'); + + // Filter by context + if ($context = $this->getState('filter.context', 'com_fields')) { + $query->where($db->quoteName('a.context') . ' = :context') + ->bind(':context', $context); + } + + // Filter by access level. + if ($access = $this->getState('filter.access')) { + if (is_array($access)) { + $access = ArrayHelper::toInteger($access); + $query->whereIn($db->quoteName('a.access'), $access); + } else { + $access = (int) $access; + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + } + + // Implement View Level Access + if (!$user->authorise('core.admin')) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('a.access'), $groups); + } + + // Filter by published state + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $state = (int) $state; + $query->where($db->quoteName('a.state') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } elseif (!$state) { + $query->whereIn($db->quoteName('a.state'), [0, 1]); + } + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :search') + ->bind(':id', $search, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where($db->quoteName('a.title') . ' LIKE :search') + ->bind(':search', $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $language = (array) $language; + + $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); + } + + // Add the list ordering clause + $listOrdering = $this->getState('list.ordering', 'a.ordering'); + $listDirn = $db->escape($this->getState('list.direction', 'ASC')); + + $query->order($db->escape($listOrdering) . ' ' . $listDirn); + + return $query; + } + + /** + * Gets an array of objects from the results of database query. + * + * @param string $query The query. + * @param integer $limitstart Offset. + * @param integer $limit The number of records. + * + * @return array An array of results. + * + * @since 3.8.7 + * @throws \RuntimeException + */ + protected function _getList($query, $limitstart = 0, $limit = 0) + { + $result = parent::_getList($query, $limitstart, $limit); + + if (is_array($result)) { + foreach ($result as $group) { + $group->params = new Registry($group->params); + } + } + + return $result; + } } diff --git a/administrator/components/com_fields/src/Plugin/FieldsListPlugin.php b/administrator/components/com_fields/src/Plugin/FieldsListPlugin.php index 4d112e36f9a76..5a67a785989ff 100644 --- a/administrator/components/com_fields/src/Plugin/FieldsListPlugin.php +++ b/administrator/components/com_fields/src/Plugin/FieldsListPlugin.php @@ -1,4 +1,5 @@ setAttribute('validate', 'options'); + $fieldNode->setAttribute('validate', 'options'); - foreach ($this->getOptionsFromField($field) as $value => $name) - { - $option = new \DOMElement('option', htmlspecialchars($value, ENT_COMPAT, 'UTF-8')); - $option->textContent = htmlspecialchars(Text::_($name), ENT_COMPAT, 'UTF-8'); + foreach ($this->getOptionsFromField($field) as $value => $name) { + $option = new \DOMElement('option', htmlspecialchars($value, ENT_COMPAT, 'UTF-8')); + $option->textContent = htmlspecialchars(Text::_($name), ENT_COMPAT, 'UTF-8'); - $element = $fieldNode->appendChild($option); - $element->setAttribute('value', $value); - } + $element = $fieldNode->appendChild($option); + $element->setAttribute('value', $value); + } - return $fieldNode; - } + return $fieldNode; + } - /** - * Returns an array of key values to put in a list from the given field. - * - * @param \stdClass $field The field. - * - * @return array - * - * @since 3.7.0 - */ - public function getOptionsFromField($field) - { - $data = array(); + /** + * Returns an array of key values to put in a list from the given field. + * + * @param \stdClass $field The field. + * + * @return array + * + * @since 3.7.0 + */ + public function getOptionsFromField($field) + { + $data = array(); - // Fetch the options from the plugin - $params = clone $this->params; - $params->merge($field->fieldparams); + // Fetch the options from the plugin + $params = clone $this->params; + $params->merge($field->fieldparams); - foreach ($params->get('options', array()) as $option) - { - $op = (object) $option; - $data[$op->value] = $op->name; - } + foreach ($params->get('options', array()) as $option) { + $op = (object) $option; + $data[$op->value] = $op->name; + } - return $data; - } + return $data; + } } diff --git a/administrator/components/com_fields/src/Plugin/FieldsPlugin.php b/administrator/components/com_fields/src/Plugin/FieldsPlugin.php index 5d2554d0f171e..5c827c2a38c99 100644 --- a/administrator/components/com_fields/src/Plugin/FieldsPlugin.php +++ b/administrator/components/com_fields/src/Plugin/FieldsPlugin.php @@ -1,4 +1,5 @@ _type . $this->_name])) - { - return $types_cache[$this->_type . $this->_name]; - } - - $types = array(); - - // The root of the plugin - $root = JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name; - - foreach (Folder::files($root . '/tmpl', '.php') as $layout) - { - // Strip the extension - $layout = str_replace('.php', '', $layout); - - // The data array - $data = array(); - - // The language key - $key = strtoupper($layout); - - if ($key != strtoupper($this->_name)) - { - $key = strtoupper($this->_name) . '_' . $layout; - } - - // Needed attributes - $data['type'] = $layout; - - if ($this->app->getLanguage()->hasKey('PLG_FIELDS_' . $key . '_LABEL')) - { - $data['label'] = Text::sprintf('PLG_FIELDS_' . $key . '_LABEL', strtolower($key)); - - // Fix wrongly set parentheses in RTL languages - if ($this->app->getLanguage()->isRtl()) - { - $data['label'] = $data['label'] . '‎'; - } - } - else - { - $data['label'] = $key; - } - - $path = $root . '/fields'; - - // Add the path when it exists - if (file_exists($path)) - { - $data['path'] = $path; - } - - $path = $root . '/rules'; - - // Add the path when it exists - if (file_exists($path)) - { - $data['rules'] = $path; - } - - $types[] = $data; - } - - // Add to cache and return the data - $types_cache[$this->_type . $this->_name] = $types; - - return $types; - } - - /** - * Prepares the field value. - * - * @param string $context The context. - * @param \stdclass $item The item. - * @param \stdclass $field The field. - * - * @return string - * - * @since 3.7.0 - */ - public function onCustomFieldsPrepareField($context, $item, $field) - { - // Check if the field should be processed by us - if (!$this->isTypeSupported($field->type)) - { - return; - } - - // Merge the params from the plugin and field which has precedence - $fieldParams = clone $this->params; - $fieldParams->merge($field->fieldparams); - - // Get the path for the layout file - $path = PluginHelper::getLayoutPath('fields', $this->_name, $field->type); - - // Render the layout - ob_start(); - include $path; - $output = ob_get_clean(); - - // Return the output - return $output; - } - - /** - * Transforms the field into a DOM XML element and appends it as a child on the given parent. - * - * @param \stdClass $field The field. - * @param \DOMElement $parent The field node parent. - * @param Form $form The form. - * - * @return \DOMElement - * - * @since 3.7.0 - */ - public function onCustomFieldsPrepareDom($field, \DOMElement $parent, Form $form) - { - // Check if the field should be processed by us - if (!$this->isTypeSupported($field->type)) - { - return null; - } - - // Detect if the field is configured to be displayed on the form - if (!FieldsHelper::displayFieldOnForm($field)) - { - return null; - } - - // Create the node - $node = $parent->appendChild(new \DOMElement('field')); - - // Set the attributes - $node->setAttribute('name', $field->name); - $node->setAttribute('type', $field->type); - $node->setAttribute('label', $field->label); - $node->setAttribute('labelclass', $field->params->get('label_class', '')); - $node->setAttribute('description', $field->description); - $node->setAttribute('class', $field->params->get('class', '')); - $node->setAttribute('hint', $field->params->get('hint', '')); - $node->setAttribute('required', $field->required ? 'true' : 'false'); - - if ($layout = $field->params->get('form_layout')) - { - $node->setAttribute('layout', $layout); - } - - if ($field->default_value !== '') - { - $defaultNode = $node->appendChild(new \DOMElement('default')); - $defaultNode->appendChild(new \DOMCdataSection($field->default_value)); - } - - // Combine the two params - $params = clone $this->params; - $params->merge($field->fieldparams); - - // Set the specific field parameters - foreach ($params->toArray() as $key => $param) - { - if (is_array($param)) - { - // Multidimensional arrays (eg. list options) can't be transformed properly - $param = count($param) == count($param, COUNT_RECURSIVE) ? implode(',', $param) : ''; - } - - if ($param === '' || (!is_string($param) && !is_numeric($param))) - { - continue; - } - - $node->setAttribute($key, $param); - } - - // Check if it is allowed to edit the field - if (!FieldsHelper::canEditFieldValue($field)) - { - $node->setAttribute('disabled', 'true'); - } - - // Return the node - return $node; - } - - /** - * The form event. Load additional parameters when available into the field form. - * Only when the type of the form is of interest. - * - * @param Form $form The form - * @param \stdClass $data The data - * - * @return void - * - * @since 3.7.0 - */ - public function onContentPrepareForm(Form $form, $data) - { - $path = $this->getFormPath($form, $data); - - if ($path === null) - { - return; - } - - // Load the specific plugin parameters - $form->load(file_get_contents($path), true, '/form/*'); - } - - /** - * Returns the path of the XML definition file for the field parameters - * - * @param Form $form The form - * @param \stdClass $data The data - * - * @return string - * - * @since 4.0.0 - */ - protected function getFormPath(Form $form, $data) - { - // Check if the field form is calling us - if (strpos($form->getName(), 'com_fields.field') !== 0) - { - return null; - } - - // Ensure it is an object - $formData = (object) $data; - - // Gather the type - $type = $form->getValue('type'); - - if (!empty($formData->type)) - { - $type = $formData->type; - } - - // Not us - if (!$this->isTypeSupported($type)) - { - return null; - } - - $path = JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name . '/params/' . $type . '.xml'; - - // Check if params file exists - if (!file_exists($path)) - { - return null; - } - - return $path; - } - - /** - * Returns true if the given type is supported by the plugin. - * - * @param string $type The type - * - * @return boolean - * - * @since 3.7.0 - */ - protected function isTypeSupported($type) - { - foreach ($this->onCustomFieldsGetTypes() as $typeSpecification) - { - if ($type == $typeSpecification['type']) - { - return true; - } - } - - return false; - } + /** + * Affects constructor behavior. If true, language files will be loaded automatically. + * + * @var boolean + * @since 3.7.0 + */ + protected $autoloadLanguage = true; + + /** + * Application object. + * + * @var \Joomla\CMS\Application\CMSApplication + * @since 4.0.0 + */ + protected $app; + + /** + * Returns the custom fields types. + * + * @return string[][] + * + * @since 3.7.0 + */ + public function onCustomFieldsGetTypes() + { + // Cache filesystem access / checks + static $types_cache = array(); + + if (isset($types_cache[$this->_type . $this->_name])) { + return $types_cache[$this->_type . $this->_name]; + } + + $types = array(); + + // The root of the plugin + $root = JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name; + + foreach (Folder::files($root . '/tmpl', '.php') as $layout) { + // Strip the extension + $layout = str_replace('.php', '', $layout); + + // The data array + $data = array(); + + // The language key + $key = strtoupper($layout); + + if ($key != strtoupper($this->_name)) { + $key = strtoupper($this->_name) . '_' . $layout; + } + + // Needed attributes + $data['type'] = $layout; + + if ($this->app->getLanguage()->hasKey('PLG_FIELDS_' . $key . '_LABEL')) { + $data['label'] = Text::sprintf('PLG_FIELDS_' . $key . '_LABEL', strtolower($key)); + + // Fix wrongly set parentheses in RTL languages + if ($this->app->getLanguage()->isRtl()) { + $data['label'] = $data['label'] . '‎'; + } + } else { + $data['label'] = $key; + } + + $path = $root . '/fields'; + + // Add the path when it exists + if (file_exists($path)) { + $data['path'] = $path; + } + + $path = $root . '/rules'; + + // Add the path when it exists + if (file_exists($path)) { + $data['rules'] = $path; + } + + $types[] = $data; + } + + // Add to cache and return the data + $types_cache[$this->_type . $this->_name] = $types; + + return $types; + } + + /** + * Prepares the field value. + * + * @param string $context The context. + * @param \stdclass $item The item. + * @param \stdclass $field The field. + * + * @return string + * + * @since 3.7.0 + */ + public function onCustomFieldsPrepareField($context, $item, $field) + { + // Check if the field should be processed by us + if (!$this->isTypeSupported($field->type)) { + return; + } + + // Merge the params from the plugin and field which has precedence + $fieldParams = clone $this->params; + $fieldParams->merge($field->fieldparams); + + // Get the path for the layout file + $path = PluginHelper::getLayoutPath('fields', $this->_name, $field->type); + + // Render the layout + ob_start(); + include $path; + $output = ob_get_clean(); + + // Return the output + return $output; + } + + /** + * Transforms the field into a DOM XML element and appends it as a child on the given parent. + * + * @param \stdClass $field The field. + * @param \DOMElement $parent The field node parent. + * @param Form $form The form. + * + * @return \DOMElement + * + * @since 3.7.0 + */ + public function onCustomFieldsPrepareDom($field, \DOMElement $parent, Form $form) + { + // Check if the field should be processed by us + if (!$this->isTypeSupported($field->type)) { + return null; + } + + // Detect if the field is configured to be displayed on the form + if (!FieldsHelper::displayFieldOnForm($field)) { + return null; + } + + // Create the node + $node = $parent->appendChild(new \DOMElement('field')); + + // Set the attributes + $node->setAttribute('name', $field->name); + $node->setAttribute('type', $field->type); + $node->setAttribute('label', $field->label); + $node->setAttribute('labelclass', $field->params->get('label_class', '')); + $node->setAttribute('description', $field->description); + $node->setAttribute('class', $field->params->get('class', '')); + $node->setAttribute('hint', $field->params->get('hint', '')); + $node->setAttribute('required', $field->required ? 'true' : 'false'); + + if ($layout = $field->params->get('form_layout')) { + $node->setAttribute('layout', $layout); + } + + if ($field->default_value !== '') { + $defaultNode = $node->appendChild(new \DOMElement('default')); + $defaultNode->appendChild(new \DOMCdataSection($field->default_value)); + } + + // Combine the two params + $params = clone $this->params; + $params->merge($field->fieldparams); + + // Set the specific field parameters + foreach ($params->toArray() as $key => $param) { + if (is_array($param)) { + // Multidimensional arrays (eg. list options) can't be transformed properly + $param = count($param) == count($param, COUNT_RECURSIVE) ? implode(',', $param) : ''; + } + + if ($param === '' || (!is_string($param) && !is_numeric($param))) { + continue; + } + + $node->setAttribute($key, $param); + } + + // Check if it is allowed to edit the field + if (!FieldsHelper::canEditFieldValue($field)) { + $node->setAttribute('disabled', 'true'); + } + + // Return the node + return $node; + } + + /** + * The form event. Load additional parameters when available into the field form. + * Only when the type of the form is of interest. + * + * @param Form $form The form + * @param \stdClass $data The data + * + * @return void + * + * @since 3.7.0 + */ + public function onContentPrepareForm(Form $form, $data) + { + $path = $this->getFormPath($form, $data); + + if ($path === null) { + return; + } + + // Load the specific plugin parameters + $form->load(file_get_contents($path), true, '/form/*'); + } + + /** + * Returns the path of the XML definition file for the field parameters + * + * @param Form $form The form + * @param \stdClass $data The data + * + * @return string + * + * @since 4.0.0 + */ + protected function getFormPath(Form $form, $data) + { + // Check if the field form is calling us + if (strpos($form->getName(), 'com_fields.field') !== 0) { + return null; + } + + // Ensure it is an object + $formData = (object) $data; + + // Gather the type + $type = $form->getValue('type'); + + if (!empty($formData->type)) { + $type = $formData->type; + } + + // Not us + if (!$this->isTypeSupported($type)) { + return null; + } + + $path = JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name . '/params/' . $type . '.xml'; + + // Check if params file exists + if (!file_exists($path)) { + return null; + } + + return $path; + } + + /** + * Returns true if the given type is supported by the plugin. + * + * @param string $type The type + * + * @return boolean + * + * @since 3.7.0 + */ + protected function isTypeSupported($type) + { + foreach ($this->onCustomFieldsGetTypes() as $typeSpecification) { + if ($type == $typeSpecification['type']) { + return true; + } + } + + return false; + } } diff --git a/administrator/components/com_fields/src/Table/FieldTable.php b/administrator/components/com_fields/src/Table/FieldTable.php index 20953310ebe4c..9995e4c7a64d6 100644 --- a/administrator/components/com_fields/src/Table/FieldTable.php +++ b/administrator/components/com_fields/src/Table/FieldTable.php @@ -1,4 +1,5 @@ setColumnAlias('published', 'state'); - } - - /** - * Method to bind an associative array or object to the JTable instance.This - * method only binds properties that are publicly accessible and optionally - * takes an array of properties to ignore when binding. - * - * @param mixed $src An associative array or object to bind to the JTable instance. - * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. - * - * @return boolean True on success. - * - * @since 3.7.0 - * @throws \InvalidArgumentException - */ - public function bind($src, $ignore = '') - { - if (isset($src['params']) && is_array($src['params'])) - { - $registry = new Registry; - $registry->loadArray($src['params']); - $src['params'] = (string) $registry; - } - - if (isset($src['fieldparams']) && is_array($src['fieldparams'])) - { - // Make sure $registry->options contains no duplicates when the field type is subform - if (isset($src['type']) && $src['type'] == 'subform' && isset($src['fieldparams']['options'])) - { - // Fast lookup map to check which custom field ids we have already seen - $seen_customfields = array(); - - // Container for the new $src['fieldparams']['options'] - $options = array(); - - // Iterate through the old options - $i = 0; - - foreach ($src['fieldparams']['options'] as $option) - { - // Check whether we have not yet seen this custom field id - if (!isset($seen_customfields[$option['customfield']])) - { - // We haven't, so add it to the final options - $seen_customfields[$option['customfield']] = true; - $options['option' . $i] = $option; - $i++; - } - } - - // And replace the options with the deduplicated ones. - $src['fieldparams']['options'] = $options; - } - - $registry = new Registry; - $registry->loadArray($src['fieldparams']); - $src['fieldparams'] = (string) $registry; - } - - // Bind the rules. - if (isset($src['rules']) && is_array($src['rules'])) - { - $rules = new Rules($src['rules']); - $this->setRules($rules); - } - - return parent::bind($src, $ignore); - } - - /** - * Method to perform sanity checks on the JTable instance properties to ensure - * they are safe to store in the database. Child classes should override this - * method to make sure the data they are storing in the database is safe and - * as expected before storage. - * - * @return boolean True if the instance is sane and able to be stored in the database. - * - * @link https://docs.joomla.org/Special:MyLanguage/JTable/check - * @since 3.7.0 - */ - public function check() - { - // Check for valid name - if (trim($this->title) == '') - { - $this->setError(Text::_('COM_FIELDS_MUSTCONTAIN_A_TITLE_FIELD')); - - return false; - } - - if (empty($this->name)) - { - $this->name = $this->title; - } - - $this->name = ApplicationHelper::stringURLSafe($this->name, $this->language); - - if (trim(str_replace('-', '', $this->name)) == '') - { - $this->name = StringHelper::increment($this->name, 'dash'); - } - - $this->name = str_replace(',', '-', $this->name); - - // Verify that the name is unique - $table = new static($this->_db); - - if ($table->load(array('name' => $this->name)) && ($table->id != $this->id || $this->id == 0)) - { - $this->setError(Text::_('COM_FIELDS_ERROR_UNIQUE_NAME')); - - return false; - } - - $this->name = str_replace(',', '-', $this->name); - - if (empty($this->type)) - { - $this->type = 'text'; - } - - if (empty($this->fieldparams)) - { - $this->fieldparams = '{}'; - } - - $date = Factory::getDate()->toSql(); - $user = Factory::getUser(); - - // Set created date if not set. - if (!(int) $this->created_time) - { - $this->created_time = $date; - } - - if ($this->id) - { - // Existing item - $this->modified_time = $date; - $this->modified_by = $user->get('id'); - } - else - { - if (!(int) $this->modified_time) - { - $this->modified_time = $this->created_time; - } - - if (empty($this->created_user_id)) - { - $this->created_user_id = $user->get('id'); - } - - if (empty($this->modified_by)) - { - $this->modified_by = $this->created_user_id; - } - } - - if (empty($this->group_id)) - { - $this->group_id = 0; - } - - return true; - } - - /** - * Overloaded store function - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return mixed False on failure, positive integer on success. - * - * @see Table::store() - * @since 4.0.0 - */ - public function store($updateNulls = true) - { - return parent::store($updateNulls); - } - - /** - * Method to compute the default name of the asset. - * The default name is in the form table_name.id - * where id is the value of the primary key of the table. - * - * @return string - * - * @since 3.7.0 - */ - protected function _getAssetName() - { - $contextArray = explode('.', $this->context); - - return $contextArray[0] . '.field.' . (int) $this->id; - } - - /** - * Method to return the title to use for the asset table. In - * tracking the assets a title is kept for each asset so that there is some - * context available in a unified access manager. Usually this would just - * return $this->title or $this->name or whatever is being used for the - * primary name of the row. If this method is not overridden, the asset name is used. - * - * @return string The string to use as the title in the asset table. - * - * @link https://docs.joomla.org/Special:MyLanguage/JTable/getAssetTitle - * @since 3.7.0 - */ - protected function _getAssetTitle() - { - return $this->title; - } - - /** - * Method to get the parent asset under which to register this one. - * By default, all assets are registered to the ROOT node with ID, - * which will default to 1 if none exists. - * The extended class can define a table and id to lookup. If the - * asset does not exist it will be created. - * - * @param Table $table A Table object for the asset parent. - * @param integer $id Id to look up - * - * @return integer - * - * @since 3.7.0 - */ - protected function _getAssetParentId(Table $table = null, $id = null) - { - $contextArray = explode('.', $this->context); - $component = $contextArray[0]; - - if ($this->group_id) - { - $assetId = $this->getAssetId($component . '.fieldgroup.' . (int) $this->group_id); - - if ($assetId) - { - return $assetId; - } - } - else - { - $assetId = $this->getAssetId($component); - - if ($assetId) - { - return $assetId; - } - } - - return parent::_getAssetParentId($table, $id); - } - - /** - * Returns an asset id for the given name or false. - * - * @param string $name The asset name - * - * @return number|boolean - * - * @since 3.7.0 - */ - private function getAssetId($name) - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__assets')) - ->where($db->quoteName('name') . ' = :name') - ->bind(':name', $name); - - // Get the asset id from the database. - $db->setQuery($query); - - $assetId = null; - - if ($result = $db->loadResult()) - { - $assetId = (int) $result; - - if ($assetId) - { - return $assetId; - } - } - - return false; - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Class constructor. + * + * @param DatabaseDriver $db DatabaseDriver object. + * + * @since 3.7.0 + */ + public function __construct($db = null) + { + parent::__construct('#__fields', 'id', $db); + + $this->setColumnAlias('published', 'state'); + } + + /** + * Method to bind an associative array or object to the JTable instance.This + * method only binds properties that are publicly accessible and optionally + * takes an array of properties to ignore when binding. + * + * @param mixed $src An associative array or object to bind to the JTable instance. + * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return boolean True on success. + * + * @since 3.7.0 + * @throws \InvalidArgumentException + */ + public function bind($src, $ignore = '') + { + if (isset($src['params']) && is_array($src['params'])) { + $registry = new Registry(); + $registry->loadArray($src['params']); + $src['params'] = (string) $registry; + } + + if (isset($src['fieldparams']) && is_array($src['fieldparams'])) { + // Make sure $registry->options contains no duplicates when the field type is subform + if (isset($src['type']) && $src['type'] == 'subform' && isset($src['fieldparams']['options'])) { + // Fast lookup map to check which custom field ids we have already seen + $seen_customfields = array(); + + // Container for the new $src['fieldparams']['options'] + $options = array(); + + // Iterate through the old options + $i = 0; + + foreach ($src['fieldparams']['options'] as $option) { + // Check whether we have not yet seen this custom field id + if (!isset($seen_customfields[$option['customfield']])) { + // We haven't, so add it to the final options + $seen_customfields[$option['customfield']] = true; + $options['option' . $i] = $option; + $i++; + } + } + + // And replace the options with the deduplicated ones. + $src['fieldparams']['options'] = $options; + } + + $registry = new Registry(); + $registry->loadArray($src['fieldparams']); + $src['fieldparams'] = (string) $registry; + } + + // Bind the rules. + if (isset($src['rules']) && is_array($src['rules'])) { + $rules = new Rules($src['rules']); + $this->setRules($rules); + } + + return parent::bind($src, $ignore); + } + + /** + * Method to perform sanity checks on the JTable instance properties to ensure + * they are safe to store in the database. Child classes should override this + * method to make sure the data they are storing in the database is safe and + * as expected before storage. + * + * @return boolean True if the instance is sane and able to be stored in the database. + * + * @link https://docs.joomla.org/Special:MyLanguage/JTable/check + * @since 3.7.0 + */ + public function check() + { + // Check for valid name + if (trim($this->title) == '') { + $this->setError(Text::_('COM_FIELDS_MUSTCONTAIN_A_TITLE_FIELD')); + + return false; + } + + if (empty($this->name)) { + $this->name = $this->title; + } + + $this->name = ApplicationHelper::stringURLSafe($this->name, $this->language); + + if (trim(str_replace('-', '', $this->name)) == '') { + $this->name = StringHelper::increment($this->name, 'dash'); + } + + $this->name = str_replace(',', '-', $this->name); + + // Verify that the name is unique + $table = new static($this->_db); + + if ($table->load(array('name' => $this->name)) && ($table->id != $this->id || $this->id == 0)) { + $this->setError(Text::_('COM_FIELDS_ERROR_UNIQUE_NAME')); + + return false; + } + + $this->name = str_replace(',', '-', $this->name); + + if (empty($this->type)) { + $this->type = 'text'; + } + + if (empty($this->fieldparams)) { + $this->fieldparams = '{}'; + } + + $date = Factory::getDate()->toSql(); + $user = Factory::getUser(); + + // Set created date if not set. + if (!(int) $this->created_time) { + $this->created_time = $date; + } + + if ($this->id) { + // Existing item + $this->modified_time = $date; + $this->modified_by = $user->get('id'); + } else { + if (!(int) $this->modified_time) { + $this->modified_time = $this->created_time; + } + + if (empty($this->created_user_id)) { + $this->created_user_id = $user->get('id'); + } + + if (empty($this->modified_by)) { + $this->modified_by = $this->created_user_id; + } + } + + if (empty($this->group_id)) { + $this->group_id = 0; + } + + return true; + } + + /** + * Overloaded store function + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return mixed False on failure, positive integer on success. + * + * @see Table::store() + * @since 4.0.0 + */ + public function store($updateNulls = true) + { + return parent::store($updateNulls); + } + + /** + * Method to compute the default name of the asset. + * The default name is in the form table_name.id + * where id is the value of the primary key of the table. + * + * @return string + * + * @since 3.7.0 + */ + protected function _getAssetName() + { + $contextArray = explode('.', $this->context); + + return $contextArray[0] . '.field.' . (int) $this->id; + } + + /** + * Method to return the title to use for the asset table. In + * tracking the assets a title is kept for each asset so that there is some + * context available in a unified access manager. Usually this would just + * return $this->title or $this->name or whatever is being used for the + * primary name of the row. If this method is not overridden, the asset name is used. + * + * @return string The string to use as the title in the asset table. + * + * @link https://docs.joomla.org/Special:MyLanguage/JTable/getAssetTitle + * @since 3.7.0 + */ + protected function _getAssetTitle() + { + return $this->title; + } + + /** + * Method to get the parent asset under which to register this one. + * By default, all assets are registered to the ROOT node with ID, + * which will default to 1 if none exists. + * The extended class can define a table and id to lookup. If the + * asset does not exist it will be created. + * + * @param Table $table A Table object for the asset parent. + * @param integer $id Id to look up + * + * @return integer + * + * @since 3.7.0 + */ + protected function _getAssetParentId(Table $table = null, $id = null) + { + $contextArray = explode('.', $this->context); + $component = $contextArray[0]; + + if ($this->group_id) { + $assetId = $this->getAssetId($component . '.fieldgroup.' . (int) $this->group_id); + + if ($assetId) { + return $assetId; + } + } else { + $assetId = $this->getAssetId($component); + + if ($assetId) { + return $assetId; + } + } + + return parent::_getAssetParentId($table, $id); + } + + /** + * Returns an asset id for the given name or false. + * + * @param string $name The asset name + * + * @return number|boolean + * + * @since 3.7.0 + */ + private function getAssetId($name) + { + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__assets')) + ->where($db->quoteName('name') . ' = :name') + ->bind(':name', $name); + + // Get the asset id from the database. + $db->setQuery($query); + + $assetId = null; + + if ($result = $db->loadResult()) { + $assetId = (int) $result; + + if ($assetId) { + return $assetId; + } + } + + return false; + } } diff --git a/administrator/components/com_fields/src/Table/GroupTable.php b/administrator/components/com_fields/src/Table/GroupTable.php index 474572238e4b6..921cafa8895c3 100644 --- a/administrator/components/com_fields/src/Table/GroupTable.php +++ b/administrator/components/com_fields/src/Table/GroupTable.php @@ -1,4 +1,5 @@ setColumnAlias('published', 'state'); - } - - /** - * Method to bind an associative array or object to the JTable instance.This - * method only binds properties that are publicly accessible and optionally - * takes an array of properties to ignore when binding. - * - * @param mixed $src An associative array or object to bind to the JTable instance. - * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. - * - * @return boolean True on success. - * - * @since 3.7.0 - * @throws \InvalidArgumentException - */ - public function bind($src, $ignore = '') - { - if (isset($src['params']) && is_array($src['params'])) - { - $registry = new Registry; - $registry->loadArray($src['params']); - $src['params'] = (string) $registry; - } - - // Bind the rules. - if (isset($src['rules']) && is_array($src['rules'])) - { - $rules = new Rules($src['rules']); - $this->setRules($rules); - } - - return parent::bind($src, $ignore); - } - - /** - * Method to perform sanity checks on the JTable instance properties to ensure - * they are safe to store in the database. Child classes should override this - * method to make sure the data they are storing in the database is safe and - * as expected before storage. - * - * @return boolean True if the instance is sane and able to be stored in the database. - * - * @link https://docs.joomla.org/Special:MyLanguage/JTable/check - * @since 3.7.0 - */ - public function check() - { - // Check for a title. - if (trim($this->title) == '') - { - $this->setError(Text::_('COM_FIELDS_MUSTCONTAIN_A_TITLE_GROUP')); - - return false; - } - - $date = Factory::getDate()->toSql(); - $user = Factory::getUser(); - - // Set created date if not set. - if (!(int) $this->created) - { - $this->created = $date; - } - - if ($this->id) - { - $this->modified = $date; - $this->modified_by = $user->get('id'); - } - else - { - if (!(int) $this->modified) - { - $this->modified = $this->created; - } - - if (empty($this->created_by)) - { - $this->created_by = $user->get('id'); - } - - if (empty($this->modified_by)) - { - $this->modified_by = $this->created_by; - } - } - - if ($this->params === null) - { - $this->params = '{}'; - } - - return true; - } - - /** - * Overloaded store function - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return mixed False on failure, positive integer on success. - * - * @see Table::store() - * @since 4.0.0 - */ - public function store($updateNulls = true) - { - return parent::store($updateNulls); - } - - /** - * Method to compute the default name of the asset. - * The default name is in the form table_name.id - * where id is the value of the primary key of the table. - * - * @return string - * - * @since 3.7.0 - */ - protected function _getAssetName() - { - $component = explode('.', $this->context); - - return $component[0] . '.fieldgroup.' . (int) $this->id; - } - - /** - * Method to return the title to use for the asset table. In - * tracking the assets a title is kept for each asset so that there is some - * context available in a unified access manager. Usually this would just - * return $this->title or $this->name or whatever is being used for the - * primary name of the row. If this method is not overridden, the asset name is used. - * - * @return string The string to use as the title in the asset table. - * - * @link https://docs.joomla.org/Special:MyLanguage/JTable/getAssetTitle - * @since 3.7.0 - */ - protected function _getAssetTitle() - { - return $this->title; - } - - /** - * Method to get the parent asset under which to register this one. - * By default, all assets are registered to the ROOT node with ID, - * which will default to 1 if none exists. - * The extended class can define a table and id to lookup. If the - * asset does not exist it will be created. - * - * @param Table $table A Table object for the asset parent. - * @param integer $id Id to look up - * - * @return integer - * - * @since 3.7.0 - */ - protected function _getAssetParentId(Table $table = null, $id = null) - { - $component = explode('.', $this->context); - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__assets')) - ->where($db->quoteName('name') . ' = :name') - ->bind(':name', $component[0]); - $db->setQuery($query); - - if ($assetId = (int) $db->loadResult()) - { - return $assetId; - } - - return parent::_getAssetParentId($table, $id); - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Class constructor. + * + * @param DatabaseDriver $db DatabaseDriver object. + * + * @since 3.7.0 + */ + public function __construct($db = null) + { + parent::__construct('#__fields_groups', 'id', $db); + + $this->setColumnAlias('published', 'state'); + } + + /** + * Method to bind an associative array or object to the JTable instance.This + * method only binds properties that are publicly accessible and optionally + * takes an array of properties to ignore when binding. + * + * @param mixed $src An associative array or object to bind to the JTable instance. + * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return boolean True on success. + * + * @since 3.7.0 + * @throws \InvalidArgumentException + */ + public function bind($src, $ignore = '') + { + if (isset($src['params']) && is_array($src['params'])) { + $registry = new Registry(); + $registry->loadArray($src['params']); + $src['params'] = (string) $registry; + } + + // Bind the rules. + if (isset($src['rules']) && is_array($src['rules'])) { + $rules = new Rules($src['rules']); + $this->setRules($rules); + } + + return parent::bind($src, $ignore); + } + + /** + * Method to perform sanity checks on the JTable instance properties to ensure + * they are safe to store in the database. Child classes should override this + * method to make sure the data they are storing in the database is safe and + * as expected before storage. + * + * @return boolean True if the instance is sane and able to be stored in the database. + * + * @link https://docs.joomla.org/Special:MyLanguage/JTable/check + * @since 3.7.0 + */ + public function check() + { + // Check for a title. + if (trim($this->title) == '') { + $this->setError(Text::_('COM_FIELDS_MUSTCONTAIN_A_TITLE_GROUP')); + + return false; + } + + $date = Factory::getDate()->toSql(); + $user = Factory::getUser(); + + // Set created date if not set. + if (!(int) $this->created) { + $this->created = $date; + } + + if ($this->id) { + $this->modified = $date; + $this->modified_by = $user->get('id'); + } else { + if (!(int) $this->modified) { + $this->modified = $this->created; + } + + if (empty($this->created_by)) { + $this->created_by = $user->get('id'); + } + + if (empty($this->modified_by)) { + $this->modified_by = $this->created_by; + } + } + + if ($this->params === null) { + $this->params = '{}'; + } + + return true; + } + + /** + * Overloaded store function + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return mixed False on failure, positive integer on success. + * + * @see Table::store() + * @since 4.0.0 + */ + public function store($updateNulls = true) + { + return parent::store($updateNulls); + } + + /** + * Method to compute the default name of the asset. + * The default name is in the form table_name.id + * where id is the value of the primary key of the table. + * + * @return string + * + * @since 3.7.0 + */ + protected function _getAssetName() + { + $component = explode('.', $this->context); + + return $component[0] . '.fieldgroup.' . (int) $this->id; + } + + /** + * Method to return the title to use for the asset table. In + * tracking the assets a title is kept for each asset so that there is some + * context available in a unified access manager. Usually this would just + * return $this->title or $this->name or whatever is being used for the + * primary name of the row. If this method is not overridden, the asset name is used. + * + * @return string The string to use as the title in the asset table. + * + * @link https://docs.joomla.org/Special:MyLanguage/JTable/getAssetTitle + * @since 3.7.0 + */ + protected function _getAssetTitle() + { + return $this->title; + } + + /** + * Method to get the parent asset under which to register this one. + * By default, all assets are registered to the ROOT node with ID, + * which will default to 1 if none exists. + * The extended class can define a table and id to lookup. If the + * asset does not exist it will be created. + * + * @param Table $table A Table object for the asset parent. + * @param integer $id Id to look up + * + * @return integer + * + * @since 3.7.0 + */ + protected function _getAssetParentId(Table $table = null, $id = null) + { + $component = explode('.', $this->context); + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__assets')) + ->where($db->quoteName('name') . ' = :name') + ->bind(':name', $component[0]); + $db->setQuery($query); + + if ($assetId = (int) $db->loadResult()) { + return $assetId; + } + + return parent::_getAssetParentId($table, $id); + } } diff --git a/administrator/components/com_fields/src/View/Field/HtmlView.php b/administrator/components/com_fields/src/View/Field/HtmlView.php index 0b8e8a209cb23..0e93d13160fbb 100644 --- a/administrator/components/com_fields/src/View/Field/HtmlView.php +++ b/administrator/components/com_fields/src/View/Field/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - $this->canDo = ContentHelper::getActions($this->state->get('field.component'), 'field', $this->item->id); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - Factory::getApplication()->input->set('hidemainmenu', true); - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Adds the toolbar. - * - * @return void - * - * @since 3.7.0 - */ - protected function addToolbar() - { - $component = $this->state->get('field.component'); - $section = $this->state->get('field.section'); - $userId = $this->getCurrentUser()->get('id'); - $canDo = $this->canDo; - - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); - - // Avoid nonsense situation. - if ($component == 'com_fields') - { - return; - } - - // Load component language file - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_ADMINISTRATOR) - || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); - - $title = Text::sprintf('COM_FIELDS_VIEW_FIELD_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE', Text::_(strtoupper($component))); - - // Prepare the toolbar. - ToolbarHelper::title( - $title, - 'puzzle field-' . ($isNew ? 'add' : 'edit') . ' ' . substr($component, 4) . ($section ? "-$section" : '') . '-field-' . - ($isNew ? 'add' : 'edit') - ); - - // For new records, check the create permission. - if ($isNew) - { - ToolbarHelper::apply('field.apply'); - - ToolbarHelper::saveGroup( - [ - ['save', 'field.save'], - ['save2new', 'field.save2new'] - ], - 'btn-success' - ); - - ToolbarHelper::cancel('field.cancel'); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); - - $toolbarButtons = []; - - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable) - { - ToolbarHelper::apply('field.apply'); - - $toolbarButtons[] = ['save', 'field.save']; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'field.save2new']; - } - } - - // If an existing item, can save to a copy. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'field.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel('field.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::help('Component:_New_or_Edit_Field'); - } + /** + * @var \Joomla\CMS\Form\Form + * + * @since 3.7.0 + */ + protected $form; + + /** + * @var CMSObject + * + * @since 3.7.0 + */ + protected $item; + + /** + * @var CMSObject + * + * @since 3.7.0 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see HtmlView::loadTemplate() + * + * @since 3.7.0 + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + $this->canDo = ContentHelper::getActions($this->state->get('field.component'), 'field', $this->item->id); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + Factory::getApplication()->input->set('hidemainmenu', true); + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Adds the toolbar. + * + * @return void + * + * @since 3.7.0 + */ + protected function addToolbar() + { + $component = $this->state->get('field.component'); + $section = $this->state->get('field.section'); + $userId = $this->getCurrentUser()->get('id'); + $canDo = $this->canDo; + + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); + + // Avoid nonsense situation. + if ($component == 'com_fields') { + return; + } + + // Load component language file + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_ADMINISTRATOR) + || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); + + $title = Text::sprintf('COM_FIELDS_VIEW_FIELD_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE', Text::_(strtoupper($component))); + + // Prepare the toolbar. + ToolbarHelper::title( + $title, + 'puzzle field-' . ($isNew ? 'add' : 'edit') . ' ' . substr($component, 4) . ($section ? "-$section" : '') . '-field-' . + ($isNew ? 'add' : 'edit') + ); + + // For new records, check the create permission. + if ($isNew) { + ToolbarHelper::apply('field.apply'); + + ToolbarHelper::saveGroup( + [ + ['save', 'field.save'], + ['save2new', 'field.save2new'] + ], + 'btn-success' + ); + + ToolbarHelper::cancel('field.cancel'); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); + + $toolbarButtons = []; + + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable) { + ToolbarHelper::apply('field.apply'); + + $toolbarButtons[] = ['save', 'field.save']; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'field.save2new']; + } + } + + // If an existing item, can save to a copy. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'field.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel('field.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::help('Component:_New_or_Edit_Field'); + } } diff --git a/administrator/components/com_fields/src/View/Fields/HtmlView.php b/administrator/components/com_fields/src/View/Fields/HtmlView.php index e7bb2fdf8d170..1cd02e8e94bc9 100644 --- a/administrator/components/com_fields/src/View/Fields/HtmlView.php +++ b/administrator/components/com_fields/src/View/Fields/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Display a warning if the fields system plugin is disabled - if (!PluginHelper::isEnabled('system', 'fields')) - { - $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . FieldsHelper::getFieldsPluginId()); - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_FIELDS_SYSTEM_PLUGIN_NOT_ENABLED', $link), 'warning'); - } - - // Only add toolbar when not in modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - - parent::display($tpl); - } - - /** - * Adds the toolbar. - * - * @return void - * - * @since 3.7.0 - */ - protected function addToolbar() - { - $fieldId = $this->state->get('filter.field_id'); - $component = $this->state->get('filter.component'); - $section = $this->state->get('filter.section'); - $canDo = ContentHelper::getActions($component, 'field', $fieldId); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - // Avoid nonsense situation. - if ($component == 'com_fields') - { - return; - } - - // Load extension language file - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_ADMINISTRATOR) - || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); - - $title = Text::sprintf('COM_FIELDS_VIEW_FIELDS_TITLE', Text::_(strtoupper($component))); - - // Prepare the toolbar. - ToolbarHelper::title($title, 'puzzle-piece fields ' . substr($component, 4) . ($section ? "-$section" : '') . '-fields'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('field.add'); - } - - if ($canDo->get('core.edit.state') || $this->getCurrentUser()->authorise('core.admin')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('fields.publish')->listCheck(true); - - $childBar->unpublish('fields.unpublish')->listCheck(true); - - $childBar->archive('fields.archive')->listCheck(true); - } - - if ($this->getCurrentUser()->authorise('core.admin')) - { - $childBar->checkin('fields.checkin')->listCheck(true); - } - - if ($canDo->get('core.edit.state') && !$this->state->get('filter.state') == -2) - { - $childBar->trash('fields.trash')->listCheck(true); - } - - // Add a batch button - if ($canDo->get('core.create') && $canDo->get('core.edit') && $canDo->get('core.edit.state')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if ($this->state->get('filter.state') == -2 && $canDo->get('core.delete', $component)) - { - $toolbar->delete('fields.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences($component); - } - - $toolbar->help('Component:_Fields'); - } + /** + * @var \Joomla\CMS\Form\Form + * + * @since 3.7.0 + */ + public $filterForm; + + /** + * @var array + * + * @since 3.7.0 + */ + public $activeFilters; + + /** + * @var array + * + * @since 3.7.0 + */ + protected $items; + + /** + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 3.7.0 + */ + protected $pagination; + + /** + * @var \Joomla\CMS\Object\CMSObject + * + * @since 3.7.0 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see \Joomla\CMS\MVC\View\HtmlView::loadTemplate() + * + * @since 3.7.0 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Display a warning if the fields system plugin is disabled + if (!PluginHelper::isEnabled('system', 'fields')) { + $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . FieldsHelper::getFieldsPluginId()); + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_FIELDS_SYSTEM_PLUGIN_NOT_ENABLED', $link), 'warning'); + } + + // Only add toolbar when not in modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } + + parent::display($tpl); + } + + /** + * Adds the toolbar. + * + * @return void + * + * @since 3.7.0 + */ + protected function addToolbar() + { + $fieldId = $this->state->get('filter.field_id'); + $component = $this->state->get('filter.component'); + $section = $this->state->get('filter.section'); + $canDo = ContentHelper::getActions($component, 'field', $fieldId); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + // Avoid nonsense situation. + if ($component == 'com_fields') { + return; + } + + // Load extension language file + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_ADMINISTRATOR) + || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); + + $title = Text::sprintf('COM_FIELDS_VIEW_FIELDS_TITLE', Text::_(strtoupper($component))); + + // Prepare the toolbar. + ToolbarHelper::title($title, 'puzzle-piece fields ' . substr($component, 4) . ($section ? "-$section" : '') . '-fields'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('field.add'); + } + + if ($canDo->get('core.edit.state') || $this->getCurrentUser()->authorise('core.admin')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state')) { + $childBar->publish('fields.publish')->listCheck(true); + + $childBar->unpublish('fields.unpublish')->listCheck(true); + + $childBar->archive('fields.archive')->listCheck(true); + } + + if ($this->getCurrentUser()->authorise('core.admin')) { + $childBar->checkin('fields.checkin')->listCheck(true); + } + + if ($canDo->get('core.edit.state') && !$this->state->get('filter.state') == -2) { + $childBar->trash('fields.trash')->listCheck(true); + } + + // Add a batch button + if ($canDo->get('core.create') && $canDo->get('core.edit') && $canDo->get('core.edit.state')) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if ($this->state->get('filter.state') == -2 && $canDo->get('core.delete', $component)) { + $toolbar->delete('fields.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences($component); + } + + $toolbar->help('Component:_Fields'); + } } diff --git a/administrator/components/com_fields/src/View/Group/HtmlView.php b/administrator/components/com_fields/src/View/Group/HtmlView.php index 7e1421f78ee71..754d37c97751c 100644 --- a/administrator/components/com_fields/src/View/Group/HtmlView.php +++ b/administrator/components/com_fields/src/View/Group/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - $component = ''; - $parts = FieldsHelper::extract($this->state->get('filter.context')); - - if ($parts) - { - $component = $parts[0]; - } - - $this->canDo = ContentHelper::getActions($component, 'fieldgroup', $this->item->id); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - Factory::getApplication()->input->set('hidemainmenu', true); - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Adds the toolbar. - * - * @return void - * - * @since 3.7.0 - */ - protected function addToolbar() - { - $component = ''; - $parts = FieldsHelper::extract($this->state->get('filter.context')); - - if ($parts) - { - $component = $parts[0]; - } - - $userId = $this->getCurrentUser()->get('id'); - $canDo = $this->canDo; - - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); - - // Avoid nonsense situation. - if ($component == 'com_fields') - { - return; - } - - // Load component language file - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_ADMINISTRATOR) - || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); - - $title = Text::sprintf('COM_FIELDS_VIEW_GROUP_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE', Text::_(strtoupper($component))); - - // Prepare the toolbar. - ToolbarHelper::title( - $title, - 'puzzle-piece field-' . ($isNew ? 'add' : 'edit') . ' ' . substr($component, 4) . '-group-' . - ($isNew ? 'add' : 'edit') - ); - - $toolbarButtons = []; - - // For new records, check the create permission. - if ($isNew) - { - ToolbarHelper::apply('group.apply'); - - ToolbarHelper::saveGroup( - [ - ['save', 'group.save'], - ['save2new', 'group.save2new'] - ], - 'btn-success' - ); - - ToolbarHelper::cancel('group.cancel'); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); - - $toolbarButtons = []; - - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable) - { - ToolbarHelper::apply('group.apply'); - - $toolbarButtons[] = ['save', 'group.save']; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'group.save2new']; - } - } - - // If an existing item, can save to a copy. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'group.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel('group.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::help('Component:_New_or_Edit_Field_Group'); - } + /** + * @var \Joomla\CMS\Form\Form + * + * @since 3.7.0 + */ + protected $form; + + /** + * @var CMSObject + * + * @since 3.7.0 + */ + protected $item; + + /** + * @var CMSObject + * + * @since 3.7.0 + */ + protected $state; + + /** + * The actions the user is authorised to perform + * + * @var CMSObject + * + * @since 3.7.0 + */ + protected $canDo; + + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see JViewLegacy::loadTemplate() + * + * @since 3.7.0 + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + $component = ''; + $parts = FieldsHelper::extract($this->state->get('filter.context')); + + if ($parts) { + $component = $parts[0]; + } + + $this->canDo = ContentHelper::getActions($component, 'fieldgroup', $this->item->id); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + Factory::getApplication()->input->set('hidemainmenu', true); + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Adds the toolbar. + * + * @return void + * + * @since 3.7.0 + */ + protected function addToolbar() + { + $component = ''; + $parts = FieldsHelper::extract($this->state->get('filter.context')); + + if ($parts) { + $component = $parts[0]; + } + + $userId = $this->getCurrentUser()->get('id'); + $canDo = $this->canDo; + + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); + + // Avoid nonsense situation. + if ($component == 'com_fields') { + return; + } + + // Load component language file + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_ADMINISTRATOR) + || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); + + $title = Text::sprintf('COM_FIELDS_VIEW_GROUP_' . ($isNew ? 'ADD' : 'EDIT') . '_TITLE', Text::_(strtoupper($component))); + + // Prepare the toolbar. + ToolbarHelper::title( + $title, + 'puzzle-piece field-' . ($isNew ? 'add' : 'edit') . ' ' . substr($component, 4) . '-group-' . + ($isNew ? 'add' : 'edit') + ); + + $toolbarButtons = []; + + // For new records, check the create permission. + if ($isNew) { + ToolbarHelper::apply('group.apply'); + + ToolbarHelper::saveGroup( + [ + ['save', 'group.save'], + ['save2new', 'group.save2new'] + ], + 'btn-success' + ); + + ToolbarHelper::cancel('group.cancel'); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); + + $toolbarButtons = []; + + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable) { + ToolbarHelper::apply('group.apply'); + + $toolbarButtons[] = ['save', 'group.save']; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'group.save2new']; + } + } + + // If an existing item, can save to a copy. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'group.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel('group.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::help('Component:_New_or_Edit_Field_Group'); + } } diff --git a/administrator/components/com_fields/src/View/Groups/HtmlView.php b/administrator/components/com_fields/src/View/Groups/HtmlView.php index 2c80ab45f2cec..5ec02db383512 100644 --- a/administrator/components/com_fields/src/View/Groups/HtmlView.php +++ b/administrator/components/com_fields/src/View/Groups/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Display a warning if the fields system plugin is disabled - if (!PluginHelper::isEnabled('system', 'fields')) - { - $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . FieldsHelper::getFieldsPluginId()); - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_FIELDS_SYSTEM_PLUGIN_NOT_ENABLED', $link), 'warning'); - } - - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - - parent::display($tpl); - } - - /** - * Adds the toolbar. - * - * @return void - * - * @since 3.7.0 - */ - protected function addToolbar() - { - $groupId = $this->state->get('filter.group_id'); - $component = ''; - $parts = FieldsHelper::extract($this->state->get('filter.context')); - - if ($parts) - { - $component = $parts[0]; - } - - $canDo = ContentHelper::getActions($component, 'fieldgroup', $groupId); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - // Avoid nonsense situation. - if ($component == 'com_fields') - { - return; - } - - // Load component language file - $lang = Factory::getLanguage(); - $lang->load($component, JPATH_ADMINISTRATOR) - || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); - - $title = Text::sprintf('COM_FIELDS_VIEW_GROUPS_TITLE', Text::_(strtoupper($component))); - - // Prepare the toolbar. - ToolbarHelper::title($title, 'puzzle-piece fields ' . substr($component, 4) . '-groups'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('group.add'); - } - - if ($canDo->get('core.edit.state') || $this->getCurrentUser()->authorise('core.admin')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('groups.publish')->listCheck(true); - - $childBar->unpublish('groups.unpublish')->listCheck(true); - - $childBar->archive('groups.archive')->listCheck(true); - } - - if ($this->getCurrentUser()->authorise('core.admin')) - { - $childBar->checkin('groups.checkin')->listCheck(true); - } - - if ($canDo->get('core.edit.state') && !$this->state->get('filter.state') == -2) - { - $childBar->trash('groups.trash')->listCheck(true); - } - - // Add a batch button - if ($canDo->get('core.create') && $canDo->get('core.edit') && $canDo->get('core.edit.state')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if ($this->state->get('filter.state') == -2 && $canDo->get('core.delete', $component)) - { - $toolbar->delete('groups.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences($component); - } - - $toolbar->help('Component:_Field_Groups'); - } + /** + * @var \Joomla\CMS\Form\Form + * + * @since 3.7.0 + */ + public $filterForm; + + /** + * @var array + * + * @since 3.7.0 + */ + public $activeFilters; + + /** + * @var array + * + * @since 3.7.0 + */ + protected $items; + + /** + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 3.7.0 + */ + protected $pagination; + + /** + * @var \Joomla\CMS\Object\CMSObject + * + * @since 3.7.0 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see HtmlView::loadTemplate() + * + * @since 3.7.0 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Display a warning if the fields system plugin is disabled + if (!PluginHelper::isEnabled('system', 'fields')) { + $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . FieldsHelper::getFieldsPluginId()); + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_FIELDS_SYSTEM_PLUGIN_NOT_ENABLED', $link), 'warning'); + } + + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + + parent::display($tpl); + } + + /** + * Adds the toolbar. + * + * @return void + * + * @since 3.7.0 + */ + protected function addToolbar() + { + $groupId = $this->state->get('filter.group_id'); + $component = ''; + $parts = FieldsHelper::extract($this->state->get('filter.context')); + + if ($parts) { + $component = $parts[0]; + } + + $canDo = ContentHelper::getActions($component, 'fieldgroup', $groupId); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + // Avoid nonsense situation. + if ($component == 'com_fields') { + return; + } + + // Load component language file + $lang = Factory::getLanguage(); + $lang->load($component, JPATH_ADMINISTRATOR) + || $lang->load($component, Path::clean(JPATH_ADMINISTRATOR . '/components/' . $component)); + + $title = Text::sprintf('COM_FIELDS_VIEW_GROUPS_TITLE', Text::_(strtoupper($component))); + + // Prepare the toolbar. + ToolbarHelper::title($title, 'puzzle-piece fields ' . substr($component, 4) . '-groups'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('group.add'); + } + + if ($canDo->get('core.edit.state') || $this->getCurrentUser()->authorise('core.admin')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state')) { + $childBar->publish('groups.publish')->listCheck(true); + + $childBar->unpublish('groups.unpublish')->listCheck(true); + + $childBar->archive('groups.archive')->listCheck(true); + } + + if ($this->getCurrentUser()->authorise('core.admin')) { + $childBar->checkin('groups.checkin')->listCheck(true); + } + + if ($canDo->get('core.edit.state') && !$this->state->get('filter.state') == -2) { + $childBar->trash('groups.trash')->listCheck(true); + } + + // Add a batch button + if ($canDo->get('core.create') && $canDo->get('core.edit') && $canDo->get('core.edit.state')) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if ($this->state->get('filter.state') == -2 && $canDo->get('core.delete', $component)) { + $toolbar->delete('groups.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences($component); + } + + $toolbar->help('Component:_Field_Groups'); + } } diff --git a/administrator/components/com_fields/tmpl/field/edit.php b/administrator/components/com_fields/tmpl/field/edit.php index 1a6f95a144c16..61171f19fbbd9 100644 --- a/administrator/components/com_fields/tmpl/field/edit.php +++ b/administrator/components/com_fields/tmpl/field/edit.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Factory; @@ -22,78 +24,79 @@ /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_fields.admin-field-edit'); + ->useScript('form.validate') + ->useScript('com_fields.admin-field-edit'); ?>
- + -
- 'general', 'recall' => true, 'breakpoint' => 768]); ?> - -
-
- form->renderField('type'); ?> - form->renderField('name'); ?> - form->renderField('label'); ?> - form->renderField('description'); ?> - form->renderField('required'); ?> - form->renderField('only_use_in_subform'); ?> - form->renderField('default_value'); ?> +
+ 'general', 'recall' => true, 'breakpoint' => 768]); ?> + +
+
+ form->renderField('type'); ?> + form->renderField('name'); ?> + form->renderField('label'); ?> + form->renderField('description'); ?> + form->renderField('required'); ?> + form->renderField('only_use_in_subform'); ?> + form->renderField('default_value'); ?> - form->getFieldsets('fieldparams') as $name => $fieldSet) : ?> - form->getFieldset($name) as $field) : ?> - renderField(); ?> - - -
-
- set('fields', - array( - array( - 'published', - 'state', - 'enabled', - ), - 'group_id', - 'assigned_cat_ids', - 'access', - 'language', - 'note', - ) - ); ?> - - set('fields', null); ?> -
-
- - set('ignore_fieldsets', array('fieldparams')); ?> - - -
- -
- - form->renderField('searchindexing'); ?> -
-
- - canDo->get('core.admin')) : ?> - -
- -
- form->getInput('rules'); ?> -
-
- - - - form->getInput('context'); ?> - - -
+ form->getFieldsets('fieldparams') as $name => $fieldSet) : ?> + form->getFieldset($name) as $field) : ?> + renderField(); ?> + + +
+
+ set( + 'fields', + array( + array( + 'published', + 'state', + 'enabled', + ), + 'group_id', + 'assigned_cat_ids', + 'access', + 'language', + 'note', + ) + ); ?> + + set('fields', null); ?> +
+
+ + set('ignore_fieldsets', array('fieldparams')); ?> + + +
+ +
+ + form->renderField('searchindexing'); ?> +
+
+ + canDo->get('core.admin')) : ?> + +
+ +
+ form->getInput('rules'); ?> +
+
+ + + + form->getInput('context'); ?> + + +
diff --git a/administrator/components/com_fields/tmpl/fields/default.php b/administrator/components/com_fields/tmpl/fields/default.php index 39808d489be6a..0a2b0033b6178 100644 --- a/administrator/components/com_fields/tmpl/fields/default.php +++ b/administrator/components/com_fields/tmpl/fields/default.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Categories\Categories; @@ -21,7 +23,7 @@ /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $app = Factory::getApplication(); $user = Factory::getUser(); @@ -38,184 +40,185 @@ $category = Categories::getInstance(str_replace('com_', '', $component) . '.' . $section); // If there is no category for the component and section, then check the component only -if (!$category) -{ - $category = Categories::getInstance(str_replace('com_', '', $component)); +if (!$category) { + $category = Categories::getInstance(str_replace('com_', '', $component)); } -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_fields&task=fields.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_fields&task=fields.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } $searchToolsOptions = []; // Only show field contexts filter if there are more than one option -if (count($this->filterForm->getField('context')->options) > 1) -{ - $searchToolsOptions['selectorFieldName'] = 'context'; +if (count($this->filterForm->getField('context')->options) > 1) { + $searchToolsOptions['selectorFieldName'] = 'context'; } ?>
-
-
-
- $this, 'options' => $searchToolsOptions)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items as $i => $item) : ?> - - authorise('core.edit', $component . '.field.' . $item->id); ?> - authorise('core.admin', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); ?> - authorise('core.edit.own', $component . '.field.' . $item->id) && $item->created_user_id == $userId; ?> - authorise('core.edit.state', $component . '.field.' . $item->id) && $canCheckin; ?> - - - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - - - -
- id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - - - - - - state, $i, 'fields.', $canChange, 'cb'); ?> - -
- checked_out) : ?> - editor, $item->checked_out_time, 'fields.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - -
- note)) : ?> - escape($item->name)); ?> - - escape($item->name), $this->escape($item->note)); ?> - -
- only_use_in_subform) : ?> -
- -
- -
- - id); ?> - - - - id); ?> - - -
- -
-
- escape($item->type); ?> - - escape($item->group_title); ?> - - escape($item->access_level); ?> - - - - id; ?> -
+
+
+
+ $this, 'options' => $searchToolsOptions)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items as $i => $item) : ?> + + authorise('core.edit', $component . '.field.' . $item->id); ?> + authorise('core.admin', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); ?> + authorise('core.edit.own', $component . '.field.' . $item->id) && $item->created_user_id == $userId; ?> + authorise('core.edit.state', $component . '.field.' . $item->id) && $canCheckin; ?> + + + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + + + + + + state, $i, 'fields.', $canChange, 'cb'); ?> + +
+ checked_out) : ?> + editor, $item->checked_out_time, 'fields.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + +
+ note)) : ?> + escape($item->name)); ?> + + escape($item->name), $this->escape($item->note)); ?> + +
+ only_use_in_subform) : ?> +
+ +
+ +
+ + id); ?> + + + + id); ?> + + +
+ +
+
+ escape($item->type); ?> + + escape($item->group_title); ?> + + escape($item->access_level); ?> + + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', $component) - && $user->authorise('core.edit', $component) - && $user->authorise('core.edit.state', $component)) : ?> - Text::_('COM_FIELDS_VIEW_FIELDS_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer') - ), - $this->loadTemplate('batch_body') - ); ?> - - - - - -
-
-
+ + authorise('core.create', $component) + && $user->authorise('core.edit', $component) + && $user->authorise('core.edit.state', $component) +) : ?> + Text::_('COM_FIELDS_VIEW_FIELDS_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer') + ), + $this->loadTemplate('batch_body') + ); ?> + + + + + +
+
+
diff --git a/administrator/components/com_fields/tmpl/fields/default_batch_body.php b/administrator/components/com_fields/tmpl/fields/default_batch_body.php index 700f26699adcb..c5b1e0c000253 100644 --- a/administrator/components/com_fields/tmpl/fields/default_batch_body.php +++ b/administrator/components/com_fields/tmpl/fields/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\HTML\HTMLHelper; @@ -22,43 +24,43 @@ ?>
-
- -
-
- -
-
- -
-
- -
-
-
-
-
-
- - -
- -
-
- - -
-
-
-
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ + +
+ +
+
+ + +
+
+
+
diff --git a/administrator/components/com_fields/tmpl/fields/default_batch_footer.php b/administrator/components/com_fields/tmpl/fields/default_batch_footer.php index 512cb848f01ad..b48333c6a5466 100644 --- a/administrator/components/com_fields/tmpl/fields/default_batch_footer.php +++ b/administrator/components/com_fields/tmpl/fields/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/administrator/components/com_fields/tmpl/fields/modal.php b/administrator/components/com_fields/tmpl/fields/modal.php index 5f7d1c11620f2..15a39f1c485ee 100644 --- a/administrator/components/com_fields/tmpl/fields/modal.php +++ b/administrator/components/com_fields/tmpl/fields/modal.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Factory; @@ -15,9 +17,8 @@ use Joomla\CMS\Router\Route; use Joomla\CMS\Session\Session; -if (Factory::getApplication()->isClient('site')) -{ - Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); +if (Factory::getApplication()->isClient('site')) { + Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); } /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ @@ -30,93 +31,93 @@ ?>
-
+ - $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - 2 => 'icon-folder', - ); - foreach ($this->items as $i => $item) : - ?> - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - -
- - - - - escape($item->title); ?> - - group_id ? $this->escape($item->group_title) : Text::_('JNONE'); ?> - - type; ?> - - escape($item->access_level); ?> - - - - id; ?> -
+ $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + 2 => 'icon-folder', + ); + foreach ($this->items as $i => $item) : + ?> + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + +
+ + + + + escape($item->title); ?> + + group_id ? $this->escape($item->group_title) : Text::_('JNONE'); ?> + + type; ?> + + escape($item->access_level); ?> + + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - + + + -
+
diff --git a/administrator/components/com_fields/tmpl/group/edit.php b/administrator/components/com_fields/tmpl/group/edit.php index 45761b81a852d..cf6f0c1d9baca 100644 --- a/administrator/components/com_fields/tmpl/group/edit.php +++ b/administrator/components/com_fields/tmpl/group/edit.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Factory; @@ -17,7 +19,7 @@ /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $app = Factory::getApplication(); $input = $app->input; @@ -27,56 +29,57 @@ ?>
- -
- 'general', 'recall' => true, 'breakpoint' => 768]); ?> - -
-
- form->renderField('label'); ?> - form->renderField('description'); ?> -
-
- set('fields', - array( - array( - 'published', - 'state', - 'enabled', - ), - 'access', - 'language', - 'note', - ) - ); ?> - - set('fields', null); ?> -
-
- - -
- -
- -
-
- - set('ignore_fieldsets', array('fieldparams')); ?> - - canDo->get('core.admin')) : ?> - -
- -
- form->getInput('rules'); ?> -
-
- - - - form->getInput('context'); ?> - - -
+ +
+ 'general', 'recall' => true, 'breakpoint' => 768]); ?> + +
+
+ form->renderField('label'); ?> + form->renderField('description'); ?> +
+
+ set( + 'fields', + array( + array( + 'published', + 'state', + 'enabled', + ), + 'access', + 'language', + 'note', + ) + ); ?> + + set('fields', null); ?> +
+
+ + +
+ +
+ +
+
+ + set('ignore_fieldsets', array('fieldparams')); ?> + + canDo->get('core.admin')) : ?> + +
+ +
+ form->getInput('rules'); ?> +
+
+ + + + form->getInput('context'); ?> + + +
diff --git a/administrator/components/com_fields/tmpl/groups/default.php b/administrator/components/com_fields/tmpl/groups/default.php index ecc9a5bf4b0e2..49c9477e37219 100644 --- a/administrator/components/com_fields/tmpl/groups/default.php +++ b/administrator/components/com_fields/tmpl/groups/default.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Factory; @@ -20,7 +22,7 @@ /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $app = Factory::getApplication(); $user = Factory::getUser(); @@ -29,9 +31,8 @@ $component = ''; $parts = FieldsHelper::extract($this->state->get('filter.context')); -if ($parts) -{ - $component = $this->escape($parts[0]); +if ($parts) { + $component = $this->escape($parts[0]); } $listOrder = $this->escape($this->state->get('list.ordering')); @@ -39,10 +40,9 @@ $ordering = ($listOrder == 'a.ordering'); $saveOrder = ($listOrder == 'a.ordering' && strtolower($listDirn) == 'asc'); -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_fields&task=groups.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_fields&task=groups.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } $context = $this->escape($this->state->get('filter.context')); @@ -50,140 +50,143 @@ $searchToolsOptions = []; // Only show field contexts filter if there are more than one option -if (count($this->filterForm->getField('context')->options) > 1) -{ - $searchToolsOptions['selectorFieldName'] = 'context'; +if (count($this->filterForm->getField('context')->options) > 1) { + $searchToolsOptions['selectorFieldName'] = 'context'; } ?>
-
-
-
- $this, 'options' => $searchToolsOptions)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items as $i => $item) : ?> - - authorise('core.edit', $component . '.fieldgroup.' . $item->id); ?> - authorise('core.admin', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); ?> - authorise('core.edit.own', $component . '.fieldgroup.' . $item->id) && $item->created_by == $userId; ?> - authorise('core.edit.state', $component . '.fieldgroup.' . $item->id) && $canCheckin; ?> - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - -
- id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - - - - - - state, $i, 'groups.', $canChange, 'cb'); ?> - -
- checked_out) : ?> - editor, $item->checked_out_time, 'groups.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - -
- note) : ?> - escape($item->note)); ?> - -
-
-
- escape($item->access_level); ?> - - - - id; ?> -
+
+
+
+ $this, 'options' => $searchToolsOptions)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items as $i => $item) : ?> + + authorise('core.edit', $component . '.fieldgroup.' . $item->id); ?> + authorise('core.admin', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); ?> + authorise('core.edit.own', $component . '.fieldgroup.' . $item->id) && $item->created_by == $userId; ?> + authorise('core.edit.state', $component . '.fieldgroup.' . $item->id) && $canCheckin; ?> + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + + + + + + state, $i, 'groups.', $canChange, 'cb'); ?> + +
+ checked_out) : ?> + editor, $item->checked_out_time, 'groups.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + +
+ note) : ?> + escape($item->note)); ?> + +
+
+
+ escape($item->access_level); ?> + + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', $component) - && $user->authorise('core.edit', $component) - && $user->authorise('core.edit.state', $component)) : ?> - Text::_('COM_FIELDS_VIEW_GROUPS_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer') - ), - $this->loadTemplate('batch_body') - ); ?> - - - - - -
-
-
+ + authorise('core.create', $component) + && $user->authorise('core.edit', $component) + && $user->authorise('core.edit.state', $component) +) : ?> + Text::_('COM_FIELDS_VIEW_GROUPS_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer') + ), + $this->loadTemplate('batch_body') + ); ?> + + + + + +
+
+
diff --git a/administrator/components/com_fields/tmpl/groups/default_batch_body.php b/administrator/components/com_fields/tmpl/groups/default_batch_body.php index 9b44ea54a4396..97e7bad090dc2 100644 --- a/administrator/components/com_fields/tmpl/groups/default_batch_body.php +++ b/administrator/components/com_fields/tmpl/groups/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Multilanguage; @@ -13,18 +15,18 @@ ?>
-
- -
-
- -
-
- -
-
- -
-
-
+
+ +
+
+ +
+
+ +
+
+ +
+
+
diff --git a/administrator/components/com_fields/tmpl/groups/default_batch_footer.php b/administrator/components/com_fields/tmpl/groups/default_batch_footer.php index a0b7215782176..bc7cd3530c4e6 100644 --- a/administrator/components/com_fields/tmpl/groups/default_batch_footer.php +++ b/administrator/components/com_fields/tmpl/groups/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/administrator/components/com_finder/helpers/indexer/adapter.php b/administrator/components/com_finder/helpers/indexer/adapter.php index 02316348db69a..e00a38c6d6fcf 100644 --- a/administrator/components/com_finder/helpers/indexer/adapter.php +++ b/administrator/components/com_finder/helpers/indexer/adapter.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt - */ -defined('_JEXEC') or die; + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + */ use Joomla\Component\Finder\Administrator\Helper\LanguageHelper; diff --git a/administrator/components/com_finder/services/provider.php b/administrator/components/com_finder/services/provider.php index 7688f99cd8a58..2fc098a563e55 100644 --- a/administrator/components/com_finder/services/provider.php +++ b/administrator/components/com_finder/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Finder')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Finder')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Finder')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Finder')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Finder')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Finder')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new FinderComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new FinderComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_finder/src/Controller/DisplayController.php b/administrator/components/com_finder/src/Controller/DisplayController.php index aaa94590f34a9..c878d88db0b6c 100644 --- a/administrator/components/com_finder/src/Controller/DisplayController.php +++ b/administrator/components/com_finder/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'index', 'word'); - $layout = $this->input->get('layout', 'index', 'word'); - $filterId = $this->input->get('filter_id', null, 'int'); + /** + * Method to display a view. + * + * @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()}. + * + * @return static|boolean A Controller object to support chaining or false on failure. + * + * @since 2.5 + */ + public function display($cachable = false, $urlparams = array()) + { + $view = $this->input->get('view', 'index', 'word'); + $layout = $this->input->get('layout', 'index', 'word'); + $filterId = $this->input->get('filter_id', null, 'int'); - if ($view === 'index') - { - $pluginEnabled = PluginHelper::isEnabled('content', 'finder'); + if ($view === 'index') { + $pluginEnabled = PluginHelper::isEnabled('content', 'finder'); - if (!$pluginEnabled) - { - $finderPluginId = FinderHelper::getFinderPluginId(); - $link = HTMLHelper::_( - 'link', - '#plugin' . $finderPluginId . 'Modal', - Text::_('COM_FINDER_CONTENT_PLUGIN'), - 'class="alert-link" data-bs-toggle="modal" id="title-' . $finderPluginId . '"' - ); - $this->app->enqueueMessage(Text::sprintf('COM_FINDER_INDEX_PLUGIN_CONTENT_NOT_ENABLED_LINK', $link), 'warning'); - } - } + if (!$pluginEnabled) { + $finderPluginId = FinderHelper::getFinderPluginId(); + $link = HTMLHelper::_( + 'link', + '#plugin' . $finderPluginId . 'Modal', + Text::_('COM_FINDER_CONTENT_PLUGIN'), + 'class="alert-link" data-bs-toggle="modal" id="title-' . $finderPluginId . '"' + ); + $this->app->enqueueMessage(Text::sprintf('COM_FINDER_INDEX_PLUGIN_CONTENT_NOT_ENABLED_LINK', $link), 'warning'); + } + } - // Check for edit form. - if ($view === 'filter' && $layout === 'edit' && !$this->checkEditId('com_finder.edit.filter', $filterId)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $f_id), 'error'); - } + // Check for edit form. + if ($view === 'filter' && $layout === 'edit' && !$this->checkEditId('com_finder.edit.filter', $filterId)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $f_id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_finder&view=filters', false)); + $this->setRedirect(Route::_('index.php?option=com_finder&view=filters', false)); - return false; - } + return false; + } - return parent::display(); - } + return parent::display(); + } } diff --git a/administrator/components/com_finder/src/Controller/FilterController.php b/administrator/components/com_finder/src/Controller/FilterController.php index bd85d98034368..3f66664b38461 100644 --- a/administrator/components/com_finder/src/Controller/FilterController.php +++ b/administrator/components/com_finder/src/Controller/FilterController.php @@ -1,4 +1,5 @@ checkToken(); - - /** @var \Joomla\Component\Finder\Administrator\Model\FilterModel $model */ - $model = $this->getModel(); - $table = $model->getTable(); - $data = $this->input->post->get('jform', array(), 'array'); - $checkin = $table->hasField('checked_out'); - $context = "$this->option.edit.$this->context"; - $task = $this->getTask(); - - // Determine the name of the primary key for the data. - if (empty($key)) - { - $key = $table->getKeyName(); - } - - // To avoid data collisions the urlVar may be different from the primary key. - if (empty($urlVar)) - { - $urlVar = $key; - } - - $recordId = $this->input->get($urlVar, '', 'int'); - - if (!$this->checkEditId($context, $recordId)) - { - // Somehow the person just went to the form and tried to save it. We don't allow that. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $recordId), 'error'); - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); - - return false; - } - - // Populate the row id from the session. - $data[$key] = $recordId; - - // The save2copy task needs to be handled slightly differently. - if ($task === 'save2copy') - { - // Check-in the original row. - if ($checkin && $model->checkin($data[$key]) === false) - { - // Check-in failed. Go back to the item and display a notice. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'error'); - } - - $this->setRedirect('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $urlVar)); - - return false; - } - - // Reset the ID and then treat the request as for Apply. - $data[$key] = 0; - $task = 'apply'; - } - - // Access check. - if (!$this->allowSave($data, $key)) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); - - return false; - } - - // Validate the posted data. - // Sometimes the form needs some posted data, such as for plugins and modules. - $form = $model->getForm($data, false); - - if (!$form) - { - $this->app->enqueueMessage($model->getError(), 'error'); - - return false; - } - - // Test whether the data is valid. - $validData = $model->validate($form, $data); - - // Check for validation errors. - if ($validData === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $this->app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Save the data in the session. - $this->app->setUserState($context . '.data', $data); - - // Redirect back to the edit screen. - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key), false) - ); - - return false; - } - - // Get and sanitize the filter data. - $validData['data'] = $this->input->post->get('t', array(), 'array'); - $validData['data'] = array_unique($validData['data']); - $validData['data'] = ArrayHelper::toInteger($validData['data']); - - // Remove any values of zero. - if (array_search(0, $validData['data'], true)) - { - unset($validData['data'][array_search(0, $validData['data'], true)]); - } - - // Attempt to save the data. - if (!$model->save($validData)) - { - // Save the data in the session. - $this->app->setUserState($context . '.data', $validData); - - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key), false) - ); - - return false; - } - - // Save succeeded, so check-in the record. - if ($checkin && $model->checkin($validData[$key]) === false) - { - // Save the data in the session. - $this->app->setUserState($context . '.data', $validData); - - // Check-in failed, so go back to the record and display a notice. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'error'); - $this->setRedirect('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key)); - - return false; - } - - $this->setMessage( - Text::_( - ($this->app->getLanguage()->hasKey($this->text_prefix . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS') - ? $this->text_prefix : 'JLIB_APPLICATION') . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS' - ) - ); - - // Redirect the user and adjust session state based on the chosen task. - switch ($task) - { - case 'apply': - // Set the record data in the session. - $recordId = $model->getState($this->context . '.id'); - $this->holdEditId($context, $recordId); - $this->app->setUserState($context . '.data', null); - $model->checkout($recordId); - - // Redirect back to the edit screen. - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key), false) - ); - - break; - - case 'save2new': - // Clear the record id and data from the session. - $this->releaseEditId($context, $recordId); - $this->app->setUserState($context . '.data', null); - - // Redirect back to the edit screen. - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend(null, $key), false) - ); - - break; - - default: - // Clear the record id and data from the session. - $this->releaseEditId($context, $recordId); - $this->app->setUserState($context . '.data', null); - - // Redirect to the list screen. - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false) - ); - - break; - } - - // Invoke the postSave method to allow for the child class to access the model. - $this->postSaveHook($model, $validData); - - return true; - } + /** + * Method to save a record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 2.5 + */ + public function save($key = null, $urlVar = null) + { + // Check for request forgeries. + $this->checkToken(); + + /** @var \Joomla\Component\Finder\Administrator\Model\FilterModel $model */ + $model = $this->getModel(); + $table = $model->getTable(); + $data = $this->input->post->get('jform', array(), 'array'); + $checkin = $table->hasField('checked_out'); + $context = "$this->option.edit.$this->context"; + $task = $this->getTask(); + + // Determine the name of the primary key for the data. + if (empty($key)) { + $key = $table->getKeyName(); + } + + // To avoid data collisions the urlVar may be different from the primary key. + if (empty($urlVar)) { + $urlVar = $key; + } + + $recordId = $this->input->get($urlVar, '', 'int'); + + if (!$this->checkEditId($context, $recordId)) { + // Somehow the person just went to the form and tried to save it. We don't allow that. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $recordId), 'error'); + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); + + return false; + } + + // Populate the row id from the session. + $data[$key] = $recordId; + + // The save2copy task needs to be handled slightly differently. + if ($task === 'save2copy') { + // Check-in the original row. + if ($checkin && $model->checkin($data[$key]) === false) { + // Check-in failed. Go back to the item and display a notice. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'error'); + } + + $this->setRedirect('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $urlVar)); + + return false; + } + + // Reset the ID and then treat the request as for Apply. + $data[$key] = 0; + $task = 'apply'; + } + + // Access check. + if (!$this->allowSave($data, $key)) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); + + return false; + } + + // Validate the posted data. + // Sometimes the form needs some posted data, such as for plugins and modules. + $form = $model->getForm($data, false); + + if (!$form) { + $this->app->enqueueMessage($model->getError(), 'error'); + + return false; + } + + // Test whether the data is valid. + $validData = $model->validate($form, $data); + + // Check for validation errors. + if ($validData === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $this->app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Save the data in the session. + $this->app->setUserState($context . '.data', $data); + + // Redirect back to the edit screen. + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key), false) + ); + + return false; + } + + // Get and sanitize the filter data. + $validData['data'] = $this->input->post->get('t', array(), 'array'); + $validData['data'] = array_unique($validData['data']); + $validData['data'] = ArrayHelper::toInteger($validData['data']); + + // Remove any values of zero. + if (array_search(0, $validData['data'], true)) { + unset($validData['data'][array_search(0, $validData['data'], true)]); + } + + // Attempt to save the data. + if (!$model->save($validData)) { + // Save the data in the session. + $this->app->setUserState($context . '.data', $validData); + + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key), false) + ); + + return false; + } + + // Save succeeded, so check-in the record. + if ($checkin && $model->checkin($validData[$key]) === false) { + // Save the data in the session. + $this->app->setUserState($context . '.data', $validData); + + // Check-in failed, so go back to the record and display a notice. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'error'); + $this->setRedirect('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key)); + + return false; + } + + $this->setMessage( + Text::_( + ($this->app->getLanguage()->hasKey($this->text_prefix . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS') + ? $this->text_prefix : 'JLIB_APPLICATION') . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS' + ) + ); + + // Redirect the user and adjust session state based on the chosen task. + switch ($task) { + case 'apply': + // Set the record data in the session. + $recordId = $model->getState($this->context . '.id'); + $this->holdEditId($context, $recordId); + $this->app->setUserState($context . '.data', null); + $model->checkout($recordId); + + // Redirect back to the edit screen. + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, $key), false) + ); + + break; + + case 'save2new': + // Clear the record id and data from the session. + $this->releaseEditId($context, $recordId); + $this->app->setUserState($context . '.data', null); + + // Redirect back to the edit screen. + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend(null, $key), false) + ); + + break; + + default: + // Clear the record id and data from the session. + $this->releaseEditId($context, $recordId); + $this->app->setUserState($context . '.data', null); + + // Redirect to the list screen. + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false) + ); + + break; + } + + // Invoke the postSave method to allow for the child class to access the model. + $this->postSaveHook($model, $validData); + + return true; + } } diff --git a/administrator/components/com_finder/src/Controller/FiltersController.php b/administrator/components/com_finder/src/Controller/FiltersController.php index ade78adb5ffd5..9efc8bedafc2d 100644 --- a/administrator/components/com_finder/src/Controller/FiltersController.php +++ b/administrator/components/com_finder/src/Controller/FiltersController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * 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 \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 2.5 + */ + public function getModel($name = 'Filter', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_finder/src/Controller/IndexController.php b/administrator/components/com_finder/src/Controller/IndexController.php index cd93c9bb16fdc..f8881f57aecfe 100644 --- a/administrator/components/com_finder/src/Controller/IndexController.php +++ b/administrator/components/com_finder/src/Controller/IndexController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to optimise the index by removing orphaned entries. - * - * @return boolean True on success. - * - * @since 4.2.0 - */ - public function optimise() - { - $this->checkToken(); - - // Optimise the index by first running the garbage collection - PluginHelper::importPlugin('finder'); - $this->app->triggerEvent('onFinderGarbageCollection'); - - // Now run the optimisation method from the indexer - $indexer = new Indexer; - $indexer->optimize(); - - $message = Text::_('COM_FINDER_INDEX_OPTIMISE_FINISHED'); - $this->setRedirect('index.php?option=com_finder&view=index', $message); - - return true; - } - - /** - * Method to purge all indexed links from the database. - * - * @return boolean True on success. - * - * @since 2.5 - */ - public function purge() - { - $this->checkToken(); - - // Remove the script time limit. - @set_time_limit(0); - - /** @var \Joomla\Component\Finder\Administrator\Model\IndexModel $model */ - $model = $this->getModel('Index', 'Administrator'); - - // Attempt to purge the index. - $return = $model->purge(); - - if (!$return) - { - $message = Text::_('COM_FINDER_INDEX_PURGE_FAILED', $model->getError()); - $this->setRedirect('index.php?option=com_finder&view=index', $message); - - return false; - } - else - { - $message = Text::_('COM_FINDER_INDEX_PURGE_SUCCESS'); - $this->setRedirect('index.php?option=com_finder&view=index', $message); - - return true; - } - } + /** + * 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 \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 2.5 + */ + public function getModel($name = 'Index', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to optimise the index by removing orphaned entries. + * + * @return boolean True on success. + * + * @since 4.2.0 + */ + public function optimise() + { + $this->checkToken(); + + // Optimise the index by first running the garbage collection + PluginHelper::importPlugin('finder'); + $this->app->triggerEvent('onFinderGarbageCollection'); + + // Now run the optimisation method from the indexer + $indexer = new Indexer(); + $indexer->optimize(); + + $message = Text::_('COM_FINDER_INDEX_OPTIMISE_FINISHED'); + $this->setRedirect('index.php?option=com_finder&view=index', $message); + + return true; + } + + /** + * Method to purge all indexed links from the database. + * + * @return boolean True on success. + * + * @since 2.5 + */ + public function purge() + { + $this->checkToken(); + + // Remove the script time limit. + @set_time_limit(0); + + /** @var \Joomla\Component\Finder\Administrator\Model\IndexModel $model */ + $model = $this->getModel('Index', 'Administrator'); + + // Attempt to purge the index. + $return = $model->purge(); + + if (!$return) { + $message = Text::_('COM_FINDER_INDEX_PURGE_FAILED', $model->getError()); + $this->setRedirect('index.php?option=com_finder&view=index', $message); + + return false; + } else { + $message = Text::_('COM_FINDER_INDEX_PURGE_SUCCESS'); + $this->setRedirect('index.php?option=com_finder&view=index', $message); + + return true; + } + } } diff --git a/administrator/components/com_finder/src/Controller/IndexerController.php b/administrator/components/com_finder/src/Controller/IndexerController.php index 2852d7393cda7..f08b2c6950157 100644 --- a/administrator/components/com_finder/src/Controller/IndexerController.php +++ b/administrator/components/com_finder/src/Controller/IndexerController.php @@ -1,4 +1,5 @@ get('enable_logging', '0')) - { - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'indexer.php'; - Log::addLogger($options); - } - - // Log the start - try - { - Log::add('Starting the indexer', Log::INFO); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - // We don't want this form to be cached. - $this->app->allowCache(false); - - // Put in a buffer to silence noise. - ob_start(); - - // Reset the indexer state. - Indexer::resetState(); - - // Import the finder plugins. - PluginHelper::importPlugin('finder'); - - // Add the indexer language to \JS - Text::script('COM_FINDER_AN_ERROR_HAS_OCCURRED'); - Text::script('COM_FINDER_NO_ERROR_RETURNED'); - - // Start the indexer. - try - { - // Trigger the onStartIndex event. - $this->app->triggerEvent('onStartIndex'); - - // Get the indexer state. - $state = Indexer::getState(); - $state->start = 1; - - // Send the response. - static::sendResponse($state); - } - - // Catch an exception and return the response. - catch (\Exception $e) - { - static::sendResponse($e); - } - } - - /** - * Method to run the next batch of content through the indexer. - * - * @return void - * - * @since 2.5 - */ - public function batch() - { - // Check for a valid token. If invalid, send a 403 with the error message. - if (!Session::checkToken('request')) - { - static::sendResponse(new \Exception(Text::_('JINVALID_TOKEN_NOTICE'), 403)); - - return; - } - - $params = ComponentHelper::getParams('com_finder'); - - if ($params->get('enable_logging', '0')) - { - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'indexer.php'; - Log::addLogger($options); - } - - // Log the start - try - { - Log::add('Starting the indexer batch process', Log::INFO); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - // We don't want this form to be cached. - $this->app->allowCache(false); - - // Put in a buffer to silence noise. - ob_start(); - - // Remove the script time limit. - @set_time_limit(0); - - // Get the indexer state. - $state = Indexer::getState(); - - // Reset the batch offset. - $state->batchOffset = 0; - - // Update the indexer state. - Indexer::setState($state); - - // Import the finder plugins. - PluginHelper::importPlugin('finder'); - - /* - * We are going to swap out the raw document object with an HTML document - * in order to work around some plugins that don't do proper environment - * checks before trying to use HTML document functions. - */ - $lang = Factory::getLanguage(); - - // Get the document properties. - $attributes = array ( - 'charset' => 'utf-8', - 'lineend' => 'unix', - 'tab' => ' ', - 'language' => $lang->getTag(), - 'direction' => $lang->isRtl() ? 'rtl' : 'ltr' - ); - - // Start the indexer. - try - { - // Trigger the onBeforeIndex event. - Factory::getApplication()->triggerEvent('onBeforeIndex'); - - // Trigger the onBuildIndex event. - Factory::getApplication()->triggerEvent('onBuildIndex'); - - // Get the indexer state. - $state = Indexer::getState(); - $state->start = 0; - $state->complete = 0; - - // Log batch completion and memory high-water mark. - try - { - Log::add('Batch completed, peak memory usage: ' . number_format(memory_get_peak_usage(true)) . ' bytes', Log::INFO); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - // Send the response. - static::sendResponse($state); - } - - // Catch an exception and return the response. - catch (\Exception $e) - { - // Send the response. - static::sendResponse($e); - } - } - - /** - * Method to optimize the index and perform any necessary cleanup. - * - * @return void - * - * @since 2.5 - */ - public function optimize() - { - // Check for a valid token. If invalid, send a 403 with the error message. - if (!Session::checkToken('request')) - { - static::sendResponse(new \Exception(Text::_('JINVALID_TOKEN_NOTICE'), 403)); - - return; - } - - // We don't want this form to be cached. - $this->app->allowCache(false); - - // Put in a buffer to silence noise. - ob_start(); - - // Import the finder plugins. - PluginHelper::importPlugin('finder'); - - try - { - // Optimize the index - $indexer = new Indexer; - $indexer->optimize(); - - // Get the indexer state. - $state = Indexer::getState(); - $state->start = 0; - $state->complete = 1; - - // Send the response. - static::sendResponse($state); - } - - // Catch an exception and return the response. - catch (\Exception $e) - { - static::sendResponse($e); - } - } - - /** - * Method to handle a send a \JSON response. The body parameter - * can be an \Exception object for when an error has occurred or - * a CMSObject for a good response. - * - * @param \Joomla\CMS\Object\CMSObject|\Exception $data CMSObject on success, \Exception on error. [optional] - * - * @return void - * - * @since 2.5 - */ - public static function sendResponse($data = null) - { - $app = Factory::getApplication(); - - $params = ComponentHelper::getParams('com_finder'); - - if ($params->get('enable_logging', '0')) - { - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'indexer.php'; - Log::addLogger($options); - } - - // Send the assigned error code if we are catching an exception. - if ($data instanceof \Exception) - { - try - { - Log::add($data->getMessage(), Log::ERROR); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - $app->setHeader('status', $data->getCode()); - } - - // Create the response object. - $response = new Response($data); - - if (\JDEBUG) - { - // Add the buffer and memory usage - $response->buffer = ob_get_contents(); - $response->memory = memory_get_usage(true); - } - - // Send the JSON response. - echo json_encode($response); - } + /** + * Method to start the indexer. + * + * @return void + * + * @since 2.5 + */ + public function start() + { + // Check for a valid token. If invalid, send a 403 with the error message. + if (!Session::checkToken('request')) { + static::sendResponse(new \Exception(Text::_('JINVALID_TOKEN_NOTICE'), 403)); + + return; + } + + $params = ComponentHelper::getParams('com_finder'); + + if ($params->get('enable_logging', '0')) { + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'indexer.php'; + Log::addLogger($options); + } + + // Log the start + try { + Log::add('Starting the indexer', Log::INFO); + } catch (\RuntimeException $exception) { + // Informational log only + } + + // We don't want this form to be cached. + $this->app->allowCache(false); + + // Put in a buffer to silence noise. + ob_start(); + + // Reset the indexer state. + Indexer::resetState(); + + // Import the finder plugins. + PluginHelper::importPlugin('finder'); + + // Add the indexer language to \JS + Text::script('COM_FINDER_AN_ERROR_HAS_OCCURRED'); + Text::script('COM_FINDER_NO_ERROR_RETURNED'); + + // Start the indexer. + try { + // Trigger the onStartIndex event. + $this->app->triggerEvent('onStartIndex'); + + // Get the indexer state. + $state = Indexer::getState(); + $state->start = 1; + + // Send the response. + static::sendResponse($state); + } catch (\Exception $e) { + // Catch an exception and return the response. + static::sendResponse($e); + } + } + + /** + * Method to run the next batch of content through the indexer. + * + * @return void + * + * @since 2.5 + */ + public function batch() + { + // Check for a valid token. If invalid, send a 403 with the error message. + if (!Session::checkToken('request')) { + static::sendResponse(new \Exception(Text::_('JINVALID_TOKEN_NOTICE'), 403)); + + return; + } + + $params = ComponentHelper::getParams('com_finder'); + + if ($params->get('enable_logging', '0')) { + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'indexer.php'; + Log::addLogger($options); + } + + // Log the start + try { + Log::add('Starting the indexer batch process', Log::INFO); + } catch (\RuntimeException $exception) { + // Informational log only + } + + // We don't want this form to be cached. + $this->app->allowCache(false); + + // Put in a buffer to silence noise. + ob_start(); + + // Remove the script time limit. + @set_time_limit(0); + + // Get the indexer state. + $state = Indexer::getState(); + + // Reset the batch offset. + $state->batchOffset = 0; + + // Update the indexer state. + Indexer::setState($state); + + // Import the finder plugins. + PluginHelper::importPlugin('finder'); + + /* + * We are going to swap out the raw document object with an HTML document + * in order to work around some plugins that don't do proper environment + * checks before trying to use HTML document functions. + */ + $lang = Factory::getLanguage(); + + // Get the document properties. + $attributes = array ( + 'charset' => 'utf-8', + 'lineend' => 'unix', + 'tab' => ' ', + 'language' => $lang->getTag(), + 'direction' => $lang->isRtl() ? 'rtl' : 'ltr' + ); + + // Start the indexer. + try { + // Trigger the onBeforeIndex event. + Factory::getApplication()->triggerEvent('onBeforeIndex'); + + // Trigger the onBuildIndex event. + Factory::getApplication()->triggerEvent('onBuildIndex'); + + // Get the indexer state. + $state = Indexer::getState(); + $state->start = 0; + $state->complete = 0; + + // Log batch completion and memory high-water mark. + try { + Log::add('Batch completed, peak memory usage: ' . number_format(memory_get_peak_usage(true)) . ' bytes', Log::INFO); + } catch (\RuntimeException $exception) { + // Informational log only + } + + // Send the response. + static::sendResponse($state); + } catch (\Exception $e) { + // Catch an exception and return the response. + // Send the response. + static::sendResponse($e); + } + } + + /** + * Method to optimize the index and perform any necessary cleanup. + * + * @return void + * + * @since 2.5 + */ + public function optimize() + { + // Check for a valid token. If invalid, send a 403 with the error message. + if (!Session::checkToken('request')) { + static::sendResponse(new \Exception(Text::_('JINVALID_TOKEN_NOTICE'), 403)); + + return; + } + + // We don't want this form to be cached. + $this->app->allowCache(false); + + // Put in a buffer to silence noise. + ob_start(); + + // Import the finder plugins. + PluginHelper::importPlugin('finder'); + + try { + // Optimize the index + $indexer = new Indexer(); + $indexer->optimize(); + + // Get the indexer state. + $state = Indexer::getState(); + $state->start = 0; + $state->complete = 1; + + // Send the response. + static::sendResponse($state); + } catch (\Exception $e) { + // Catch an exception and return the response. + static::sendResponse($e); + } + } + + /** + * Method to handle a send a \JSON response. The body parameter + * can be an \Exception object for when an error has occurred or + * a CMSObject for a good response. + * + * @param \Joomla\CMS\Object\CMSObject|\Exception $data CMSObject on success, \Exception on error. [optional] + * + * @return void + * + * @since 2.5 + */ + public static function sendResponse($data = null) + { + $app = Factory::getApplication(); + + $params = ComponentHelper::getParams('com_finder'); + + if ($params->get('enable_logging', '0')) { + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'indexer.php'; + Log::addLogger($options); + } + + // Send the assigned error code if we are catching an exception. + if ($data instanceof \Exception) { + try { + Log::add($data->getMessage(), Log::ERROR); + } catch (\RuntimeException $exception) { + // Informational log only + } + + $app->setHeader('status', $data->getCode()); + } + + // Create the response object. + $response = new Response($data); + + if (\JDEBUG) { + // Add the buffer and memory usage + $response->buffer = ob_get_contents(); + $response->memory = memory_get_usage(true); + } + + // Send the JSON response. + echo json_encode($response); + } } diff --git a/administrator/components/com_finder/src/Controller/MapsController.php b/administrator/components/com_finder/src/Controller/MapsController.php index 149520edb4066..e0390f9df66c9 100644 --- a/administrator/components/com_finder/src/Controller/MapsController.php +++ b/administrator/components/com_finder/src/Controller/MapsController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * 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 \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6 + */ + public function getModel($name = 'Maps', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_finder/src/Controller/SearchesController.php b/administrator/components/com_finder/src/Controller/SearchesController.php index 325893b9eede0..a820b2ee784c3 100644 --- a/administrator/components/com_finder/src/Controller/SearchesController.php +++ b/administrator/components/com_finder/src/Controller/SearchesController.php @@ -1,4 +1,5 @@ getModel('Searches'); + $model = $this->getModel('Searches'); - if (!$model->reset()) - { - $this->app->enqueueMessage($model->getError(), 'error'); - } + if (!$model->reset()) { + $this->app->enqueueMessage($model->getError(), 'error'); + } - $this->setRedirect('index.php?option=com_finder&view=searches'); - } + $this->setRedirect('index.php?option=com_finder&view=searches'); + } } diff --git a/administrator/components/com_finder/src/Extension/FinderComponent.php b/administrator/components/com_finder/src/Extension/FinderComponent.php index 65944b880778a..ab48149a47b49 100644 --- a/administrator/components/com_finder/src/Extension/FinderComponent.php +++ b/administrator/components/com_finder/src/Extension/FinderComponent.php @@ -1,4 +1,5 @@ setDatabase($container->get(DatabaseInterface::class)); - - $this->getRegistry()->register('finder', $finder); - - $filter = new Filter; - $filter->setDatabase($container->get(DatabaseInterface::class)); - - $this->getRegistry()->register('filter', $filter); - - $this->getRegistry()->register('query', new Query); - } + use RouterServiceTrait; + use HTMLRegistryAwareTrait; + + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $finder = new Finder(); + $finder->setDatabase($container->get(DatabaseInterface::class)); + + $this->getRegistry()->register('finder', $finder); + + $filter = new Filter(); + $filter->setDatabase($container->get(DatabaseInterface::class)); + + $this->getRegistry()->register('filter', $filter); + + $this->getRegistry()->register('query', new Query()); + } } diff --git a/administrator/components/com_finder/src/Field/BranchesField.php b/administrator/components/com_finder/src/Field/BranchesField.php index 6d3994ff76b79..7c22537b75d73 100644 --- a/administrator/components/com_finder/src/Field/BranchesField.php +++ b/administrator/components/com_finder/src/Field/BranchesField.php @@ -1,4 +1,5 @@ bootComponent('com_finder'); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.5 + */ + public function getOptions() + { + Factory::getApplication()->bootComponent('com_finder'); - return HTMLHelper::_('finder.mapslist'); - } + return HTMLHelper::_('finder.mapslist'); + } } diff --git a/administrator/components/com_finder/src/Field/ContentmapField.php b/administrator/components/com_finder/src/Field/ContentmapField.php index 8a71a3f710f43..2dece7db516eb 100644 --- a/administrator/components/com_finder/src/Field/ContentmapField.php +++ b/administrator/components/com_finder/src/Field/ContentmapField.php @@ -1,4 +1,5 @@ getDatabase(); - - // Main query. - $query = $db->getQuery(true) - ->select($db->quoteName('a.title', 'text')) - ->select($db->quoteName('a.id', 'value')) - ->select($db->quoteName('a.parent_id')) - ->select($db->quoteName('a.level')) - ->from($db->quoteName('#__finder_taxonomy', 'a')) - ->where($db->quoteName('a.parent_id') . ' <> 0') - ->order('a.title ASC'); - - $db->setQuery($query); - - try - { - $contentMap = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - return []; - } - - // Build the grouped list array. - if ($contentMap) - { - $parents = []; - - foreach ($contentMap as $item) - { - if (!isset($parents[$item->parent_id])) - { - $parents[$item->parent_id] = []; - } - - $parents[$item->parent_id][] = $item; - } - - foreach ($parents[1] as $branch) - { - $groups[$branch->text] = $this->prepareLevel($branch->value, $parents); - } - } - - // Merge any additional groups in the XML definition. - $groups = array_merge(parent::getGroups(), $groups); - - return $groups; - } - - /** - * Indenting and translating options for the list - * - * @param int $parent Parent ID to process - * @param array $parents Array of arrays of items with parent IDs as keys - * - * @return array The indented list of entries for this branch - * - * @since 4.1.5 - */ - private function prepareLevel($parent, $parents) - { - $lang = Factory::getLanguage(); - $entries = []; - - foreach ($parents[$parent] as $item) - { - $levelPrefix = str_repeat('- ', $item->level - 1); - - if (trim($item->text, '*') === 'Language') - { - $text = LanguageHelper::branchLanguageTitle($item->text); - } - else - { - $key = LanguageHelper::branchSingular($item->text); - $text = $lang->hasKey($key) ? Text::_($key) : $item->text; - } - - $entries[] = HTMLHelper::_('select.option', $item->value, $levelPrefix . $text); - - if (isset($parents[$item->value])) - { - $entries = array_merge($entries, $this->prepareLevel($item->value, $parents)); - } - } - - return $entries; - } + /** + * The form field type. + * + * @var string + * @since 3.6.0 + */ + public $type = 'ContentMap'; + + /** + * Method to get the list of content map options grouped by first level. + * + * @return array The field option objects as a nested array in groups. + * + * @since 3.6.0 + */ + protected function getGroups() + { + $groups = array(); + + // Get the database object and a new query object. + $db = $this->getDatabase(); + + // Main query. + $query = $db->getQuery(true) + ->select($db->quoteName('a.title', 'text')) + ->select($db->quoteName('a.id', 'value')) + ->select($db->quoteName('a.parent_id')) + ->select($db->quoteName('a.level')) + ->from($db->quoteName('#__finder_taxonomy', 'a')) + ->where($db->quoteName('a.parent_id') . ' <> 0') + ->order('a.title ASC'); + + $db->setQuery($query); + + try { + $contentMap = $db->loadObjectList(); + } catch (\RuntimeException $e) { + return []; + } + + // Build the grouped list array. + if ($contentMap) { + $parents = []; + + foreach ($contentMap as $item) { + if (!isset($parents[$item->parent_id])) { + $parents[$item->parent_id] = []; + } + + $parents[$item->parent_id][] = $item; + } + + foreach ($parents[1] as $branch) { + $groups[$branch->text] = $this->prepareLevel($branch->value, $parents); + } + } + + // Merge any additional groups in the XML definition. + $groups = array_merge(parent::getGroups(), $groups); + + return $groups; + } + + /** + * Indenting and translating options for the list + * + * @param int $parent Parent ID to process + * @param array $parents Array of arrays of items with parent IDs as keys + * + * @return array The indented list of entries for this branch + * + * @since 4.1.5 + */ + private function prepareLevel($parent, $parents) + { + $lang = Factory::getLanguage(); + $entries = []; + + foreach ($parents[$parent] as $item) { + $levelPrefix = str_repeat('- ', $item->level - 1); + + if (trim($item->text, '*') === 'Language') { + $text = LanguageHelper::branchLanguageTitle($item->text); + } else { + $key = LanguageHelper::branchSingular($item->text); + $text = $lang->hasKey($key) ? Text::_($key) : $item->text; + } + + $entries[] = HTMLHelper::_('select.option', $item->value, $levelPrefix . $text); + + if (isset($parents[$item->value])) { + $entries = array_merge($entries, $this->prepareLevel($item->value, $parents)); + } + } + + return $entries; + } } diff --git a/administrator/components/com_finder/src/Field/ContenttypesField.php b/administrator/components/com_finder/src/Field/ContenttypesField.php index cefd638be56c2..3072ec3049904 100644 --- a/administrator/components/com_finder/src/Field/ContenttypesField.php +++ b/administrator/components/com_finder/src/Field/ContenttypesField.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('id', 'value')) - ->select($db->quoteName('title', 'text')) - ->from($db->quoteName('#__finder_types')); + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('id', 'value')) + ->select($db->quoteName('title', 'text')) + ->from($db->quoteName('#__finder_types')); - // Get the options. - $db->setQuery($query); + // Get the options. + $db->setQuery($query); - try - { - $contentTypes = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } + try { + $contentTypes = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } - // Translate. - foreach ($contentTypes as $contentType) - { - $key = LanguageHelper::branchSingular($contentType->text); - $contentType->translatedText = $lang->hasKey($key) ? Text::_($key) : $contentType->text; - } + // Translate. + foreach ($contentTypes as $contentType) { + $key = LanguageHelper::branchSingular($contentType->text); + $contentType->translatedText = $lang->hasKey($key) ? Text::_($key) : $contentType->text; + } - // Order by title. - $contentTypes = ArrayHelper::sortObjects($contentTypes, 'translatedText', 1, true, true); + // Order by title. + $contentTypes = ArrayHelper::sortObjects($contentTypes, 'translatedText', 1, true, true); - // Convert the values to options. - foreach ($contentTypes as $contentType) - { - $options[] = HTMLHelper::_('select.option', $contentType->value, $contentType->translatedText); - } + // Convert the values to options. + foreach ($contentTypes as $contentType) { + $options[] = HTMLHelper::_('select.option', $contentType->value, $contentType->translatedText); + } - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $options); + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $options); - return $options; - } + return $options; + } } diff --git a/administrator/components/com_finder/src/Field/SearchfilterField.php b/administrator/components/com_finder/src/Field/SearchfilterField.php index d784b75cc1f7d..d55abe528b5f0 100644 --- a/administrator/components/com_finder/src/Field/SearchfilterField.php +++ b/administrator/components/com_finder/src/Field/SearchfilterField.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select('f.title AS text, f.filter_id AS value') - ->from($db->quoteName('#__finder_filters') . ' AS f') - ->where('f.state = 1') - ->order('f.title ASC'); - $db->setQuery($query); - $options = $db->loadObjectList(); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 2.5 + */ + public function getOptions() + { + // Build the query. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('f.title AS text, f.filter_id AS value') + ->from($db->quoteName('#__finder_filters') . ' AS f') + ->where('f.state = 1') + ->order('f.title ASC'); + $db->setQuery($query); + $options = $db->loadObjectList(); - array_unshift($options, HTMLHelper::_('select.option', '', Text::_('COM_FINDER_SELECT_SEARCH_FILTER'), 'value', 'text')); + array_unshift($options, HTMLHelper::_('select.option', '', Text::_('COM_FINDER_SELECT_SEARCH_FILTER'), 'value', 'text')); - return $options; - } + return $options; + } } diff --git a/administrator/components/com_finder/src/Helper/FinderHelper.php b/administrator/components/com_finder/src/Helper/FinderHelper.php index ca51836df7381..d3b76042482ec 100644 --- a/administrator/components/com_finder/src/Helper/FinderHelper.php +++ b/administrator/components/com_finder/src/Helper/FinderHelper.php @@ -1,4 +1,5 @@ extension_id : 0; - } + return $pluginRecord !== null ? $pluginRecord->extension_id : 0; + } } diff --git a/administrator/components/com_finder/src/Helper/LanguageHelper.php b/administrator/components/com_finder/src/Helper/LanguageHelper.php index a6e5edf8e3ab7..dc388b1d4ec53 100644 --- a/administrator/components/com_finder/src/Helper/LanguageHelper.php +++ b/administrator/components/com_finder/src/Helper/LanguageHelper.php @@ -1,4 +1,5 @@ getLanguage(); - - if ($language->hasKey('PLG_FINDER_QUERY_FILTER_BRANCH_S_' . $return) || JDEBUG) - { - return 'PLG_FINDER_QUERY_FILTER_BRANCH_S_' . $return; - } - - return $branchName; - } - - /** - * Method to return the language name for a language taxonomy branch. - * - * @param string $branchName Language branch name. - * - * @return string The language title. - * - * @since 3.6.0 - */ - public static function branchLanguageTitle($branchName) - { - $title = $branchName; - - if ($branchName === '*') - { - $title = Text::_('JALL_LANGUAGE'); - } - else - { - $languages = CMSLanguageHelper::getLanguages('lang_code'); - - if (isset($languages[$branchName])) - { - $title = $languages[$branchName]->title; - } - } - - return $title; - } - - /** - * Method to load Smart Search component language file. - * - * @return void - * - * @since 2.5 - */ - public static function loadComponentLanguage() - { - Factory::getLanguage()->load('com_finder', JPATH_SITE); - } - - /** - * Method to load Smart Search plugin language files. - * - * @return void - * - * @since 2.5 - */ - public static function loadPluginLanguage() - { - static $loaded = false; - - // If already loaded, don't load again. - if ($loaded) - { - return; - } - - $loaded = true; - - // Get array of all the enabled Smart Search plugin names. - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select(array($db->quoteName('name'), $db->quoteName('element'))) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('finder')) - ->where($db->quoteName('enabled') . ' = 1'); - $db->setQuery($query); - $plugins = $db->loadObjectList(); - - if (empty($plugins)) - { - return; - } - - // Load generic language strings. - $lang = Factory::getLanguage(); - $lang->load('plg_content_finder', JPATH_ADMINISTRATOR); - - // Load language file for each plugin. - foreach ($plugins as $plugin) - { - $lang->load($plugin->name, JPATH_ADMINISTRATOR) - || $lang->load($plugin->name, JPATH_PLUGINS . '/finder/' . $plugin->element); - } - } + /** + * Method to return a plural language code for a taxonomy branch. + * + * @param string $branchName Branch title. + * + * @return string Language key code. + * + * @since 2.5 + */ + public static function branchPlural($branchName) + { + $return = preg_replace('/[^a-zA-Z0-9]+/', '_', strtoupper($branchName)); + + if ($return !== '_') { + return 'PLG_FINDER_QUERY_FILTER_BRANCH_P_' . $return; + } + + return $branchName; + } + + /** + * Method to return a singular language code for a taxonomy branch. + * + * @param string $branchName Branch name. + * + * @return string Language key code. + * + * @since 2.5 + */ + public static function branchSingular($branchName) + { + $return = preg_replace('/[^a-zA-Z0-9]+/', '_', strtoupper($branchName)); + $language = Factory::getApplication()->getLanguage(); + + if ($language->hasKey('PLG_FINDER_QUERY_FILTER_BRANCH_S_' . $return) || JDEBUG) { + return 'PLG_FINDER_QUERY_FILTER_BRANCH_S_' . $return; + } + + return $branchName; + } + + /** + * Method to return the language name for a language taxonomy branch. + * + * @param string $branchName Language branch name. + * + * @return string The language title. + * + * @since 3.6.0 + */ + public static function branchLanguageTitle($branchName) + { + $title = $branchName; + + if ($branchName === '*') { + $title = Text::_('JALL_LANGUAGE'); + } else { + $languages = CMSLanguageHelper::getLanguages('lang_code'); + + if (isset($languages[$branchName])) { + $title = $languages[$branchName]->title; + } + } + + return $title; + } + + /** + * Method to load Smart Search component language file. + * + * @return void + * + * @since 2.5 + */ + public static function loadComponentLanguage() + { + Factory::getLanguage()->load('com_finder', JPATH_SITE); + } + + /** + * Method to load Smart Search plugin language files. + * + * @return void + * + * @since 2.5 + */ + public static function loadPluginLanguage() + { + static $loaded = false; + + // If already loaded, don't load again. + if ($loaded) { + return; + } + + $loaded = true; + + // Get array of all the enabled Smart Search plugin names. + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select(array($db->quoteName('name'), $db->quoteName('element'))) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('finder')) + ->where($db->quoteName('enabled') . ' = 1'); + $db->setQuery($query); + $plugins = $db->loadObjectList(); + + if (empty($plugins)) { + return; + } + + // Load generic language strings. + $lang = Factory::getLanguage(); + $lang->load('plg_content_finder', JPATH_ADMINISTRATOR); + + // Load language file for each plugin. + foreach ($plugins as $plugin) { + $lang->load($plugin->name, JPATH_ADMINISTRATOR) + || $lang->load($plugin->name, JPATH_PLUGINS . '/finder/' . $plugin->element); + } + } } diff --git a/administrator/components/com_finder/src/Indexer/Adapter.php b/administrator/components/com_finder/src/Indexer/Adapter.php index 0d7d9b341bbe9..5ae4656181dfe 100644 --- a/administrator/components/com_finder/src/Indexer/Adapter.php +++ b/administrator/components/com_finder/src/Indexer/Adapter.php @@ -1,4 +1,5 @@ type_id = $this->getTypeId(); - - // Add the content type if it doesn't exist and is set. - if (empty($this->type_id) && !empty($this->type_title)) - { - $this->type_id = Helper::addContentType($this->type_title, $this->mime); - } - - // Check for a layout override. - if ($this->params->get('layout')) - { - $this->layout = $this->params->get('layout'); - } - - // Get the indexer object - $this->indexer = new Indexer($this->db); - } - - /** - * Method to get the adapter state and push it into the indexer. - * - * @return void - * - * @since 2.5 - * @throws Exception on error. - */ - public function onStartIndex() - { - // Get the indexer state. - $iState = Indexer::getState(); - - // Get the number of content items. - $total = (int) $this->getContentCount(); - - // Add the content count to the total number of items. - $iState->totalItems += $total; - - // Populate the indexer state information for the adapter. - $iState->pluginState[$this->context]['total'] = $total; - $iState->pluginState[$this->context]['offset'] = 0; - - // Set the indexer state. - Indexer::setState($iState); - } - - /** - * Method to prepare for the indexer to be run. This method will often - * be used to include dependencies and things of that nature. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on error. - */ - public function onBeforeIndex() - { - // Get the indexer and adapter state. - $iState = Indexer::getState(); - $aState = $iState->pluginState[$this->context]; - - // Check the progress of the indexer and the adapter. - if ($iState->batchOffset == $iState->batchSize || $aState['offset'] == $aState['total']) - { - return true; - } - - // Run the setup method. - return $this->setup(); - } - - /** - * Method to index a batch of content items. This method can be called by - * the indexer many times throughout the indexing process depending on how - * much content is available for indexing. It is important to track the - * progress correctly so we can display it to the user. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on error. - */ - public function onBuildIndex() - { - // Get the indexer and adapter state. - $iState = Indexer::getState(); - $aState = $iState->pluginState[$this->context]; - - // Check the progress of the indexer and the adapter. - if ($iState->batchOffset == $iState->batchSize || $aState['offset'] == $aState['total']) - { - return true; - } - - // Get the batch offset and size. - $offset = (int) $aState['offset']; - $limit = (int) ($iState->batchSize - $iState->batchOffset); - - // Get the content items to index. - $items = $this->getItems($offset, $limit); - - // Iterate through the items and index them. - for ($i = 0, $n = count($items); $i < $n; $i++) - { - // Index the item. - $this->index($items[$i]); - - // Adjust the offsets. - $offset++; - $iState->batchOffset++; - $iState->totalItems--; - } - - // Update the indexer state. - $aState['offset'] = $offset; - $iState->pluginState[$this->context] = $aState; - Indexer::setState($iState); - - return true; - } - - /** - * Method to remove outdated index entries - * - * @return integer - * - * @since 4.2.0 - */ - public function onFinderGarbageCollection() - { - $db = $this->db; - $type_id = $this->getTypeId(); - - $query = $db->getQuery(true); - $subquery = $db->getQuery(true); - $subquery->select('CONCAT(' . $db->quote($this->getUrl('', $this->extension, $this->layout)) . ', id)') - ->from($db->quoteName($this->table)); - $query->select($db->quoteName('l.link_id')) - ->from($db->quoteName('#__finder_links', 'l')) - ->where($db->quoteName('l.type_id') . ' = ' . $type_id) - ->where($db->quoteName('l.url') . ' LIKE ' . $db->quote($this->getUrl('%', $this->extension, $this->layout))) - ->where($db->quoteName('l.url') . ' NOT IN (' . $subquery . ')'); - $db->setQuery($query); - $items = $db->loadColumn(); - - foreach ($items as $item) - { - $this->indexer->remove($item); - } - - return count($items); - } - - /** - * Method to change the value of a content item's property in the links - * table. This is used to synchronize published and access states that - * are changed when not editing an item directly. - * - * @param string $id The ID of the item to change. - * @param string $property The property that is being changed. - * @param integer $value The new value of that property. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function change($id, $property, $value) - { - // Check for a property we know how to handle. - if ($property !== 'state' && $property !== 'access') - { - return true; - } - - // Get the URL for the content id. - $item = $this->db->quote($this->getUrl($id, $this->extension, $this->layout)); - - // Update the content items. - $query = $this->db->getQuery(true) - ->update($this->db->quoteName('#__finder_links')) - ->set($this->db->quoteName($property) . ' = ' . (int) $value) - ->where($this->db->quoteName('url') . ' = ' . $item); - $this->db->setQuery($query); - $this->db->execute(); - - return true; - } - - /** - * Method to index an item. - * - * @param Result $item The item to index as a Result object. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - abstract protected function index(Result $item); - - /** - * Method to reindex an item. - * - * @param integer $id The ID of the item to reindex. - * - * @return void - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function reindex($id) - { - // Run the setup method. - $this->setup(); - - // Remove the old item. - $this->remove($id, false); - - // Get the item. - $item = $this->getItem($id); - - // Index the item. - $this->index($item); - - Taxonomy::removeOrphanNodes(); - } - - /** - * Method to remove an item from the index. - * - * @param string $id The ID of the item to remove. - * @param bool $removeTaxonomies Remove empty taxonomies - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function remove($id, $removeTaxonomies = true) - { - // Get the item's URL - $url = $this->db->quote($this->getUrl($id, $this->extension, $this->layout)); - - // Get the link ids for the content items. - $query = $this->db->getQuery(true) - ->select($this->db->quoteName('link_id')) - ->from($this->db->quoteName('#__finder_links')) - ->where($this->db->quoteName('url') . ' = ' . $url); - $this->db->setQuery($query); - $items = $this->db->loadColumn(); - - // Check the items. - if (empty($items)) - { - Factory::getApplication()->triggerEvent('onFinderIndexAfterDelete', array($id)); - - return true; - } - - // Remove the items. - foreach ($items as $item) - { - $this->indexer->remove($item, $removeTaxonomies); - } - - return true; - } - - /** - * Method to setup the adapter before indexing. - * - * @return boolean True on success, false on failure. - * - * @since 2.5 - * @throws Exception on database error. - */ - abstract protected function setup(); - - /** - * Method to update index data on category access level changes - * - * @param Table $row A Table object - * - * @return void - * - * @since 2.5 - */ - protected function categoryAccessChange($row) - { - $query = clone $this->getStateQuery(); - $query->where('c.id = ' . (int) $row->id); - - // Get the access level. - $this->db->setQuery($query); - $items = $this->db->loadObjectList(); - - // Adjust the access level for each item within the category. - foreach ($items as $item) - { - // Set the access level. - $temp = max($item->access, $row->access); - - // Update the item. - $this->change((int) $item->id, 'access', $temp); - } - } - - /** - * Method to update index data on category access level changes - * - * @param array $pks A list of primary key ids of the content that has changed state. - * @param integer $value The value of the state that the content has been changed to. - * - * @return void - * - * @since 2.5 - */ - protected function categoryStateChange($pks, $value) - { - /* - * The item's published state is tied to the category - * published state so we need to look up all published states - * before we change anything. - */ - foreach ($pks as $pk) - { - $query = clone $this->getStateQuery(); - $query->where('c.id = ' . (int) $pk); - - // Get the published states. - $this->db->setQuery($query); - $items = $this->db->loadObjectList(); - - // Adjust the state for each item within the category. - foreach ($items as $item) - { - // Translate the state. - $temp = $this->translateState($item->state, $value); - - // Update the item. - $this->change($item->id, 'state', $temp); - } - } - } - - /** - * Method to check the existing access level for categories - * - * @param Table $row A Table object - * - * @return void - * - * @since 2.5 - */ - protected function checkCategoryAccess($row) - { - $query = $this->db->getQuery(true) - ->select($this->db->quoteName('access')) - ->from($this->db->quoteName('#__categories')) - ->where($this->db->quoteName('id') . ' = ' . (int) $row->id); - $this->db->setQuery($query); - - // Store the access level to determine if it changes - $this->old_cataccess = $this->db->loadResult(); - } - - /** - * Method to check the existing access level for items - * - * @param Table $row A Table object - * - * @return void - * - * @since 2.5 - */ - protected function checkItemAccess($row) - { - $query = $this->db->getQuery(true) - ->select($this->db->quoteName('access')) - ->from($this->db->quoteName($this->table)) - ->where($this->db->quoteName('id') . ' = ' . (int) $row->id); - $this->db->setQuery($query); - - // Store the access level to determine if it changes - $this->old_access = $this->db->loadResult(); - } - - /** - * Method to get the number of content items available to index. - * - * @return integer The number of content items available to index. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function getContentCount() - { - $return = 0; - - // Get the list query. - $query = $this->getListQuery(); - - // Check if the query is valid. - if (empty($query)) - { - return $return; - } - - // Tweak the SQL query to make the total lookup faster. - if ($query instanceof QueryInterface) - { - $query = clone $query; - $query->clear('select') - ->select('COUNT(*)') - ->clear('order'); - } - - // Get the total number of content items to index. - $this->db->setQuery($query); - - return (int) $this->db->loadResult(); - } - - /** - * Method to get a content item to index. - * - * @param integer $id The id of the content item. - * - * @return Result A Result object. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function getItem($id) - { - // Get the list query and add the extra WHERE clause. - $query = $this->getListQuery(); - $query->where('a.id = ' . (int) $id); - - // Get the item to index. - $this->db->setQuery($query); - $item = $this->db->loadAssoc(); - - // Convert the item to a result object. - $item = ArrayHelper::toObject((array) $item, Result::class); - - // Set the item type. - $item->type_id = $this->type_id; - - // Set the item layout. - $item->layout = $this->layout; - - return $item; - } - - /** - * Method to get a list of content items to index. - * - * @param integer $offset The list offset. - * @param integer $limit The list limit. - * @param QueryInterface $query A QueryInterface object. [optional] - * - * @return Result[] An array of Result objects. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function getItems($offset, $limit, $query = null) - { - // Get the content items to index. - $this->db->setQuery($this->getListQuery($query)->setLimit($limit, $offset)); - $items = $this->db->loadAssocList(); - - foreach ($items as &$item) - { - $item = ArrayHelper::toObject($item, Result::class); - - // Set the item type. - $item->type_id = $this->type_id; - - // Set the mime type. - $item->mime = $this->mime; - - // Set the item layout. - $item->layout = $this->layout; - } - - return $items; - } - - /** - * Method to get the SQL query used to retrieve the list of content items. - * - * @param mixed $query A QueryInterface object. [optional] - * - * @return QueryInterface A database object. - * - * @since 2.5 - */ - protected function getListQuery($query = null) - { - // Check if we can use the supplied SQL query. - return $query instanceof QueryInterface ? $query : $this->db->getQuery(true); - } - - /** - * Method to get the plugin type - * - * @param integer $id The plugin ID - * - * @return string The plugin type - * - * @since 2.5 - */ - protected function getPluginType($id) - { - // Prepare the query - $query = $this->db->getQuery(true) - ->select($this->db->quoteName('element')) - ->from($this->db->quoteName('#__extensions')) - ->where($this->db->quoteName('extension_id') . ' = ' . (int) $id); - $this->db->setQuery($query); - - return $this->db->loadResult(); - } - - /** - * Method to get a SQL query to load the published and access states for - * an article and category. - * - * @return QueryInterface A database object. - * - * @since 2.5 - */ - protected function getStateQuery() - { - $query = $this->db->getQuery(true); - - // Item ID - $query->select('a.id'); - - // Item and category published state - $query->select('a.' . $this->state_field . ' AS state, c.published AS cat_state'); - - // Item and category access levels - $query->select('a.access, c.access AS cat_access') - ->from($this->table . ' AS a') - ->join('LEFT', '#__categories AS c ON c.id = a.catid'); - - return $query; - } - - /** - * Method to get the query clause for getting items to update by time. - * - * @param string $time The modified timestamp. - * - * @return QueryInterface A database object. - * - * @since 2.5 - */ - protected function getUpdateQueryByTime($time) - { - // Build an SQL query based on the modified time. - $query = $this->db->getQuery(true) - ->where('a.modified >= ' . $this->db->quote($time)); - - return $query; - } - - /** - * Method to get the query clause for getting items to update by id. - * - * @param array $ids The ids to load. - * - * @return QueryInterface A database object. - * - * @since 2.5 - */ - protected function getUpdateQueryByIds($ids) - { - // Build an SQL query based on the item ids. - $query = $this->db->getQuery(true) - ->where('a.id IN(' . implode(',', $ids) . ')'); - - return $query; - } - - /** - * Method to get the type id for the adapter content. - * - * @return integer The numeric type id for the content. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function getTypeId() - { - // Get the type id from the database. - $query = $this->db->getQuery(true) - ->select($this->db->quoteName('id')) - ->from($this->db->quoteName('#__finder_types')) - ->where($this->db->quoteName('title') . ' = ' . $this->db->quote($this->type_title)); - $this->db->setQuery($query); - - return (int) $this->db->loadResult(); - } - - /** - * Method to get the URL for the item. The URL is how we look up the link - * in the Finder index. - * - * @param integer $id The id of the item. - * @param string $extension The extension the category is in. - * @param string $view The view for the URL. - * - * @return string The URL of the item. - * - * @since 2.5 - */ - protected function getUrl($id, $extension, $view) - { - return 'index.php?option=' . $extension . '&view=' . $view . '&id=' . $id; - } - - /** - * Method to get the page title of any menu item that is linked to the - * content item, if it exists and is set. - * - * @param string $url The URL of the item. - * - * @return mixed The title on success, null if not found. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function getItemMenuTitle($url) - { - $return = null; - - // Set variables - $user = Factory::getUser(); - $groups = implode(',', $user->getAuthorisedViewLevels()); - - // Build a query to get the menu params. - $query = $this->db->getQuery(true) - ->select($this->db->quoteName('params')) - ->from($this->db->quoteName('#__menu')) - ->where($this->db->quoteName('link') . ' = ' . $this->db->quote($url)) - ->where($this->db->quoteName('published') . ' = 1') - ->where($this->db->quoteName('access') . ' IN (' . $groups . ')'); - - // Get the menu params from the database. - $this->db->setQuery($query); - $params = $this->db->loadResult(); - - // Check the results. - if (empty($params)) - { - return $return; - } - - // Instantiate the params. - $params = json_decode($params); - - // Get the page title if it is set. - if (isset($params->page_title) && $params->page_title) - { - $return = $params->page_title; - } - - return $return; - } - - /** - * Method to update index data on access level changes - * - * @param Table $row A Table object - * - * @return void - * - * @since 2.5 - */ - protected function itemAccessChange($row) - { - $query = clone $this->getStateQuery(); - $query->where('a.id = ' . (int) $row->id); - - // Get the access level. - $this->db->setQuery($query); - $item = $this->db->loadObject(); - - // Set the access level. - $temp = max($row->access, $item->cat_access); - - // Update the item. - $this->change((int) $row->id, 'access', $temp); - } - - /** - * Method to update index data on published state changes - * - * @param array $pks A list of primary key ids of the content that has changed state. - * @param integer $value The value of the state that the content has been changed to. - * - * @return void - * - * @since 2.5 - */ - protected function itemStateChange($pks, $value) - { - /* - * The item's published state is tied to the category - * published state so we need to look up all published states - * before we change anything. - */ - foreach ($pks as $pk) - { - $query = clone $this->getStateQuery(); - $query->where('a.id = ' . (int) $pk); - - // Get the published states. - $this->db->setQuery($query); - $item = $this->db->loadObject(); - - // Translate the state. - $temp = $this->translateState($value, $item->cat_state); - - // Update the item. - $this->change($pk, 'state', $temp); - } - } - - /** - * Method to update index data when a plugin is disabled - * - * @param array $pks A list of primary key ids of the content that has changed state. - * - * @return void - * - * @since 2.5 - */ - protected function pluginDisable($pks) - { - // Since multiple plugins may be disabled at a time, we need to check first - // that we're handling the appropriate one for the context - foreach ($pks as $pk) - { - if ($this->getPluginType($pk) == strtolower($this->context)) - { - // Get all of the items to unindex them - $query = clone $this->getStateQuery(); - $this->db->setQuery($query); - $items = $this->db->loadColumn(); - - // Remove each item - foreach ($items as $item) - { - $this->remove($item); - } - } - } - } - - /** - * Method to translate the native content states into states that the - * indexer can use. - * - * @param integer $item The item state. - * @param integer $category The category state. [optional] - * - * @return integer The translated indexer state. - * - * @since 2.5 - */ - protected function translateState($item, $category = null) - { - // If category is present, factor in its states as well - if ($category !== null && $category == 0) - { - $item = 0; - } - - // Translate the state - switch ($item) - { - // Published and archived items only should return a published state - case 1: - case 2: - return 1; - - // All other states should return an unpublished state - default: - return 0; - } - } + /** + * The context is somewhat arbitrary but it must be unique or there will be + * conflicts when managing plugin/indexer state. A good best practice is to + * use the plugin name suffix as the context. For example, if the plugin is + * named 'plgFinderContent', the context could be 'Content'. + * + * @var string + * @since 2.5 + */ + protected $context; + + /** + * The extension name. + * + * @var string + * @since 2.5 + */ + protected $extension; + + /** + * The sublayout to use when rendering the results. + * + * @var string + * @since 2.5 + */ + protected $layout; + + /** + * The mime type of the content the adapter indexes. + * + * @var string + * @since 2.5 + */ + protected $mime; + + /** + * The access level of an item before save. + * + * @var integer + * @since 2.5 + */ + protected $old_access; + + /** + * The access level of a category before save. + * + * @var integer + * @since 2.5 + */ + protected $old_cataccess; + + /** + * The type of content the adapter indexes. + * + * @var string + * @since 2.5 + */ + protected $type_title; + + /** + * The type id of the content. + * + * @var integer + * @since 2.5 + */ + protected $type_id; + + /** + * The database object. + * + * @var DatabaseInterface + * @since 2.5 + */ + protected $db; + + /** + * The table name. + * + * @var string + * @since 2.5 + */ + protected $table; + + /** + * The indexer object. + * + * @var Indexer + * @since 3.0 + */ + protected $indexer; + + /** + * The field the published state is stored in. + * + * @var string + * @since 2.5 + */ + protected $state_field = 'state'; + + /** + * Method to instantiate the indexer adapter. + * + * @param object $subject The object to observe. + * @param array $config An array that holds the plugin configuration. + * + * @since 2.5 + */ + public function __construct(&$subject, $config) + { + // Call the parent constructor. + parent::__construct($subject, $config); + + // Get the type id. + $this->type_id = $this->getTypeId(); + + // Add the content type if it doesn't exist and is set. + if (empty($this->type_id) && !empty($this->type_title)) { + $this->type_id = Helper::addContentType($this->type_title, $this->mime); + } + + // Check for a layout override. + if ($this->params->get('layout')) { + $this->layout = $this->params->get('layout'); + } + + // Get the indexer object + $this->indexer = new Indexer($this->db); + } + + /** + * Method to get the adapter state and push it into the indexer. + * + * @return void + * + * @since 2.5 + * @throws Exception on error. + */ + public function onStartIndex() + { + // Get the indexer state. + $iState = Indexer::getState(); + + // Get the number of content items. + $total = (int) $this->getContentCount(); + + // Add the content count to the total number of items. + $iState->totalItems += $total; + + // Populate the indexer state information for the adapter. + $iState->pluginState[$this->context]['total'] = $total; + $iState->pluginState[$this->context]['offset'] = 0; + + // Set the indexer state. + Indexer::setState($iState); + } + + /** + * Method to prepare for the indexer to be run. This method will often + * be used to include dependencies and things of that nature. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on error. + */ + public function onBeforeIndex() + { + // Get the indexer and adapter state. + $iState = Indexer::getState(); + $aState = $iState->pluginState[$this->context]; + + // Check the progress of the indexer and the adapter. + if ($iState->batchOffset == $iState->batchSize || $aState['offset'] == $aState['total']) { + return true; + } + + // Run the setup method. + return $this->setup(); + } + + /** + * Method to index a batch of content items. This method can be called by + * the indexer many times throughout the indexing process depending on how + * much content is available for indexing. It is important to track the + * progress correctly so we can display it to the user. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on error. + */ + public function onBuildIndex() + { + // Get the indexer and adapter state. + $iState = Indexer::getState(); + $aState = $iState->pluginState[$this->context]; + + // Check the progress of the indexer and the adapter. + if ($iState->batchOffset == $iState->batchSize || $aState['offset'] == $aState['total']) { + return true; + } + + // Get the batch offset and size. + $offset = (int) $aState['offset']; + $limit = (int) ($iState->batchSize - $iState->batchOffset); + + // Get the content items to index. + $items = $this->getItems($offset, $limit); + + // Iterate through the items and index them. + for ($i = 0, $n = count($items); $i < $n; $i++) { + // Index the item. + $this->index($items[$i]); + + // Adjust the offsets. + $offset++; + $iState->batchOffset++; + $iState->totalItems--; + } + + // Update the indexer state. + $aState['offset'] = $offset; + $iState->pluginState[$this->context] = $aState; + Indexer::setState($iState); + + return true; + } + + /** + * Method to remove outdated index entries + * + * @return integer + * + * @since 4.2.0 + */ + public function onFinderGarbageCollection() + { + $db = $this->db; + $type_id = $this->getTypeId(); + + $query = $db->getQuery(true); + $subquery = $db->getQuery(true); + $subquery->select('CONCAT(' . $db->quote($this->getUrl('', $this->extension, $this->layout)) . ', id)') + ->from($db->quoteName($this->table)); + $query->select($db->quoteName('l.link_id')) + ->from($db->quoteName('#__finder_links', 'l')) + ->where($db->quoteName('l.type_id') . ' = ' . $type_id) + ->where($db->quoteName('l.url') . ' LIKE ' . $db->quote($this->getUrl('%', $this->extension, $this->layout))) + ->where($db->quoteName('l.url') . ' NOT IN (' . $subquery . ')'); + $db->setQuery($query); + $items = $db->loadColumn(); + + foreach ($items as $item) { + $this->indexer->remove($item); + } + + return count($items); + } + + /** + * Method to change the value of a content item's property in the links + * table. This is used to synchronize published and access states that + * are changed when not editing an item directly. + * + * @param string $id The ID of the item to change. + * @param string $property The property that is being changed. + * @param integer $value The new value of that property. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function change($id, $property, $value) + { + // Check for a property we know how to handle. + if ($property !== 'state' && $property !== 'access') { + return true; + } + + // Get the URL for the content id. + $item = $this->db->quote($this->getUrl($id, $this->extension, $this->layout)); + + // Update the content items. + $query = $this->db->getQuery(true) + ->update($this->db->quoteName('#__finder_links')) + ->set($this->db->quoteName($property) . ' = ' . (int) $value) + ->where($this->db->quoteName('url') . ' = ' . $item); + $this->db->setQuery($query); + $this->db->execute(); + + return true; + } + + /** + * Method to index an item. + * + * @param Result $item The item to index as a Result object. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + abstract protected function index(Result $item); + + /** + * Method to reindex an item. + * + * @param integer $id The ID of the item to reindex. + * + * @return void + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function reindex($id) + { + // Run the setup method. + $this->setup(); + + // Remove the old item. + $this->remove($id, false); + + // Get the item. + $item = $this->getItem($id); + + // Index the item. + $this->index($item); + + Taxonomy::removeOrphanNodes(); + } + + /** + * Method to remove an item from the index. + * + * @param string $id The ID of the item to remove. + * @param bool $removeTaxonomies Remove empty taxonomies + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function remove($id, $removeTaxonomies = true) + { + // Get the item's URL + $url = $this->db->quote($this->getUrl($id, $this->extension, $this->layout)); + + // Get the link ids for the content items. + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('link_id')) + ->from($this->db->quoteName('#__finder_links')) + ->where($this->db->quoteName('url') . ' = ' . $url); + $this->db->setQuery($query); + $items = $this->db->loadColumn(); + + // Check the items. + if (empty($items)) { + Factory::getApplication()->triggerEvent('onFinderIndexAfterDelete', array($id)); + + return true; + } + + // Remove the items. + foreach ($items as $item) { + $this->indexer->remove($item, $removeTaxonomies); + } + + return true; + } + + /** + * Method to setup the adapter before indexing. + * + * @return boolean True on success, false on failure. + * + * @since 2.5 + * @throws Exception on database error. + */ + abstract protected function setup(); + + /** + * Method to update index data on category access level changes + * + * @param Table $row A Table object + * + * @return void + * + * @since 2.5 + */ + protected function categoryAccessChange($row) + { + $query = clone $this->getStateQuery(); + $query->where('c.id = ' . (int) $row->id); + + // Get the access level. + $this->db->setQuery($query); + $items = $this->db->loadObjectList(); + + // Adjust the access level for each item within the category. + foreach ($items as $item) { + // Set the access level. + $temp = max($item->access, $row->access); + + // Update the item. + $this->change((int) $item->id, 'access', $temp); + } + } + + /** + * Method to update index data on category access level changes + * + * @param array $pks A list of primary key ids of the content that has changed state. + * @param integer $value The value of the state that the content has been changed to. + * + * @return void + * + * @since 2.5 + */ + protected function categoryStateChange($pks, $value) + { + /* + * The item's published state is tied to the category + * published state so we need to look up all published states + * before we change anything. + */ + foreach ($pks as $pk) { + $query = clone $this->getStateQuery(); + $query->where('c.id = ' . (int) $pk); + + // Get the published states. + $this->db->setQuery($query); + $items = $this->db->loadObjectList(); + + // Adjust the state for each item within the category. + foreach ($items as $item) { + // Translate the state. + $temp = $this->translateState($item->state, $value); + + // Update the item. + $this->change($item->id, 'state', $temp); + } + } + } + + /** + * Method to check the existing access level for categories + * + * @param Table $row A Table object + * + * @return void + * + * @since 2.5 + */ + protected function checkCategoryAccess($row) + { + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('access')) + ->from($this->db->quoteName('#__categories')) + ->where($this->db->quoteName('id') . ' = ' . (int) $row->id); + $this->db->setQuery($query); + + // Store the access level to determine if it changes + $this->old_cataccess = $this->db->loadResult(); + } + + /** + * Method to check the existing access level for items + * + * @param Table $row A Table object + * + * @return void + * + * @since 2.5 + */ + protected function checkItemAccess($row) + { + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('access')) + ->from($this->db->quoteName($this->table)) + ->where($this->db->quoteName('id') . ' = ' . (int) $row->id); + $this->db->setQuery($query); + + // Store the access level to determine if it changes + $this->old_access = $this->db->loadResult(); + } + + /** + * Method to get the number of content items available to index. + * + * @return integer The number of content items available to index. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function getContentCount() + { + $return = 0; + + // Get the list query. + $query = $this->getListQuery(); + + // Check if the query is valid. + if (empty($query)) { + return $return; + } + + // Tweak the SQL query to make the total lookup faster. + if ($query instanceof QueryInterface) { + $query = clone $query; + $query->clear('select') + ->select('COUNT(*)') + ->clear('order'); + } + + // Get the total number of content items to index. + $this->db->setQuery($query); + + return (int) $this->db->loadResult(); + } + + /** + * Method to get a content item to index. + * + * @param integer $id The id of the content item. + * + * @return Result A Result object. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function getItem($id) + { + // Get the list query and add the extra WHERE clause. + $query = $this->getListQuery(); + $query->where('a.id = ' . (int) $id); + + // Get the item to index. + $this->db->setQuery($query); + $item = $this->db->loadAssoc(); + + // Convert the item to a result object. + $item = ArrayHelper::toObject((array) $item, Result::class); + + // Set the item type. + $item->type_id = $this->type_id; + + // Set the item layout. + $item->layout = $this->layout; + + return $item; + } + + /** + * Method to get a list of content items to index. + * + * @param integer $offset The list offset. + * @param integer $limit The list limit. + * @param QueryInterface $query A QueryInterface object. [optional] + * + * @return Result[] An array of Result objects. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function getItems($offset, $limit, $query = null) + { + // Get the content items to index. + $this->db->setQuery($this->getListQuery($query)->setLimit($limit, $offset)); + $items = $this->db->loadAssocList(); + + foreach ($items as &$item) { + $item = ArrayHelper::toObject($item, Result::class); + + // Set the item type. + $item->type_id = $this->type_id; + + // Set the mime type. + $item->mime = $this->mime; + + // Set the item layout. + $item->layout = $this->layout; + } + + return $items; + } + + /** + * Method to get the SQL query used to retrieve the list of content items. + * + * @param mixed $query A QueryInterface object. [optional] + * + * @return QueryInterface A database object. + * + * @since 2.5 + */ + protected function getListQuery($query = null) + { + // Check if we can use the supplied SQL query. + return $query instanceof QueryInterface ? $query : $this->db->getQuery(true); + } + + /** + * Method to get the plugin type + * + * @param integer $id The plugin ID + * + * @return string The plugin type + * + * @since 2.5 + */ + protected function getPluginType($id) + { + // Prepare the query + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('element')) + ->from($this->db->quoteName('#__extensions')) + ->where($this->db->quoteName('extension_id') . ' = ' . (int) $id); + $this->db->setQuery($query); + + return $this->db->loadResult(); + } + + /** + * Method to get a SQL query to load the published and access states for + * an article and category. + * + * @return QueryInterface A database object. + * + * @since 2.5 + */ + protected function getStateQuery() + { + $query = $this->db->getQuery(true); + + // Item ID + $query->select('a.id'); + + // Item and category published state + $query->select('a.' . $this->state_field . ' AS state, c.published AS cat_state'); + + // Item and category access levels + $query->select('a.access, c.access AS cat_access') + ->from($this->table . ' AS a') + ->join('LEFT', '#__categories AS c ON c.id = a.catid'); + + return $query; + } + + /** + * Method to get the query clause for getting items to update by time. + * + * @param string $time The modified timestamp. + * + * @return QueryInterface A database object. + * + * @since 2.5 + */ + protected function getUpdateQueryByTime($time) + { + // Build an SQL query based on the modified time. + $query = $this->db->getQuery(true) + ->where('a.modified >= ' . $this->db->quote($time)); + + return $query; + } + + /** + * Method to get the query clause for getting items to update by id. + * + * @param array $ids The ids to load. + * + * @return QueryInterface A database object. + * + * @since 2.5 + */ + protected function getUpdateQueryByIds($ids) + { + // Build an SQL query based on the item ids. + $query = $this->db->getQuery(true) + ->where('a.id IN(' . implode(',', $ids) . ')'); + + return $query; + } + + /** + * Method to get the type id for the adapter content. + * + * @return integer The numeric type id for the content. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function getTypeId() + { + // Get the type id from the database. + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('id')) + ->from($this->db->quoteName('#__finder_types')) + ->where($this->db->quoteName('title') . ' = ' . $this->db->quote($this->type_title)); + $this->db->setQuery($query); + + return (int) $this->db->loadResult(); + } + + /** + * Method to get the URL for the item. The URL is how we look up the link + * in the Finder index. + * + * @param integer $id The id of the item. + * @param string $extension The extension the category is in. + * @param string $view The view for the URL. + * + * @return string The URL of the item. + * + * @since 2.5 + */ + protected function getUrl($id, $extension, $view) + { + return 'index.php?option=' . $extension . '&view=' . $view . '&id=' . $id; + } + + /** + * Method to get the page title of any menu item that is linked to the + * content item, if it exists and is set. + * + * @param string $url The URL of the item. + * + * @return mixed The title on success, null if not found. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function getItemMenuTitle($url) + { + $return = null; + + // Set variables + $user = Factory::getUser(); + $groups = implode(',', $user->getAuthorisedViewLevels()); + + // Build a query to get the menu params. + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('params')) + ->from($this->db->quoteName('#__menu')) + ->where($this->db->quoteName('link') . ' = ' . $this->db->quote($url)) + ->where($this->db->quoteName('published') . ' = 1') + ->where($this->db->quoteName('access') . ' IN (' . $groups . ')'); + + // Get the menu params from the database. + $this->db->setQuery($query); + $params = $this->db->loadResult(); + + // Check the results. + if (empty($params)) { + return $return; + } + + // Instantiate the params. + $params = json_decode($params); + + // Get the page title if it is set. + if (isset($params->page_title) && $params->page_title) { + $return = $params->page_title; + } + + return $return; + } + + /** + * Method to update index data on access level changes + * + * @param Table $row A Table object + * + * @return void + * + * @since 2.5 + */ + protected function itemAccessChange($row) + { + $query = clone $this->getStateQuery(); + $query->where('a.id = ' . (int) $row->id); + + // Get the access level. + $this->db->setQuery($query); + $item = $this->db->loadObject(); + + // Set the access level. + $temp = max($row->access, $item->cat_access); + + // Update the item. + $this->change((int) $row->id, 'access', $temp); + } + + /** + * Method to update index data on published state changes + * + * @param array $pks A list of primary key ids of the content that has changed state. + * @param integer $value The value of the state that the content has been changed to. + * + * @return void + * + * @since 2.5 + */ + protected function itemStateChange($pks, $value) + { + /* + * The item's published state is tied to the category + * published state so we need to look up all published states + * before we change anything. + */ + foreach ($pks as $pk) { + $query = clone $this->getStateQuery(); + $query->where('a.id = ' . (int) $pk); + + // Get the published states. + $this->db->setQuery($query); + $item = $this->db->loadObject(); + + // Translate the state. + $temp = $this->translateState($value, $item->cat_state); + + // Update the item. + $this->change($pk, 'state', $temp); + } + } + + /** + * Method to update index data when a plugin is disabled + * + * @param array $pks A list of primary key ids of the content that has changed state. + * + * @return void + * + * @since 2.5 + */ + protected function pluginDisable($pks) + { + // Since multiple plugins may be disabled at a time, we need to check first + // that we're handling the appropriate one for the context + foreach ($pks as $pk) { + if ($this->getPluginType($pk) == strtolower($this->context)) { + // Get all of the items to unindex them + $query = clone $this->getStateQuery(); + $this->db->setQuery($query); + $items = $this->db->loadColumn(); + + // Remove each item + foreach ($items as $item) { + $this->remove($item); + } + } + } + } + + /** + * Method to translate the native content states into states that the + * indexer can use. + * + * @param integer $item The item state. + * @param integer $category The category state. [optional] + * + * @return integer The translated indexer state. + * + * @since 2.5 + */ + protected function translateState($item, $category = null) + { + // If category is present, factor in its states as well + if ($category !== null && $category == 0) { + $item = 0; + } + + // Translate the state + switch ($item) { + // Published and archived items only should return a published state + case 1: + case 2: + return 1; + + // All other states should return an unpublished state + default: + return 0; + } + } } diff --git a/administrator/components/com_finder/src/Indexer/Helper.php b/administrator/components/com_finder/src/Indexer/Helper.php index 12042f11cabeb..ad79234248ad4 100644 --- a/administrator/components/com_finder/src/Indexer/Helper.php +++ b/administrator/components/com_finder/src/Indexer/Helper.php @@ -1,4 +1,5 @@ parse($input); - } - - /** - * Method to tokenize a text string. - * - * @param string $input The input to tokenize. - * @param string $lang The language of the input. - * @param boolean $phrase Flag to indicate whether input could be a phrase. [optional] - * - * @return Token[] An array of Token objects. - * - * @since 2.5 - */ - public static function tokenize($input, $lang, $phrase = false) - { - static $cache = [], $tuplecount; - static $multilingual; - static $defaultLanguage; - - if (!$tuplecount) - { - $params = ComponentHelper::getParams('com_finder'); - $tuplecount = $params->get('tuplecount', 1); - } - - if (is_null($multilingual)) - { - $multilingual = Multilanguage::isEnabled(); - $config = ComponentHelper::getParams('com_finder'); - - if ($config->get('language_default', '') == '') - { - $defaultLang = '*'; - } - elseif ($config->get('language_default', '') == '-1') - { - $defaultLang = self::getDefaultLanguage(); - } - else - { - $defaultLang = $config->get('language_default'); - } - - /* - * The default language always has the language code '*'. - * In order to not overwrite the language code of the language - * object that we are using, we are cloning it here. - */ - $obj = Language::getInstance($defaultLang); - $defaultLanguage = clone $obj; - $defaultLanguage->language = '*'; - } - - if (!$multilingual || $lang == '*') - { - $language = $defaultLanguage; - } - else - { - $language = Language::getInstance($lang); - } - - if (!isset($cache[$lang])) - { - $cache[$lang] = []; - } - - $tokens = array(); - $terms = $language->tokenise($input); - - // @todo: array_filter removes any number 0's from the terms. Not sure this is entirely intended - $terms = array_filter($terms); - $terms = array_values($terms); - - /* - * If we have to handle the input as a phrase, that means we don't - * tokenize the individual terms and we do not create the two and three - * term combinations. The phrase must contain more than one word! - */ - if ($phrase === true && count($terms) > 1) - { - // Create tokens from the phrase. - $tokens[] = new Token($terms, $language->language, $language->spacer); - } - else - { - // Create tokens from the terms. - for ($i = 0, $n = count($terms); $i < $n; $i++) - { - if (isset($cache[$lang][$terms[$i]])) - { - $tokens[] = $cache[$lang][$terms[$i]]; - } - else - { - $token = new Token($terms[$i], $language->language); - $tokens[] = $token; - $cache[$lang][$terms[$i]] = $token; - } - } - - // Create multi-word phrase tokens from the individual words. - if ($tuplecount > 1) - { - for ($i = 0, $n = count($tokens); $i < $n; $i++) - { - $temp = array($tokens[$i]->term); - - // Create tokens for 2 to $tuplecount length phrases - for ($j = 1; $j < $tuplecount; $j++) - { - if ($i + $j >= $n || !isset($tokens[$i + $j])) - { - break; - } - - $temp[] = $tokens[$i + $j]->term; - $key = implode('::', $temp); - - if (isset($cache[$lang][$key])) - { - $tokens[] = $cache[$lang][$key]; - } - else - { - $token = new Token($temp, $language->language, $language->spacer); - $token->derived = true; - $tokens[] = $token; - $cache[$lang][$key] = $token; - } - } - } - } - } - - // Prevent the cache to fill up the memory - while (count($cache[$lang]) > 1024) - { - /** - * We want to cache the most common words/tokens. At the same time - * we don't want to cache too much. The most common words will also - * be early in the text, so we are dropping all terms/tokens which - * have been cached later. - */ - array_pop($cache[$lang]); - } - - return $tokens; - } - - /** - * Method to get the base word of a token. - * - * @param string $token The token to stem. - * @param string $lang The language of the token. - * - * @return string The root token. - * - * @since 2.5 - */ - public static function stem($token, $lang) - { - static $multilingual; - static $defaultStemmer; - - if (is_null($multilingual)) - { - $multilingual = Multilanguage::isEnabled(); - $config = ComponentHelper::getParams('com_finder'); - - if ($config->get('language_default', '') == '') - { - $defaultStemmer = Language::getInstance('*'); - } - elseif ($config->get('language_default', '') == '-1') - { - $defaultStemmer = Language::getInstance(self::getDefaultLanguage()); - } - else - { - $defaultStemmer = Language::getInstance($config->get('language_default')); - } - } - - if (!$multilingual || $lang == '*') - { - $language = $defaultStemmer; - } - else - { - $language = Language::getInstance($lang); - } - - return $language->stem($token); - } - - /** - * Method to add a content type to the database. - * - * @param string $title The type of content. For example: PDF - * @param string $mime The mime type of the content. For example: PDF [optional] - * - * @return integer The id of the content type. - * - * @since 2.5 - * @throws Exception on database error. - */ - public static function addContentType($title, $mime = null) - { - static $types; - - $db = Factory::getDbo(); - $query = $db->getQuery(true); - - // Check if the types are loaded. - if (empty($types)) - { - // Build the query to get the types. - $query->select('*') - ->from($db->quoteName('#__finder_types')); - - // Get the types. - $db->setQuery($query); - $types = $db->loadObjectList('title'); - } - - // Check if the type already exists. - if (isset($types[$title])) - { - return (int) $types[$title]->id; - } - - // Add the type. - $query->clear() - ->insert($db->quoteName('#__finder_types')) - ->columns(array($db->quoteName('title'), $db->quoteName('mime'))) - ->values($db->quote($title) . ', ' . $db->quote($mime)); - $db->setQuery($query); - $db->execute(); - - // Return the new id. - return (int) $db->insertid(); - } - - /** - * Method to check if a token is common in a language. - * - * @param string $token The token to test. - * @param string $lang The language to reference. - * - * @return boolean True if common, false otherwise. - * - * @since 2.5 - */ - public static function isCommon($token, $lang) - { - static $data, $default, $multilingual; - - if (is_null($multilingual)) - { - $multilingual = Multilanguage::isEnabled(); - $config = ComponentHelper::getParams('com_finder'); - - if ($config->get('language_default', '') == '') - { - $default = '*'; - } - elseif ($config->get('language_default', '') == '-1') - { - $default = self::getPrimaryLanguage(self::getDefaultLanguage()); - } - else - { - $default = self::getPrimaryLanguage($config->get('language_default')); - } - } - - if (!$multilingual || $lang == '*') - { - $lang = $default; - } - - // Load the common tokens for the language if necessary. - if (!isset($data[$lang])) - { - $data[$lang] = self::getCommonWords($lang); - } - - // Check if the token is in the common array. - return in_array($token, $data[$lang], true); - } - - /** - * Method to get an array of common terms for a language. - * - * @param string $lang The language to use. - * - * @return array Array of common terms. - * - * @since 2.5 - * @throws Exception on database error. - */ - public static function getCommonWords($lang) - { - $db = Factory::getDbo(); - - // Create the query to load all the common terms for the language. - $query = $db->getQuery(true) - ->select($db->quoteName('term')) - ->from($db->quoteName('#__finder_terms_common')) - ->where($db->quoteName('language') . ' = ' . $db->quote($lang)); - - // Load all of the common terms for the language. - $db->setQuery($query); - - return $db->loadColumn(); - } - - /** - * Method to get the default language for the site. - * - * @return string The default language string. - * - * @since 2.5 - */ - public static function getDefaultLanguage() - { - static $lang; - - // We need to go to com_languages to get the site default language, it's the best we can guess. - if (empty($lang)) - { - $lang = ComponentHelper::getParams('com_languages')->get('site', 'en-GB'); - } - - return $lang; - } - - /** - * Method to parse a language/locale key and return a simple language string. - * - * @param string $lang The language/locale key. For example: en-GB - * - * @return string The simple language string. For example: en - * - * @since 2.5 - */ - public static function getPrimaryLanguage($lang) - { - static $data; - - // Only parse the identifier if necessary. - if (!isset($data[$lang])) - { - if (is_callable(array('Locale', 'getPrimaryLanguage'))) - { - // Get the language key using the Locale package. - $data[$lang] = \Locale::getPrimaryLanguage($lang); - } - else - { - // Get the language key using string position. - $data[$lang] = StringHelper::substr($lang, 0, StringHelper::strpos($lang, '-')); - } - } - - return $data[$lang]; - } - - /** - * Method to get extra data for a content before being indexed. This is how - * we add Comments, Tags, Labels, etc. that should be available to Finder. - * - * @param Result $item The item to index as a Result object. - * - * @return boolean True on success, false on failure. - * - * @since 2.5 - * @throws Exception on database error. - */ - public static function getContentExtras(Result $item) - { - // Load the finder plugin group. - PluginHelper::importPlugin('finder'); - - Factory::getApplication()->triggerEvent('onPrepareFinderContent', array(&$item)); - - return true; - } - - /** - * Add custom fields for the item to the Result object - * - * @param Result $item Result object to add the custom fields to - * @param string $context Context of the item in the custom fields - * - * @return void - * - * @since 4.2.0 - */ - public static function addCustomFields(Result $item, $context) - { - $obj = new \stdClass; - $obj->id = $item->id; - - $fields = FieldsHelper::getFields($context, $obj, true); - - foreach ($fields as $field) - { - $searchindex = $field->params->get('searchindex', 0); - - // We want to add this field to the search index - if ($searchindex == 1 || $searchindex == 3) - { - $name = 'jsfield_' . $field->name; - $item->$name = $field->value; - $item->addInstruction(Indexer::META_CONTEXT, $name); - } - - // We want to add this field as a taxonomy - if (($searchindex == 2 || $searchindex == 3) && $field->value) - { - $item->addTaxonomy($field->title, $field->value, $field->state, $field->access, $field->language); - } - } - } - - /** - * Method to process content text using the onContentPrepare event trigger. - * - * @param string $text The content to process. - * @param Registry $params The parameters object. [optional] - * @param Result $item The item which get prepared. [optional] - * - * @return string The processed content. - * - * @since 2.5 - */ - public static function prepareContent($text, $params = null, Result $item = null) - { - static $loaded; - - // Load the content plugins if necessary. - if (empty($loaded)) - { - PluginHelper::importPlugin('content'); - $loaded = true; - } - - // Instantiate the parameter object if necessary. - if (!($params instanceof Registry)) - { - $registry = new Registry($params); - $params = $registry; - } - - // Create a mock content object. - $content = Table::getInstance('Content'); - $content->text = $text; - - if ($item) - { - $content->bind((array) $item); - $content->bind($item->getElements()); - } - - if ($item && !empty($item->context)) - { - $content->context = $item->context; - } - - // Fire the onContentPrepare event. - Factory::getApplication()->triggerEvent('onContentPrepare', array('com_finder.indexer', &$content, &$params, 0)); - - return $content->text; - } + /** + * Method to parse input into plain text. + * + * @param string $input The raw input. + * @param string $format The format of the input. [optional] + * + * @return string The parsed input. + * + * @since 2.5 + * @throws Exception on invalid parser. + */ + public static function parse($input, $format = 'html') + { + // Get a parser for the specified format and parse the input. + return Parser::getInstance($format)->parse($input); + } + + /** + * Method to tokenize a text string. + * + * @param string $input The input to tokenize. + * @param string $lang The language of the input. + * @param boolean $phrase Flag to indicate whether input could be a phrase. [optional] + * + * @return Token[] An array of Token objects. + * + * @since 2.5 + */ + public static function tokenize($input, $lang, $phrase = false) + { + static $cache = [], $tuplecount; + static $multilingual; + static $defaultLanguage; + + if (!$tuplecount) { + $params = ComponentHelper::getParams('com_finder'); + $tuplecount = $params->get('tuplecount', 1); + } + + if (is_null($multilingual)) { + $multilingual = Multilanguage::isEnabled(); + $config = ComponentHelper::getParams('com_finder'); + + if ($config->get('language_default', '') == '') { + $defaultLang = '*'; + } elseif ($config->get('language_default', '') == '-1') { + $defaultLang = self::getDefaultLanguage(); + } else { + $defaultLang = $config->get('language_default'); + } + + /* + * The default language always has the language code '*'. + * In order to not overwrite the language code of the language + * object that we are using, we are cloning it here. + */ + $obj = Language::getInstance($defaultLang); + $defaultLanguage = clone $obj; + $defaultLanguage->language = '*'; + } + + if (!$multilingual || $lang == '*') { + $language = $defaultLanguage; + } else { + $language = Language::getInstance($lang); + } + + if (!isset($cache[$lang])) { + $cache[$lang] = []; + } + + $tokens = array(); + $terms = $language->tokenise($input); + + // @todo: array_filter removes any number 0's from the terms. Not sure this is entirely intended + $terms = array_filter($terms); + $terms = array_values($terms); + + /* + * If we have to handle the input as a phrase, that means we don't + * tokenize the individual terms and we do not create the two and three + * term combinations. The phrase must contain more than one word! + */ + if ($phrase === true && count($terms) > 1) { + // Create tokens from the phrase. + $tokens[] = new Token($terms, $language->language, $language->spacer); + } else { + // Create tokens from the terms. + for ($i = 0, $n = count($terms); $i < $n; $i++) { + if (isset($cache[$lang][$terms[$i]])) { + $tokens[] = $cache[$lang][$terms[$i]]; + } else { + $token = new Token($terms[$i], $language->language); + $tokens[] = $token; + $cache[$lang][$terms[$i]] = $token; + } + } + + // Create multi-word phrase tokens from the individual words. + if ($tuplecount > 1) { + for ($i = 0, $n = count($tokens); $i < $n; $i++) { + $temp = array($tokens[$i]->term); + + // Create tokens for 2 to $tuplecount length phrases + for ($j = 1; $j < $tuplecount; $j++) { + if ($i + $j >= $n || !isset($tokens[$i + $j])) { + break; + } + + $temp[] = $tokens[$i + $j]->term; + $key = implode('::', $temp); + + if (isset($cache[$lang][$key])) { + $tokens[] = $cache[$lang][$key]; + } else { + $token = new Token($temp, $language->language, $language->spacer); + $token->derived = true; + $tokens[] = $token; + $cache[$lang][$key] = $token; + } + } + } + } + } + + // Prevent the cache to fill up the memory + while (count($cache[$lang]) > 1024) { + /** + * We want to cache the most common words/tokens. At the same time + * we don't want to cache too much. The most common words will also + * be early in the text, so we are dropping all terms/tokens which + * have been cached later. + */ + array_pop($cache[$lang]); + } + + return $tokens; + } + + /** + * Method to get the base word of a token. + * + * @param string $token The token to stem. + * @param string $lang The language of the token. + * + * @return string The root token. + * + * @since 2.5 + */ + public static function stem($token, $lang) + { + static $multilingual; + static $defaultStemmer; + + if (is_null($multilingual)) { + $multilingual = Multilanguage::isEnabled(); + $config = ComponentHelper::getParams('com_finder'); + + if ($config->get('language_default', '') == '') { + $defaultStemmer = Language::getInstance('*'); + } elseif ($config->get('language_default', '') == '-1') { + $defaultStemmer = Language::getInstance(self::getDefaultLanguage()); + } else { + $defaultStemmer = Language::getInstance($config->get('language_default')); + } + } + + if (!$multilingual || $lang == '*') { + $language = $defaultStemmer; + } else { + $language = Language::getInstance($lang); + } + + return $language->stem($token); + } + + /** + * Method to add a content type to the database. + * + * @param string $title The type of content. For example: PDF + * @param string $mime The mime type of the content. For example: PDF [optional] + * + * @return integer The id of the content type. + * + * @since 2.5 + * @throws Exception on database error. + */ + public static function addContentType($title, $mime = null) + { + static $types; + + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + // Check if the types are loaded. + if (empty($types)) { + // Build the query to get the types. + $query->select('*') + ->from($db->quoteName('#__finder_types')); + + // Get the types. + $db->setQuery($query); + $types = $db->loadObjectList('title'); + } + + // Check if the type already exists. + if (isset($types[$title])) { + return (int) $types[$title]->id; + } + + // Add the type. + $query->clear() + ->insert($db->quoteName('#__finder_types')) + ->columns(array($db->quoteName('title'), $db->quoteName('mime'))) + ->values($db->quote($title) . ', ' . $db->quote($mime)); + $db->setQuery($query); + $db->execute(); + + // Return the new id. + return (int) $db->insertid(); + } + + /** + * Method to check if a token is common in a language. + * + * @param string $token The token to test. + * @param string $lang The language to reference. + * + * @return boolean True if common, false otherwise. + * + * @since 2.5 + */ + public static function isCommon($token, $lang) + { + static $data, $default, $multilingual; + + if (is_null($multilingual)) { + $multilingual = Multilanguage::isEnabled(); + $config = ComponentHelper::getParams('com_finder'); + + if ($config->get('language_default', '') == '') { + $default = '*'; + } elseif ($config->get('language_default', '') == '-1') { + $default = self::getPrimaryLanguage(self::getDefaultLanguage()); + } else { + $default = self::getPrimaryLanguage($config->get('language_default')); + } + } + + if (!$multilingual || $lang == '*') { + $lang = $default; + } + + // Load the common tokens for the language if necessary. + if (!isset($data[$lang])) { + $data[$lang] = self::getCommonWords($lang); + } + + // Check if the token is in the common array. + return in_array($token, $data[$lang], true); + } + + /** + * Method to get an array of common terms for a language. + * + * @param string $lang The language to use. + * + * @return array Array of common terms. + * + * @since 2.5 + * @throws Exception on database error. + */ + public static function getCommonWords($lang) + { + $db = Factory::getDbo(); + + // Create the query to load all the common terms for the language. + $query = $db->getQuery(true) + ->select($db->quoteName('term')) + ->from($db->quoteName('#__finder_terms_common')) + ->where($db->quoteName('language') . ' = ' . $db->quote($lang)); + + // Load all of the common terms for the language. + $db->setQuery($query); + + return $db->loadColumn(); + } + + /** + * Method to get the default language for the site. + * + * @return string The default language string. + * + * @since 2.5 + */ + public static function getDefaultLanguage() + { + static $lang; + + // We need to go to com_languages to get the site default language, it's the best we can guess. + if (empty($lang)) { + $lang = ComponentHelper::getParams('com_languages')->get('site', 'en-GB'); + } + + return $lang; + } + + /** + * Method to parse a language/locale key and return a simple language string. + * + * @param string $lang The language/locale key. For example: en-GB + * + * @return string The simple language string. For example: en + * + * @since 2.5 + */ + public static function getPrimaryLanguage($lang) + { + static $data; + + // Only parse the identifier if necessary. + if (!isset($data[$lang])) { + if (is_callable(array('Locale', 'getPrimaryLanguage'))) { + // Get the language key using the Locale package. + $data[$lang] = \Locale::getPrimaryLanguage($lang); + } else { + // Get the language key using string position. + $data[$lang] = StringHelper::substr($lang, 0, StringHelper::strpos($lang, '-')); + } + } + + return $data[$lang]; + } + + /** + * Method to get extra data for a content before being indexed. This is how + * we add Comments, Tags, Labels, etc. that should be available to Finder. + * + * @param Result $item The item to index as a Result object. + * + * @return boolean True on success, false on failure. + * + * @since 2.5 + * @throws Exception on database error. + */ + public static function getContentExtras(Result $item) + { + // Load the finder plugin group. + PluginHelper::importPlugin('finder'); + + Factory::getApplication()->triggerEvent('onPrepareFinderContent', array(&$item)); + + return true; + } + + /** + * Add custom fields for the item to the Result object + * + * @param Result $item Result object to add the custom fields to + * @param string $context Context of the item in the custom fields + * + * @return void + * + * @since 4.2.0 + */ + public static function addCustomFields(Result $item, $context) + { + $obj = new \stdClass(); + $obj->id = $item->id; + + $fields = FieldsHelper::getFields($context, $obj, true); + + foreach ($fields as $field) { + $searchindex = $field->params->get('searchindex', 0); + + // We want to add this field to the search index + if ($searchindex == 1 || $searchindex == 3) { + $name = 'jsfield_' . $field->name; + $item->$name = $field->value; + $item->addInstruction(Indexer::META_CONTEXT, $name); + } + + // We want to add this field as a taxonomy + if (($searchindex == 2 || $searchindex == 3) && $field->value) { + $item->addTaxonomy($field->title, $field->value, $field->state, $field->access, $field->language); + } + } + } + + /** + * Method to process content text using the onContentPrepare event trigger. + * + * @param string $text The content to process. + * @param Registry $params The parameters object. [optional] + * @param Result $item The item which get prepared. [optional] + * + * @return string The processed content. + * + * @since 2.5 + */ + public static function prepareContent($text, $params = null, Result $item = null) + { + static $loaded; + + // Load the content plugins if necessary. + if (empty($loaded)) { + PluginHelper::importPlugin('content'); + $loaded = true; + } + + // Instantiate the parameter object if necessary. + if (!($params instanceof Registry)) { + $registry = new Registry($params); + $params = $registry; + } + + // Create a mock content object. + $content = Table::getInstance('Content'); + $content->text = $text; + + if ($item) { + $content->bind((array) $item); + $content->bind($item->getElements()); + } + + if ($item && !empty($item->context)) { + $content->context = $item->context; + } + + // Fire the onContentPrepare event. + Factory::getApplication()->triggerEvent('onContentPrepare', array('com_finder.indexer', &$content, &$params, 0)); + + return $content->text; + } } diff --git a/administrator/components/com_finder/src/Indexer/Indexer.php b/administrator/components/com_finder/src/Indexer/Indexer.php index 9625c424b94b2..a5d86d55568c0 100644 --- a/administrator/components/com_finder/src/Indexer/Indexer.php +++ b/administrator/components/com_finder/src/Indexer/Indexer.php @@ -1,4 +1,5 @@ get(DatabaseInterface::class); - } - - $this->db = $db; - - // Set up query template for addTokensToDb - $this->addTokensToDbQueryTemplate = $db->getQuery(true)->insert($db->quoteName('#__finder_tokens')) - ->columns( - array( - $db->quoteName('term'), - $db->quoteName('stem'), - $db->quoteName('common'), - $db->quoteName('phrase'), - $db->quoteName('weight'), - $db->quoteName('context'), - $db->quoteName('language') - ) - ); - } - - /** - * Method to get the indexer state. - * - * @return object The indexer state object. - * - * @since 2.5 - */ - public static function getState() - { - // First, try to load from the internal state. - if ((bool) static::$state) - { - return static::$state; - } - - // If we couldn't load from the internal state, try the session. - $session = Factory::getSession(); - $data = $session->get('_finder.state', null); - - // If the state is empty, load the values for the first time. - if (empty($data)) - { - $data = new CMSObject; - - // Load the default configuration options. - $data->options = ComponentHelper::getParams('com_finder'); - - // Setup the weight lookup information. - $data->weights = array( - self::TITLE_CONTEXT => round($data->options->get('title_multiplier', 1.7), 2), - self::TEXT_CONTEXT => round($data->options->get('text_multiplier', 0.7), 2), - self::META_CONTEXT => round($data->options->get('meta_multiplier', 1.2), 2), - self::PATH_CONTEXT => round($data->options->get('path_multiplier', 2.0), 2), - self::MISC_CONTEXT => round($data->options->get('misc_multiplier', 0.3), 2) - ); - - // Set the current time as the start time. - $data->startTime = Factory::getDate()->toSql(); - - // Set the remaining default values. - $data->batchSize = (int) $data->options->get('batch_size', 50); - $data->batchOffset = 0; - $data->totalItems = 0; - $data->pluginState = array(); - } - - // Setup the profiler if debugging is enabled. - if (Factory::getApplication()->get('debug')) - { - static::$profiler = Profiler::getInstance('FinderIndexer'); - } - - // Set the state. - static::$state = $data; - - return static::$state; - } - - /** - * Method to set the indexer state. - * - * @param CMSObject $data A new indexer state object. - * - * @return boolean True on success, false on failure. - * - * @since 2.5 - */ - public static function setState($data) - { - // Check the state object. - if (empty($data) || !$data instanceof CMSObject) - { - return false; - } - - // Set the new internal state. - static::$state = $data; - - // Set the new session state. - Factory::getSession()->set('_finder.state', $data); - - return true; - } - - /** - * Method to reset the indexer state. - * - * @return void - * - * @since 2.5 - */ - public static function resetState() - { - // Reset the internal state to null. - self::$state = null; - - // Reset the session state to null. - Factory::getSession()->set('_finder.state', null); - } - - /** - * Method to index a content item. - * - * @param Result $item The content item to index. - * @param string $format The format of the content. [optional] - * - * @return integer The ID of the record in the links table. - * - * @since 2.5 - * @throws \Exception on database error. - */ - public function index($item, $format = 'html') - { - // Mark beforeIndexing in the profiler. - static::$profiler ? static::$profiler->mark('beforeIndexing') : null; - $db = $this->db; - $serverType = strtolower($db->getServerType()); - - // Check if the item is in the database. - $query = $db->getQuery(true) - ->select($db->quoteName('link_id') . ', ' . $db->quoteName('md5sum')) - ->from($db->quoteName('#__finder_links')) - ->where($db->quoteName('url') . ' = ' . $db->quote($item->url)); - - // Load the item from the database. - $db->setQuery($query); - $link = $db->loadObject(); - - // Get the indexer state. - $state = static::getState(); - - // Get the signatures of the item. - $curSig = static::getSignature($item); - $oldSig = $link->md5sum ?? null; - - // Get the other item information. - $linkId = empty($link->link_id) ? null : $link->link_id; - $isNew = empty($link->link_id); - - // Check the signatures. If they match, the item is up to date. - if (!$isNew && $curSig == $oldSig) - { - return $linkId; - } - - /* - * If the link already exists, flush all the term maps for the item. - * Maps are stored in 16 tables so we need to iterate through and flush - * each table one at a time. - */ - if (!$isNew) - { - // Flush the maps for the link. - $query->clear() - ->delete($db->quoteName('#__finder_links_terms')) - ->where($db->quoteName('link_id') . ' = ' . (int) $linkId); - $db->setQuery($query); - $db->execute(); - - // Remove the taxonomy maps. - Taxonomy::removeMaps($linkId); - } - - // Mark afterUnmapping in the profiler. - static::$profiler ? static::$profiler->mark('afterUnmapping') : null; - - // Perform cleanup on the item data. - $item->publish_start_date = (int) $item->publish_start_date != 0 ? $item->publish_start_date : null; - $item->publish_end_date = (int) $item->publish_end_date != 0 ? $item->publish_end_date : null; - $item->start_date = (int) $item->start_date != 0 ? $item->start_date : null; - $item->end_date = (int) $item->end_date != 0 ? $item->end_date : null; - - // Prepare the item description. - $item->description = Helper::parse($item->summary ?? ''); - - /* - * Now, we need to enter the item into the links table. If the item - * already exists in the database, we need to use an UPDATE query. - * Otherwise, we need to use an INSERT to get the link id back. - */ - $entry = new \stdClass; - $entry->url = $item->url; - $entry->route = $item->route; - $entry->title = $item->title; - - // We are shortening the description in order to not run into length issues with this field - $entry->description = StringHelper::substr($item->description, 0, 32000); - $entry->indexdate = Factory::getDate()->toSql(); - $entry->state = (int) $item->state; - $entry->access = (int) $item->access; - $entry->language = $item->language; - $entry->type_id = (int) $item->type_id; - $entry->object = ''; - $entry->publish_start_date = $item->publish_start_date; - $entry->publish_end_date = $item->publish_end_date; - $entry->start_date = $item->start_date; - $entry->end_date = $item->end_date; - $entry->list_price = (double) ($item->list_price ?: 0); - $entry->sale_price = (double) ($item->sale_price ?: 0); - - if ($isNew) - { - // Insert the link and get its id. - $db->insertObject('#__finder_links', $entry); - $linkId = (int) $db->insertid(); - } - else - { - // Update the link. - $entry->link_id = $linkId; - $db->updateObject('#__finder_links', $entry, 'link_id'); - } - - // Set up the variables we will need during processing. - $count = 0; - - // Mark afterLinking in the profiler. - static::$profiler ? static::$profiler->mark('afterLinking') : null; - - // Truncate the tokens tables. - $db->truncateTable('#__finder_tokens'); - - // Truncate the tokens aggregate table. - $db->truncateTable('#__finder_tokens_aggregate'); - - /* - * Process the item's content. The items can customize their - * processing instructions to define extra properties to process - * or rearrange how properties are weighted. - */ - foreach ($item->getInstructions() as $group => $properties) - { - // Iterate through the properties of the group. - foreach ($properties as $property) - { - // Check if the property exists in the item. - if (empty($item->$property)) - { - continue; - } - - // Tokenize the property. - if (is_array($item->$property)) - { - // Tokenize an array of content and add it to the database. - foreach ($item->$property as $ip) - { - /* - * If the group is path, we need to a few extra processing - * steps to strip the extension and convert slashes and dashes - * to spaces. - */ - if ($group === static::PATH_CONTEXT) - { - $ip = File::stripExt($ip); - $ip = str_replace(array('/', '-'), ' ', $ip); - } - - // Tokenize a string of content and add it to the database. - $count += $this->tokenizeToDb($ip, $group, $item->language, $format); - - // Check if we're approaching the memory limit of the token table. - if ($count > static::$state->options->get('memory_table_limit', 30000)) - { - $this->toggleTables(false); - } - } - } - else - { - /* - * If the group is path, we need to a few extra processing - * steps to strip the extension and convert slashes and dashes - * to spaces. - */ - if ($group === static::PATH_CONTEXT) - { - $item->$property = File::stripExt($item->$property); - $item->$property = str_replace('/', ' ', $item->$property); - $item->$property = str_replace('-', ' ', $item->$property); - } - - // Tokenize a string of content and add it to the database. - $count += $this->tokenizeToDb($item->$property, $group, $item->language, $format); - - // Check if we're approaching the memory limit of the token table. - if ($count > static::$state->options->get('memory_table_limit', 30000)) - { - $this->toggleTables(false); - } - } - } - } - - /* - * Process the item's taxonomy. The items can customize their - * taxonomy mappings to define extra properties to map. - */ - foreach ($item->getTaxonomy() as $branch => $nodes) - { - // Iterate through the nodes and map them to the branch. - foreach ($nodes as $node) - { - // Add the node to the tree. - if ($node->nested) - { - $nodeId = Taxonomy::addNestedNode($branch, $node->node, $node->state, $node->access, $node->language); - } - else - { - $nodeId = Taxonomy::addNode($branch, $node->title, $node->state, $node->access, $node->language); - } - - // Add the link => node map. - Taxonomy::addMap($linkId, $nodeId); - $node->id = $nodeId; - } - } - - // Mark afterProcessing in the profiler. - static::$profiler ? static::$profiler->mark('afterProcessing') : null; - - /* - * At this point, all of the item's content has been parsed, tokenized - * and inserted into the #__finder_tokens table. Now, we need to - * aggregate all the data into that table into a more usable form. The - * aggregated data will be inserted into #__finder_tokens_aggregate - * table. - */ - $query = 'INSERT INTO ' . $db->quoteName('#__finder_tokens_aggregate') . - ' (' . $db->quoteName('term_id') . - ', ' . $db->quoteName('term') . - ', ' . $db->quoteName('stem') . - ', ' . $db->quoteName('common') . - ', ' . $db->quoteName('phrase') . - ', ' . $db->quoteName('term_weight') . - ', ' . $db->quoteName('context') . - ', ' . $db->quoteName('context_weight') . - ', ' . $db->quoteName('total_weight') . - ', ' . $db->quoteName('language') . ')' . - ' SELECT' . - ' COALESCE(t.term_id, 0), t1.term, t1.stem, t1.common, t1.phrase, t1.weight, t1.context,' . - ' ROUND( t1.weight * COUNT( t2.term ) * %F, 8 ) AS context_weight, 0, t1.language' . - ' FROM (' . - ' SELECT DISTINCT t1.term, t1.stem, t1.common, t1.phrase, t1.weight, t1.context, t1.language' . - ' FROM ' . $db->quoteName('#__finder_tokens') . ' AS t1' . - ' WHERE t1.context = %d' . - ' ) AS t1' . - ' JOIN ' . $db->quoteName('#__finder_tokens') . ' AS t2 ON t2.term = t1.term AND t2.language = t1.language' . - ' LEFT JOIN ' . $db->quoteName('#__finder_terms') . ' AS t ON t.term = t1.term AND t.language = t1.language' . - ' WHERE t2.context = %d' . - ' GROUP BY t1.term, t.term_id, t1.term, t1.stem, t1.common, t1.phrase, t1.weight, t1.context, t1.language' . - ' ORDER BY t1.term DESC'; - - // Iterate through the contexts and aggregate the tokens per context. - foreach ($state->weights as $context => $multiplier) - { - // Run the query to aggregate the tokens for this context.. - $db->setQuery(sprintf($query, $multiplier, $context, $context)); - $db->execute(); - } - - // Mark afterAggregating in the profiler. - static::$profiler ? static::$profiler->mark('afterAggregating') : null; - - /* - * When we pulled down all of the aggregate data, we did a LEFT JOIN - * over the terms table to try to find all the term ids that - * already exist for our tokens. If any of the rows in the aggregate - * table have a term of 0, then no term record exists for that - * term so we need to add it to the terms table. - */ - $db->setQuery( - 'INSERT INTO ' . $db->quoteName('#__finder_terms') . - ' (' . $db->quoteName('term') . - ', ' . $db->quoteName('stem') . - ', ' . $db->quoteName('common') . - ', ' . $db->quoteName('phrase') . - ', ' . $db->quoteName('weight') . - ', ' . $db->quoteName('soundex') . - ', ' . $db->quoteName('language') . ')' . - ' SELECT ta.term, ta.stem, ta.common, ta.phrase, ta.term_weight, SOUNDEX(ta.term), ta.language' . - ' FROM ' . $db->quoteName('#__finder_tokens_aggregate') . ' AS ta' . - ' WHERE ta.term_id = 0' . - ' GROUP BY ta.term, ta.stem, ta.common, ta.phrase, ta.term_weight, SOUNDEX(ta.term), ta.language' - ); - $db->execute(); - - /* - * Now, we just inserted a bunch of new records into the terms table - * so we need to go back and update the aggregate table with all the - * new term ids. - */ - $query = $db->getQuery(true) - ->update($db->quoteName('#__finder_tokens_aggregate', 'ta')) - ->innerJoin($db->quoteName('#__finder_terms', 't'), 't.term = ta.term AND t.language = ta.language') - ->where('ta.term_id = 0'); - - if ($serverType == 'mysql') - { - $query->set($db->quoteName('ta.term_id') . ' = ' . $db->quoteName('t.term_id')); - } - else - { - $query->set($db->quoteName('term_id') . ' = ' . $db->quoteName('t.term_id')); - } - - $db->setQuery($query); - $db->execute(); - - // Mark afterTerms in the profiler. - static::$profiler ? static::$profiler->mark('afterTerms') : null; - - /* - * After we've made sure that all of the terms are in the terms table - * and the aggregate table has the correct term ids, we need to update - * the links counter for each term by one. - */ - $query->clear() - ->update($db->quoteName('#__finder_terms', 't')) - ->innerJoin($db->quoteName('#__finder_tokens_aggregate', 'ta'), 'ta.term_id = t.term_id'); - - if ($serverType == 'mysql') - { - $query->set($db->quoteName('t.links') . ' = t.links + 1'); - } - else - { - $query->set($db->quoteName('links') . ' = t.links + 1'); - } - - $db->setQuery($query); - $db->execute(); - - // Mark afterTerms in the profiler. - static::$profiler ? static::$profiler->mark('afterTerms') : null; - - /* - * At this point, the aggregate table contains a record for each - * term in each context. So, we're going to pull down all of that - * data while grouping the records by term and add all of the - * sub-totals together to arrive at the final total for each token for - * this link. Then, we insert all of that data into the mapping table. - */ - $db->setQuery( - 'INSERT INTO ' . $db->quoteName('#__finder_links_terms') . - ' (' . $db->quoteName('link_id') . - ', ' . $db->quoteName('term_id') . - ', ' . $db->quoteName('weight') . ')' . - ' SELECT ' . (int) $linkId . ', ' . $db->quoteName('term_id') . ',' . - ' ROUND(SUM(' . $db->quoteName('context_weight') . '), 8)' . - ' FROM ' . $db->quoteName('#__finder_tokens_aggregate') . - ' GROUP BY ' . $db->quoteName('term') . ', ' . $db->quoteName('term_id') . - ' ORDER BY ' . $db->quoteName('term') . ' DESC' - ); - $db->execute(); - - // Mark afterMapping in the profiler. - static::$profiler ? static::$profiler->mark('afterMapping') : null; - - // Update the signature. - $object = serialize($item); - $query->clear() - ->update($db->quoteName('#__finder_links')) - ->set($db->quoteName('md5sum') . ' = :md5sum') - ->set($db->quoteName('object') . ' = :object') - ->where($db->quoteName('link_id') . ' = :linkid') - ->bind(':md5sum', $curSig) - ->bind(':object', $object, ParameterType::LARGE_OBJECT) - ->bind(':linkid', $linkId, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - // Mark afterSigning in the profiler. - static::$profiler ? static::$profiler->mark('afterSigning') : null; - - // Truncate the tokens tables. - $db->truncateTable('#__finder_tokens'); - - // Truncate the tokens aggregate table. - $db->truncateTable('#__finder_tokens_aggregate'); - - // Toggle the token tables back to memory tables. - $this->toggleTables(true); - - // Mark afterTruncating in the profiler. - static::$profiler ? static::$profiler->mark('afterTruncating') : null; - - // Trigger a plugin event after indexing - PluginHelper::importPlugin('finder'); - Factory::getApplication()->triggerEvent('onFinderIndexAfterIndex', array($item, $linkId)); - - return $linkId; - } - - /** - * Method to remove a link from the index. - * - * @param integer $linkId The id of the link. - * @param bool $removeTaxonomies Remove empty taxonomies - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - public function remove($linkId, $removeTaxonomies = true) - { - $db = $this->db; - $query = $db->getQuery(true); - $linkId = (int) $linkId; - - // Update the link counts for the terms. - $query->clear() - ->update($db->quoteName('#__finder_terms', 't')) - ->join('INNER', $db->quoteName('#__finder_links_terms', 'm'), $db->quoteName('m.term_id') . ' = ' . $db->quoteName('t.term_id')) - ->set($db->quoteName('links') . ' = ' . $db->quoteName('links') . ' - 1') - ->where($db->quoteName('m.link_id') . ' = :linkid') - ->bind(':linkid', $linkId, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - - // Remove all records from the mapping tables. - $query->clear() - ->delete($db->quoteName('#__finder_links_terms')) - ->where($db->quoteName('link_id') . ' = :linkid') - ->bind(':linkid', $linkId, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - - // Delete all orphaned terms. - $query->clear() - ->delete($db->quoteName('#__finder_terms')) - ->where($db->quoteName('links') . ' <= 0'); - $db->setQuery($query)->execute(); - - // Delete the link from the index. - $query->clear() - ->delete($db->quoteName('#__finder_links')) - ->where($db->quoteName('link_id') . ' = :linkid') - ->bind(':linkid', $linkId, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - - // Remove the taxonomy maps. - Taxonomy::removeMaps($linkId); - - // Remove the orphaned taxonomy nodes. - if ($removeTaxonomies) - { - Taxonomy::removeOrphanNodes(); - } - - PluginHelper::importPlugin('finder'); - Factory::getApplication()->triggerEvent('onFinderIndexAfterDelete', array($linkId)); - - return true; - } - - /** - * Method to optimize the index. We use this method to remove unused terms - * and any other optimizations that might be necessary. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - public function optimize() - { - // Get the database object. - $db = $this->db; - $serverType = strtolower($db->getServerType()); - $query = $db->getQuery(true); - - // Delete all orphaned terms. - $query->delete($db->quoteName('#__finder_terms')) - ->where($db->quoteName('links') . ' <= 0'); - $db->setQuery($query); - $db->execute(); - - // Delete all broken links. (Links missing the object) - $query = $db->getQuery(true) - ->delete('#__finder_links') - ->where($db->quoteName('object') . ' = ' . $db->quote('')); - $db->setQuery($query); - $db->execute(); - - // Delete all orphaned mappings of terms to links - $query2 = $db->getQuery(true) - ->select($db->quoteName('link_id')) - ->from($db->quoteName('#__finder_links')); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__finder_links_terms')) - ->where($db->quoteName('link_id') . ' NOT IN (' . $query2 . ')'); - $db->setQuery($query); - $db->execute(); - - // Delete all orphaned terms - $query2 = $db->getQuery(true) - ->select($db->quoteName('term_id')) - ->from($db->quoteName('#__finder_links_terms')); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__finder_terms')) - ->where($db->quoteName('term_id') . ' NOT IN (' . $query2 . ')'); - $db->setQuery($query); - $db->execute(); - - // Delete all orphaned taxonomies - Taxonomy::removeOrphanMaps(); - Taxonomy::removeOrphanNodes(); - - // Optimize the tables. - $tables = [ - '#__finder_links', - '#__finder_links_terms', - '#__finder_filters', - '#__finder_terms_common', - '#__finder_types', - '#__finder_taxonomy_map', - '#__finder_taxonomy' - ]; - - foreach ($tables as $table) - { - if ($serverType == 'mysql') - { - $db->setQuery('OPTIMIZE TABLE ' . $db->quoteName($table)); - $db->execute(); - } - else - { - $db->setQuery('VACUUM ' . $db->quoteName($table)); - $db->execute(); - $db->setQuery('REINDEX TABLE ' . $db->quoteName($table)); - $db->execute(); - } - } - - return true; - } - - /** - * Method to get a content item's signature. - * - * @param object $item The content item to index. - * - * @return string The content item's signature. - * - * @since 2.5 - */ - protected static function getSignature($item) - { - // Get the indexer state. - $state = static::getState(); - - // Get the relevant configuration variables. - $config = array( - $state->weights, - $state->options->get('stem', 1), - $state->options->get('stemmer', 'porter_en') - ); - - return md5(serialize(array($item, $config))); - } - - /** - * Method to parse input, tokenize it, and then add it to the database. - * - * @param mixed $input String or resource to use as input. A resource input will automatically be chunked to conserve - * memory. Strings will be chunked if longer than 2K in size. - * @param integer $context The context of the input. See context constants. - * @param string $lang The language of the input. - * @param string $format The format of the input. - * - * @return integer The number of tokens extracted from the input. - * - * @since 2.5 - */ - protected function tokenizeToDb($input, $context, $lang, $format) - { - $count = 0; - $buffer = null; - - if (empty($input)) - { - return $count; - } - - // If the input is a resource, batch the process out. - if (is_resource($input)) - { - // Batch the process out to avoid memory limits. - while (!feof($input)) - { - // Read into the buffer. - $buffer .= fread($input, 2048); - - /* - * If we haven't reached the end of the file, seek to the last - * space character and drop whatever is after that to make sure - * we didn't truncate a term while reading the input. - */ - if (!feof($input)) - { - // Find the last space character. - $ls = strrpos($buffer, ' '); - - // Adjust string based on the last space character. - if ($ls) - { - // Truncate the string to the last space character. - $string = substr($buffer, 0, $ls); - - // Adjust the buffer based on the last space for the next iteration and trim. - $buffer = StringHelper::trim(substr($buffer, $ls)); - } - // No space character was found. - else - { - $string = $buffer; - } - } - // We've reached the end of the file, so parse whatever remains. - else - { - $string = $buffer; - } - - // Parse, tokenise and add tokens to the database. - $count = $this->tokenizeToDbShort($string, $context, $lang, $format, $count); - - unset($string); - } - - return $count; - } - - // Parse, tokenise and add tokens to the database. - $count = $this->tokenizeToDbShort($input, $context, $lang, $format, $count); - - return $count; - } - - /** - * Method to parse input, tokenise it, then add the tokens to the database. - * - * @param string $input String to parse, tokenise and add to database. - * @param integer $context The context of the input. See context constants. - * @param string $lang The language of the input. - * @param string $format The format of the input. - * @param integer $count The number of tokens processed so far. - * - * @return integer Cumulative number of tokens extracted from the input so far. - * - * @since 3.7.0 - */ - private function tokenizeToDbShort($input, $context, $lang, $format, $count) - { - // Parse the input. - $input = Helper::parse($input, $format); - - // Check the input. - if (empty($input)) - { - return $count; - } - - // Tokenize the input. - $tokens = Helper::tokenize($input, $lang); - - if (count($tokens) == 0) - { - return $count; - } - - // Add the tokens to the database. - $count += $this->addTokensToDb($tokens, $context); - - // Check if we're approaching the memory limit of the token table. - if ($count > static::$state->options->get('memory_table_limit', 10000)) - { - $this->toggleTables(false); - } - - return $count; - } - - /** - * Method to add a set of tokens to the database. - * - * @param Token[]|Token $tokens An array or single Token object. - * @param mixed $context The context of the tokens. See context constants. [optional] - * - * @return integer The number of tokens inserted into the database. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function addTokensToDb($tokens, $context = '') - { - static $filterCommon, $filterNumeric; - - if (is_null($filterCommon)) - { - $params = ComponentHelper::getParams('com_finder'); - $filterCommon = $params->get('filter_commonwords', false); - $filterNumeric = $params->get('filter_numerics', false); - } - - // Get the database object. - $db = $this->db; - - $query = clone $this->addTokensToDbQueryTemplate; - - // Check if a single FinderIndexerToken object was given and make it to be an array of FinderIndexerToken objects - $tokens = is_array($tokens) ? $tokens : array($tokens); - - // Count the number of token values. - $values = 0; - - // Break into chunks of no more than 128 items - $chunks = array_chunk($tokens, 128); - - foreach ($chunks as $tokens) - { - $query->clear('values'); - - foreach ($tokens as $token) - { - // Database size for a term field - if ($token->length > 75) - { - continue; - } - - if ($filterCommon && $token->common) - { - continue; - } - - if ($filterNumeric && $token->numeric) - { - continue; - } - - $query->values( - $db->quote($token->term) . ', ' - . $db->quote($token->stem) . ', ' - . (int) $token->common . ', ' - . (int) $token->phrase . ', ' - . $db->quote($token->weight) . ', ' - . (int) $context . ', ' - . $db->quote($token->language) - ); - ++$values; - } - - // Only execute the query if there are tokens to insert - if ($query->values !== null) - { - $db->setQuery($query)->execute(); - } - - // Check if we're approaching the memory limit of the token table. - if ($values > static::$state->options->get('memory_table_limit', 10000)) - { - $this->toggleTables(false); - } - } - - return $values; - } - - /** - * Method to switch the token tables from Memory tables to Disk tables - * when they are close to running out of memory. - * Since this is not supported/implemented in all DB-drivers, the default is a stub method, which simply returns true. - * - * @param boolean $memory Flag to control how they should be toggled. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function toggleTables($memory) - { - if (strtolower($this->db->getServerType()) != 'mysql') - { - return true; - } - - static $state; - - // Get the database adapter. - $db = $this->db; - - // Check if we are setting the tables to the Memory engine. - if ($memory === true && $state !== true) - { - // Set the tokens table to Memory. - $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens') . ' ENGINE = MEMORY'); - $db->execute(); - - // Set the tokens aggregate table to Memory. - $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens_aggregate') . ' ENGINE = MEMORY'); - $db->execute(); - - // Set the internal state. - $state = $memory; - } - // We must be setting the tables to the InnoDB engine. - elseif ($memory === false && $state !== false) - { - // Set the tokens table to InnoDB. - $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens') . ' ENGINE = INNODB'); - $db->execute(); - - // Set the tokens aggregate table to InnoDB. - $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens_aggregate') . ' ENGINE = INNODB'); - $db->execute(); - - // Set the internal state. - $state = $memory; - } - - return true; - } + /** + * The title context identifier. + * + * @var integer + * @since 2.5 + */ + public const TITLE_CONTEXT = 1; + + /** + * The text context identifier. + * + * @var integer + * @since 2.5 + */ + public const TEXT_CONTEXT = 2; + + /** + * The meta context identifier. + * + * @var integer + * @since 2.5 + */ + public const META_CONTEXT = 3; + + /** + * The path context identifier. + * + * @var integer + * @since 2.5 + */ + public const PATH_CONTEXT = 4; + + /** + * The misc context identifier. + * + * @var integer + * @since 2.5 + */ + public const MISC_CONTEXT = 5; + + /** + * The indexer state object. + * + * @var CMSObject + * @since 2.5 + */ + public static $state; + + /** + * The indexer profiler object. + * + * @var Profiler + * @since 2.5 + */ + public static $profiler; + + /** + * Database driver cache. + * + * @var \Joomla\Database\DatabaseDriver + * @since 3.8.0 + */ + protected $db; + + /** + * Reusable Query Template. To be used with clone. + * + * @var QueryInterface + * @since 3.8.0 + */ + protected $addTokensToDbQueryTemplate; + + /** + * Indexer constructor. + * + * @param DatabaseInterface $db The database + * + * @since 3.8.0 + */ + public function __construct(DatabaseInterface $db = null) + { + if ($db === null) { + @trigger_error(sprintf('Database will be mandatory in 5.0.'), E_USER_DEPRECATED); + $db = Factory::getContainer()->get(DatabaseInterface::class); + } + + $this->db = $db; + + // Set up query template for addTokensToDb + $this->addTokensToDbQueryTemplate = $db->getQuery(true)->insert($db->quoteName('#__finder_tokens')) + ->columns( + array( + $db->quoteName('term'), + $db->quoteName('stem'), + $db->quoteName('common'), + $db->quoteName('phrase'), + $db->quoteName('weight'), + $db->quoteName('context'), + $db->quoteName('language') + ) + ); + } + + /** + * Method to get the indexer state. + * + * @return object The indexer state object. + * + * @since 2.5 + */ + public static function getState() + { + // First, try to load from the internal state. + if ((bool) static::$state) { + return static::$state; + } + + // If we couldn't load from the internal state, try the session. + $session = Factory::getSession(); + $data = $session->get('_finder.state', null); + + // If the state is empty, load the values for the first time. + if (empty($data)) { + $data = new CMSObject(); + + // Load the default configuration options. + $data->options = ComponentHelper::getParams('com_finder'); + + // Setup the weight lookup information. + $data->weights = array( + self::TITLE_CONTEXT => round($data->options->get('title_multiplier', 1.7), 2), + self::TEXT_CONTEXT => round($data->options->get('text_multiplier', 0.7), 2), + self::META_CONTEXT => round($data->options->get('meta_multiplier', 1.2), 2), + self::PATH_CONTEXT => round($data->options->get('path_multiplier', 2.0), 2), + self::MISC_CONTEXT => round($data->options->get('misc_multiplier', 0.3), 2) + ); + + // Set the current time as the start time. + $data->startTime = Factory::getDate()->toSql(); + + // Set the remaining default values. + $data->batchSize = (int) $data->options->get('batch_size', 50); + $data->batchOffset = 0; + $data->totalItems = 0; + $data->pluginState = array(); + } + + // Setup the profiler if debugging is enabled. + if (Factory::getApplication()->get('debug')) { + static::$profiler = Profiler::getInstance('FinderIndexer'); + } + + // Set the state. + static::$state = $data; + + return static::$state; + } + + /** + * Method to set the indexer state. + * + * @param CMSObject $data A new indexer state object. + * + * @return boolean True on success, false on failure. + * + * @since 2.5 + */ + public static function setState($data) + { + // Check the state object. + if (empty($data) || !$data instanceof CMSObject) { + return false; + } + + // Set the new internal state. + static::$state = $data; + + // Set the new session state. + Factory::getSession()->set('_finder.state', $data); + + return true; + } + + /** + * Method to reset the indexer state. + * + * @return void + * + * @since 2.5 + */ + public static function resetState() + { + // Reset the internal state to null. + self::$state = null; + + // Reset the session state to null. + Factory::getSession()->set('_finder.state', null); + } + + /** + * Method to index a content item. + * + * @param Result $item The content item to index. + * @param string $format The format of the content. [optional] + * + * @return integer The ID of the record in the links table. + * + * @since 2.5 + * @throws \Exception on database error. + */ + public function index($item, $format = 'html') + { + // Mark beforeIndexing in the profiler. + static::$profiler ? static::$profiler->mark('beforeIndexing') : null; + $db = $this->db; + $serverType = strtolower($db->getServerType()); + + // Check if the item is in the database. + $query = $db->getQuery(true) + ->select($db->quoteName('link_id') . ', ' . $db->quoteName('md5sum')) + ->from($db->quoteName('#__finder_links')) + ->where($db->quoteName('url') . ' = ' . $db->quote($item->url)); + + // Load the item from the database. + $db->setQuery($query); + $link = $db->loadObject(); + + // Get the indexer state. + $state = static::getState(); + + // Get the signatures of the item. + $curSig = static::getSignature($item); + $oldSig = $link->md5sum ?? null; + + // Get the other item information. + $linkId = empty($link->link_id) ? null : $link->link_id; + $isNew = empty($link->link_id); + + // Check the signatures. If they match, the item is up to date. + if (!$isNew && $curSig == $oldSig) { + return $linkId; + } + + /* + * If the link already exists, flush all the term maps for the item. + * Maps are stored in 16 tables so we need to iterate through and flush + * each table one at a time. + */ + if (!$isNew) { + // Flush the maps for the link. + $query->clear() + ->delete($db->quoteName('#__finder_links_terms')) + ->where($db->quoteName('link_id') . ' = ' . (int) $linkId); + $db->setQuery($query); + $db->execute(); + + // Remove the taxonomy maps. + Taxonomy::removeMaps($linkId); + } + + // Mark afterUnmapping in the profiler. + static::$profiler ? static::$profiler->mark('afterUnmapping') : null; + + // Perform cleanup on the item data. + $item->publish_start_date = (int) $item->publish_start_date != 0 ? $item->publish_start_date : null; + $item->publish_end_date = (int) $item->publish_end_date != 0 ? $item->publish_end_date : null; + $item->start_date = (int) $item->start_date != 0 ? $item->start_date : null; + $item->end_date = (int) $item->end_date != 0 ? $item->end_date : null; + + // Prepare the item description. + $item->description = Helper::parse($item->summary ?? ''); + + /* + * Now, we need to enter the item into the links table. If the item + * already exists in the database, we need to use an UPDATE query. + * Otherwise, we need to use an INSERT to get the link id back. + */ + $entry = new \stdClass(); + $entry->url = $item->url; + $entry->route = $item->route; + $entry->title = $item->title; + + // We are shortening the description in order to not run into length issues with this field + $entry->description = StringHelper::substr($item->description, 0, 32000); + $entry->indexdate = Factory::getDate()->toSql(); + $entry->state = (int) $item->state; + $entry->access = (int) $item->access; + $entry->language = $item->language; + $entry->type_id = (int) $item->type_id; + $entry->object = ''; + $entry->publish_start_date = $item->publish_start_date; + $entry->publish_end_date = $item->publish_end_date; + $entry->start_date = $item->start_date; + $entry->end_date = $item->end_date; + $entry->list_price = (double) ($item->list_price ?: 0); + $entry->sale_price = (double) ($item->sale_price ?: 0); + + if ($isNew) { + // Insert the link and get its id. + $db->insertObject('#__finder_links', $entry); + $linkId = (int) $db->insertid(); + } else { + // Update the link. + $entry->link_id = $linkId; + $db->updateObject('#__finder_links', $entry, 'link_id'); + } + + // Set up the variables we will need during processing. + $count = 0; + + // Mark afterLinking in the profiler. + static::$profiler ? static::$profiler->mark('afterLinking') : null; + + // Truncate the tokens tables. + $db->truncateTable('#__finder_tokens'); + + // Truncate the tokens aggregate table. + $db->truncateTable('#__finder_tokens_aggregate'); + + /* + * Process the item's content. The items can customize their + * processing instructions to define extra properties to process + * or rearrange how properties are weighted. + */ + foreach ($item->getInstructions() as $group => $properties) { + // Iterate through the properties of the group. + foreach ($properties as $property) { + // Check if the property exists in the item. + if (empty($item->$property)) { + continue; + } + + // Tokenize the property. + if (is_array($item->$property)) { + // Tokenize an array of content and add it to the database. + foreach ($item->$property as $ip) { + /* + * If the group is path, we need to a few extra processing + * steps to strip the extension and convert slashes and dashes + * to spaces. + */ + if ($group === static::PATH_CONTEXT) { + $ip = File::stripExt($ip); + $ip = str_replace(array('/', '-'), ' ', $ip); + } + + // Tokenize a string of content and add it to the database. + $count += $this->tokenizeToDb($ip, $group, $item->language, $format); + + // Check if we're approaching the memory limit of the token table. + if ($count > static::$state->options->get('memory_table_limit', 30000)) { + $this->toggleTables(false); + } + } + } else { + /* + * If the group is path, we need to a few extra processing + * steps to strip the extension and convert slashes and dashes + * to spaces. + */ + if ($group === static::PATH_CONTEXT) { + $item->$property = File::stripExt($item->$property); + $item->$property = str_replace('/', ' ', $item->$property); + $item->$property = str_replace('-', ' ', $item->$property); + } + + // Tokenize a string of content and add it to the database. + $count += $this->tokenizeToDb($item->$property, $group, $item->language, $format); + + // Check if we're approaching the memory limit of the token table. + if ($count > static::$state->options->get('memory_table_limit', 30000)) { + $this->toggleTables(false); + } + } + } + } + + /* + * Process the item's taxonomy. The items can customize their + * taxonomy mappings to define extra properties to map. + */ + foreach ($item->getTaxonomy() as $branch => $nodes) { + // Iterate through the nodes and map them to the branch. + foreach ($nodes as $node) { + // Add the node to the tree. + if ($node->nested) { + $nodeId = Taxonomy::addNestedNode($branch, $node->node, $node->state, $node->access, $node->language); + } else { + $nodeId = Taxonomy::addNode($branch, $node->title, $node->state, $node->access, $node->language); + } + + // Add the link => node map. + Taxonomy::addMap($linkId, $nodeId); + $node->id = $nodeId; + } + } + + // Mark afterProcessing in the profiler. + static::$profiler ? static::$profiler->mark('afterProcessing') : null; + + /* + * At this point, all of the item's content has been parsed, tokenized + * and inserted into the #__finder_tokens table. Now, we need to + * aggregate all the data into that table into a more usable form. The + * aggregated data will be inserted into #__finder_tokens_aggregate + * table. + */ + $query = 'INSERT INTO ' . $db->quoteName('#__finder_tokens_aggregate') . + ' (' . $db->quoteName('term_id') . + ', ' . $db->quoteName('term') . + ', ' . $db->quoteName('stem') . + ', ' . $db->quoteName('common') . + ', ' . $db->quoteName('phrase') . + ', ' . $db->quoteName('term_weight') . + ', ' . $db->quoteName('context') . + ', ' . $db->quoteName('context_weight') . + ', ' . $db->quoteName('total_weight') . + ', ' . $db->quoteName('language') . ')' . + ' SELECT' . + ' COALESCE(t.term_id, 0), t1.term, t1.stem, t1.common, t1.phrase, t1.weight, t1.context,' . + ' ROUND( t1.weight * COUNT( t2.term ) * %F, 8 ) AS context_weight, 0, t1.language' . + ' FROM (' . + ' SELECT DISTINCT t1.term, t1.stem, t1.common, t1.phrase, t1.weight, t1.context, t1.language' . + ' FROM ' . $db->quoteName('#__finder_tokens') . ' AS t1' . + ' WHERE t1.context = %d' . + ' ) AS t1' . + ' JOIN ' . $db->quoteName('#__finder_tokens') . ' AS t2 ON t2.term = t1.term AND t2.language = t1.language' . + ' LEFT JOIN ' . $db->quoteName('#__finder_terms') . ' AS t ON t.term = t1.term AND t.language = t1.language' . + ' WHERE t2.context = %d' . + ' GROUP BY t1.term, t.term_id, t1.term, t1.stem, t1.common, t1.phrase, t1.weight, t1.context, t1.language' . + ' ORDER BY t1.term DESC'; + + // Iterate through the contexts and aggregate the tokens per context. + foreach ($state->weights as $context => $multiplier) { + // Run the query to aggregate the tokens for this context.. + $db->setQuery(sprintf($query, $multiplier, $context, $context)); + $db->execute(); + } + + // Mark afterAggregating in the profiler. + static::$profiler ? static::$profiler->mark('afterAggregating') : null; + + /* + * When we pulled down all of the aggregate data, we did a LEFT JOIN + * over the terms table to try to find all the term ids that + * already exist for our tokens. If any of the rows in the aggregate + * table have a term of 0, then no term record exists for that + * term so we need to add it to the terms table. + */ + $db->setQuery( + 'INSERT INTO ' . $db->quoteName('#__finder_terms') . + ' (' . $db->quoteName('term') . + ', ' . $db->quoteName('stem') . + ', ' . $db->quoteName('common') . + ', ' . $db->quoteName('phrase') . + ', ' . $db->quoteName('weight') . + ', ' . $db->quoteName('soundex') . + ', ' . $db->quoteName('language') . ')' . + ' SELECT ta.term, ta.stem, ta.common, ta.phrase, ta.term_weight, SOUNDEX(ta.term), ta.language' . + ' FROM ' . $db->quoteName('#__finder_tokens_aggregate') . ' AS ta' . + ' WHERE ta.term_id = 0' . + ' GROUP BY ta.term, ta.stem, ta.common, ta.phrase, ta.term_weight, SOUNDEX(ta.term), ta.language' + ); + $db->execute(); + + /* + * Now, we just inserted a bunch of new records into the terms table + * so we need to go back and update the aggregate table with all the + * new term ids. + */ + $query = $db->getQuery(true) + ->update($db->quoteName('#__finder_tokens_aggregate', 'ta')) + ->innerJoin($db->quoteName('#__finder_terms', 't'), 't.term = ta.term AND t.language = ta.language') + ->where('ta.term_id = 0'); + + if ($serverType == 'mysql') { + $query->set($db->quoteName('ta.term_id') . ' = ' . $db->quoteName('t.term_id')); + } else { + $query->set($db->quoteName('term_id') . ' = ' . $db->quoteName('t.term_id')); + } + + $db->setQuery($query); + $db->execute(); + + // Mark afterTerms in the profiler. + static::$profiler ? static::$profiler->mark('afterTerms') : null; + + /* + * After we've made sure that all of the terms are in the terms table + * and the aggregate table has the correct term ids, we need to update + * the links counter for each term by one. + */ + $query->clear() + ->update($db->quoteName('#__finder_terms', 't')) + ->innerJoin($db->quoteName('#__finder_tokens_aggregate', 'ta'), 'ta.term_id = t.term_id'); + + if ($serverType == 'mysql') { + $query->set($db->quoteName('t.links') . ' = t.links + 1'); + } else { + $query->set($db->quoteName('links') . ' = t.links + 1'); + } + + $db->setQuery($query); + $db->execute(); + + // Mark afterTerms in the profiler. + static::$profiler ? static::$profiler->mark('afterTerms') : null; + + /* + * At this point, the aggregate table contains a record for each + * term in each context. So, we're going to pull down all of that + * data while grouping the records by term and add all of the + * sub-totals together to arrive at the final total for each token for + * this link. Then, we insert all of that data into the mapping table. + */ + $db->setQuery( + 'INSERT INTO ' . $db->quoteName('#__finder_links_terms') . + ' (' . $db->quoteName('link_id') . + ', ' . $db->quoteName('term_id') . + ', ' . $db->quoteName('weight') . ')' . + ' SELECT ' . (int) $linkId . ', ' . $db->quoteName('term_id') . ',' . + ' ROUND(SUM(' . $db->quoteName('context_weight') . '), 8)' . + ' FROM ' . $db->quoteName('#__finder_tokens_aggregate') . + ' GROUP BY ' . $db->quoteName('term') . ', ' . $db->quoteName('term_id') . + ' ORDER BY ' . $db->quoteName('term') . ' DESC' + ); + $db->execute(); + + // Mark afterMapping in the profiler. + static::$profiler ? static::$profiler->mark('afterMapping') : null; + + // Update the signature. + $object = serialize($item); + $query->clear() + ->update($db->quoteName('#__finder_links')) + ->set($db->quoteName('md5sum') . ' = :md5sum') + ->set($db->quoteName('object') . ' = :object') + ->where($db->quoteName('link_id') . ' = :linkid') + ->bind(':md5sum', $curSig) + ->bind(':object', $object, ParameterType::LARGE_OBJECT) + ->bind(':linkid', $linkId, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + // Mark afterSigning in the profiler. + static::$profiler ? static::$profiler->mark('afterSigning') : null; + + // Truncate the tokens tables. + $db->truncateTable('#__finder_tokens'); + + // Truncate the tokens aggregate table. + $db->truncateTable('#__finder_tokens_aggregate'); + + // Toggle the token tables back to memory tables. + $this->toggleTables(true); + + // Mark afterTruncating in the profiler. + static::$profiler ? static::$profiler->mark('afterTruncating') : null; + + // Trigger a plugin event after indexing + PluginHelper::importPlugin('finder'); + Factory::getApplication()->triggerEvent('onFinderIndexAfterIndex', array($item, $linkId)); + + return $linkId; + } + + /** + * Method to remove a link from the index. + * + * @param integer $linkId The id of the link. + * @param bool $removeTaxonomies Remove empty taxonomies + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + public function remove($linkId, $removeTaxonomies = true) + { + $db = $this->db; + $query = $db->getQuery(true); + $linkId = (int) $linkId; + + // Update the link counts for the terms. + $query->clear() + ->update($db->quoteName('#__finder_terms', 't')) + ->join('INNER', $db->quoteName('#__finder_links_terms', 'm'), $db->quoteName('m.term_id') . ' = ' . $db->quoteName('t.term_id')) + ->set($db->quoteName('links') . ' = ' . $db->quoteName('links') . ' - 1') + ->where($db->quoteName('m.link_id') . ' = :linkid') + ->bind(':linkid', $linkId, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + + // Remove all records from the mapping tables. + $query->clear() + ->delete($db->quoteName('#__finder_links_terms')) + ->where($db->quoteName('link_id') . ' = :linkid') + ->bind(':linkid', $linkId, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + + // Delete all orphaned terms. + $query->clear() + ->delete($db->quoteName('#__finder_terms')) + ->where($db->quoteName('links') . ' <= 0'); + $db->setQuery($query)->execute(); + + // Delete the link from the index. + $query->clear() + ->delete($db->quoteName('#__finder_links')) + ->where($db->quoteName('link_id') . ' = :linkid') + ->bind(':linkid', $linkId, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + + // Remove the taxonomy maps. + Taxonomy::removeMaps($linkId); + + // Remove the orphaned taxonomy nodes. + if ($removeTaxonomies) { + Taxonomy::removeOrphanNodes(); + } + + PluginHelper::importPlugin('finder'); + Factory::getApplication()->triggerEvent('onFinderIndexAfterDelete', array($linkId)); + + return true; + } + + /** + * Method to optimize the index. We use this method to remove unused terms + * and any other optimizations that might be necessary. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + public function optimize() + { + // Get the database object. + $db = $this->db; + $serverType = strtolower($db->getServerType()); + $query = $db->getQuery(true); + + // Delete all orphaned terms. + $query->delete($db->quoteName('#__finder_terms')) + ->where($db->quoteName('links') . ' <= 0'); + $db->setQuery($query); + $db->execute(); + + // Delete all broken links. (Links missing the object) + $query = $db->getQuery(true) + ->delete('#__finder_links') + ->where($db->quoteName('object') . ' = ' . $db->quote('')); + $db->setQuery($query); + $db->execute(); + + // Delete all orphaned mappings of terms to links + $query2 = $db->getQuery(true) + ->select($db->quoteName('link_id')) + ->from($db->quoteName('#__finder_links')); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__finder_links_terms')) + ->where($db->quoteName('link_id') . ' NOT IN (' . $query2 . ')'); + $db->setQuery($query); + $db->execute(); + + // Delete all orphaned terms + $query2 = $db->getQuery(true) + ->select($db->quoteName('term_id')) + ->from($db->quoteName('#__finder_links_terms')); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__finder_terms')) + ->where($db->quoteName('term_id') . ' NOT IN (' . $query2 . ')'); + $db->setQuery($query); + $db->execute(); + + // Delete all orphaned taxonomies + Taxonomy::removeOrphanMaps(); + Taxonomy::removeOrphanNodes(); + + // Optimize the tables. + $tables = [ + '#__finder_links', + '#__finder_links_terms', + '#__finder_filters', + '#__finder_terms_common', + '#__finder_types', + '#__finder_taxonomy_map', + '#__finder_taxonomy' + ]; + + foreach ($tables as $table) { + if ($serverType == 'mysql') { + $db->setQuery('OPTIMIZE TABLE ' . $db->quoteName($table)); + $db->execute(); + } else { + $db->setQuery('VACUUM ' . $db->quoteName($table)); + $db->execute(); + $db->setQuery('REINDEX TABLE ' . $db->quoteName($table)); + $db->execute(); + } + } + + return true; + } + + /** + * Method to get a content item's signature. + * + * @param object $item The content item to index. + * + * @return string The content item's signature. + * + * @since 2.5 + */ + protected static function getSignature($item) + { + // Get the indexer state. + $state = static::getState(); + + // Get the relevant configuration variables. + $config = array( + $state->weights, + $state->options->get('stem', 1), + $state->options->get('stemmer', 'porter_en') + ); + + return md5(serialize(array($item, $config))); + } + + /** + * Method to parse input, tokenize it, and then add it to the database. + * + * @param mixed $input String or resource to use as input. A resource input will automatically be chunked to conserve + * memory. Strings will be chunked if longer than 2K in size. + * @param integer $context The context of the input. See context constants. + * @param string $lang The language of the input. + * @param string $format The format of the input. + * + * @return integer The number of tokens extracted from the input. + * + * @since 2.5 + */ + protected function tokenizeToDb($input, $context, $lang, $format) + { + $count = 0; + $buffer = null; + + if (empty($input)) { + return $count; + } + + // If the input is a resource, batch the process out. + if (is_resource($input)) { + // Batch the process out to avoid memory limits. + while (!feof($input)) { + // Read into the buffer. + $buffer .= fread($input, 2048); + + /* + * If we haven't reached the end of the file, seek to the last + * space character and drop whatever is after that to make sure + * we didn't truncate a term while reading the input. + */ + if (!feof($input)) { + // Find the last space character. + $ls = strrpos($buffer, ' '); + + // Adjust string based on the last space character. + if ($ls) { + // Truncate the string to the last space character. + $string = substr($buffer, 0, $ls); + + // Adjust the buffer based on the last space for the next iteration and trim. + $buffer = StringHelper::trim(substr($buffer, $ls)); + } else { + // No space character was found. + $string = $buffer; + } + } else { + // We've reached the end of the file, so parse whatever remains. + $string = $buffer; + } + + // Parse, tokenise and add tokens to the database. + $count = $this->tokenizeToDbShort($string, $context, $lang, $format, $count); + + unset($string); + } + + return $count; + } + + // Parse, tokenise and add tokens to the database. + $count = $this->tokenizeToDbShort($input, $context, $lang, $format, $count); + + return $count; + } + + /** + * Method to parse input, tokenise it, then add the tokens to the database. + * + * @param string $input String to parse, tokenise and add to database. + * @param integer $context The context of the input. See context constants. + * @param string $lang The language of the input. + * @param string $format The format of the input. + * @param integer $count The number of tokens processed so far. + * + * @return integer Cumulative number of tokens extracted from the input so far. + * + * @since 3.7.0 + */ + private function tokenizeToDbShort($input, $context, $lang, $format, $count) + { + // Parse the input. + $input = Helper::parse($input, $format); + + // Check the input. + if (empty($input)) { + return $count; + } + + // Tokenize the input. + $tokens = Helper::tokenize($input, $lang); + + if (count($tokens) == 0) { + return $count; + } + + // Add the tokens to the database. + $count += $this->addTokensToDb($tokens, $context); + + // Check if we're approaching the memory limit of the token table. + if ($count > static::$state->options->get('memory_table_limit', 10000)) { + $this->toggleTables(false); + } + + return $count; + } + + /** + * Method to add a set of tokens to the database. + * + * @param Token[]|Token $tokens An array or single Token object. + * @param mixed $context The context of the tokens. See context constants. [optional] + * + * @return integer The number of tokens inserted into the database. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function addTokensToDb($tokens, $context = '') + { + static $filterCommon, $filterNumeric; + + if (is_null($filterCommon)) { + $params = ComponentHelper::getParams('com_finder'); + $filterCommon = $params->get('filter_commonwords', false); + $filterNumeric = $params->get('filter_numerics', false); + } + + // Get the database object. + $db = $this->db; + + $query = clone $this->addTokensToDbQueryTemplate; + + // Check if a single FinderIndexerToken object was given and make it to be an array of FinderIndexerToken objects + $tokens = is_array($tokens) ? $tokens : array($tokens); + + // Count the number of token values. + $values = 0; + + // Break into chunks of no more than 128 items + $chunks = array_chunk($tokens, 128); + + foreach ($chunks as $tokens) { + $query->clear('values'); + + foreach ($tokens as $token) { + // Database size for a term field + if ($token->length > 75) { + continue; + } + + if ($filterCommon && $token->common) { + continue; + } + + if ($filterNumeric && $token->numeric) { + continue; + } + + $query->values( + $db->quote($token->term) . ', ' + . $db->quote($token->stem) . ', ' + . (int) $token->common . ', ' + . (int) $token->phrase . ', ' + . $db->quote($token->weight) . ', ' + . (int) $context . ', ' + . $db->quote($token->language) + ); + ++$values; + } + + // Only execute the query if there are tokens to insert + if ($query->values !== null) { + $db->setQuery($query)->execute(); + } + + // Check if we're approaching the memory limit of the token table. + if ($values > static::$state->options->get('memory_table_limit', 10000)) { + $this->toggleTables(false); + } + } + + return $values; + } + + /** + * Method to switch the token tables from Memory tables to Disk tables + * when they are close to running out of memory. + * Since this is not supported/implemented in all DB-drivers, the default is a stub method, which simply returns true. + * + * @param boolean $memory Flag to control how they should be toggled. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function toggleTables($memory) + { + if (strtolower($this->db->getServerType()) != 'mysql') { + return true; + } + + static $state; + + // Get the database adapter. + $db = $this->db; + + // Check if we are setting the tables to the Memory engine. + if ($memory === true && $state !== true) { + // Set the tokens table to Memory. + $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens') . ' ENGINE = MEMORY'); + $db->execute(); + + // Set the tokens aggregate table to Memory. + $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens_aggregate') . ' ENGINE = MEMORY'); + $db->execute(); + + // Set the internal state. + $state = $memory; + } elseif ($memory === false && $state !== false) { + // We must be setting the tables to the InnoDB engine. + // Set the tokens table to InnoDB. + $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens') . ' ENGINE = INNODB'); + $db->execute(); + + // Set the tokens aggregate table to InnoDB. + $db->setQuery('ALTER TABLE ' . $db->quoteName('#__finder_tokens_aggregate') . ' ENGINE = INNODB'); + $db->execute(); + + // Set the internal state. + $state = $memory; + } + + return true; + } } diff --git a/administrator/components/com_finder/src/Indexer/Language.php b/administrator/components/com_finder/src/Indexer/Language.php index 6f78889df6cf2..4b87c8d7aaec5 100644 --- a/administrator/components/com_finder/src/Indexer/Language.php +++ b/administrator/components/com_finder/src/Indexer/Language.php @@ -1,4 +1,5 @@ language = $locale; - } - - // Use our generic language handler if no language is set - if ($this->language === null) - { - $this->language = '*'; - } - - try - { - $this->stemmer = StemmerFactory::create($this->language); - } - catch (NotFoundException $e) - { - // We don't have a stemmer for the language - } - } - - /** - * Method to get a language support object. - * - * @param string $language The language of the support object. - * - * @return Language A Language instance. - * - * @since 4.0.0 - */ - public static function getInstance($language) - { - if (isset(self::$instances[$language])) - { - return self::$instances[$language]; - } - - $locale = '*'; - - if ($language !== '*') - { - $locale = Helper::getPrimaryLanguage($language); - $class = '\\Joomla\\Component\\Finder\\Administrator\\Indexer\\Language\\' . ucfirst($locale); - - if (class_exists($class)) - { - self::$instances[$language] = new $class; - - return self::$instances[$language]; - } - } - - self::$instances[$language] = new self($locale); - - return self::$instances[$language]; - } - - /** - * Method to tokenise a text string. - * - * @param string $input The input to tokenise. - * - * @return array An array of term strings. - * - * @since 4.0.0 - */ - public function tokenise($input) - { - $quotes = html_entity_decode('‘’'', ENT_QUOTES, 'UTF-8'); - - /* - * Parsing the string input into terms is a multi-step process. - * - * Regexes: - * 1. Remove everything except letters, numbers, quotes, apostrophe, plus, dash, period, and comma. - * 2. Remove plus, dash, period, and comma characters located before letter characters. - * 3. Remove plus, dash, period, and comma characters located after other characters. - * 4. Remove plus, period, and comma characters enclosed in alphabetical characters. Ungreedy. - * 5. Remove orphaned apostrophe, plus, dash, period, and comma characters. - * 6. Remove orphaned quote characters. - * 7. Replace the assorted single quotation marks with the ASCII standard single quotation. - * 8. Remove multiple space characters and replaces with a single space. - */ - $input = StringHelper::strtolower($input); - $input = preg_replace('#[^\pL\pM\pN\p{Pi}\p{Pf}\'+-.,]+#mui', ' ', $input); - $input = preg_replace('#(^|\s)[+-.,]+([\pL\pM]+)#mui', ' $1', $input); - $input = preg_replace('#([\pL\pM\pN]+)[+-.,]+(\s|$)#mui', '$1 ', $input); - $input = preg_replace('#([\pL\pM]+)[+.,]+([\pL\pM]+)#muiU', '$1 $2', $input); - $input = preg_replace('#(^|\s)[\'+-.,]+(\s|$)#mui', ' ', $input); - $input = preg_replace('#(^|\s)[\p{Pi}\p{Pf}]+(\s|$)#mui', ' ', $input); - $input = preg_replace('#[' . $quotes . ']+#mui', '\'', $input); - $input = preg_replace('#\s+#mui', ' ', $input); - $input = trim($input); - - // Explode the normalized string to get the terms. - $terms = explode(' ', $input); - - return $terms; - } - - /** - * Method to stem a token. - * - * @param string $token The token to stem. - * - * @return string The stemmed token. - * - * @since 4.0.0 - */ - public function stem($token) - { - if ($this->stemmer !== null) - { - return $this->stemmer->stem($token); - } - - return $token; - } + /** + * Language support instances container. + * + * @var Language[] + * @since 4.0.0 + */ + protected static $instances = array(); + + /** + * Language locale of the class + * + * @var string + * @since 4.0.0 + */ + public $language; + + /** + * Spacer to use between terms + * + * @var string + * @since 4.0.0 + */ + public $spacer = ' '; + + /** + * The stemmer object. + * + * @var Stemmer + * @since 4.0.0 + */ + protected $stemmer = null; + + /** + * Method to construct the language object. + * + * @since 4.0.0 + */ + public function __construct($locale = null) + { + if ($locale !== null) { + $this->language = $locale; + } + + // Use our generic language handler if no language is set + if ($this->language === null) { + $this->language = '*'; + } + + try { + $this->stemmer = StemmerFactory::create($this->language); + } catch (NotFoundException $e) { + // We don't have a stemmer for the language + } + } + + /** + * Method to get a language support object. + * + * @param string $language The language of the support object. + * + * @return Language A Language instance. + * + * @since 4.0.0 + */ + public static function getInstance($language) + { + if (isset(self::$instances[$language])) { + return self::$instances[$language]; + } + + $locale = '*'; + + if ($language !== '*') { + $locale = Helper::getPrimaryLanguage($language); + $class = '\\Joomla\\Component\\Finder\\Administrator\\Indexer\\Language\\' . ucfirst($locale); + + if (class_exists($class)) { + self::$instances[$language] = new $class(); + + return self::$instances[$language]; + } + } + + self::$instances[$language] = new self($locale); + + return self::$instances[$language]; + } + + /** + * Method to tokenise a text string. + * + * @param string $input The input to tokenise. + * + * @return array An array of term strings. + * + * @since 4.0.0 + */ + public function tokenise($input) + { + $quotes = html_entity_decode('‘’'', ENT_QUOTES, 'UTF-8'); + + /* + * Parsing the string input into terms is a multi-step process. + * + * Regexes: + * 1. Remove everything except letters, numbers, quotes, apostrophe, plus, dash, period, and comma. + * 2. Remove plus, dash, period, and comma characters located before letter characters. + * 3. Remove plus, dash, period, and comma characters located after other characters. + * 4. Remove plus, period, and comma characters enclosed in alphabetical characters. Ungreedy. + * 5. Remove orphaned apostrophe, plus, dash, period, and comma characters. + * 6. Remove orphaned quote characters. + * 7. Replace the assorted single quotation marks with the ASCII standard single quotation. + * 8. Remove multiple space characters and replaces with a single space. + */ + $input = StringHelper::strtolower($input); + $input = preg_replace('#[^\pL\pM\pN\p{Pi}\p{Pf}\'+-.,]+#mui', ' ', $input); + $input = preg_replace('#(^|\s)[+-.,]+([\pL\pM]+)#mui', ' $1', $input); + $input = preg_replace('#([\pL\pM\pN]+)[+-.,]+(\s|$)#mui', '$1 ', $input); + $input = preg_replace('#([\pL\pM]+)[+.,]+([\pL\pM]+)#muiU', '$1 $2', $input); + $input = preg_replace('#(^|\s)[\'+-.,]+(\s|$)#mui', ' ', $input); + $input = preg_replace('#(^|\s)[\p{Pi}\p{Pf}]+(\s|$)#mui', ' ', $input); + $input = preg_replace('#[' . $quotes . ']+#mui', '\'', $input); + $input = preg_replace('#\s+#mui', ' ', $input); + $input = trim($input); + + // Explode the normalized string to get the terms. + $terms = explode(' ', $input); + + return $terms; + } + + /** + * Method to stem a token. + * + * @param string $token The token to stem. + * + * @return string The stemmed token. + * + * @since 4.0.0 + */ + public function stem($token) + { + if ($this->stemmer !== null) { + return $this->stemmer->stem($token); + } + + return $token; + } } diff --git a/administrator/components/com_finder/src/Indexer/Language/El.php b/administrator/components/com_finder/src/Indexer/Language/El.php index 895feecba0c2d..25385cce066db 100644 --- a/administrator/components/com_finder/src/Indexer/Language/El.php +++ b/administrator/components/com_finder/src/Indexer/Language/El.php @@ -1,4 +1,5 @@ toUpperCase($token, $wCase); - - // Stop-word removal - $stop_words = '/^(ΕΚΟ|ΑΒΑ|ΑΓΑ|ΑΓΗ|ΑΓΩ|ΑΔΗ|ΑΔΩ|ΑΕ|ΑΕΙ|ΑΘΩ|ΑΙ|ΑΙΚ|ΑΚΗ|ΑΚΟΜΑ|ΑΚΟΜΗ|ΑΚΡΙΒΩΣ|ΑΛΑ|ΑΛΗΘΕΙΑ|ΑΛΗΘΙΝΑ|ΑΛΛΑΧΟΥ|ΑΛΛΙΩΣ|ΑΛΛΙΩΤΙΚΑ|' - . 'ΑΛΛΟΙΩΣ|ΑΛΛΟΙΩΤΙΚΑ|ΑΛΛΟΤΕ|ΑΛΤ|ΑΛΩ|ΑΜΑ|ΑΜΕ|ΑΜΕΣΑ|ΑΜΕΣΩΣ|ΑΜΩ|ΑΝ|ΑΝΑ|ΑΝΑΜΕΣΑ|ΑΝΑΜΕΤΑΞΥ|ΑΝΕΥ|ΑΝΤΙ|ΑΝΤΙΠΕΡΑ|ΑΝΤΙΣ|ΑΝΩ|ΑΝΩΤΕΡΩ|ΑΞΑΦΝΑ|' - . 'ΑΠ|ΑΠΕΝΑΝΤΙ|ΑΠΟ|ΑΠΟΨΕ|ΑΠΩ|ΑΡΑ|ΑΡΑΓΕ|ΑΡΕ|ΑΡΚ|ΑΡΚΕΤΑ|ΑΡΛ|ΑΡΜ|ΑΡΤ|ΑΡΥ|ΑΡΩ|ΑΣ|ΑΣΑ|ΑΣΟ|ΑΤΑ|ΑΤΕ|ΑΤΗ|ΑΤΙ|ΑΤΜ|ΑΤΟ|ΑΥΡΙΟ|ΑΦΗ|ΑΦΟΤΟΥ|ΑΦΟΥ|' - . 'ΑΧ|ΑΧΕ|ΑΧΟ|ΑΨΑ|ΑΨΕ|ΑΨΗ|ΑΨΥ|ΑΩΕ|ΑΩΟ|ΒΑΝ|ΒΑΤ|ΒΑΧ|ΒΕΑ|ΒΕΒΑΙΟΤΑΤΑ|ΒΗΞ|ΒΙΑ|ΒΙΕ|ΒΙΗ|ΒΙΟ|ΒΟΗ|ΒΟΩ|ΒΡΕ|ΓΑ|ΓΑΒ|ΓΑΡ|ΓΕΝ|ΓΕΣ||ΓΗ|ΓΗΝ|ΓΙ|ΓΙΑ|' - . 'ΓΙΕ|ΓΙΝ|ΓΙΟ|ΓΚΙ|ΓΙΑΤΙ|ΓΚΥ|ΓΟΗ|ΓΟΟ|ΓΡΗΓΟΡΑ|ΓΡΙ|ΓΡΥ|ΓΥΗ|ΓΥΡΩ|ΔΑ|ΔΕ|ΔΕΗ|ΔΕΙ|ΔΕΝ|ΔΕΣ|ΔΗ|ΔΗΘΕΝ|ΔΗΛΑΔΗ|ΔΗΩ|ΔΙ|ΔΙΑ|ΔΙΑΡΚΩΣ|ΔΙΟΛΟΥ|ΔΙΣ|' - . 'ΔΙΧΩΣ|ΔΟΛ|ΔΟΝ|ΔΡΑ|ΔΡΥ|ΔΡΧ|ΔΥΕ|ΔΥΟ|ΔΩ|ΕΑΜ|ΕΑΝ|ΕΑΡ|ΕΘΗ|ΕΙ|ΕΙΔΕΜΗ|ΕΙΘΕ|ΕΙΜΑΙ|ΕΙΜΑΣΤΕ|ΕΙΝΑΙ|ΕΙΣ|ΕΙΣΑΙ|ΕΙΣΑΣΤΕ|ΕΙΣΤΕ|ΕΙΤΕ|ΕΙΧΑ|ΕΙΧΑΜΕ|' - . 'ΕΙΧΑΝ|ΕΙΧΑΤΕ|ΕΙΧΕ|ΕΙΧΕΣ|ΕΚ|ΕΚΕΙ|ΕΛΑ|ΕΛΙ|ΕΜΠ|ΕΝ|ΕΝΤΕΛΩΣ|ΕΝΤΟΣ|ΕΝΤΩΜΕΤΑΞΥ|ΕΝΩ|ΕΞ|ΕΞΑΦΝΑ|ΕΞΙ|ΕΞΙΣΟΥ|ΕΞΩ|ΕΟΚ|ΕΠΑΝΩ|ΕΠΕΙΔΗ|ΕΠΕΙΤΑ|ΕΠΗ|' - . 'ΕΠΙ|ΕΠΙΣΗΣ|ΕΠΟΜΕΝΩΣ|ΕΡΑ|ΕΣ|ΕΣΑΣ|ΕΣΕ|ΕΣΕΙΣ|ΕΣΕΝΑ|ΕΣΗ|ΕΣΤΩ|ΕΣΥ|ΕΣΩ|ΕΤΙ|ΕΤΣΙ|ΕΥ|ΕΥΑ|ΕΥΓΕ|ΕΥΘΥΣ|ΕΥΤΥΧΩΣ|ΕΦΕ|ΕΦΕΞΗΣ|ΕΦΤ|ΕΧΕ|ΕΧΕΙ|' - . 'ΕΧΕΙΣ|ΕΧΕΤΕ|ΕΧΘΕΣ|ΕΧΟΜΕ|ΕΧΟΥΜΕ|ΕΧΟΥΝ|ΕΧΤΕΣ|ΕΧΩ|ΕΩΣ|ΖΕΑ|ΖΕΗ|ΖΕΙ|ΖΕΝ|ΖΗΝ|ΖΩ|Η|ΗΔΗ|ΗΔΥ|ΗΘΗ|ΗΛΟ|ΗΜΙ|ΗΠΑ|ΗΣΑΣΤΕ|ΗΣΟΥΝ|ΗΤΑ|ΗΤΑΝ|ΗΤΑΝΕ|' - . 'ΗΤΟΙ|ΗΤΤΟΝ|ΗΩ|ΘΑ|ΘΥΕ|ΘΩΡ|Ι|ΙΑ|ΙΒΟ|ΙΔΗ|ΙΔΙΩΣ|ΙΕ|ΙΙ|ΙΙΙ|ΙΚΑ|ΙΛΟ|ΙΜΑ|ΙΝΑ|ΙΝΩ|ΙΞΕ|ΙΞΟ|ΙΟ|ΙΟΙ|ΙΣΑ|ΙΣΑΜΕ|ΙΣΕ|ΙΣΗ|ΙΣΙΑ|ΙΣΟ|ΙΣΩΣ|ΙΩΒ|ΙΩΝ|' - . 'ΙΩΣ|ΙΑΝ|ΚΑΘ|ΚΑΘΕ|ΚΑΘΕΤΙ|ΚΑΘΟΛΟΥ|ΚΑΘΩΣ|ΚΑΙ|ΚΑΝ|ΚΑΠΟΤΕ|ΚΑΠΟΥ|ΚΑΠΩΣ|ΚΑΤ|ΚΑΤΑ|ΚΑΤΙ|ΚΑΤΙΤΙ|ΚΑΤΟΠΙΝ|ΚΑΤΩ|ΚΑΩ|ΚΒΟ|ΚΕΑ|ΚΕΙ|ΚΕΝ|ΚΙ|ΚΙΜ|' - . 'ΚΙΟΛΑΣ|ΚΙΤ|ΚΙΧ|ΚΚΕ|ΚΛΙΣΕ|ΚΛΠ|ΚΟΚ|ΚΟΝΤΑ|ΚΟΧ|ΚΤΛ|ΚΥΡ|ΚΥΡΙΩΣ|ΚΩ|ΚΩΝ|ΛΑ|ΛΕΑ|ΛΕΝ|ΛΕΟ|ΛΙΑ|ΛΙΓΑΚΙ|ΛΙΓΟΥΛΑΚΙ|ΛΙΓΟ|ΛΙΓΩΤΕΡΟ|ΛΙΟ|ΛΙΡ|ΛΟΓΩ|' - . 'ΛΟΙΠΑ|ΛΟΙΠΟΝ|ΛΟΣ|ΛΣ|ΛΥΩ|ΜΑ|ΜΑΖΙ|ΜΑΚΑΡΙ|ΜΑΛΙΣΤΑ|ΜΑΛΛΟΝ|ΜΑΝ|ΜΑΞ|ΜΑΣ|ΜΑΤ|ΜΕ|ΜΕΘΑΥΡΙΟ|ΜΕΙ|ΜΕΙΟΝ|ΜΕΛ|ΜΕΛΕΙ|ΜΕΛΛΕΤΑΙ|ΜΕΜΙΑΣ|ΜΕΝ|ΜΕΣ|' - . 'ΜΕΣΑ|ΜΕΤ|ΜΕΤΑ|ΜΕΤΑΞΥ|ΜΕΧΡΙ|ΜΗ|ΜΗΔΕ|ΜΗΝ|ΜΗΠΩΣ|ΜΗΤΕ|ΜΙ|ΜΙΞ|ΜΙΣ|ΜΜΕ|ΜΝΑ|ΜΟΒ|ΜΟΛΙΣ|ΜΟΛΟΝΟΤΙ|ΜΟΝΑΧΑ|ΜΟΝΟΜΙΑΣ|ΜΙΑ|ΜΟΥ|ΜΠΑ|ΜΠΟΡΕΙ|' - . 'ΜΠΟΡΟΥΝ|ΜΠΡΑΒΟ|ΜΠΡΟΣ|ΜΠΩ|ΜΥ|ΜΥΑ|ΜΥΝ|ΝΑ|ΝΑΕ|ΝΑΙ|ΝΑΟ|ΝΔ|ΝΕΐ|ΝΕΑ|ΝΕΕ|ΝΕΟ|ΝΙ|ΝΙΑ|ΝΙΚ|ΝΙΛ|ΝΙΝ|ΝΙΟ|ΝΤΑ|ΝΤΕ|ΝΤΙ|ΝΤΟ|ΝΥΝ|ΝΩΕ|ΝΩΡΙΣ|ΞΑΝΑ|' - . 'ΞΑΦΝΙΚΑ|ΞΕΩ|ΞΙ|Ο|ΟΑ|ΟΑΠ|ΟΔΟ|ΟΕ|ΟΖΟ|ΟΗΕ|ΟΙ|ΟΙΑ|ΟΙΗ|ΟΚΑ|ΟΛΟΓΥΡΑ|ΟΛΟΝΕΝ|ΟΛΟΤΕΛΑ|ΟΛΩΣΔΙΟΛΟΥ|ΟΜΩΣ|ΟΝ|ΟΝΕ|ΟΝΟ|ΟΠΑ|ΟΠΕ|ΟΠΗ|ΟΠΟ|' - . 'ΟΠΟΙΑΔΗΠΟΤΕ|ΟΠΟΙΑΝΔΗΠΟΤΕ|ΟΠΟΙΑΣΔΗΠΟΤΕ|ΟΠΟΙΔΗΠΟΤΕ|ΟΠΟΙΕΣΔΗΠΟΤΕ|ΟΠΟΙΟΔΗΠΟΤΕ|ΟΠΟΙΟΝΔΗΠΟΤΕ|ΟΠΟΙΟΣΔΗΠΟΤΕ|ΟΠΟΙΟΥΔΗΠΟΤΕ|ΟΠΟΙΟΥΣΔΗΠΟΤΕ|' - . 'ΟΠΟΙΩΝΔΗΠΟΤΕ|ΟΠΟΤΕΔΗΠΟΤΕ|ΟΠΟΥ|ΟΠΟΥΔΗΠΟΤΕ|ΟΠΩΣ|ΟΡΑ|ΟΡΕ|ΟΡΗ|ΟΡΟ|ΟΡΦ|ΟΡΩ|ΟΣΑ|ΟΣΑΔΗΠΟΤΕ|ΟΣΕ|ΟΣΕΣΔΗΠΟΤΕ|ΟΣΗΔΗΠΟΤΕ|ΟΣΗΝΔΗΠΟΤΕ|' - . 'ΟΣΗΣΔΗΠΟΤΕ|ΟΣΟΔΗΠΟΤΕ|ΟΣΟΙΔΗΠΟΤΕ|ΟΣΟΝΔΗΠΟΤΕ|ΟΣΟΣΔΗΠΟΤΕ|ΟΣΟΥΔΗΠΟΤΕ|ΟΣΟΥΣΔΗΠΟΤΕ|ΟΣΩΝΔΗΠΟΤΕ|ΟΤΑΝ|ΟΤΕ|ΟΤΙ|ΟΤΙΔΗΠΟΤΕ|ΟΥ|ΟΥΔΕ|ΟΥΚ|ΟΥΣ|' - . 'ΟΥΤΕ|ΟΥΦ|ΟΧΙ|ΟΨΑ|ΟΨΕ|ΟΨΗ|ΟΨΙ|ΟΨΟ|ΠΑ|ΠΑΛΙ|ΠΑΝ|ΠΑΝΤΟΤΕ|ΠΑΝΤΟΥ|ΠΑΝΤΩΣ|ΠΑΠ|ΠΑΡ|ΠΑΡΑ|ΠΕΙ|ΠΕΡ|ΠΕΡΑ|ΠΕΡΙ|ΠΕΡΙΠΟΥ|ΠΕΡΣΙ|ΠΕΡΥΣΙ|ΠΕΣ|ΠΙ|' - . 'ΠΙΑ|ΠΙΘΑΝΟΝ|ΠΙΚ|ΠΙΟ|ΠΙΣΩ|ΠΙΤ|ΠΙΩ|ΠΛΑΙ|ΠΛΕΟΝ|ΠΛΗΝ|ΠΛΩ|ΠΜ|ΠΟΑ|ΠΟΕ|ΠΟΛ|ΠΟΛΥ|ΠΟΠ|ΠΟΤΕ|ΠΟΥ|ΠΟΥΘΕ|ΠΟΥΘΕΝΑ|ΠΡΕΠΕΙ|ΠΡΙ|ΠΡΙΝ|ΠΡΟ|' - . 'ΠΡΟΚΕΙΜΕΝΟΥ|ΠΡΟΚΕΙΤΑΙ|ΠΡΟΠΕΡΣΙ|ΠΡΟΣ|ΠΡΟΤΟΥ|ΠΡΟΧΘΕΣ|ΠΡΟΧΤΕΣ|ΠΡΩΤΥΤΕΡΑ|ΠΥΑ|ΠΥΞ|ΠΥΟ|ΠΥΡ|ΠΧ|ΠΩ|ΠΩΛ|ΠΩΣ|ΡΑ|ΡΑΙ|ΡΑΠ|ΡΑΣ|ΡΕ|ΡΕΑ|ΡΕΕ|ΡΕΙ|' - . 'ΡΗΣ|ΡΘΩ|ΡΙΟ|ΡΟ|ΡΟΐ|ΡΟΕ|ΡΟΖ|ΡΟΗ|ΡΟΘ|ΡΟΙ|ΡΟΚ|ΡΟΛ|ΡΟΝ|ΡΟΣ|ΡΟΥ|ΣΑΙ|ΣΑΝ|ΣΑΟ|ΣΑΣ|ΣΕ|ΣΕΙΣ|ΣΕΚ|ΣΕΞ|ΣΕΡ|ΣΕΤ|ΣΕΦ|ΣΗΜΕΡΑ|ΣΙ|ΣΙΑ|ΣΙΓΑ|ΣΙΚ|' - . 'ΣΙΧ|ΣΚΙ|ΣΟΙ|ΣΟΚ|ΣΟΛ|ΣΟΝ|ΣΟΣ|ΣΟΥ|ΣΡΙ|ΣΤΑ|ΣΤΗ|ΣΤΗΝ|ΣΤΗΣ|ΣΤΙΣ|ΣΤΟ|ΣΤΟΝ|ΣΤΟΥ|ΣΤΟΥΣ|ΣΤΩΝ|ΣΥ|ΣΥΓΧΡΟΝΩΣ|ΣΥΝ|ΣΥΝΑΜΑ|ΣΥΝΕΠΩΣ|ΣΥΝΗΘΩΣ|' - . 'ΣΧΕΔΟΝ|ΣΩΣΤΑ|ΤΑ|ΤΑΔΕ|ΤΑΚ|ΤΑΝ|ΤΑΟ|ΤΑΥ|ΤΑΧΑ|ΤΑΧΑΤΕ|ΤΕ|ΤΕΙ|ΤΕΛ|ΤΕΛΙΚΑ|ΤΕΛΙΚΩΣ|ΤΕΣ|ΤΕΤ|ΤΖΟ|ΤΗ|ΤΗΛ|ΤΗΝ|ΤΗΣ|ΤΙ|ΤΙΚ|ΤΙΜ|ΤΙΠΟΤΑ|ΤΙΠΟΤΕ|' - . 'ΤΙΣ|ΤΝΤ|ΤΟ|ΤΟΙ|ΤΟΚ|ΤΟΜ|ΤΟΝ|ΤΟΠ|ΤΟΣ|ΤΟΣ?Ν|ΤΟΣΑ|ΤΟΣΕΣ|ΤΟΣΗ|ΤΟΣΗΝ|ΤΟΣΗΣ|ΤΟΣΟ|ΤΟΣΟΙ|ΤΟΣΟΝ|ΤΟΣΟΣ|ΤΟΣΟΥ|ΤΟΣΟΥΣ|ΤΟΤΕ|ΤΟΥ|ΤΟΥΛΑΧΙΣΤΟ|' - . 'ΤΟΥΛΑΧΙΣΤΟΝ|ΤΟΥΣ|ΤΣ|ΤΣΑ|ΤΣΕ|ΤΥΧΟΝ|ΤΩ|ΤΩΝ|ΤΩΡΑ|ΥΑΣ|ΥΒΑ|ΥΒΟ|ΥΙΕ|ΥΙΟ|ΥΛΑ|ΥΛΗ|ΥΝΙ|ΥΠ|ΥΠΕΡ|ΥΠΟ|ΥΠΟΨΗ|ΥΠΟΨΙΝ|ΥΣΤΕΡΑ|ΥΦΗ|ΥΨΗ|ΦΑ|ΦΑΐ|ΦΑΕ|' - . 'ΦΑΝ|ΦΑΞ|ΦΑΣ|ΦΑΩ|ΦΕΖ|ΦΕΙ|ΦΕΤΟΣ|ΦΕΥ|ΦΙ|ΦΙΛ|ΦΙΣ|ΦΟΞ|ΦΠΑ|ΦΡΙ|ΧΑ|ΧΑΗ|ΧΑΛ|ΧΑΝ|ΧΑΦ|ΧΕ|ΧΕΙ|ΧΘΕΣ|ΧΙ|ΧΙΑ|ΧΙΛ|ΧΙΟ|ΧΛΜ|ΧΜ|ΧΟΗ|ΧΟΛ|ΧΡΩ|ΧΤΕΣ|' - . 'ΧΩΡΙΣ|ΧΩΡΙΣΤΑ|ΨΕΣ|ΨΗΛΑ|ΨΙ|ΨΙΤ|Ω|ΩΑ|ΩΑΣ|ΩΔΕ|ΩΕΣ|ΩΘΩ|ΩΜΑ|ΩΜΕ|ΩΝ|ΩΟ|ΩΟΝ|ΩΟΥ|ΩΣ|ΩΣΑΝ|ΩΣΗ|ΩΣΟΤΟΥ|ΩΣΠΟΥ|ΩΣΤΕ|ΩΣΤΟΣΟ|ΩΤΑ|ΩΧ|ΩΩΝ)$/'; - - if (preg_match($stop_words, $token)) - { - return $this->toLowerCase($token, $wCase); - } - - // Vowels - $v = '(Α|Ε|Η|Ι|Ο|Υ|Ω)'; - - // Vowels without Y - $v2 = '(Α|Ε|Η|Ι|Ο|Ω)'; - - $test1 = true; - - // Step S1. 14 stems - $re = '/^(.+?)(ΙΖΑ|ΙΖΕΣ|ΙΖΕ|ΙΖΑΜΕ|ΙΖΑΤΕ|ΙΖΑΝ|ΙΖΑΝΕ|ΙΖΩ|ΙΖΕΙΣ|ΙΖΕΙ|ΙΖΟΥΜΕ|ΙΖΕΤΕ|ΙΖΟΥΝ|ΙΖΟΥΝΕ)$/'; - $exceptS1 = '/^(ΑΝΑΜΠΑ|ΕΜΠΑ|ΕΠΑ|ΞΑΝΑΠΑ|ΠΑ|ΠΕΡΙΠΑ|ΑΘΡΟ|ΣΥΝΑΘΡΟ|ΔΑΝΕ)$/'; - $exceptS2 = '/^(ΜΑΡΚ|ΚΟΡΝ|ΑΜΠΑΡ|ΑΡΡ|ΒΑΘΥΡΙ|ΒΑΡΚ|Β|ΒΟΛΒΟΡ|ΓΚΡ|ΓΛΥΚΟΡ|ΓΛΥΚΥΡ|ΙΜΠ|Λ|ΛΟΥ|ΜΑΡ|Μ|ΠΡ|ΜΠΡ|ΠΟΛΥΡ|Π|Ρ|ΠΙΠΕΡΟΡ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . 'I'; - } - - if (preg_match($exceptS2, $token)) - { - $token = $token . 'IΖ'; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S2. 7 stems - $re = '/^(.+?)(ΩΘΗΚΑ|ΩΘΗΚΕΣ|ΩΘΗΚΕ|ΩΘΗΚΑΜΕ|ΩΘΗΚΑΤΕ|ΩΘΗΚΑΝ|ΩΘΗΚΑΝΕ)$/'; - $exceptS1 = '/^(ΑΛ|ΒΙ|ΕΝ|ΥΨ|ΛΙ|ΖΩ|Σ|Χ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . 'ΩΝ'; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S3. 7 stems - $re = '/^(.+?)(ΙΣΑ|ΙΣΕΣ|ΙΣΕ|ΙΣΑΜΕ|ΙΣΑΤΕ|ΙΣΑΝ|ΙΣΑΝΕ)$/'; - $exceptS1 = '/^(ΑΝΑΜΠΑ|ΑΘΡΟ|ΕΜΠΑ|ΕΣΕ|ΕΣΩΚΛΕ|ΕΠΑ|ΞΑΝΑΠΑ|ΕΠΕ|ΠΕΡΙΠΑ|ΑΘΡΟ|ΣΥΝΑΘΡΟ|ΔΑΝΕ|ΚΛΕ|ΧΑΡΤΟΠΑ|ΕΞΑΡΧΑ|ΜΕΤΕΠΕ|ΑΠΟΚΛΕ|ΑΠΕΚΛΕ|ΕΚΛΕ|ΠΕ|ΠΕΡΙΠΑ)$/'; - $exceptS2 = '/^(ΑΝ|ΑΦ|ΓΕ|ΓΙΓΑΝΤΟΑΦ|ΓΚΕ|ΔΗΜΟΚΡΑΤ|ΚΟΜ|ΓΚ|Μ|Π|ΠΟΥΚΑΜ|ΟΛΟ|ΛΑΡ)$/'; - - if ($token == "ΙΣΑ") - { - $token = "ΙΣ"; - - return $token; - } - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . 'Ι'; - } - - if (preg_match($exceptS2, $token)) - { - $token = $token . 'ΙΣ'; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S4. 7 stems - $re = '/^(.+?)(ΙΣΩ|ΙΣΕΙΣ|ΙΣΕΙ|ΙΣΟΥΜΕ|ΙΣΕΤΕ|ΙΣΟΥΝ|ΙΣΟΥΝΕ)$/'; - $exceptS1 = '/^(ΑΝΑΜΠΑ|ΕΜΠΑ|ΕΣΕ|ΕΣΩΚΛΕ|ΕΠΑ|ΞΑΝΑΠΑ|ΕΠΕ|ΠΕΡΙΠΑ|ΑΘΡΟ|ΣΥΝΑΘΡΟ|ΔΑΝΕ|ΚΛΕ|ΧΑΡΤΟΠΑ|ΕΞΑΡΧΑ|ΜΕΤΕΠΕ|ΑΠΟΚΛΕ|ΑΠΕΚΛΕ|ΕΚΛΕ|ΠΕ|ΠΕΡΙΠΑ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . 'Ι'; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S5. 11 stems - $re = '/^(.+?)(ΙΣΤΟΣ|ΙΣΤΟΥ|ΙΣΤΟ|ΙΣΤΕ|ΙΣΤΟΙ|ΙΣΤΩΝ|ΙΣΤΟΥΣ|ΙΣΤΗ|ΙΣΤΗΣ|ΙΣΤΑ|ΙΣΤΕΣ)$/'; - $exceptS1 = '/^(Μ|Π|ΑΠ|ΑΡ|ΗΔ|ΚΤ|ΣΚ|ΣΧ|ΥΨ|ΦΑ|ΧΡ|ΧΤ|ΑΚΤ|ΑΟΡ|ΑΣΧ|ΑΤΑ|ΑΧΝ|ΑΧΤ|ΓΕΜ|ΓΥΡ|ΕΜΠ|ΕΥΠ|ΕΧΘ|ΗΦΑ|ΚΑΘ|ΚΑΚ|ΚΥΛ|ΛΥΓ|ΜΑΚ|ΜΕΓ|ΤΑΧ|ΦΙΛ|ΧΩΡ)$/'; - $exceptS2 = '/^(ΔΑΝΕ|ΣΥΝΑΘΡΟ|ΚΛΕ|ΣΕ|ΕΣΩΚΛΕ|ΑΣΕ|ΠΛΕ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . 'ΙΣΤ'; - } - - if (preg_match($exceptS2, $token)) - { - $token = $token . 'Ι'; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S6. 6 stems - $re = '/^(.+?)(ΙΣΜΟ|ΙΣΜΟΙ|ΙΣΜΟΣ|ΙΣΜΟΥ|ΙΣΜΟΥΣ|ΙΣΜΩΝ)$/'; - $exceptS1 = '/^(ΑΓΝΩΣΤΙΚ|ΑΤΟΜΙΚ|ΓΝΩΣΤΙΚ|ΕΘΝΙΚ|ΕΚΛΕΚΤΙΚ|ΣΚΕΠΤΙΚ|ΤΟΠΙΚ)$/'; - $exceptS2 = '/^(ΣΕ|ΜΕΤΑΣΕ|ΜΙΚΡΟΣΕ|ΕΓΚΛΕ|ΑΠΟΚΛΕ)$/'; - $exceptS3 = '/^(ΔΑΝΕ|ΑΝΤΙΔΑΝΕ)$/'; - $exceptS4 = '/^(ΑΛΕΞΑΝΔΡΙΝ|ΒΥΖΑΝΤΙΝ|ΘΕΑΤΡΙΝ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = str_replace('ΙΚ', "", $token); - } - - if (preg_match($exceptS2, $token)) - { - $token = $token . "ΙΣΜ"; - } - - if (preg_match($exceptS3, $token)) - { - $token = $token . "Ι"; - } - - if (preg_match($exceptS4, $token)) - { - $token = str_replace('ΙΝ', "", $token); - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S7. 4 stems - $re = '/^(.+?)(ΑΡΑΚΙ|ΑΡΑΚΙΑ|ΟΥΔΑΚΙ|ΟΥΔΑΚΙΑ)$/'; - $exceptS1 = '/^(Σ|Χ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . "AΡΑΚ"; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S8. 8 stems - $re = '/^(.+?)(ΑΚΙ|ΑΚΙΑ|ΙΤΣΑ|ΙΤΣΑΣ|ΙΤΣΕΣ|ΙΤΣΩΝ|ΑΡΑΚΙ|ΑΡΑΚΙΑ)$/'; - $exceptS1 = '/^(ΑΝΘΡ|ΒΑΜΒ|ΒΡ|ΚΑΙΜ|ΚΟΝ|ΚΟΡ|ΛΑΒΡ|ΛΟΥΛ|ΜΕΡ|ΜΟΥΣΤ|ΝΑΓΚΑΣ|ΠΛ|Ρ|ΡΥ|Σ|ΣΚ|ΣΟΚ|ΣΠΑΝ|ΤΖ|ΦΑΡΜ|Χ|' - . 'ΚΑΠΑΚ|ΑΛΙΣΦ|ΑΜΒΡ|ΑΝΘΡ|Κ|ΦΥΛ|ΚΑΤΡΑΠ|ΚΛΙΜ|ΜΑΛ|ΣΛΟΒ|Φ|ΣΦ|ΤΣΕΧΟΣΛΟΒ)$/'; - $exceptS2 = '/^(Β|ΒΑΛ|ΓΙΑΝ|ΓΛ|Ζ|ΗΓΟΥΜΕΝ|ΚΑΡΔ|ΚΟΝ|ΜΑΚΡΥΝ|ΝΥΦ|ΠΑΤΕΡ|Π|ΣΚ|ΤΟΣ|ΤΡΙΠΟΛ)$/'; - - // For words like ΠΛΟΥΣΙΟΚΟΡΙΤΣΑ, ΠΑΛΙΟΚΟΡΙΤΣΑ etc - $exceptS3 = '/(ΚΟΡ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . "ΑΚ"; - } - - if (preg_match($exceptS2, $token)) - { - $token = $token . "ΙΤΣ"; - } - - if (preg_match($exceptS3, $token)) - { - $token = $token . "ΙΤΣ"; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S9. 3 stems - $re = '/^(.+?)(ΙΔΙΟ|ΙΔΙΑ|ΙΔΙΩΝ)$/'; - $exceptS1 = '/^(ΑΙΦΝ|ΙΡ|ΟΛΟ|ΨΑΛ)$/'; - $exceptS2 = '/(Ε|ΠΑΙΧΝ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . "ΙΔ"; - } - - if (preg_match($exceptS2, $token)) - { - $token = $token . "ΙΔ"; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step S10. 4 stems - $re = '/^(.+?)(ΙΣΚΟΣ|ΙΣΚΟΥ|ΙΣΚΟ|ΙΣΚΕ)$/'; - $exceptS1 = '/^(Δ|ΙΒ|ΜΗΝ|Ρ|ΦΡΑΓΚ|ΛΥΚ|ΟΒΕΛ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - - if (preg_match($exceptS1, $token)) - { - $token = $token . "ΙΣΚ"; - } - - return $this->toLowerCase($token, $wCase); - } - - // Step 1 - // step1list is used in Step 1. 41 stems - $step1list = Array(); - $step1list["ΦΑΓΙΑ"] = "ΦΑ"; - $step1list["ΦΑΓΙΟΥ"] = "ΦΑ"; - $step1list["ΦΑΓΙΩΝ"] = "ΦΑ"; - $step1list["ΣΚΑΓΙΑ"] = "ΣΚΑ"; - $step1list["ΣΚΑΓΙΟΥ"] = "ΣΚΑ"; - $step1list["ΣΚΑΓΙΩΝ"] = "ΣΚΑ"; - $step1list["ΟΛΟΓΙΟΥ"] = "ΟΛΟ"; - $step1list["ΟΛΟΓΙΑ"] = "ΟΛΟ"; - $step1list["ΟΛΟΓΙΩΝ"] = "ΟΛΟ"; - $step1list["ΣΟΓΙΟΥ"] = "ΣΟ"; - $step1list["ΣΟΓΙΑ"] = "ΣΟ"; - $step1list["ΣΟΓΙΩΝ"] = "ΣΟ"; - $step1list["ΤΑΤΟΓΙΑ"] = "ΤΑΤΟ"; - $step1list["ΤΑΤΟΓΙΟΥ"] = "ΤΑΤΟ"; - $step1list["ΤΑΤΟΓΙΩΝ"] = "ΤΑΤΟ"; - $step1list["ΚΡΕΑΣ"] = "ΚΡΕ"; - $step1list["ΚΡΕΑΤΟΣ"] = "ΚΡΕ"; - $step1list["ΚΡΕΑΤΑ"] = "ΚΡΕ"; - $step1list["ΚΡΕΑΤΩΝ"] = "ΚΡΕ"; - $step1list["ΠΕΡΑΣ"] = "ΠΕΡ"; - $step1list["ΠΕΡΑΤΟΣ"] = "ΠΕΡ"; - - // Added by Spyros. Also at $re in step1 - $step1list["ΠΕΡΑΤΗ"] = "ΠΕΡ"; - $step1list["ΠΕΡΑΤΑ"] = "ΠΕΡ"; - $step1list["ΠΕΡΑΤΩΝ"] = "ΠΕΡ"; - $step1list["ΤΕΡΑΣ"] = "ΤΕΡ"; - $step1list["ΤΕΡΑΤΟΣ"] = "ΤΕΡ"; - $step1list["ΤΕΡΑΤΑ"] = "ΤΕΡ"; - $step1list["ΤΕΡΑΤΩΝ"] = "ΤΕΡ"; - $step1list["ΦΩΣ"] = "ΦΩ"; - $step1list["ΦΩΤΟΣ"] = "ΦΩ"; - $step1list["ΦΩΤΑ"] = "ΦΩ"; - $step1list["ΦΩΤΩΝ"] = "ΦΩ"; - $step1list["ΚΑΘΕΣΤΩΣ"] = "ΚΑΘΕΣΤ"; - $step1list["ΚΑΘΕΣΤΩΤΟΣ"] = "ΚΑΘΕΣΤ"; - $step1list["ΚΑΘΕΣΤΩΤΑ"] = "ΚΑΘΕΣΤ"; - $step1list["ΚΑΘΕΣΤΩΤΩΝ"] = "ΚΑΘΕΣΤ"; - $step1list["ΓΕΓΟΝΟΣ"] = "ΓΕΓΟΝ"; - $step1list["ΓΕΓΟΝΟΤΟΣ"] = "ΓΕΓΟΝ"; - $step1list["ΓΕΓΟΝΟΤΑ"] = "ΓΕΓΟΝ"; - $step1list["ΓΕΓΟΝΟΤΩΝ"] = "ΓΕΓΟΝ"; - - $re = '/(.*)(ΦΑΓΙΑ|ΦΑΓΙΟΥ|ΦΑΓΙΩΝ|ΣΚΑΓΙΑ|ΣΚΑΓΙΟΥ|ΣΚΑΓΙΩΝ|ΟΛΟΓΙΟΥ|ΟΛΟΓΙΑ|ΟΛΟΓΙΩΝ|ΣΟΓΙΟΥ|ΣΟΓΙΑ|ΣΟΓΙΩΝ|ΤΑΤΟΓΙΑ|ΤΑΤΟΓΙΟΥ|ΤΑΤΟΓΙΩΝ|ΚΡΕΑΣ|ΚΡΕΑΤΟΣ|' - . 'ΚΡΕΑΤΑ|ΚΡΕΑΤΩΝ|ΠΕΡΑΣ|ΠΕΡΑΤΟΣ|ΠΕΡΑΤΗ|ΠΕΡΑΤΑ|ΠΕΡΑΤΩΝ|ΤΕΡΑΣ|ΤΕΡΑΤΟΣ|ΤΕΡΑΤΑ|ΤΕΡΑΤΩΝ|ΦΩΣ|ΦΩΤΟΣ|ΦΩΤΑ|ΦΩΤΩΝ|ΚΑΘΕΣΤΩΣ|ΚΑΘΕΣΤΩΤΟΣ|' - . 'ΚΑΘΕΣΤΩΤΑ|ΚΑΘΕΣΤΩΤΩΝ|ΓΕΓΟΝΟΣ|ΓΕΓΟΝΟΤΟΣ|ΓΕΓΟΝΟΤΑ|ΓΕΓΟΝΟΤΩΝ)$/'; - - if (preg_match($re, $token, $match)) - { - $stem = $match[1]; - $suffix = $match[2]; - $token = $stem . (array_key_exists($suffix, $step1list) ? $step1list[$suffix] : ''); - $test1 = false; - } - - // Step 2a. 2 stems - $re = '/^(.+?)(ΑΔΕΣ|ΑΔΩΝ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1]; - $re = '/(ΟΚ|ΜΑΜ|ΜΑΝ|ΜΠΑΜΠ|ΠΑΤΕΡ|ΓΙΑΓΙ|ΝΤΑΝΤ|ΚΥΡ|ΘΕΙ|ΠΕΘΕΡ)$/'; - - if (!preg_match($re, $token)) - { - $token = $token . "ΑΔ"; - } - } - - // Step 2b. 2 stems - $re = '/^(.+?)(ΕΔΕΣ|ΕΔΩΝ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $exept2 = '/(ΟΠ|ΙΠ|ΕΜΠ|ΥΠ|ΓΗΠ|ΔΑΠ|ΚΡΑΣΠ|ΜΙΛ)$/'; - - if (preg_match($exept2, $token)) - { - $token = $token . 'ΕΔ'; - } - } - - // Step 2c - $re = '/^(.+?)(ΟΥΔΕΣ|ΟΥΔΩΝ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - - $exept3 = '/(ΑΡΚ|ΚΑΛΙΑΚ|ΠΕΤΑΛ|ΛΙΧ|ΠΛΕΞ|ΣΚ|Σ|ΦΛ|ΦΡ|ΒΕΛ|ΛΟΥΛ|ΧΝ|ΣΠ|ΤΡΑΓ|ΦΕ)$/'; - - if (preg_match($exept3, $token)) - { - $token = $token . 'ΟΥΔ'; - } - } - - // Step 2d - $re = '/^(.+?)(ΕΩΣ|ΕΩΝ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept4 = '/^(Θ|Δ|ΕΛ|ΓΑΛ|Ν|Π|ΙΔ|ΠΑΡ)$/'; - - if (preg_match($exept4, $token)) - { - $token = $token . 'Ε'; - } - } - - // Step 3 - $re = '/^(.+?)(ΙΑ|ΙΟΥ|ΙΩΝ)$/'; - - if (preg_match($re, $token, $fp)) - { - $stem = $fp[1]; - $token = $stem; - $re = '/' . $v . '$/'; - $test1 = false; - - if (preg_match($re, $token)) - { - $token = $stem . 'Ι'; - } - } - - // Step 4 - $re = '/^(.+?)(ΙΚΑ|ΙΚΟ|ΙΚΟΥ|ΙΚΩΝ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $re = '/' . $v . '$/'; - $exept5 = '/^(ΑΛ|ΑΔ|ΕΝΔ|ΑΜΑΝ|ΑΜΜΟΧΑΛ|ΗΘ|ΑΝΗΘ|ΑΝΤΙΔ|ΦΥΣ|ΒΡΩΜ|ΓΕΡ|ΕΞΩΔ|ΚΑΛΠ|ΚΑΛΛΙΝ|ΚΑΤΑΔ|ΜΟΥΛ|ΜΠΑΝ|ΜΠΑΓΙΑΤ|ΜΠΟΛ|ΜΠΟΣ|ΝΙΤ|ΞΙΚ|ΣΥΝΟΜΗΛ|ΠΕΤΣ|' - . 'ΠΙΤΣ|ΠΙΚΑΝΤ|ΠΛΙΑΤΣ|ΠΟΣΤΕΛΝ|ΠΡΩΤΟΔ|ΣΕΡΤ|ΣΥΝΑΔ|ΤΣΑΜ|ΥΠΟΔ|ΦΙΛΟΝ|ΦΥΛΟΔ|ΧΑΣ)$/'; - - if (preg_match($re, $token) || preg_match($exept5, $token)) - { - $token = $token . 'ΙΚ'; - } - } - - // Step 5a - $re = '/^(.+?)(ΑΜΕ)$/'; - $re2 = '/^(.+?)(ΑΓΑΜΕ|ΗΣΑΜΕ|ΟΥΣΑΜΕ|ΗΚΑΜΕ|ΗΘΗΚΑΜΕ)$/'; - - if ($token == "ΑΓΑΜΕ") - { - $token = "ΑΓΑΜ"; - } - - if (preg_match($re2, $token)) - { - preg_match($re2, $token, $match); - $token = $match[1]; - $test1 = false; - } - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept6 = '/^(ΑΝΑΠ|ΑΠΟΘ|ΑΠΟΚ|ΑΠΟΣΤ|ΒΟΥΒ|ΞΕΘ|ΟΥΛ|ΠΕΘ|ΠΙΚΡ|ΠΟΤ|ΣΙΧ|Χ)$/'; - - if (preg_match($exept6, $token)) - { - $token = $token . "ΑΜ"; - } - } - - // Step 5b - $re2 = '/^(.+?)(ΑΝΕ)$/'; - $re3 = '/^(.+?)(ΑΓΑΝΕ|ΗΣΑΝΕ|ΟΥΣΑΝΕ|ΙΟΝΤΑΝΕ|ΙΟΤΑΝΕ|ΙΟΥΝΤΑΝΕ|ΟΝΤΑΝΕ|ΟΤΑΝΕ|ΟΥΝΤΑΝΕ|ΗΚΑΝΕ|ΗΘΗΚΑΝΕ)$/'; - - if (preg_match($re3, $token)) - { - preg_match($re3, $token, $match); - $token = $match[1]; - $test1 = false; - $re3 = '/^(ΤΡ|ΤΣ)$/'; - - if (preg_match($re3, $token)) - { - $token = $token . "ΑΓΑΝ"; - } - } - - if (preg_match($re2, $token)) - { - preg_match($re2, $token, $match); - $token = $match[1]; - $test1 = false; - $re2 = '/' . $v2 . '$/'; - $exept7 = '/^(ΒΕΤΕΡ|ΒΟΥΛΚ|ΒΡΑΧΜ|Γ|ΔΡΑΔΟΥΜ|Θ|ΚΑΛΠΟΥΖ|ΚΑΣΤΕΛ|ΚΟΡΜΟΡ|ΛΑΟΠΛ|ΜΩΑΜΕΘ|Μ|ΜΟΥΣΟΥΛΜ|Ν|ΟΥΛ|Π|ΠΕΛΕΚ|ΠΛ|ΠΟΛΙΣ|ΠΟΡΤΟΛ|ΣΑΡΑΚΑΤΣ|ΣΟΥΛΤ|' - . 'ΤΣΑΡΛΑΤ|ΟΡΦ|ΤΣΙΓΓ|ΤΣΟΠ|ΦΩΤΟΣΤΕΦ|Χ|ΨΥΧΟΠΛ|ΑΓ|ΟΡΦ|ΓΑΛ|ΓΕΡ|ΔΕΚ|ΔΙΠΛ|ΑΜΕΡΙΚΑΝ|ΟΥΡ|ΠΙΘ|ΠΟΥΡΙΤ|Σ|ΖΩΝΤ|ΙΚ|ΚΑΣΤ|ΚΟΠ|ΛΙΧ|ΛΟΥΘΗΡ|ΜΑΙΝΤ|' - . 'ΜΕΛ|ΣΙΓ|ΣΠ|ΣΤΕΓ|ΤΡΑΓ|ΤΣΑΓ|Φ|ΕΡ|ΑΔΑΠ|ΑΘΙΓΓ|ΑΜΗΧ|ΑΝΙΚ|ΑΝΟΡΓ|ΑΠΗΓ|ΑΠΙΘ|ΑΤΣΙΓΓ|ΒΑΣ|ΒΑΣΚ|ΒΑΘΥΓΑΛ|ΒΙΟΜΗΧ|ΒΡΑΧΥΚ|ΔΙΑΤ|ΔΙΑΦ|ΕΝΟΡΓ|' - . 'ΘΥΣ|ΚΑΠΝΟΒΙΟΜΗΧ|ΚΑΤΑΓΑΛ|ΚΛΙΒ|ΚΟΙΛΑΡΦ|ΛΙΒ|ΜΕΓΛΟΒΙΟΜΗΧ|ΜΙΚΡΟΒΙΟΜΗΧ|ΝΤΑΒ|ΞΗΡΟΚΛΙΒ|ΟΛΙΓΟΔΑΜ|ΟΛΟΓΑΛ|ΠΕΝΤΑΡΦ|ΠΕΡΗΦ|ΠΕΡΙΤΡ|ΠΛΑΤ|' - . 'ΠΟΛΥΔΑΠ|ΠΟΛΥΜΗΧ|ΣΤΕΦ|ΤΑΒ|ΤΕΤ|ΥΠΕΡΗΦ|ΥΠΟΚΟΠ|ΧΑΜΗΛΟΔΑΠ|ΨΗΛΟΤΑΒ)$/'; - - if (preg_match($re2, $token) || preg_match($exept7, $token)) - { - $token = $token . "ΑΝ"; - } - } - - // Step 5c - $re3 = '/^(.+?)(ΕΤΕ)$/'; - $re4 = '/^(.+?)(ΗΣΕΤΕ)$/'; - - if (preg_match($re4, $token)) - { - preg_match($re4, $token, $match); - $token = $match[1]; - $test1 = false; - } - - if (preg_match($re3, $token)) - { - preg_match($re3, $token, $match); - $token = $match[1]; - $test1 = false; - $re3 = '/' . $v2 . '$/'; - $exept8 = '/(ΟΔ|ΑΙΡ|ΦΟΡ|ΤΑΘ|ΔΙΑΘ|ΣΧ|ΕΝΔ|ΕΥΡ|ΤΙΘ|ΥΠΕΡΘ|ΡΑΘ|ΕΝΘ|ΡΟΘ|ΣΘ|ΠΥΡ|ΑΙΝ|ΣΥΝΔ|ΣΥΝ|ΣΥΝΘ|ΧΩΡ|ΠΟΝ|ΒΡ|ΚΑΘ|ΕΥΘ|ΕΚΘ|ΝΕΤ|ΡΟΝ|ΑΡΚ|ΒΑΡ|ΒΟΛ|ΩΦΕΛ)$/'; - $exept9 = '/^(ΑΒΑΡ|ΒΕΝ|ΕΝΑΡ|ΑΒΡ|ΑΔ|ΑΘ|ΑΝ|ΑΠΛ|ΒΑΡΟΝ|ΝΤΡ|ΣΚ|ΚΟΠ|ΜΠΟΡ|ΝΙΦ|ΠΑΓ|ΠΑΡΑΚΑΛ|ΣΕΡΠ|ΣΚΕΛ|ΣΥΡΦ|ΤΟΚ|Υ|Δ|ΕΜ|ΘΑΡΡ|Θ)$/'; - - if (preg_match($re3, $token) || preg_match($exept8, $token) || preg_match($exept9, $token)) - { - $token = $token . "ΕΤ"; - } - } - - // Step 5d - $re = '/^(.+?)(ΟΝΤΑΣ|ΩΝΤΑΣ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept10 = '/^(ΑΡΧ)$/'; - $exept11 = '/(ΚΡΕ)$/'; - - if (preg_match($exept10, $token)) - { - $token = $token . "ΟΝΤ"; - } - - if (preg_match($exept11, $token)) - { - $token = $token . "ΩΝΤ"; - } - } - - // Step 5e - $re = '/^(.+?)(ΟΜΑΣΤΕ|ΙΟΜΑΣΤΕ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept11 = '/^(ΟΝ)$/'; - - if (preg_match($exept11, $token)) - { - $token = $token . "ΟΜΑΣΤ"; - } - } - - // Step 5f - $re = '/^(.+?)(ΕΣΤΕ)$/'; - $re2 = '/^(.+?)(ΙΕΣΤΕ)$/'; - - if (preg_match($re2, $token)) - { - preg_match($re2, $token, $match); - $token = $match[1]; - $test1 = false; - $re2 = '/^(Π|ΑΠ|ΣΥΜΠ|ΑΣΥΜΠ|ΑΚΑΤΑΠ|ΑΜΕΤΑΜΦ)$/'; - - if (preg_match($re2, $token)) - { - $token = $token . "ΙΕΣΤ"; - } - } - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept12 = '/^(ΑΛ|ΑΡ|ΕΚΤΕΛ|Ζ|Μ|Ξ|ΠΑΡΑΚΑΛ|ΑΡ|ΠΡΟ|ΝΙΣ)$/'; - - if (preg_match($exept12, $token)) - { - $token = $token . "ΕΣΤ"; - } - } - - // Step 5g - $re = '/^(.+?)(ΗΚΑ|ΗΚΕΣ|ΗΚΕ)$/'; - $re2 = '/^(.+?)(ΗΘΗΚΑ|ΗΘΗΚΕΣ|ΗΘΗΚΕ)$/'; - - if (preg_match($re2, $token)) - { - preg_match($re2, $token, $match); - $token = $match[1]; - $test1 = false; - } - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept13 = '/(ΣΚΩΛ|ΣΚΟΥΛ|ΝΑΡΘ|ΣΦ|ΟΘ|ΠΙΘ)$/'; - $exept14 = '/^(ΔΙΑΘ|Θ|ΠΑΡΑΚΑΤΑΘ|ΠΡΟΣΘ|ΣΥΝΘ|)$/'; - - if (preg_match($exept13, $token) || preg_match($exept14, $token)) - { - $token = $token . "ΗΚ"; - } - } - - // Step 5h - $re = '/^(.+?)(ΟΥΣΑ|ΟΥΣΕΣ|ΟΥΣΕ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept15 = '/^(ΦΑΡΜΑΚ|ΧΑΔ|ΑΓΚ|ΑΝΑΡΡ|ΒΡΟΜ|ΕΚΛΙΠ|ΛΑΜΠΙΔ|ΛΕΧ|Μ|ΠΑΤ|Ρ|Λ|ΜΕΔ|ΜΕΣΑΖ|ΥΠΟΤΕΙΝ|ΑΜ|ΑΙΘ|ΑΝΗΚ|ΔΕΣΠΟΖ|ΕΝΔΙΑΦΕΡ|ΔΕ|ΔΕΥΤΕΡΕΥ|ΚΑΘΑΡΕΥ|ΠΛΕ|ΤΣΑ)$/'; - $exept16 = '/(ΠΟΔΑΡ|ΒΛΕΠ|ΠΑΝΤΑΧ|ΦΡΥΔ|ΜΑΝΤΙΛ|ΜΑΛΛ|ΚΥΜΑΤ|ΛΑΧ|ΛΗΓ|ΦΑΓ|ΟΜ|ΠΡΩΤ)$/'; - - if (preg_match($exept15, $token) || preg_match($exept16, $token)) - { - $token = $token . "ΟΥΣ"; - } - } - - // Step 5i - $re = '/^(.+?)(ΑΓΑ|ΑΓΕΣ|ΑΓΕ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept17 = '/^(ΨΟΦ|ΝΑΥΛΟΧ)$/'; - $exept20 = '/(ΚΟΛΛ)$/'; - $exept18 = '/^(ΑΒΑΣΤ|ΠΟΛΥΦ|ΑΔΗΦ|ΠΑΜΦ|Ρ|ΑΣΠ|ΑΦ|ΑΜΑΛ|ΑΜΑΛΛΙ|ΑΝΥΣΤ|ΑΠΕΡ|ΑΣΠΑΡ|ΑΧΑΡ|ΔΕΡΒΕΝ|ΔΡΟΣΟΠ|ΞΕΦ|ΝΕΟΠ|ΝΟΜΟΤ|ΟΛΟΠ|ΟΜΟΤ|ΠΡΟΣΤ|ΠΡΟΣΩΠΟΠ|' - . 'ΣΥΜΠ|ΣΥΝΤ|Τ|ΥΠΟΤ|ΧΑΡ|ΑΕΙΠ|ΑΙΜΟΣΤ|ΑΝΥΠ|ΑΠΟΤ|ΑΡΤΙΠ|ΔΙΑΤ|ΕΝ|ΕΠΙΤ|ΚΡΟΚΑΛΟΠ|ΣΙΔΗΡΟΠ|Λ|ΝΑΥ|ΟΥΛΑΜ|ΟΥΡ|Π|ΤΡ|Μ)$/'; - $exept19 = '/(ΟΦ|ΠΕΛ|ΧΟΡΤ|ΛΛ|ΣΦ|ΡΠ|ΦΡ|ΠΡ|ΛΟΧ|ΣΜΗΝ)$/'; - - if ((preg_match($exept18, $token) || preg_match($exept19, $token)) - && !(preg_match($exept17, $token) || preg_match($exept20, $token))) - { - $token = $token . "ΑΓ"; - } - } - - // Step 5j - $re = '/^(.+?)(ΗΣΕ|ΗΣΟΥ|ΗΣΑ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept21 = '/^(Ν|ΧΕΡΣΟΝ|ΔΩΔΕΚΑΝ|ΕΡΗΜΟΝ|ΜΕΓΑΛΟΝ|ΕΠΤΑΝ)$/'; - - if (preg_match($exept21, $token)) - { - $token = $token . "ΗΣ"; - } - } - - // Step 5k - $re = '/^(.+?)(ΗΣΤΕ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept22 = '/^(ΑΣΒ|ΣΒ|ΑΧΡ|ΧΡ|ΑΠΛ|ΑΕΙΜΝ|ΔΥΣΧΡ|ΕΥΧΡ|ΚΟΙΝΟΧΡ|ΠΑΛΙΜΨ)$/'; - - if (preg_match($exept22, $token)) - { - $token = $token . "ΗΣΤ"; - } - } - - // Step 5l - $re = '/^(.+?)(ΟΥΝΕ|ΗΣΟΥΝΕ|ΗΘΟΥΝΕ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept23 = '/^(Ν|Ρ|ΣΠΙ|ΣΤΡΑΒΟΜΟΥΤΣ|ΚΑΚΟΜΟΥΤΣ|ΕΞΩΝ)$/'; - - if (preg_match($exept23, $token)) - { - $token = $token . "ΟΥΝ"; - } - } - - // Step 5m - $re = '/^(.+?)(ΟΥΜΕ|ΗΣΟΥΜΕ|ΗΘΟΥΜΕ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - $test1 = false; - $exept24 = '/^(ΠΑΡΑΣΟΥΣ|Φ|Χ|ΩΡΙΟΠΛ|ΑΖ|ΑΛΛΟΣΟΥΣ|ΑΣΟΥΣ)$/'; - - if (preg_match($exept24, $token)) - { - $token = $token . "ΟΥΜ"; - } - } - - // Step 6 - $re = '/^(.+?)(ΜΑΤΑ|ΜΑΤΩΝ|ΜΑΤΟΣ)$/'; - $re2 = '/^(.+?)(Α|ΑΓΑΤΕ|ΑΓΑΝ|ΑΕΙ|ΑΜΑΙ|ΑΝ|ΑΣ|ΑΣΑΙ|ΑΤΑΙ|ΑΩ|Ε|ΕΙ|ΕΙΣ|ΕΙΤΕ|ΕΣΑΙ|ΕΣ|ΕΤΑΙ|Ι|ΙΕΜΑΙ|ΙΕΜΑΣΤΕ|ΙΕΤΑΙ|ΙΕΣΑΙ|ΙΕΣΑΣΤΕ|ΙΟΜΑΣΤΑΝ|ΙΟΜΟΥΝ|' - . 'ΙΟΜΟΥΝΑ|ΙΟΝΤΑΝ|ΙΟΝΤΟΥΣΑΝ|ΙΟΣΑΣΤΑΝ|ΙΟΣΑΣΤΕ|ΙΟΣΟΥΝ|ΙΟΣΟΥΝΑ|ΙΟΤΑΝ|ΙΟΥΜΑ|ΙΟΥΜΑΣΤΕ|ΙΟΥΝΤΑΙ|ΙΟΥΝΤΑΝ|Η|ΗΔΕΣ|ΗΔΩΝ|ΗΘΕΙ|ΗΘΕΙΣ|ΗΘΕΙΤΕ|' - . 'ΗΘΗΚΑΤΕ|ΗΘΗΚΑΝ|ΗΘΟΥΝ|ΗΘΩ|ΗΚΑΤΕ|ΗΚΑΝ|ΗΣ|ΗΣΑΝ|ΗΣΑΤΕ|ΗΣΕΙ|ΗΣΕΣ|ΗΣΟΥΝ|ΗΣΩ|Ο|ΟΙ|ΟΜΑΙ|ΟΜΑΣΤΑΝ|ΟΜΟΥΝ|ΟΜΟΥΝΑ|ΟΝΤΑΙ|ΟΝΤΑΝ|ΟΝΤΟΥΣΑΝ|ΟΣ|' - . 'ΟΣΑΣΤΑΝ|ΟΣΑΣΤΕ|ΟΣΟΥΝ|ΟΣΟΥΝΑ|ΟΤΑΝ|ΟΥ|ΟΥΜΑΙ|ΟΥΜΑΣΤΕ|ΟΥΝ|ΟΥΝΤΑΙ|ΟΥΝΤΑΝ|ΟΥΣ|ΟΥΣΑΝ|ΟΥΣΑΤΕ|Υ|ΥΣ|Ω|ΩΝ)$/'; - - if (preg_match($re, $token, $match)) - { - $token = $match[1] . "ΜΑ"; - } - - if (preg_match($re2, $token) && $test1) - { - preg_match($re2, $token, $match); - $token = $match[1]; - } - - // Step 7 (ΠΑΡΑΘΕΤΙΚΑ) - $re = '/^(.+?)(ΕΣΤΕΡ|ΕΣΤΑΤ|ΟΤΕΡ|ΟΤΑΤ|ΥΤΕΡ|ΥΤΑΤ|ΩΤΕΡ|ΩΤΑΤ)$/'; - - if (preg_match($re, $token)) - { - preg_match($re, $token, $match); - $token = $match[1]; - } - - return $this->toLowerCase($token, $wCase); - } - - /** - * Converts the token to uppercase, suppressing accents and diaeresis. The array $wCase contains a special map of - * the uppercase rule used to convert each character at each position. - * - * @param string $token Token to process - * @param array &$wCase Map of uppercase rules - * - * @return string - * - * @since 4.0.0 - */ - protected function toUpperCase($token, &$wCase) - { - $wCase = array_fill(0, mb_strlen($token, 'UTF-8'), 0); - $caseConvert = array( - "α" => 'Α', - "β" => 'Β', - "γ" => 'Γ', - "δ" => 'Δ', - "ε" => 'Ε', - "ζ" => 'Ζ', - "η" => 'Η', - "θ" => 'Θ', - "ι" => 'Ι', - "κ" => 'Κ', - "λ" => 'Λ', - "μ" => 'Μ', - "ν" => 'Ν', - "ξ" => 'Ξ', - "ο" => 'Ο', - "π" => 'Π', - "ρ" => 'Ρ', - "σ" => 'Σ', - "τ" => 'Τ', - "υ" => 'Υ', - "φ" => 'Φ', - "χ" => 'Χ', - "ψ" => 'Ψ', - "ω" => 'Ω', - "ά" => 'Α', - "έ" => 'Ε', - "ή" => 'Η', - "ί" => 'Ι', - "ό" => 'Ο', - "ύ" => 'Υ', - "ώ" => 'Ω', - "ς" => 'Σ', - "ϊ" => 'Ι', - "ϋ" => 'Ι', - "ΐ" => 'Ι', - "ΰ" => 'Υ', - ); - $newToken = ''; - - for ($i = 0; $i < mb_strlen($token); $i++) - { - $char = mb_substr($token, $i, 1); - $isLower = array_key_exists($char, $caseConvert); - - if (!$isLower) - { - $newToken .= $char; - - continue; - } - - $upperCase = $caseConvert[$char]; - $newToken .= $upperCase; - - $wCase[$i] = 1; - - if (in_array($char, ['ά', 'έ', 'ή', 'ί', 'ό', 'ύ', 'ώ', 'ς'])) - { - $wCase[$i] = 2; - } - - if (in_array($char, ['ϊ', 'ϋ'])) - { - $wCase[$i] = 3; - } - - if (in_array($char, ['ΐ', 'ΰ'])) - { - $wCase[$i] = 4; - } - } - - return $newToken; - } - - /** - * Converts the suppressed uppercase token back to lowercase, using the $wCase map to add back the accents, - * diaeresis and handle the special case of final sigma (different lowercase glyph than the regular sigma, only - * used at the end of words). - * - * @param string $token Token to process - * @param array $wCase Map of lowercase rules - * - * @return string - * - * @since 4.0.0 - */ - protected function toLowerCase($token, $wCase) - { - $newToken = ''; - - for ($i = 0; $i < mb_strlen($token); $i++) - { - $char = mb_substr($token, $i, 1); - - // Is $wCase not set at this position? We assume no case conversion ever took place. - if (!isset($wCase[$i])) - { - $newToken .= $char; - - continue; - } - - // The character was not case-converted - if ($wCase[$i] == 0) - { - $newToken .= $char; - - continue; - } - - // Case 1: Unaccented letter - if ($wCase[$i] == 1) - { - $newToken .= mb_strtolower($char); - - continue; - } - - // Case 2: Vowel with accent (tonos); or the special case of final sigma - if ($wCase[$i] == 2) - { - $charMap = [ - 'Α' => 'ά', - 'Ε' => 'έ', - 'Η' => 'ή', - 'Ι' => 'ί', - 'Ο' => 'ό', - 'Υ' => 'ύ', - 'Ω' => 'ώ', - 'Σ' => 'ς' - ]; - - $newToken .= $charMap[$char]; - - continue; - } - - // Case 3: vowels with diaeresis (dialytika) - if ($wCase[$i] == 3) - { - $charMap = [ - 'Ι' => 'ϊ', - 'Υ' => 'ϋ' - ]; - - $newToken .= $charMap[$char]; - - continue; - } - - // Case 4: vowels with both diaeresis (dialytika) and accent (tonos) - if ($wCase[$i] == 4) - { - $charMap = [ - 'Ι' => 'ΐ', - 'Υ' => 'ΰ' - ]; - - $newToken .= $charMap[$char]; - - continue; - } - - // This should never happen! - $newToken .= $char; - } - - return $newToken; - } + /** + * Language locale of the class + * + * @var string + * @since 4.0.0 + */ + public $language = 'el'; + + /** + * Method to construct the language object. + * + * @since 4.0.0 + */ + public function __construct($locale = null) + { + // Override parent constructor since we don't need to load an external stemmer + } + + /** + * Method to tokenise a text string. It takes into account the odd punctuation commonly used in Greek text, mapping + * it to ASCII punctuation. + * + * Reference: http://www.teicrete.gr/users/kutrulis/Glosika/Stixi.htm + * + * @param string $input The input to tokenise. + * + * @return array An array of term strings. + * + * @since 4.0.0 + */ + public function tokenise($input) + { + // Replace Greek calligraphic double quotes (various styles) to dumb double quotes + $input = str_replace(['“', '”', '„', '«' ,'»'], '"', $input); + + // Replace Greek calligraphic single quotes (various styles) to dumb single quotes + $input = str_replace(['‘','’','‚'], "'", $input); + + // Replace the middle dot (ano teleia) with a comma, adequate for the purpose of stemming + $input = str_replace('·', ',', $input); + + // Dot and dash (τελεία και παύλα), used to denote the end of a context at the end of a paragraph. + $input = str_replace('.–', '.', $input); + + // Ellipsis, two styles (separate dots or single glyph) + $input = str_replace(['...', '…'], '.', $input); + + // Cross. Marks the death date of a person. Removed. + $input = str_replace('†', '', $input); + + // Star. Reference, supposition word (in philology), birth date of a person. + $input = str_replace('*', '', $input); + + // Paragraph. Indicates change of subject. + $input = str_replace('§', '.', $input); + + // Plus/minus. Shows approximation. Not relevant for the stemmer, hence its conversion to a space. + $input = str_replace('±', ' ', $input); + + return parent::tokenise($input); + } + + /** + * Method to stem a token. + * + * @param string $token The token to stem. + * + * @return string The stemmed token. + * + * @since 4.0.0 + */ + public function stem($token) + { + $token = $this->toUpperCase($token, $wCase); + + // Stop-word removal + $stop_words = '/^(ΕΚΟ|ΑΒΑ|ΑΓΑ|ΑΓΗ|ΑΓΩ|ΑΔΗ|ΑΔΩ|ΑΕ|ΑΕΙ|ΑΘΩ|ΑΙ|ΑΙΚ|ΑΚΗ|ΑΚΟΜΑ|ΑΚΟΜΗ|ΑΚΡΙΒΩΣ|ΑΛΑ|ΑΛΗΘΕΙΑ|ΑΛΗΘΙΝΑ|ΑΛΛΑΧΟΥ|ΑΛΛΙΩΣ|ΑΛΛΙΩΤΙΚΑ|' + . 'ΑΛΛΟΙΩΣ|ΑΛΛΟΙΩΤΙΚΑ|ΑΛΛΟΤΕ|ΑΛΤ|ΑΛΩ|ΑΜΑ|ΑΜΕ|ΑΜΕΣΑ|ΑΜΕΣΩΣ|ΑΜΩ|ΑΝ|ΑΝΑ|ΑΝΑΜΕΣΑ|ΑΝΑΜΕΤΑΞΥ|ΑΝΕΥ|ΑΝΤΙ|ΑΝΤΙΠΕΡΑ|ΑΝΤΙΣ|ΑΝΩ|ΑΝΩΤΕΡΩ|ΑΞΑΦΝΑ|' + . 'ΑΠ|ΑΠΕΝΑΝΤΙ|ΑΠΟ|ΑΠΟΨΕ|ΑΠΩ|ΑΡΑ|ΑΡΑΓΕ|ΑΡΕ|ΑΡΚ|ΑΡΚΕΤΑ|ΑΡΛ|ΑΡΜ|ΑΡΤ|ΑΡΥ|ΑΡΩ|ΑΣ|ΑΣΑ|ΑΣΟ|ΑΤΑ|ΑΤΕ|ΑΤΗ|ΑΤΙ|ΑΤΜ|ΑΤΟ|ΑΥΡΙΟ|ΑΦΗ|ΑΦΟΤΟΥ|ΑΦΟΥ|' + . 'ΑΧ|ΑΧΕ|ΑΧΟ|ΑΨΑ|ΑΨΕ|ΑΨΗ|ΑΨΥ|ΑΩΕ|ΑΩΟ|ΒΑΝ|ΒΑΤ|ΒΑΧ|ΒΕΑ|ΒΕΒΑΙΟΤΑΤΑ|ΒΗΞ|ΒΙΑ|ΒΙΕ|ΒΙΗ|ΒΙΟ|ΒΟΗ|ΒΟΩ|ΒΡΕ|ΓΑ|ΓΑΒ|ΓΑΡ|ΓΕΝ|ΓΕΣ||ΓΗ|ΓΗΝ|ΓΙ|ΓΙΑ|' + . 'ΓΙΕ|ΓΙΝ|ΓΙΟ|ΓΚΙ|ΓΙΑΤΙ|ΓΚΥ|ΓΟΗ|ΓΟΟ|ΓΡΗΓΟΡΑ|ΓΡΙ|ΓΡΥ|ΓΥΗ|ΓΥΡΩ|ΔΑ|ΔΕ|ΔΕΗ|ΔΕΙ|ΔΕΝ|ΔΕΣ|ΔΗ|ΔΗΘΕΝ|ΔΗΛΑΔΗ|ΔΗΩ|ΔΙ|ΔΙΑ|ΔΙΑΡΚΩΣ|ΔΙΟΛΟΥ|ΔΙΣ|' + . 'ΔΙΧΩΣ|ΔΟΛ|ΔΟΝ|ΔΡΑ|ΔΡΥ|ΔΡΧ|ΔΥΕ|ΔΥΟ|ΔΩ|ΕΑΜ|ΕΑΝ|ΕΑΡ|ΕΘΗ|ΕΙ|ΕΙΔΕΜΗ|ΕΙΘΕ|ΕΙΜΑΙ|ΕΙΜΑΣΤΕ|ΕΙΝΑΙ|ΕΙΣ|ΕΙΣΑΙ|ΕΙΣΑΣΤΕ|ΕΙΣΤΕ|ΕΙΤΕ|ΕΙΧΑ|ΕΙΧΑΜΕ|' + . 'ΕΙΧΑΝ|ΕΙΧΑΤΕ|ΕΙΧΕ|ΕΙΧΕΣ|ΕΚ|ΕΚΕΙ|ΕΛΑ|ΕΛΙ|ΕΜΠ|ΕΝ|ΕΝΤΕΛΩΣ|ΕΝΤΟΣ|ΕΝΤΩΜΕΤΑΞΥ|ΕΝΩ|ΕΞ|ΕΞΑΦΝΑ|ΕΞΙ|ΕΞΙΣΟΥ|ΕΞΩ|ΕΟΚ|ΕΠΑΝΩ|ΕΠΕΙΔΗ|ΕΠΕΙΤΑ|ΕΠΗ|' + . 'ΕΠΙ|ΕΠΙΣΗΣ|ΕΠΟΜΕΝΩΣ|ΕΡΑ|ΕΣ|ΕΣΑΣ|ΕΣΕ|ΕΣΕΙΣ|ΕΣΕΝΑ|ΕΣΗ|ΕΣΤΩ|ΕΣΥ|ΕΣΩ|ΕΤΙ|ΕΤΣΙ|ΕΥ|ΕΥΑ|ΕΥΓΕ|ΕΥΘΥΣ|ΕΥΤΥΧΩΣ|ΕΦΕ|ΕΦΕΞΗΣ|ΕΦΤ|ΕΧΕ|ΕΧΕΙ|' + . 'ΕΧΕΙΣ|ΕΧΕΤΕ|ΕΧΘΕΣ|ΕΧΟΜΕ|ΕΧΟΥΜΕ|ΕΧΟΥΝ|ΕΧΤΕΣ|ΕΧΩ|ΕΩΣ|ΖΕΑ|ΖΕΗ|ΖΕΙ|ΖΕΝ|ΖΗΝ|ΖΩ|Η|ΗΔΗ|ΗΔΥ|ΗΘΗ|ΗΛΟ|ΗΜΙ|ΗΠΑ|ΗΣΑΣΤΕ|ΗΣΟΥΝ|ΗΤΑ|ΗΤΑΝ|ΗΤΑΝΕ|' + . 'ΗΤΟΙ|ΗΤΤΟΝ|ΗΩ|ΘΑ|ΘΥΕ|ΘΩΡ|Ι|ΙΑ|ΙΒΟ|ΙΔΗ|ΙΔΙΩΣ|ΙΕ|ΙΙ|ΙΙΙ|ΙΚΑ|ΙΛΟ|ΙΜΑ|ΙΝΑ|ΙΝΩ|ΙΞΕ|ΙΞΟ|ΙΟ|ΙΟΙ|ΙΣΑ|ΙΣΑΜΕ|ΙΣΕ|ΙΣΗ|ΙΣΙΑ|ΙΣΟ|ΙΣΩΣ|ΙΩΒ|ΙΩΝ|' + . 'ΙΩΣ|ΙΑΝ|ΚΑΘ|ΚΑΘΕ|ΚΑΘΕΤΙ|ΚΑΘΟΛΟΥ|ΚΑΘΩΣ|ΚΑΙ|ΚΑΝ|ΚΑΠΟΤΕ|ΚΑΠΟΥ|ΚΑΠΩΣ|ΚΑΤ|ΚΑΤΑ|ΚΑΤΙ|ΚΑΤΙΤΙ|ΚΑΤΟΠΙΝ|ΚΑΤΩ|ΚΑΩ|ΚΒΟ|ΚΕΑ|ΚΕΙ|ΚΕΝ|ΚΙ|ΚΙΜ|' + . 'ΚΙΟΛΑΣ|ΚΙΤ|ΚΙΧ|ΚΚΕ|ΚΛΙΣΕ|ΚΛΠ|ΚΟΚ|ΚΟΝΤΑ|ΚΟΧ|ΚΤΛ|ΚΥΡ|ΚΥΡΙΩΣ|ΚΩ|ΚΩΝ|ΛΑ|ΛΕΑ|ΛΕΝ|ΛΕΟ|ΛΙΑ|ΛΙΓΑΚΙ|ΛΙΓΟΥΛΑΚΙ|ΛΙΓΟ|ΛΙΓΩΤΕΡΟ|ΛΙΟ|ΛΙΡ|ΛΟΓΩ|' + . 'ΛΟΙΠΑ|ΛΟΙΠΟΝ|ΛΟΣ|ΛΣ|ΛΥΩ|ΜΑ|ΜΑΖΙ|ΜΑΚΑΡΙ|ΜΑΛΙΣΤΑ|ΜΑΛΛΟΝ|ΜΑΝ|ΜΑΞ|ΜΑΣ|ΜΑΤ|ΜΕ|ΜΕΘΑΥΡΙΟ|ΜΕΙ|ΜΕΙΟΝ|ΜΕΛ|ΜΕΛΕΙ|ΜΕΛΛΕΤΑΙ|ΜΕΜΙΑΣ|ΜΕΝ|ΜΕΣ|' + . 'ΜΕΣΑ|ΜΕΤ|ΜΕΤΑ|ΜΕΤΑΞΥ|ΜΕΧΡΙ|ΜΗ|ΜΗΔΕ|ΜΗΝ|ΜΗΠΩΣ|ΜΗΤΕ|ΜΙ|ΜΙΞ|ΜΙΣ|ΜΜΕ|ΜΝΑ|ΜΟΒ|ΜΟΛΙΣ|ΜΟΛΟΝΟΤΙ|ΜΟΝΑΧΑ|ΜΟΝΟΜΙΑΣ|ΜΙΑ|ΜΟΥ|ΜΠΑ|ΜΠΟΡΕΙ|' + . 'ΜΠΟΡΟΥΝ|ΜΠΡΑΒΟ|ΜΠΡΟΣ|ΜΠΩ|ΜΥ|ΜΥΑ|ΜΥΝ|ΝΑ|ΝΑΕ|ΝΑΙ|ΝΑΟ|ΝΔ|ΝΕΐ|ΝΕΑ|ΝΕΕ|ΝΕΟ|ΝΙ|ΝΙΑ|ΝΙΚ|ΝΙΛ|ΝΙΝ|ΝΙΟ|ΝΤΑ|ΝΤΕ|ΝΤΙ|ΝΤΟ|ΝΥΝ|ΝΩΕ|ΝΩΡΙΣ|ΞΑΝΑ|' + . 'ΞΑΦΝΙΚΑ|ΞΕΩ|ΞΙ|Ο|ΟΑ|ΟΑΠ|ΟΔΟ|ΟΕ|ΟΖΟ|ΟΗΕ|ΟΙ|ΟΙΑ|ΟΙΗ|ΟΚΑ|ΟΛΟΓΥΡΑ|ΟΛΟΝΕΝ|ΟΛΟΤΕΛΑ|ΟΛΩΣΔΙΟΛΟΥ|ΟΜΩΣ|ΟΝ|ΟΝΕ|ΟΝΟ|ΟΠΑ|ΟΠΕ|ΟΠΗ|ΟΠΟ|' + . 'ΟΠΟΙΑΔΗΠΟΤΕ|ΟΠΟΙΑΝΔΗΠΟΤΕ|ΟΠΟΙΑΣΔΗΠΟΤΕ|ΟΠΟΙΔΗΠΟΤΕ|ΟΠΟΙΕΣΔΗΠΟΤΕ|ΟΠΟΙΟΔΗΠΟΤΕ|ΟΠΟΙΟΝΔΗΠΟΤΕ|ΟΠΟΙΟΣΔΗΠΟΤΕ|ΟΠΟΙΟΥΔΗΠΟΤΕ|ΟΠΟΙΟΥΣΔΗΠΟΤΕ|' + . 'ΟΠΟΙΩΝΔΗΠΟΤΕ|ΟΠΟΤΕΔΗΠΟΤΕ|ΟΠΟΥ|ΟΠΟΥΔΗΠΟΤΕ|ΟΠΩΣ|ΟΡΑ|ΟΡΕ|ΟΡΗ|ΟΡΟ|ΟΡΦ|ΟΡΩ|ΟΣΑ|ΟΣΑΔΗΠΟΤΕ|ΟΣΕ|ΟΣΕΣΔΗΠΟΤΕ|ΟΣΗΔΗΠΟΤΕ|ΟΣΗΝΔΗΠΟΤΕ|' + . 'ΟΣΗΣΔΗΠΟΤΕ|ΟΣΟΔΗΠΟΤΕ|ΟΣΟΙΔΗΠΟΤΕ|ΟΣΟΝΔΗΠΟΤΕ|ΟΣΟΣΔΗΠΟΤΕ|ΟΣΟΥΔΗΠΟΤΕ|ΟΣΟΥΣΔΗΠΟΤΕ|ΟΣΩΝΔΗΠΟΤΕ|ΟΤΑΝ|ΟΤΕ|ΟΤΙ|ΟΤΙΔΗΠΟΤΕ|ΟΥ|ΟΥΔΕ|ΟΥΚ|ΟΥΣ|' + . 'ΟΥΤΕ|ΟΥΦ|ΟΧΙ|ΟΨΑ|ΟΨΕ|ΟΨΗ|ΟΨΙ|ΟΨΟ|ΠΑ|ΠΑΛΙ|ΠΑΝ|ΠΑΝΤΟΤΕ|ΠΑΝΤΟΥ|ΠΑΝΤΩΣ|ΠΑΠ|ΠΑΡ|ΠΑΡΑ|ΠΕΙ|ΠΕΡ|ΠΕΡΑ|ΠΕΡΙ|ΠΕΡΙΠΟΥ|ΠΕΡΣΙ|ΠΕΡΥΣΙ|ΠΕΣ|ΠΙ|' + . 'ΠΙΑ|ΠΙΘΑΝΟΝ|ΠΙΚ|ΠΙΟ|ΠΙΣΩ|ΠΙΤ|ΠΙΩ|ΠΛΑΙ|ΠΛΕΟΝ|ΠΛΗΝ|ΠΛΩ|ΠΜ|ΠΟΑ|ΠΟΕ|ΠΟΛ|ΠΟΛΥ|ΠΟΠ|ΠΟΤΕ|ΠΟΥ|ΠΟΥΘΕ|ΠΟΥΘΕΝΑ|ΠΡΕΠΕΙ|ΠΡΙ|ΠΡΙΝ|ΠΡΟ|' + . 'ΠΡΟΚΕΙΜΕΝΟΥ|ΠΡΟΚΕΙΤΑΙ|ΠΡΟΠΕΡΣΙ|ΠΡΟΣ|ΠΡΟΤΟΥ|ΠΡΟΧΘΕΣ|ΠΡΟΧΤΕΣ|ΠΡΩΤΥΤΕΡΑ|ΠΥΑ|ΠΥΞ|ΠΥΟ|ΠΥΡ|ΠΧ|ΠΩ|ΠΩΛ|ΠΩΣ|ΡΑ|ΡΑΙ|ΡΑΠ|ΡΑΣ|ΡΕ|ΡΕΑ|ΡΕΕ|ΡΕΙ|' + . 'ΡΗΣ|ΡΘΩ|ΡΙΟ|ΡΟ|ΡΟΐ|ΡΟΕ|ΡΟΖ|ΡΟΗ|ΡΟΘ|ΡΟΙ|ΡΟΚ|ΡΟΛ|ΡΟΝ|ΡΟΣ|ΡΟΥ|ΣΑΙ|ΣΑΝ|ΣΑΟ|ΣΑΣ|ΣΕ|ΣΕΙΣ|ΣΕΚ|ΣΕΞ|ΣΕΡ|ΣΕΤ|ΣΕΦ|ΣΗΜΕΡΑ|ΣΙ|ΣΙΑ|ΣΙΓΑ|ΣΙΚ|' + . 'ΣΙΧ|ΣΚΙ|ΣΟΙ|ΣΟΚ|ΣΟΛ|ΣΟΝ|ΣΟΣ|ΣΟΥ|ΣΡΙ|ΣΤΑ|ΣΤΗ|ΣΤΗΝ|ΣΤΗΣ|ΣΤΙΣ|ΣΤΟ|ΣΤΟΝ|ΣΤΟΥ|ΣΤΟΥΣ|ΣΤΩΝ|ΣΥ|ΣΥΓΧΡΟΝΩΣ|ΣΥΝ|ΣΥΝΑΜΑ|ΣΥΝΕΠΩΣ|ΣΥΝΗΘΩΣ|' + . 'ΣΧΕΔΟΝ|ΣΩΣΤΑ|ΤΑ|ΤΑΔΕ|ΤΑΚ|ΤΑΝ|ΤΑΟ|ΤΑΥ|ΤΑΧΑ|ΤΑΧΑΤΕ|ΤΕ|ΤΕΙ|ΤΕΛ|ΤΕΛΙΚΑ|ΤΕΛΙΚΩΣ|ΤΕΣ|ΤΕΤ|ΤΖΟ|ΤΗ|ΤΗΛ|ΤΗΝ|ΤΗΣ|ΤΙ|ΤΙΚ|ΤΙΜ|ΤΙΠΟΤΑ|ΤΙΠΟΤΕ|' + . 'ΤΙΣ|ΤΝΤ|ΤΟ|ΤΟΙ|ΤΟΚ|ΤΟΜ|ΤΟΝ|ΤΟΠ|ΤΟΣ|ΤΟΣ?Ν|ΤΟΣΑ|ΤΟΣΕΣ|ΤΟΣΗ|ΤΟΣΗΝ|ΤΟΣΗΣ|ΤΟΣΟ|ΤΟΣΟΙ|ΤΟΣΟΝ|ΤΟΣΟΣ|ΤΟΣΟΥ|ΤΟΣΟΥΣ|ΤΟΤΕ|ΤΟΥ|ΤΟΥΛΑΧΙΣΤΟ|' + . 'ΤΟΥΛΑΧΙΣΤΟΝ|ΤΟΥΣ|ΤΣ|ΤΣΑ|ΤΣΕ|ΤΥΧΟΝ|ΤΩ|ΤΩΝ|ΤΩΡΑ|ΥΑΣ|ΥΒΑ|ΥΒΟ|ΥΙΕ|ΥΙΟ|ΥΛΑ|ΥΛΗ|ΥΝΙ|ΥΠ|ΥΠΕΡ|ΥΠΟ|ΥΠΟΨΗ|ΥΠΟΨΙΝ|ΥΣΤΕΡΑ|ΥΦΗ|ΥΨΗ|ΦΑ|ΦΑΐ|ΦΑΕ|' + . 'ΦΑΝ|ΦΑΞ|ΦΑΣ|ΦΑΩ|ΦΕΖ|ΦΕΙ|ΦΕΤΟΣ|ΦΕΥ|ΦΙ|ΦΙΛ|ΦΙΣ|ΦΟΞ|ΦΠΑ|ΦΡΙ|ΧΑ|ΧΑΗ|ΧΑΛ|ΧΑΝ|ΧΑΦ|ΧΕ|ΧΕΙ|ΧΘΕΣ|ΧΙ|ΧΙΑ|ΧΙΛ|ΧΙΟ|ΧΛΜ|ΧΜ|ΧΟΗ|ΧΟΛ|ΧΡΩ|ΧΤΕΣ|' + . 'ΧΩΡΙΣ|ΧΩΡΙΣΤΑ|ΨΕΣ|ΨΗΛΑ|ΨΙ|ΨΙΤ|Ω|ΩΑ|ΩΑΣ|ΩΔΕ|ΩΕΣ|ΩΘΩ|ΩΜΑ|ΩΜΕ|ΩΝ|ΩΟ|ΩΟΝ|ΩΟΥ|ΩΣ|ΩΣΑΝ|ΩΣΗ|ΩΣΟΤΟΥ|ΩΣΠΟΥ|ΩΣΤΕ|ΩΣΤΟΣΟ|ΩΤΑ|ΩΧ|ΩΩΝ)$/'; + + if (preg_match($stop_words, $token)) { + return $this->toLowerCase($token, $wCase); + } + + // Vowels + $v = '(Α|Ε|Η|Ι|Ο|Υ|Ω)'; + + // Vowels without Y + $v2 = '(Α|Ε|Η|Ι|Ο|Ω)'; + + $test1 = true; + + // Step S1. 14 stems + $re = '/^(.+?)(ΙΖΑ|ΙΖΕΣ|ΙΖΕ|ΙΖΑΜΕ|ΙΖΑΤΕ|ΙΖΑΝ|ΙΖΑΝΕ|ΙΖΩ|ΙΖΕΙΣ|ΙΖΕΙ|ΙΖΟΥΜΕ|ΙΖΕΤΕ|ΙΖΟΥΝ|ΙΖΟΥΝΕ)$/'; + $exceptS1 = '/^(ΑΝΑΜΠΑ|ΕΜΠΑ|ΕΠΑ|ΞΑΝΑΠΑ|ΠΑ|ΠΕΡΙΠΑ|ΑΘΡΟ|ΣΥΝΑΘΡΟ|ΔΑΝΕ)$/'; + $exceptS2 = '/^(ΜΑΡΚ|ΚΟΡΝ|ΑΜΠΑΡ|ΑΡΡ|ΒΑΘΥΡΙ|ΒΑΡΚ|Β|ΒΟΛΒΟΡ|ΓΚΡ|ΓΛΥΚΟΡ|ΓΛΥΚΥΡ|ΙΜΠ|Λ|ΛΟΥ|ΜΑΡ|Μ|ΠΡ|ΜΠΡ|ΠΟΛΥΡ|Π|Ρ|ΠΙΠΕΡΟΡ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . 'I'; + } + + if (preg_match($exceptS2, $token)) { + $token = $token . 'IΖ'; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S2. 7 stems + $re = '/^(.+?)(ΩΘΗΚΑ|ΩΘΗΚΕΣ|ΩΘΗΚΕ|ΩΘΗΚΑΜΕ|ΩΘΗΚΑΤΕ|ΩΘΗΚΑΝ|ΩΘΗΚΑΝΕ)$/'; + $exceptS1 = '/^(ΑΛ|ΒΙ|ΕΝ|ΥΨ|ΛΙ|ΖΩ|Σ|Χ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . 'ΩΝ'; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S3. 7 stems + $re = '/^(.+?)(ΙΣΑ|ΙΣΕΣ|ΙΣΕ|ΙΣΑΜΕ|ΙΣΑΤΕ|ΙΣΑΝ|ΙΣΑΝΕ)$/'; + $exceptS1 = '/^(ΑΝΑΜΠΑ|ΑΘΡΟ|ΕΜΠΑ|ΕΣΕ|ΕΣΩΚΛΕ|ΕΠΑ|ΞΑΝΑΠΑ|ΕΠΕ|ΠΕΡΙΠΑ|ΑΘΡΟ|ΣΥΝΑΘΡΟ|ΔΑΝΕ|ΚΛΕ|ΧΑΡΤΟΠΑ|ΕΞΑΡΧΑ|ΜΕΤΕΠΕ|ΑΠΟΚΛΕ|ΑΠΕΚΛΕ|ΕΚΛΕ|ΠΕ|ΠΕΡΙΠΑ)$/'; + $exceptS2 = '/^(ΑΝ|ΑΦ|ΓΕ|ΓΙΓΑΝΤΟΑΦ|ΓΚΕ|ΔΗΜΟΚΡΑΤ|ΚΟΜ|ΓΚ|Μ|Π|ΠΟΥΚΑΜ|ΟΛΟ|ΛΑΡ)$/'; + + if ($token == "ΙΣΑ") { + $token = "ΙΣ"; + + return $token; + } + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . 'Ι'; + } + + if (preg_match($exceptS2, $token)) { + $token = $token . 'ΙΣ'; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S4. 7 stems + $re = '/^(.+?)(ΙΣΩ|ΙΣΕΙΣ|ΙΣΕΙ|ΙΣΟΥΜΕ|ΙΣΕΤΕ|ΙΣΟΥΝ|ΙΣΟΥΝΕ)$/'; + $exceptS1 = '/^(ΑΝΑΜΠΑ|ΕΜΠΑ|ΕΣΕ|ΕΣΩΚΛΕ|ΕΠΑ|ΞΑΝΑΠΑ|ΕΠΕ|ΠΕΡΙΠΑ|ΑΘΡΟ|ΣΥΝΑΘΡΟ|ΔΑΝΕ|ΚΛΕ|ΧΑΡΤΟΠΑ|ΕΞΑΡΧΑ|ΜΕΤΕΠΕ|ΑΠΟΚΛΕ|ΑΠΕΚΛΕ|ΕΚΛΕ|ΠΕ|ΠΕΡΙΠΑ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . 'Ι'; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S5. 11 stems + $re = '/^(.+?)(ΙΣΤΟΣ|ΙΣΤΟΥ|ΙΣΤΟ|ΙΣΤΕ|ΙΣΤΟΙ|ΙΣΤΩΝ|ΙΣΤΟΥΣ|ΙΣΤΗ|ΙΣΤΗΣ|ΙΣΤΑ|ΙΣΤΕΣ)$/'; + $exceptS1 = '/^(Μ|Π|ΑΠ|ΑΡ|ΗΔ|ΚΤ|ΣΚ|ΣΧ|ΥΨ|ΦΑ|ΧΡ|ΧΤ|ΑΚΤ|ΑΟΡ|ΑΣΧ|ΑΤΑ|ΑΧΝ|ΑΧΤ|ΓΕΜ|ΓΥΡ|ΕΜΠ|ΕΥΠ|ΕΧΘ|ΗΦΑ|ΚΑΘ|ΚΑΚ|ΚΥΛ|ΛΥΓ|ΜΑΚ|ΜΕΓ|ΤΑΧ|ΦΙΛ|ΧΩΡ)$/'; + $exceptS2 = '/^(ΔΑΝΕ|ΣΥΝΑΘΡΟ|ΚΛΕ|ΣΕ|ΕΣΩΚΛΕ|ΑΣΕ|ΠΛΕ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . 'ΙΣΤ'; + } + + if (preg_match($exceptS2, $token)) { + $token = $token . 'Ι'; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S6. 6 stems + $re = '/^(.+?)(ΙΣΜΟ|ΙΣΜΟΙ|ΙΣΜΟΣ|ΙΣΜΟΥ|ΙΣΜΟΥΣ|ΙΣΜΩΝ)$/'; + $exceptS1 = '/^(ΑΓΝΩΣΤΙΚ|ΑΤΟΜΙΚ|ΓΝΩΣΤΙΚ|ΕΘΝΙΚ|ΕΚΛΕΚΤΙΚ|ΣΚΕΠΤΙΚ|ΤΟΠΙΚ)$/'; + $exceptS2 = '/^(ΣΕ|ΜΕΤΑΣΕ|ΜΙΚΡΟΣΕ|ΕΓΚΛΕ|ΑΠΟΚΛΕ)$/'; + $exceptS3 = '/^(ΔΑΝΕ|ΑΝΤΙΔΑΝΕ)$/'; + $exceptS4 = '/^(ΑΛΕΞΑΝΔΡΙΝ|ΒΥΖΑΝΤΙΝ|ΘΕΑΤΡΙΝ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = str_replace('ΙΚ', "", $token); + } + + if (preg_match($exceptS2, $token)) { + $token = $token . "ΙΣΜ"; + } + + if (preg_match($exceptS3, $token)) { + $token = $token . "Ι"; + } + + if (preg_match($exceptS4, $token)) { + $token = str_replace('ΙΝ', "", $token); + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S7. 4 stems + $re = '/^(.+?)(ΑΡΑΚΙ|ΑΡΑΚΙΑ|ΟΥΔΑΚΙ|ΟΥΔΑΚΙΑ)$/'; + $exceptS1 = '/^(Σ|Χ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . "AΡΑΚ"; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S8. 8 stems + $re = '/^(.+?)(ΑΚΙ|ΑΚΙΑ|ΙΤΣΑ|ΙΤΣΑΣ|ΙΤΣΕΣ|ΙΤΣΩΝ|ΑΡΑΚΙ|ΑΡΑΚΙΑ)$/'; + $exceptS1 = '/^(ΑΝΘΡ|ΒΑΜΒ|ΒΡ|ΚΑΙΜ|ΚΟΝ|ΚΟΡ|ΛΑΒΡ|ΛΟΥΛ|ΜΕΡ|ΜΟΥΣΤ|ΝΑΓΚΑΣ|ΠΛ|Ρ|ΡΥ|Σ|ΣΚ|ΣΟΚ|ΣΠΑΝ|ΤΖ|ΦΑΡΜ|Χ|' + . 'ΚΑΠΑΚ|ΑΛΙΣΦ|ΑΜΒΡ|ΑΝΘΡ|Κ|ΦΥΛ|ΚΑΤΡΑΠ|ΚΛΙΜ|ΜΑΛ|ΣΛΟΒ|Φ|ΣΦ|ΤΣΕΧΟΣΛΟΒ)$/'; + $exceptS2 = '/^(Β|ΒΑΛ|ΓΙΑΝ|ΓΛ|Ζ|ΗΓΟΥΜΕΝ|ΚΑΡΔ|ΚΟΝ|ΜΑΚΡΥΝ|ΝΥΦ|ΠΑΤΕΡ|Π|ΣΚ|ΤΟΣ|ΤΡΙΠΟΛ)$/'; + + // For words like ΠΛΟΥΣΙΟΚΟΡΙΤΣΑ, ΠΑΛΙΟΚΟΡΙΤΣΑ etc + $exceptS3 = '/(ΚΟΡ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . "ΑΚ"; + } + + if (preg_match($exceptS2, $token)) { + $token = $token . "ΙΤΣ"; + } + + if (preg_match($exceptS3, $token)) { + $token = $token . "ΙΤΣ"; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S9. 3 stems + $re = '/^(.+?)(ΙΔΙΟ|ΙΔΙΑ|ΙΔΙΩΝ)$/'; + $exceptS1 = '/^(ΑΙΦΝ|ΙΡ|ΟΛΟ|ΨΑΛ)$/'; + $exceptS2 = '/(Ε|ΠΑΙΧΝ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . "ΙΔ"; + } + + if (preg_match($exceptS2, $token)) { + $token = $token . "ΙΔ"; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step S10. 4 stems + $re = '/^(.+?)(ΙΣΚΟΣ|ΙΣΚΟΥ|ΙΣΚΟ|ΙΣΚΕ)$/'; + $exceptS1 = '/^(Δ|ΙΒ|ΜΗΝ|Ρ|ΦΡΑΓΚ|ΛΥΚ|ΟΒΕΛ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + + if (preg_match($exceptS1, $token)) { + $token = $token . "ΙΣΚ"; + } + + return $this->toLowerCase($token, $wCase); + } + + // Step 1 + // step1list is used in Step 1. 41 stems + $step1list = array(); + $step1list["ΦΑΓΙΑ"] = "ΦΑ"; + $step1list["ΦΑΓΙΟΥ"] = "ΦΑ"; + $step1list["ΦΑΓΙΩΝ"] = "ΦΑ"; + $step1list["ΣΚΑΓΙΑ"] = "ΣΚΑ"; + $step1list["ΣΚΑΓΙΟΥ"] = "ΣΚΑ"; + $step1list["ΣΚΑΓΙΩΝ"] = "ΣΚΑ"; + $step1list["ΟΛΟΓΙΟΥ"] = "ΟΛΟ"; + $step1list["ΟΛΟΓΙΑ"] = "ΟΛΟ"; + $step1list["ΟΛΟΓΙΩΝ"] = "ΟΛΟ"; + $step1list["ΣΟΓΙΟΥ"] = "ΣΟ"; + $step1list["ΣΟΓΙΑ"] = "ΣΟ"; + $step1list["ΣΟΓΙΩΝ"] = "ΣΟ"; + $step1list["ΤΑΤΟΓΙΑ"] = "ΤΑΤΟ"; + $step1list["ΤΑΤΟΓΙΟΥ"] = "ΤΑΤΟ"; + $step1list["ΤΑΤΟΓΙΩΝ"] = "ΤΑΤΟ"; + $step1list["ΚΡΕΑΣ"] = "ΚΡΕ"; + $step1list["ΚΡΕΑΤΟΣ"] = "ΚΡΕ"; + $step1list["ΚΡΕΑΤΑ"] = "ΚΡΕ"; + $step1list["ΚΡΕΑΤΩΝ"] = "ΚΡΕ"; + $step1list["ΠΕΡΑΣ"] = "ΠΕΡ"; + $step1list["ΠΕΡΑΤΟΣ"] = "ΠΕΡ"; + + // Added by Spyros. Also at $re in step1 + $step1list["ΠΕΡΑΤΗ"] = "ΠΕΡ"; + $step1list["ΠΕΡΑΤΑ"] = "ΠΕΡ"; + $step1list["ΠΕΡΑΤΩΝ"] = "ΠΕΡ"; + $step1list["ΤΕΡΑΣ"] = "ΤΕΡ"; + $step1list["ΤΕΡΑΤΟΣ"] = "ΤΕΡ"; + $step1list["ΤΕΡΑΤΑ"] = "ΤΕΡ"; + $step1list["ΤΕΡΑΤΩΝ"] = "ΤΕΡ"; + $step1list["ΦΩΣ"] = "ΦΩ"; + $step1list["ΦΩΤΟΣ"] = "ΦΩ"; + $step1list["ΦΩΤΑ"] = "ΦΩ"; + $step1list["ΦΩΤΩΝ"] = "ΦΩ"; + $step1list["ΚΑΘΕΣΤΩΣ"] = "ΚΑΘΕΣΤ"; + $step1list["ΚΑΘΕΣΤΩΤΟΣ"] = "ΚΑΘΕΣΤ"; + $step1list["ΚΑΘΕΣΤΩΤΑ"] = "ΚΑΘΕΣΤ"; + $step1list["ΚΑΘΕΣΤΩΤΩΝ"] = "ΚΑΘΕΣΤ"; + $step1list["ΓΕΓΟΝΟΣ"] = "ΓΕΓΟΝ"; + $step1list["ΓΕΓΟΝΟΤΟΣ"] = "ΓΕΓΟΝ"; + $step1list["ΓΕΓΟΝΟΤΑ"] = "ΓΕΓΟΝ"; + $step1list["ΓΕΓΟΝΟΤΩΝ"] = "ΓΕΓΟΝ"; + + $re = '/(.*)(ΦΑΓΙΑ|ΦΑΓΙΟΥ|ΦΑΓΙΩΝ|ΣΚΑΓΙΑ|ΣΚΑΓΙΟΥ|ΣΚΑΓΙΩΝ|ΟΛΟΓΙΟΥ|ΟΛΟΓΙΑ|ΟΛΟΓΙΩΝ|ΣΟΓΙΟΥ|ΣΟΓΙΑ|ΣΟΓΙΩΝ|ΤΑΤΟΓΙΑ|ΤΑΤΟΓΙΟΥ|ΤΑΤΟΓΙΩΝ|ΚΡΕΑΣ|ΚΡΕΑΤΟΣ|' + . 'ΚΡΕΑΤΑ|ΚΡΕΑΤΩΝ|ΠΕΡΑΣ|ΠΕΡΑΤΟΣ|ΠΕΡΑΤΗ|ΠΕΡΑΤΑ|ΠΕΡΑΤΩΝ|ΤΕΡΑΣ|ΤΕΡΑΤΟΣ|ΤΕΡΑΤΑ|ΤΕΡΑΤΩΝ|ΦΩΣ|ΦΩΤΟΣ|ΦΩΤΑ|ΦΩΤΩΝ|ΚΑΘΕΣΤΩΣ|ΚΑΘΕΣΤΩΤΟΣ|' + . 'ΚΑΘΕΣΤΩΤΑ|ΚΑΘΕΣΤΩΤΩΝ|ΓΕΓΟΝΟΣ|ΓΕΓΟΝΟΤΟΣ|ΓΕΓΟΝΟΤΑ|ΓΕΓΟΝΟΤΩΝ)$/'; + + if (preg_match($re, $token, $match)) { + $stem = $match[1]; + $suffix = $match[2]; + $token = $stem . (array_key_exists($suffix, $step1list) ? $step1list[$suffix] : ''); + $test1 = false; + } + + // Step 2a. 2 stems + $re = '/^(.+?)(ΑΔΕΣ|ΑΔΩΝ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1]; + $re = '/(ΟΚ|ΜΑΜ|ΜΑΝ|ΜΠΑΜΠ|ΠΑΤΕΡ|ΓΙΑΓΙ|ΝΤΑΝΤ|ΚΥΡ|ΘΕΙ|ΠΕΘΕΡ)$/'; + + if (!preg_match($re, $token)) { + $token = $token . "ΑΔ"; + } + } + + // Step 2b. 2 stems + $re = '/^(.+?)(ΕΔΕΣ|ΕΔΩΝ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $exept2 = '/(ΟΠ|ΙΠ|ΕΜΠ|ΥΠ|ΓΗΠ|ΔΑΠ|ΚΡΑΣΠ|ΜΙΛ)$/'; + + if (preg_match($exept2, $token)) { + $token = $token . 'ΕΔ'; + } + } + + // Step 2c + $re = '/^(.+?)(ΟΥΔΕΣ|ΟΥΔΩΝ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + + $exept3 = '/(ΑΡΚ|ΚΑΛΙΑΚ|ΠΕΤΑΛ|ΛΙΧ|ΠΛΕΞ|ΣΚ|Σ|ΦΛ|ΦΡ|ΒΕΛ|ΛΟΥΛ|ΧΝ|ΣΠ|ΤΡΑΓ|ΦΕ)$/'; + + if (preg_match($exept3, $token)) { + $token = $token . 'ΟΥΔ'; + } + } + + // Step 2d + $re = '/^(.+?)(ΕΩΣ|ΕΩΝ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept4 = '/^(Θ|Δ|ΕΛ|ΓΑΛ|Ν|Π|ΙΔ|ΠΑΡ)$/'; + + if (preg_match($exept4, $token)) { + $token = $token . 'Ε'; + } + } + + // Step 3 + $re = '/^(.+?)(ΙΑ|ΙΟΥ|ΙΩΝ)$/'; + + if (preg_match($re, $token, $fp)) { + $stem = $fp[1]; + $token = $stem; + $re = '/' . $v . '$/'; + $test1 = false; + + if (preg_match($re, $token)) { + $token = $stem . 'Ι'; + } + } + + // Step 4 + $re = '/^(.+?)(ΙΚΑ|ΙΚΟ|ΙΚΟΥ|ΙΚΩΝ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $re = '/' . $v . '$/'; + $exept5 = '/^(ΑΛ|ΑΔ|ΕΝΔ|ΑΜΑΝ|ΑΜΜΟΧΑΛ|ΗΘ|ΑΝΗΘ|ΑΝΤΙΔ|ΦΥΣ|ΒΡΩΜ|ΓΕΡ|ΕΞΩΔ|ΚΑΛΠ|ΚΑΛΛΙΝ|ΚΑΤΑΔ|ΜΟΥΛ|ΜΠΑΝ|ΜΠΑΓΙΑΤ|ΜΠΟΛ|ΜΠΟΣ|ΝΙΤ|ΞΙΚ|ΣΥΝΟΜΗΛ|ΠΕΤΣ|' + . 'ΠΙΤΣ|ΠΙΚΑΝΤ|ΠΛΙΑΤΣ|ΠΟΣΤΕΛΝ|ΠΡΩΤΟΔ|ΣΕΡΤ|ΣΥΝΑΔ|ΤΣΑΜ|ΥΠΟΔ|ΦΙΛΟΝ|ΦΥΛΟΔ|ΧΑΣ)$/'; + + if (preg_match($re, $token) || preg_match($exept5, $token)) { + $token = $token . 'ΙΚ'; + } + } + + // Step 5a + $re = '/^(.+?)(ΑΜΕ)$/'; + $re2 = '/^(.+?)(ΑΓΑΜΕ|ΗΣΑΜΕ|ΟΥΣΑΜΕ|ΗΚΑΜΕ|ΗΘΗΚΑΜΕ)$/'; + + if ($token == "ΑΓΑΜΕ") { + $token = "ΑΓΑΜ"; + } + + if (preg_match($re2, $token)) { + preg_match($re2, $token, $match); + $token = $match[1]; + $test1 = false; + } + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept6 = '/^(ΑΝΑΠ|ΑΠΟΘ|ΑΠΟΚ|ΑΠΟΣΤ|ΒΟΥΒ|ΞΕΘ|ΟΥΛ|ΠΕΘ|ΠΙΚΡ|ΠΟΤ|ΣΙΧ|Χ)$/'; + + if (preg_match($exept6, $token)) { + $token = $token . "ΑΜ"; + } + } + + // Step 5b + $re2 = '/^(.+?)(ΑΝΕ)$/'; + $re3 = '/^(.+?)(ΑΓΑΝΕ|ΗΣΑΝΕ|ΟΥΣΑΝΕ|ΙΟΝΤΑΝΕ|ΙΟΤΑΝΕ|ΙΟΥΝΤΑΝΕ|ΟΝΤΑΝΕ|ΟΤΑΝΕ|ΟΥΝΤΑΝΕ|ΗΚΑΝΕ|ΗΘΗΚΑΝΕ)$/'; + + if (preg_match($re3, $token)) { + preg_match($re3, $token, $match); + $token = $match[1]; + $test1 = false; + $re3 = '/^(ΤΡ|ΤΣ)$/'; + + if (preg_match($re3, $token)) { + $token = $token . "ΑΓΑΝ"; + } + } + + if (preg_match($re2, $token)) { + preg_match($re2, $token, $match); + $token = $match[1]; + $test1 = false; + $re2 = '/' . $v2 . '$/'; + $exept7 = '/^(ΒΕΤΕΡ|ΒΟΥΛΚ|ΒΡΑΧΜ|Γ|ΔΡΑΔΟΥΜ|Θ|ΚΑΛΠΟΥΖ|ΚΑΣΤΕΛ|ΚΟΡΜΟΡ|ΛΑΟΠΛ|ΜΩΑΜΕΘ|Μ|ΜΟΥΣΟΥΛΜ|Ν|ΟΥΛ|Π|ΠΕΛΕΚ|ΠΛ|ΠΟΛΙΣ|ΠΟΡΤΟΛ|ΣΑΡΑΚΑΤΣ|ΣΟΥΛΤ|' + . 'ΤΣΑΡΛΑΤ|ΟΡΦ|ΤΣΙΓΓ|ΤΣΟΠ|ΦΩΤΟΣΤΕΦ|Χ|ΨΥΧΟΠΛ|ΑΓ|ΟΡΦ|ΓΑΛ|ΓΕΡ|ΔΕΚ|ΔΙΠΛ|ΑΜΕΡΙΚΑΝ|ΟΥΡ|ΠΙΘ|ΠΟΥΡΙΤ|Σ|ΖΩΝΤ|ΙΚ|ΚΑΣΤ|ΚΟΠ|ΛΙΧ|ΛΟΥΘΗΡ|ΜΑΙΝΤ|' + . 'ΜΕΛ|ΣΙΓ|ΣΠ|ΣΤΕΓ|ΤΡΑΓ|ΤΣΑΓ|Φ|ΕΡ|ΑΔΑΠ|ΑΘΙΓΓ|ΑΜΗΧ|ΑΝΙΚ|ΑΝΟΡΓ|ΑΠΗΓ|ΑΠΙΘ|ΑΤΣΙΓΓ|ΒΑΣ|ΒΑΣΚ|ΒΑΘΥΓΑΛ|ΒΙΟΜΗΧ|ΒΡΑΧΥΚ|ΔΙΑΤ|ΔΙΑΦ|ΕΝΟΡΓ|' + . 'ΘΥΣ|ΚΑΠΝΟΒΙΟΜΗΧ|ΚΑΤΑΓΑΛ|ΚΛΙΒ|ΚΟΙΛΑΡΦ|ΛΙΒ|ΜΕΓΛΟΒΙΟΜΗΧ|ΜΙΚΡΟΒΙΟΜΗΧ|ΝΤΑΒ|ΞΗΡΟΚΛΙΒ|ΟΛΙΓΟΔΑΜ|ΟΛΟΓΑΛ|ΠΕΝΤΑΡΦ|ΠΕΡΗΦ|ΠΕΡΙΤΡ|ΠΛΑΤ|' + . 'ΠΟΛΥΔΑΠ|ΠΟΛΥΜΗΧ|ΣΤΕΦ|ΤΑΒ|ΤΕΤ|ΥΠΕΡΗΦ|ΥΠΟΚΟΠ|ΧΑΜΗΛΟΔΑΠ|ΨΗΛΟΤΑΒ)$/'; + + if (preg_match($re2, $token) || preg_match($exept7, $token)) { + $token = $token . "ΑΝ"; + } + } + + // Step 5c + $re3 = '/^(.+?)(ΕΤΕ)$/'; + $re4 = '/^(.+?)(ΗΣΕΤΕ)$/'; + + if (preg_match($re4, $token)) { + preg_match($re4, $token, $match); + $token = $match[1]; + $test1 = false; + } + + if (preg_match($re3, $token)) { + preg_match($re3, $token, $match); + $token = $match[1]; + $test1 = false; + $re3 = '/' . $v2 . '$/'; + $exept8 = '/(ΟΔ|ΑΙΡ|ΦΟΡ|ΤΑΘ|ΔΙΑΘ|ΣΧ|ΕΝΔ|ΕΥΡ|ΤΙΘ|ΥΠΕΡΘ|ΡΑΘ|ΕΝΘ|ΡΟΘ|ΣΘ|ΠΥΡ|ΑΙΝ|ΣΥΝΔ|ΣΥΝ|ΣΥΝΘ|ΧΩΡ|ΠΟΝ|ΒΡ|ΚΑΘ|ΕΥΘ|ΕΚΘ|ΝΕΤ|ΡΟΝ|ΑΡΚ|ΒΑΡ|ΒΟΛ|ΩΦΕΛ)$/'; + $exept9 = '/^(ΑΒΑΡ|ΒΕΝ|ΕΝΑΡ|ΑΒΡ|ΑΔ|ΑΘ|ΑΝ|ΑΠΛ|ΒΑΡΟΝ|ΝΤΡ|ΣΚ|ΚΟΠ|ΜΠΟΡ|ΝΙΦ|ΠΑΓ|ΠΑΡΑΚΑΛ|ΣΕΡΠ|ΣΚΕΛ|ΣΥΡΦ|ΤΟΚ|Υ|Δ|ΕΜ|ΘΑΡΡ|Θ)$/'; + + if (preg_match($re3, $token) || preg_match($exept8, $token) || preg_match($exept9, $token)) { + $token = $token . "ΕΤ"; + } + } + + // Step 5d + $re = '/^(.+?)(ΟΝΤΑΣ|ΩΝΤΑΣ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept10 = '/^(ΑΡΧ)$/'; + $exept11 = '/(ΚΡΕ)$/'; + + if (preg_match($exept10, $token)) { + $token = $token . "ΟΝΤ"; + } + + if (preg_match($exept11, $token)) { + $token = $token . "ΩΝΤ"; + } + } + + // Step 5e + $re = '/^(.+?)(ΟΜΑΣΤΕ|ΙΟΜΑΣΤΕ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept11 = '/^(ΟΝ)$/'; + + if (preg_match($exept11, $token)) { + $token = $token . "ΟΜΑΣΤ"; + } + } + + // Step 5f + $re = '/^(.+?)(ΕΣΤΕ)$/'; + $re2 = '/^(.+?)(ΙΕΣΤΕ)$/'; + + if (preg_match($re2, $token)) { + preg_match($re2, $token, $match); + $token = $match[1]; + $test1 = false; + $re2 = '/^(Π|ΑΠ|ΣΥΜΠ|ΑΣΥΜΠ|ΑΚΑΤΑΠ|ΑΜΕΤΑΜΦ)$/'; + + if (preg_match($re2, $token)) { + $token = $token . "ΙΕΣΤ"; + } + } + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept12 = '/^(ΑΛ|ΑΡ|ΕΚΤΕΛ|Ζ|Μ|Ξ|ΠΑΡΑΚΑΛ|ΑΡ|ΠΡΟ|ΝΙΣ)$/'; + + if (preg_match($exept12, $token)) { + $token = $token . "ΕΣΤ"; + } + } + + // Step 5g + $re = '/^(.+?)(ΗΚΑ|ΗΚΕΣ|ΗΚΕ)$/'; + $re2 = '/^(.+?)(ΗΘΗΚΑ|ΗΘΗΚΕΣ|ΗΘΗΚΕ)$/'; + + if (preg_match($re2, $token)) { + preg_match($re2, $token, $match); + $token = $match[1]; + $test1 = false; + } + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept13 = '/(ΣΚΩΛ|ΣΚΟΥΛ|ΝΑΡΘ|ΣΦ|ΟΘ|ΠΙΘ)$/'; + $exept14 = '/^(ΔΙΑΘ|Θ|ΠΑΡΑΚΑΤΑΘ|ΠΡΟΣΘ|ΣΥΝΘ|)$/'; + + if (preg_match($exept13, $token) || preg_match($exept14, $token)) { + $token = $token . "ΗΚ"; + } + } + + // Step 5h + $re = '/^(.+?)(ΟΥΣΑ|ΟΥΣΕΣ|ΟΥΣΕ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept15 = '/^(ΦΑΡΜΑΚ|ΧΑΔ|ΑΓΚ|ΑΝΑΡΡ|ΒΡΟΜ|ΕΚΛΙΠ|ΛΑΜΠΙΔ|ΛΕΧ|Μ|ΠΑΤ|Ρ|Λ|ΜΕΔ|ΜΕΣΑΖ|ΥΠΟΤΕΙΝ|ΑΜ|ΑΙΘ|ΑΝΗΚ|ΔΕΣΠΟΖ|ΕΝΔΙΑΦΕΡ|ΔΕ|ΔΕΥΤΕΡΕΥ|ΚΑΘΑΡΕΥ|ΠΛΕ|ΤΣΑ)$/'; + $exept16 = '/(ΠΟΔΑΡ|ΒΛΕΠ|ΠΑΝΤΑΧ|ΦΡΥΔ|ΜΑΝΤΙΛ|ΜΑΛΛ|ΚΥΜΑΤ|ΛΑΧ|ΛΗΓ|ΦΑΓ|ΟΜ|ΠΡΩΤ)$/'; + + if (preg_match($exept15, $token) || preg_match($exept16, $token)) { + $token = $token . "ΟΥΣ"; + } + } + + // Step 5i + $re = '/^(.+?)(ΑΓΑ|ΑΓΕΣ|ΑΓΕ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept17 = '/^(ΨΟΦ|ΝΑΥΛΟΧ)$/'; + $exept20 = '/(ΚΟΛΛ)$/'; + $exept18 = '/^(ΑΒΑΣΤ|ΠΟΛΥΦ|ΑΔΗΦ|ΠΑΜΦ|Ρ|ΑΣΠ|ΑΦ|ΑΜΑΛ|ΑΜΑΛΛΙ|ΑΝΥΣΤ|ΑΠΕΡ|ΑΣΠΑΡ|ΑΧΑΡ|ΔΕΡΒΕΝ|ΔΡΟΣΟΠ|ΞΕΦ|ΝΕΟΠ|ΝΟΜΟΤ|ΟΛΟΠ|ΟΜΟΤ|ΠΡΟΣΤ|ΠΡΟΣΩΠΟΠ|' + . 'ΣΥΜΠ|ΣΥΝΤ|Τ|ΥΠΟΤ|ΧΑΡ|ΑΕΙΠ|ΑΙΜΟΣΤ|ΑΝΥΠ|ΑΠΟΤ|ΑΡΤΙΠ|ΔΙΑΤ|ΕΝ|ΕΠΙΤ|ΚΡΟΚΑΛΟΠ|ΣΙΔΗΡΟΠ|Λ|ΝΑΥ|ΟΥΛΑΜ|ΟΥΡ|Π|ΤΡ|Μ)$/'; + $exept19 = '/(ΟΦ|ΠΕΛ|ΧΟΡΤ|ΛΛ|ΣΦ|ΡΠ|ΦΡ|ΠΡ|ΛΟΧ|ΣΜΗΝ)$/'; + + if ( + (preg_match($exept18, $token) || preg_match($exept19, $token)) + && !(preg_match($exept17, $token) || preg_match($exept20, $token)) + ) { + $token = $token . "ΑΓ"; + } + } + + // Step 5j + $re = '/^(.+?)(ΗΣΕ|ΗΣΟΥ|ΗΣΑ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept21 = '/^(Ν|ΧΕΡΣΟΝ|ΔΩΔΕΚΑΝ|ΕΡΗΜΟΝ|ΜΕΓΑΛΟΝ|ΕΠΤΑΝ)$/'; + + if (preg_match($exept21, $token)) { + $token = $token . "ΗΣ"; + } + } + + // Step 5k + $re = '/^(.+?)(ΗΣΤΕ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept22 = '/^(ΑΣΒ|ΣΒ|ΑΧΡ|ΧΡ|ΑΠΛ|ΑΕΙΜΝ|ΔΥΣΧΡ|ΕΥΧΡ|ΚΟΙΝΟΧΡ|ΠΑΛΙΜΨ)$/'; + + if (preg_match($exept22, $token)) { + $token = $token . "ΗΣΤ"; + } + } + + // Step 5l + $re = '/^(.+?)(ΟΥΝΕ|ΗΣΟΥΝΕ|ΗΘΟΥΝΕ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept23 = '/^(Ν|Ρ|ΣΠΙ|ΣΤΡΑΒΟΜΟΥΤΣ|ΚΑΚΟΜΟΥΤΣ|ΕΞΩΝ)$/'; + + if (preg_match($exept23, $token)) { + $token = $token . "ΟΥΝ"; + } + } + + // Step 5m + $re = '/^(.+?)(ΟΥΜΕ|ΗΣΟΥΜΕ|ΗΘΟΥΜΕ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + $test1 = false; + $exept24 = '/^(ΠΑΡΑΣΟΥΣ|Φ|Χ|ΩΡΙΟΠΛ|ΑΖ|ΑΛΛΟΣΟΥΣ|ΑΣΟΥΣ)$/'; + + if (preg_match($exept24, $token)) { + $token = $token . "ΟΥΜ"; + } + } + + // Step 6 + $re = '/^(.+?)(ΜΑΤΑ|ΜΑΤΩΝ|ΜΑΤΟΣ)$/'; + $re2 = '/^(.+?)(Α|ΑΓΑΤΕ|ΑΓΑΝ|ΑΕΙ|ΑΜΑΙ|ΑΝ|ΑΣ|ΑΣΑΙ|ΑΤΑΙ|ΑΩ|Ε|ΕΙ|ΕΙΣ|ΕΙΤΕ|ΕΣΑΙ|ΕΣ|ΕΤΑΙ|Ι|ΙΕΜΑΙ|ΙΕΜΑΣΤΕ|ΙΕΤΑΙ|ΙΕΣΑΙ|ΙΕΣΑΣΤΕ|ΙΟΜΑΣΤΑΝ|ΙΟΜΟΥΝ|' + . 'ΙΟΜΟΥΝΑ|ΙΟΝΤΑΝ|ΙΟΝΤΟΥΣΑΝ|ΙΟΣΑΣΤΑΝ|ΙΟΣΑΣΤΕ|ΙΟΣΟΥΝ|ΙΟΣΟΥΝΑ|ΙΟΤΑΝ|ΙΟΥΜΑ|ΙΟΥΜΑΣΤΕ|ΙΟΥΝΤΑΙ|ΙΟΥΝΤΑΝ|Η|ΗΔΕΣ|ΗΔΩΝ|ΗΘΕΙ|ΗΘΕΙΣ|ΗΘΕΙΤΕ|' + . 'ΗΘΗΚΑΤΕ|ΗΘΗΚΑΝ|ΗΘΟΥΝ|ΗΘΩ|ΗΚΑΤΕ|ΗΚΑΝ|ΗΣ|ΗΣΑΝ|ΗΣΑΤΕ|ΗΣΕΙ|ΗΣΕΣ|ΗΣΟΥΝ|ΗΣΩ|Ο|ΟΙ|ΟΜΑΙ|ΟΜΑΣΤΑΝ|ΟΜΟΥΝ|ΟΜΟΥΝΑ|ΟΝΤΑΙ|ΟΝΤΑΝ|ΟΝΤΟΥΣΑΝ|ΟΣ|' + . 'ΟΣΑΣΤΑΝ|ΟΣΑΣΤΕ|ΟΣΟΥΝ|ΟΣΟΥΝΑ|ΟΤΑΝ|ΟΥ|ΟΥΜΑΙ|ΟΥΜΑΣΤΕ|ΟΥΝ|ΟΥΝΤΑΙ|ΟΥΝΤΑΝ|ΟΥΣ|ΟΥΣΑΝ|ΟΥΣΑΤΕ|Υ|ΥΣ|Ω|ΩΝ)$/'; + + if (preg_match($re, $token, $match)) { + $token = $match[1] . "ΜΑ"; + } + + if (preg_match($re2, $token) && $test1) { + preg_match($re2, $token, $match); + $token = $match[1]; + } + + // Step 7 (ΠΑΡΑΘΕΤΙΚΑ) + $re = '/^(.+?)(ΕΣΤΕΡ|ΕΣΤΑΤ|ΟΤΕΡ|ΟΤΑΤ|ΥΤΕΡ|ΥΤΑΤ|ΩΤΕΡ|ΩΤΑΤ)$/'; + + if (preg_match($re, $token)) { + preg_match($re, $token, $match); + $token = $match[1]; + } + + return $this->toLowerCase($token, $wCase); + } + + /** + * Converts the token to uppercase, suppressing accents and diaeresis. The array $wCase contains a special map of + * the uppercase rule used to convert each character at each position. + * + * @param string $token Token to process + * @param array &$wCase Map of uppercase rules + * + * @return string + * + * @since 4.0.0 + */ + protected function toUpperCase($token, &$wCase) + { + $wCase = array_fill(0, mb_strlen($token, 'UTF-8'), 0); + $caseConvert = array( + "α" => 'Α', + "β" => 'Β', + "γ" => 'Γ', + "δ" => 'Δ', + "ε" => 'Ε', + "ζ" => 'Ζ', + "η" => 'Η', + "θ" => 'Θ', + "ι" => 'Ι', + "κ" => 'Κ', + "λ" => 'Λ', + "μ" => 'Μ', + "ν" => 'Ν', + "ξ" => 'Ξ', + "ο" => 'Ο', + "π" => 'Π', + "ρ" => 'Ρ', + "σ" => 'Σ', + "τ" => 'Τ', + "υ" => 'Υ', + "φ" => 'Φ', + "χ" => 'Χ', + "ψ" => 'Ψ', + "ω" => 'Ω', + "ά" => 'Α', + "έ" => 'Ε', + "ή" => 'Η', + "ί" => 'Ι', + "ό" => 'Ο', + "ύ" => 'Υ', + "ώ" => 'Ω', + "ς" => 'Σ', + "ϊ" => 'Ι', + "ϋ" => 'Ι', + "ΐ" => 'Ι', + "ΰ" => 'Υ', + ); + $newToken = ''; + + for ($i = 0; $i < mb_strlen($token); $i++) { + $char = mb_substr($token, $i, 1); + $isLower = array_key_exists($char, $caseConvert); + + if (!$isLower) { + $newToken .= $char; + + continue; + } + + $upperCase = $caseConvert[$char]; + $newToken .= $upperCase; + + $wCase[$i] = 1; + + if (in_array($char, ['ά', 'έ', 'ή', 'ί', 'ό', 'ύ', 'ώ', 'ς'])) { + $wCase[$i] = 2; + } + + if (in_array($char, ['ϊ', 'ϋ'])) { + $wCase[$i] = 3; + } + + if (in_array($char, ['ΐ', 'ΰ'])) { + $wCase[$i] = 4; + } + } + + return $newToken; + } + + /** + * Converts the suppressed uppercase token back to lowercase, using the $wCase map to add back the accents, + * diaeresis and handle the special case of final sigma (different lowercase glyph than the regular sigma, only + * used at the end of words). + * + * @param string $token Token to process + * @param array $wCase Map of lowercase rules + * + * @return string + * + * @since 4.0.0 + */ + protected function toLowerCase($token, $wCase) + { + $newToken = ''; + + for ($i = 0; $i < mb_strlen($token); $i++) { + $char = mb_substr($token, $i, 1); + + // Is $wCase not set at this position? We assume no case conversion ever took place. + if (!isset($wCase[$i])) { + $newToken .= $char; + + continue; + } + + // The character was not case-converted + if ($wCase[$i] == 0) { + $newToken .= $char; + + continue; + } + + // Case 1: Unaccented letter + if ($wCase[$i] == 1) { + $newToken .= mb_strtolower($char); + + continue; + } + + // Case 2: Vowel with accent (tonos); or the special case of final sigma + if ($wCase[$i] == 2) { + $charMap = [ + 'Α' => 'ά', + 'Ε' => 'έ', + 'Η' => 'ή', + 'Ι' => 'ί', + 'Ο' => 'ό', + 'Υ' => 'ύ', + 'Ω' => 'ώ', + 'Σ' => 'ς' + ]; + + $newToken .= $charMap[$char]; + + continue; + } + + // Case 3: vowels with diaeresis (dialytika) + if ($wCase[$i] == 3) { + $charMap = [ + 'Ι' => 'ϊ', + 'Υ' => 'ϋ' + ]; + + $newToken .= $charMap[$char]; + + continue; + } + + // Case 4: vowels with both diaeresis (dialytika) and accent (tonos) + if ($wCase[$i] == 4) { + $charMap = [ + 'Ι' => 'ΐ', + 'Υ' => 'ΰ' + ]; + + $newToken .= $charMap[$char]; + + continue; + } + + // This should never happen! + $newToken .= $char; + } + + return $newToken; + } } diff --git a/administrator/components/com_finder/src/Indexer/Language/Zh.php b/administrator/components/com_finder/src/Indexer/Language/Zh.php index 7e5bcbfa00551..3e4539cbf1f1f 100644 --- a/administrator/components/com_finder/src/Indexer/Language/Zh.php +++ b/administrator/components/com_finder/src/Indexer/Language/Zh.php @@ -1,4 +1,5 @@ clean($format, 'cmd'); - - // Only create one parser for each format. - if (isset(self::$instances[$format])) - { - return self::$instances[$format]; - } - - // Setup the adapter for the parser. - $class = '\\Joomla\\Component\\Finder\\Administrator\\Indexer\\Parser\\' . ucfirst($format); - - // Check if a parser exists for the format. - if (class_exists($class)) - { - self::$instances[$format] = new $class; - - return self::$instances[$format]; - } - - // Throw invalid format exception. - throw new \Exception(Text::sprintf('COM_FINDER_INDEXER_INVALID_PARSER', $format)); - } - - /** - * Method to parse input and extract the plain text. Because this method is - * called from both inside and outside the indexer, it needs to be able to - * batch out its parsing functionality to deal with the inefficiencies of - * regular expressions. We will parse recursively in 2KB chunks. - * - * @param string $input The input to parse. - * - * @return string The plain text input. - * - * @since 2.5 - */ - public function parse($input) - { - // If the input is less than 2KB we can parse it in one go. - if (strlen($input) <= 2048) - { - return $this->process($input); - } - - // Input is longer than 2Kb so parse it in chunks of 2Kb or less. - $start = 0; - $end = strlen($input); - $chunk = 2048; - $return = null; - - while ($start < $end) - { - // Setup the string. - $string = substr($input, $start, $chunk); - - // Find the last space character if we aren't at the end. - $ls = (($start + $chunk) < $end ? strrpos($string, ' ') : false); - - // Truncate to the last space character. - if ($ls !== false) - { - $string = substr($string, 0, $ls); - } - - // Adjust the start position for the next iteration. - $start += ($ls !== false ? ($ls + 1 - $chunk) + $chunk : $chunk); - - // Parse the chunk. - $return .= $this->process($string); - } - - return $return; - } - - /** - * Method to process input and extract the plain text. - * - * @param string $input The input to process. - * - * @return string The plain text input. - * - * @since 2.5 - */ - abstract protected function process($input); + /** + * Parser support instances container. + * + * @var Parser[] + * @since 4.0.0 + */ + protected static $instances = array(); + + /** + * Method to get a parser, creating it if necessary. + * + * @param string $format The type of parser to load. + * + * @return Parser A Parser instance. + * + * @since 2.5 + * @throws \Exception on invalid parser. + */ + public static function getInstance($format) + { + $format = InputFilter::getInstance()->clean($format, 'cmd'); + + // Only create one parser for each format. + if (isset(self::$instances[$format])) { + return self::$instances[$format]; + } + + // Setup the adapter for the parser. + $class = '\\Joomla\\Component\\Finder\\Administrator\\Indexer\\Parser\\' . ucfirst($format); + + // Check if a parser exists for the format. + if (class_exists($class)) { + self::$instances[$format] = new $class(); + + return self::$instances[$format]; + } + + // Throw invalid format exception. + throw new \Exception(Text::sprintf('COM_FINDER_INDEXER_INVALID_PARSER', $format)); + } + + /** + * Method to parse input and extract the plain text. Because this method is + * called from both inside and outside the indexer, it needs to be able to + * batch out its parsing functionality to deal with the inefficiencies of + * regular expressions. We will parse recursively in 2KB chunks. + * + * @param string $input The input to parse. + * + * @return string The plain text input. + * + * @since 2.5 + */ + public function parse($input) + { + // If the input is less than 2KB we can parse it in one go. + if (strlen($input) <= 2048) { + return $this->process($input); + } + + // Input is longer than 2Kb so parse it in chunks of 2Kb or less. + $start = 0; + $end = strlen($input); + $chunk = 2048; + $return = null; + + while ($start < $end) { + // Setup the string. + $string = substr($input, $start, $chunk); + + // Find the last space character if we aren't at the end. + $ls = (($start + $chunk) < $end ? strrpos($string, ' ') : false); + + // Truncate to the last space character. + if ($ls !== false) { + $string = substr($string, 0, $ls); + } + + // Adjust the start position for the next iteration. + $start += ($ls !== false ? ($ls + 1 - $chunk) + $chunk : $chunk); + + // Parse the chunk. + $return .= $this->process($string); + } + + return $return; + } + + /** + * Method to process input and extract the plain text. + * + * @param string $input The input to process. + * + * @return string The plain text input. + * + * @since 2.5 + */ + abstract protected function process($input); } diff --git a/administrator/components/com_finder/src/Indexer/Parser/Html.php b/administrator/components/com_finder/src/Indexer/Parser/Html.php index 2c59281085828..e5c11c095371d 100644 --- a/administrator/components/com_finder/src/Indexer/Parser/Html.php +++ b/administrator/components/com_finder/src/Indexer/Parser/Html.php @@ -1,4 +1,5 @@ and tags. Do this first - // because there might be '); - - // Decode HTML entities. - $input = html_entity_decode($input, ENT_QUOTES, 'UTF-8'); - - // Convert entities equivalent to spaces to actual spaces. - $input = str_replace(array(' ', ' '), ' ', $input); - - // Add a space before both the OPEN and CLOSE tags of BLOCK and LINE BREAKING elements, - // e.g. 'all

mobile List

' will become 'all mobile List' - $input = preg_replace('/(<|<\/)(' . - 'address|article|aside|blockquote|br|canvas|dd|div|dl|dt|' . - 'fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|li|' . - 'main|nav|noscript|ol|output|p|pre|section|table|tfoot|ul|video' . - ')\b/i', ' $1$2', $input - ); - - // Strip HTML tags. - $input = strip_tags($input); - - return parent::parse($input); - } - - /** - * Method to process HTML input and extract the plain text. - * - * @param string $input The input to process. - * - * @return string The plain text input. - * - * @since 2.5 - */ - protected function process($input) - { - // Replace any amount of white space with a single space. - return preg_replace('#\s+#u', ' ', $input); - } - - /** - * Method to remove blocks of text between a start and an end tag. - * Each block removed is effectively replaced by a single space. - * - * Note: The start tag and the end tag must be different. - * Note: Blocks must not be nested. - * Note: This method will function correctly with multi-byte strings. - * - * @param string $input String to be processed. - * @param string $startTag String representing the start tag. - * @param string $endTag String representing the end tag. - * - * @return string with blocks removed. - * - * @since 3.4 - */ - private function removeBlocks($input, $startTag, $endTag) - { - $return = ''; - $offset = 0; - $startTagLength = strlen($startTag); - $endTagLength = strlen($endTag); - - // Find the first start tag. - $start = stripos($input, $startTag); - - // If no start tags were found, return the string unchanged. - if ($start === false) - { - return $input; - } - - // Look for all blocks defined by the start and end tags. - while ($start !== false) - { - // Accumulate the substring up to the start tag. - $return .= substr($input, $offset, $start - $offset) . ' '; - - // Look for an end tag corresponding to the start tag. - $end = stripos($input, $endTag, $start + $startTagLength); - - // If no corresponding end tag, leave the string alone. - if ($end === false) - { - // Fix the offset so part of the string is not duplicated. - $offset = $start; - break; - } - - // Advance the start position. - $offset = $end + $endTagLength; - - // Look for the next start tag and loop. - $start = stripos($input, $startTag, $offset); - } - - // Add in the final substring after the last end tag. - $return .= substr($input, $offset); - - return $return; - } + /** + * Method to parse input and extract the plain text. Because this method is + * called from both inside and outside the indexer, it needs to be able to + * batch out its parsing functionality to deal with the inefficiencies of + * regular expressions. We will parse recursively in 2KB chunks. + * + * @param string $input The input to parse. + * + * @return string The plain text input. + * + * @since 2.5 + */ + public function parse($input) + { + // Strip invalid UTF-8 characters. + $oldSetting = ini_get('mbstring.substitute_character'); + ini_set('mbstring.substitute_character', 'none'); + $input = mb_convert_encoding($input, 'UTF-8', 'UTF-8'); + ini_set('mbstring.substitute_character', $oldSetting); + + // Remove anything between and tags. Do this first + // because there might be '); + + // Decode HTML entities. + $input = html_entity_decode($input, ENT_QUOTES, 'UTF-8'); + + // Convert entities equivalent to spaces to actual spaces. + $input = str_replace(array(' ', ' '), ' ', $input); + + // Add a space before both the OPEN and CLOSE tags of BLOCK and LINE BREAKING elements, + // e.g. 'all

mobile List

' will become 'all mobile List' + $input = preg_replace('/(<|<\/)(' . + 'address|article|aside|blockquote|br|canvas|dd|div|dl|dt|' . + 'fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|li|' . + 'main|nav|noscript|ol|output|p|pre|section|table|tfoot|ul|video' . + ')\b/i', ' $1$2', $input); + + // Strip HTML tags. + $input = strip_tags($input); + + return parent::parse($input); + } + + /** + * Method to process HTML input and extract the plain text. + * + * @param string $input The input to process. + * + * @return string The plain text input. + * + * @since 2.5 + */ + protected function process($input) + { + // Replace any amount of white space with a single space. + return preg_replace('#\s+#u', ' ', $input); + } + + /** + * Method to remove blocks of text between a start and an end tag. + * Each block removed is effectively replaced by a single space. + * + * Note: The start tag and the end tag must be different. + * Note: Blocks must not be nested. + * Note: This method will function correctly with multi-byte strings. + * + * @param string $input String to be processed. + * @param string $startTag String representing the start tag. + * @param string $endTag String representing the end tag. + * + * @return string with blocks removed. + * + * @since 3.4 + */ + private function removeBlocks($input, $startTag, $endTag) + { + $return = ''; + $offset = 0; + $startTagLength = strlen($startTag); + $endTagLength = strlen($endTag); + + // Find the first start tag. + $start = stripos($input, $startTag); + + // If no start tags were found, return the string unchanged. + if ($start === false) { + return $input; + } + + // Look for all blocks defined by the start and end tags. + while ($start !== false) { + // Accumulate the substring up to the start tag. + $return .= substr($input, $offset, $start - $offset) . ' '; + + // Look for an end tag corresponding to the start tag. + $end = stripos($input, $endTag, $start + $startTagLength); + + // If no corresponding end tag, leave the string alone. + if ($end === false) { + // Fix the offset so part of the string is not duplicated. + $offset = $start; + break; + } + + // Advance the start position. + $offset = $end + $endTagLength; + + // Look for the next start tag and loop. + $start = stripos($input, $startTag, $offset); + } + + // Add in the final substring after the last end tag. + $return .= substr($input, $offset); + + return $return; + } } diff --git a/administrator/components/com_finder/src/Indexer/Parser/Rtf.php b/administrator/components/com_finder/src/Indexer/Parser/Rtf.php index 3006ad8194159..d61dd66810032 100644 --- a/administrator/components/com_finder/src/Indexer/Parser/Rtf.php +++ b/administrator/components/com_finder/src/Indexer/Parser/Rtf.php @@ -1,4 +1,5 @@ get(DatabaseInterface::class); - } - - $this->setDatabase($db); - - // Get the input string. - $this->input = $options['input'] ?? ''; - - // Get the empty query setting. - $this->empty = isset($options['empty']) ? (bool) $options['empty'] : false; - - // Get the input language. - $this->language = !empty($options['language']) ? $options['language'] : Helper::getDefaultLanguage(); - - // Get the matching mode. - $this->mode = 'AND'; - - // Set the word matching mode - $this->wordmode = !empty($options['word_match']) ? $options['word_match'] : 'exact'; - - // Initialize the temporary date storage. - $this->dates = new Registry; - - // Populate the temporary date storage. - if (!empty($options['date1'])) - { - $this->dates->set('date1', $options['date1']); - } - - if (!empty($options['date2'])) - { - $this->dates->set('date2', $options['date2']); - } - - if (!empty($options['when1'])) - { - $this->dates->set('when1', $options['when1']); - } - - if (!empty($options['when2'])) - { - $this->dates->set('when2', $options['when2']); - } - - // Process the static taxonomy filters. - if (!empty($options['filter'])) - { - $this->processStaticTaxonomy($options['filter']); - } - - // Process the dynamic taxonomy filters. - if (!empty($options['filters'])) - { - $this->processDynamicTaxonomy($options['filters']); - } - - // Get the date filters. - $d1 = $this->dates->get('date1'); - $d2 = $this->dates->get('date2'); - $w1 = $this->dates->get('when1'); - $w2 = $this->dates->get('when2'); - - // Process the date filters. - if (!empty($d1) || !empty($d2)) - { - $this->processDates($d1, $d2, $w1, $w2); - } - - // Process the input string. - $this->processString($this->input, $this->language, $this->mode); - - // Get the number of matching terms. - foreach ($this->included as $token) - { - $this->terms += count($token->matches); - } - - // Remove the temporary date storage. - unset($this->dates); - - // Lastly, determine whether this query can return a result set. - - // Check if we have a query string. - if (!empty($this->input)) - { - $this->search = true; - } - // Check if we can search without a query string. - elseif ($this->empty && (!empty($this->filter) || !empty($this->filters) || !empty($this->date1) || !empty($this->date2))) - { - $this->search = true; - } - // We do not have a valid search query. - else - { - $this->search = false; - } - } - - /** - * Method to convert the query object into a URI string. - * - * @param string $base The base URI. [optional] - * - * @return string The complete query URI. - * - * @since 2.5 - */ - public function toUri($base = '') - { - // Set the base if not specified. - if ($base === '') - { - $base = 'index.php?option=com_finder&view=search'; - } - - // Get the base URI. - $uri = Uri::getInstance($base); - - // Add the static taxonomy filter if present. - if ((bool) $this->filter) - { - $uri->setVar('f', $this->filter); - } - - // Get the filters in the request. - $t = Factory::getApplication()->input->request->get('t', array(), 'array'); - - // Add the dynamic taxonomy filters if present. - if ((bool) $this->filters) - { - foreach ($this->filters as $nodes) - { - foreach ($nodes as $node) - { - if (!in_array($node, $t)) - { - continue; - } - - $uri->setVar('t[]', $node); - } - } - } - - // Add the input string if present. - if (!empty($this->input)) - { - $uri->setVar('q', $this->input); - } - - // Add the start date if present. - if (!empty($this->date1)) - { - $uri->setVar('d1', $this->date1); - } - - // Add the end date if present. - if (!empty($this->date2)) - { - $uri->setVar('d2', $this->date2); - } - - // Add the start date modifier if present. - if (!empty($this->when1)) - { - $uri->setVar('w1', $this->when1); - } - - // Add the end date modifier if present. - if (!empty($this->when2)) - { - $uri->setVar('w2', $this->when2); - } - - // Add a menu item id if one is not present. - if (!$uri->getVar('Itemid')) - { - // Get the menu item id. - $query = array( - 'view' => $uri->getVar('view'), - 'f' => $uri->getVar('f'), - 'q' => $uri->getVar('q'), - ); - - $item = \FinderHelperRoute::getItemid($query); - - // Add the menu item id if present. - if ($item !== null) - { - $uri->setVar('Itemid', $item); - } - } - - return $uri->toString(array('path', 'query')); - } - - /** - * Method to get a list of excluded search term ids. - * - * @return array An array of excluded term ids. - * - * @since 2.5 - */ - public function getExcludedTermIds() - { - $results = array(); - - // Iterate through the excluded tokens and compile the matching terms. - for ($i = 0, $c = count($this->excluded); $i < $c; $i++) - { - foreach ($this->excluded[$i]->matches as $match) - { - $results = array_merge($results, $match); - } - } - - // Sanitize the terms. - $results = array_unique($results); - - return ArrayHelper::toInteger($results); - } - - /** - * Method to get a list of included search term ids. - * - * @return array An array of included term ids. - * - * @since 2.5 - */ - public function getIncludedTermIds() - { - $results = array(); - - // Iterate through the included tokens and compile the matching terms. - for ($i = 0, $c = count($this->included); $i < $c; $i++) - { - // Check if we have any terms. - if (empty($this->included[$i]->matches)) - { - continue; - } - - // Get the term. - $term = $this->included[$i]->term; - - // Prepare the container for the term if necessary. - if (!array_key_exists($term, $results)) - { - $results[$term] = array(); - } - - // Add the matches to the stack. - foreach ($this->included[$i]->matches as $match) - { - $results[$term] = array_merge($results[$term], $match); - } - } - - // Sanitize the terms. - foreach ($results as $key => $value) - { - $results[$key] = array_unique($results[$key]); - $results[$key] = ArrayHelper::toInteger($results[$key]); - } - - return $results; - } - - /** - * Method to get a list of required search term ids. - * - * @return array An array of required term ids. - * - * @since 2.5 - */ - public function getRequiredTermIds() - { - $results = array(); - - // Iterate through the included tokens and compile the matching terms. - for ($i = 0, $c = count($this->included); $i < $c; $i++) - { - // Check if the token is required. - if ($this->included[$i]->required) - { - // Get the term. - $term = $this->included[$i]->term; - - // Prepare the container for the term if necessary. - if (!array_key_exists($term, $results)) - { - $results[$term] = array(); - } - - // Add the matches to the stack. - foreach ($this->included[$i]->matches as $match) - { - $results[$term] = array_merge($results[$term], $match); - } - } - } - - // Sanitize the terms. - foreach ($results as $key => $value) - { - $results[$key] = array_unique($results[$key]); - $results[$key] = ArrayHelper::toInteger($results[$key]); - } - - return $results; - } - - /** - * Method to process the static taxonomy input. The static taxonomy input - * comes in the form of a pre-defined search filter that is assigned to the - * search form. - * - * @param integer $filterId The id of static filter. - * - * @return boolean True on success, false on failure. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function processStaticTaxonomy($filterId) - { - // Get the database object. - $db = $this->getDatabase(); - - // Initialize user variables - $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); - - // Load the predefined filter. - $query = $db->getQuery(true) - ->select('f.data, f.params') - ->from($db->quoteName('#__finder_filters') . ' AS f') - ->where('f.filter_id = ' . (int) $filterId); - - $db->setQuery($query); - $return = $db->loadObject(); - - // Check the returned filter. - if (empty($return)) - { - return false; - } - - // Set the filter. - $this->filter = (int) $filterId; - - // Get a parameter object for the filter date options. - $registry = new Registry($return->params); - $params = $registry; - - // Set the dates if not already set. - $this->dates->def('d1', $params->get('d1')); - $this->dates->def('d2', $params->get('d2')); - $this->dates->def('w1', $params->get('w1')); - $this->dates->def('w2', $params->get('w2')); - - // Remove duplicates and sanitize. - $filters = explode(',', $return->data); - $filters = array_unique($filters); - $filters = ArrayHelper::toInteger($filters); - - // Remove any values of zero. - if (in_array(0, $filters, true) !== false) - { - unset($filters[array_search(0, $filters, true)]); - } - - // Check if we have any real input. - if (empty($filters)) - { - return true; - } - - /* - * Create the query to get filters from the database. We do this for - * two reasons: one, it allows us to ensure that the filters being used - * are real; two, we need to sort the filters by taxonomy branch. - */ - $query->clear() - ->select('t1.id, t1.title, t2.title AS branch') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t1') - ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS t2 ON t2.id = t1.parent_id') - ->where('t1.state = 1') - ->where('t1.access IN (' . $groups . ')') - ->where('t1.id IN (' . implode(',', $filters) . ')') - ->where('t2.state = 1') - ->where('t2.access IN (' . $groups . ')'); - - // Load the filters. - $db->setQuery($query); - $results = $db->loadObjectList(); - - // Sort the filter ids by branch. - foreach ($results as $result) - { - $this->filters[$result->branch][$result->title] = (int) $result->id; - } - - return true; - } - - /** - * Method to process the dynamic taxonomy input. The dynamic taxonomy input - * comes in the form of select fields that the user chooses from. The - * dynamic taxonomy input is processed AFTER the static taxonomy input - * because the dynamic options can be used to further narrow a static - * taxonomy filter. - * - * @param array $filters An array of taxonomy node ids. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function processDynamicTaxonomy($filters) - { - // Initialize user variables - $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); - - // Remove duplicates and sanitize. - $filters = array_unique($filters); - $filters = ArrayHelper::toInteger($filters); - - // Remove any values of zero. - if (in_array(0, $filters, true) !== false) - { - unset($filters[array_search(0, $filters, true)]); - } - - // Check if we have any real input. - if (empty($filters)) - { - return true; - } - - // Get the database object. - $db = $this->getDatabase(); - - $query = $db->getQuery(true); - - /* - * Create the query to get filters from the database. We do this for - * two reasons: one, it allows us to ensure that the filters being used - * are real; two, we need to sort the filters by taxonomy branch. - */ - $query->select('t1.id, t1.title, t2.title AS branch') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t1') - ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS t2 ON t2.id = t1.parent_id') - ->where('t1.state = 1') - ->where('t1.access IN (' . $groups . ')') - ->where('t1.id IN (' . implode(',', $filters) . ')') - ->where('t2.state = 1') - ->where('t2.access IN (' . $groups . ')'); - - // Load the filters. - $db->setQuery($query); - $results = $db->loadObjectList(); - - // Cleared filter branches. - $cleared = array(); - - /* - * Sort the filter ids by branch. Because these filters are designed to - * override and further narrow the items selected in the static filter, - * we will clear the values from the static filter on a branch by - * branch basis before adding the dynamic filters. So, if the static - * filter defines a type filter of "articles" and three "category" - * filters but the user only limits the category further, the category - * filters will be flushed but the type filters will not. - */ - foreach ($results as $result) - { - // Check if the branch has been cleared. - if (!in_array($result->branch, $cleared, true)) - { - // Clear the branch. - $this->filters[$result->branch] = array(); - - // Add the branch to the cleared list. - $cleared[] = $result->branch; - } - - // Add the filter to the list. - $this->filters[$result->branch][$result->title] = (int) $result->id; - } - - return true; - } - - /** - * Method to process the query date filters to determine start and end - * date limitations. - * - * @param string $date1 The first date filter. - * @param string $date2 The second date filter. - * @param string $when1 The first date modifier. - * @param string $when2 The second date modifier. - * - * @return boolean True on success. - * - * @since 2.5 - */ - protected function processDates($date1, $date2, $when1, $when2) - { - // Clean up the inputs. - $date1 = trim(StringHelper::strtolower($date1)); - $date2 = trim(StringHelper::strtolower($date2)); - $when1 = trim(StringHelper::strtolower($when1)); - $when2 = trim(StringHelper::strtolower($when2)); - - // Get the time offset. - $offset = Factory::getApplication()->get('offset'); - - // Array of allowed when values. - $whens = array('before', 'after', 'exact'); - - // The value of 'today' is a special case that we need to handle. - if ($date1 === StringHelper::strtolower(Text::_('COM_FINDER_QUERY_FILTER_TODAY'))) - { - $date1 = Factory::getDate('now', $offset)->format('%Y-%m-%d'); - } - - // Try to parse the date string. - $date = Factory::getDate($date1, $offset); - - // Check if the date was parsed successfully. - if ($date->toUnix() !== null) - { - // Set the date filter. - $this->date1 = $date->toSql(); - $this->when1 = in_array($when1, $whens, true) ? $when1 : 'before'; - } - - // The value of 'today' is a special case that we need to handle. - if ($date2 === StringHelper::strtolower(Text::_('COM_FINDER_QUERY_FILTER_TODAY'))) - { - $date2 = Factory::getDate('now', $offset)->format('%Y-%m-%d'); - } - - // Try to parse the date string. - $date = Factory::getDate($date2, $offset); - - // Check if the date was parsed successfully. - if ($date->toUnix() !== null) - { - // Set the date filter. - $this->date2 = $date->toSql(); - $this->when2 = in_array($when2, $whens, true) ? $when2 : 'before'; - } - - return true; - } - - /** - * Method to process the query input string and extract required, optional, - * and excluded tokens; taxonomy filters; and date filters. - * - * @param string $input The query input string. - * @param string $lang The query input language. - * @param string $mode The query matching mode. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function processString($input, $lang, $mode) - { - if ($input === null) - { - $input = ''; - } - - // Clean up the input string. - $input = html_entity_decode($input, ENT_QUOTES, 'UTF-8'); - $input = StringHelper::strtolower($input); - $input = preg_replace('#\s+#mi', ' ', $input); - $input = trim($input); - $debug = Factory::getApplication()->get('debug_lang'); - $params = ComponentHelper::getParams('com_finder'); - - /* - * First, we need to handle string based modifiers. String based - * modifiers could potentially include things like "category:blah" or - * "before:2009-10-21" or "type:article", etc. - */ - $patterns = array( - 'before' => Text::_('COM_FINDER_FILTER_WHEN_BEFORE'), - 'after' => Text::_('COM_FINDER_FILTER_WHEN_AFTER'), - ); - - // Add the taxonomy branch titles to the possible patterns. - foreach (Taxonomy::getBranchTitles() as $branch) - { - // Add the pattern. - $patterns[$branch] = StringHelper::strtolower(Text::_(LanguageHelper::branchSingular($branch))); - } - - // Container for search terms and phrases. - $terms = array(); - $phrases = array(); - - // Cleared filter branches. - $cleared = array(); - - /* - * Compile the suffix pattern. This is used to match the values of the - * filter input string. Single words can be input directly, multi-word - * values have to be wrapped in double quotes. - */ - $quotes = html_entity_decode('‘’'', ENT_QUOTES, 'UTF-8'); - $suffix = '(([\w\d' . $quotes . '-]+)|\"([\w\d\s' . $quotes . '-]+)\")'; - - /* - * Iterate through the possible filter patterns and search for matches. - * We need to match the key, colon, and a value pattern for the match - * to be valid. - */ - foreach ($patterns as $modifier => $pattern) - { - $matches = array(); - - if ($debug) - { - $pattern = substr($pattern, 2, -2); - } - - // Check if the filter pattern is in the input string. - if (preg_match('#' . $pattern . '\s*:\s*' . $suffix . '#mi', $input, $matches)) - { - // Get the value given to the modifier. - $value = $matches[3] ?? $matches[1]; - - // Now we have to handle the filter string. - switch ($modifier) - { - // Handle a before and after date filters. - case 'before': - case 'after': - { - // Get the time offset. - $offset = Factory::getApplication()->get('offset'); - - // Array of allowed when values. - $whens = array('before', 'after', 'exact'); - - // The value of 'today' is a special case that we need to handle. - if ($value === StringHelper::strtolower(Text::_('COM_FINDER_QUERY_FILTER_TODAY'))) - { - $value = Factory::getDate('now', $offset)->format('%Y-%m-%d'); - } - - // Try to parse the date string. - $date = Factory::getDate($value, $offset); - - // Check if the date was parsed successfully. - if ($date->toUnix() !== null) - { - // Set the date filter. - $this->date1 = $date->toSql(); - $this->when1 = in_array($modifier, $whens, true) ? $modifier : 'before'; - } - - break; - } - - // Handle a taxonomy branch filter. - default: - { - // Try to find the node id. - $return = Taxonomy::getNodeByTitle($modifier, $value); - - // Check if the node id was found. - if ($return) - { - // Check if the branch has been cleared. - if (!in_array($modifier, $cleared, true)) - { - // Clear the branch. - $this->filters[$modifier] = array(); - - // Add the branch to the cleared list. - $cleared[] = $modifier; - } - - // Add the filter to the list. - $this->filters[$modifier][$return->title] = (int) $return->id; - } - - break; - } - } - - // Clean up the input string again. - $input = str_replace($matches[0], '', $input); - $input = preg_replace('#\s+#mi', ' ', $input); - $input = trim($input); - } - } - - /* - * Extract the tokens enclosed in double quotes so that we can handle - * them as phrases. - */ - if (StringHelper::strpos($input, '"') !== false) - { - $matches = array(); - - // Extract the tokens enclosed in double quotes. - if (preg_match_all('#\"([^"]+)\"#m', $input, $matches)) - { - /* - * One or more phrases were found so we need to iterate through - * them, tokenize them as phrases, and remove them from the raw - * input string before we move on to the next processing step. - */ - foreach ($matches[1] as $key => $match) - { - // Find the complete phrase in the input string. - $pos = StringHelper::strpos($input, $matches[0][$key]); - $len = StringHelper::strlen($matches[0][$key]); - - // Add any terms that are before this phrase to the stack. - if (trim(StringHelper::substr($input, 0, $pos))) - { - $terms = array_merge($terms, explode(' ', trim(StringHelper::substr($input, 0, $pos)))); - } - - // Strip out everything up to and including the phrase. - $input = StringHelper::substr($input, $pos + $len); - - // Clean up the input string again. - $input = preg_replace('#\s+#mi', ' ', $input); - $input = trim($input); - - // Get the number of words in the phrase. - $parts = explode(' ', $match); - $tuplecount = $params->get('tuplecount', 1); - - // Check if the phrase is longer than our $tuplecount. - if (count($parts) > $tuplecount && $tuplecount > 1) - { - $chunk = array_slice($parts, 0, $tuplecount); - $parts = array_slice($parts, $tuplecount); - - // If the chunk is not empty, add it as a phrase. - if (count($chunk)) - { - $phrases[] = implode(' ', $chunk); - $terms[] = implode(' ', $chunk); - } - - /* - * If the phrase is longer than $tuplecount words, we need to - * break it down into smaller chunks of phrases that - * are less than or equal to $tuplecount words. We overlap - * the chunks so that we can ensure that a match is - * found for the complete phrase and not just portions - * of it. - */ - for ($i = 0, $c = count($parts); $i < $c; $i++) - { - array_shift($chunk); - $chunk[] = array_shift($parts); - - // If the chunk is not empty, add it as a phrase. - if (count($chunk)) - { - $phrases[] = implode(' ', $chunk); - $terms[] = implode(' ', $chunk); - } - } - } - else - { - // The phrase is <= $tuplecount words so we can use it as is. - $phrases[] = $match; - $terms[] = $match; - } - } - } - } - - // Add the remaining terms if present. - if ((bool) $input) - { - $terms = array_merge($terms, explode(' ', $input)); - } - - // An array of our boolean operators. $operator => $translation - $operators = array( - 'AND' => StringHelper::strtolower(Text::_('COM_FINDER_QUERY_OPERATOR_AND')), - 'OR' => StringHelper::strtolower(Text::_('COM_FINDER_QUERY_OPERATOR_OR')), - 'NOT' => StringHelper::strtolower(Text::_('COM_FINDER_QUERY_OPERATOR_NOT')), - ); - - // If language debugging is enabled you need to ignore the debug strings in matching. - if (JDEBUG) - { - $debugStrings = array('**', '??'); - $operators = str_replace($debugStrings, '', $operators); - } - - /* - * Iterate through the terms and perform any sorting that needs to be - * done based on boolean search operators. Terms that are before an - * and/or/not modifier have to be handled in relation to their operator. - */ - for ($i = 0, $c = count($terms); $i < $c; $i++) - { - // Check if the term is followed by an operator that we understand. - if (isset($terms[$i + 1]) && in_array($terms[$i + 1], $operators, true)) - { - // Get the operator mode. - $op = array_search($terms[$i + 1], $operators, true); - - // Handle the AND operator. - if ($op === 'AND' && isset($terms[$i + 2])) - { - // Tokenize the current term. - $token = Helper::tokenize($terms[$i], $lang, true); - - // @todo: The previous function call may return an array, which seems not to be handled by the next one, which expects an object - $token = $this->getTokenData(array_shift($token)); - - if ($params->get('filter_commonwords', 0) && $token->common) - { - continue; - } - - if ($params->get('filter_numeric', 0) && $token->numeric) - { - continue; - } - - // Set the required flag. - $token->required = true; - - // Add the current token to the stack. - $this->included[] = $token; - $this->highlight = array_merge($this->highlight, array_keys($token->matches)); - - // Skip the next token (the mode operator). - $this->operators[] = $terms[$i + 1]; - - // Tokenize the term after the next term (current plus two). - $other = Helper::tokenize($terms[$i + 2], $lang, true); - $other = $this->getTokenData(array_shift($other)); - - // Set the required flag. - $other->required = true; - - // Add the token after the next token to the stack. - $this->included[] = $other; - $this->highlight = array_merge($this->highlight, array_keys($other->matches)); - - // Remove the processed phrases if possible. - if (($pk = array_search($terms[$i], $phrases, true)) !== false) - { - unset($phrases[$pk]); - } - - if (($pk = array_search($terms[$i + 2], $phrases, true)) !== false) - { - unset($phrases[$pk]); - } - - // Remove the processed terms. - unset($terms[$i], $terms[$i + 1], $terms[$i + 2]); - - // Adjust the loop. - $i += 2; - } - // Handle the OR operator. - elseif ($op === 'OR' && isset($terms[$i + 2])) - { - // Tokenize the current term. - $token = Helper::tokenize($terms[$i], $lang, true); - $token = $this->getTokenData(array_shift($token)); - - if ($params->get('filter_commonwords', 0) && $token->common) - { - continue; - } - - if ($params->get('filter_numeric', 0) && $token->numeric) - { - continue; - } - - // Set the required flag. - $token->required = false; - - // Add the current token to the stack. - if ((bool) $token->matches) - { - $this->included[] = $token; - $this->highlight = array_merge($this->highlight, array_keys($token->matches)); - } - else - { - $this->ignored[] = $token; - } - - // Skip the next token (the mode operator). - $this->operators[] = $terms[$i + 1]; - - // Tokenize the term after the next term (current plus two). - $other = Helper::tokenize($terms[$i + 2], $lang, true); - $other = $this->getTokenData(array_shift($other)); - - // Set the required flag. - $other->required = false; - - // Add the token after the next token to the stack. - if ((bool) $other->matches) - { - $this->included[] = $other; - $this->highlight = array_merge($this->highlight, array_keys($other->matches)); - } - else - { - $this->ignored[] = $other; - } - - // Remove the processed phrases if possible. - if (($pk = array_search($terms[$i], $phrases, true)) !== false) - { - unset($phrases[$pk]); - } - - if (($pk = array_search($terms[$i + 2], $phrases, true)) !== false) - { - unset($phrases[$pk]); - } - - // Remove the processed terms. - unset($terms[$i], $terms[$i + 1], $terms[$i + 2]); - - // Adjust the loop. - $i += 2; - } - } - // Handle an orphaned OR operator. - elseif (isset($terms[$i + 1]) && array_search($terms[$i], $operators, true) === 'OR') - { - // Skip the next token (the mode operator). - $this->operators[] = $terms[$i]; - - // Tokenize the next term (current plus one). - $other = Helper::tokenize($terms[$i + 1], $lang, true); - $other = $this->getTokenData(array_shift($other)); - - if ($params->get('filter_commonwords', 0) && $other->common) - { - continue; - } - - if ($params->get('filter_numeric', 0) && $other->numeric) - { - continue; - } - - // Set the required flag. - $other->required = false; - - // Add the token after the next token to the stack. - if ((bool) $other->matches) - { - $this->included[] = $other; - $this->highlight = array_merge($this->highlight, array_keys($other->matches)); - } - else - { - $this->ignored[] = $other; - } - - // Remove the processed phrase if possible. - if (($pk = array_search($terms[$i + 1], $phrases, true)) !== false) - { - unset($phrases[$pk]); - } - - // Remove the processed terms. - unset($terms[$i], $terms[$i + 1]); - - // Adjust the loop. - $i++; - } - // Handle the NOT operator. - elseif (isset($terms[$i + 1]) && array_search($terms[$i], $operators, true) === 'NOT') - { - // Skip the next token (the mode operator). - $this->operators[] = $terms[$i]; - - // Tokenize the next term (current plus one). - $other = Helper::tokenize($terms[$i + 1], $lang, true); - $other = $this->getTokenData(array_shift($other)); - - if ($params->get('filter_commonwords', 0) && $other->common) - { - continue; - } - - if ($params->get('filter_numeric', 0) && $other->numeric) - { - continue; - } - - // Set the required flag. - $other->required = false; - - // Add the next token to the stack. - if ((bool) $other->matches) - { - $this->excluded[] = $other; - } - else - { - $this->ignored[] = $other; - } - - // Remove the processed phrase if possible. - if (($pk = array_search($terms[$i + 1], $phrases, true)) !== false) - { - unset($phrases[$pk]); - } - - // Remove the processed terms. - unset($terms[$i], $terms[$i + 1]); - - // Adjust the loop. - $i++; - } - } - - /* - * Iterate through any search phrases and tokenize them. We handle - * phrases as autonomous units and do not break them down into two and - * three word combinations. - */ - for ($i = 0, $c = count($phrases); $i < $c; $i++) - { - // Tokenize the phrase. - $token = Helper::tokenize($phrases[$i], $lang, true); - - if (!count($token)) - { - continue; - } - - $token = $this->getTokenData(array_shift($token)); - - if ($params->get('filter_commonwords', 0) && $token->common) - { - continue; - } - - if ($params->get('filter_numeric', 0) && $token->numeric) - { - continue; - } - - // Set the required flag. - $token->required = true; - - // Add the current token to the stack. - $this->included[] = $token; - $this->highlight = array_merge($this->highlight, array_keys($token->matches)); - - // Remove the processed term if possible. - if (($pk = array_search($phrases[$i], $terms, true)) !== false) - { - unset($terms[$pk]); - } - - // Remove the processed phrase. - unset($phrases[$i]); - } - - /* - * Handle any remaining tokens using the standard processing mechanism. - */ - if ((bool) $terms) - { - // Tokenize the terms. - $terms = implode(' ', $terms); - $tokens = Helper::tokenize($terms, $lang, false); - - // Make sure we are working with an array. - $tokens = is_array($tokens) ? $tokens : array($tokens); - - // Get the token data and required state for all the tokens. - foreach ($tokens as $token) - { - // Get the token data. - $token = $this->getTokenData($token); - - if ($params->get('filter_commonwords', 0) && $token->common) - { - continue; - } - - if ($params->get('filter_numerics', 0) && $token->numeric) - { - continue; - } - - // Set the required flag for the token. - $token->required = $mode === 'AND' ? (!$token->phrase) : false; - - // Add the token to the appropriate stack. - if ($token->required || (bool) $token->matches) - { - $this->included[] = $token; - $this->highlight = array_merge($this->highlight, array_keys($token->matches)); - } - else - { - $this->ignored[] = $token; - } - } - } - - return true; - } - - /** - * Method to get the base and similar term ids and, if necessary, suggested - * term data from the database. The terms ids are identified based on a - * 'like' match in MySQL and/or a common stem. If no term ids could be - * found, then we know that we will not be able to return any results for - * that term and we should try to find a similar term to use that we can - * match so that we can suggest the alternative search query to the user. - * - * @param Token $token A Token object. - * - * @return Token A Token object. - * - * @since 2.5 - * @throws Exception on database error. - */ - protected function getTokenData($token) - { - // Get the database object. - $db = $this->getDatabase(); - - // Create a database query to build match the token. - $query = $db->getQuery(true) - ->select('t.term, t.term_id') - ->from('#__finder_terms AS t'); - - if ($token->phrase) - { - // Add the phrase to the query. - $query->where('t.term = ' . $db->quote($token->term)) - ->where('t.phrase = 1'); - } - else - { - // Add the term to the query. - - $searchTerm = $token->term; - $searchStem = $token->stem; - $term = $query->quoteName('t.term'); - $stem = $query->quoteName('t.stem'); - - if ($this->wordmode === 'begin') - { - $searchTerm .= '%'; - $searchStem .= '%'; - $query->where('(' . $term . ' LIKE :searchTerm OR ' . $stem . ' LIKE :searchStem)'); - } - elseif ($this->wordmode === 'fuzzy') - { - $searchTerm = '%' . $searchTerm . '%'; - $searchStem = '%' . $searchStem . '%'; - $query->where('(' . $term . ' LIKE :searchTerm OR ' . $stem . ' LIKE :searchStem)'); - } - else - { - $query->where('(' . $term . ' = :searchTerm OR ' . $stem . ' = :searchStem)'); - } - - $query->bind(':searchTerm', $searchTerm, ParameterType::STRING) - ->bind(':searchStem', $searchStem, ParameterType::STRING); - - $query->where('t.phrase = 0') - ->where('t.language IN (\'*\',' . $db->quote($token->language) . ')'); - } - - // Get the terms. - $db->setQuery($query); - $matches = $db->loadObjectList(); - - // Check the matching terms. - if ((bool) $matches) - { - // Add the matches to the token. - for ($i = 0, $c = count($matches); $i < $c; $i++) - { - if (!isset($token->matches[$matches[$i]->term])) - { - $token->matches[$matches[$i]->term] = array(); - } - - $token->matches[$matches[$i]->term][] = (int) $matches[$i]->term_id; - } - } - - // If no matches were found, try to find a similar but better token. - if (empty($token->matches)) - { - // Create a database query to get the similar terms. - $query->clear() - ->select('DISTINCT t.term_id AS id, t.term AS term') - ->from('#__finder_terms AS t') - // ->where('t.soundex = ' . soundex($db->quote($token->term))) - ->where('t.soundex = SOUNDEX(' . $db->quote($token->term) . ')') - ->where('t.phrase = ' . (int) $token->phrase); - - // Get the terms. - $db->setQuery($query); - $results = $db->loadObjectList(); - - // Check if any similar terms were found. - if (empty($results)) - { - return $token; - } - - // Stack for sorting the similar terms. - $suggestions = array(); - - // Get the levnshtein distance for all suggested terms. - foreach ($results as $sk => $st) - { - // Get the levenshtein distance between terms. - $distance = levenshtein($st->term, $token->term); - - // Make sure the levenshtein distance isn't over 50. - if ($distance < 50) - { - $suggestions[$sk] = $distance; - } - } - - // Sort the suggestions. - asort($suggestions, SORT_NUMERIC); - - // Get the closest match. - $keys = array_keys($suggestions); - $key = $keys[0]; - - // Add the suggested term. - $token->suggestion = $results[$key]->term; - } - - return $token; - } + use DatabaseAwareTrait; + + /** + * Flag to show whether the query can return results. + * + * @var boolean + * @since 2.5 + */ + public $search; + + /** + * The query input string. + * + * @var string + * @since 2.5 + */ + public $input; + + /** + * The language of the query. + * + * @var string + * @since 2.5 + */ + public $language; + + /** + * The query string matching mode. + * + * @var string + * @since 2.5 + */ + public $mode; + + /** + * The included tokens. + * + * @var Token[] + * @since 2.5 + */ + public $included = array(); + + /** + * The excluded tokens. + * + * @var Token[] + * @since 2.5 + */ + public $excluded = array(); + + /** + * The tokens to ignore because no matches exist. + * + * @var Token[] + * @since 2.5 + */ + public $ignored = array(); + + /** + * The operators used in the query input string. + * + * @var array + * @since 2.5 + */ + public $operators = array(); + + /** + * The terms to highlight as matches. + * + * @var array + * @since 2.5 + */ + public $highlight = array(); + + /** + * The number of matching terms for the query input. + * + * @var integer + * @since 2.5 + */ + public $terms; + + /** + * Allow empty searches + * + * @var boolean + * @since 4.0.0 + */ + public $empty; + + /** + * The static filter id. + * + * @var string + * @since 2.5 + */ + public $filter; + + /** + * The taxonomy filters. This is a multi-dimensional array of taxonomy + * branches as the first level and then the taxonomy nodes as the values. + * + * For example: + * $filters = array( + * 'Type' = array(10, 32, 29, 11, ...); + * 'Label' = array(20, 314, 349, 91, 82, ...); + * ... + * ); + * + * @var array + * @since 2.5 + */ + public $filters = array(); + + /** + * The start date filter. + * + * @var string + * @since 2.5 + */ + public $date1; + + /** + * The end date filter. + * + * @var string + * @since 2.5 + */ + public $date2; + + /** + * The start date filter modifier. + * + * @var string + * @since 2.5 + */ + public $when1; + + /** + * The end date filter modifier. + * + * @var string + * @since 2.5 + */ + public $when2; + + /** + * Match search terms exactly or with a LIKE scheme + * + * @var string + * @since 4.2.0 + */ + public $wordmode; + + /** + * Method to instantiate the query object. + * + * @param array $options An array of query options. + * + * @since 2.5 + * @throws Exception on database error. + */ + public function __construct($options, DatabaseInterface $db = null) + { + if ($db === null) { + @trigger_error(sprintf('Database will be mandatory in 5.0.'), E_USER_DEPRECATED); + $db = Factory::getContainer()->get(DatabaseInterface::class); + } + + $this->setDatabase($db); + + // Get the input string. + $this->input = $options['input'] ?? ''; + + // Get the empty query setting. + $this->empty = isset($options['empty']) ? (bool) $options['empty'] : false; + + // Get the input language. + $this->language = !empty($options['language']) ? $options['language'] : Helper::getDefaultLanguage(); + + // Get the matching mode. + $this->mode = 'AND'; + + // Set the word matching mode + $this->wordmode = !empty($options['word_match']) ? $options['word_match'] : 'exact'; + + // Initialize the temporary date storage. + $this->dates = new Registry(); + + // Populate the temporary date storage. + if (!empty($options['date1'])) { + $this->dates->set('date1', $options['date1']); + } + + if (!empty($options['date2'])) { + $this->dates->set('date2', $options['date2']); + } + + if (!empty($options['when1'])) { + $this->dates->set('when1', $options['when1']); + } + + if (!empty($options['when2'])) { + $this->dates->set('when2', $options['when2']); + } + + // Process the static taxonomy filters. + if (!empty($options['filter'])) { + $this->processStaticTaxonomy($options['filter']); + } + + // Process the dynamic taxonomy filters. + if (!empty($options['filters'])) { + $this->processDynamicTaxonomy($options['filters']); + } + + // Get the date filters. + $d1 = $this->dates->get('date1'); + $d2 = $this->dates->get('date2'); + $w1 = $this->dates->get('when1'); + $w2 = $this->dates->get('when2'); + + // Process the date filters. + if (!empty($d1) || !empty($d2)) { + $this->processDates($d1, $d2, $w1, $w2); + } + + // Process the input string. + $this->processString($this->input, $this->language, $this->mode); + + // Get the number of matching terms. + foreach ($this->included as $token) { + $this->terms += count($token->matches); + } + + // Remove the temporary date storage. + unset($this->dates); + + // Lastly, determine whether this query can return a result set. + + // Check if we have a query string. + if (!empty($this->input)) { + $this->search = true; + } elseif ($this->empty && (!empty($this->filter) || !empty($this->filters) || !empty($this->date1) || !empty($this->date2))) { + // Check if we can search without a query string. + $this->search = true; + } else { + // We do not have a valid search query. + $this->search = false; + } + } + + /** + * Method to convert the query object into a URI string. + * + * @param string $base The base URI. [optional] + * + * @return string The complete query URI. + * + * @since 2.5 + */ + public function toUri($base = '') + { + // Set the base if not specified. + if ($base === '') { + $base = 'index.php?option=com_finder&view=search'; + } + + // Get the base URI. + $uri = Uri::getInstance($base); + + // Add the static taxonomy filter if present. + if ((bool) $this->filter) { + $uri->setVar('f', $this->filter); + } + + // Get the filters in the request. + $t = Factory::getApplication()->input->request->get('t', array(), 'array'); + + // Add the dynamic taxonomy filters if present. + if ((bool) $this->filters) { + foreach ($this->filters as $nodes) { + foreach ($nodes as $node) { + if (!in_array($node, $t)) { + continue; + } + + $uri->setVar('t[]', $node); + } + } + } + + // Add the input string if present. + if (!empty($this->input)) { + $uri->setVar('q', $this->input); + } + + // Add the start date if present. + if (!empty($this->date1)) { + $uri->setVar('d1', $this->date1); + } + + // Add the end date if present. + if (!empty($this->date2)) { + $uri->setVar('d2', $this->date2); + } + + // Add the start date modifier if present. + if (!empty($this->when1)) { + $uri->setVar('w1', $this->when1); + } + + // Add the end date modifier if present. + if (!empty($this->when2)) { + $uri->setVar('w2', $this->when2); + } + + // Add a menu item id if one is not present. + if (!$uri->getVar('Itemid')) { + // Get the menu item id. + $query = array( + 'view' => $uri->getVar('view'), + 'f' => $uri->getVar('f'), + 'q' => $uri->getVar('q'), + ); + + $item = RouteHelper::getItemid($query); + + // Add the menu item id if present. + if ($item !== null) { + $uri->setVar('Itemid', $item); + } + } + + return $uri->toString(array('path', 'query')); + } + + /** + * Method to get a list of excluded search term ids. + * + * @return array An array of excluded term ids. + * + * @since 2.5 + */ + public function getExcludedTermIds() + { + $results = array(); + + // Iterate through the excluded tokens and compile the matching terms. + for ($i = 0, $c = count($this->excluded); $i < $c; $i++) { + foreach ($this->excluded[$i]->matches as $match) { + $results = array_merge($results, $match); + } + } + + // Sanitize the terms. + $results = array_unique($results); + + return ArrayHelper::toInteger($results); + } + + /** + * Method to get a list of included search term ids. + * + * @return array An array of included term ids. + * + * @since 2.5 + */ + public function getIncludedTermIds() + { + $results = array(); + + // Iterate through the included tokens and compile the matching terms. + for ($i = 0, $c = count($this->included); $i < $c; $i++) { + // Check if we have any terms. + if (empty($this->included[$i]->matches)) { + continue; + } + + // Get the term. + $term = $this->included[$i]->term; + + // Prepare the container for the term if necessary. + if (!array_key_exists($term, $results)) { + $results[$term] = array(); + } + + // Add the matches to the stack. + foreach ($this->included[$i]->matches as $match) { + $results[$term] = array_merge($results[$term], $match); + } + } + + // Sanitize the terms. + foreach ($results as $key => $value) { + $results[$key] = array_unique($results[$key]); + $results[$key] = ArrayHelper::toInteger($results[$key]); + } + + return $results; + } + + /** + * Method to get a list of required search term ids. + * + * @return array An array of required term ids. + * + * @since 2.5 + */ + public function getRequiredTermIds() + { + $results = array(); + + // Iterate through the included tokens and compile the matching terms. + for ($i = 0, $c = count($this->included); $i < $c; $i++) { + // Check if the token is required. + if ($this->included[$i]->required) { + // Get the term. + $term = $this->included[$i]->term; + + // Prepare the container for the term if necessary. + if (!array_key_exists($term, $results)) { + $results[$term] = array(); + } + + // Add the matches to the stack. + foreach ($this->included[$i]->matches as $match) { + $results[$term] = array_merge($results[$term], $match); + } + } + } + + // Sanitize the terms. + foreach ($results as $key => $value) { + $results[$key] = array_unique($results[$key]); + $results[$key] = ArrayHelper::toInteger($results[$key]); + } + + return $results; + } + + /** + * Method to process the static taxonomy input. The static taxonomy input + * comes in the form of a pre-defined search filter that is assigned to the + * search form. + * + * @param integer $filterId The id of static filter. + * + * @return boolean True on success, false on failure. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function processStaticTaxonomy($filterId) + { + // Get the database object. + $db = $this->getDatabase(); + + // Initialize user variables + $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); + + // Load the predefined filter. + $query = $db->getQuery(true) + ->select('f.data, f.params') + ->from($db->quoteName('#__finder_filters') . ' AS f') + ->where('f.filter_id = ' . (int) $filterId); + + $db->setQuery($query); + $return = $db->loadObject(); + + // Check the returned filter. + if (empty($return)) { + return false; + } + + // Set the filter. + $this->filter = (int) $filterId; + + // Get a parameter object for the filter date options. + $registry = new Registry($return->params); + $params = $registry; + + // Set the dates if not already set. + $this->dates->def('d1', $params->get('d1')); + $this->dates->def('d2', $params->get('d2')); + $this->dates->def('w1', $params->get('w1')); + $this->dates->def('w2', $params->get('w2')); + + // Remove duplicates and sanitize. + $filters = explode(',', $return->data); + $filters = array_unique($filters); + $filters = ArrayHelper::toInteger($filters); + + // Remove any values of zero. + if (in_array(0, $filters, true) !== false) { + unset($filters[array_search(0, $filters, true)]); + } + + // Check if we have any real input. + if (empty($filters)) { + return true; + } + + /* + * Create the query to get filters from the database. We do this for + * two reasons: one, it allows us to ensure that the filters being used + * are real; two, we need to sort the filters by taxonomy branch. + */ + $query->clear() + ->select('t1.id, t1.title, t2.title AS branch') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t1') + ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS t2 ON t2.id = t1.parent_id') + ->where('t1.state = 1') + ->where('t1.access IN (' . $groups . ')') + ->where('t1.id IN (' . implode(',', $filters) . ')') + ->where('t2.state = 1') + ->where('t2.access IN (' . $groups . ')'); + + // Load the filters. + $db->setQuery($query); + $results = $db->loadObjectList(); + + // Sort the filter ids by branch. + foreach ($results as $result) { + $this->filters[$result->branch][$result->title] = (int) $result->id; + } + + return true; + } + + /** + * Method to process the dynamic taxonomy input. The dynamic taxonomy input + * comes in the form of select fields that the user chooses from. The + * dynamic taxonomy input is processed AFTER the static taxonomy input + * because the dynamic options can be used to further narrow a static + * taxonomy filter. + * + * @param array $filters An array of taxonomy node ids. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function processDynamicTaxonomy($filters) + { + // Initialize user variables + $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); + + // Remove duplicates and sanitize. + $filters = array_unique($filters); + $filters = ArrayHelper::toInteger($filters); + + // Remove any values of zero. + if (in_array(0, $filters, true) !== false) { + unset($filters[array_search(0, $filters, true)]); + } + + // Check if we have any real input. + if (empty($filters)) { + return true; + } + + // Get the database object. + $db = $this->getDatabase(); + + $query = $db->getQuery(true); + + /* + * Create the query to get filters from the database. We do this for + * two reasons: one, it allows us to ensure that the filters being used + * are real; two, we need to sort the filters by taxonomy branch. + */ + $query->select('t1.id, t1.title, t2.title AS branch') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t1') + ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS t2 ON t2.id = t1.parent_id') + ->where('t1.state = 1') + ->where('t1.access IN (' . $groups . ')') + ->where('t1.id IN (' . implode(',', $filters) . ')') + ->where('t2.state = 1') + ->where('t2.access IN (' . $groups . ')'); + + // Load the filters. + $db->setQuery($query); + $results = $db->loadObjectList(); + + // Cleared filter branches. + $cleared = array(); + + /* + * Sort the filter ids by branch. Because these filters are designed to + * override and further narrow the items selected in the static filter, + * we will clear the values from the static filter on a branch by + * branch basis before adding the dynamic filters. So, if the static + * filter defines a type filter of "articles" and three "category" + * filters but the user only limits the category further, the category + * filters will be flushed but the type filters will not. + */ + foreach ($results as $result) { + // Check if the branch has been cleared. + if (!in_array($result->branch, $cleared, true)) { + // Clear the branch. + $this->filters[$result->branch] = array(); + + // Add the branch to the cleared list. + $cleared[] = $result->branch; + } + + // Add the filter to the list. + $this->filters[$result->branch][$result->title] = (int) $result->id; + } + + return true; + } + + /** + * Method to process the query date filters to determine start and end + * date limitations. + * + * @param string $date1 The first date filter. + * @param string $date2 The second date filter. + * @param string $when1 The first date modifier. + * @param string $when2 The second date modifier. + * + * @return boolean True on success. + * + * @since 2.5 + */ + protected function processDates($date1, $date2, $when1, $when2) + { + // Clean up the inputs. + $date1 = trim(StringHelper::strtolower($date1)); + $date2 = trim(StringHelper::strtolower($date2)); + $when1 = trim(StringHelper::strtolower($when1)); + $when2 = trim(StringHelper::strtolower($when2)); + + // Get the time offset. + $offset = Factory::getApplication()->get('offset'); + + // Array of allowed when values. + $whens = array('before', 'after', 'exact'); + + // The value of 'today' is a special case that we need to handle. + if ($date1 === StringHelper::strtolower(Text::_('COM_FINDER_QUERY_FILTER_TODAY'))) { + $date1 = Factory::getDate('now', $offset)->format('%Y-%m-%d'); + } + + // Try to parse the date string. + $date = Factory::getDate($date1, $offset); + + // Check if the date was parsed successfully. + if ($date->toUnix() !== null) { + // Set the date filter. + $this->date1 = $date->toSql(); + $this->when1 = in_array($when1, $whens, true) ? $when1 : 'before'; + } + + // The value of 'today' is a special case that we need to handle. + if ($date2 === StringHelper::strtolower(Text::_('COM_FINDER_QUERY_FILTER_TODAY'))) { + $date2 = Factory::getDate('now', $offset)->format('%Y-%m-%d'); + } + + // Try to parse the date string. + $date = Factory::getDate($date2, $offset); + + // Check if the date was parsed successfully. + if ($date->toUnix() !== null) { + // Set the date filter. + $this->date2 = $date->toSql(); + $this->when2 = in_array($when2, $whens, true) ? $when2 : 'before'; + } + + return true; + } + + /** + * Method to process the query input string and extract required, optional, + * and excluded tokens; taxonomy filters; and date filters. + * + * @param string $input The query input string. + * @param string $lang The query input language. + * @param string $mode The query matching mode. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function processString($input, $lang, $mode) + { + if ($input === null) { + $input = ''; + } + + // Clean up the input string. + $input = html_entity_decode($input, ENT_QUOTES, 'UTF-8'); + $input = StringHelper::strtolower($input); + $input = preg_replace('#\s+#mi', ' ', $input); + $input = trim($input); + $debug = Factory::getApplication()->get('debug_lang'); + $params = ComponentHelper::getParams('com_finder'); + + /* + * First, we need to handle string based modifiers. String based + * modifiers could potentially include things like "category:blah" or + * "before:2009-10-21" or "type:article", etc. + */ + $patterns = array( + 'before' => Text::_('COM_FINDER_FILTER_WHEN_BEFORE'), + 'after' => Text::_('COM_FINDER_FILTER_WHEN_AFTER'), + ); + + // Add the taxonomy branch titles to the possible patterns. + foreach (Taxonomy::getBranchTitles() as $branch) { + // Add the pattern. + $patterns[$branch] = StringHelper::strtolower(Text::_(LanguageHelper::branchSingular($branch))); + } + + // Container for search terms and phrases. + $terms = array(); + $phrases = array(); + + // Cleared filter branches. + $cleared = array(); + + /* + * Compile the suffix pattern. This is used to match the values of the + * filter input string. Single words can be input directly, multi-word + * values have to be wrapped in double quotes. + */ + $quotes = html_entity_decode('‘’'', ENT_QUOTES, 'UTF-8'); + $suffix = '(([\w\d' . $quotes . '-]+)|\"([\w\d\s' . $quotes . '-]+)\")'; + + /* + * Iterate through the possible filter patterns and search for matches. + * We need to match the key, colon, and a value pattern for the match + * to be valid. + */ + foreach ($patterns as $modifier => $pattern) { + $matches = array(); + + if ($debug) { + $pattern = substr($pattern, 2, -2); + } + + // Check if the filter pattern is in the input string. + if (preg_match('#' . $pattern . '\s*:\s*' . $suffix . '#mi', $input, $matches)) { + // Get the value given to the modifier. + $value = $matches[3] ?? $matches[1]; + + // Now we have to handle the filter string. + switch ($modifier) { + // Handle a before and after date filters. + case 'before': + case 'after': + // Get the time offset. + $offset = Factory::getApplication()->get('offset'); + + // Array of allowed when values. + $whens = array('before', 'after', 'exact'); + + // The value of 'today' is a special case that we need to handle. + if ($value === StringHelper::strtolower(Text::_('COM_FINDER_QUERY_FILTER_TODAY'))) { + $value = Factory::getDate('now', $offset)->format('%Y-%m-%d'); + } + + // Try to parse the date string. + $date = Factory::getDate($value, $offset); + + // Check if the date was parsed successfully. + if ($date->toUnix() !== null) { + // Set the date filter. + $this->date1 = $date->toSql(); + $this->when1 = in_array($modifier, $whens, true) ? $modifier : 'before'; + } + + break; + + // Handle a taxonomy branch filter. + default: + // Try to find the node id. + $return = Taxonomy::getNodeByTitle($modifier, $value); + + // Check if the node id was found. + if ($return) { + // Check if the branch has been cleared. + if (!in_array($modifier, $cleared, true)) { + // Clear the branch. + $this->filters[$modifier] = array(); + + // Add the branch to the cleared list. + $cleared[] = $modifier; + } + + // Add the filter to the list. + $this->filters[$modifier][$return->title] = (int) $return->id; + } + + break; + } + + // Clean up the input string again. + $input = str_replace($matches[0], '', $input); + $input = preg_replace('#\s+#mi', ' ', $input); + $input = trim($input); + } + } + + /* + * Extract the tokens enclosed in double quotes so that we can handle + * them as phrases. + */ + if (StringHelper::strpos($input, '"') !== false) { + $matches = array(); + + // Extract the tokens enclosed in double quotes. + if (preg_match_all('#\"([^"]+)\"#m', $input, $matches)) { + /* + * One or more phrases were found so we need to iterate through + * them, tokenize them as phrases, and remove them from the raw + * input string before we move on to the next processing step. + */ + foreach ($matches[1] as $key => $match) { + // Find the complete phrase in the input string. + $pos = StringHelper::strpos($input, $matches[0][$key]); + $len = StringHelper::strlen($matches[0][$key]); + + // Add any terms that are before this phrase to the stack. + if (trim(StringHelper::substr($input, 0, $pos))) { + $terms = array_merge($terms, explode(' ', trim(StringHelper::substr($input, 0, $pos)))); + } + + // Strip out everything up to and including the phrase. + $input = StringHelper::substr($input, $pos + $len); + + // Clean up the input string again. + $input = preg_replace('#\s+#mi', ' ', $input); + $input = trim($input); + + // Get the number of words in the phrase. + $parts = explode(' ', $match); + $tuplecount = $params->get('tuplecount', 1); + + // Check if the phrase is longer than our $tuplecount. + if (count($parts) > $tuplecount && $tuplecount > 1) { + $chunk = array_slice($parts, 0, $tuplecount); + $parts = array_slice($parts, $tuplecount); + + // If the chunk is not empty, add it as a phrase. + if (count($chunk)) { + $phrases[] = implode(' ', $chunk); + $terms[] = implode(' ', $chunk); + } + + /* + * If the phrase is longer than $tuplecount words, we need to + * break it down into smaller chunks of phrases that + * are less than or equal to $tuplecount words. We overlap + * the chunks so that we can ensure that a match is + * found for the complete phrase and not just portions + * of it. + */ + for ($i = 0, $c = count($parts); $i < $c; $i++) { + array_shift($chunk); + $chunk[] = array_shift($parts); + + // If the chunk is not empty, add it as a phrase. + if (count($chunk)) { + $phrases[] = implode(' ', $chunk); + $terms[] = implode(' ', $chunk); + } + } + } else { + // The phrase is <= $tuplecount words so we can use it as is. + $phrases[] = $match; + $terms[] = $match; + } + } + } + } + + // Add the remaining terms if present. + if ((bool) $input) { + $terms = array_merge($terms, explode(' ', $input)); + } + + // An array of our boolean operators. $operator => $translation + $operators = array( + 'AND' => StringHelper::strtolower(Text::_('COM_FINDER_QUERY_OPERATOR_AND')), + 'OR' => StringHelper::strtolower(Text::_('COM_FINDER_QUERY_OPERATOR_OR')), + 'NOT' => StringHelper::strtolower(Text::_('COM_FINDER_QUERY_OPERATOR_NOT')), + ); + + // If language debugging is enabled you need to ignore the debug strings in matching. + if (JDEBUG) { + $debugStrings = array('**', '??'); + $operators = str_replace($debugStrings, '', $operators); + } + + /* + * Iterate through the terms and perform any sorting that needs to be + * done based on boolean search operators. Terms that are before an + * and/or/not modifier have to be handled in relation to their operator. + */ + for ($i = 0, $c = count($terms); $i < $c; $i++) { + // Check if the term is followed by an operator that we understand. + if (isset($terms[$i + 1]) && in_array($terms[$i + 1], $operators, true)) { + // Get the operator mode. + $op = array_search($terms[$i + 1], $operators, true); + + // Handle the AND operator. + if ($op === 'AND' && isset($terms[$i + 2])) { + // Tokenize the current term. + $token = Helper::tokenize($terms[$i], $lang, true); + + // @todo: The previous function call may return an array, which seems not to be handled by the next one, which expects an object + $token = $this->getTokenData(array_shift($token)); + + if ($params->get('filter_commonwords', 0) && $token->common) { + continue; + } + + if ($params->get('filter_numeric', 0) && $token->numeric) { + continue; + } + + // Set the required flag. + $token->required = true; + + // Add the current token to the stack. + $this->included[] = $token; + $this->highlight = array_merge($this->highlight, array_keys($token->matches)); + + // Skip the next token (the mode operator). + $this->operators[] = $terms[$i + 1]; + + // Tokenize the term after the next term (current plus two). + $other = Helper::tokenize($terms[$i + 2], $lang, true); + $other = $this->getTokenData(array_shift($other)); + + // Set the required flag. + $other->required = true; + + // Add the token after the next token to the stack. + $this->included[] = $other; + $this->highlight = array_merge($this->highlight, array_keys($other->matches)); + + // Remove the processed phrases if possible. + if (($pk = array_search($terms[$i], $phrases, true)) !== false) { + unset($phrases[$pk]); + } + + if (($pk = array_search($terms[$i + 2], $phrases, true)) !== false) { + unset($phrases[$pk]); + } + + // Remove the processed terms. + unset($terms[$i], $terms[$i + 1], $terms[$i + 2]); + + // Adjust the loop. + $i += 2; + } elseif ($op === 'OR' && isset($terms[$i + 2])) { + // Handle the OR operator. + // Tokenize the current term. + $token = Helper::tokenize($terms[$i], $lang, true); + $token = $this->getTokenData(array_shift($token)); + + if ($params->get('filter_commonwords', 0) && $token->common) { + continue; + } + + if ($params->get('filter_numeric', 0) && $token->numeric) { + continue; + } + + // Set the required flag. + $token->required = false; + + // Add the current token to the stack. + if ((bool) $token->matches) { + $this->included[] = $token; + $this->highlight = array_merge($this->highlight, array_keys($token->matches)); + } else { + $this->ignored[] = $token; + } + + // Skip the next token (the mode operator). + $this->operators[] = $terms[$i + 1]; + + // Tokenize the term after the next term (current plus two). + $other = Helper::tokenize($terms[$i + 2], $lang, true); + $other = $this->getTokenData(array_shift($other)); + + // Set the required flag. + $other->required = false; + + // Add the token after the next token to the stack. + if ((bool) $other->matches) { + $this->included[] = $other; + $this->highlight = array_merge($this->highlight, array_keys($other->matches)); + } else { + $this->ignored[] = $other; + } + + // Remove the processed phrases if possible. + if (($pk = array_search($terms[$i], $phrases, true)) !== false) { + unset($phrases[$pk]); + } + + if (($pk = array_search($terms[$i + 2], $phrases, true)) !== false) { + unset($phrases[$pk]); + } + + // Remove the processed terms. + unset($terms[$i], $terms[$i + 1], $terms[$i + 2]); + + // Adjust the loop. + $i += 2; + } + } elseif (isset($terms[$i + 1]) && array_search($terms[$i], $operators, true) === 'OR') { + // Handle an orphaned OR operator. + // Skip the next token (the mode operator). + $this->operators[] = $terms[$i]; + + // Tokenize the next term (current plus one). + $other = Helper::tokenize($terms[$i + 1], $lang, true); + $other = $this->getTokenData(array_shift($other)); + + if ($params->get('filter_commonwords', 0) && $other->common) { + continue; + } + + if ($params->get('filter_numeric', 0) && $other->numeric) { + continue; + } + + // Set the required flag. + $other->required = false; + + // Add the token after the next token to the stack. + if ((bool) $other->matches) { + $this->included[] = $other; + $this->highlight = array_merge($this->highlight, array_keys($other->matches)); + } else { + $this->ignored[] = $other; + } + + // Remove the processed phrase if possible. + if (($pk = array_search($terms[$i + 1], $phrases, true)) !== false) { + unset($phrases[$pk]); + } + + // Remove the processed terms. + unset($terms[$i], $terms[$i + 1]); + + // Adjust the loop. + $i++; + } elseif (isset($terms[$i + 1]) && array_search($terms[$i], $operators, true) === 'NOT') { + // Handle the NOT operator. + // Skip the next token (the mode operator). + $this->operators[] = $terms[$i]; + + // Tokenize the next term (current plus one). + $other = Helper::tokenize($terms[$i + 1], $lang, true); + $other = $this->getTokenData(array_shift($other)); + + if ($params->get('filter_commonwords', 0) && $other->common) { + continue; + } + + if ($params->get('filter_numeric', 0) && $other->numeric) { + continue; + } + + // Set the required flag. + $other->required = false; + + // Add the next token to the stack. + if ((bool) $other->matches) { + $this->excluded[] = $other; + } else { + $this->ignored[] = $other; + } + + // Remove the processed phrase if possible. + if (($pk = array_search($terms[$i + 1], $phrases, true)) !== false) { + unset($phrases[$pk]); + } + + // Remove the processed terms. + unset($terms[$i], $terms[$i + 1]); + + // Adjust the loop. + $i++; + } + } + + /* + * Iterate through any search phrases and tokenize them. We handle + * phrases as autonomous units and do not break them down into two and + * three word combinations. + */ + for ($i = 0, $c = count($phrases); $i < $c; $i++) { + // Tokenize the phrase. + $token = Helper::tokenize($phrases[$i], $lang, true); + + if (!count($token)) { + continue; + } + + $token = $this->getTokenData(array_shift($token)); + + if ($params->get('filter_commonwords', 0) && $token->common) { + continue; + } + + if ($params->get('filter_numeric', 0) && $token->numeric) { + continue; + } + + // Set the required flag. + $token->required = true; + + // Add the current token to the stack. + $this->included[] = $token; + $this->highlight = array_merge($this->highlight, array_keys($token->matches)); + + // Remove the processed term if possible. + if (($pk = array_search($phrases[$i], $terms, true)) !== false) { + unset($terms[$pk]); + } + + // Remove the processed phrase. + unset($phrases[$i]); + } + + /* + * Handle any remaining tokens using the standard processing mechanism. + */ + if ((bool) $terms) { + // Tokenize the terms. + $terms = implode(' ', $terms); + $tokens = Helper::tokenize($terms, $lang, false); + + // Make sure we are working with an array. + $tokens = is_array($tokens) ? $tokens : array($tokens); + + // Get the token data and required state for all the tokens. + foreach ($tokens as $token) { + // Get the token data. + $token = $this->getTokenData($token); + + if ($params->get('filter_commonwords', 0) && $token->common) { + continue; + } + + if ($params->get('filter_numerics', 0) && $token->numeric) { + continue; + } + + // Set the required flag for the token. + $token->required = $mode === 'AND' ? (!$token->phrase) : false; + + // Add the token to the appropriate stack. + if ($token->required || (bool) $token->matches) { + $this->included[] = $token; + $this->highlight = array_merge($this->highlight, array_keys($token->matches)); + } else { + $this->ignored[] = $token; + } + } + } + + return true; + } + + /** + * Method to get the base and similar term ids and, if necessary, suggested + * term data from the database. The terms ids are identified based on a + * 'like' match in MySQL and/or a common stem. If no term ids could be + * found, then we know that we will not be able to return any results for + * that term and we should try to find a similar term to use that we can + * match so that we can suggest the alternative search query to the user. + * + * @param Token $token A Token object. + * + * @return Token A Token object. + * + * @since 2.5 + * @throws Exception on database error. + */ + protected function getTokenData($token) + { + // Get the database object. + $db = $this->getDatabase(); + + // Create a database query to build match the token. + $query = $db->getQuery(true) + ->select('t.term, t.term_id') + ->from('#__finder_terms AS t'); + + if ($token->phrase) { + // Add the phrase to the query. + $query->where('t.term = ' . $db->quote($token->term)) + ->where('t.phrase = 1'); + } else { + // Add the term to the query. + + $searchTerm = $token->term; + $searchStem = $token->stem; + $term = $query->quoteName('t.term'); + $stem = $query->quoteName('t.stem'); + + if ($this->wordmode === 'begin') { + $searchTerm .= '%'; + $searchStem .= '%'; + $query->where('(' . $term . ' LIKE :searchTerm OR ' . $stem . ' LIKE :searchStem)'); + } elseif ($this->wordmode === 'fuzzy') { + $searchTerm = '%' . $searchTerm . '%'; + $searchStem = '%' . $searchStem . '%'; + $query->where('(' . $term . ' LIKE :searchTerm OR ' . $stem . ' LIKE :searchStem)'); + } else { + $query->where('(' . $term . ' = :searchTerm OR ' . $stem . ' = :searchStem)'); + } + + $query->bind(':searchTerm', $searchTerm, ParameterType::STRING) + ->bind(':searchStem', $searchStem, ParameterType::STRING); + + $query->where('t.phrase = 0') + ->where('t.language IN (\'*\',' . $db->quote($token->language) . ')'); + } + + // Get the terms. + $db->setQuery($query); + $matches = $db->loadObjectList(); + + // Check the matching terms. + if ((bool) $matches) { + // Add the matches to the token. + for ($i = 0, $c = count($matches); $i < $c; $i++) { + if (!isset($token->matches[$matches[$i]->term])) { + $token->matches[$matches[$i]->term] = array(); + } + + $token->matches[$matches[$i]->term][] = (int) $matches[$i]->term_id; + } + } + + // If no matches were found, try to find a similar but better token. + if (empty($token->matches)) { + // Create a database query to get the similar terms. + $query->clear() + ->select('DISTINCT t.term_id AS id, t.term AS term') + ->from('#__finder_terms AS t') + // ->where('t.soundex = ' . soundex($db->quote($token->term))) + ->where('t.soundex = SOUNDEX(' . $db->quote($token->term) . ')') + ->where('t.phrase = ' . (int) $token->phrase); + + // Get the terms. + $db->setQuery($query); + $results = $db->loadObjectList(); + + // Check if any similar terms were found. + if (empty($results)) { + return $token; + } + + // Stack for sorting the similar terms. + $suggestions = array(); + + // Get the levnshtein distance for all suggested terms. + foreach ($results as $sk => $st) { + // Get the levenshtein distance between terms. + $distance = levenshtein($st->term, $token->term); + + // Make sure the levenshtein distance isn't over 50. + if ($distance < 50) { + $suggestions[$sk] = $distance; + } + } + + // Sort the suggestions. + asort($suggestions, SORT_NUMERIC); + + // Get the closest match. + $keys = array_keys($suggestions); + $key = $keys[0]; + + // Add the suggested term. + $token->suggestion = $results[$key]->term; + } + + return $token; + } } diff --git a/administrator/components/com_finder/src/Indexer/Result.php b/administrator/components/com_finder/src/Indexer/Result.php index cef7d04f7753b..158b49b82ff57 100644 --- a/administrator/components/com_finder/src/Indexer/Result.php +++ b/administrator/components/com_finder/src/Indexer/Result.php @@ -1,4 +1,5 @@ array('title', 'subtitle', 'id'), - Indexer::TEXT_CONTEXT => array('summary', 'body'), - Indexer::META_CONTEXT => array('meta', 'list_price', 'sale_price'), - Indexer::PATH_CONTEXT => array('path', 'alias'), - Indexer::MISC_CONTEXT => array('comments'), - ); - - /** - * The indexer will use this data to create taxonomy mapping entries for - * the item so that it can be filtered by type, label, category, - * or whatever. - * - * @var array - * @since 2.5 - */ - protected $taxonomy = array(); - - /** - * The content URL. - * - * @var string - * @since 2.5 - */ - public $url; - - /** - * The content route. - * - * @var string - * @since 2.5 - */ - public $route; - - /** - * The content title. - * - * @var string - * @since 2.5 - */ - public $title; - - /** - * The content description. - * - * @var string - * @since 2.5 - */ - public $description; - - /** - * The published state of the result. - * - * @var integer - * @since 2.5 - */ - public $published; - - /** - * The content published state. - * - * @var integer - * @since 2.5 - */ - public $state; - - /** - * The content access level. - * - * @var integer - * @since 2.5 - */ - public $access; - - /** - * The content language. - * - * @var string - * @since 2.5 - */ - public $language = '*'; - - /** - * The publishing start date. - * - * @var string - * @since 2.5 - */ - public $publish_start_date; - - /** - * The publishing end date. - * - * @var string - * @since 2.5 - */ - public $publish_end_date; - - /** - * The generic start date. - * - * @var string - * @since 2.5 - */ - public $start_date; - - /** - * The generic end date. - * - * @var string - * @since 2.5 - */ - public $end_date; - - /** - * The item list price. - * - * @var mixed - * @since 2.5 - */ - public $list_price; - - /** - * The item sale price. - * - * @var mixed - * @since 2.5 - */ - public $sale_price; - - /** - * The content type id. This is set by the adapter. - * - * @var integer - * @since 2.5 - */ - public $type_id; - - /** - * The default language for content. - * - * @var string - * @since 3.0.2 - */ - public $defaultLanguage; - - /** - * Constructor - * - * @since 3.0.3 - */ - public function __construct() - { - $this->defaultLanguage = ComponentHelper::getParams('com_languages')->get('site', 'en-GB'); - } - - /** - * The magic set method is used to push additional values into the elements - * array in order to preserve the cleanliness of the object. - * - * @param string $name The name of the element. - * @param mixed $value The value of the element. - * - * @return void - * - * @since 2.5 - */ - public function __set($name, $value) - { - $this->setElement($name, $value); - } - - /** - * The magic get method is used to retrieve additional element values from the elements array. - * - * @param string $name The name of the element. - * - * @return mixed The value of the element if set, null otherwise. - * - * @since 2.5 - */ - public function __get($name) - { - return $this->getElement($name); - } - - /** - * The magic isset method is used to check the state of additional element values in the elements array. - * - * @param string $name The name of the element. - * - * @return boolean True if set, false otherwise. - * - * @since 2.5 - */ - public function __isset($name) - { - return isset($this->elements[$name]); - } - - /** - * The magic unset method is used to unset additional element values in the elements array. - * - * @param string $name The name of the element. - * - * @return void - * - * @since 2.5 - */ - public function __unset($name) - { - unset($this->elements[$name]); - } - - /** - * Method to retrieve additional element values from the elements array. - * - * @param string $name The name of the element. - * - * @return mixed The value of the element if set, null otherwise. - * - * @since 2.5 - */ - public function getElement($name) - { - // Get the element value if set. - if (array_key_exists($name, $this->elements)) - { - return $this->elements[$name]; - } - - return null; - } - - /** - * Method to retrieve all elements. - * - * @return array The elements - * - * @since 3.8.3 - */ - public function getElements() - { - return $this->elements; - } - - /** - * Method to set additional element values in the elements array. - * - * @param string $name The name of the element. - * @param mixed $value The value of the element. - * - * @return void - * - * @since 2.5 - */ - public function setElement($name, $value) - { - $this->elements[$name] = $value; - } - - /** - * Method to get all processing instructions. - * - * @return array An array of processing instructions. - * - * @since 2.5 - */ - public function getInstructions() - { - return $this->instructions; - } - - /** - * Method to add a processing instruction for an item property. - * - * @param string $group The group to associate the property with. - * @param string $property The property to process. - * - * @return void - * - * @since 2.5 - */ - public function addInstruction($group, $property) - { - // Check if the group exists. We can't add instructions for unknown groups. - // Check if the property exists in the group. - if (array_key_exists($group, $this->instructions) && !in_array($property, $this->instructions[$group], true)) - { - // Add the property to the group. - $this->instructions[$group][] = $property; - } - } - - /** - * Method to remove a processing instruction for an item property. - * - * @param string $group The group to associate the property with. - * @param string $property The property to process. - * - * @return void - * - * @since 2.5 - */ - public function removeInstruction($group, $property) - { - // Check if the group exists. We can't remove instructions for unknown groups. - if (array_key_exists($group, $this->instructions)) - { - // Search for the property in the group. - $key = array_search($property, $this->instructions[$group]); - - // If the property was found, remove it. - if ($key !== false) - { - unset($this->instructions[$group][$key]); - } - } - } - - /** - * Method to get the taxonomy maps for an item. - * - * @param string $branch The taxonomy branch to get. [optional] - * - * @return array An array of taxonomy maps. - * - * @since 2.5 - */ - public function getTaxonomy($branch = null) - { - // Get the taxonomy branch if available. - if ($branch !== null && isset($this->taxonomy[$branch])) - { - return $this->taxonomy[$branch]; - } - - return $this->taxonomy; - } - - /** - * Method to add a taxonomy map for an item. - * - * @param string $branch The title of the taxonomy branch to add the node to. - * @param string $title The title of the taxonomy node. - * @param integer $state The published state of the taxonomy node. [optional] - * @param integer $access The access level of the taxonomy node. [optional] - * @param string $language The language of the taxonomy. [optional] - * - * @return void - * - * @since 2.5 - */ - public function addTaxonomy($branch, $title, $state = 1, $access = 1, $language = '') - { - // Filter the input. - $branch = preg_replace('#[^\pL\pM\pN\p{Pi}\p{Pf}\'+-.,_]+#mui', ' ', $branch); - - // Create the taxonomy node. - $node = new \stdClass; - $node->title = $title; - $node->state = (int) $state; - $node->access = (int) $access; - $node->language = $language; - $node->nested = false; - - // Add the node to the taxonomy branch. - $this->taxonomy[$branch][] = $node; - } - - /** - * Method to add a nested taxonomy map for an item. - * - * @param string $branch The title of the taxonomy branch to add the node to. - * @param ImmutableNodeInterface $contentNode The node object. - * @param integer $state The published state of the taxonomy node. [optional] - * @param integer $access The access level of the taxonomy node. [optional] - * @param string $language The language of the taxonomy. [optional] - * - * @return void - * - * @since 4.0.0 - */ - public function addNestedTaxonomy($branch, ImmutableNodeInterface $contentNode, $state = 1, $access = 1, $language = '') - { - // Filter the input. - $branch = preg_replace('#[^\pL\pM\pN\p{Pi}\p{Pf}\'+-.,_]+#mui', ' ', $branch); - - // Create the taxonomy node. - $node = new \stdClass; - $node->title = $contentNode->title; - $node->state = (int) $state; - $node->access = (int) $access; - $node->language = $language; - $node->nested = true; - $node->node = $contentNode; - - // Add the node to the taxonomy branch. - $this->taxonomy[$branch][] = $node; - } - - /** - * Method to set the item language - * - * @return void - * - * @since 3.0 - */ - public function setLanguage() - { - if ($this->language == '') - { - $this->language = $this->defaultLanguage; - } - } - - /** - * Helper function to serialise the data of a Result object - * - * @return string The serialised data - * - * @since 4.0.0 - */ - public function serialize() - { - return serialize($this->__serialize()); - } - - /** - * Helper function to unserialise the data for this object - * - * @param string $serialized Serialised data to unserialise - * - * @return void - * - * @since 4.0.0 - */ - public function unserialize($serialized): void - { - $this->__unserialize(unserialize($serialized)); - } - - /** - * Magic method used for serializing. - * - * @since 4.1.3 - */ - public function __serialize(): array - { - $taxonomy = []; - - foreach ($this->taxonomy as $branch => $nodes) - { - $taxonomy[$branch] = []; - - foreach ($nodes as $node) - { - if ($node->nested) - { - $n = clone $node; - unset($n->node); - $taxonomy[$branch][] = $n; - } - else - { - $taxonomy[$branch][] = $node; - } - } - } - - // This order must match EXACTLY the order of the $properties in the self::__unserialize method - return [ - $this->access, - $this->defaultLanguage, - $this->description, - $this->elements, - $this->end_date, - $this->instructions, - $this->language, - $this->list_price, - $this->publish_end_date, - $this->publish_start_date, - $this->published, - $this->route, - $this->sale_price, - $this->start_date, - $this->state, - $taxonomy, - $this->title, - $this->type_id, - $this->url - ]; - } - - /** - * Magic method used for unserializing. - * - * @since 4.1.3 - */ - public function __unserialize(array $serialized): void - { - // This order must match EXACTLY the order of the array in the self::__serialize method - $properties = [ - 'access', - 'defaultLanguage', - 'description', - 'elements', - 'end_date', - 'instructions', - 'language', - 'list_price', - 'publish_end_date', - 'publish_start_date', - 'published', - 'route', - 'sale_price', - 'start_date', - 'state', - 'taxonomy', - 'title', - 'type_id', - 'url', - ]; - - foreach ($properties as $k => $v) - { - $this->$v = $serialized[$k]; - } - - foreach ($this->taxonomy as $nodes) - { - foreach ($nodes as $node) - { - $curTaxonomy = Taxonomy::getTaxonomy($node->id); - $node->state = $curTaxonomy->state; - $node->access = $curTaxonomy->access; - } - } - } + /** + * An array of extra result properties. + * + * @var array + * @since 2.5 + */ + protected $elements = array(); + + /** + * This array tells the indexer which properties should be indexed and what + * weights to use for those properties. + * + * @var array + * @since 2.5 + */ + protected $instructions = array( + Indexer::TITLE_CONTEXT => array('title', 'subtitle', 'id'), + Indexer::TEXT_CONTEXT => array('summary', 'body'), + Indexer::META_CONTEXT => array('meta', 'list_price', 'sale_price'), + Indexer::PATH_CONTEXT => array('path', 'alias'), + Indexer::MISC_CONTEXT => array('comments'), + ); + + /** + * The indexer will use this data to create taxonomy mapping entries for + * the item so that it can be filtered by type, label, category, + * or whatever. + * + * @var array + * @since 2.5 + */ + protected $taxonomy = array(); + + /** + * The content URL. + * + * @var string + * @since 2.5 + */ + public $url; + + /** + * The content route. + * + * @var string + * @since 2.5 + */ + public $route; + + /** + * The content title. + * + * @var string + * @since 2.5 + */ + public $title; + + /** + * The content description. + * + * @var string + * @since 2.5 + */ + public $description; + + /** + * The published state of the result. + * + * @var integer + * @since 2.5 + */ + public $published; + + /** + * The content published state. + * + * @var integer + * @since 2.5 + */ + public $state; + + /** + * The content access level. + * + * @var integer + * @since 2.5 + */ + public $access; + + /** + * The content language. + * + * @var string + * @since 2.5 + */ + public $language = '*'; + + /** + * The publishing start date. + * + * @var string + * @since 2.5 + */ + public $publish_start_date; + + /** + * The publishing end date. + * + * @var string + * @since 2.5 + */ + public $publish_end_date; + + /** + * The generic start date. + * + * @var string + * @since 2.5 + */ + public $start_date; + + /** + * The generic end date. + * + * @var string + * @since 2.5 + */ + public $end_date; + + /** + * The item list price. + * + * @var mixed + * @since 2.5 + */ + public $list_price; + + /** + * The item sale price. + * + * @var mixed + * @since 2.5 + */ + public $sale_price; + + /** + * The content type id. This is set by the adapter. + * + * @var integer + * @since 2.5 + */ + public $type_id; + + /** + * The default language for content. + * + * @var string + * @since 3.0.2 + */ + public $defaultLanguage; + + /** + * Constructor + * + * @since 3.0.3 + */ + public function __construct() + { + $this->defaultLanguage = ComponentHelper::getParams('com_languages')->get('site', 'en-GB'); + } + + /** + * The magic set method is used to push additional values into the elements + * array in order to preserve the cleanliness of the object. + * + * @param string $name The name of the element. + * @param mixed $value The value of the element. + * + * @return void + * + * @since 2.5 + */ + public function __set($name, $value) + { + $this->setElement($name, $value); + } + + /** + * The magic get method is used to retrieve additional element values from the elements array. + * + * @param string $name The name of the element. + * + * @return mixed The value of the element if set, null otherwise. + * + * @since 2.5 + */ + public function __get($name) + { + return $this->getElement($name); + } + + /** + * The magic isset method is used to check the state of additional element values in the elements array. + * + * @param string $name The name of the element. + * + * @return boolean True if set, false otherwise. + * + * @since 2.5 + */ + public function __isset($name) + { + return isset($this->elements[$name]); + } + + /** + * The magic unset method is used to unset additional element values in the elements array. + * + * @param string $name The name of the element. + * + * @return void + * + * @since 2.5 + */ + public function __unset($name) + { + unset($this->elements[$name]); + } + + /** + * Method to retrieve additional element values from the elements array. + * + * @param string $name The name of the element. + * + * @return mixed The value of the element if set, null otherwise. + * + * @since 2.5 + */ + public function getElement($name) + { + // Get the element value if set. + if (array_key_exists($name, $this->elements)) { + return $this->elements[$name]; + } + + return null; + } + + /** + * Method to retrieve all elements. + * + * @return array The elements + * + * @since 3.8.3 + */ + public function getElements() + { + return $this->elements; + } + + /** + * Method to set additional element values in the elements array. + * + * @param string $name The name of the element. + * @param mixed $value The value of the element. + * + * @return void + * + * @since 2.5 + */ + public function setElement($name, $value) + { + $this->elements[$name] = $value; + } + + /** + * Method to get all processing instructions. + * + * @return array An array of processing instructions. + * + * @since 2.5 + */ + public function getInstructions() + { + return $this->instructions; + } + + /** + * Method to add a processing instruction for an item property. + * + * @param string $group The group to associate the property with. + * @param string $property The property to process. + * + * @return void + * + * @since 2.5 + */ + public function addInstruction($group, $property) + { + // Check if the group exists. We can't add instructions for unknown groups. + // Check if the property exists in the group. + if (array_key_exists($group, $this->instructions) && !in_array($property, $this->instructions[$group], true)) { + // Add the property to the group. + $this->instructions[$group][] = $property; + } + } + + /** + * Method to remove a processing instruction for an item property. + * + * @param string $group The group to associate the property with. + * @param string $property The property to process. + * + * @return void + * + * @since 2.5 + */ + public function removeInstruction($group, $property) + { + // Check if the group exists. We can't remove instructions for unknown groups. + if (array_key_exists($group, $this->instructions)) { + // Search for the property in the group. + $key = array_search($property, $this->instructions[$group]); + + // If the property was found, remove it. + if ($key !== false) { + unset($this->instructions[$group][$key]); + } + } + } + + /** + * Method to get the taxonomy maps for an item. + * + * @param string $branch The taxonomy branch to get. [optional] + * + * @return array An array of taxonomy maps. + * + * @since 2.5 + */ + public function getTaxonomy($branch = null) + { + // Get the taxonomy branch if available. + if ($branch !== null && isset($this->taxonomy[$branch])) { + return $this->taxonomy[$branch]; + } + + return $this->taxonomy; + } + + /** + * Method to add a taxonomy map for an item. + * + * @param string $branch The title of the taxonomy branch to add the node to. + * @param string $title The title of the taxonomy node. + * @param integer $state The published state of the taxonomy node. [optional] + * @param integer $access The access level of the taxonomy node. [optional] + * @param string $language The language of the taxonomy. [optional] + * + * @return void + * + * @since 2.5 + */ + public function addTaxonomy($branch, $title, $state = 1, $access = 1, $language = '') + { + // Filter the input. + $branch = preg_replace('#[^\pL\pM\pN\p{Pi}\p{Pf}\'+-.,_]+#mui', ' ', $branch); + + // Create the taxonomy node. + $node = new \stdClass(); + $node->title = $title; + $node->state = (int) $state; + $node->access = (int) $access; + $node->language = $language; + $node->nested = false; + + // Add the node to the taxonomy branch. + $this->taxonomy[$branch][] = $node; + } + + /** + * Method to add a nested taxonomy map for an item. + * + * @param string $branch The title of the taxonomy branch to add the node to. + * @param ImmutableNodeInterface $contentNode The node object. + * @param integer $state The published state of the taxonomy node. [optional] + * @param integer $access The access level of the taxonomy node. [optional] + * @param string $language The language of the taxonomy. [optional] + * + * @return void + * + * @since 4.0.0 + */ + public function addNestedTaxonomy($branch, ImmutableNodeInterface $contentNode, $state = 1, $access = 1, $language = '') + { + // Filter the input. + $branch = preg_replace('#[^\pL\pM\pN\p{Pi}\p{Pf}\'+-.,_]+#mui', ' ', $branch); + + // Create the taxonomy node. + $node = new \stdClass(); + $node->title = $contentNode->title; + $node->state = (int) $state; + $node->access = (int) $access; + $node->language = $language; + $node->nested = true; + $node->node = $contentNode; + + // Add the node to the taxonomy branch. + $this->taxonomy[$branch][] = $node; + } + + /** + * Method to set the item language + * + * @return void + * + * @since 3.0 + */ + public function setLanguage() + { + if ($this->language == '') { + $this->language = $this->defaultLanguage; + } + } + + /** + * Helper function to serialise the data of a Result object + * + * @return string The serialised data + * + * @since 4.0.0 + */ + public function serialize() + { + return serialize($this->__serialize()); + } + + /** + * Helper function to unserialise the data for this object + * + * @param string $serialized Serialised data to unserialise + * + * @return void + * + * @since 4.0.0 + */ + public function unserialize($serialized): void + { + $this->__unserialize(unserialize($serialized)); + } + + /** + * Magic method used for serializing. + * + * @since 4.1.3 + */ + public function __serialize(): array + { + $taxonomy = []; + + foreach ($this->taxonomy as $branch => $nodes) { + $taxonomy[$branch] = []; + + foreach ($nodes as $node) { + if ($node->nested) { + $n = clone $node; + unset($n->node); + $taxonomy[$branch][] = $n; + } else { + $taxonomy[$branch][] = $node; + } + } + } + + // This order must match EXACTLY the order of the $properties in the self::__unserialize method + return [ + $this->access, + $this->defaultLanguage, + $this->description, + $this->elements, + $this->end_date, + $this->instructions, + $this->language, + $this->list_price, + $this->publish_end_date, + $this->publish_start_date, + $this->published, + $this->route, + $this->sale_price, + $this->start_date, + $this->state, + $taxonomy, + $this->title, + $this->type_id, + $this->url + ]; + } + + /** + * Magic method used for unserializing. + * + * @since 4.1.3 + */ + public function __unserialize(array $serialized): void + { + // This order must match EXACTLY the order of the array in the self::__serialize method + $properties = [ + 'access', + 'defaultLanguage', + 'description', + 'elements', + 'end_date', + 'instructions', + 'language', + 'list_price', + 'publish_end_date', + 'publish_start_date', + 'published', + 'route', + 'sale_price', + 'start_date', + 'state', + 'taxonomy', + 'title', + 'type_id', + 'url', + ]; + + foreach ($properties as $k => $v) { + $this->$v = $serialized[$k]; + } + + foreach ($this->taxonomy as $nodes) { + foreach ($nodes as $node) { + $curTaxonomy = Taxonomy::getTaxonomy($node->id); + $node->state = $curTaxonomy->state; + $node->access = $curTaxonomy->access; + } + } + } } diff --git a/administrator/components/com_finder/src/Indexer/Taxonomy.php b/administrator/components/com_finder/src/Indexer/Taxonomy.php index bbc76c123b5bb..a24c9c9204323 100644 --- a/administrator/components/com_finder/src/Indexer/Taxonomy.php +++ b/administrator/components/com_finder/src/Indexer/Taxonomy.php @@ -1,4 +1,5 @@ title = $title; - $node->state = $state; - $node->access = $access; - $node->parent_id = 1; - $node->language = ''; - - return self::storeNode($node, 1); - } - - /** - * Method to add a node to the taxonomy tree. - * - * @param string $branch The title of the branch to store the node in. - * @param string $title The title of the node. - * @param integer $state The published state of the node. [optional] - * @param integer $access The access state of the node. [optional] - * @param string $language The language of the node. [optional] - * - * @return integer The id of the node. - * - * @since 2.5 - * @throws \RuntimeException on database error. - */ - public static function addNode($branch, $title, $state = 1, $access = 1, $language = '') - { - // Get the branch id, insert it if it does not exist. - $branchId = static::addBranch($branch); - - $node = new \stdClass; - $node->title = $title; - $node->state = $state; - $node->access = $access; - $node->parent_id = $branchId; - $node->language = $language; - - return self::storeNode($node, $branchId); - } - - /** - * Method to add a nested node to the taxonomy tree. - * - * @param string $branch The title of the branch to store the node in. - * @param NodeInterface $node The source-node of the taxonomy node. - * @param integer $state The published state of the node. [optional] - * @param integer $access The access state of the node. [optional] - * @param string $language The language of the node. [optional] - * @param integer $branchId ID of a branch if known. [optional] - * - * @return integer The id of the node. - * - * @since 4.0.0 - */ - public static function addNestedNode($branch, NodeInterface $node, $state = 1, $access = 1, $language = '', $branchId = null) - { - if (!$branchId) - { - // Get the branch id, insert it if it does not exist. - $branchId = static::addBranch($branch); - } - - $parent = $node->getParent(); - - if ($parent && $parent->title != 'ROOT') - { - $parentId = self::addNestedNode($branch, $parent, $state, $access, $language, $branchId); - } - else - { - $parentId = $branchId; - } - - $temp = new \stdClass; - $temp->title = $node->title; - $temp->state = $state; - $temp->access = $access; - $temp->parent_id = $parentId; - $temp->language = $language; - - return self::storeNode($temp, $parentId); - } - - /** - * A helper method to store a node in the taxonomy - * - * @param object $node The node data to include - * @param integer $parentId The parent id of the node to add. - * - * @return integer The id of the inserted node. - * - * @since 4.0.0 - * @throws \RuntimeException - */ - protected static function storeNode($node, $parentId) - { - // Check to see if the node is in the cache. - if (isset(static::$nodes[$parentId . ':' . $node->title])) - { - return static::$nodes[$parentId . ':' . $node->title]->id; - } - - // Check to see if the node is in the table. - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__finder_taxonomy')) - ->where($db->quoteName('parent_id') . ' = ' . $db->quote($parentId)) - ->where($db->quoteName('title') . ' = ' . $db->quote($node->title)) - ->where($db->quoteName('language') . ' = ' . $db->quote($node->language)); - - $db->setQuery($query); - - // Get the result. - $result = $db->loadObject(); - - // Check if the database matches the input data. - if ((bool) $result && $result->state == $node->state && $result->access == $node->access) - { - // The data matches, add the item to the cache. - static::$nodes[$parentId . ':' . $node->title] = $result; - - return static::$nodes[$parentId . ':' . $node->title]->id; - } - - /* - * The database did not match the input. This could be because the - * state has changed or because the node does not exist. Let's figure - * out which case is true and deal with it. - * @todo: use factory? - */ - $nodeTable = new MapTable($db); - - if (empty($result)) - { - // Prepare the node object. - $nodeTable->title = $node->title; - $nodeTable->state = (int) $node->state; - $nodeTable->access = (int) $node->access; - $nodeTable->language = $node->language; - $nodeTable->setLocation((int) $parentId, 'last-child'); - } - else - { - // Prepare the node object. - $nodeTable->id = (int) $result->id; - $nodeTable->title = $result->title; - $nodeTable->state = (int) ($node->state > 0 ? $node->state : $result->state); - $nodeTable->access = (int) $result->access; - $nodeTable->language = $node->language; - $nodeTable->setLocation($result->parent_id, 'last-child'); - } - - // Check the data. - if (!$nodeTable->check()) - { - $error = $nodeTable->getError(); - - if ($error instanceof \Exception) - { - // \Joomla\CMS\Table\NestedTable sets errors of exceptions, so in this case we can pass on more - // information - throw new \RuntimeException( - $error->getMessage(), - $error->getCode(), - $error - ); - } - - // Standard string returned. Probably from the \Joomla\CMS\Table\Table class - throw new \RuntimeException($error, 500); - } - - // Store the data. - if (!$nodeTable->store()) - { - $error = $nodeTable->getError(); - - if ($error instanceof \Exception) - { - // \Joomla\CMS\Table\NestedTable sets errors of exceptions, so in this case we can pass on more - // information - throw new \RuntimeException( - $error->getMessage(), - $error->getCode(), - $error - ); - } - - // Standard string returned. Probably from the \Joomla\CMS\Table\Table class - throw new \RuntimeException($error, 500); - } - - $nodeTable->rebuildPath($nodeTable->id); - - // Add the node to the cache. - static::$nodes[$parentId . ':' . $nodeTable->title] = (object) $nodeTable->getProperties(); - - return static::$nodes[$parentId . ':' . $nodeTable->title]->id; - } - - /** - * Method to add a map entry between a link and a taxonomy node. - * - * @param integer $linkId The link to map to. - * @param integer $nodeId The node to map to. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws \RuntimeException on database error. - */ - public static function addMap($linkId, $nodeId) - { - // Insert the map. - $db = Factory::getDbo(); - - $query = $db->getQuery(true) - ->select($db->quoteName('link_id')) - ->from($db->quoteName('#__finder_taxonomy_map')) - ->where($db->quoteName('link_id') . ' = ' . (int) $linkId) - ->where($db->quoteName('node_id') . ' = ' . (int) $nodeId); - $db->setQuery($query); - $db->execute(); - $id = (int) $db->loadResult(); - - if (!$id) - { - $map = new \stdClass; - $map->link_id = (int) $linkId; - $map->node_id = (int) $nodeId; - $db->insertObject('#__finder_taxonomy_map', $map); - } - - return true; - } - - /** - * Method to get the title of all taxonomy branches. - * - * @return array An array of branch titles. - * - * @since 2.5 - * @throws \RuntimeException on database error. - */ - public static function getBranchTitles() - { - $db = Factory::getDbo(); - - // Set user variables - $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); - - // Create a query to get the taxonomy branch titles. - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__finder_taxonomy')) - ->where($db->quoteName('parent_id') . ' = 1') - ->where($db->quoteName('state') . ' = 1') - ->where($db->quoteName('access') . ' IN (' . $groups . ')'); - - // Get the branch titles. - $db->setQuery($query); - - return $db->loadColumn(); - } - - /** - * Method to find a taxonomy node in a branch. - * - * @param string $branch The branch to search. - * @param string $title The title of the node. - * - * @return mixed Integer id on success, null on no match. - * - * @since 2.5 - * @throws \RuntimeException on database error. - */ - public static function getNodeByTitle($branch, $title) - { - $db = Factory::getDbo(); - - // Set user variables - $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); - - // Create a query to get the node. - $query = $db->getQuery(true) - ->select('t1.*') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t1') - ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS t2 ON t2.id = t1.parent_id') - ->where('t1.access IN (' . $groups . ')') - ->where('t1.state = 1') - ->where('t1.title LIKE ' . $db->quote($db->escape($title) . '%')) - ->where('t2.access IN (' . $groups . ')') - ->where('t2.state = 1') - ->where('t2.title = ' . $db->quote($branch)); - - // Get the node. - $query->setLimit(1); - $db->setQuery($query); - - return $db->loadObject(); - } - - /** - * Method to remove map entries for a link. - * - * @param integer $linkId The link to remove. - * - * @return boolean True on success. - * - * @since 2.5 - * @throws \RuntimeException on database error. - */ - public static function removeMaps($linkId) - { - // Delete the maps. - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__finder_taxonomy_map')) - ->where($db->quoteName('link_id') . ' = ' . (int) $linkId); - $db->setQuery($query); - $db->execute(); - - return true; - } - - /** - * Method to remove orphaned taxonomy maps - * - * @return integer The number of deleted rows. - * - * @since 4.2.0 - * @throws \RuntimeException on database error. - */ - public static function removeOrphanMaps() - { - // Delete all orphaned maps - $db = Factory::getDbo(); - $query2 = $db->getQuery(true) - ->select($db->quoteName('link_id')) - ->from($db->quoteName('#__finder_links')); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__finder_taxonomy_map')) - ->where($db->quoteName('link_id') . ' NOT IN (' . $query2 . ')'); - $db->setQuery($query); - $db->execute(); - $count = $db->getAffectedRows(); - - return $count; - } - - /** - * Method to remove orphaned taxonomy nodes and branches. - * - * @return integer The number of deleted rows. - * - * @since 2.5 - * @throws \RuntimeException on database error. - */ - public static function removeOrphanNodes() - { - // Delete all orphaned nodes. - $affectedRows = 0; - $db = Factory::getDbo(); - $nodeTable = new MapTable($db); - $query = $db->getQuery(true); - - $query->select($db->quoteName('t.id')) - ->from($db->quoteName('#__finder_taxonomy', 't')) - ->join('LEFT', $db->quoteName('#__finder_taxonomy_map', 'm') . ' ON ' . $db->quoteName('m.node_id') . '=' . $db->quoteName('t.id')) - ->where($db->quoteName('t.parent_id') . ' > 1 ') - ->where('t.lft + 1 = t.rgt') - ->where($db->quoteName('m.link_id') . ' IS NULL'); - - do - { - $db->setQuery($query); - $nodes = $db->loadColumn(); - - foreach ($nodes as $node) - { - $nodeTable->delete($node); - $affectedRows++; - } - } - while ($nodes); - - return $affectedRows; - } - - /** - * Get a taxonomy based on its id or all taxonomies - * - * @param integer $id Id of the taxonomy - * - * @return object|array A taxonomy object or an array of all taxonomies - * - * @since 4.0.0 - */ - public static function getTaxonomy($id = 0) - { - if (!count(self::$taxonomies)) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true); - - $query->select(array('id','parent_id','lft','rgt','level','path','title','alias','state','access','language')) - ->from($db->quoteName('#__finder_taxonomy')) - ->order($db->quoteName('lft')); - - $db->setQuery($query); - self::$taxonomies = $db->loadObjectList('id'); - } - - if ($id == 0) - { - return self::$taxonomies; - } - - if (isset(self::$taxonomies[$id])) - { - return self::$taxonomies[$id]; - } - - return false; - } - - /** - * Get a taxonomy branch object based on its title or all branches - * - * @param string $title Title of the branch - * - * @return object|array The object with the branch data or an array of all branches - * - * @since 4.0.0 - */ - public static function getBranch($title = '') - { - if (!count(self::$branches)) - { - $taxonomies = self::getTaxonomy(); - - foreach ($taxonomies as $t) - { - if ($t->level == 1) - { - self::$branches[$t->title] = $t; - } - } - } - - if ($title == '') - { - return self::$branches; - } - - if (isset(self::$branches[$title])) - { - return self::$branches[$title]; - } - - return false; - } + /** + * An internal cache of taxonomy data. + * + * @var object[] + * @since 4.0.0 + */ + public static $taxonomies = array(); + + /** + * An internal cache of branch data. + * + * @var object[] + * @since 4.0.0 + */ + public static $branches = array(); + + /** + * An internal cache of taxonomy node data for inserting it. + * + * @var object[] + * @since 2.5 + */ + public static $nodes = array(); + + /** + * Method to add a branch to the taxonomy tree. + * + * @param string $title The title of the branch. + * @param integer $state The published state of the branch. [optional] + * @param integer $access The access state of the branch. [optional] + * + * @return integer The id of the branch. + * + * @since 2.5 + * @throws \RuntimeException on database error. + */ + public static function addBranch($title, $state = 1, $access = 1) + { + $node = new \stdClass(); + $node->title = $title; + $node->state = $state; + $node->access = $access; + $node->parent_id = 1; + $node->language = ''; + + return self::storeNode($node, 1); + } + + /** + * Method to add a node to the taxonomy tree. + * + * @param string $branch The title of the branch to store the node in. + * @param string $title The title of the node. + * @param integer $state The published state of the node. [optional] + * @param integer $access The access state of the node. [optional] + * @param string $language The language of the node. [optional] + * + * @return integer The id of the node. + * + * @since 2.5 + * @throws \RuntimeException on database error. + */ + public static function addNode($branch, $title, $state = 1, $access = 1, $language = '') + { + // Get the branch id, insert it if it does not exist. + $branchId = static::addBranch($branch); + + $node = new \stdClass(); + $node->title = $title; + $node->state = $state; + $node->access = $access; + $node->parent_id = $branchId; + $node->language = $language; + + return self::storeNode($node, $branchId); + } + + /** + * Method to add a nested node to the taxonomy tree. + * + * @param string $branch The title of the branch to store the node in. + * @param NodeInterface $node The source-node of the taxonomy node. + * @param integer $state The published state of the node. [optional] + * @param integer $access The access state of the node. [optional] + * @param string $language The language of the node. [optional] + * @param integer $branchId ID of a branch if known. [optional] + * + * @return integer The id of the node. + * + * @since 4.0.0 + */ + public static function addNestedNode($branch, NodeInterface $node, $state = 1, $access = 1, $language = '', $branchId = null) + { + if (!$branchId) { + // Get the branch id, insert it if it does not exist. + $branchId = static::addBranch($branch); + } + + $parent = $node->getParent(); + + if ($parent && $parent->title != 'ROOT') { + $parentId = self::addNestedNode($branch, $parent, $state, $access, $language, $branchId); + } else { + $parentId = $branchId; + } + + $temp = new \stdClass(); + $temp->title = $node->title; + $temp->state = $state; + $temp->access = $access; + $temp->parent_id = $parentId; + $temp->language = $language; + + return self::storeNode($temp, $parentId); + } + + /** + * A helper method to store a node in the taxonomy + * + * @param object $node The node data to include + * @param integer $parentId The parent id of the node to add. + * + * @return integer The id of the inserted node. + * + * @since 4.0.0 + * @throws \RuntimeException + */ + protected static function storeNode($node, $parentId) + { + // Check to see if the node is in the cache. + if (isset(static::$nodes[$parentId . ':' . $node->title])) { + return static::$nodes[$parentId . ':' . $node->title]->id; + } + + // Check to see if the node is in the table. + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__finder_taxonomy')) + ->where($db->quoteName('parent_id') . ' = ' . $db->quote($parentId)) + ->where($db->quoteName('title') . ' = ' . $db->quote($node->title)) + ->where($db->quoteName('language') . ' = ' . $db->quote($node->language)); + + $db->setQuery($query); + + // Get the result. + $result = $db->loadObject(); + + // Check if the database matches the input data. + if ((bool) $result && $result->state == $node->state && $result->access == $node->access) { + // The data matches, add the item to the cache. + static::$nodes[$parentId . ':' . $node->title] = $result; + + return static::$nodes[$parentId . ':' . $node->title]->id; + } + + /* + * The database did not match the input. This could be because the + * state has changed or because the node does not exist. Let's figure + * out which case is true and deal with it. + * @todo: use factory? + */ + $nodeTable = new MapTable($db); + + if (empty($result)) { + // Prepare the node object. + $nodeTable->title = $node->title; + $nodeTable->state = (int) $node->state; + $nodeTable->access = (int) $node->access; + $nodeTable->language = $node->language; + $nodeTable->setLocation((int) $parentId, 'last-child'); + } else { + // Prepare the node object. + $nodeTable->id = (int) $result->id; + $nodeTable->title = $result->title; + $nodeTable->state = (int) ($node->state > 0 ? $node->state : $result->state); + $nodeTable->access = (int) $result->access; + $nodeTable->language = $node->language; + $nodeTable->setLocation($result->parent_id, 'last-child'); + } + + // Check the data. + if (!$nodeTable->check()) { + $error = $nodeTable->getError(); + + if ($error instanceof \Exception) { + // \Joomla\CMS\Table\NestedTable sets errors of exceptions, so in this case we can pass on more + // information + throw new \RuntimeException( + $error->getMessage(), + $error->getCode(), + $error + ); + } + + // Standard string returned. Probably from the \Joomla\CMS\Table\Table class + throw new \RuntimeException($error, 500); + } + + // Store the data. + if (!$nodeTable->store()) { + $error = $nodeTable->getError(); + + if ($error instanceof \Exception) { + // \Joomla\CMS\Table\NestedTable sets errors of exceptions, so in this case we can pass on more + // information + throw new \RuntimeException( + $error->getMessage(), + $error->getCode(), + $error + ); + } + + // Standard string returned. Probably from the \Joomla\CMS\Table\Table class + throw new \RuntimeException($error, 500); + } + + $nodeTable->rebuildPath($nodeTable->id); + + // Add the node to the cache. + static::$nodes[$parentId . ':' . $nodeTable->title] = (object) $nodeTable->getProperties(); + + return static::$nodes[$parentId . ':' . $nodeTable->title]->id; + } + + /** + * Method to add a map entry between a link and a taxonomy node. + * + * @param integer $linkId The link to map to. + * @param integer $nodeId The node to map to. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws \RuntimeException on database error. + */ + public static function addMap($linkId, $nodeId) + { + // Insert the map. + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select($db->quoteName('link_id')) + ->from($db->quoteName('#__finder_taxonomy_map')) + ->where($db->quoteName('link_id') . ' = ' . (int) $linkId) + ->where($db->quoteName('node_id') . ' = ' . (int) $nodeId); + $db->setQuery($query); + $db->execute(); + $id = (int) $db->loadResult(); + + if (!$id) { + $map = new \stdClass(); + $map->link_id = (int) $linkId; + $map->node_id = (int) $nodeId; + $db->insertObject('#__finder_taxonomy_map', $map); + } + + return true; + } + + /** + * Method to get the title of all taxonomy branches. + * + * @return array An array of branch titles. + * + * @since 2.5 + * @throws \RuntimeException on database error. + */ + public static function getBranchTitles() + { + $db = Factory::getDbo(); + + // Set user variables + $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); + + // Create a query to get the taxonomy branch titles. + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__finder_taxonomy')) + ->where($db->quoteName('parent_id') . ' = 1') + ->where($db->quoteName('state') . ' = 1') + ->where($db->quoteName('access') . ' IN (' . $groups . ')'); + + // Get the branch titles. + $db->setQuery($query); + + return $db->loadColumn(); + } + + /** + * Method to find a taxonomy node in a branch. + * + * @param string $branch The branch to search. + * @param string $title The title of the node. + * + * @return mixed Integer id on success, null on no match. + * + * @since 2.5 + * @throws \RuntimeException on database error. + */ + public static function getNodeByTitle($branch, $title) + { + $db = Factory::getDbo(); + + // Set user variables + $groups = implode(',', Factory::getUser()->getAuthorisedViewLevels()); + + // Create a query to get the node. + $query = $db->getQuery(true) + ->select('t1.*') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t1') + ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS t2 ON t2.id = t1.parent_id') + ->where('t1.access IN (' . $groups . ')') + ->where('t1.state = 1') + ->where('t1.title LIKE ' . $db->quote($db->escape($title) . '%')) + ->where('t2.access IN (' . $groups . ')') + ->where('t2.state = 1') + ->where('t2.title = ' . $db->quote($branch)); + + // Get the node. + $query->setLimit(1); + $db->setQuery($query); + + return $db->loadObject(); + } + + /** + * Method to remove map entries for a link. + * + * @param integer $linkId The link to remove. + * + * @return boolean True on success. + * + * @since 2.5 + * @throws \RuntimeException on database error. + */ + public static function removeMaps($linkId) + { + // Delete the maps. + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__finder_taxonomy_map')) + ->where($db->quoteName('link_id') . ' = ' . (int) $linkId); + $db->setQuery($query); + $db->execute(); + + return true; + } + + /** + * Method to remove orphaned taxonomy maps + * + * @return integer The number of deleted rows. + * + * @since 4.2.0 + * @throws \RuntimeException on database error. + */ + public static function removeOrphanMaps() + { + // Delete all orphaned maps + $db = Factory::getDbo(); + $query2 = $db->getQuery(true) + ->select($db->quoteName('link_id')) + ->from($db->quoteName('#__finder_links')); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__finder_taxonomy_map')) + ->where($db->quoteName('link_id') . ' NOT IN (' . $query2 . ')'); + $db->setQuery($query); + $db->execute(); + $count = $db->getAffectedRows(); + + return $count; + } + + /** + * Method to remove orphaned taxonomy nodes and branches. + * + * @return integer The number of deleted rows. + * + * @since 2.5 + * @throws \RuntimeException on database error. + */ + public static function removeOrphanNodes() + { + // Delete all orphaned nodes. + $affectedRows = 0; + $db = Factory::getDbo(); + $nodeTable = new MapTable($db); + $query = $db->getQuery(true); + + $query->select($db->quoteName('t.id')) + ->from($db->quoteName('#__finder_taxonomy', 't')) + ->join('LEFT', $db->quoteName('#__finder_taxonomy_map', 'm') . ' ON ' . $db->quoteName('m.node_id') . '=' . $db->quoteName('t.id')) + ->where($db->quoteName('t.parent_id') . ' > 1 ') + ->where('t.lft + 1 = t.rgt') + ->where($db->quoteName('m.link_id') . ' IS NULL'); + + do { + $db->setQuery($query); + $nodes = $db->loadColumn(); + + foreach ($nodes as $node) { + $nodeTable->delete($node); + $affectedRows++; + } + } while ($nodes); + + return $affectedRows; + } + + /** + * Get a taxonomy based on its id or all taxonomies + * + * @param integer $id Id of the taxonomy + * + * @return object|array A taxonomy object or an array of all taxonomies + * + * @since 4.0.0 + */ + public static function getTaxonomy($id = 0) + { + if (!count(self::$taxonomies)) { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + $query->select(array('id','parent_id','lft','rgt','level','path','title','alias','state','access','language')) + ->from($db->quoteName('#__finder_taxonomy')) + ->order($db->quoteName('lft')); + + $db->setQuery($query); + self::$taxonomies = $db->loadObjectList('id'); + } + + if ($id == 0) { + return self::$taxonomies; + } + + if (isset(self::$taxonomies[$id])) { + return self::$taxonomies[$id]; + } + + return false; + } + + /** + * Get a taxonomy branch object based on its title or all branches + * + * @param string $title Title of the branch + * + * @return object|array The object with the branch data or an array of all branches + * + * @since 4.0.0 + */ + public static function getBranch($title = '') + { + if (!count(self::$branches)) { + $taxonomies = self::getTaxonomy(); + + foreach ($taxonomies as $t) { + if ($t->level == 1) { + self::$branches[$t->title] = $t; + } + } + } + + if ($title == '') { + return self::$branches; + } + + if (isset(self::$branches[$title])) { + return self::$branches[$title]; + } + + return false; + } } diff --git a/administrator/components/com_finder/src/Indexer/Token.php b/administrator/components/com_finder/src/Indexer/Token.php index d52d9d18d8f53..fb0407e5f931f 100644 --- a/administrator/components/com_finder/src/Indexer/Token.php +++ b/administrator/components/com_finder/src/Indexer/Token.php @@ -1,4 +1,5 @@ language = '*'; - } - else - { - $this->language = $lang; - } - - // Tokens can be a single word or an array of words representing a phrase. - if (is_array($term)) - { - // Populate the token instance. - $this->term = implode($spacer, $term); - $this->stem = implode($spacer, array_map(array(Helper::class, 'stem'), $term, array($lang))); - $this->numeric = false; - $this->common = false; - $this->phrase = true; - $this->length = StringHelper::strlen($this->term); - - /* - * Calculate the weight of the token. - * - * 1. Length of the token up to 30 and divide by 30, add 1. - * 2. Round weight to 4 decimal points. - */ - $this->weight = (($this->length >= 30 ? 30 : $this->length) / 30) + 1; - $this->weight = round($this->weight, 4); - } - else - { - // Populate the token instance. - $this->term = $term; - $this->stem = Helper::stem($this->term, $lang); - $this->numeric = (is_numeric($this->term) || (bool) preg_match('#^[0-9,.\-\+]+$#', $this->term)); - $this->common = $this->numeric ? false : Helper::isCommon($this->term, $lang); - $this->phrase = false; - $this->length = StringHelper::strlen($this->term); - - /* - * Calculate the weight of the token. - * - * 1. Length of the token up to 15 and divide by 15. - * 2. If common term, divide weight by 8. - * 3. If numeric, multiply weight by 1.5. - * 4. Round weight to 4 decimal points. - */ - $this->weight = ($this->length >= 15 ? 15 : $this->length) / 15; - $this->weight = $this->common === true ? $this->weight / 8 : $this->weight; - $this->weight = $this->numeric === true ? $this->weight * 1.5 : $this->weight; - $this->weight = round($this->weight, 4); - } - } + /** + * This is the term that will be referenced in the terms table and the + * mapping tables. + * + * @var string + * @since 2.5 + */ + public $term; + + /** + * The stem is used to match the root term and produce more potential + * matches when searching the index. + * + * @var string + * @since 2.5 + */ + public $stem; + + /** + * If the token is numeric, it is likely to be short and uncommon so the + * weight is adjusted to compensate for that situation. + * + * @var boolean + * @since 2.5 + */ + public $numeric; + + /** + * If the token is a common term, the weight is adjusted to compensate for + * the higher frequency of the term in relation to other terms. + * + * @var boolean + * @since 2.5 + */ + public $common; + + /** + * Flag for phrase tokens. + * + * @var boolean + * @since 2.5 + */ + public $phrase; + + /** + * The length is used to calculate the weight of the token. + * + * @var integer + * @since 2.5 + */ + public $length; + + /** + * The weight is calculated based on token size and whether the token is + * considered a common term. + * + * @var integer + * @since 2.5 + */ + public $weight; + + /** + * The simple language identifier for the token. + * + * @var string + * @since 2.5 + */ + public $language; + + /** + * The container for matches. + * + * @var array + * @since 3.8.12 + */ + public $matches = array(); + + /** + * Is derived token (from individual words) + * + * @var boolean + * @since 3.8.12 + */ + public $derived; + + /** + * The suggested term + * + * @var string + * @since 3.8.12 + */ + public $suggestion; + + /** + * Method to construct the token object. + * + * @param mixed $term The term as a string for words or an array for phrases. + * @param string $lang The simple language identifier. + * @param string $spacer The space separator for phrases. [optional] + * + * @since 2.5 + */ + public function __construct($term, $lang, $spacer = ' ') + { + if (!$lang) { + $this->language = '*'; + } else { + $this->language = $lang; + } + + // Tokens can be a single word or an array of words representing a phrase. + if (is_array($term)) { + // Populate the token instance. + $this->term = implode($spacer, $term); + $this->stem = implode($spacer, array_map(array(Helper::class, 'stem'), $term, array($lang))); + $this->numeric = false; + $this->common = false; + $this->phrase = true; + $this->length = StringHelper::strlen($this->term); + + /* + * Calculate the weight of the token. + * + * 1. Length of the token up to 30 and divide by 30, add 1. + * 2. Round weight to 4 decimal points. + */ + $this->weight = (($this->length >= 30 ? 30 : $this->length) / 30) + 1; + $this->weight = round($this->weight, 4); + } else { + // Populate the token instance. + $this->term = $term; + $this->stem = Helper::stem($this->term, $lang); + $this->numeric = (is_numeric($this->term) || (bool) preg_match('#^[0-9,.\-\+]+$#', $this->term)); + $this->common = $this->numeric ? false : Helper::isCommon($this->term, $lang); + $this->phrase = false; + $this->length = StringHelper::strlen($this->term); + + /* + * Calculate the weight of the token. + * + * 1. Length of the token up to 15 and divide by 15. + * 2. If common term, divide weight by 8. + * 3. If numeric, multiply weight by 1.5. + * 4. Round weight to 4 decimal points. + */ + $this->weight = ($this->length >= 15 ? 15 : $this->length) / 15; + $this->weight = $this->common === true ? $this->weight / 8 : $this->weight; + $this->weight = $this->numeric === true ? $this->weight * 1.5 : $this->weight; + $this->weight = round($this->weight, 4); + } + } } diff --git a/administrator/components/com_finder/src/Model/FilterModel.php b/administrator/components/com_finder/src/Model/FilterModel.php index 1c1111923b319..c7f8869fe5e3c 100644 --- a/administrator/components/com_finder/src/Model/FilterModel.php +++ b/administrator/components/com_finder/src/Model/FilterModel.php @@ -1,4 +1,5 @@ getState('filter.id'); - - // Get a FinderTableFilter instance. - $filter = $this->getTable(); - - // Attempt to load the row. - $return = $filter->load($filter_id); - - // Check for a database error. - if ($return === false && $filter->getError()) - { - $this->setError($filter->getError()); - - return false; - } - - // Process the filter data. - if (!empty($filter->data)) - { - $filter->data = explode(',', $filter->data); - } - elseif (empty($filter->data)) - { - $filter->data = array(); - } - - return $filter; - } - - /** - * Method to get the record form. - * - * @param array $data Data for the form. [optional] - * @param boolean $loadData True if the form is to load its own data (default case), false if not. [optional] - * - * @return Form|boolean A Form object on success, false on failure - * - * @since 2.5 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_finder.filter', 'filter', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 2.5 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_finder.edit.filter.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_finder.filter', $data); - - return $data; - } - - /** - * Method to get the total indexed items - * - * @return number the number of indexed items - * - * @since 3.5 - */ - public function getTotal() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('MAX(link_id)') - ->from('#__finder_links'); - - return $db->setQuery($query)->loadResult(); - } + /** + * The prefix to use with controller messages. + * + * @var string + * @since 2.5 + */ + protected $text_prefix = 'COM_FINDER'; + + /** + * Model context string. + * + * @var string + * @since 2.5 + */ + protected $context = 'com_finder.filter'; + + /** + * Custom clean cache method. + * + * @param string $group The component name. [optional] + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 2.5 + */ + protected function cleanCache($group = 'com_finder', $clientId = 0) + { + parent::cleanCache($group); + } + + /** + * Method to get the filter data. + * + * @return FilterTable|boolean The filter data or false on a failure. + * + * @since 2.5 + */ + public function getFilter() + { + $filter_id = (int) $this->getState('filter.id'); + + // Get a FinderTableFilter instance. + $filter = $this->getTable(); + + // Attempt to load the row. + $return = $filter->load($filter_id); + + // Check for a database error. + if ($return === false && $filter->getError()) { + $this->setError($filter->getError()); + + return false; + } + + // Process the filter data. + if (!empty($filter->data)) { + $filter->data = explode(',', $filter->data); + } elseif (empty($filter->data)) { + $filter->data = array(); + } + + return $filter; + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. [optional] + * @param boolean $loadData True if the form is to load its own data (default case), false if not. [optional] + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 2.5 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_finder.filter', 'filter', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 2.5 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_finder.edit.filter.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_finder.filter', $data); + + return $data; + } + + /** + * Method to get the total indexed items + * + * @return number the number of indexed items + * + * @since 3.5 + */ + public function getTotal() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('MAX(link_id)') + ->from('#__finder_links'); + + return $db->setQuery($query)->loadResult(); + } } diff --git a/administrator/components/com_finder/src/Model/FiltersModel.php b/administrator/components/com_finder/src/Model/FiltersModel.php index 619910d57fd2d..0ff98dd0c92cf 100644 --- a/administrator/components/com_finder/src/Model/FiltersModel.php +++ b/administrator/components/com_finder/src/Model/FiltersModel.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true); - - // Select all fields from the table. - $query->select('a.*') - ->from($db->quoteName('#__finder_filters', 'a')); - - // Join over the users for the checked out user. - $query->select($db->quoteName('uc.name', 'editor')) - ->join('LEFT', $db->quoteName('#__users', 'uc') . ' ON ' . $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); - - // Join over the users for the author. - $query->select($db->quoteName('ua.name', 'user_name')) - ->join('LEFT', $db->quoteName('#__users', 'ua') . ' ON ' . $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_by')); - - // Check for a search filter. - if ($search = $this->getState('filter.search')) - { - $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); - $query->where($db->quoteName('a.title') . ' LIKE ' . $search); - } - - // If the model is set to check item state, add to the query. - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $query->where($db->quoteName('a.state') . ' = ' . (int) $state); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.title') . ' ' . $db->escape($this->getState('list.direction', 'ASC')))); - - return $query; - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. [optional] - * - * @return string A store id. - * - * @since 2.5 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - - return parent::getStoreId($id); - } - - /** - * Method to auto-populate the model state. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. [optional] - * @param string $direction An optional direction. [optional] - * - * @return void - * - * @since 2.5 - */ - protected function populateState($ordering = 'a.title', $direction = 'asc') - { - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); - - // Load the parameters. - $params = ComponentHelper::getParams('com_finder'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.7 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'filter_id', 'a.filter_id', + 'title', 'a.title', + 'state', 'a.state', + 'created_by_alias', 'a.created_by_alias', + 'created', 'a.created', + 'map_count', 'a.map_count' + ); + } + + parent::__construct($config, $factory); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 2.5 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select all fields from the table. + $query->select('a.*') + ->from($db->quoteName('#__finder_filters', 'a')); + + // Join over the users for the checked out user. + $query->select($db->quoteName('uc.name', 'editor')) + ->join('LEFT', $db->quoteName('#__users', 'uc') . ' ON ' . $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); + + // Join over the users for the author. + $query->select($db->quoteName('ua.name', 'user_name')) + ->join('LEFT', $db->quoteName('#__users', 'ua') . ' ON ' . $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_by')); + + // Check for a search filter. + if ($search = $this->getState('filter.search')) { + $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); + $query->where($db->quoteName('a.title') . ' LIKE ' . $search); + } + + // If the model is set to check item state, add to the query. + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $query->where($db->quoteName('a.state') . ' = ' . (int) $state); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.title') . ' ' . $db->escape($this->getState('list.direction', 'ASC')))); + + return $query; + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. [optional] + * + * @return string A store id. + * + * @since 2.5 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + + return parent::getStoreId($id); + } + + /** + * Method to auto-populate the model state. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. [optional] + * @param string $direction An optional direction. [optional] + * + * @return void + * + * @since 2.5 + */ + protected function populateState($ordering = 'a.title', $direction = 'asc') + { + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); + + // Load the parameters. + $params = ComponentHelper::getParams('com_finder'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } } diff --git a/administrator/components/com_finder/src/Model/IndexModel.php b/administrator/components/com_finder/src/Model/IndexModel.php index 752bd982e7a1d..5773b8211d255 100644 --- a/administrator/components/com_finder/src/Model/IndexModel.php +++ b/administrator/components/com_finder/src/Model/IndexModel.php @@ -1,4 +1,5 @@ authorise('core.delete', $this->option); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission for the component. - * - * @since 2.5 - */ - protected function canEditState($record) - { - return Factory::getUser()->authorise('core.edit.state', $this->option); - } - - /** - * Method to delete one or more records. - * - * @param array $pks An array of record primary keys. - * - * @return boolean True if successful, false if an error occurs. - * - * @since 2.5 - */ - public function delete(&$pks) - { - $pks = (array) $pks; - $table = $this->getTable(); - - // Include the content plugins for the on delete events. - PluginHelper::importPlugin('content'); - - // Iterate the items to delete each one. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - if ($this->canDelete($table)) - { - $context = $this->option . '.' . $this->name; - - // Trigger the onContentBeforeDelete event. - $result = Factory::getApplication()->triggerEvent($this->event_before_delete, array($context, $table)); - - if (in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - if (!$table->delete($pk)) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the onContentAfterDelete event. - Factory::getApplication()->triggerEvent($this->event_after_delete, array($context, $table)); - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - $error = $this->getError(); - - if ($error) - { - $this->setError($error); - } - else - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED')); - } - } - } - else - { - $this->setError($table->getError()); - - return false; - } - } - - // Clear the component's cache - $this->cleanCache(); - - return true; - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 2.5 - */ - protected function getListQuery() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('l.*') - ->select($db->quoteName('t.title', 't_title')) - ->from($db->quoteName('#__finder_links', 'l')) - ->join('INNER', $db->quoteName('#__finder_types', 't') . ' ON ' . $db->quoteName('t.id') . ' = ' . $db->quoteName('l.type_id')); - - // Check the type filter. - $type = $this->getState('filter.type'); - - // Join over the language - $query->select('la.title AS language_title, la.image AS language_image') - ->join('LEFT', $db->quoteName('#__languages') . ' AS la ON la.lang_code = l.language'); - - if (is_numeric($type)) - { - $query->where($db->quoteName('l.type_id') . ' = ' . (int) $type); - } - - // Check the map filter. - $contentMapId = $this->getState('filter.content_map'); - - if (is_numeric($contentMapId)) - { - $query->join('INNER', $db->quoteName('#__finder_taxonomy_map', 'm') . ' ON ' . $db->quoteName('m.link_id') . ' = ' . $db->quoteName('l.link_id')) - ->where($db->quoteName('m.node_id') . ' = ' . (int) $contentMapId); - } - - // Check for state filter. - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $query->where($db->quoteName('l.published') . ' = ' . (int) $state); - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('l.language') . ' = ' . $db->quote($language)); - } - - // Check the search phrase. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); - $orSearchSql = $db->quoteName('l.title') . ' LIKE ' . $search . ' OR ' . $db->quoteName('l.url') . ' LIKE ' . $search; - - // Filter by indexdate only if $search doesn't contains non-ascii characters - if (!preg_match('/[^\x00-\x7F]/', $search)) - { - $orSearchSql .= ' OR ' . $query->castAsChar($db->quoteName('l.indexdate')) . ' LIKE ' . $search; - } - - $query->where('(' . $orSearchSql . ')'); - } - - // Handle the list ordering. - $listOrder = $this->getState('list.ordering', 'l.title'); - $listDir = $this->getState('list.direction', 'ASC'); - - if ($listOrder === 't.title') - { - $ordering = $db->quoteName('t.title') . ' ' . $db->escape($listDir) . ', ' . $db->quoteName('l.title') . ' ' . $db->escape($listDir); - } - else - { - $ordering = $db->escape($listOrder) . ' ' . $db->escape($listDir); - } - - $query->order($ordering); - - return $query; - } - - /** - * Method to get the state of the Smart Search Plugins. - * - * @return array Array of relevant plugins and whether they are enabled or not. - * - * @since 2.5 - */ - public function getPluginState() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('name, enabled') - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' IN (' . $db->quote('system') . ',' . $db->quote('content') . ')') - ->where($db->quoteName('element') . ' = ' . $db->quote('finder')); - $db->setQuery($query); - - return $db->loadObjectList('name'); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. [optional] - * - * @return string A store id. - * - * @since 2.5 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.type'); - $id .= ':' . $this->getState('filter.content_map'); - - return parent::getStoreId($id); - } - - /** - * Gets the total of indexed items. - * - * @return integer The total of indexed items. - * - * @since 3.6.0 - */ - public function getTotalIndexed() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('COUNT(link_id)') - ->from($db->quoteName('#__finder_links')); - $db->setQuery($query); - - return (int) $db->loadResult(); - } - - /** - * Returns a Table object, always creating it. - * - * @param string $type The table type to instantiate. [optional] - * @param string $prefix A prefix for the table class name. [optional] - * @param array $config Configuration array for model. [optional] - * - * @return \Joomla\CMS\Table\Table A database object - * - * @since 2.5 - */ - public function getTable($type = 'Link', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } - - /** - * Method to purge the index, deleting all links. - * - * @return boolean True on success, false on failure. - * - * @since 2.5 - * @throws \Exception on database error - */ - public function purge() - { - $db = $this->getDatabase(); - - // Truncate the links table. - $db->truncateTable('#__finder_links'); - - // Truncate the links terms tables. - $db->truncateTable('#__finder_links_terms'); - - // Truncate the terms table. - $db->truncateTable('#__finder_terms'); - - // Truncate the taxonomy map table. - $db->truncateTable('#__finder_taxonomy_map'); - - // Truncate the taxonomy table and insert the root node. - $db->truncateTable('#__finder_taxonomy'); - $root = (object) array( - 'id' => 1, - 'parent_id' => 0, - 'lft' => 0, - 'rgt' => 1, - 'level' => 0, - 'path' => '', - 'title' => 'ROOT', - 'alias' => 'root', - 'state' => 1, - 'access' => 1, - 'language' => '*' - ); - $db->insertObject('#__finder_taxonomy', $root); - - // Truncate the tokens tables. - $db->truncateTable('#__finder_tokens'); - - // Truncate the tokens aggregate table. - $db->truncateTable('#__finder_tokens_aggregate'); - - // Include the finder plugins for the on purge events. - PluginHelper::importPlugin('finder'); - Factory::getApplication()->triggerEvent($this->event_after_purge); - - return true; - } - - /** - * Method to auto-populate the model state. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. [optional] - * @param string $direction An optional direction. [optional] - * - * @return void - * - * @since 2.5 - */ - protected function populateState($ordering = 'l.title', $direction = 'asc') - { - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); - $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'cmd')); - $this->setState('filter.content_map', $this->getUserStateFromRequest($this->context . '.filter.content_map', 'filter_content_map', '', 'cmd')); - $this->setState('filter.language', $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', '')); - - // Load the parameters. - $params = ComponentHelper::getParams('com_finder'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to change the published state of one or more records. - * - * @param array $pks A list of the primary keys to change. - * @param integer $value The value of the published state. [optional] - * - * @return boolean True on success. - * - * @since 2.5 - */ - public function publish(&$pks, $value = 1) - { - $user = Factory::getUser(); - $table = $this->getTable(); - $pks = (array) $pks; - - // Include the content plugins for the change of state event. - PluginHelper::importPlugin('content'); - - // Access checks. - foreach ($pks as $i => $pk) - { - $table->reset(); - - if ($table->load($pk) && !$this->canEditState($table)) - { - // Prune items that you can't change. - unset($pks[$i]); - $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); - - return false; - } - } - - // Attempt to change the state of the records. - if (!$table->publish($pks, $value, $user->get('id'))) - { - $this->setError($table->getError()); - - return false; - } - - $context = $this->option . '.' . $this->name; - - // Trigger the onContentChangeState event. - $result = Factory::getApplication()->triggerEvent('onContentChangeState', array($context, $pks, $value)); - - if (in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - // Clear the component's cache - $this->cleanCache(); - - return true; - } + /** + * The event to trigger after deleting the data. + * + * @var string + * @since 2.5 + */ + protected $event_after_delete = 'onContentAfterDelete'; + + /** + * The event to trigger before deleting the data. + * + * @var string + * @since 2.5 + */ + protected $event_before_delete = 'onContentBeforeDelete'; + + /** + * The event to trigger after purging the data. + * + * @var string + * @since 4.0.0 + */ + protected $event_after_purge = 'onFinderIndexAfterPurge'; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.7 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'state', 'published', 'l.published', + 'title', 'l.title', + 'type', 'type_id', 'l.type_id', + 't.title', 't_title', + 'url', 'l.url', + 'language', 'l.language', + 'indexdate', 'l.indexdate', + 'content_map', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission for the component. + * + * @since 2.5 + */ + protected function canDelete($record) + { + return Factory::getUser()->authorise('core.delete', $this->option); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission for the component. + * + * @since 2.5 + */ + protected function canEditState($record) + { + return Factory::getUser()->authorise('core.edit.state', $this->option); + } + + /** + * Method to delete one or more records. + * + * @param array $pks An array of record primary keys. + * + * @return boolean True if successful, false if an error occurs. + * + * @since 2.5 + */ + public function delete(&$pks) + { + $pks = (array) $pks; + $table = $this->getTable(); + + // Include the content plugins for the on delete events. + PluginHelper::importPlugin('content'); + + // Iterate the items to delete each one. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + if ($this->canDelete($table)) { + $context = $this->option . '.' . $this->name; + + // Trigger the onContentBeforeDelete event. + $result = Factory::getApplication()->triggerEvent($this->event_before_delete, array($context, $table)); + + if (in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + if (!$table->delete($pk)) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the onContentAfterDelete event. + Factory::getApplication()->triggerEvent($this->event_after_delete, array($context, $table)); + } else { + // Prune items that you can't change. + unset($pks[$i]); + $error = $this->getError(); + + if ($error) { + $this->setError($error); + } else { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED')); + } + } + } else { + $this->setError($table->getError()); + + return false; + } + } + + // Clear the component's cache + $this->cleanCache(); + + return true; + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 2.5 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('l.*') + ->select($db->quoteName('t.title', 't_title')) + ->from($db->quoteName('#__finder_links', 'l')) + ->join('INNER', $db->quoteName('#__finder_types', 't') . ' ON ' . $db->quoteName('t.id') . ' = ' . $db->quoteName('l.type_id')); + + // Check the type filter. + $type = $this->getState('filter.type'); + + // Join over the language + $query->select('la.title AS language_title, la.image AS language_image') + ->join('LEFT', $db->quoteName('#__languages') . ' AS la ON la.lang_code = l.language'); + + if (is_numeric($type)) { + $query->where($db->quoteName('l.type_id') . ' = ' . (int) $type); + } + + // Check the map filter. + $contentMapId = $this->getState('filter.content_map'); + + if (is_numeric($contentMapId)) { + $query->join('INNER', $db->quoteName('#__finder_taxonomy_map', 'm') . ' ON ' . $db->quoteName('m.link_id') . ' = ' . $db->quoteName('l.link_id')) + ->where($db->quoteName('m.node_id') . ' = ' . (int) $contentMapId); + } + + // Check for state filter. + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $query->where($db->quoteName('l.published') . ' = ' . (int) $state); + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('l.language') . ' = ' . $db->quote($language)); + } + + // Check the search phrase. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); + $orSearchSql = $db->quoteName('l.title') . ' LIKE ' . $search . ' OR ' . $db->quoteName('l.url') . ' LIKE ' . $search; + + // Filter by indexdate only if $search doesn't contains non-ascii characters + if (!preg_match('/[^\x00-\x7F]/', $search)) { + $orSearchSql .= ' OR ' . $query->castAsChar($db->quoteName('l.indexdate')) . ' LIKE ' . $search; + } + + $query->where('(' . $orSearchSql . ')'); + } + + // Handle the list ordering. + $listOrder = $this->getState('list.ordering', 'l.title'); + $listDir = $this->getState('list.direction', 'ASC'); + + if ($listOrder === 't.title') { + $ordering = $db->quoteName('t.title') . ' ' . $db->escape($listDir) . ', ' . $db->quoteName('l.title') . ' ' . $db->escape($listDir); + } else { + $ordering = $db->escape($listOrder) . ' ' . $db->escape($listDir); + } + + $query->order($ordering); + + return $query; + } + + /** + * Method to get the state of the Smart Search Plugins. + * + * @return array Array of relevant plugins and whether they are enabled or not. + * + * @since 2.5 + */ + public function getPluginState() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('name, enabled') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' IN (' . $db->quote('system') . ',' . $db->quote('content') . ')') + ->where($db->quoteName('element') . ' = ' . $db->quote('finder')); + $db->setQuery($query); + + return $db->loadObjectList('name'); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. [optional] + * + * @return string A store id. + * + * @since 2.5 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.type'); + $id .= ':' . $this->getState('filter.content_map'); + + return parent::getStoreId($id); + } + + /** + * Gets the total of indexed items. + * + * @return integer The total of indexed items. + * + * @since 3.6.0 + */ + public function getTotalIndexed() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('COUNT(link_id)') + ->from($db->quoteName('#__finder_links')); + $db->setQuery($query); + + return (int) $db->loadResult(); + } + + /** + * Returns a Table object, always creating it. + * + * @param string $type The table type to instantiate. [optional] + * @param string $prefix A prefix for the table class name. [optional] + * @param array $config Configuration array for model. [optional] + * + * @return \Joomla\CMS\Table\Table A database object + * + * @since 2.5 + */ + public function getTable($type = 'Link', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } + + /** + * Method to purge the index, deleting all links. + * + * @return boolean True on success, false on failure. + * + * @since 2.5 + * @throws \Exception on database error + */ + public function purge() + { + $db = $this->getDatabase(); + + // Truncate the links table. + $db->truncateTable('#__finder_links'); + + // Truncate the links terms tables. + $db->truncateTable('#__finder_links_terms'); + + // Truncate the terms table. + $db->truncateTable('#__finder_terms'); + + // Truncate the taxonomy map table. + $db->truncateTable('#__finder_taxonomy_map'); + + // Truncate the taxonomy table and insert the root node. + $db->truncateTable('#__finder_taxonomy'); + $root = (object) array( + 'id' => 1, + 'parent_id' => 0, + 'lft' => 0, + 'rgt' => 1, + 'level' => 0, + 'path' => '', + 'title' => 'ROOT', + 'alias' => 'root', + 'state' => 1, + 'access' => 1, + 'language' => '*' + ); + $db->insertObject('#__finder_taxonomy', $root); + + // Truncate the tokens tables. + $db->truncateTable('#__finder_tokens'); + + // Truncate the tokens aggregate table. + $db->truncateTable('#__finder_tokens_aggregate'); + + // Include the finder plugins for the on purge events. + PluginHelper::importPlugin('finder'); + Factory::getApplication()->triggerEvent($this->event_after_purge); + + return true; + } + + /** + * Method to auto-populate the model state. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. [optional] + * @param string $direction An optional direction. [optional] + * + * @return void + * + * @since 2.5 + */ + protected function populateState($ordering = 'l.title', $direction = 'asc') + { + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); + $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'cmd')); + $this->setState('filter.content_map', $this->getUserStateFromRequest($this->context . '.filter.content_map', 'filter_content_map', '', 'cmd')); + $this->setState('filter.language', $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', '')); + + // Load the parameters. + $params = ComponentHelper::getParams('com_finder'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to change the published state of one or more records. + * + * @param array $pks A list of the primary keys to change. + * @param integer $value The value of the published state. [optional] + * + * @return boolean True on success. + * + * @since 2.5 + */ + public function publish(&$pks, $value = 1) + { + $user = Factory::getUser(); + $table = $this->getTable(); + $pks = (array) $pks; + + // Include the content plugins for the change of state event. + PluginHelper::importPlugin('content'); + + // Access checks. + foreach ($pks as $i => $pk) { + $table->reset(); + + if ($table->load($pk) && !$this->canEditState($table)) { + // Prune items that you can't change. + unset($pks[$i]); + $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); + + return false; + } + } + + // Attempt to change the state of the records. + if (!$table->publish($pks, $value, $user->get('id'))) { + $this->setError($table->getError()); + + return false; + } + + $context = $this->option . '.' . $this->name; + + // Trigger the onContentChangeState event. + $result = Factory::getApplication()->triggerEvent('onContentChangeState', array($context, $pks, $value)); + + if (in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + // Clear the component's cache + $this->cleanCache(); + + return true; + } } diff --git a/administrator/components/com_finder/src/Model/IndexerModel.php b/administrator/components/com_finder/src/Model/IndexerModel.php index 34a9f023259e5..867f83a412448 100644 --- a/administrator/components/com_finder/src/Model/IndexerModel.php +++ b/administrator/components/com_finder/src/Model/IndexerModel.php @@ -1,4 +1,5 @@ authorise('core.delete', $this->option); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission for the component. - * - * @since 2.5 - */ - protected function canEditState($record) - { - return Factory::getUser()->authorise('core.edit.state', $this->option); - } - - /** - * Method to delete one or more records. - * - * @param array $pks An array of record primary keys. - * - * @return boolean True if successful, false if an error occurs. - * - * @since 2.5 - */ - public function delete(&$pks) - { - $pks = (array) $pks; - $table = $this->getTable(); - - // Include the content plugins for the on delete events. - PluginHelper::importPlugin('content'); - - // Iterate the items to check if all of them exist. - foreach ($pks as $i => $pk) - { - if (!$table->load($pk)) - { - // Item is not in the table. - $this->setError($table->getError()); - - return false; - } - } - - // Iterate the items to delete each one. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - if ($this->canDelete($table)) - { - $context = $this->option . '.' . $this->name; - - // Trigger the onContentBeforeDelete event. - $result = Factory::getApplication()->triggerEvent('onContentBeforeDelete', array($context, $table)); - - if (in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - if (!$table->delete($pk)) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the onContentAfterDelete event. - Factory::getApplication()->triggerEvent('onContentAfterDelete', array($context, $table)); - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - $error = $this->getError(); - - if ($error) - { - $this->setError($error); - } - else - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED')); - } - } - } - } - - // Clear the component's cache - $this->cleanCache(); - - return true; - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 2.5 - */ - protected function getListQuery() - { - $db = $this->getDatabase(); - - // Select all fields from the table. - $query = $db->getQuery(true) - ->select('a.id, a.parent_id, a.lft, a.rgt, a.level, a.path, a.title, a.alias, a.state, a.access, a.language') - ->from($db->quoteName('#__finder_taxonomy', 'a')) - ->where('a.parent_id != 0'); - - // Join to get the branch title - $query->select([$db->quoteName('b.id', 'branch_id'), $db->quoteName('b.title', 'branch_title')]) - ->leftJoin($db->quoteName('#__finder_taxonomy', 'b') . ' ON b.level = 1 AND b.lft <= a.lft AND a.rgt <= b.rgt'); - - // Join to get the map links. - $stateQuery = $db->getQuery(true) - ->select('m.node_id') - ->select('COUNT(NULLIF(l.published, 0)) AS count_published') - ->select('COUNT(NULLIF(l.published, 1)) AS count_unpublished') - ->from($db->quoteName('#__finder_taxonomy_map', 'm')) - ->leftJoin($db->quoteName('#__finder_links', 'l') . ' ON l.link_id = m.link_id') - ->group('m.node_id'); - - $query->select('COALESCE(s.count_published, 0) AS count_published'); - $query->select('COALESCE(s.count_unpublished, 0) AS count_unpublished'); - $query->leftJoin('(' . $stateQuery . ') AS s ON s.node_id = a.id'); - - // If the model is set to check item state, add to the query. - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $query->where('a.state = ' . (int) $state); - } - - // Filter over level. - $level = $this->getState('filter.level'); - - if (is_numeric($level) && (int) $level === 1) - { - $query->where('a.parent_id = 1'); - } - - // Filter the maps over the branch if set. - $branchId = $this->getState('filter.branch'); - - if (is_numeric($branchId)) - { - $query->where('a.parent_id = ' . (int) $branchId); - } - - // Filter the maps over the search string if set. - if ($search = $this->getState('filter.search')) - { - $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); - $query->where('a.title LIKE ' . $search); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'branch_title, a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Returns a record count for the query. - * - * @param \Joomla\Database\DatabaseQuery|string - * - * @return integer Number of rows for query. - * - * @since 3.0 - */ - protected function _getListCount($query) - { - $query = clone $query; - $query->clear('select')->clear('join')->clear('order')->clear('limit')->clear('offset')->select('COUNT(*)'); - - return (int) $this->getDatabase()->setQuery($query)->loadResult(); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. [optional] - * - * @return string A store id. - * - * @since 2.5 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.branch'); - $id .= ':' . $this->getState('filter.level'); - - return parent::getStoreId($id); - } - - /** - * Returns a Table object, always creating it. - * - * @param string $type The table type to instantiate. [optional] - * @param string $prefix A prefix for the table class name. [optional] - * @param array $config Configuration array for model. [optional] - * - * @return \Joomla\CMS\Table\Table A database object - * - * @since 2.5 - */ - public function getTable($type = 'Map', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } - - /** - * Method to auto-populate the model state. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. [optional] - * @param string $direction An optional direction. [optional] - * - * @return void - * - * @since 2.5 - */ - protected function populateState($ordering = 'branch_title, a.lft', $direction = 'ASC') - { - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); - $this->setState('filter.branch', $this->getUserStateFromRequest($this->context . '.filter.branch', 'filter_branch', '', 'cmd')); - $this->setState('filter.level', $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level', '', 'cmd')); - - // Load the parameters. - $params = ComponentHelper::getParams('com_finder'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to change the published state of one or more records. - * - * @param array $pks A list of the primary keys to change. - * @param integer $value The value of the published state. [optional] - * - * @return boolean True on success. - * - * @since 2.5 - */ - public function publish(&$pks, $value = 1) - { - $user = Factory::getUser(); - $table = $this->getTable(); - $pks = (array) $pks; - - // Include the content plugins for the change of state event. - PluginHelper::importPlugin('content'); - - // Access checks. - foreach ($pks as $i => $pk) - { - $table->reset(); - - if ($table->load($pk) && !$this->canEditState($table)) - { - // Prune items that you can't change. - unset($pks[$i]); - $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); - - return false; - } - } - - // Attempt to change the state of the records. - if (!$table->publish($pks, $value, $user->get('id'))) - { - $this->setError($table->getError()); - - return false; - } - - $context = $this->option . '.' . $this->name; - - // Trigger the onContentChangeState event. - $result = Factory::getApplication()->triggerEvent('onContentChangeState', array($context, $pks, $value)); - - if (in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - // Clear the component's cache - $this->cleanCache(); - - return true; - } - - /** - * Method to purge all maps from the taxonomy. - * - * @return boolean Returns true on success, false on failure. - * - * @since 2.5 - */ - public function purge() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__finder_taxonomy')) - ->where($db->quoteName('parent_id') . ' > 1'); - $db->setQuery($query); - $db->execute(); - - $query->clear() - ->delete($db->quoteName('#__finder_taxonomy_map')); - $db->setQuery($query); - $db->execute(); - - return true; - } - - /** - * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. - * - * @return DatabaseQuery - * - * @since 4.0.0 - */ - protected function getEmptyStateQuery() - { - $query = parent::getEmptyStateQuery(); - - $title = 'ROOT'; - - $query->where($this->getDatabase()->quoteName('title') . ' <> :title') - ->bind(':title', $title); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.7 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'state', 'a.state', + 'title', 'a.title', + 'branch', + 'branch_title', 'd.branch_title', + 'level', 'd.level', + 'language', 'a.language', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission for the component. + * + * @since 2.5 + */ + protected function canDelete($record) + { + return Factory::getUser()->authorise('core.delete', $this->option); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission for the component. + * + * @since 2.5 + */ + protected function canEditState($record) + { + return Factory::getUser()->authorise('core.edit.state', $this->option); + } + + /** + * Method to delete one or more records. + * + * @param array $pks An array of record primary keys. + * + * @return boolean True if successful, false if an error occurs. + * + * @since 2.5 + */ + public function delete(&$pks) + { + $pks = (array) $pks; + $table = $this->getTable(); + + // Include the content plugins for the on delete events. + PluginHelper::importPlugin('content'); + + // Iterate the items to check if all of them exist. + foreach ($pks as $i => $pk) { + if (!$table->load($pk)) { + // Item is not in the table. + $this->setError($table->getError()); + + return false; + } + } + + // Iterate the items to delete each one. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + if ($this->canDelete($table)) { + $context = $this->option . '.' . $this->name; + + // Trigger the onContentBeforeDelete event. + $result = Factory::getApplication()->triggerEvent('onContentBeforeDelete', array($context, $table)); + + if (in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + if (!$table->delete($pk)) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the onContentAfterDelete event. + Factory::getApplication()->triggerEvent('onContentAfterDelete', array($context, $table)); + } else { + // Prune items that you can't change. + unset($pks[$i]); + $error = $this->getError(); + + if ($error) { + $this->setError($error); + } else { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED')); + } + } + } + } + + // Clear the component's cache + $this->cleanCache(); + + return true; + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 2.5 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + + // Select all fields from the table. + $query = $db->getQuery(true) + ->select('a.id, a.parent_id, a.lft, a.rgt, a.level, a.path, a.title, a.alias, a.state, a.access, a.language') + ->from($db->quoteName('#__finder_taxonomy', 'a')) + ->where('a.parent_id != 0'); + + // Join to get the branch title + $query->select([$db->quoteName('b.id', 'branch_id'), $db->quoteName('b.title', 'branch_title')]) + ->leftJoin($db->quoteName('#__finder_taxonomy', 'b') . ' ON b.level = 1 AND b.lft <= a.lft AND a.rgt <= b.rgt'); + + // Join to get the map links. + $stateQuery = $db->getQuery(true) + ->select('m.node_id') + ->select('COUNT(NULLIF(l.published, 0)) AS count_published') + ->select('COUNT(NULLIF(l.published, 1)) AS count_unpublished') + ->from($db->quoteName('#__finder_taxonomy_map', 'm')) + ->leftJoin($db->quoteName('#__finder_links', 'l') . ' ON l.link_id = m.link_id') + ->group('m.node_id'); + + $query->select('COALESCE(s.count_published, 0) AS count_published'); + $query->select('COALESCE(s.count_unpublished, 0) AS count_unpublished'); + $query->leftJoin('(' . $stateQuery . ') AS s ON s.node_id = a.id'); + + // If the model is set to check item state, add to the query. + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $query->where('a.state = ' . (int) $state); + } + + // Filter over level. + $level = $this->getState('filter.level'); + + if (is_numeric($level) && (int) $level === 1) { + $query->where('a.parent_id = 1'); + } + + // Filter the maps over the branch if set. + $branchId = $this->getState('filter.branch'); + + if (is_numeric($branchId)) { + $query->where('a.parent_id = ' . (int) $branchId); + } + + // Filter the maps over the search string if set. + if ($search = $this->getState('filter.search')) { + $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); + $query->where('a.title LIKE ' . $search); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'branch_title, a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Returns a record count for the query. + * + * @param \Joomla\Database\DatabaseQuery|string + * + * @return integer Number of rows for query. + * + * @since 3.0 + */ + protected function _getListCount($query) + { + $query = clone $query; + $query->clear('select')->clear('join')->clear('order')->clear('limit')->clear('offset')->select('COUNT(*)'); + + return (int) $this->getDatabase()->setQuery($query)->loadResult(); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. [optional] + * + * @return string A store id. + * + * @since 2.5 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.branch'); + $id .= ':' . $this->getState('filter.level'); + + return parent::getStoreId($id); + } + + /** + * Returns a Table object, always creating it. + * + * @param string $type The table type to instantiate. [optional] + * @param string $prefix A prefix for the table class name. [optional] + * @param array $config Configuration array for model. [optional] + * + * @return \Joomla\CMS\Table\Table A database object + * + * @since 2.5 + */ + public function getTable($type = 'Map', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } + + /** + * Method to auto-populate the model state. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. [optional] + * @param string $direction An optional direction. [optional] + * + * @return void + * + * @since 2.5 + */ + protected function populateState($ordering = 'branch_title, a.lft', $direction = 'ASC') + { + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'cmd')); + $this->setState('filter.branch', $this->getUserStateFromRequest($this->context . '.filter.branch', 'filter_branch', '', 'cmd')); + $this->setState('filter.level', $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level', '', 'cmd')); + + // Load the parameters. + $params = ComponentHelper::getParams('com_finder'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to change the published state of one or more records. + * + * @param array $pks A list of the primary keys to change. + * @param integer $value The value of the published state. [optional] + * + * @return boolean True on success. + * + * @since 2.5 + */ + public function publish(&$pks, $value = 1) + { + $user = Factory::getUser(); + $table = $this->getTable(); + $pks = (array) $pks; + + // Include the content plugins for the change of state event. + PluginHelper::importPlugin('content'); + + // Access checks. + foreach ($pks as $i => $pk) { + $table->reset(); + + if ($table->load($pk) && !$this->canEditState($table)) { + // Prune items that you can't change. + unset($pks[$i]); + $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); + + return false; + } + } + + // Attempt to change the state of the records. + if (!$table->publish($pks, $value, $user->get('id'))) { + $this->setError($table->getError()); + + return false; + } + + $context = $this->option . '.' . $this->name; + + // Trigger the onContentChangeState event. + $result = Factory::getApplication()->triggerEvent('onContentChangeState', array($context, $pks, $value)); + + if (in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + // Clear the component's cache + $this->cleanCache(); + + return true; + } + + /** + * Method to purge all maps from the taxonomy. + * + * @return boolean Returns true on success, false on failure. + * + * @since 2.5 + */ + public function purge() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__finder_taxonomy')) + ->where($db->quoteName('parent_id') . ' > 1'); + $db->setQuery($query); + $db->execute(); + + $query->clear() + ->delete($db->quoteName('#__finder_taxonomy_map')); + $db->setQuery($query); + $db->execute(); + + return true; + } + + /** + * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. + * + * @return DatabaseQuery + * + * @since 4.0.0 + */ + protected function getEmptyStateQuery() + { + $query = parent::getEmptyStateQuery(); + + $title = 'ROOT'; + + $query->where($this->getDatabase()->quoteName('title') . ' <> :title') + ->bind(':title', $title); + + return $query; + } } diff --git a/administrator/components/com_finder/src/Model/SearchesModel.php b/administrator/components/com_finder/src/Model/SearchesModel.php index e4027989971bc..98f44f97a568e 100644 --- a/administrator/components/com_finder/src/Model/SearchesModel.php +++ b/administrator/components/com_finder/src/Model/SearchesModel.php @@ -1,4 +1,5 @@ setState('show_results', $this->getUserStateFromRequest($this->context . '.show_results', 'show_results', 1, 'int')); - - // Load the parameters. - $params = ComponentHelper::getParams('com_finder'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 4.0.0 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('show_results'); - $id .= ':' . $this->getState('filter.search'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 4.0.0 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.*' - ) - ); - $query->from($db->quoteName('#__finder_logging', 'a')); - - // Filter by search in title - if ($search = $this->getState('filter.search')) - { - $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); - $query->where($db->quoteName('a.searchterm') . ' LIKE ' . $search); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.hits')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Override the parent getItems to inject optional data. - * - * @return mixed An array of objects on success, false on failure. - * - * @since 4.0.0 - */ - public function getItems() - { - $items = parent::getItems(); - - foreach ($items as $item) - { - if (is_resource($item->query)) - { - $item->query = unserialize(stream_get_contents($item->query)); - } - else - { - $item->query = unserialize($item->query); - } - } - - return $items; - } - - /** - * Method to reset the search log table. - * - * @return boolean - * - * @since 4.0.0 - */ - public function reset() - { - $db = $this->getDatabase(); - - try - { - $db->truncateTable('#__finder_logging'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return true; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 4.0.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'searchterm', 'a.searchterm', + 'hits', 'a.hits', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 4.0.0 + */ + protected function populateState($ordering = 'a.hits', $direction = 'asc') + { + // Special state for toggle results button. + $this->setState('show_results', $this->getUserStateFromRequest($this->context . '.show_results', 'show_results', 1, 'int')); + + // Load the parameters. + $params = ComponentHelper::getParams('com_finder'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 4.0.0 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('show_results'); + $id .= ':' . $this->getState('filter.search'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 4.0.0 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.*' + ) + ); + $query->from($db->quoteName('#__finder_logging', 'a')); + + // Filter by search in title + if ($search = $this->getState('filter.search')) { + $search = $db->quote('%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%')); + $query->where($db->quoteName('a.searchterm') . ' LIKE ' . $search); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.hits')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Override the parent getItems to inject optional data. + * + * @return mixed An array of objects on success, false on failure. + * + * @since 4.0.0 + */ + public function getItems() + { + $items = parent::getItems(); + + foreach ($items as $item) { + if (is_resource($item->query)) { + $item->query = unserialize(stream_get_contents($item->query)); + } else { + $item->query = unserialize($item->query); + } + } + + return $items; + } + + /** + * Method to reset the search log table. + * + * @return boolean + * + * @since 4.0.0 + */ + public function reset() + { + $db = $this->getDatabase(); + + try { + $db->truncateTable('#__finder_logging'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return true; + } } diff --git a/administrator/components/com_finder/src/Model/StatisticsModel.php b/administrator/components/com_finder/src/Model/StatisticsModel.php index 7757ddd755d98..4e22ec3ad110c 100644 --- a/administrator/components/com_finder/src/Model/StatisticsModel.php +++ b/administrator/components/com_finder/src/Model/StatisticsModel.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true); - $data = new CMSObject; + /** + * Method to get the component statistics + * + * @return CMSObject The component statistics + * + * @since 2.5 + */ + public function getData() + { + // Initialise + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $data = new CMSObject(); - $query->select('COUNT(term_id)') - ->from($db->quoteName('#__finder_terms')); - $db->setQuery($query); - $data->term_count = $db->loadResult(); + $query->select('COUNT(term_id)') + ->from($db->quoteName('#__finder_terms')); + $db->setQuery($query); + $data->term_count = $db->loadResult(); - $query->clear() - ->select('COUNT(link_id)') - ->from($db->quoteName('#__finder_links')); - $db->setQuery($query); - $data->link_count = $db->loadResult(); + $query->clear() + ->select('COUNT(link_id)') + ->from($db->quoteName('#__finder_links')); + $db->setQuery($query); + $data->link_count = $db->loadResult(); - $query->clear() - ->select('COUNT(id)') - ->from($db->quoteName('#__finder_taxonomy')) - ->where($db->quoteName('parent_id') . ' = 1'); - $db->setQuery($query); - $data->taxonomy_branch_count = $db->loadResult(); + $query->clear() + ->select('COUNT(id)') + ->from($db->quoteName('#__finder_taxonomy')) + ->where($db->quoteName('parent_id') . ' = 1'); + $db->setQuery($query); + $data->taxonomy_branch_count = $db->loadResult(); - $query->clear() - ->select('COUNT(id)') - ->from($db->quoteName('#__finder_taxonomy')) - ->where($db->quoteName('parent_id') . ' > 1'); - $db->setQuery($query); - $data->taxonomy_node_count = $db->loadResult(); + $query->clear() + ->select('COUNT(id)') + ->from($db->quoteName('#__finder_taxonomy')) + ->where($db->quoteName('parent_id') . ' > 1'); + $db->setQuery($query); + $data->taxonomy_node_count = $db->loadResult(); - $query->clear() - ->select('t.title AS type_title, COUNT(a.link_id) AS link_count') - ->from($db->quoteName('#__finder_links') . ' AS a') - ->join('INNER', $db->quoteName('#__finder_types') . ' AS t ON t.id = a.type_id') - ->group('a.type_id, t.title') - ->order($db->quoteName('type_title') . ' ASC'); - $db->setQuery($query); - $data->type_list = $db->loadObjectList(); + $query->clear() + ->select('t.title AS type_title, COUNT(a.link_id) AS link_count') + ->from($db->quoteName('#__finder_links') . ' AS a') + ->join('INNER', $db->quoteName('#__finder_types') . ' AS t ON t.id = a.type_id') + ->group('a.type_id, t.title') + ->order($db->quoteName('type_title') . ' ASC'); + $db->setQuery($query); + $data->type_list = $db->loadObjectList(); - $lang = Factory::getLanguage(); - $plugins = PluginHelper::getPlugin('finder'); + $lang = Factory::getLanguage(); + $plugins = PluginHelper::getPlugin('finder'); - foreach ($plugins as $plugin) - { - $lang->load('plg_finder_' . $plugin->name . '.sys', JPATH_ADMINISTRATOR) - || $lang->load('plg_finder_' . $plugin->name . '.sys', JPATH_PLUGINS . '/finder/' . $plugin->name); - } + foreach ($plugins as $plugin) { + $lang->load('plg_finder_' . $plugin->name . '.sys', JPATH_ADMINISTRATOR) + || $lang->load('plg_finder_' . $plugin->name . '.sys', JPATH_PLUGINS . '/finder/' . $plugin->name); + } - return $data; - } + return $data; + } } diff --git a/administrator/components/com_finder/src/Response/Response.php b/administrator/components/com_finder/src/Response/Response.php index d45a7f40f7710..745319ff4fead 100644 --- a/administrator/components/com_finder/src/Response/Response.php +++ b/administrator/components/com_finder/src/Response/Response.php @@ -1,4 +1,5 @@ get('enable_logging', '0')) - { - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'indexer.php'; - Log::addLogger($options); - } + if ($params->get('enable_logging', '0')) { + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'indexer.php'; + Log::addLogger($options); + } - // Check if we are dealing with an error. - if ($state instanceof \Exception) - { - // Log the error - try - { - Log::add($state->getMessage(), Log::ERROR); - } - catch (\RuntimeException $exception) - { - // Informational log only - } + // Check if we are dealing with an error. + if ($state instanceof \Exception) { + // Log the error + try { + Log::add($state->getMessage(), Log::ERROR); + } catch (\RuntimeException $exception) { + // Informational log only + } - // Prepare the error response. - $this->error = true; - $this->header = Text::_('COM_FINDER_INDEXER_HEADER_ERROR'); - $this->message = $state->getMessage(); - } - else - { - // Prepare the response data. - $this->batchSize = (int) $state->batchSize; - $this->batchOffset = (int) $state->batchOffset; - $this->totalItems = (int) $state->totalItems; - $this->pluginState = $state->pluginState; + // Prepare the error response. + $this->error = true; + $this->header = Text::_('COM_FINDER_INDEXER_HEADER_ERROR'); + $this->message = $state->getMessage(); + } else { + // Prepare the response data. + $this->batchSize = (int) $state->batchSize; + $this->batchOffset = (int) $state->batchOffset; + $this->totalItems = (int) $state->totalItems; + $this->pluginState = $state->pluginState; - $this->startTime = $state->startTime; - $this->endTime = Factory::getDate()->toSql(); + $this->startTime = $state->startTime; + $this->endTime = Factory::getDate()->toSql(); - $this->start = !empty($state->start) ? (int) $state->start : 0; - $this->complete = !empty($state->complete) ? (int) $state->complete : 0; + $this->start = !empty($state->start) ? (int) $state->start : 0; + $this->complete = !empty($state->complete) ? (int) $state->complete : 0; - // Set the appropriate messages. - if ($this->totalItems <= 0 && $this->complete) - { - $this->header = Text::_('COM_FINDER_INDEXER_HEADER_COMPLETE'); - $this->message = Text::_('COM_FINDER_INDEXER_MESSAGE_COMPLETE'); - } - elseif ($this->totalItems <= 0) - { - $this->header = Text::_('COM_FINDER_INDEXER_HEADER_OPTIMIZE'); - $this->message = Text::_('COM_FINDER_INDEXER_MESSAGE_OPTIMIZE'); - } - else - { - $this->header = Text::_('COM_FINDER_INDEXER_HEADER_RUNNING'); - $this->message = Text::_('COM_FINDER_INDEXER_MESSAGE_RUNNING'); - } - } - } + // Set the appropriate messages. + if ($this->totalItems <= 0 && $this->complete) { + $this->header = Text::_('COM_FINDER_INDEXER_HEADER_COMPLETE'); + $this->message = Text::_('COM_FINDER_INDEXER_MESSAGE_COMPLETE'); + } elseif ($this->totalItems <= 0) { + $this->header = Text::_('COM_FINDER_INDEXER_HEADER_OPTIMIZE'); + $this->message = Text::_('COM_FINDER_INDEXER_MESSAGE_OPTIMIZE'); + } else { + $this->header = Text::_('COM_FINDER_INDEXER_HEADER_RUNNING'); + $this->message = Text::_('COM_FINDER_INDEXER_MESSAGE_RUNNING'); + } + } + } } diff --git a/administrator/components/com_finder/src/Service/HTML/Filter.php b/administrator/components/com_finder/src/Service/HTML/Filter.php index f81a1610a0e96..695bc361c418f 100644 --- a/administrator/components/com_finder/src/Service/HTML/Filter.php +++ b/administrator/components/com_finder/src/Service/HTML/Filter.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - $groups = implode(',', $user->getAuthorisedViewLevels()); - $html = ''; - $filter = null; - - // Get the configuration options. - $filterId = $options['filter_id'] ?? null; - $activeNodes = array_key_exists('selected_nodes', $options) ? $options['selected_nodes'] : array(); - $classSuffix = array_key_exists('class_suffix', $options) ? $options['class_suffix'] : ''; - - // Load the predefined filter if specified. - if (!empty($filterId)) - { - $query->select('f.data, f.params') - ->from($db->quoteName('#__finder_filters') . ' AS f') - ->where('f.filter_id = ' . (int) $filterId); - - // Load the filter data. - $db->setQuery($query); - - try - { - $filter = $db->loadObject(); - } - catch (\RuntimeException $e) - { - return null; - } - - // Initialize the filter parameters. - if ($filter) - { - $filter->params = new Registry($filter->params); - } - } - - // Build the query to get the branch data and the number of child nodes. - $query->clear() - ->select('t.*, count(c.id) AS children') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t') - ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS c ON c.parent_id = t.id') - ->where('t.parent_id = 1') - ->where('t.state = 1') - ->where('t.access IN (' . $groups . ')') - ->group('t.id, t.parent_id, t.state, t.access, t.title, c.parent_id') - ->order('t.lft, t.title'); - - // Limit the branch children to a predefined filter. - if ($filter) - { - $query->where('c.id IN(' . $filter->data . ')'); - } - - // Load the branches. - $db->setQuery($query); - - try - { - $branches = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - return null; - } - - // Check that we have at least one branch. - if (count($branches) === 0) - { - return null; - } - - $branch_keys = array_keys($branches); - $html .= HTMLHelper::_('bootstrap.startAccordion', 'accordion', array('active' => 'accordion-' . $branch_keys[0]) - ); - - // Load plugin language files. - LanguageHelper::loadPluginLanguage(); - - // Iterate through the branches and build the branch groups. - foreach ($branches as $bk => $bv) - { - // If the multi-lang plugin is enabled then drop the language branch. - if ($bv->title === 'Language' && Multilanguage::isEnabled()) - { - continue; - } - - // Build the query to get the child nodes for this branch. - $query->clear() - ->select('t.*') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t') - ->where('t.lft > ' . (int) $bv->lft) - ->where('t.rgt < ' . (int) $bv->rgt) - ->where('t.state = 1') - ->where('t.access IN (' . $groups . ')') - ->order('t.lft, t.title'); - - // Self-join to get the parent title. - $query->select('e.title AS parent_title') - ->join('LEFT', $db->quoteName('#__finder_taxonomy', 'e') . ' ON ' . $db->quoteName('e.id') . ' = ' . $db->quoteName('t.parent_id')); - - // Load the branches. - $db->setQuery($query); - - try - { - $nodes = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - return null; - } - - // Translate node titles if possible. - $lang = Factory::getLanguage(); - - foreach ($nodes as $nk => $nv) - { - if (trim($nv->parent_title, '*') === 'Language') - { - $title = LanguageHelper::branchLanguageTitle($nv->title); - } - else - { - $key = LanguageHelper::branchPlural($nv->title); - $title = $lang->hasKey($key) ? Text::_($key) : $nv->title; - } - - $nodes[$nk]->title = $title; - } - - // Adding slides - $html .= HTMLHelper::_('bootstrap.addSlide', - 'accordion', - Text::sprintf('COM_FINDER_FILTER_BRANCH_LABEL', - Text::_(LanguageHelper::branchSingular($bv->title)) . ' - ' . count($nodes) - ), - 'accordion-' . $bk - ); - - // Populate the toggle button. - $html .= '
'; - - // Populate the group with nodes. - foreach ($nodes as $nk => $nv) - { - // Determine if the node should be checked. - $checked = in_array($nk, $activeNodes) ? ' checked="checked"' : ''; - - // Build a node. - $html .= '
'; - $html .= ''; - $html .= '
'; - } - - $html .= HTMLHelper::_('bootstrap.endSlide'); - } - - $html .= HTMLHelper::_('bootstrap.endAccordion'); - - return $html; - } - - /** - * Method to generate filters using select box dropdown controls. - * - * @param Query $idxQuery A Query object. - * @param array $options An array of options. - * - * @return mixed A rendered HTML widget on success, null otherwise. - * - * @since 2.5 - */ - public function select($idxQuery, $options) - { - $user = Factory::getUser(); - $groups = implode(',', $user->getAuthorisedViewLevels()); - $filter = null; - - // Get the configuration options. - $classSuffix = $options->get('class_suffix', null); - $showDates = $options->get('show_date_filters', false); - - // Try to load the results from cache. - $cache = Factory::getCache('com_finder', ''); - $cacheId = 'filter_select_' . serialize(array($idxQuery->filter, $options, $groups, Factory::getLanguage()->getTag())); - - // Check the cached results. - if ($cache->contains($cacheId)) - { - $branches = $cache->get($cacheId); - } - else - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Load the predefined filter if specified. - if (!empty($idxQuery->filter)) - { - $query->select('f.data, ' . $db->quoteName('f.params')) - ->from($db->quoteName('#__finder_filters') . ' AS f') - ->where('f.filter_id = ' . (int) $idxQuery->filter); - - // Load the filter data. - $db->setQuery($query); - - try - { - $filter = $db->loadObject(); - } - catch (\RuntimeException $e) - { - return null; - } - - // Initialize the filter parameters. - if ($filter) - { - $filter->params = new Registry($filter->params); - } - } - - // Build the query to get the branch data and the number of child nodes. - $query->clear() - ->select('t.*, count(c.id) AS children') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t') - ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS c ON c.parent_id = t.id') - ->where('t.parent_id = 1') - ->where('t.state = 1') - ->where('t.access IN (' . $groups . ')') - ->where('c.state = 1') - ->where('c.access IN (' . $groups . ')') - ->group($db->quoteName('t.id')) - ->group($db->quoteName('t.parent_id')) - ->group('t.title, t.state, t.access, t.lft') - ->order('t.lft, t.title'); - - // Limit the branch children to a predefined filter. - if (!empty($filter->data)) - { - $query->where('c.id IN(' . $filter->data . ')'); - } - - // Load the branches. - $db->setQuery($query); - - try - { - $branches = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - return null; - } - - // Check that we have at least one branch. - if (count($branches) === 0) - { - return null; - } - - // Iterate through the branches and build the branch groups. - foreach ($branches as $bk => $bv) - { - // If the multi-lang plugin is enabled then drop the language branch. - if ($bv->title === 'Language' && Multilanguage::isEnabled()) - { - continue; - } - - // Build the query to get the child nodes for this branch. - $query->clear() - ->select('t.*') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t') - ->where('t.lft > ' . (int) $bv->lft) - ->where('t.rgt < ' . (int) $bv->rgt) - ->where('t.state = 1') - ->where('t.access IN (' . $groups . ')') - ->order('t.title'); - - // Self-join to get the parent title. - $query->select('e.title AS parent_title') - ->join('LEFT', $db->quoteName('#__finder_taxonomy', 'e') . ' ON ' . $db->quoteName('e.id') . ' = ' . $db->quoteName('t.parent_id')); - - // Limit the nodes to a predefined filter. - if (!empty($filter->data)) - { - $query->where('t.id IN(' . $filter->data . ')'); - } - - // Load the branches. - $db->setQuery($query); - - try - { - $branches[$bk]->nodes = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - return null; - } - - // Translate branch nodes if possible. - $language = Factory::getLanguage(); - - foreach ($branches[$bk]->nodes as $node_id => $node) - { - if (trim($node->parent_title, '*') === 'Language') - { - $title = LanguageHelper::branchLanguageTitle($node->title); - } - else - { - $key = LanguageHelper::branchPlural($node->title); - $title = $language->hasKey($key) ? Text::_($key) : $node->title; - } - - if ($node->level > 2) - { - $branches[$bk]->nodes[$node_id]->title = str_repeat('-', $node->level - 2) . $title; - } - else - { - $branches[$bk]->nodes[$node_id]->title = $title; - } - } - - // Add the Search All option to the branch. - array_unshift($branches[$bk]->nodes, array('id' => null, 'title' => Text::_('COM_FINDER_FILTER_SELECT_ALL_LABEL'))); - } - - // Store the data in cache. - $cache->store($branches, $cacheId); - } - - $html = ''; - - // Add the dates if enabled. - if ($showDates) - { - $html .= HTMLHelper::_('filter.dates', $idxQuery, $options); - } - - $html .= '
'; - - // Iterate through all branches and build code. - foreach ($branches as $bk => $bv) - { - // If the multi-lang plugin is enabled then drop the language branch. - if ($bv->title === 'Language' && Multilanguage::isEnabled()) - { - continue; - } - - $active = null; - - // Check if the branch is in the filter. - if (array_key_exists($bv->title, $idxQuery->filters)) - { - // Get the request filters. - $temp = Factory::getApplication()->input->request->get('t', array(), 'array'); - - // Search for active nodes in the branch and get the active node. - $active = array_intersect($temp, $idxQuery->filters[$bv->title]); - $active = count($active) === 1 ? array_shift($active) : null; - } - - // Build a node. - $html .= '
'; - $html .= '
'; - $html .= ''; - $html .= '
'; - $html .= '
'; - $html .= HTMLHelper::_( - 'select.genericlist', - $branches[$bk]->nodes, 't[]', 'class="form-select advancedSelect"', 'id', 'title', $active, - 'tax-' . OutputFilter::stringURLSafe($bv->title) - ); - $html .= '
'; - $html .= '
'; - } - - $html .= '
'; - - return $html; - } - - /** - * Method to generate fields for filtering dates - * - * @param Query $idxQuery A Query object. - * @param array $options An array of options. - * - * @return mixed A rendered HTML widget on success, null otherwise. - * - * @since 2.5 - */ - public function dates($idxQuery, $options) - { - $html = ''; - - // Get the configuration options. - $classSuffix = $options->get('class_suffix', null); - $loadMedia = $options->get('load_media', true); - $showDates = $options->get('show_date_filters', false); - - if (!empty($showDates)) - { - // Build the date operators options. - $operators = array(); - $operators[] = HTMLHelper::_('select.option', 'before', Text::_('COM_FINDER_FILTER_DATE_BEFORE')); - $operators[] = HTMLHelper::_('select.option', 'exact', Text::_('COM_FINDER_FILTER_DATE_EXACTLY')); - $operators[] = HTMLHelper::_('select.option', 'after', Text::_('COM_FINDER_FILTER_DATE_AFTER')); - - // Load the CSS/JS resources. - if ($loadMedia) - { - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); - $wa->useStyle('com_finder.dates'); - } - - // Open the widget. - $html .= '
    '; - - // Start date filter. - $attribs['class'] = 'input-medium'; - $html .= '
  • '; - $html .= ''; - $html .= '
    '; - $html .= HTMLHelper::_( - 'select.genericlist', - $operators, 'w1', 'class="inputbox filter-date-operator advancedSelect form-select w-auto mb-2"', 'value', 'text', $idxQuery->when1, 'finder-filter-w1' - ); - $html .= HTMLHelper::_('calendar', $idxQuery->date1, 'd1', 'filter_date1', '%Y-%m-%d', $attribs); - $html .= '
  • '; - - // End date filter. - $html .= '
  • '; - $html .= ''; - $html .= '
    '; - $html .= HTMLHelper::_( - 'select.genericlist', - $operators, 'w2', 'class="inputbox filter-date-operator advancedSelect form-select w-auto mb-2"', 'value', 'text', $idxQuery->when2, 'finder-filter-w2' - ); - $html .= HTMLHelper::_('calendar', $idxQuery->date2, 'd2', 'filter_date2', '%Y-%m-%d', $attribs); - $html .= '
  • '; - - // Close the widget. - $html .= '
'; - } - - return $html; - } + use DatabaseAwareTrait; + + /** + * Method to generate filters using the slider widget and decorated + * with the FinderFilter JavaScript behaviors. + * + * @param array $options An array of configuration options. [optional] + * + * @return mixed A rendered HTML widget on success, null otherwise. + * + * @since 2.5 + */ + public function slider($options = array()) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + $groups = implode(',', $user->getAuthorisedViewLevels()); + $html = ''; + $filter = null; + + // Get the configuration options. + $filterId = $options['filter_id'] ?? null; + $activeNodes = array_key_exists('selected_nodes', $options) ? $options['selected_nodes'] : array(); + $classSuffix = array_key_exists('class_suffix', $options) ? $options['class_suffix'] : ''; + + // Load the predefined filter if specified. + if (!empty($filterId)) { + $query->select('f.data, f.params') + ->from($db->quoteName('#__finder_filters') . ' AS f') + ->where('f.filter_id = ' . (int) $filterId); + + // Load the filter data. + $db->setQuery($query); + + try { + $filter = $db->loadObject(); + } catch (\RuntimeException $e) { + return null; + } + + // Initialize the filter parameters. + if ($filter) { + $filter->params = new Registry($filter->params); + } + } + + // Build the query to get the branch data and the number of child nodes. + $query->clear() + ->select('t.*, count(c.id) AS children') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t') + ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS c ON c.parent_id = t.id') + ->where('t.parent_id = 1') + ->where('t.state = 1') + ->where('t.access IN (' . $groups . ')') + ->group('t.id, t.parent_id, t.state, t.access, t.title, c.parent_id') + ->order('t.lft, t.title'); + + // Limit the branch children to a predefined filter. + if ($filter) { + $query->where('c.id IN(' . $filter->data . ')'); + } + + // Load the branches. + $db->setQuery($query); + + try { + $branches = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + return null; + } + + // Check that we have at least one branch. + if (count($branches) === 0) { + return null; + } + + $branch_keys = array_keys($branches); + $html .= HTMLHelper::_('bootstrap.startAccordion', 'accordion', array('active' => 'accordion-' . $branch_keys[0])); + + // Load plugin language files. + LanguageHelper::loadPluginLanguage(); + + // Iterate through the branches and build the branch groups. + foreach ($branches as $bk => $bv) { + // If the multi-lang plugin is enabled then drop the language branch. + if ($bv->title === 'Language' && Multilanguage::isEnabled()) { + continue; + } + + // Build the query to get the child nodes for this branch. + $query->clear() + ->select('t.*') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t') + ->where('t.lft > ' . (int) $bv->lft) + ->where('t.rgt < ' . (int) $bv->rgt) + ->where('t.state = 1') + ->where('t.access IN (' . $groups . ')') + ->order('t.lft, t.title'); + + // Self-join to get the parent title. + $query->select('e.title AS parent_title') + ->join('LEFT', $db->quoteName('#__finder_taxonomy', 'e') . ' ON ' . $db->quoteName('e.id') . ' = ' . $db->quoteName('t.parent_id')); + + // Load the branches. + $db->setQuery($query); + + try { + $nodes = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + return null; + } + + // Translate node titles if possible. + $lang = Factory::getLanguage(); + + foreach ($nodes as $nk => $nv) { + if (trim($nv->parent_title, '*') === 'Language') { + $title = LanguageHelper::branchLanguageTitle($nv->title); + } else { + $key = LanguageHelper::branchPlural($nv->title); + $title = $lang->hasKey($key) ? Text::_($key) : $nv->title; + } + + $nodes[$nk]->title = $title; + } + + // Adding slides + $html .= HTMLHelper::_( + 'bootstrap.addSlide', + 'accordion', + Text::sprintf( + 'COM_FINDER_FILTER_BRANCH_LABEL', + Text::_(LanguageHelper::branchSingular($bv->title)) . ' - ' . count($nodes) + ), + 'accordion-' . $bk + ); + + // Populate the toggle button. + $html .= '
'; + + // Populate the group with nodes. + foreach ($nodes as $nk => $nv) { + // Determine if the node should be checked. + $checked = in_array($nk, $activeNodes) ? ' checked="checked"' : ''; + + // Build a node. + $html .= '
'; + $html .= ''; + $html .= '
'; + } + + $html .= HTMLHelper::_('bootstrap.endSlide'); + } + + $html .= HTMLHelper::_('bootstrap.endAccordion'); + + return $html; + } + + /** + * Method to generate filters using select box dropdown controls. + * + * @param Query $idxQuery A Query object. + * @param array $options An array of options. + * + * @return mixed A rendered HTML widget on success, null otherwise. + * + * @since 2.5 + */ + public function select($idxQuery, $options) + { + $user = Factory::getUser(); + $groups = implode(',', $user->getAuthorisedViewLevels()); + $filter = null; + + // Get the configuration options. + $classSuffix = $options->get('class_suffix', null); + $showDates = $options->get('show_date_filters', false); + + // Try to load the results from cache. + $cache = Factory::getCache('com_finder', ''); + $cacheId = 'filter_select_' . serialize(array($idxQuery->filter, $options, $groups, Factory::getLanguage()->getTag())); + + // Check the cached results. + if ($cache->contains($cacheId)) { + $branches = $cache->get($cacheId); + } else { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Load the predefined filter if specified. + if (!empty($idxQuery->filter)) { + $query->select('f.data, ' . $db->quoteName('f.params')) + ->from($db->quoteName('#__finder_filters') . ' AS f') + ->where('f.filter_id = ' . (int) $idxQuery->filter); + + // Load the filter data. + $db->setQuery($query); + + try { + $filter = $db->loadObject(); + } catch (\RuntimeException $e) { + return null; + } + + // Initialize the filter parameters. + if ($filter) { + $filter->params = new Registry($filter->params); + } + } + + // Build the query to get the branch data and the number of child nodes. + $query->clear() + ->select('t.*, count(c.id) AS children') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t') + ->join('INNER', $db->quoteName('#__finder_taxonomy') . ' AS c ON c.parent_id = t.id') + ->where('t.parent_id = 1') + ->where('t.state = 1') + ->where('t.access IN (' . $groups . ')') + ->where('c.state = 1') + ->where('c.access IN (' . $groups . ')') + ->group($db->quoteName('t.id')) + ->group($db->quoteName('t.parent_id')) + ->group('t.title, t.state, t.access, t.lft') + ->order('t.lft, t.title'); + + // Limit the branch children to a predefined filter. + if (!empty($filter->data)) { + $query->where('c.id IN(' . $filter->data . ')'); + } + + // Load the branches. + $db->setQuery($query); + + try { + $branches = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + return null; + } + + // Check that we have at least one branch. + if (count($branches) === 0) { + return null; + } + + // Iterate through the branches and build the branch groups. + foreach ($branches as $bk => $bv) { + // If the multi-lang plugin is enabled then drop the language branch. + if ($bv->title === 'Language' && Multilanguage::isEnabled()) { + continue; + } + + // Build the query to get the child nodes for this branch. + $query->clear() + ->select('t.*') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t') + ->where('t.lft > ' . (int) $bv->lft) + ->where('t.rgt < ' . (int) $bv->rgt) + ->where('t.state = 1') + ->where('t.access IN (' . $groups . ')') + ->order('t.title'); + + // Self-join to get the parent title. + $query->select('e.title AS parent_title') + ->join('LEFT', $db->quoteName('#__finder_taxonomy', 'e') . ' ON ' . $db->quoteName('e.id') . ' = ' . $db->quoteName('t.parent_id')); + + // Limit the nodes to a predefined filter. + if (!empty($filter->data)) { + $query->where('t.id IN(' . $filter->data . ')'); + } + + // Load the branches. + $db->setQuery($query); + + try { + $branches[$bk]->nodes = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + return null; + } + + // Translate branch nodes if possible. + $language = Factory::getLanguage(); + + foreach ($branches[$bk]->nodes as $node_id => $node) { + if (trim($node->parent_title, '*') === 'Language') { + $title = LanguageHelper::branchLanguageTitle($node->title); + } else { + $key = LanguageHelper::branchPlural($node->title); + $title = $language->hasKey($key) ? Text::_($key) : $node->title; + } + + if ($node->level > 2) { + $branches[$bk]->nodes[$node_id]->title = str_repeat('-', $node->level - 2) . $title; + } else { + $branches[$bk]->nodes[$node_id]->title = $title; + } + } + + // Add the Search All option to the branch. + array_unshift($branches[$bk]->nodes, array('id' => null, 'title' => Text::_('COM_FINDER_FILTER_SELECT_ALL_LABEL'))); + } + + // Store the data in cache. + $cache->store($branches, $cacheId); + } + + $html = ''; + + // Add the dates if enabled. + if ($showDates) { + $html .= HTMLHelper::_('filter.dates', $idxQuery, $options); + } + + $html .= '
'; + + // Iterate through all branches and build code. + foreach ($branches as $bk => $bv) { + // If the multi-lang plugin is enabled then drop the language branch. + if ($bv->title === 'Language' && Multilanguage::isEnabled()) { + continue; + } + + $active = null; + + // Check if the branch is in the filter. + if (array_key_exists($bv->title, $idxQuery->filters)) { + // Get the request filters. + $temp = Factory::getApplication()->input->request->get('t', array(), 'array'); + + // Search for active nodes in the branch and get the active node. + $active = array_intersect($temp, $idxQuery->filters[$bv->title]); + $active = count($active) === 1 ? array_shift($active) : null; + } + + // Build a node. + $html .= '
'; + $html .= '
'; + $html .= ''; + $html .= '
'; + $html .= '
'; + $html .= HTMLHelper::_( + 'select.genericlist', + $branches[$bk]->nodes, + 't[]', + 'class="form-select advancedSelect"', + 'id', + 'title', + $active, + 'tax-' . OutputFilter::stringURLSafe($bv->title) + ); + $html .= '
'; + $html .= '
'; + } + + $html .= '
'; + + return $html; + } + + /** + * Method to generate fields for filtering dates + * + * @param Query $idxQuery A Query object. + * @param array $options An array of options. + * + * @return mixed A rendered HTML widget on success, null otherwise. + * + * @since 2.5 + */ + public function dates($idxQuery, $options) + { + $html = ''; + + // Get the configuration options. + $classSuffix = $options->get('class_suffix', null); + $loadMedia = $options->get('load_media', true); + $showDates = $options->get('show_date_filters', false); + + if (!empty($showDates)) { + // Build the date operators options. + $operators = array(); + $operators[] = HTMLHelper::_('select.option', 'before', Text::_('COM_FINDER_FILTER_DATE_BEFORE')); + $operators[] = HTMLHelper::_('select.option', 'exact', Text::_('COM_FINDER_FILTER_DATE_EXACTLY')); + $operators[] = HTMLHelper::_('select.option', 'after', Text::_('COM_FINDER_FILTER_DATE_AFTER')); + + // Load the CSS/JS resources. + if ($loadMedia) { + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + $wa->useStyle('com_finder.dates'); + } + + // Open the widget. + $html .= '
    '; + + // Start date filter. + $attribs['class'] = 'input-medium'; + $html .= '
  • '; + $html .= ''; + $html .= '
    '; + $html .= HTMLHelper::_( + 'select.genericlist', + $operators, + 'w1', + 'class="inputbox filter-date-operator advancedSelect form-select w-auto mb-2"', + 'value', + 'text', + $idxQuery->when1, + 'finder-filter-w1' + ); + $html .= HTMLHelper::_('calendar', $idxQuery->date1, 'd1', 'filter_date1', '%Y-%m-%d', $attribs); + $html .= '
  • '; + + // End date filter. + $html .= '
  • '; + $html .= ''; + $html .= '
    '; + $html .= HTMLHelper::_( + 'select.genericlist', + $operators, + 'w2', + 'class="inputbox filter-date-operator advancedSelect form-select w-auto mb-2"', + 'value', + 'text', + $idxQuery->when2, + 'finder-filter-w2' + ); + $html .= HTMLHelper::_('calendar', $idxQuery->date2, 'd2', 'filter_date2', '%Y-%m-%d', $attribs); + $html .= '
  • '; + + // Close the widget. + $html .= '
'; + } + + return $html; + } } diff --git a/administrator/components/com_finder/src/Service/HTML/Finder.php b/administrator/components/com_finder/src/Service/HTML/Finder.php index 0968df797eb34..9924a7dd1a104 100644 --- a/administrator/components/com_finder/src/Service/HTML/Finder.php +++ b/administrator/components/com_finder/src/Service/HTML/Finder.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select('DISTINCT t.title AS text, t.id AS value') - ->from($db->quoteName('#__finder_types') . ' AS t') - ->join('LEFT', $db->quoteName('#__finder_links') . ' AS l ON l.type_id = t.id') - ->order('t.title ASC'); - $db->setQuery($query); - - try - { - $rows = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - return array(); - } - - // Compile the options. - $options = array(); - - $lang = Factory::getLanguage(); - - foreach ($rows as $row) - { - $key = $lang->hasKey(LanguageHelper::branchPlural($row->text)) ? LanguageHelper::branchPlural($row->text) : $row->text; - $options[] = HTMLHelper::_('select.option', $row->value, Text::sprintf('COM_FINDER_ITEM_X_ONLY', Text::_($key))); - } - - return $options; - } - - /** - * Creates a list of maps. - * - * @return array An array containing the maps that can be selected. - * - * @since 2.5 - */ - public function mapslist() - { - // Load the finder types. - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('title', 'text')) - ->select($db->quoteName('id', 'value')) - ->from($db->quoteName('#__finder_taxonomy')) - ->where($db->quoteName('parent_id') . ' = 1'); - $db->setQuery($query); - - try - { - $branches = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - // Translate. - $lang = Factory::getLanguage(); - - foreach ($branches as $branch) - { - $key = LanguageHelper::branchPlural($branch->text); - $branch->translatedText = $lang->hasKey($key) ? Text::_($key) : $branch->text; - } - - // Order by title. - $branches = ArrayHelper::sortObjects($branches, 'translatedText', 1, true, true); - - // Compile the options. - $options = array(); - $options[] = HTMLHelper::_('select.option', '', Text::_('COM_FINDER_MAPS_SELECT_BRANCH')); - - // Convert the values to options. - foreach ($branches as $branch) - { - $options[] = HTMLHelper::_('select.option', $branch->value, $branch->translatedText); - } - - return $options; - } - - /** - * Creates a list of published states. - * - * @return array An array containing the states that can be selected. - * - * @since 2.5 - */ - public static function statelist() - { - return array( - HTMLHelper::_('select.option', '1', Text::sprintf('COM_FINDER_ITEM_X_ONLY', Text::_('JPUBLISHED'))), - HTMLHelper::_('select.option', '0', Text::sprintf('COM_FINDER_ITEM_X_ONLY', Text::_('JUNPUBLISHED'))) - ); - } + use DatabaseAwareTrait; + + /** + * Creates a list of types to filter on. + * + * @return array An array containing the types that can be selected. + * + * @since 2.5 + */ + public function typeslist() + { + // Load the finder types. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('DISTINCT t.title AS text, t.id AS value') + ->from($db->quoteName('#__finder_types') . ' AS t') + ->join('LEFT', $db->quoteName('#__finder_links') . ' AS l ON l.type_id = t.id') + ->order('t.title ASC'); + $db->setQuery($query); + + try { + $rows = $db->loadObjectList(); + } catch (\RuntimeException $e) { + return array(); + } + + // Compile the options. + $options = array(); + + $lang = Factory::getLanguage(); + + foreach ($rows as $row) { + $key = $lang->hasKey(LanguageHelper::branchPlural($row->text)) ? LanguageHelper::branchPlural($row->text) : $row->text; + $options[] = HTMLHelper::_('select.option', $row->value, Text::sprintf('COM_FINDER_ITEM_X_ONLY', Text::_($key))); + } + + return $options; + } + + /** + * Creates a list of maps. + * + * @return array An array containing the maps that can be selected. + * + * @since 2.5 + */ + public function mapslist() + { + // Load the finder types. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title', 'text')) + ->select($db->quoteName('id', 'value')) + ->from($db->quoteName('#__finder_taxonomy')) + ->where($db->quoteName('parent_id') . ' = 1'); + $db->setQuery($query); + + try { + $branches = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + // Translate. + $lang = Factory::getLanguage(); + + foreach ($branches as $branch) { + $key = LanguageHelper::branchPlural($branch->text); + $branch->translatedText = $lang->hasKey($key) ? Text::_($key) : $branch->text; + } + + // Order by title. + $branches = ArrayHelper::sortObjects($branches, 'translatedText', 1, true, true); + + // Compile the options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '', Text::_('COM_FINDER_MAPS_SELECT_BRANCH')); + + // Convert the values to options. + foreach ($branches as $branch) { + $options[] = HTMLHelper::_('select.option', $branch->value, $branch->translatedText); + } + + return $options; + } + + /** + * Creates a list of published states. + * + * @return array An array containing the states that can be selected. + * + * @since 2.5 + */ + public static function statelist() + { + return array( + HTMLHelper::_('select.option', '1', Text::sprintf('COM_FINDER_ITEM_X_ONLY', Text::_('JPUBLISHED'))), + HTMLHelper::_('select.option', '0', Text::sprintf('COM_FINDER_ITEM_X_ONLY', Text::_('JUNPUBLISHED'))) + ); + } } diff --git a/administrator/components/com_finder/src/Service/HTML/Query.php b/administrator/components/com_finder/src/Service/HTML/Query.php index 47540a83035c3..097786b216cde 100644 --- a/administrator/components/com_finder/src/Service/HTML/Query.php +++ b/administrator/components/com_finder/src/Service/HTML/Query.php @@ -1,4 +1,5 @@ included as $token) - { - if ($token->required && (!isset($token->derived) || $token->derived == false)) - { - $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_TOKEN_REQUIRED', $token->term) . ''; - } - } - - // Process the optional tokens. - foreach ($query->included as $token) - { - if (!$token->required && (!isset($token->derived) || $token->derived == false)) - { - $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_TOKEN_OPTIONAL', $token->term) . ''; - } - } - - // Process the excluded tokens. - foreach ($query->excluded as $token) - { - if (!isset($token->derived) || $token->derived === false) - { - $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_TOKEN_EXCLUDED', $token->term) . ''; - } - } - - // Process the start date. - if ($query->date1) - { - $date = Factory::getDate($query->date1)->format(Text::_('DATE_FORMAT_LC')); - $datecondition = Text::_('COM_FINDER_QUERY_DATE_CONDITION_' . strtoupper($query->when1)); - $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_START_DATE', $datecondition, $date) . ''; - } - - // Process the end date. - if ($query->date2) - { - $date = Factory::getDate($query->date2)->format(Text::_('DATE_FORMAT_LC')); - $datecondition = Text::_('COM_FINDER_QUERY_DATE_CONDITION_' . strtoupper($query->when2)); - $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_END_DATE', $datecondition, $date) . ''; - } - - // Process the taxonomy filters. - if (!empty($query->filters)) - { - // Get the filters in the request. - $t = Factory::getApplication()->input->request->get('t', array(), 'array'); - - // Process the taxonomy branches. - foreach ($query->filters as $branch => $nodes) - { - // Process the taxonomy nodes. - $lang = Factory::getLanguage(); - - foreach ($nodes as $title => $id) - { - // Translate the title for Types - $key = LanguageHelper::branchPlural($title); - - if ($lang->hasKey($key)) - { - $title = Text::_($key); - } - - // Don't include the node if it is not in the request. - if (!in_array($id, $t)) - { - continue; - } - - // Add the node to the explanation. - $parts[] = '' - . Text::sprintf('COM_FINDER_QUERY_TAXONOMY_NODE', $title, Text::_(LanguageHelper::branchSingular($branch))) - . ''; - } - } - } - - // Build the interpreted query. - return count($parts) ? implode(Text::_('COM_FINDER_QUERY_TOKEN_GLUE'), $parts) : null; - } - - /** - * Method to get the suggested search query. - * - * @param IndexerQuery $query A IndexerQuery object. - * - * @return mixed String if there is a suggestion, false otherwise. - * - * @since 2.5 - */ - public static function suggested(IndexerQuery $query) - { - $suggested = false; - - // Check if the query input is empty. - if (empty($query->input)) - { - return $suggested; - } - - // Check if there were any ignored or included keywords. - if (count($query->ignored) || count($query->included)) - { - $suggested = $query->input; - - // Replace the ignored keyword suggestions. - foreach (array_reverse($query->ignored) as $token) - { - if (isset($token->suggestion)) - { - $suggested = str_ireplace($token->term, $token->suggestion, $suggested); - } - } - - // Replace the included keyword suggestions. - foreach (array_reverse($query->included) as $token) - { - if (isset($token->suggestion)) - { - $suggested = str_ireplace($token->term, $token->suggestion, $suggested); - } - } - - // Check if we made any changes. - if ($suggested == $query->input) - { - $suggested = false; - } - } - - return $suggested; - } + /** + * Method to get the explained (human-readable) search query. + * + * @param IndexerQuery $query A IndexerQuery object to explain. + * + * @return mixed String if there is data to explain, null otherwise. + * + * @since 2.5 + */ + public static function explained(IndexerQuery $query) + { + $parts = array(); + + // Process the required tokens. + foreach ($query->included as $token) { + if ($token->required && (!isset($token->derived) || $token->derived == false)) { + $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_TOKEN_REQUIRED', $token->term) . ''; + } + } + + // Process the optional tokens. + foreach ($query->included as $token) { + if (!$token->required && (!isset($token->derived) || $token->derived == false)) { + $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_TOKEN_OPTIONAL', $token->term) . ''; + } + } + + // Process the excluded tokens. + foreach ($query->excluded as $token) { + if (!isset($token->derived) || $token->derived === false) { + $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_TOKEN_EXCLUDED', $token->term) . ''; + } + } + + // Process the start date. + if ($query->date1) { + $date = Factory::getDate($query->date1)->format(Text::_('DATE_FORMAT_LC')); + $datecondition = Text::_('COM_FINDER_QUERY_DATE_CONDITION_' . strtoupper($query->when1)); + $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_START_DATE', $datecondition, $date) . ''; + } + + // Process the end date. + if ($query->date2) { + $date = Factory::getDate($query->date2)->format(Text::_('DATE_FORMAT_LC')); + $datecondition = Text::_('COM_FINDER_QUERY_DATE_CONDITION_' . strtoupper($query->when2)); + $parts[] = '' . Text::sprintf('COM_FINDER_QUERY_END_DATE', $datecondition, $date) . ''; + } + + // Process the taxonomy filters. + if (!empty($query->filters)) { + // Get the filters in the request. + $t = Factory::getApplication()->input->request->get('t', array(), 'array'); + + // Process the taxonomy branches. + foreach ($query->filters as $branch => $nodes) { + // Process the taxonomy nodes. + $lang = Factory::getLanguage(); + + foreach ($nodes as $title => $id) { + // Translate the title for Types + $key = LanguageHelper::branchPlural($title); + + if ($lang->hasKey($key)) { + $title = Text::_($key); + } + + // Don't include the node if it is not in the request. + if (!in_array($id, $t)) { + continue; + } + + // Add the node to the explanation. + $parts[] = '' + . Text::sprintf('COM_FINDER_QUERY_TAXONOMY_NODE', $title, Text::_(LanguageHelper::branchSingular($branch))) + . ''; + } + } + } + + // Build the interpreted query. + return count($parts) ? implode(Text::_('COM_FINDER_QUERY_TOKEN_GLUE'), $parts) : null; + } + + /** + * Method to get the suggested search query. + * + * @param IndexerQuery $query A IndexerQuery object. + * + * @return mixed String if there is a suggestion, false otherwise. + * + * @since 2.5 + */ + public static function suggested(IndexerQuery $query) + { + $suggested = false; + + // Check if the query input is empty. + if (empty($query->input)) { + return $suggested; + } + + // Check if there were any ignored or included keywords. + if (count($query->ignored) || count($query->included)) { + $suggested = $query->input; + + // Replace the ignored keyword suggestions. + foreach (array_reverse($query->ignored) as $token) { + if (isset($token->suggestion)) { + $suggested = str_ireplace($token->term, $token->suggestion, $suggested); + } + } + + // Replace the included keyword suggestions. + foreach (array_reverse($query->included) as $token) { + if (isset($token->suggestion)) { + $suggested = str_ireplace($token->term, $token->suggestion, $suggested); + } + } + + // Check if we made any changes. + if ($suggested == $query->input) { + $suggested = false; + } + } + + return $suggested; + } } diff --git a/administrator/components/com_finder/src/Table/FilterTable.php b/administrator/components/com_finder/src/Table/FilterTable.php index 2e12e043829c5..222216d2c22ba 100644 --- a/administrator/components/com_finder/src/Table/FilterTable.php +++ b/administrator/components/com_finder/src/Table/FilterTable.php @@ -1,4 +1,5 @@ setColumnAlias('published', 'state'); - } - - /** - * Method to perform sanity checks on the \JTable instance properties to ensure - * they are safe to store in the database. Child classes should override this - * method to make sure the data they are storing in the database is safe and - * as expected before storage. - * - * @return boolean True if the instance is sane and able to be stored in the database. - * - * @since 2.5 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (trim($this->alias) === '') - { - $this->alias = $this->title; - } - - $this->alias = ApplicationHelper::stringURLSafe($this->alias); - - if (trim(str_replace('-', '', $this->alias)) === '') - { - $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); - } - - $params = new Registry($this->params); - - $d1 = $params->get('d1', ''); - $d2 = $params->get('d2', ''); - - // Check the end date is not earlier than the start date. - if (!empty($d1) && !empty($d2) && $d2 < $d1) - { - // Swap the dates. - $params->set('d1', $d2); - $params->set('d2', $d1); - $this->params = (string) $params; - } - - return true; - } - - /** - * Method to store a row in the database from the \JTable instance properties. - * If a primary key value is set the row with that primary key value will be - * updated with the instance property values. If no primary key value is set - * a new row will be inserted into the database with the properties from the - * \JTable instance. - * - * @param boolean $updateNulls True to update fields even if they are null. [optional] - * - * @return boolean True on success. - * - * @since 2.5 - */ - public function store($updateNulls = true) - { - $date = Factory::getDate()->toSql(); - $userId = Factory::getUser()->id; - - // Set created date if not set. - if (!(int) $this->created) - { - $this->created = $date; - } - - if ($this->filter_id) - { - // Existing item - $this->modified_by = $userId; - $this->modified = $date; - } - else - { - if (empty($this->created_by)) - { - $this->created_by = $userId; - } - - if (!(int) $this->modified) - { - $this->modified = $this->created; - } - - if (empty($this->modified_by)) - { - $this->modified_by = $this->created_by; - } - } - - if (is_array($this->data)) - { - $this->map_count = count($this->data); - $this->data = implode(',', $this->data); - } - else - { - $this->map_count = 0; - $this->data = implode(',', array()); - } - - // Verify that the alias is unique - $table = new static($this->getDbo()); - - if ($table->load(array('alias' => $this->alias)) && ($table->filter_id != $this->filter_id || $this->filter_id == 0)) - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_ARTICLE_UNIQUE_ALIAS')); - - return false; - } - - return parent::store($updateNulls); - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Ensure the params are json encoded in the bind method + * + * @var array + * @since 4.0.0 + */ + protected $_jsonEncode = array('params'); + + /** + * Constructor + * + * @param DatabaseDriver $db Database Driver connector object. + * + * @since 2.5 + */ + public function __construct(DatabaseDriver $db) + { + parent::__construct('#__finder_filters', 'filter_id', $db); + + $this->setColumnAlias('published', 'state'); + } + + /** + * Method to perform sanity checks on the \JTable instance properties to ensure + * they are safe to store in the database. Child classes should override this + * method to make sure the data they are storing in the database is safe and + * as expected before storage. + * + * @return boolean True if the instance is sane and able to be stored in the database. + * + * @since 2.5 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (trim($this->alias) === '') { + $this->alias = $this->title; + } + + $this->alias = ApplicationHelper::stringURLSafe($this->alias); + + if (trim(str_replace('-', '', $this->alias)) === '') { + $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); + } + + $params = new Registry($this->params); + + $d1 = $params->get('d1', ''); + $d2 = $params->get('d2', ''); + + // Check the end date is not earlier than the start date. + if (!empty($d1) && !empty($d2) && $d2 < $d1) { + // Swap the dates. + $params->set('d1', $d2); + $params->set('d2', $d1); + $this->params = (string) $params; + } + + return true; + } + + /** + * Method to store a row in the database from the \JTable instance properties. + * If a primary key value is set the row with that primary key value will be + * updated with the instance property values. If no primary key value is set + * a new row will be inserted into the database with the properties from the + * \JTable instance. + * + * @param boolean $updateNulls True to update fields even if they are null. [optional] + * + * @return boolean True on success. + * + * @since 2.5 + */ + public function store($updateNulls = true) + { + $date = Factory::getDate()->toSql(); + $userId = Factory::getUser()->id; + + // Set created date if not set. + if (!(int) $this->created) { + $this->created = $date; + } + + if ($this->filter_id) { + // Existing item + $this->modified_by = $userId; + $this->modified = $date; + } else { + if (empty($this->created_by)) { + $this->created_by = $userId; + } + + if (!(int) $this->modified) { + $this->modified = $this->created; + } + + if (empty($this->modified_by)) { + $this->modified_by = $this->created_by; + } + } + + if (is_array($this->data)) { + $this->map_count = count($this->data); + $this->data = implode(',', $this->data); + } else { + $this->map_count = 0; + $this->data = implode(',', array()); + } + + // Verify that the alias is unique + $table = new static($this->getDbo()); + + if ($table->load(array('alias' => $this->alias)) && ($table->filter_id != $this->filter_id || $this->filter_id == 0)) { + $this->setError(Text::_('JLIB_DATABASE_ERROR_ARTICLE_UNIQUE_ALIAS')); + + return false; + } + + return parent::store($updateNulls); + } } diff --git a/administrator/components/com_finder/src/Table/LinkTable.php b/administrator/components/com_finder/src/Table/LinkTable.php index 62f3c0e11fb21..913d9775862d7 100644 --- a/administrator/components/com_finder/src/Table/LinkTable.php +++ b/administrator/components/com_finder/src/Table/LinkTable.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Finder\Administrator\Table; use Joomla\CMS\Table\Table; use Joomla\Database\DatabaseDriver; -\defined('_JEXEC') or die; - /** * Link table class for the Finder package. * @@ -20,38 +20,38 @@ */ class LinkTable extends Table { - /** - * Indicates that columns fully support the NULL value in the database - * - * @var boolean - * @since 4.0.0 - */ - protected $_supportNullValue = true; + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; - /** - * Constructor - * - * @param DatabaseDriver $db Database Driver connector object. - * - * @since 2.5 - */ - public function __construct(DatabaseDriver $db) - { - parent::__construct('#__finder_links', 'link_id', $db); - } + /** + * Constructor + * + * @param DatabaseDriver $db Database Driver connector object. + * + * @since 2.5 + */ + public function __construct(DatabaseDriver $db) + { + parent::__construct('#__finder_links', 'link_id', $db); + } - /** - * Overloaded store function - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return mixed False on failure, positive integer on success. - * - * @see Table::store() - * @since 4.0.0 - */ - public function store($updateNulls = true) - { - return parent::store($updateNulls); - } + /** + * Overloaded store function + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return mixed False on failure, positive integer on success. + * + * @see Table::store() + * @since 4.0.0 + */ + public function store($updateNulls = true) + { + return parent::store($updateNulls); + } } diff --git a/administrator/components/com_finder/src/Table/MapTable.php b/administrator/components/com_finder/src/Table/MapTable.php index 26bc302cb6b24..2f93404eb8154 100644 --- a/administrator/components/com_finder/src/Table/MapTable.php +++ b/administrator/components/com_finder/src/Table/MapTable.php @@ -1,4 +1,5 @@ setColumnAlias('published', 'state'); - $this->access = (int) Factory::getApplication()->get('access'); - } + $this->setColumnAlias('published', 'state'); + $this->access = (int) Factory::getApplication()->get('access'); + } - /** - * Override check function - * - * @return boolean - * - * @see Table::check() - * @since 4.0.0 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); + /** + * Override check function + * + * @return boolean + * + * @see Table::check() + * @since 4.0.0 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); - return false; - } + return false; + } - // Check for a title. - if (trim($this->title) == '') - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_CATEGORY')); + // Check for a title. + if (trim($this->title) == '') { + $this->setError(Text::_('JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_CATEGORY')); - return false; - } + return false; + } - $this->alias = ApplicationHelper::stringURLSafe($this->title, $this->language); + $this->alias = ApplicationHelper::stringURLSafe($this->title, $this->language); - if (trim($this->alias) == '') - { - $this->alias = md5(serialize($this->getProperties())); - } + if (trim($this->alias) == '') { + $this->alias = md5(serialize($this->getProperties())); + } - return true; - } + return true; + } } diff --git a/administrator/components/com_finder/src/View/Filter/HtmlView.php b/administrator/components/com_finder/src/View/Filter/HtmlView.php index b51c2166d9a89..0943d34b4fec9 100644 --- a/administrator/components/com_finder/src/View/Filter/HtmlView.php +++ b/administrator/components/com_finder/src/View/Filter/HtmlView.php @@ -1,4 +1,5 @@ filter = $this->get('Filter'); - $this->item = $this->get('Item'); - $this->form = $this->get('Form'); - $this->state = $this->get('State'); - $this->total = $this->get('Total'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Configure the toolbar. - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Method to configure the toolbar for this view. - * - * @return void - * - * @since 2.5 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $isNew = ($this->item->filter_id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $this->getCurrentUser()->id); - $canDo = ContentHelper::getActions('com_finder'); - - // Configure the toolbar. - ToolbarHelper::title( - $isNew ? Text::_('COM_FINDER_FILTER_NEW_TOOLBAR_TITLE') : Text::_('COM_FINDER_FILTER_EDIT_TOOLBAR_TITLE'), - 'zoom-in finder' - ); - - // Set the actions for new and existing records. - if ($isNew) - { - // For new records, check the create permission. - if ($canDo->get('core.create')) - { - ToolbarHelper::apply('filter.apply'); - - ToolbarHelper::saveGroup( - [ - ['save', 'filter.save'], - ['save2new', 'filter.save2new'] - ], - 'btn-success' - ); - } - - ToolbarHelper::cancel('filter.cancel'); - } - else - { - $toolbarButtons = []; - - // Can't save the record if it's checked out. - // Since it's an existing record, check the edit permission. - if (!$checkedOut && $canDo->get('core.edit')) - { - ToolbarHelper::apply('filter.apply'); - - $toolbarButtons[] = ['save', 'filter.save']; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'filter.save2new']; - } - } - - // If an existing item, can save as a copy - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'filter.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel('filter.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Smart_Search:_New_or_Edit_Filter'); - } + /** + * The filter object + * + * @var \Joomla\Component\Finder\Administrator\Table\FilterTable + * + * @since 3.6.2 + */ + protected $filter; + + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + * + * @since 3.6.2 + */ + protected $form; + + /** + * The active item + * + * @var CMSObject|boolean + * + * @since 3.6.2 + */ + protected $item; + + /** + * The model state + * + * @var CMSObject + * + * @since 3.6.2 + */ + protected $state; + + /** + * The total indexed items + * + * @var integer + * + * @since 3.8.0 + */ + protected $total; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + // Load the view data. + $this->filter = $this->get('Filter'); + $this->item = $this->get('Item'); + $this->form = $this->get('Form'); + $this->state = $this->get('State'); + $this->total = $this->get('Total'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Configure the toolbar. + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Method to configure the toolbar for this view. + * + * @return void + * + * @since 2.5 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $isNew = ($this->item->filter_id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $this->getCurrentUser()->id); + $canDo = ContentHelper::getActions('com_finder'); + + // Configure the toolbar. + ToolbarHelper::title( + $isNew ? Text::_('COM_FINDER_FILTER_NEW_TOOLBAR_TITLE') : Text::_('COM_FINDER_FILTER_EDIT_TOOLBAR_TITLE'), + 'zoom-in finder' + ); + + // Set the actions for new and existing records. + if ($isNew) { + // For new records, check the create permission. + if ($canDo->get('core.create')) { + ToolbarHelper::apply('filter.apply'); + + ToolbarHelper::saveGroup( + [ + ['save', 'filter.save'], + ['save2new', 'filter.save2new'] + ], + 'btn-success' + ); + } + + ToolbarHelper::cancel('filter.cancel'); + } else { + $toolbarButtons = []; + + // Can't save the record if it's checked out. + // Since it's an existing record, check the edit permission. + if (!$checkedOut && $canDo->get('core.edit')) { + ToolbarHelper::apply('filter.apply'); + + $toolbarButtons[] = ['save', 'filter.save']; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'filter.save2new']; + } + } + + // If an existing item, can save as a copy + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'filter.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel('filter.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Smart_Search:_New_or_Edit_Filter'); + } } diff --git a/administrator/components/com_finder/src/View/Filters/HtmlView.php b/administrator/components/com_finder/src/View/Filters/HtmlView.php index f935a72ed948b..7781f2dc5f583 100644 --- a/administrator/components/com_finder/src/View/Filters/HtmlView.php +++ b/administrator/components/com_finder/src/View/Filters/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->total = $this->get('Total'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (\count($this->items) === 0 && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Configure the toolbar. - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Method to configure the toolbar for this view. - * - * @return void - * - * @since 2.5 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_finder'); - - ToolbarHelper::title(Text::_('COM_FINDER_FILTERS_TOOLBAR_TITLE'), 'search-plus finder'); - $toolbar = Toolbar::getInstance('toolbar'); - - if ($canDo->get('core.create')) - { - ToolbarHelper::addNew('filter.add'); - ToolbarHelper::divider(); - } - - if ($this->isEmptyState === false) - { - if ($canDo->get('core.edit.state')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('filters.publish')->listCheck(true); - $childBar->unpublish('filters.unpublish')->listCheck(true); - $childBar->checkin('filters.checkin')->listCheck(true); - } - - ToolbarHelper::divider(); - $toolbar->appendButton('Popup', 'bars', 'COM_FINDER_STATISTICS', 'index.php?option=com_finder&view=statistics&tmpl=component', 550, 350, '', '', '', Text::_('COM_FINDER_STATISTICS_TITLE')); - ToolbarHelper::divider(); - - if ($canDo->get('core.delete')) - { - ToolbarHelper::deleteList('', 'filters.delete'); - ToolbarHelper::divider(); - } - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_finder'); - } - - ToolbarHelper::help('Smart_Search:_Search_Filters'); - } + /** + * An array of items + * + * @var array + * + * @since 3.6.1 + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 3.6.1 + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 3.6.1 + */ + protected $state; + + /** + * The total number of items + * + * @var integer + * + * @since 3.6.1 + */ + protected $total; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * + * @since 4.0.0 + */ + public $activeFilters; + + /** + * @var boolean + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + // Load the view data. + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->total = $this->get('Total'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (\count($this->items) === 0 && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Configure the toolbar. + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Method to configure the toolbar for this view. + * + * @return void + * + * @since 2.5 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_finder'); + + ToolbarHelper::title(Text::_('COM_FINDER_FILTERS_TOOLBAR_TITLE'), 'search-plus finder'); + $toolbar = Toolbar::getInstance('toolbar'); + + if ($canDo->get('core.create')) { + ToolbarHelper::addNew('filter.add'); + ToolbarHelper::divider(); + } + + if ($this->isEmptyState === false) { + if ($canDo->get('core.edit.state')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('filters.publish')->listCheck(true); + $childBar->unpublish('filters.unpublish')->listCheck(true); + $childBar->checkin('filters.checkin')->listCheck(true); + } + + ToolbarHelper::divider(); + $toolbar->appendButton('Popup', 'bars', 'COM_FINDER_STATISTICS', 'index.php?option=com_finder&view=statistics&tmpl=component', 550, 350, '', '', '', Text::_('COM_FINDER_STATISTICS_TITLE')); + ToolbarHelper::divider(); + + if ($canDo->get('core.delete')) { + ToolbarHelper::deleteList('', 'filters.delete'); + ToolbarHelper::divider(); + } + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_finder'); + } + + ToolbarHelper::help('Smart_Search:_Search_Filters'); + } } diff --git a/administrator/components/com_finder/src/View/Index/HtmlView.php b/administrator/components/com_finder/src/View/Index/HtmlView.php index 3bdfa77b23359..95d9485d952a3 100644 --- a/administrator/components/com_finder/src/View/Index/HtmlView.php +++ b/administrator/components/com_finder/src/View/Index/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->total = $this->get('Total'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->pluginState = $this->get('pluginState'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if ($this->get('TotalIndexed') === 0 && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check that the content - finder plugin is enabled - if (!PluginHelper::isEnabled('content', 'finder')) - { - $this->finderPluginId = FinderHelper::getFinderPluginId(); - } - - // Configure the toolbar. - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Method to configure the toolbar for this view. - * - * @return void - * - * @since 2.5 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_finder'); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_FINDER_INDEX_TOOLBAR_TITLE'), 'search-plus finder'); - - $toolbar->appendButton( - 'Popup', 'archive', 'COM_FINDER_INDEX', 'index.php?option=com_finder&view=indexer&tmpl=component', 500, 210, 0, 0, - 'window.parent.location.reload()', Text::_('COM_FINDER_HEADING_INDEXER') - ); - - if (!$this->isEmptyState) - { - if ($canDo->get('core.edit.state')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('index.publish')->listCheck(true); - $childBar->unpublish('index.unpublish')->listCheck(true); - } - - $toolbar->appendButton('Popup', 'bars', 'COM_FINDER_STATISTICS', 'index.php?option=com_finder&view=statistics&tmpl=component', 550, 350, '', '', '', Text::_('COM_FINDER_STATISTICS_TITLE')); - - if ($canDo->get('core.delete')) - { - $toolbar->confirmButton('', 'JTOOLBAR_DELETE', 'index.delete') - ->icon('icon-delete') - ->message('COM_FINDER_INDEX_CONFIRM_DELETE_PROMPT') - ->listCheck(true); - $toolbar->divider(); - } - - if ($canDo->get('core.edit.state')) - { - $dropdown = $toolbar->dropdownButton('maintenance-group'); - $dropdown->text('COM_FINDER_INDEX_TOOLBAR_MAINTENANCE') - ->toggleSplit(false) - ->icon('icon-wrench') - ->buttonClass('btn btn-action'); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->standardButton('cog', 'COM_FINDER_INDEX_TOOLBAR_OPTIMISE', 'index.optimise', false); - $childBar->confirmButton('index.purge', 'COM_FINDER_INDEX_TOOLBAR_PURGE', 'index.purge') - ->icon('icon-trash') - ->message('COM_FINDER_INDEX_CONFIRM_PURGE_PROMPT'); - } - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_finder'); - } - - ToolbarHelper::help('Smart_Search:_Indexed_Content'); - } + /** + * An array of items + * + * @var array + * + * @since 3.6.1 + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 3.6.1 + */ + protected $pagination; + + /** + * The state of core Smart Search plugins + * + * @var array + * + * @since 3.6.1 + */ + protected $pluginState; + + /** + * The id of the content - finder plugin in mysql + * + * @var integer + * + * @since 4.0.0 + */ + protected $finderPluginId = 0; + + /** + * The model state + * + * @var mixed + * + * @since 3.6.1 + */ + protected $state; + + /** + * The total number of items + * + * @var integer + * + * @since 3.6.1 + */ + protected $total; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * + * @since 4.0.0 + */ + public $activeFilters; + + /** + * @var mixed + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + // Load plugin language files. + LanguageHelper::loadPluginLanguage(); + + $this->items = $this->get('Items'); + $this->total = $this->get('Total'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->pluginState = $this->get('pluginState'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if ($this->get('TotalIndexed') === 0 && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check that the content - finder plugin is enabled + if (!PluginHelper::isEnabled('content', 'finder')) { + $this->finderPluginId = FinderHelper::getFinderPluginId(); + } + + // Configure the toolbar. + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Method to configure the toolbar for this view. + * + * @return void + * + * @since 2.5 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_finder'); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_FINDER_INDEX_TOOLBAR_TITLE'), 'search-plus finder'); + + $toolbar->appendButton( + 'Popup', + 'archive', + 'COM_FINDER_INDEX', + 'index.php?option=com_finder&view=indexer&tmpl=component', + 500, + 210, + 0, + 0, + 'window.parent.location.reload()', + Text::_('COM_FINDER_HEADING_INDEXER') + ); + + if (!$this->isEmptyState) { + if ($canDo->get('core.edit.state')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('index.publish')->listCheck(true); + $childBar->unpublish('index.unpublish')->listCheck(true); + } + + $toolbar->appendButton('Popup', 'bars', 'COM_FINDER_STATISTICS', 'index.php?option=com_finder&view=statistics&tmpl=component', 550, 350, '', '', '', Text::_('COM_FINDER_STATISTICS_TITLE')); + + if ($canDo->get('core.delete')) { + $toolbar->confirmButton('', 'JTOOLBAR_DELETE', 'index.delete') + ->icon('icon-delete') + ->message('COM_FINDER_INDEX_CONFIRM_DELETE_PROMPT') + ->listCheck(true); + $toolbar->divider(); + } + + if ($canDo->get('core.edit.state')) { + $dropdown = $toolbar->dropdownButton('maintenance-group'); + $dropdown->text('COM_FINDER_INDEX_TOOLBAR_MAINTENANCE') + ->toggleSplit(false) + ->icon('icon-wrench') + ->buttonClass('btn btn-action'); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->standardButton('cog', 'COM_FINDER_INDEX_TOOLBAR_OPTIMISE', 'index.optimise', false); + $childBar->confirmButton('index.purge', 'COM_FINDER_INDEX_TOOLBAR_PURGE', 'index.purge') + ->icon('icon-trash') + ->message('COM_FINDER_INDEX_CONFIRM_PURGE_PROMPT'); + } + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_finder'); + } + + ToolbarHelper::help('Smart_Search:_Indexed_Content'); + } } diff --git a/administrator/components/com_finder/src/View/Indexer/HtmlView.php b/administrator/components/com_finder/src/View/Indexer/HtmlView.php index 981ffbbf6b5a6..261bcfd9b9a93 100644 --- a/administrator/components/com_finder/src/View/Indexer/HtmlView.php +++ b/administrator/components/com_finder/src/View/Indexer/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->total = $this->get('Total'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if ($this->total === 0 && $this->isEmptyState = $this->get('isEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Prepare the view. - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Method to configure the toolbar for this view. - * - * @return void - * - * @since 2.5 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_finder'); - - ToolbarHelper::title(Text::_('COM_FINDER_MAPS_TOOLBAR_TITLE'), 'search-plus finder'); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - if (!$this->isEmptyState) - { - if ($canDo->get('core.edit.state')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('maps.publish')->listCheck(true); - $childBar->unpublish('maps.unpublish')->listCheck(true); - } - - ToolbarHelper::divider(); - $toolbar->appendButton('Popup', 'bars', 'COM_FINDER_STATISTICS', 'index.php?option=com_finder&view=statistics&tmpl=component', 550, 350, '', '', '', Text::_('COM_FINDER_STATISTICS_TITLE')); - ToolbarHelper::divider(); - - if ($canDo->get('core.delete')) - { - ToolbarHelper::deleteList('', 'maps.delete'); - ToolbarHelper::divider(); - } - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_finder'); - } - - ToolbarHelper::help('Smart_Search:_Content_Maps'); - } + /** + * An array of items + * + * @var array + * + * @since 3.6.1 + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 3.6.1 + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 3.6.1 + */ + protected $state; + + /** + * The total number of items + * + * @var integer + * + * @since 3.6.1 + */ + protected $total; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * + * @since 4.0.0 + */ + public $activeFilters; + + /** + * @var boolean + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + // Load plugin language files. + LanguageHelper::loadPluginLanguage(); + + // Load the view data. + $this->items = $this->get('Items'); + $this->total = $this->get('Total'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if ($this->total === 0 && $this->isEmptyState = $this->get('isEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Prepare the view. + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Method to configure the toolbar for this view. + * + * @return void + * + * @since 2.5 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_finder'); + + ToolbarHelper::title(Text::_('COM_FINDER_MAPS_TOOLBAR_TITLE'), 'search-plus finder'); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + if (!$this->isEmptyState) { + if ($canDo->get('core.edit.state')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('maps.publish')->listCheck(true); + $childBar->unpublish('maps.unpublish')->listCheck(true); + } + + ToolbarHelper::divider(); + $toolbar->appendButton('Popup', 'bars', 'COM_FINDER_STATISTICS', 'index.php?option=com_finder&view=statistics&tmpl=component', 550, 350, '', '', '', Text::_('COM_FINDER_STATISTICS_TITLE')); + ToolbarHelper::divider(); + + if ($canDo->get('core.delete')) { + ToolbarHelper::deleteList('', 'maps.delete'); + ToolbarHelper::divider(); + } + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_finder'); + } + + ToolbarHelper::help('Smart_Search:_Content_Maps'); + } } diff --git a/administrator/components/com_finder/src/View/Searches/HtmlView.php b/administrator/components/com_finder/src/View/Searches/HtmlView.php index b8029582de735..594a775116b41 100644 --- a/administrator/components/com_finder/src/View/Searches/HtmlView.php +++ b/administrator/components/com_finder/src/View/Searches/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->enabled = $this->state->params->get('gather_search_statistics', 0); - $this->canDo = ContentHelper::getActions('com_finder'); - $uri = Uri::getInstance(); - $link = 'index.php?option=com_config&view=component&component=com_finder&return=' . base64_encode($uri); - $output = HTMLHelper::_('link', Route::_($link), Text::_('JOPTIONS')); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check if component is enabled - if (!$this->enabled) - { - $app->enqueueMessage(Text::sprintf('COM_FINDER_LOGGING_DISABLED', $output), 'warning'); - } - - // Prepare the view. - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = $this->canDo; - - ToolbarHelper::title(Text::_('COM_FINDER_MANAGER_SEARCHES'), 'search'); - - if (!$this->isEmptyState) - { - if ($canDo->get('core.edit.state')) - { - ToolbarHelper::custom('searches.reset', 'refresh', '', 'JSEARCH_RESET', false); - } - - ToolbarHelper::divider(); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_finder'); - } - - ToolbarHelper::help('Smart_Search:_Search_Term_Analysis'); - } + /** + * True if gathering search statistics is enabled + * + * @var boolean + */ + protected $enabled; + + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * + * @since 4.0.0 + */ + public $activeFilters; + + /** + * The actions the user is authorised to perform + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 4.0.0 + */ + protected $canDo; + + /** + * @var boolean + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->enabled = $this->state->params->get('gather_search_statistics', 0); + $this->canDo = ContentHelper::getActions('com_finder'); + $uri = Uri::getInstance(); + $link = 'index.php?option=com_config&view=component&component=com_finder&return=' . base64_encode($uri); + $output = HTMLHelper::_('link', Route::_($link), Text::_('JOPTIONS')); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check if component is enabled + if (!$this->enabled) { + $app->enqueueMessage(Text::sprintf('COM_FINDER_LOGGING_DISABLED', $output), 'warning'); + } + + // Prepare the view. + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = $this->canDo; + + ToolbarHelper::title(Text::_('COM_FINDER_MANAGER_SEARCHES'), 'search'); + + if (!$this->isEmptyState) { + if ($canDo->get('core.edit.state')) { + ToolbarHelper::custom('searches.reset', 'refresh', '', 'JSEARCH_RESET', false); + } + + ToolbarHelper::divider(); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_finder'); + } + + ToolbarHelper::help('Smart_Search:_Search_Term_Analysis'); + } } diff --git a/administrator/components/com_finder/src/View/Statistics/HtmlView.php b/administrator/components/com_finder/src/View/Statistics/HtmlView.php index 1ccaf2fce7191..5800db88016ca 100644 --- a/administrator/components/com_finder/src/View/Statistics/HtmlView.php +++ b/administrator/components/com_finder/src/View/Statistics/HtmlView.php @@ -1,4 +1,5 @@ data = $this->get('Data'); + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + // Load the view data. + $this->data = $this->get('Data'); - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } - parent::display($tpl); - } + parent::display($tpl); + } } diff --git a/administrator/components/com_finder/tmpl/filter/edit.php b/administrator/components/com_finder/tmpl/filter/edit.php index 7cec87a044de4..1dd6f47bee024 100644 --- a/administrator/components/com_finder/tmpl/filter/edit.php +++ b/administrator/components/com_finder/tmpl/filter/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_finder.finder-edit'); + ->useScript('form.validate') + ->useScript('com_finder.finder-edit'); ?>
- - -
- 'details', 'recall' => true, 'breakpoint' => 768]); ?> - - -
-
- total > 0) : ?> -
- form->renderField('map_count'); ?> -
- - - -
- - - $this->filter->data)); ?> -
-
- -
-
- - - -
-
-
- -
- -
-
-
-
-
- -
- form->renderFieldset('jbasic'); ?> -
-
-
-
- - - - - - - - - -
+ + +
+ 'details', 'recall' => true, 'breakpoint' => 768]); ?> + + +
+
+ total > 0) : ?> +
+ form->renderField('map_count'); ?> +
+ + + +
+ + + $this->filter->data)); ?> +
+
+ +
+
+ + + +
+
+
+ +
+ +
+
+
+
+
+ +
+ form->renderFieldset('jbasic'); ?> +
+
+
+
+ + + + + + + + + +
diff --git a/administrator/components/com_finder/tmpl/filters/default.php b/administrator/components/com_finder/tmpl/filters/default.php index 3b38e40eec239..3be3fc9feeef3 100644 --- a/administrator/components/com_finder/tmpl/filters/default.php +++ b/administrator/components/com_finder/tmpl/filters/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('com_finder.filters') - ->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('table.columns') + ->useScript('multiselect'); ?>
-
-
-
- $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - authorise('core.create', 'com_finder'); - $canEdit = $user->authorise('core.edit', 'com_finder'); - $userAuthoriseCoreManage = $user->authorise('core.manage', 'com_checkin'); - $userAuthoriseCoreEditState = $user->authorise('core.edit.state', 'com_finder'); - $userId = $user->id; - foreach ($this->items as $i => $item) : - $canCheckIn = $userAuthoriseCoreManage || $item->checked_out == $userId || is_null($item->checked_out); - $canChange = $userAuthoriseCoreEditState && $canCheckIn; - $escapedTitle = $this->escape($item->title); - ?> - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - -
- filter_id, false, 'cid', 'cb', $item->title); ?> - - state, $i, 'filters.', $canChange); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'filters.', $canCheckIn); ?> - - - - - - - - - created_by_alias ?: $item->user_name; ?> - - created, Text::_('DATE_FORMAT_LC4')); ?> - - map_count; ?> - - filter_id; ?> -
+
+
+
+ $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + authorise('core.create', 'com_finder'); + $canEdit = $user->authorise('core.edit', 'com_finder'); + $userAuthoriseCoreManage = $user->authorise('core.manage', 'com_checkin'); + $userAuthoriseCoreEditState = $user->authorise('core.edit.state', 'com_finder'); + $userId = $user->id; + foreach ($this->items as $i => $item) : + $canCheckIn = $userAuthoriseCoreManage || $item->checked_out == $userId || is_null($item->checked_out); + $canChange = $userAuthoriseCoreEditState && $canCheckIn; + $escapedTitle = $this->escape($item->title); + ?> + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + +
+ filter_id, false, 'cid', 'cb', $item->title); ?> + + state, $i, 'filters.', $canChange); ?> + + checked_out) : ?> + editor, $item->checked_out_time, 'filters.', $canCheckIn); ?> + + + + + + + + + created_by_alias ?: $item->user_name; ?> + + created, Text::_('DATE_FORMAT_LC4')); ?> + + map_count; ?> + + filter_id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
-
-
+ + + + +
+
+
diff --git a/administrator/components/com_finder/tmpl/filters/emptystate.php b/administrator/components/com_finder/tmpl/filters/emptystate.php index aaa26c47d062f..bea0eab762273 100644 --- a/administrator/components/com_finder/tmpl/filters/emptystate.php +++ b/administrator/components/com_finder/tmpl/filters/emptystate.php @@ -1,4 +1,5 @@ 'COM_FINDER', - 'formURL' => 'index.php?option=com_finder&view=filters', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Smart_Search_quickstart_guide', - 'icon' => 'icon-search-plus finder', - 'btnadd' => Text::_('COM_FINDER_FILTERS_EMPTYSTATE_BUTTON_ADD'), - 'content' => Text::_('COM_FINDER_FILTERS_EMPTYSTATE_CONTENT'), - 'title' => Text::_('COM_FINDER_FILTERS_TOOLBAR_TITLE'), + 'textPrefix' => 'COM_FINDER', + 'formURL' => 'index.php?option=com_finder&view=filters', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Smart_Search_quickstart_guide', + 'icon' => 'icon-search-plus finder', + 'btnadd' => Text::_('COM_FINDER_FILTERS_EMPTYSTATE_BUTTON_ADD'), + 'content' => Text::_('COM_FINDER_FILTERS_EMPTYSTATE_CONTENT'), + 'title' => Text::_('COM_FINDER_FILTERS_TOOLBAR_TITLE'), ]; -if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_finder')) -{ - $displayData['createURL'] = "index.php?option=com_finder&task=filter.add"; +if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_finder')) { + $displayData['createURL'] = "index.php?option=com_finder&task=filter.add"; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_finder/tmpl/index/default.php b/administrator/components/com_finder/tmpl/index/default.php index 80b33832daa21..bc5d1c1279214 100644 --- a/administrator/components/com_finder/tmpl/index/default.php +++ b/administrator/components/com_finder/tmpl/index/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('multiselect') - ->useScript('table.columns'); + ->useScript('table.columns'); ?>
-
-
-
- $this)); ?> - finderPluginId) : ?> - finderPluginId . '&tmpl=component&layout=modal'); ?> - finderPluginId . 'Modal', - array( - 'url' => $link, - 'title' => Text::_('COM_FINDER_EDIT_PLUGIN_SETTINGS'), - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => '70', - 'modalWidth' => '80', - 'closeButton' => false, - 'backdrop' => 'static', - 'keyboard' => false, - 'footer' => '' - . '' - . '' - ) - ); ?> - - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - - authorise('core.manage', 'com_finder'); ?> - items as $i => $item) : ?> - - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - -
- link_id, false, 'cid', 'cb', $item->title); ?> - - published, $i, 'index.', $canChange, 'cb'); ?> - - escape($item->title); ?> - - t_title); - echo $lang->hasKey($key) ? Text::_($key) : $item->t_title; - ?> - - indexdate, Text::_('DATE_FORMAT_LC4')); ?> - - - - publish_start_date or (int) $item->publish_end_date or (int) $item->start_date or (int) $item->end_date) : ?> - - - - - - - - url) > 80) ? substr($item->url, 0, 70) . '...' : $item->url; ?> -
+
+
+
+ $this)); ?> + finderPluginId) : ?> + finderPluginId . '&tmpl=component&layout=modal'); ?> + finderPluginId . 'Modal', + array( + 'url' => $link, + 'title' => Text::_('COM_FINDER_EDIT_PLUGIN_SETTINGS'), + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => '70', + 'modalWidth' => '80', + 'closeButton' => false, + 'backdrop' => 'static', + 'keyboard' => false, + 'footer' => '' + . '' + . '' + ) + ); ?> + + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + + authorise('core.manage', 'com_finder'); ?> + items as $i => $item) : ?> + + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + +
+ link_id, false, 'cid', 'cb', $item->title); ?> + + published, $i, 'index.', $canChange, 'cb'); ?> + + escape($item->title); ?> + + t_title); + echo $lang->hasKey($key) ? Text::_($key) : $item->t_title; + ?> + + indexdate, Text::_('DATE_FORMAT_LC4')); ?> + + + + publish_start_date or (int) $item->publish_end_date or (int) $item->start_date or (int) $item->end_date) : ?> + + + + + + + + url) > 80) ? substr($item->url, 0, 70) . '...' : $item->url; ?> +
- - pagination->getListFooter(); ?> - + + pagination->getListFooter(); ?> + - - - -
-
-
+ + + +
+
+
diff --git a/administrator/components/com_finder/tmpl/index/emptystate.php b/administrator/components/com_finder/tmpl/index/emptystate.php index dea524c6f41c4..e6d54ce79733c 100644 --- a/administrator/components/com_finder/tmpl/index/emptystate.php +++ b/administrator/components/com_finder/tmpl/index/emptystate.php @@ -1,4 +1,5 @@ 'COM_FINDER', - 'formURL' => 'index.php?option=com_finder&view=index', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Smart_Search_quickstart_guide', - 'icon' => 'icon-search-plus finder', - 'content' => Text::_('COM_FINDER_INDEX_NO_DATA') . '
' . Text::_('COM_FINDER_INDEX_TIP'), - 'title' => Text::_('COM_FINDER_HEADING_INDEXER'), - 'createURL' => "javascript:document.getElementsByClassName('button-archive')[0].click();", + 'textPrefix' => 'COM_FINDER', + 'formURL' => 'index.php?option=com_finder&view=index', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Smart_Search_quickstart_guide', + 'icon' => 'icon-search-plus finder', + 'content' => Text::_('COM_FINDER_INDEX_NO_DATA') . '
' . Text::_('COM_FINDER_INDEX_TIP'), + 'title' => Text::_('COM_FINDER_HEADING_INDEXER'), + 'createURL' => "javascript:document.getElementsByClassName('button-archive')[0].click();", ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); if ($this->finderPluginId) : ?> - finderPluginId . '&tmpl=component&layout=modal'); ?> - finderPluginId . 'Modal', - array( - 'url' => $link, - 'title' => Text::_('COM_FINDER_EDIT_PLUGIN_SETTINGS'), - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => '70', - 'modalWidth' => '80', - 'closeButton' => false, - 'backdrop' => 'static', - 'keyboard' => false, - 'footer' => '' - . '' - . '' - ) - ); ?> - + finderPluginId . '&tmpl=component&layout=modal'); ?> + finderPluginId . 'Modal', + array( + 'url' => $link, + 'title' => Text::_('COM_FINDER_EDIT_PLUGIN_SETTINGS'), + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => '70', + 'modalWidth' => '80', + 'closeButton' => false, + 'backdrop' => 'static', + 'keyboard' => false, + 'footer' => '' + . '' + . '' + ) + ); ?> +document->getWebAssetManager(); $wa->useScript('keepalive') - ->useStyle('com_finder.indexer') - ->useScript('com_finder.indexer'); + ->useStyle('com_finder.indexer') + ->useScript('com_finder.indexer'); ?>
-

-

-
-
-
- -
-
- - +

+

+
+
+
+ +
+
+ +
diff --git a/administrator/components/com_finder/tmpl/maps/default.php b/administrator/components/com_finder/tmpl/maps/default.php index 305e92d67867f..9466adc8267aa 100644 --- a/administrator/components/com_finder/tmpl/maps/default.php +++ b/administrator/components/com_finder/tmpl/maps/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('com_finder.maps') - ->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('table.columns') + ->useScript('multiselect'); ?>
-
-
-
- $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - - - getIdentity()->authorise('core.manage', 'com_finder'); ?> - items as $i => $item) : ?> - - - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - -
- id, false, 'cid', 'cb', $item->title); ?> - - state, $i, 'maps.', $canChange, 'cb'); ?> - - branch_title, '*') === 'Language') - { - $title = LanguageHelper::branchLanguageTitle($item->title); - } - else - { - $key = LanguageHelper::branchSingular($item->title); - $title = $lang->hasKey($key) ? Text::_($key) : $item->title; - } - ?> - —', $item->level - 1); ?> - escape($title); ?> - escape(trim($title, '*')) === 'Language' && Multilanguage::isEnabled()) : ?> -
- -
- -
- rgt - $item->lft > 1) : ?> - - rgt - $item->lft) / 2); ?> - - - - - - - - level > 1) : ?> - - count_published; ?> - - - - - - - - level > 1) : ?> - - count_unpublished; ?> - - - - - - - - language; ?> -
+
+
+
+ $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + + + getIdentity()->authorise('core.manage', 'com_finder'); ?> + items as $i => $item) : ?> + + + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->title); ?> + + state, $i, 'maps.', $canChange, 'cb'); ?> + + branch_title, '*') === 'Language') { + $title = LanguageHelper::branchLanguageTitle($item->title); + } else { + $key = LanguageHelper::branchSingular($item->title); + $title = $lang->hasKey($key) ? Text::_($key) : $item->title; + } + ?> + —', $item->level - 1); ?> + escape($title); ?> + escape(trim($title, '*')) === 'Language' && Multilanguage::isEnabled()) : ?> +
+ +
+ +
+ rgt - $item->lft > 1) : ?> + + rgt - $item->lft) / 2); ?> + + + + - + + + level > 1) : ?> + + count_published; ?> + + + + - + + + level > 1) : ?> + + count_unpublished; ?> + + + + - + + + language; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - -
+ +
- - - -
-
+ + + +
+
diff --git a/administrator/components/com_finder/tmpl/maps/emptystate.php b/administrator/components/com_finder/tmpl/maps/emptystate.php index 3675b05b6677b..301df7df10f8a 100644 --- a/administrator/components/com_finder/tmpl/maps/emptystate.php +++ b/administrator/components/com_finder/tmpl/maps/emptystate.php @@ -1,4 +1,5 @@ 'COM_FINDER', - 'formURL' => 'index.php?option=com_finder&view=maps', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Smart_Search:_Content_Maps', - 'icon' => 'icon-search-plus finder', - 'title' => Text::_('COM_FINDER_MAPS_TOOLBAR_TITLE') + 'textPrefix' => 'COM_FINDER', + 'formURL' => 'index.php?option=com_finder&view=maps', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Smart_Search:_Content_Maps', + 'icon' => 'icon-search-plus finder', + 'title' => Text::_('COM_FINDER_MAPS_TOOLBAR_TITLE') ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_finder/tmpl/searches/default.php b/administrator/components/com_finder/tmpl/searches/default.php index b0557ece11f48..81dc67ee05789 100644 --- a/administrator/components/com_finder/tmpl/searches/default.php +++ b/administrator/components/com_finder/tmpl/searches/default.php @@ -1,4 +1,5 @@
-
-
-
- $this, 'options' => array('filterButton' => false))); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - items as $i => $item) : ?> - - - - - - - -
- , - , - -
- - - - - -
- escape($item->searchterm); ?> - - hits; ?> - - results; ?> -
+
+
+
+ $this, 'options' => array('filterButton' => false))); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + items as $i => $item) : ?> + + + + + + + +
+ , + , + +
+ + + + + +
+ escape($item->searchterm); ?> + + hits; ?> + + results; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
-
-
+ + + + +
+
+
diff --git a/administrator/components/com_finder/tmpl/searches/emptystate.php b/administrator/components/com_finder/tmpl/searches/emptystate.php index 27c523194d499..f43471bc5aede 100644 --- a/administrator/components/com_finder/tmpl/searches/emptystate.php +++ b/administrator/components/com_finder/tmpl/searches/emptystate.php @@ -1,4 +1,5 @@ 'COM_FINDER', - 'formURL' => 'index.php?option=com_finder&view=searches', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Smart_Search:_Search_Term_Analysis', - 'icon' => 'icon-search', - 'title' => Text::_('COM_FINDER_MANAGER_SEARCHES'), - 'content' => Text::_('COM_FINDER_EMPTYSTATE_SEARCHES_CONTENT'), + 'textPrefix' => 'COM_FINDER', + 'formURL' => 'index.php?option=com_finder&view=searches', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Smart_Search:_Search_Term_Analysis', + 'icon' => 'icon-search', + 'title' => Text::_('COM_FINDER_MANAGER_SEARCHES'), + 'content' => Text::_('COM_FINDER_EMPTYSTATE_SEARCHES_CONTENT'), ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_finder/tmpl/statistics/default.php b/administrator/components/com_finder/tmpl/statistics/default.php index 2726038bc966b..4c2e2444c3cff 100644 --- a/administrator/components/com_finder/tmpl/statistics/default.php +++ b/administrator/components/com_finder/tmpl/statistics/default.php @@ -1,4 +1,5 @@
- - - - - - - - - - data->type_list as $type) : ?> - - - - - - - - - - -
data->term_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')), number_format($this->data->link_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')), number_format($this->data->taxonomy_node_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')), number_format($this->data->taxonomy_branch_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR'))); ?>
- - - -
- type_title); - $lang_string = Text::_($lang_key); - echo $lang_string === $lang_key ? $type->type_title : $lang_string; - ?> - - link_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')); ?> -
- - - data->link_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')); ?> -
+ + + + + + + + + + data->type_list as $type) : ?> + + + + + + + + + + +
data->term_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')), number_format($this->data->link_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')), number_format($this->data->taxonomy_node_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')), number_format($this->data->taxonomy_branch_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR'))); ?>
+ + + +
+ type_title); + $lang_string = Text::_($lang_key); + echo $lang_string === $lang_key ? $type->type_title : $lang_string; + ?> + + link_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')); ?> +
+ + + data->link_count, 0, Text::_('DECIMALS_SEPARATOR'), Text::_('THOUSANDS_SEPARATOR')); ?> +
diff --git a/administrator/components/com_installer/helpers/installer.php b/administrator/components/com_installer/helpers/installer.php index 59ccf41a9a435..a88cd33c9eef5 100644 --- a/administrator/components/com_installer/helpers/installer.php +++ b/administrator/components/com_installer/helpers/installer.php @@ -1,13 +1,16 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; + /** * Installer helper. @@ -18,5 +21,4 @@ */ class InstallerHelper extends \Joomla\Component\Installer\Administrator\Helper\InstallerHelper { - } diff --git a/administrator/components/com_installer/services/provider.php b/administrator/components/com_installer/services/provider.php index f619ea55af342..0cc60d9fea3fb 100644 --- a/administrator/components/com_installer/services/provider.php +++ b/administrator/components/com_installer/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Installer')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Installer')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Installer')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Installer')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new InstallerComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new InstallerComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_installer/src/Controller/DatabaseController.php b/administrator/components/com_installer/src/Controller/DatabaseController.php index 8b54fc5efda46..8351d77a2924e 100644 --- a/administrator/components/com_installer/src/Controller/DatabaseController.php +++ b/administrator/components/com_installer/src/Controller/DatabaseController.php @@ -1,4 +1,5 @@ checkToken(); - - // Get items to fix the database. - $cid = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $cid = array_filter($cid); - - if (empty($cid)) - { - $this->app->getLogger()->warning( - Text::_( - 'COM_INSTALLER_ERROR_NO_EXTENSIONS_SELECTED' - ), array('category' => 'jerror') - ); - } - else - { - /** @var DatabaseModel $model */ - $model = $this->getModel('Database'); - $model->fix($cid); - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $updateModel */ - $updateModel = $this->app->bootComponent('com_joomlaupdate') - ->getMVCFactory()->createModel('Update', 'Administrator', ['ignore_request' => true]); - $updateModel->purge(); - - // Refresh versionable assets cache - $this->app->flushAssets(); - } - - $this->setRedirect(Route::_('index.php?option=com_installer&view=database', false)); - } - - /** - * Provide the data for a badge in a menu item via JSON - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function getMenuBadgeData() - { - if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } - - $model = $this->getModel('Database'); - - $changeSet = $model->getItems(); - - $changeSetCount = 0; - - foreach ($changeSet as $item) - { - $changeSetCount += $item['errorsCount']; - } - - echo new JsonResponse($changeSetCount); - } + /** + * Tries to fix missing database updates + * + * @return void + * + * @throws \Exception + * + * @since 2.5 + * @todo Purge updates has to be replaced with an events system + */ + public function fix() + { + // Check for request forgeries. + $this->checkToken(); + + // Get items to fix the database. + $cid = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $cid = array_filter($cid); + + if (empty($cid)) { + $this->app->getLogger()->warning( + Text::_( + 'COM_INSTALLER_ERROR_NO_EXTENSIONS_SELECTED' + ), + array('category' => 'jerror') + ); + } else { + /** @var DatabaseModel $model */ + $model = $this->getModel('Database'); + $model->fix($cid); + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $updateModel */ + $updateModel = $this->app->bootComponent('com_joomlaupdate') + ->getMVCFactory()->createModel('Update', 'Administrator', ['ignore_request' => true]); + $updateModel->purge(); + + // Refresh versionable assets cache + $this->app->flushAssets(); + } + + $this->setRedirect(Route::_('index.php?option=com_installer&view=database', false)); + } + + /** + * Provide the data for a badge in a menu item via JSON + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function getMenuBadgeData() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } + + $model = $this->getModel('Database'); + + $changeSet = $model->getItems(); + + $changeSetCount = 0; + + foreach ($changeSet as $item) { + $changeSetCount += $item['errorsCount']; + } + + echo new JsonResponse($changeSetCount); + } } diff --git a/administrator/components/com_installer/src/Controller/DiscoverController.php b/administrator/components/com_installer/src/Controller/DiscoverController.php index 65a74580f8970..9c7ffce8a24dc 100644 --- a/administrator/components/com_installer/src/Controller/DiscoverController.php +++ b/administrator/components/com_installer/src/Controller/DiscoverController.php @@ -1,4 +1,5 @@ checkToken('request'); + /** + * Refreshes the cache of discovered extensions. + * + * @return void + * + * @since 1.6 + */ + public function refresh() + { + $this->checkToken('request'); - /** @var \Joomla\Component\Installer\Administrator\Model\DiscoverModel $model */ - $model = $this->getModel('discover'); - $model->discover(); + /** @var \Joomla\Component\Installer\Administrator\Model\DiscoverModel $model */ + $model = $this->getModel('discover'); + $model->discover(); - if (!$model->getTotal()) - { - $this->setMessage(Text::_('COM_INSTALLER_ERROR_NO_EXTENSIONS_DISCOVERED'), 'info'); - } + if (!$model->getTotal()) { + $this->setMessage(Text::_('COM_INSTALLER_ERROR_NO_EXTENSIONS_DISCOVERED'), 'info'); + } - $this->setRedirect(Route::_('index.php?option=com_installer&view=discover', false)); - } + $this->setRedirect(Route::_('index.php?option=com_installer&view=discover', false)); + } - /** - * Install a discovered extension. - * - * @return void - * - * @since 1.6 - */ - public function install() - { - $this->checkToken(); + /** + * Install a discovered extension. + * + * @return void + * + * @since 1.6 + */ + public function install() + { + $this->checkToken(); - /** @var \Joomla\Component\Installer\Administrator\Model\DiscoverModel $model */ - $model = $this->getModel('discover'); - $model->discover_install(); - $this->setRedirect(Route::_('index.php?option=com_installer&view=discover', false)); - } + /** @var \Joomla\Component\Installer\Administrator\Model\DiscoverModel $model */ + $model = $this->getModel('discover'); + $model->discover_install(); + $this->setRedirect(Route::_('index.php?option=com_installer&view=discover', false)); + } - /** - * Clean out the discovered extension cache. - * - * @return void - * - * @since 1.6 - */ - public function purge() - { - $this->checkToken('request'); + /** + * Clean out the discovered extension cache. + * + * @return void + * + * @since 1.6 + */ + public function purge() + { + $this->checkToken('request'); - /** @var \Joomla\Component\Installer\Administrator\Model\DiscoverModel $model */ - $model = $this->getModel('discover'); - $model->purge(); - $this->setRedirect(Route::_('index.php?option=com_installer&view=discover', false), $model->_message); - } + /** @var \Joomla\Component\Installer\Administrator\Model\DiscoverModel $model */ + $model = $this->getModel('discover'); + $model->purge(); + $this->setRedirect(Route::_('index.php?option=com_installer&view=discover', false), $model->_message); + } - /** - * Provide the data for a badge in a menu item via JSON - * - * @return void - * - * @since 4.0.0 - */ - public function getMenuBadgeData() - { - if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } + /** + * Provide the data for a badge in a menu item via JSON + * + * @return void + * + * @since 4.0.0 + */ + public function getMenuBadgeData() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } - $model = $this->getModel('Discover'); + $model = $this->getModel('Discover'); - echo new JsonResponse($model->getTotal()); - } + echo new JsonResponse($model->getTotal()); + } } diff --git a/administrator/components/com_installer/src/Controller/DisplayController.php b/administrator/components/com_installer/src/Controller/DisplayController.php index 9584877bdb194..4c8cbabe1860f 100644 --- a/administrator/components/com_installer/src/Controller/DisplayController.php +++ b/administrator/components/com_installer/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ app->getDocument(); - - // Set the default view name and format from the Request. - $vName = $this->input->get('view', 'install'); - $vFormat = $document->getType(); - $lName = $this->input->get('layout', 'default', 'string'); - $id = $this->input->getInt('update_site_id'); - - // Check for edit form. - if ($vName === 'updatesite' && $lName === 'edit' && !$this->checkEditId('com_installer.edit.updatesite', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); - - $this->redirect(); - } - - // Get and render the view. - if ($view = $this->getView($vName, $vFormat)) - { - // Get the model for the view. - $model = $this->getModel($vName); - - // Push the model into the view (as default). - $view->setModel($model, true); - $view->setLayout($lName); - - // Push document object into the view. - $view->document = $document; - - $view->display(); - } - - return $this; - } - - /** - * Provide the data for a badge in a menu item via JSON - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function getMenuBadgeData() - { - if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } - - $model = $this->getModel('Warnings'); - - echo new JsonResponse(count($model->getItems())); - } + /** + * Method to display a view. + * + * @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()}. + * + * @return static This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + // Get the document object. + $document = $this->app->getDocument(); + + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'install'); + $vFormat = $document->getType(); + $lName = $this->input->get('layout', 'default', 'string'); + $id = $this->input->getInt('update_site_id'); + + // Check for edit form. + if ($vName === 'updatesite' && $lName === 'edit' && !$this->checkEditId('com_installer.edit.updatesite', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); + + $this->redirect(); + } + + // Get and render the view. + if ($view = $this->getView($vName, $vFormat)) { + // Get the model for the view. + $model = $this->getModel($vName); + + // Push the model into the view (as default). + $view->setModel($model, true); + $view->setLayout($lName); + + // Push document object into the view. + $view->document = $document; + + $view->display(); + } + + return $this; + } + + /** + * Provide the data for a badge in a menu item via JSON + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function getMenuBadgeData() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } + + $model = $this->getModel('Warnings'); + + echo new JsonResponse(count($model->getItems())); + } } diff --git a/administrator/components/com_installer/src/Controller/InstallController.php b/administrator/components/com_installer/src/Controller/InstallController.php index 3382e5e6d9746..a3376744e51bb 100644 --- a/administrator/components/com_installer/src/Controller/InstallController.php +++ b/administrator/components/com_installer/src/Controller/InstallController.php @@ -1,4 +1,5 @@ checkToken(); - - if (!$this->app->getIdentity()->authorise('core.admin')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - /** @var \Joomla\Component\Installer\Administrator\Model\InstallModel $model */ - $model = $this->getModel('install'); - - // @todo: Reset the users acl here as well to kill off any missing bits. - $result = $model->install(); - - $app = $this->app; - $redirect_url = $app->getUserState('com_installer.redirect_url'); - $return = $this->input->getBase64('return'); - - if (!$redirect_url && $return) - { - $redirect_url = base64_decode($return); - } - - // Don't redirect to an external URL. - if ($redirect_url && !Uri::isInternal($redirect_url)) - { - $redirect_url = ''; - } - - if (empty($redirect_url)) - { - $redirect_url = Route::_('index.php?option=com_installer&view=install', false); - } - else - { - // Wipe out the user state when we're going to redirect. - $app->setUserState('com_installer.redirect_url', ''); - $app->setUserState('com_installer.message', ''); - $app->setUserState('com_installer.extension_message', ''); - } - - $this->setRedirect($redirect_url); - - return $result; - } - - /** - * Install an extension from drag & drop ajax upload. - * - * @return void - * - * @since 3.7.0 - */ - public function ajax_upload() - { - // Check for request forgeries. - Session::checkToken() or jexit(Text::_('JINVALID_TOKEN')); - - if (!$this->app->getIdentity()->authorise('core.admin')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - $message = $this->app->getUserState('com_installer.message'); - - // Do install - $result = $this->install(); - - // Get redirect URL - $redirect = $this->redirect; - - // Push message queue to session because we will redirect page by \Javascript, not $app->redirect(). - // The "application.queue" is only set in redirect() method, so we must manually store it. - $this->app->getSession()->set('application.queue', $this->app->getMessageQueue()); - - header('Content-Type: application/json'); - - echo new JsonResponse(array('redirect' => $redirect), $message, !$result); - - $this->app->close(); - } + /** + * Install an extension. + * + * @return mixed + * + * @since 1.5 + */ + public function install() + { + // Check for request forgeries. + $this->checkToken(); + + if (!$this->app->getIdentity()->authorise('core.admin')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + /** @var \Joomla\Component\Installer\Administrator\Model\InstallModel $model */ + $model = $this->getModel('install'); + + // @todo: Reset the users acl here as well to kill off any missing bits. + $result = $model->install(); + + $app = $this->app; + $redirect_url = $app->getUserState('com_installer.redirect_url'); + $return = $this->input->getBase64('return'); + + if (!$redirect_url && $return) { + $redirect_url = base64_decode($return); + } + + // Don't redirect to an external URL. + if ($redirect_url && !Uri::isInternal($redirect_url)) { + $redirect_url = ''; + } + + if (empty($redirect_url)) { + $redirect_url = Route::_('index.php?option=com_installer&view=install', false); + } else { + // Wipe out the user state when we're going to redirect. + $app->setUserState('com_installer.redirect_url', ''); + $app->setUserState('com_installer.message', ''); + $app->setUserState('com_installer.extension_message', ''); + } + + $this->setRedirect($redirect_url); + + return $result; + } + + /** + * Install an extension from drag & drop ajax upload. + * + * @return void + * + * @since 3.7.0 + */ + public function ajax_upload() + { + // Check for request forgeries. + Session::checkToken() or jexit(Text::_('JINVALID_TOKEN')); + + if (!$this->app->getIdentity()->authorise('core.admin')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $message = $this->app->getUserState('com_installer.message'); + + // Do install + $result = $this->install(); + + // Get redirect URL + $redirect = $this->redirect; + + // Push message queue to session because we will redirect page by \Javascript, not $app->redirect(). + // The "application.queue" is only set in redirect() method, so we must manually store it. + $this->app->getSession()->set('application.queue', $this->app->getMessageQueue()); + + header('Content-Type: application/json'); + + echo new JsonResponse(array('redirect' => $redirect), $message, !$result); + + $this->app->close(); + } } diff --git a/administrator/components/com_installer/src/Controller/ManageController.php b/administrator/components/com_installer/src/Controller/ManageController.php index 3496706ad99d7..4d1b24a0bbbac 100644 --- a/administrator/components/com_installer/src/Controller/ManageController.php +++ b/administrator/components/com_installer/src/Controller/ManageController.php @@ -1,4 +1,5 @@ registerTask('unpublish', 'publish'); - $this->registerTask('publish', 'publish'); - } - - /** - * Enable/Disable an extension (if supported). - * - * @return void - * - * @throws \Exception - * - * @since 1.6 - */ - public function publish() - { - // Check for request forgeries. - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - $values = array('publish' => 1, 'unpublish' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($values, $task, 0, 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - $this->setMessage(Text::_('COM_INSTALLER_ERROR_NO_EXTENSIONS_SELECTED'), 'warning'); - } - else - { - /** @var ManageModel $model */ - $model = $this->getModel('manage'); - - // Change the state of the records. - if (!$model->publish($ids, $value)) - { - $this->setMessage(implode('
', $model->getErrors()), 'warning'); - } - else - { - if ($value == 1) - { - $ntext = 'COM_INSTALLER_N_EXTENSIONS_PUBLISHED'; - } - else - { - $ntext = 'COM_INSTALLER_N_EXTENSIONS_UNPUBLISHED'; - } - - $this->setMessage(Text::plural($ntext, count($ids))); - } - } - - $this->setRedirect(Route::_('index.php?option=com_installer&view=manage', false)); - } - - /** - * Remove an extension (Uninstall). - * - * @return void - * - * @throws \Exception - * - * @since 1.5 - */ - public function remove() - { - // Check for request forgeries. - $this->checkToken(); - - $eid = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $eid = array_filter($eid); - - if (!empty($eid)) - { - /** @var ManageModel $model */ - $model = $this->getModel('manage'); - - $model->remove($eid); - } - - $this->setRedirect(Route::_('index.php?option=com_installer&view=manage', false)); - } - - /** - * Refreshes the cached metadata about an extension. - * - * Useful for debugging and testing purposes when the XML file might change. - * - * @return void - * - * @since 1.6 - */ - public function refresh() - { - // Check for request forgeries. - $this->checkToken(); - - $uid = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $uid = array_filter($uid); - - if (!empty($uid)) - { - /** @var ManageModel $model */ - $model = $this->getModel('manage'); - - $model->refresh($uid); - } - - $this->setRedirect(Route::_('index.php?option=com_installer&view=manage', false)); - } - - /** - * Load the changelog for a given extension. - * - * @return void - * - * @since 4.0.0 - */ - public function loadChangelog() - { - /** @var ManageModel $model */ - $model = $this->getModel('manage'); - - $eid = $this->input->get('eid', 0, 'int'); - $source = $this->input->get('source', 'manage', 'string'); - - if (!$eid) - { - return; - } - - $output = $model->loadChangelog($eid, $source); - - echo (new JsonResponse($output)); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('unpublish', 'publish'); + $this->registerTask('publish', 'publish'); + } + + /** + * Enable/Disable an extension (if supported). + * + * @return void + * + * @throws \Exception + * + * @since 1.6 + */ + public function publish() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + $values = array('publish' => 1, 'unpublish' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($values, $task, 0, 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + $this->setMessage(Text::_('COM_INSTALLER_ERROR_NO_EXTENSIONS_SELECTED'), 'warning'); + } else { + /** @var ManageModel $model */ + $model = $this->getModel('manage'); + + // Change the state of the records. + if (!$model->publish($ids, $value)) { + $this->setMessage(implode('
', $model->getErrors()), 'warning'); + } else { + if ($value == 1) { + $ntext = 'COM_INSTALLER_N_EXTENSIONS_PUBLISHED'; + } else { + $ntext = 'COM_INSTALLER_N_EXTENSIONS_UNPUBLISHED'; + } + + $this->setMessage(Text::plural($ntext, count($ids))); + } + } + + $this->setRedirect(Route::_('index.php?option=com_installer&view=manage', false)); + } + + /** + * Remove an extension (Uninstall). + * + * @return void + * + * @throws \Exception + * + * @since 1.5 + */ + public function remove() + { + // Check for request forgeries. + $this->checkToken(); + + $eid = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $eid = array_filter($eid); + + if (!empty($eid)) { + /** @var ManageModel $model */ + $model = $this->getModel('manage'); + + $model->remove($eid); + } + + $this->setRedirect(Route::_('index.php?option=com_installer&view=manage', false)); + } + + /** + * Refreshes the cached metadata about an extension. + * + * Useful for debugging and testing purposes when the XML file might change. + * + * @return void + * + * @since 1.6 + */ + public function refresh() + { + // Check for request forgeries. + $this->checkToken(); + + $uid = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $uid = array_filter($uid); + + if (!empty($uid)) { + /** @var ManageModel $model */ + $model = $this->getModel('manage'); + + $model->refresh($uid); + } + + $this->setRedirect(Route::_('index.php?option=com_installer&view=manage', false)); + } + + /** + * Load the changelog for a given extension. + * + * @return void + * + * @since 4.0.0 + */ + public function loadChangelog() + { + /** @var ManageModel $model */ + $model = $this->getModel('manage'); + + $eid = $this->input->get('eid', 0, 'int'); + $source = $this->input->get('source', 'manage', 'string'); + + if (!$eid) { + return; + } + + $output = $model->loadChangelog($eid, $source); + + echo (new JsonResponse($output)); + } } diff --git a/administrator/components/com_installer/src/Controller/UpdateController.php b/administrator/components/com_installer/src/Controller/UpdateController.php index cc3a6e89f6372..d1ed185ff6e70 100644 --- a/administrator/components/com_installer/src/Controller/UpdateController.php +++ b/administrator/components/com_installer/src/Controller/UpdateController.php @@ -1,4 +1,5 @@ checkToken(); - - /** @var UpdateModel $model */ - $model = $this->getModel('update'); - - $uid = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $uid = array_filter($uid); - - // Get the minimum stability. - $params = ComponentHelper::getComponent('com_installer')->getParams(); - $minimum_stability = (int) $params->get('minimum_stability', Updater::STABILITY_STABLE); - - $model->update($uid, $minimum_stability); - - $app = $this->app; - $redirect_url = $app->getUserState('com_installer.redirect_url'); - - // Don't redirect to an external URL. - if (!Uri::isInternal($redirect_url)) - { - $redirect_url = ''; - } - - if (empty($redirect_url)) - { - $redirect_url = Route::_('index.php?option=com_installer&view=update', false); - } - else - { - // Wipe out the user state when we're going to redirect. - $app->setUserState('com_installer.redirect_url', ''); - $app->setUserState('com_installer.message', ''); - $app->setUserState('com_installer.extension_message', ''); - } - - $this->setRedirect($redirect_url); - } - - /** - * Find new updates. - * - * @return void - * - * @since 1.6 - */ - public function find() - { - $this->checkToken('request'); - - // Get the caching duration. - $params = ComponentHelper::getComponent('com_installer')->getParams(); - $cache_timeout = (int) $params->get('cachetimeout', 6); - $cache_timeout = 3600 * $cache_timeout; - - // Get the minimum stability. - $minimum_stability = (int) $params->get('minimum_stability', Updater::STABILITY_STABLE); - - // Find updates. - /** @var UpdateModel $model */ - $model = $this->getModel('update'); - - // Purge the table before checking again - $model->purge(); - - $disabledUpdateSites = $model->getDisabledUpdateSites(); - - if ($disabledUpdateSites) - { - $updateSitesUrl = Route::_('index.php?option=com_installer&view=updatesites'); - $this->app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_UPDATE_SITES_COUNT_CHECK', $updateSitesUrl), 'warning'); - } - - $model->findUpdates(0, $cache_timeout, $minimum_stability); - - if (0 === $model->getTotal()) - { - $this->app->enqueueMessage(Text::_('COM_INSTALLER_MSG_UPDATE_NOUPDATES'), 'info'); - } - - $this->setRedirect(Route::_('index.php?option=com_installer&view=update', false)); - } - - /** - * Fetch and report updates in \JSON format, for AJAX requests - * - * @return void - * - * @since 2.5 - */ - public function ajax() - { - $app = $this->app; - - if (!Session::checkToken('get')) - { - $app->setHeader('status', 403, true); - $app->sendHeaders(); - echo Text::_('JINVALID_TOKEN_NOTICE'); - $app->close(); - } - - // Close the session before we make a long running request - $app->getSession()->abort(); - - $eid = $this->input->getInt('eid', 0); - $skip = $this->input->get('skip', array(), 'array'); - $cache_timeout = $this->input->getInt('cache_timeout', 0); - $minimum_stability = $this->input->getInt('minimum_stability', -1); - - $params = ComponentHelper::getComponent('com_installer')->getParams(); - - if ($cache_timeout == 0) - { - $cache_timeout = (int) $params->get('cachetimeout', 6); - $cache_timeout = 3600 * $cache_timeout; - } - - if ($minimum_stability < 0) - { - $minimum_stability = (int) $params->get('minimum_stability', Updater::STABILITY_STABLE); - } - - /** @var UpdateModel $model */ - $model = $this->getModel('update'); - $model->findUpdates($eid, $cache_timeout, $minimum_stability); - - $model->setState('list.start', 0); - $model->setState('list.limit', 0); - - if ($eid != 0) - { - $model->setState('filter.extension_id', $eid); - } - - $updates = $model->getItems(); - - if (!empty($skip)) - { - $unfiltered_updates = $updates; - $updates = array(); - - foreach ($unfiltered_updates as $update) - { - if (!in_array($update->extension_id, $skip)) - { - $updates[] = $update; - } - } - } - - echo json_encode($updates); - - $app->close(); - } - - /** - * Provide the data for a badge in a menu item via JSON - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function getMenuBadgeData() - { - if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } - - $model = $this->getModel('Update'); - - echo new JsonResponse($model->getTotal()); - } + /** + * Update a set of extensions. + * + * @return void + * + * @since 1.6 + */ + public function update() + { + // Check for request forgeries. + $this->checkToken(); + + /** @var UpdateModel $model */ + $model = $this->getModel('update'); + + $uid = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $uid = array_filter($uid); + + // Get the minimum stability. + $params = ComponentHelper::getComponent('com_installer')->getParams(); + $minimum_stability = (int) $params->get('minimum_stability', Updater::STABILITY_STABLE); + + $model->update($uid, $minimum_stability); + + $app = $this->app; + $redirect_url = $app->getUserState('com_installer.redirect_url'); + + // Don't redirect to an external URL. + if (!Uri::isInternal($redirect_url)) { + $redirect_url = ''; + } + + if (empty($redirect_url)) { + $redirect_url = Route::_('index.php?option=com_installer&view=update', false); + } else { + // Wipe out the user state when we're going to redirect. + $app->setUserState('com_installer.redirect_url', ''); + $app->setUserState('com_installer.message', ''); + $app->setUserState('com_installer.extension_message', ''); + } + + $this->setRedirect($redirect_url); + } + + /** + * Find new updates. + * + * @return void + * + * @since 1.6 + */ + public function find() + { + $this->checkToken('request'); + + // Get the caching duration. + $params = ComponentHelper::getComponent('com_installer')->getParams(); + $cache_timeout = (int) $params->get('cachetimeout', 6); + $cache_timeout = 3600 * $cache_timeout; + + // Get the minimum stability. + $minimum_stability = (int) $params->get('minimum_stability', Updater::STABILITY_STABLE); + + // Find updates. + /** @var UpdateModel $model */ + $model = $this->getModel('update'); + + // Purge the table before checking again + $model->purge(); + + $disabledUpdateSites = $model->getDisabledUpdateSites(); + + if ($disabledUpdateSites) { + $updateSitesUrl = Route::_('index.php?option=com_installer&view=updatesites'); + $this->app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_UPDATE_SITES_COUNT_CHECK', $updateSitesUrl), 'warning'); + } + + $model->findUpdates(0, $cache_timeout, $minimum_stability); + + if (0 === $model->getTotal()) { + $this->app->enqueueMessage(Text::_('COM_INSTALLER_MSG_UPDATE_NOUPDATES'), 'info'); + } + + $this->setRedirect(Route::_('index.php?option=com_installer&view=update', false)); + } + + /** + * Fetch and report updates in \JSON format, for AJAX requests + * + * @return void + * + * @since 2.5 + */ + public function ajax() + { + $app = $this->app; + + if (!Session::checkToken('get')) { + $app->setHeader('status', 403, true); + $app->sendHeaders(); + echo Text::_('JINVALID_TOKEN_NOTICE'); + $app->close(); + } + + // Close the session before we make a long running request + $app->getSession()->abort(); + + $eid = $this->input->getInt('eid', 0); + $skip = $this->input->get('skip', array(), 'array'); + $cache_timeout = $this->input->getInt('cache_timeout', 0); + $minimum_stability = $this->input->getInt('minimum_stability', -1); + + $params = ComponentHelper::getComponent('com_installer')->getParams(); + + if ($cache_timeout == 0) { + $cache_timeout = (int) $params->get('cachetimeout', 6); + $cache_timeout = 3600 * $cache_timeout; + } + + if ($minimum_stability < 0) { + $minimum_stability = (int) $params->get('minimum_stability', Updater::STABILITY_STABLE); + } + + /** @var UpdateModel $model */ + $model = $this->getModel('update'); + $model->findUpdates($eid, $cache_timeout, $minimum_stability); + + $model->setState('list.start', 0); + $model->setState('list.limit', 0); + + if ($eid != 0) { + $model->setState('filter.extension_id', $eid); + } + + $updates = $model->getItems(); + + if (!empty($skip)) { + $unfiltered_updates = $updates; + $updates = array(); + + foreach ($unfiltered_updates as $update) { + if (!in_array($update->extension_id, $skip)) { + $updates[] = $update; + } + } + } + + echo json_encode($updates); + + $app->close(); + } + + /** + * Provide the data for a badge in a menu item via JSON + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function getMenuBadgeData() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_installer')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } + + $model = $this->getModel('Update'); + + echo new JsonResponse($model->getTotal()); + } } diff --git a/administrator/components/com_installer/src/Controller/UpdatesiteController.php b/administrator/components/com_installer/src/Controller/UpdatesiteController.php index 1ce791f760e02..d584644d0e5d9 100644 --- a/administrator/components/com_installer/src/Controller/UpdatesiteController.php +++ b/administrator/components/com_installer/src/Controller/UpdatesiteController.php @@ -1,4 +1,5 @@ registerTask('unpublish', 'publish'); - $this->registerTask('publish', 'publish'); - $this->registerTask('delete', 'delete'); - $this->registerTask('rebuild', 'rebuild'); - } - - /** - * Proxy for getModel. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config The array of possible config values. Optional. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel - * - * @since 4.0.0 - */ - public function getModel($name = 'Updatesite', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Enable/Disable an extension (if supported). - * - * @return void - * - * @since 3.4 - * - * @throws \Exception on error - */ - public function publish() - { - // Check for request forgeries. - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - $values = array('publish' => 1, 'unpublish' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($values, $task, 0, 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - throw new \Exception(Text::_('COM_INSTALLER_ERROR_NO_UPDATESITES_SELECTED'), 500); - } - - // Get the model. - /** @var \Joomla\Component\Installer\Administrator\Model\UpdatesitesModel $model */ - $model = $this->getModel('Updatesites'); - - // Change the state of the records. - if (!$model->publish($ids, $value)) - { - throw new \Exception(implode('
', $model->getErrors()), 500); - } - - $ntext = ($value == 0) ? 'COM_INSTALLER_N_UPDATESITES_UNPUBLISHED' : 'COM_INSTALLER_N_UPDATESITES_PUBLISHED'; - - $this->setMessage(Text::plural($ntext, count($ids))); - - $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); - } - - /** - * Deletes an update site (if supported). - * - * @return void - * - * @since 3.6 - * - * @throws \Exception on error - */ - public function delete() - { - // Check for request forgeries. - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - throw new \Exception(Text::_('COM_INSTALLER_ERROR_NO_UPDATESITES_SELECTED'), 500); - } - - // Delete the records. - $this->getModel('Updatesites')->delete($ids); - - $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); - } - - /** - * Rebuild update sites tables. - * - * @return void - * - * @since 3.6 - */ - public function rebuild() - { - // Check for request forgeries. - $this->checkToken(); - - // Rebuild the update sites. - $this->getModel('Updatesites')->rebuild(); - - $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); - } + /** + * The prefix to use with controller messages. + * + * @var string + * @since 4.0.0 + */ + protected $text_prefix = 'COM_INSTALLER_UPDATESITES'; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('unpublish', 'publish'); + $this->registerTask('publish', 'publish'); + $this->registerTask('delete', 'delete'); + $this->registerTask('rebuild', 'rebuild'); + } + + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config The array of possible config values. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel + * + * @since 4.0.0 + */ + public function getModel($name = 'Updatesite', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Enable/Disable an extension (if supported). + * + * @return void + * + * @since 3.4 + * + * @throws \Exception on error + */ + public function publish() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + $values = array('publish' => 1, 'unpublish' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($values, $task, 0, 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + throw new \Exception(Text::_('COM_INSTALLER_ERROR_NO_UPDATESITES_SELECTED'), 500); + } + + // Get the model. + /** @var \Joomla\Component\Installer\Administrator\Model\UpdatesitesModel $model */ + $model = $this->getModel('Updatesites'); + + // Change the state of the records. + if (!$model->publish($ids, $value)) { + throw new \Exception(implode('
', $model->getErrors()), 500); + } + + $ntext = ($value == 0) ? 'COM_INSTALLER_N_UPDATESITES_UNPUBLISHED' : 'COM_INSTALLER_N_UPDATESITES_PUBLISHED'; + + $this->setMessage(Text::plural($ntext, count($ids))); + + $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); + } + + /** + * Deletes an update site (if supported). + * + * @return void + * + * @since 3.6 + * + * @throws \Exception on error + */ + public function delete() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + throw new \Exception(Text::_('COM_INSTALLER_ERROR_NO_UPDATESITES_SELECTED'), 500); + } + + // Delete the records. + $this->getModel('Updatesites')->delete($ids); + + $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); + } + + /** + * Rebuild update sites tables. + * + * @return void + * + * @since 3.6 + */ + public function rebuild() + { + // Check for request forgeries. + $this->checkToken(); + + // Rebuild the update sites. + $this->getModel('Updatesites')->rebuild(); + + $this->setRedirect(Route::_('index.php?option=com_installer&view=updatesites', false)); + } } diff --git a/administrator/components/com_installer/src/Extension/InstallerComponent.php b/administrator/components/com_installer/src/Extension/InstallerComponent.php index 2a48011a0fc4b..8cf74535735db 100644 --- a/administrator/components/com_installer/src/Extension/InstallerComponent.php +++ b/administrator/components/com_installer/src/Extension/InstallerComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('manage', new Manage); - $this->getRegistry()->register('updatesites', new Updatesites); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('manage', new Manage()); + $this->getRegistry()->register('updatesites', new Updatesites()); + } } diff --git a/administrator/components/com_installer/src/Field/ExtensionstatusField.php b/administrator/components/com_installer/src/Field/ExtensionstatusField.php index 5a418cb5f2494..c24d0d9b9da73 100644 --- a/administrator/components/com_installer/src/Field/ExtensionstatusField.php +++ b/administrator/components/com_installer/src/Field/ExtensionstatusField.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('DISTINCT ' . $db->quoteName('type')) - ->from($db->quoteName('#__extensions')); - $db->setQuery($query); - $types = $db->loadColumn(); - - $options = array(); - - foreach ($types as $type) - { - $options[] = HTMLHelper::_('select.option', $type, Text::_('COM_INSTALLER_TYPE_' . strtoupper($type))); - } - - return $options; - } - - /** - * Get a list of filter options for the extension types. - * - * @return array An array of \stdClass objects. - * - * @since 3.0 - */ - public static function getExtensionGroups() - { - $nofolder = ''; - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('folder')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('folder') . ' != :folder') - ->bind(':folder', $nofolder) - ->order($db->quoteName('folder')); - $db->setQuery($query); - $folders = $db->loadColumn(); - - $options = array(); - - foreach ($folders as $folder) - { - $options[] = HTMLHelper::_('select.option', $folder, $folder); - } - - return $options; - } - - /** - * Get a list of filter options for the application clients. - * - * @return array An array of \JHtmlOption elements. - * - * @since 3.5 - */ - public static function getClientOptions() - { - // Build the filter options. - $options = array(); - $options[] = HTMLHelper::_('select.option', '0', Text::_('JSITE')); - $options[] = HTMLHelper::_('select.option', '1', Text::_('JADMINISTRATOR')); - $options[] = HTMLHelper::_('select.option', '3', Text::_('JAPI')); - - return $options; - } - - /** - * Get a list of filter options for the application statuses. - * - * @return array An array of \JHtmlOption elements. - * - * @since 3.5 - */ - public static function getStateOptions() - { - // Build the filter options. - $options = array(); - $options[] = HTMLHelper::_('select.option', '0', Text::_('JDISABLED')); - $options[] = HTMLHelper::_('select.option', '1', Text::_('JENABLED')); - $options[] = HTMLHelper::_('select.option', '2', Text::_('JPROTECTED')); - $options[] = HTMLHelper::_('select.option', '3', Text::_('JUNPROTECTED')); - - return $options; - } - - /** - * Get a list of filter options for extensions of the "package" type. - * - * @return array - * @since 4.2.0 - */ - public static function getPackageOptions(): array - { - $options = []; - - /** @var DatabaseDriver $db The application's database driver object */ - $db = Factory::getContainer()->get(DatabaseDriver::class); - $query = $db->getQuery(true) - ->select( - $db->quoteName( - [ - 'extension_id', - 'name', - 'element', - ] - ) - ) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('package')); - $extensions = $db->setQuery($query)->loadObjectList() ?: []; - - if (empty($extensions)) - { - return $options; - } - - $language = Factory::getApplication()->getLanguage(); - $arrayKeys = array_map( - function (object $entry) use ($language): string - { - $language->load($entry->element, JPATH_ADMINISTRATOR); - - return Text::_($entry->name); - }, - $extensions - ); - $arrayValues = array_map( - function (object $entry): int - { - return $entry->extension_id; - }, - $extensions - ); - - $extensions = array_combine($arrayKeys, $arrayValues); - ksort($extensions); - - foreach ($extensions as $label => $id) - { - $options[] = HTMLHelper::_('select.option', $id, $label); - } - - return $options; - } - - /** - * Get a list of filter options for the application statuses. - * - * @param string $element element of an extension - * @param string $type type of an extension - * @param integer $clientId client_id of an extension - * @param string $folder folder of an extension - * - * @return SimpleXMLElement - * - * @since 4.0.0 - */ - public static function getInstallationXML(string $element, string $type, int $clientId = 1, - ?string $folder = null - ): ?SimpleXMLElement - { - $path = [0 => JPATH_SITE, 1 => JPATH_ADMINISTRATOR, 3 => JPATH_API][$clientId] ?? JPATH_SITE; - - switch ($type) - { - case 'component': - $path .= '/components/' . $element . '/' . substr($element, 4) . '.xml'; - break; - case 'plugin': - $path .= '/plugins/' . $folder . '/' . $element . '/' . $element . '.xml'; - break; - case 'module': - $path .= '/modules/' . $element . '/' . $element . '.xml'; - break; - case 'template': - $path .= '/templates/' . $element . '/templateDetails.xml'; - break; - case 'library': - $path = JPATH_ADMINISTRATOR . '/manifests/libraries/' . $element . '.xml'; - break; - case 'file': - $path = JPATH_ADMINISTRATOR . '/manifests/files/' . $element . '.xml'; - break; - case 'package': - $path = JPATH_ADMINISTRATOR . '/manifests/packages/' . $element . '.xml'; - break; - case 'language': - $path .= '/language/' . $element . '/install.xml'; - } - - if (file_exists($path) === false) - { - return null; - } - - $xmlElement = simplexml_load_file($path); - - return ($xmlElement !== false) ? $xmlElement : null; - } - - /** - * Get the download key of an extension going through their installation xml - * - * @param CMSObject $extension element of an extension - * - * @return array An array with the prefix, suffix and value of the download key - * - * @since 4.0.0 - */ - public static function getDownloadKey(CMSObject $extension): array - { - $installXmlFile = self::getInstallationXML( - $extension->get('element'), - $extension->get('type'), - $extension->get('client_id'), - $extension->get('folder') - ); - - if (!$installXmlFile) - { - return [ - 'supported' => false, - 'valid' => false, - ]; - } - - if (!isset($installXmlFile->dlid)) - { - return [ - 'supported' => false, - 'valid' => false, - ]; - } - - $prefix = (string) $installXmlFile->dlid['prefix']; - $suffix = (string) $installXmlFile->dlid['suffix']; - $value = substr($extension->get('extra_query'), strlen($prefix)); - - if ($suffix) - { - $value = substr($value, 0, -strlen($suffix)); - } - - $downloadKey = [ - 'supported' => true, - 'valid' => $value ? true : false, - 'prefix' => $prefix, - 'suffix' => $suffix, - 'value' => $value - ]; - - return $downloadKey; - } - - /** - * Get the download key of an extension given enough information to locate it in the #__extensions table - * - * @param string $element Name of the extension, e.g. com_foo - * @param string $type The type of the extension, e.g. component - * @param int $clientId [optional] Joomla client for the extension, see the #__extensions table - * @param string|null $folder Extension folder, only applies for 'plugin' type - * - * @return array - * - * @since 4.0.0 - */ - public static function getExtensionDownloadKey(string $element, string $type, int $clientId = 1, - ?string $folder = null - ): array - { - // Get the database driver. If it fails we cannot report whether the extension supports download keys. - try - { - $db = Factory::getDbo(); - } - catch (Exception $e) - { - return [ - 'supported' => false, - 'valid' => false, - ]; - } - - // Try to retrieve the extension information as a CMSObject - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = :type') - ->where($db->quoteName('element') . ' = :element') - ->where($db->quoteName('folder') . ' = :folder') - ->where($db->quoteName('client_id') . ' = :client_id'); - $query->bind(':type', $type, ParameterType::STRING); - $query->bind(':element', $element, ParameterType::STRING); - $query->bind(':client_id', $clientId, ParameterType::INTEGER); - $query->bind(':folder', $folder, ParameterType::STRING); - - try - { - $extension = new CMSObject($db->setQuery($query)->loadAssoc()); - } - catch (Exception $e) - { - return [ - 'supported' => false, - 'valid' => false, - ]; - } - - // Use the getDownloadKey() method to return the download key information - return self::getDownloadKey($extension); - } - - /** - * Returns a list of update site IDs which support download keys. By default this returns all qualifying update - * sites, even if they are not enabled. - * - * - * @param bool $onlyEnabled [optional] Set true to only returned enabled update sites. - * - * @return int[] - * @since 4.0.0 - */ - public static function getDownloadKeySupportedSites($onlyEnabled = false): array - { - /** - * NOTE: The closures are not inlined because in this case the Joomla Code Style standard produces two mutually - * exclusive errors, making the file impossible to commit. Using closures in variables makes the code less - * readable but works around that issue. - */ - - $extensions = self::getUpdateSitesInformation($onlyEnabled); - - $filterClosure = function (CMSObject $extension) { - $dlidInfo = self::getDownloadKey($extension); - - return $dlidInfo['supported']; - }; - $extensions = array_filter($extensions, $filterClosure); - - $mapClosure = function (CMSObject $extension) { - return $extension->get('update_site_id'); - }; - - return array_map($mapClosure, $extensions); - } - - /** - * Returns a list of update site IDs which are missing download keys. By default this returns all qualifying update - * sites, even if they are not enabled. - * - * @param bool $exists [optional] If true, returns update sites with a valid download key. When false, - * returns update sites with an invalid / missing download key. - * @param bool $onlyEnabled [optional] Set true to only returned enabled update sites. - * - * @return int[] - * @since 4.0.0 - */ - public static function getDownloadKeyExistsSites(bool $exists = true, $onlyEnabled = false): array - { - /** - * NOTE: The closures are not inlined because in this case the Joomla Code Style standard produces two mutually - * exclusive errors, making the file impossible to commit. Using closures in variables makes the code less - * readable but works around that issue. - */ - - $extensions = self::getUpdateSitesInformation($onlyEnabled); - - // Filter the extensions by what supports Download Keys - $filterClosure = function (CMSObject $extension) use ($exists) { - $dlidInfo = self::getDownloadKey($extension); - - if (!$dlidInfo['supported']) - { - return false; - } - - return $exists ? $dlidInfo['valid'] : !$dlidInfo['valid']; - }; - $extensions = array_filter($extensions, $filterClosure); - - // Return only the update site IDs - $mapClosure = function (CMSObject $extension) { - return $extension->get('update_site_id'); - }; - - return array_map($mapClosure, $extensions); - } - - - /** - * Get information about the update sites - * - * @param bool $onlyEnabled Only return enabled update sites - * - * @return CMSObject[] List of update site and linked extension information - * @since 4.0.0 - */ - protected static function getUpdateSitesInformation(bool $onlyEnabled): array - { - try - { - $db = Factory::getDbo(); - } - catch (Exception $e) - { - return []; - } - - $query = $db->getQuery(true) - ->select( - $db->quoteName( - [ - 's.update_site_id', - 's.enabled', - 's.extra_query', - 'e.extension_id', - 'e.type', - 'e.element', - 'e.folder', - 'e.client_id', - 'e.manifest_cache', - ], - [ - 'update_site_id', - 'enabled', - 'extra_query', - 'extension_id', - 'type', - 'element', - 'folder', - 'client_id', - 'manifest_cache', - ] - ) - ) - ->from($db->quoteName('#__update_sites', 's')) - ->innerJoin( - $db->quoteName('#__update_sites_extensions', 'se'), - $db->quoteName('se.update_site_id') . ' = ' . $db->quoteName('s.update_site_id') - ) - ->innerJoin( - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('se.extension_id') - ) - ->where($db->quoteName('state') . ' = 0'); - - if ($onlyEnabled) - { - $enabled = 1; - $query->where($db->quoteName('s.enabled') . ' = :enabled') - ->bind(':enabled', $enabled, ParameterType::INTEGER); - } - - // Try to get all of the update sites, including related extension information - try - { - $items = []; - $db->setQuery($query); - - foreach ($db->getIterator() as $item) - { - $items[] = new CMSObject($item); - } - - return $items; - } - catch (Exception $e) - { - return []; - } - } + /** + * Get a list of filter options for the extension types. + * + * @return array An array of \stdClass objects. + * + * @since 3.0 + */ + public static function getExtensionTypes() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('type')) + ->from($db->quoteName('#__extensions')); + $db->setQuery($query); + $types = $db->loadColumn(); + + $options = array(); + + foreach ($types as $type) { + $options[] = HTMLHelper::_('select.option', $type, Text::_('COM_INSTALLER_TYPE_' . strtoupper($type))); + } + + return $options; + } + + /** + * Get a list of filter options for the extension types. + * + * @return array An array of \stdClass objects. + * + * @since 3.0 + */ + public static function getExtensionGroups() + { + $nofolder = ''; + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('folder')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('folder') . ' != :folder') + ->bind(':folder', $nofolder) + ->order($db->quoteName('folder')); + $db->setQuery($query); + $folders = $db->loadColumn(); + + $options = array(); + + foreach ($folders as $folder) { + $options[] = HTMLHelper::_('select.option', $folder, $folder); + } + + return $options; + } + + /** + * Get a list of filter options for the application clients. + * + * @return array An array of \JHtmlOption elements. + * + * @since 3.5 + */ + public static function getClientOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '0', Text::_('JSITE')); + $options[] = HTMLHelper::_('select.option', '1', Text::_('JADMINISTRATOR')); + $options[] = HTMLHelper::_('select.option', '3', Text::_('JAPI')); + + return $options; + } + + /** + * Get a list of filter options for the application statuses. + * + * @return array An array of \JHtmlOption elements. + * + * @since 3.5 + */ + public static function getStateOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '0', Text::_('JDISABLED')); + $options[] = HTMLHelper::_('select.option', '1', Text::_('JENABLED')); + $options[] = HTMLHelper::_('select.option', '2', Text::_('JPROTECTED')); + $options[] = HTMLHelper::_('select.option', '3', Text::_('JUNPROTECTED')); + + return $options; + } + + /** + * Get a list of filter options for extensions of the "package" type. + * + * @return array + * @since 4.2.0 + */ + public static function getPackageOptions(): array + { + $options = []; + + /** @var DatabaseDriver $db The application's database driver object */ + $db = Factory::getContainer()->get(DatabaseDriver::class); + $query = $db->getQuery(true) + ->select( + $db->quoteName( + [ + 'extension_id', + 'name', + 'element', + ] + ) + ) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('package')); + $extensions = $db->setQuery($query)->loadObjectList() ?: []; + + if (empty($extensions)) { + return $options; + } + + $language = Factory::getApplication()->getLanguage(); + $arrayKeys = array_map( + function (object $entry) use ($language): string { + $language->load($entry->element, JPATH_ADMINISTRATOR); + + return Text::_($entry->name); + }, + $extensions + ); + $arrayValues = array_map( + function (object $entry): int { + return $entry->extension_id; + }, + $extensions + ); + + $extensions = array_combine($arrayKeys, $arrayValues); + ksort($extensions); + + foreach ($extensions as $label => $id) { + $options[] = HTMLHelper::_('select.option', $id, $label); + } + + return $options; + } + + /** + * Get a list of filter options for the application statuses. + * + * @param string $element element of an extension + * @param string $type type of an extension + * @param integer $clientId client_id of an extension + * @param string $folder folder of an extension + * + * @return SimpleXMLElement + * + * @since 4.0.0 + */ + public static function getInstallationXML( + string $element, + string $type, + int $clientId = 1, + ?string $folder = null + ): ?SimpleXMLElement { + $path = [0 => JPATH_SITE, 1 => JPATH_ADMINISTRATOR, 3 => JPATH_API][$clientId] ?? JPATH_SITE; + + switch ($type) { + case 'component': + $path .= '/components/' . $element . '/' . substr($element, 4) . '.xml'; + break; + case 'plugin': + $path .= '/plugins/' . $folder . '/' . $element . '/' . $element . '.xml'; + break; + case 'module': + $path .= '/modules/' . $element . '/' . $element . '.xml'; + break; + case 'template': + $path .= '/templates/' . $element . '/templateDetails.xml'; + break; + case 'library': + $path = JPATH_ADMINISTRATOR . '/manifests/libraries/' . $element . '.xml'; + break; + case 'file': + $path = JPATH_ADMINISTRATOR . '/manifests/files/' . $element . '.xml'; + break; + case 'package': + $path = JPATH_ADMINISTRATOR . '/manifests/packages/' . $element . '.xml'; + break; + case 'language': + $path .= '/language/' . $element . '/install.xml'; + } + + if (file_exists($path) === false) { + return null; + } + + $xmlElement = simplexml_load_file($path); + + return ($xmlElement !== false) ? $xmlElement : null; + } + + /** + * Get the download key of an extension going through their installation xml + * + * @param CMSObject $extension element of an extension + * + * @return array An array with the prefix, suffix and value of the download key + * + * @since 4.0.0 + */ + public static function getDownloadKey(CMSObject $extension): array + { + $installXmlFile = self::getInstallationXML( + $extension->get('element'), + $extension->get('type'), + $extension->get('client_id'), + $extension->get('folder') + ); + + if (!$installXmlFile) { + return [ + 'supported' => false, + 'valid' => false, + ]; + } + + if (!isset($installXmlFile->dlid)) { + return [ + 'supported' => false, + 'valid' => false, + ]; + } + + $prefix = (string) $installXmlFile->dlid['prefix']; + $suffix = (string) $installXmlFile->dlid['suffix']; + $value = substr($extension->get('extra_query'), strlen($prefix)); + + if ($suffix) { + $value = substr($value, 0, -strlen($suffix)); + } + + $downloadKey = [ + 'supported' => true, + 'valid' => $value ? true : false, + 'prefix' => $prefix, + 'suffix' => $suffix, + 'value' => $value + ]; + + return $downloadKey; + } + + /** + * Get the download key of an extension given enough information to locate it in the #__extensions table + * + * @param string $element Name of the extension, e.g. com_foo + * @param string $type The type of the extension, e.g. component + * @param int $clientId [optional] Joomla client for the extension, see the #__extensions table + * @param string|null $folder Extension folder, only applies for 'plugin' type + * + * @return array + * + * @since 4.0.0 + */ + public static function getExtensionDownloadKey( + string $element, + string $type, + int $clientId = 1, + ?string $folder = null + ): array { + // Get the database driver. If it fails we cannot report whether the extension supports download keys. + try { + $db = Factory::getDbo(); + } catch (Exception $e) { + return [ + 'supported' => false, + 'valid' => false, + ]; + } + + // Try to retrieve the extension information as a CMSObject + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = :type') + ->where($db->quoteName('element') . ' = :element') + ->where($db->quoteName('folder') . ' = :folder') + ->where($db->quoteName('client_id') . ' = :client_id'); + $query->bind(':type', $type, ParameterType::STRING); + $query->bind(':element', $element, ParameterType::STRING); + $query->bind(':client_id', $clientId, ParameterType::INTEGER); + $query->bind(':folder', $folder, ParameterType::STRING); + + try { + $extension = new CMSObject($db->setQuery($query)->loadAssoc()); + } catch (Exception $e) { + return [ + 'supported' => false, + 'valid' => false, + ]; + } + + // Use the getDownloadKey() method to return the download key information + return self::getDownloadKey($extension); + } + + /** + * Returns a list of update site IDs which support download keys. By default this returns all qualifying update + * sites, even if they are not enabled. + * + * + * @param bool $onlyEnabled [optional] Set true to only returned enabled update sites. + * + * @return int[] + * @since 4.0.0 + */ + public static function getDownloadKeySupportedSites($onlyEnabled = false): array + { + /** + * NOTE: The closures are not inlined because in this case the Joomla Code Style standard produces two mutually + * exclusive errors, making the file impossible to commit. Using closures in variables makes the code less + * readable but works around that issue. + */ + + $extensions = self::getUpdateSitesInformation($onlyEnabled); + + $filterClosure = function (CMSObject $extension) { + $dlidInfo = self::getDownloadKey($extension); + + return $dlidInfo['supported']; + }; + $extensions = array_filter($extensions, $filterClosure); + + $mapClosure = function (CMSObject $extension) { + return $extension->get('update_site_id'); + }; + + return array_map($mapClosure, $extensions); + } + + /** + * Returns a list of update site IDs which are missing download keys. By default this returns all qualifying update + * sites, even if they are not enabled. + * + * @param bool $exists [optional] If true, returns update sites with a valid download key. When false, + * returns update sites with an invalid / missing download key. + * @param bool $onlyEnabled [optional] Set true to only returned enabled update sites. + * + * @return int[] + * @since 4.0.0 + */ + public static function getDownloadKeyExistsSites(bool $exists = true, $onlyEnabled = false): array + { + /** + * NOTE: The closures are not inlined because in this case the Joomla Code Style standard produces two mutually + * exclusive errors, making the file impossible to commit. Using closures in variables makes the code less + * readable but works around that issue. + */ + + $extensions = self::getUpdateSitesInformation($onlyEnabled); + + // Filter the extensions by what supports Download Keys + $filterClosure = function (CMSObject $extension) use ($exists) { + $dlidInfo = self::getDownloadKey($extension); + + if (!$dlidInfo['supported']) { + return false; + } + + return $exists ? $dlidInfo['valid'] : !$dlidInfo['valid']; + }; + $extensions = array_filter($extensions, $filterClosure); + + // Return only the update site IDs + $mapClosure = function (CMSObject $extension) { + return $extension->get('update_site_id'); + }; + + return array_map($mapClosure, $extensions); + } + + + /** + * Get information about the update sites + * + * @param bool $onlyEnabled Only return enabled update sites + * + * @return CMSObject[] List of update site and linked extension information + * @since 4.0.0 + */ + protected static function getUpdateSitesInformation(bool $onlyEnabled): array + { + try { + $db = Factory::getDbo(); + } catch (Exception $e) { + return []; + } + + $query = $db->getQuery(true) + ->select( + $db->quoteName( + [ + 's.update_site_id', + 's.enabled', + 's.extra_query', + 'e.extension_id', + 'e.type', + 'e.element', + 'e.folder', + 'e.client_id', + 'e.manifest_cache', + ], + [ + 'update_site_id', + 'enabled', + 'extra_query', + 'extension_id', + 'type', + 'element', + 'folder', + 'client_id', + 'manifest_cache', + ] + ) + ) + ->from($db->quoteName('#__update_sites', 's')) + ->innerJoin( + $db->quoteName('#__update_sites_extensions', 'se'), + $db->quoteName('se.update_site_id') . ' = ' . $db->quoteName('s.update_site_id') + ) + ->innerJoin( + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('se.extension_id') + ) + ->where($db->quoteName('state') . ' = 0'); + + if ($onlyEnabled) { + $enabled = 1; + $query->where($db->quoteName('s.enabled') . ' = :enabled') + ->bind(':enabled', $enabled, ParameterType::INTEGER); + } + + // Try to get all of the update sites, including related extension information + try { + $items = []; + $db->setQuery($query); + + foreach ($db->getIterator() as $item) { + $items[] = new CMSObject($item); + } + + return $items; + } catch (Exception $e) { + return []; + } + } } diff --git a/administrator/components/com_installer/src/Model/DatabaseModel.php b/administrator/components/com_installer/src/Model/DatabaseModel.php index 3e8b24400769f..11085d58671e6 100644 --- a/administrator/components/com_installer/src/Model/DatabaseModel.php +++ b/administrator/components/com_installer/src/Model/DatabaseModel.php @@ -1,4 +1,5 @@ errorCount; - } - - /** - * Method to populate the schema cache. - * - * @param integer $cid The extension ID to get the schema for - * - * @return void - * - * @throws \Exception - * - * @since 4.0.0 - */ - private function fetchSchemaCache($cid = 0) - { - // We already have it - if (array_key_exists($cid, $this->changeSetList)) - { - return; - } - - // Add the ID to the state so it can be used for filtering - if ($cid) - { - $this->setState('filter.extension_id', $cid); - } - - // With the parent::save it can get the limit and we need to make sure it gets all extensions - $results = $this->_getList($this->getListQuery()); - - foreach ($results as $result) - { - $errorMessages = array(); - $errorCount = 0; - - if (strcmp($result->element, 'joomla') === 0) - { - $result->element = 'com_admin'; - - if (!$this->getDefaultTextFilters()) - { - $errorMessages[] = Text::_('COM_INSTALLER_MSG_DATABASE_FILTER_ERROR'); - $errorCount++; - } - } - - $db = $this->getDatabase(); - - if ($result->type === 'component') - { - $basePath = JPATH_ADMINISTRATOR . '/components/' . $result->element; - } - elseif ($result->type === 'plugin') - { - $basePath = JPATH_PLUGINS . '/' . $result->folder . '/' . $result->element; - } - elseif ($result->type === 'module') - { - // Typehint to integer to normalise some DBs returning strings and others integers - if ((int) $result->client_id === 1) - { - $basePath = JPATH_ADMINISTRATOR . '/modules/' . $result->element; - } - elseif ((int) $result->client_id === 0) - { - $basePath = JPATH_SITE . '/modules/' . $result->element; - } - else - { - // Module with unknown client id!? - bail - continue; - } - } - // Specific bodge for the Joomla CMS special database check which points to com_admin - elseif ($result->type === 'file' && $result->element === 'com_admin') - { - $basePath = JPATH_ADMINISTRATOR . '/components/' . $result->element; - } - else - { - // Unknown extension type (library, files etc which don't have known SQL paths right now) - continue; - } - - // Search the standard SQL Path for the SQL Updates and then if not there check the configuration of the XML - // file. This just gives us a small performance win of not parsing the XML every time. - $folderTmp = $basePath . '/sql/updates/'; - - if (!file_exists($folderTmp)) - { - $installationXML = InstallerHelper::getInstallationXML( - $result->element, - $result->type, - $result->client_id, - $result->type === 'plugin' ? $result->folder : null - ); - - if ($installationXML !== null) - { - $folderTmp = (string) $installationXML->update->schemas->schemapath[0]; - $a = explode('/', $folderTmp); - array_pop($a); - $folderTmp = $basePath . '/' . implode('/', $a); - } - } - - // Can't find the folder still - give up now and move on. - if (!file_exists($folderTmp)) - { - continue; - } - - $changeSet = new ChangeSet($db, $folderTmp); - - // If the version in the #__schemas is different - // than the update files, add to problems message - $schema = $changeSet->getSchema(); - - // If the schema is empty we couldn't find any update files. Just ignore the extension. - if (empty($schema)) - { - continue; - } - - if ($result->version_id !== $schema) - { - $errorMessages[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_SCHEMA_ERROR', $result->version_id, $schema); - $errorCount++; - } - - // If the version in the manifest_cache is different than the - // version in the installation xml, add to problems message - $compareUpdateMessage = $this->compareUpdateVersion($result); - - if ($compareUpdateMessage) - { - $errorMessages[] = $compareUpdateMessage; - $errorCount++; - } - - // If there are errors in the database, add to the problems message - $errors = $changeSet->check(); - - $errorsMessage = $this->getErrorsMessage($errors); - - if ($errorsMessage) - { - $errorMessages = array_merge($errorMessages, $errorsMessage); - $errorCount++; - } - - // Number of database tables Checked and Skipped - $errorMessages = array_merge($errorMessages, $this->getOtherInformationMessage($changeSet->getStatus())); - - // Set the total number of errors - $this->errorCount += $errorCount; - - // Collect the extension details - $this->changeSetList[$result->extension_id] = array( - 'folderTmp' => $folderTmp, - 'errorsMessage' => $errorMessages, - 'errorsCount' => $errorCount, - 'results' => $changeSet->getStatus(), - 'schema' => $schema, - 'extension' => $result - ); - } - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'name', $direction = 'asc') - { - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); - $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); - $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); - - parent::populateState($ordering, $direction); - } - - /** - * Fixes database problems. - * - * @param array $cids List of the selected extensions to fix - * - * @return void|boolean - * - * @throws \Exception - * - * @since 4.0.0 - */ - public function fix($cids = array()) - { - $db = $this->getDatabase(); - - foreach ($cids as $i => $cid) - { - // Load the database issues - $this->fetchSchemaCache($cid); - - $changeSet = $this->changeSetList[$cid]; - $changeSet['changeset'] = new ChangeSet($db, $changeSet['folderTmp']); - $changeSet['changeset']->fix(); - - $this->fixSchemaVersion($changeSet['changeset'], $changeSet['extension']->extension_id); - $this->fixUpdateVersion($changeSet['extension']->extension_id); - - if ($changeSet['extension']->element === 'com_admin') - { - $installer = new \JoomlaInstallerScript; - $installer->deleteUnexistingFiles(); - $this->fixDefaultTextFilters(); - - /* - * Finally, if the schema updates succeeded, make sure the database table is - * converted to utf8mb4 or, if not supported by the server, compatible to it. - */ - $statusArray = $changeSet['changeset']->getStatus(); - - if (count($statusArray['error']) == 0) - { - $installer->convertTablesToUtf8mb4(false); - } - } - } - } - - /** - * Gets the changeset array. - * - * @return array Array with the information of the versions problems, errors and the extensions itself - * - * @throws \Exception - * - * @since 4.0.0 - */ - public function getItems() - { - $this->fetchSchemaCache(); - - $results = parent::getItems(); - $results = $this->mergeSchemaCache($results); - - return $results; - } - - /** - * Method to get the database query - * - * @return DatabaseQuery The database query - * - * @since 4.0.0 - */ - protected function getListQuery() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select( - $db->quoteName( - [ - 'extensions.client_id', - 'extensions.element', - 'extensions.extension_id', - 'extensions.folder', - 'extensions.manifest_cache', - 'extensions.name', - 'extensions.type', - 'schemas.version_id' - ] - ) - ) - ->from( - $db->quoteName( - '#__schemas', - 'schemas' - ) - ) - ->join( - 'INNER', - $db->quoteName('#__extensions', 'extensions'), - $db->quoteName('schemas.extension_id') . ' = ' . $db->quoteName('extensions.extension_id') - ); - - $type = $this->getState('filter.type'); - $clientId = $this->getState('filter.client_id'); - $extensionId = $this->getState('filter.extension_id'); - $folder = $this->getState('filter.folder'); - - if ($type) - { - $query->where($db->quoteName('extensions.type') . ' = :type') - ->bind(':type', $type); - } - - if ($clientId != '') - { - $clientId = (int) $clientId; - $query->where($db->quoteName('extensions.client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); - } - - if ($extensionId != '') - { - $extensionId = (int) $extensionId; - $query->where($db->quoteName('extensions.extension_id') . ' = :extensionid') - ->bind(':extensionid', $extensionId, ParameterType::INTEGER); - } - - if ($folder != '' && in_array($type, array('plugin', 'library', ''))) - { - $folder = $folder === '*' ? '' : $folder; - $query->where($db->quoteName('extensions.folder') . ' = :folder') - ->bind(':folder', $folder); - } - - // Process search filter (update site id). - $search = $this->getState('filter.search'); - - if (!empty($search) && stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('schemas.extension_id') . ' = :eid') - ->bind(':eid', $ids, ParameterType::INTEGER); - } - - return $query; - } - - /** - * Merge the items that will be visible with the changeSet information in cache - * - * @param array $results extensions returned from parent::getItems(). - * - * @return array the changeSetList of the merged items - * - * @since 4.0.0 - */ - protected function mergeSchemaCache($results) - { - $changeSetList = $this->changeSetList; - $finalResults = array(); - - foreach ($results as $result) - { - if (array_key_exists($result->extension_id, $changeSetList) && $changeSetList[$result->extension_id]) - { - $finalResults[] = $changeSetList[$result->extension_id]; - } - } - - return $finalResults; - } - - /** - * Get version from #__schemas table. - * - * @param integer $extensionId id of the extensions. - * - * @return mixed the return value from the query, or null if the query fails. - * - * @throws \Exception - * - * @since 4.0.0 - */ - public function getSchemaVersion($extensionId) - { - $db = $this->getDatabase(); - $extensionId = (int) $extensionId; - $query = $db->getQuery(true) - ->select($db->quoteName('version_id')) - ->from($db->quoteName('#__schemas')) - ->where($db->quoteName('extension_id') . ' = :extensionid') - ->bind(':extensionid', $extensionId, ParameterType::INTEGER); - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Fix schema version if wrong. - * - * @param ChangeSet $changeSet Schema change set. - * @param integer $extensionId ID of the extensions. - * - * @return mixed string schema version if success, false if fail. - * - * @throws \Exception - * - * @since 4.0.0 - */ - public function fixSchemaVersion($changeSet, $extensionId) - { - // Get correct schema version -- last file in array. - $schema = $changeSet->getSchema(); - - // Check value. If ok, don't do update. - if ($schema == $this->getSchemaVersion($extensionId)) - { - return $schema; - } - - // Delete old row. - $extensionId = (int) $extensionId; - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__schemas')) - ->where($db->quoteName('extension_id') . ' = :extensionid') - ->bind(':extensionid', $extensionId, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - - // Add new row. - $query->clear() - ->insert($db->quoteName('#__schemas')) - ->columns($db->quoteName('extension_id') . ',' . $db->quoteName('version_id')) - ->values(':extensionid, :schema') - ->bind(':extensionid', $extensionId, ParameterType::INTEGER) - ->bind(':schema', $schema); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (ExecutionFailureException $e) - { - return false; - } - - return $schema; - } - - /** - * Get current version from #__extensions table. - * - * @param object $extension data from #__extensions of a single extension. - * - * @return mixed string message with the errors with the update version or null if none - * - * @since 4.0.0 - */ - public function compareUpdateVersion($extension) - { - $updateVersion = json_decode($extension->manifest_cache)->version; - - if ($extension->element === 'com_admin') - { - $extensionVersion = JVERSION; - } - else - { - $installationXML = InstallerHelper::getInstallationXML( - $extension->element, - $extension->type, - $extension->client_id, - $extension->type === 'plugin' ? $extension->folder : null - ); - - $extensionVersion = (string) $installationXML->version; - } - - if (version_compare($extensionVersion, $updateVersion) != 0) - { - return Text::sprintf('COM_INSTALLER_MSG_DATABASE_UPDATEVERSION_ERROR', $updateVersion, $extension->name, $extensionVersion); - } - - return null; - } - - /** - * Get a message of the tables skipped and checked - * - * @param array $status status of of the update files - * - * @return array Messages with the errors with the update version - * - * @since 4.0.0 - */ - private function getOtherInformationMessage($status) - { - $problemsMessage = array(); - $problemsMessage[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_CHECKED_OK', count($status['ok'])); - $problemsMessage[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_SKIPPED', count($status['skipped'])); - - return $problemsMessage; - } - - /** - * Get a message with all errors found in a given extension - * - * @param array $errors data from #__extensions of a single extension. - * - * @return array List of messages with the errors in the database - * - * @since 4.0.0 - */ - private function getErrorsMessage($errors) - { - $errorMessages = array(); - - foreach ($errors as $line => $error) - { - $key = 'COM_INSTALLER_MSG_DATABASE_' . $error->queryType; - $messages = $error->msgElements; - $file = basename($error->file); - $message0 = isset($messages[0]) ? $messages[0] : ' '; - $message1 = isset($messages[1]) ? $messages[1] : ' '; - $message2 = isset($messages[2]) ? $messages[2] : ' '; - $errorMessages[] = Text::sprintf($key, $file, $message0, $message1, $message2); - } - - return $errorMessages; - } - - /** - * Fix Joomla version in #__extensions table if wrong (doesn't equal \JVersion short version). - * - * @param integer $extensionId id of the extension - * - * @return mixed string update version if success, false if fail. - * - * @since 4.0.0 - */ - public function fixUpdateVersion($extensionId) - { - $table = new Extension($this->getDatabase()); - $table->load($extensionId); - $cache = new Registry($table->manifest_cache); - $updateVersion = $cache->get('version'); - - if ($table->get('type') === 'file' && $table->get('element') === 'joomla') - { - $extensionVersion = new Version; - $extensionVersion = $extensionVersion->getShortVersion(); - } - else - { - $installationXML = InstallerHelper::getInstallationXML( - $table->get('element'), - $table->get('type'), - $table->get('client_id'), - $table->get('type') === 'plugin' ? $table->get('folder') : null - ); - $extensionVersion = (string) $installationXML->version; - } - - if ($updateVersion === $extensionVersion) - { - return $updateVersion; - } - - $cache->set('version', $extensionVersion); - $table->set('manifest_cache', $cache->toString()); - - if ($table->store()) - { - return $extensionVersion; - } - - return false; - } - - /** - * For version 2.5.x only - * Check if com_config parameters are blank. - * - * @return string default text filters (if any). - * - * @since 4.0.0 - */ - public function getDefaultTextFilters() - { - $table = new Extension($this->getDatabase()); - $table->load($table->find(array('name' => 'com_config'))); - - return $table->params; - } - - /** - * For version 2.5.x only - * Check if com_config parameters are blank. If so, populate with com_content text filters. - * - * @return void - * - * @since 4.0.0 - */ - private function fixDefaultTextFilters() - { - $table = new Extension($this->getDatabase()); - $table->load($table->find(array('name' => 'com_config'))); - - // Check for empty $config and non-empty content filters. - if (!$table->params) - { - // Get filters from com_content and store if you find them. - $contentParams = ComponentHelper::getComponent('com_content')->getParams(); - - if ($contentParams->get('filters')) - { - $newParams = new Registry; - $newParams->set('filters', $contentParams->get('filters')); - $table->params = (string) $newParams; - $table->store(); - } - } - } + /** + * Set the model context + * + * @var string + * + * @since 4.0.0 + */ + protected $_context = 'com_installer.discover'; + + /** + * ChangeSet of all extensions + * + * @var array + * + * @since 4.0.0 + */ + private $changeSetList = array(); + + /** + * Total of errors + * + * @var integer + * + * @since 4.0.0 + */ + private $errorCount = 0; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see ListModel + * @since 4.0.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'update_site_name', + 'name', + 'client_id', + 'client', 'client_translated', + 'status', + 'type', 'type_translated', + 'folder', 'folder_translated', + 'extension_id' + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to return the total number of errors in all the extensions, saved in cache. + * + * @return integer + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function getErrorCount() + { + return $this->errorCount; + } + + /** + * Method to populate the schema cache. + * + * @param integer $cid The extension ID to get the schema for + * + * @return void + * + * @throws \Exception + * + * @since 4.0.0 + */ + private function fetchSchemaCache($cid = 0) + { + // We already have it + if (array_key_exists($cid, $this->changeSetList)) { + return; + } + + // Add the ID to the state so it can be used for filtering + if ($cid) { + $this->setState('filter.extension_id', $cid); + } + + // With the parent::save it can get the limit and we need to make sure it gets all extensions + $results = $this->_getList($this->getListQuery()); + + foreach ($results as $result) { + $errorMessages = array(); + $errorCount = 0; + + if (strcmp($result->element, 'joomla') === 0) { + $result->element = 'com_admin'; + + if (!$this->getDefaultTextFilters()) { + $errorMessages[] = Text::_('COM_INSTALLER_MSG_DATABASE_FILTER_ERROR'); + $errorCount++; + } + } + + $db = $this->getDatabase(); + + if ($result->type === 'component') { + $basePath = JPATH_ADMINISTRATOR . '/components/' . $result->element; + } elseif ($result->type === 'plugin') { + $basePath = JPATH_PLUGINS . '/' . $result->folder . '/' . $result->element; + } elseif ($result->type === 'module') { + // Typehint to integer to normalise some DBs returning strings and others integers + if ((int) $result->client_id === 1) { + $basePath = JPATH_ADMINISTRATOR . '/modules/' . $result->element; + } elseif ((int) $result->client_id === 0) { + $basePath = JPATH_SITE . '/modules/' . $result->element; + } else { + // Module with unknown client id!? - bail + continue; + } + } elseif ($result->type === 'file' && $result->element === 'com_admin') { + // Specific bodge for the Joomla CMS special database check which points to com_admin + $basePath = JPATH_ADMINISTRATOR . '/components/' . $result->element; + } else { + // Unknown extension type (library, files etc which don't have known SQL paths right now) + continue; + } + + // Search the standard SQL Path for the SQL Updates and then if not there check the configuration of the XML + // file. This just gives us a small performance win of not parsing the XML every time. + $folderTmp = $basePath . '/sql/updates/'; + + if (!file_exists($folderTmp)) { + $installationXML = InstallerHelper::getInstallationXML( + $result->element, + $result->type, + $result->client_id, + $result->type === 'plugin' ? $result->folder : null + ); + + if ($installationXML !== null) { + $folderTmp = (string) $installationXML->update->schemas->schemapath[0]; + $a = explode('/', $folderTmp); + array_pop($a); + $folderTmp = $basePath . '/' . implode('/', $a); + } + } + + // Can't find the folder still - give up now and move on. + if (!file_exists($folderTmp)) { + continue; + } + + $changeSet = new ChangeSet($db, $folderTmp); + + // If the version in the #__schemas is different + // than the update files, add to problems message + $schema = $changeSet->getSchema(); + + // If the schema is empty we couldn't find any update files. Just ignore the extension. + if (empty($schema)) { + continue; + } + + if ($result->version_id !== $schema) { + $errorMessages[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_SCHEMA_ERROR', $result->version_id, $schema); + $errorCount++; + } + + // If the version in the manifest_cache is different than the + // version in the installation xml, add to problems message + $compareUpdateMessage = $this->compareUpdateVersion($result); + + if ($compareUpdateMessage) { + $errorMessages[] = $compareUpdateMessage; + $errorCount++; + } + + // If there are errors in the database, add to the problems message + $errors = $changeSet->check(); + + $errorsMessage = $this->getErrorsMessage($errors); + + if ($errorsMessage) { + $errorMessages = array_merge($errorMessages, $errorsMessage); + $errorCount++; + } + + // Number of database tables Checked and Skipped + $errorMessages = array_merge($errorMessages, $this->getOtherInformationMessage($changeSet->getStatus())); + + // Set the total number of errors + $this->errorCount += $errorCount; + + // Collect the extension details + $this->changeSetList[$result->extension_id] = array( + 'folderTmp' => $folderTmp, + 'errorsMessage' => $errorMessages, + 'errorsCount' => $errorCount, + 'results' => $changeSet->getStatus(), + 'schema' => $schema, + 'extension' => $result + ); + } + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'name', $direction = 'asc') + { + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); + $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); + $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); + + parent::populateState($ordering, $direction); + } + + /** + * Fixes database problems. + * + * @param array $cids List of the selected extensions to fix + * + * @return void|boolean + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function fix($cids = array()) + { + $db = $this->getDatabase(); + + foreach ($cids as $i => $cid) { + // Load the database issues + $this->fetchSchemaCache($cid); + + $changeSet = $this->changeSetList[$cid]; + $changeSet['changeset'] = new ChangeSet($db, $changeSet['folderTmp']); + $changeSet['changeset']->fix(); + + $this->fixSchemaVersion($changeSet['changeset'], $changeSet['extension']->extension_id); + $this->fixUpdateVersion($changeSet['extension']->extension_id); + + if ($changeSet['extension']->element === 'com_admin') { + $installer = new \JoomlaInstallerScript(); + $installer->deleteUnexistingFiles(); + $this->fixDefaultTextFilters(); + + /* + * Finally, if the schema updates succeeded, make sure the database table is + * converted to utf8mb4 or, if not supported by the server, compatible to it. + */ + $statusArray = $changeSet['changeset']->getStatus(); + + if (count($statusArray['error']) == 0) { + $installer->convertTablesToUtf8mb4(false); + } + } + } + } + + /** + * Gets the changeset array. + * + * @return array Array with the information of the versions problems, errors and the extensions itself + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function getItems() + { + $this->fetchSchemaCache(); + + $results = parent::getItems(); + $results = $this->mergeSchemaCache($results); + + return $results; + } + + /** + * Method to get the database query + * + * @return DatabaseQuery The database query + * + * @since 4.0.0 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + $db->quoteName( + [ + 'extensions.client_id', + 'extensions.element', + 'extensions.extension_id', + 'extensions.folder', + 'extensions.manifest_cache', + 'extensions.name', + 'extensions.type', + 'schemas.version_id' + ] + ) + ) + ->from( + $db->quoteName( + '#__schemas', + 'schemas' + ) + ) + ->join( + 'INNER', + $db->quoteName('#__extensions', 'extensions'), + $db->quoteName('schemas.extension_id') . ' = ' . $db->quoteName('extensions.extension_id') + ); + + $type = $this->getState('filter.type'); + $clientId = $this->getState('filter.client_id'); + $extensionId = $this->getState('filter.extension_id'); + $folder = $this->getState('filter.folder'); + + if ($type) { + $query->where($db->quoteName('extensions.type') . ' = :type') + ->bind(':type', $type); + } + + if ($clientId != '') { + $clientId = (int) $clientId; + $query->where($db->quoteName('extensions.client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); + } + + if ($extensionId != '') { + $extensionId = (int) $extensionId; + $query->where($db->quoteName('extensions.extension_id') . ' = :extensionid') + ->bind(':extensionid', $extensionId, ParameterType::INTEGER); + } + + if ($folder != '' && in_array($type, array('plugin', 'library', ''))) { + $folder = $folder === '*' ? '' : $folder; + $query->where($db->quoteName('extensions.folder') . ' = :folder') + ->bind(':folder', $folder); + } + + // Process search filter (update site id). + $search = $this->getState('filter.search'); + + if (!empty($search) && stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('schemas.extension_id') . ' = :eid') + ->bind(':eid', $ids, ParameterType::INTEGER); + } + + return $query; + } + + /** + * Merge the items that will be visible with the changeSet information in cache + * + * @param array $results extensions returned from parent::getItems(). + * + * @return array the changeSetList of the merged items + * + * @since 4.0.0 + */ + protected function mergeSchemaCache($results) + { + $changeSetList = $this->changeSetList; + $finalResults = array(); + + foreach ($results as $result) { + if (array_key_exists($result->extension_id, $changeSetList) && $changeSetList[$result->extension_id]) { + $finalResults[] = $changeSetList[$result->extension_id]; + } + } + + return $finalResults; + } + + /** + * Get version from #__schemas table. + * + * @param integer $extensionId id of the extensions. + * + * @return mixed the return value from the query, or null if the query fails. + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function getSchemaVersion($extensionId) + { + $db = $this->getDatabase(); + $extensionId = (int) $extensionId; + $query = $db->getQuery(true) + ->select($db->quoteName('version_id')) + ->from($db->quoteName('#__schemas')) + ->where($db->quoteName('extension_id') . ' = :extensionid') + ->bind(':extensionid', $extensionId, ParameterType::INTEGER); + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Fix schema version if wrong. + * + * @param ChangeSet $changeSet Schema change set. + * @param integer $extensionId ID of the extensions. + * + * @return mixed string schema version if success, false if fail. + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function fixSchemaVersion($changeSet, $extensionId) + { + // Get correct schema version -- last file in array. + $schema = $changeSet->getSchema(); + + // Check value. If ok, don't do update. + if ($schema == $this->getSchemaVersion($extensionId)) { + return $schema; + } + + // Delete old row. + $extensionId = (int) $extensionId; + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__schemas')) + ->where($db->quoteName('extension_id') . ' = :extensionid') + ->bind(':extensionid', $extensionId, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + + // Add new row. + $query->clear() + ->insert($db->quoteName('#__schemas')) + ->columns($db->quoteName('extension_id') . ',' . $db->quoteName('version_id')) + ->values(':extensionid, :schema') + ->bind(':extensionid', $extensionId, ParameterType::INTEGER) + ->bind(':schema', $schema); + $db->setQuery($query); + + try { + $db->execute(); + } catch (ExecutionFailureException $e) { + return false; + } + + return $schema; + } + + /** + * Get current version from #__extensions table. + * + * @param object $extension data from #__extensions of a single extension. + * + * @return mixed string message with the errors with the update version or null if none + * + * @since 4.0.0 + */ + public function compareUpdateVersion($extension) + { + $updateVersion = json_decode($extension->manifest_cache)->version; + + if ($extension->element === 'com_admin') { + $extensionVersion = JVERSION; + } else { + $installationXML = InstallerHelper::getInstallationXML( + $extension->element, + $extension->type, + $extension->client_id, + $extension->type === 'plugin' ? $extension->folder : null + ); + + $extensionVersion = (string) $installationXML->version; + } + + if (version_compare($extensionVersion, $updateVersion) != 0) { + return Text::sprintf('COM_INSTALLER_MSG_DATABASE_UPDATEVERSION_ERROR', $updateVersion, $extension->name, $extensionVersion); + } + + return null; + } + + /** + * Get a message of the tables skipped and checked + * + * @param array $status status of of the update files + * + * @return array Messages with the errors with the update version + * + * @since 4.0.0 + */ + private function getOtherInformationMessage($status) + { + $problemsMessage = array(); + $problemsMessage[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_CHECKED_OK', count($status['ok'])); + $problemsMessage[] = Text::sprintf('COM_INSTALLER_MSG_DATABASE_SKIPPED', count($status['skipped'])); + + return $problemsMessage; + } + + /** + * Get a message with all errors found in a given extension + * + * @param array $errors data from #__extensions of a single extension. + * + * @return array List of messages with the errors in the database + * + * @since 4.0.0 + */ + private function getErrorsMessage($errors) + { + $errorMessages = array(); + + foreach ($errors as $line => $error) { + $key = 'COM_INSTALLER_MSG_DATABASE_' . $error->queryType; + $messages = $error->msgElements; + $file = basename($error->file); + $message0 = isset($messages[0]) ? $messages[0] : ' '; + $message1 = isset($messages[1]) ? $messages[1] : ' '; + $message2 = isset($messages[2]) ? $messages[2] : ' '; + $errorMessages[] = Text::sprintf($key, $file, $message0, $message1, $message2); + } + + return $errorMessages; + } + + /** + * Fix Joomla version in #__extensions table if wrong (doesn't equal \JVersion short version). + * + * @param integer $extensionId id of the extension + * + * @return mixed string update version if success, false if fail. + * + * @since 4.0.0 + */ + public function fixUpdateVersion($extensionId) + { + $table = new Extension($this->getDatabase()); + $table->load($extensionId); + $cache = new Registry($table->manifest_cache); + $updateVersion = $cache->get('version'); + + if ($table->get('type') === 'file' && $table->get('element') === 'joomla') { + $extensionVersion = new Version(); + $extensionVersion = $extensionVersion->getShortVersion(); + } else { + $installationXML = InstallerHelper::getInstallationXML( + $table->get('element'), + $table->get('type'), + $table->get('client_id'), + $table->get('type') === 'plugin' ? $table->get('folder') : null + ); + $extensionVersion = (string) $installationXML->version; + } + + if ($updateVersion === $extensionVersion) { + return $updateVersion; + } + + $cache->set('version', $extensionVersion); + $table->set('manifest_cache', $cache->toString()); + + if ($table->store()) { + return $extensionVersion; + } + + return false; + } + + /** + * For version 2.5.x only + * Check if com_config parameters are blank. + * + * @return string default text filters (if any). + * + * @since 4.0.0 + */ + public function getDefaultTextFilters() + { + $table = new Extension($this->getDatabase()); + $table->load($table->find(array('name' => 'com_config'))); + + return $table->params; + } + + /** + * For version 2.5.x only + * Check if com_config parameters are blank. If so, populate with com_content text filters. + * + * @return void + * + * @since 4.0.0 + */ + private function fixDefaultTextFilters() + { + $table = new Extension($this->getDatabase()); + $table->load($table->find(array('name' => 'com_config'))); + + // Check for empty $config and non-empty content filters. + if (!$table->params) { + // Get filters from com_content and store if you find them. + $contentParams = ComponentHelper::getComponent('com_content')->getParams(); + + if ($contentParams->get('filters')) { + $newParams = new Registry(); + $newParams->set('filters', $contentParams->get('filters')); + $table->params = (string) $newParams; + $table->store(); + } + } + } } diff --git a/administrator/components/com_installer/src/Model/DiscoverModel.php b/administrator/components/com_installer/src/Model/DiscoverModel.php index 425cfca7588b3..ab91eace3c1b6 100644 --- a/administrator/components/com_installer/src/Model/DiscoverModel.php +++ b/administrator/components/com_installer/src/Model/DiscoverModel.php @@ -1,4 +1,5 @@ setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); - $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); - $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); - - $this->setState('message', $app->getUserState('com_installer.message')); - $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); - - $app->setUserState('com_installer.message', ''); - $app->setUserState('com_installer.extension_message', ''); - - parent::populateState($ordering, $direction); - } - - /** - * Method to get the database query. - * - * @return DatabaseQuery The database query - * - * @since 3.1 - */ - protected function getListQuery() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('state') . ' = -1'); - - // Process select filters. - $type = $this->getState('filter.type'); - $clientId = $this->getState('filter.client_id'); - $folder = $this->getState('filter.folder'); - - if ($type) - { - $query->where($db->quoteName('type') . ' = :type') - ->bind(':type', $type); - } - - if ($clientId != '') - { - $clientId = (int) $clientId; - $query->where($db->quoteName('client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); - } - - if ($folder != '' && in_array($type, array('plugin', 'library', ''))) - { - $folder = $folder === '*' ? '' : $folder; - $query->where($db->quoteName('folder') . ' = :folder') - ->bind(':folder', $folder); - } - - // Process search filter. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('extension_id') . ' = :eid') - ->bind(':eid', $ids, ParameterType::INTEGER); - } - } - - // Note: The search for name, ordering and pagination are processed by the parent InstallerModel class (in extension.php). - - return $query; - } - - /** - * Discover extensions. - * - * Finds uninstalled extensions - * - * @return int The count of discovered extensions - * - * @since 1.6 - */ - public function discover() - { - // Purge the list of discovered extensions and fetch them again. - $this->purge(); - $results = Installer::getInstance()->discover(); - - // Get all templates, including discovered ones - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName(['extension_id', 'element', 'folder', 'client_id', 'type'])) - ->from($db->quoteName('#__extensions')); - $db->setQuery($query); - $installedtmp = $db->loadObjectList(); - - $extensions = array(); - - foreach ($installedtmp as $install) - { - $key = implode(':', - [ - $install->type, - str_replace('\\', '/', $install->element), - $install->folder, - $install->client_id - ] - ); - $extensions[$key] = $install; - } - - $count = 0; - - foreach ($results as $result) - { - // Check if we have a match on the element - $key = implode(':', - [ - $result->type, - str_replace('\\', '/', $result->element), - $result->folder, - $result->client_id - ] - ); - - if (!array_key_exists($key, $extensions)) - { - // Put it into the table - $result->check(); - $result->store(); - $count++; - } - } - - return $count; - } - - /** - * Installs a discovered extension. - * - * @return void - * - * @since 1.6 - */ - public function discover_install() - { - $app = Factory::getApplication(); - $input = $app->input; - $eid = $input->get('cid', 0, 'array'); - - if (is_array($eid) || $eid) - { - if (!is_array($eid)) - { - $eid = array($eid); - } - - $eid = ArrayHelper::toInteger($eid); - $failed = false; - - foreach ($eid as $id) - { - $installer = new Installer; - $installer->setDatabase($this->getDatabase()); - - $result = $installer->discover_install($id); - - if (!$result) - { - $failed = true; - $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DISCOVER_INSTALLFAILED') . ': ' . $id); - } - } - - // @todo - We are only receiving the message for the last Installer instance - $this->setState('action', 'remove'); - $this->setState('name', $installer->get('name')); - $app->setUserState('com_installer.message', $installer->message); - $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); - - if (!$failed) - { - $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DISCOVER_INSTALLSUCCESSFUL')); - } - } - else - { - $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DISCOVER_NOEXTENSIONSELECTED')); - } - } - - /** - * Cleans out the list of discovered extensions. - * - * @return boolean True on success - * - * @since 1.6 - */ - public function purge() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__extensions')) - ->where($db->quoteName('state') . ' = -1'); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (ExecutionFailureException $e) - { - $this->_message = Text::_('COM_INSTALLER_MSG_DISCOVER_FAILEDTOPURGEEXTENSIONS'); - - return false; - } - - $this->_message = Text::_('COM_INSTALLER_MSG_DISCOVER_PURGEDDISCOVEREDEXTENSIONS'); - - return true; - } - - /** - * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. - * - * @return DatabaseQuery - * - * @since 4.0.0 - */ - protected function getEmptyStateQuery() - { - $query = parent::getEmptyStateQuery(); - - $query->where($this->getDatabase()->quoteName('state') . ' = -1'); - - return $query; - } - - /** - * Checks for not installed extensions in extensions table. - * - * @return boolean True if there are discovered extensions in the database. - * - * @since 4.2.0 - */ - public function checkExtensions() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('state') . ' = -1'); - $db->setQuery($query); - $discoveredExtensions = $db->loadObjectList(); - - return count($discoveredExtensions) > 0; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\ListModel + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'name', + 'client_id', + 'client', 'client_translated', + 'type', 'type_translated', + 'folder', 'folder_translated', + 'extension_id', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.1 + */ + protected function populateState($ordering = 'name', $direction = 'asc') + { + $app = Factory::getApplication(); + + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); + $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); + $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); + + $this->setState('message', $app->getUserState('com_installer.message')); + $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); + + $app->setUserState('com_installer.message', ''); + $app->setUserState('com_installer.extension_message', ''); + + parent::populateState($ordering, $direction); + } + + /** + * Method to get the database query. + * + * @return DatabaseQuery The database query + * + * @since 3.1 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('state') . ' = -1'); + + // Process select filters. + $type = $this->getState('filter.type'); + $clientId = $this->getState('filter.client_id'); + $folder = $this->getState('filter.folder'); + + if ($type) { + $query->where($db->quoteName('type') . ' = :type') + ->bind(':type', $type); + } + + if ($clientId != '') { + $clientId = (int) $clientId; + $query->where($db->quoteName('client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); + } + + if ($folder != '' && in_array($type, array('plugin', 'library', ''))) { + $folder = $folder === '*' ? '' : $folder; + $query->where($db->quoteName('folder') . ' = :folder') + ->bind(':folder', $folder); + } + + // Process search filter. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('extension_id') . ' = :eid') + ->bind(':eid', $ids, ParameterType::INTEGER); + } + } + + // Note: The search for name, ordering and pagination are processed by the parent InstallerModel class (in extension.php). + + return $query; + } + + /** + * Discover extensions. + * + * Finds uninstalled extensions + * + * @return int The count of discovered extensions + * + * @since 1.6 + */ + public function discover() + { + // Purge the list of discovered extensions and fetch them again. + $this->purge(); + $results = Installer::getInstance()->discover(); + + // Get all templates, including discovered ones + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['extension_id', 'element', 'folder', 'client_id', 'type'])) + ->from($db->quoteName('#__extensions')); + $db->setQuery($query); + $installedtmp = $db->loadObjectList(); + + $extensions = array(); + + foreach ($installedtmp as $install) { + $key = implode( + ':', + [ + $install->type, + str_replace('\\', '/', $install->element), + $install->folder, + $install->client_id + ] + ); + $extensions[$key] = $install; + } + + $count = 0; + + foreach ($results as $result) { + // Check if we have a match on the element + $key = implode( + ':', + [ + $result->type, + str_replace('\\', '/', $result->element), + $result->folder, + $result->client_id + ] + ); + + if (!array_key_exists($key, $extensions)) { + // Put it into the table + $result->check(); + $result->store(); + $count++; + } + } + + return $count; + } + + /** + * Installs a discovered extension. + * + * @return void + * + * @since 1.6 + */ + public function discover_install() + { + $app = Factory::getApplication(); + $input = $app->input; + $eid = $input->get('cid', 0, 'array'); + + if (is_array($eid) || $eid) { + if (!is_array($eid)) { + $eid = array($eid); + } + + $eid = ArrayHelper::toInteger($eid); + $failed = false; + + foreach ($eid as $id) { + $installer = new Installer(); + $installer->setDatabase($this->getDatabase()); + + $result = $installer->discover_install($id); + + if (!$result) { + $failed = true; + $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DISCOVER_INSTALLFAILED') . ': ' . $id); + } + } + + // @todo - We are only receiving the message for the last Installer instance + $this->setState('action', 'remove'); + $this->setState('name', $installer->get('name')); + $app->setUserState('com_installer.message', $installer->message); + $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); + + if (!$failed) { + $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DISCOVER_INSTALLSUCCESSFUL')); + } + } else { + $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DISCOVER_NOEXTENSIONSELECTED')); + } + } + + /** + * Cleans out the list of discovered extensions. + * + * @return boolean True on success + * + * @since 1.6 + */ + public function purge() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__extensions')) + ->where($db->quoteName('state') . ' = -1'); + $db->setQuery($query); + + try { + $db->execute(); + } catch (ExecutionFailureException $e) { + $this->_message = Text::_('COM_INSTALLER_MSG_DISCOVER_FAILEDTOPURGEEXTENSIONS'); + + return false; + } + + $this->_message = Text::_('COM_INSTALLER_MSG_DISCOVER_PURGEDDISCOVEREDEXTENSIONS'); + + return true; + } + + /** + * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. + * + * @return DatabaseQuery + * + * @since 4.0.0 + */ + protected function getEmptyStateQuery() + { + $query = parent::getEmptyStateQuery(); + + $query->where($this->getDatabase()->quoteName('state') . ' = -1'); + + return $query; + } + + /** + * Checks for not installed extensions in extensions table. + * + * @return boolean True if there are discovered extensions in the database. + * + * @since 4.2.0 + */ + public function checkExtensions() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('state') . ' = -1'); + $db->setQuery($query); + $discoveredExtensions = $db->loadObjectList(); + + return count($discoveredExtensions) > 0; + } } diff --git a/administrator/components/com_installer/src/Model/InstallModel.php b/administrator/components/com_installer/src/Model/InstallModel.php index e3ea81ebc0b08..df72b3edb593a 100644 --- a/administrator/components/com_installer/src/Model/InstallModel.php +++ b/administrator/components/com_installer/src/Model/InstallModel.php @@ -1,4 +1,5 @@ setState('message', $app->getUserState('com_installer.message')); - $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); - $app->setUserState('com_installer.message', ''); - $app->setUserState('com_installer.extension_message', ''); - - parent::populateState(); - } - - /** - * Install an extension from either folder, URL or upload. - * - * @return boolean - * - * @since 1.5 - */ - public function install() - { - $this->setState('action', 'install'); - - $app = Factory::getApplication(); - - // Load installer plugins for assistance if required: - PluginHelper::importPlugin('installer'); - - $package = null; - - // This event allows an input pre-treatment, a custom pre-packing or custom installation. - // (e.g. from a \JSON description). - $results = $app->triggerEvent('onInstallerBeforeInstallation', array($this, &$package)); - - if (in_array(true, $results, true)) - { - return true; - } - - if (in_array(false, $results, true)) - { - return false; - } - - $installType = $app->input->getWord('installtype'); - $installLang = $app->input->getWord('package'); - - if ($package === null) - { - switch ($installType) - { - case 'folder': - // Remember the 'Install from Directory' path. - $app->getUserStateFromRequest($this->_context . '.install_directory', 'install_directory'); - $package = $this->_getPackageFromFolder(); - break; - - case 'upload': - $package = $this->_getPackageFromUpload(); - break; - - case 'url': - $package = $this->_getPackageFromUrl(); - break; - - default: - $app->setUserState('com_installer.message', Text::_('COM_INSTALLER_NO_INSTALL_TYPE_FOUND')); - - return false; - } - } - - // This event allows a custom installation of the package or a customization of the package: - $results = $app->triggerEvent('onInstallerBeforeInstaller', array($this, &$package)); - - if (in_array(true, $results, true)) - { - return true; - } - - if (in_array(false, $results, true)) - { - if (in_array($installType, array('upload', 'url'))) - { - InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); - } - - return false; - } - - // Check if package was uploaded successfully. - if (!\is_array($package)) - { - $app->enqueueMessage(Text::_('COM_INSTALLER_UNABLE_TO_FIND_INSTALL_PACKAGE'), 'error'); - - return false; - } - - // Get an installer instance. - $installer = Installer::getInstance(); - - /* - * Check for a Joomla core package. - * To do this we need to set the source path to find the manifest (the same first step as Installer::install()) - * - * This must be done before the unpacked check because InstallerHelper::detectType() returns a boolean false since the manifest - * can't be found in the expected location. - */ - if (isset($package['dir']) && is_dir($package['dir'])) - { - $installer->setPath('source', $package['dir']); - - if (!$installer->findManifest()) - { - // If a manifest isn't found at the source, this may be a Joomla package; check the package directory for the Joomla manifest - if (file_exists($package['dir'] . '/administrator/manifests/files/joomla.xml')) - { - // We have a Joomla package - if (in_array($installType, array('upload', 'url'))) - { - InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); - } - - $app->enqueueMessage( - Text::sprintf('COM_INSTALLER_UNABLE_TO_INSTALL_JOOMLA_PACKAGE', Route::_('index.php?option=com_joomlaupdate')), - 'warning' - ); - - return false; - } - } - } - - // Was the package unpacked? - if (empty($package['type'])) - { - if (in_array($installType, array('upload', 'url'))) - { - InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); - } - - $app->enqueueMessage(Text::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST'), 'error'); - - return false; - } - - // Install the package. - if (!$installer->install($package['dir'])) - { - // There was an error installing the package. - $msg = Text::sprintf('COM_INSTALLER_INSTALL_ERROR', Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type']))); - $result = false; - $msgType = 'error'; - } - else - { - // Package installed successfully. - $msg = Text::sprintf('COM_INSTALLER_INSTALL_SUCCESS', Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($installLang . $package['type']))); - $result = true; - $msgType = 'message'; - } - - // This event allows a custom a post-flight: - $app->triggerEvent('onInstallerAfterInstaller', array($this, &$package, $installer, &$result, &$msg)); - - // Set some model state values. - $app->enqueueMessage($msg, $msgType); - $this->setState('name', $installer->get('name')); - $this->setState('result', $result); - $app->setUserState('com_installer.message', $installer->message); - $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); - $app->setUserState('com_installer.redirect_url', $installer->get('redirect_url')); - - // Cleanup the install files. - if (!is_file($package['packagefile'])) - { - $package['packagefile'] = $app->get('tmp_path') . '/' . $package['packagefile']; - } - - InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); - - // Clear the cached extension data and menu cache - $this->cleanCache('_system'); - $this->cleanCache('com_modules'); - $this->cleanCache('com_plugins'); - $this->cleanCache('mod_menu'); - - return $result; - } - - /** - * Works out an installation package from a HTTP upload. - * - * @return mixed Package definition or false on failure. - */ - protected function _getPackageFromUpload() - { - // Get the uploaded file information. - $input = Factory::getApplication()->input; - - // Do not change the filter type 'raw'. We need this to let files containing PHP code to upload. See \JInputFiles::get. - $userfile = $input->files->get('install_package', null, 'raw'); - - // Make sure that file uploads are enabled in php. - if (!(bool) ini_get('file_uploads')) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLFILE'), 'error'); - - return false; - } - - // Make sure that zlib is loaded so that the package can be unpacked. - if (!extension_loaded('zlib')) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLZLIB'), 'error'); - - return false; - } - - // If there is no uploaded file, we have a problem... - if (!is_array($userfile)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_NO_FILE_SELECTED'), 'error'); - - return false; - } - - // Is the PHP tmp directory missing? - if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_NO_TMP_DIR)) - { - Factory::getApplication()->enqueueMessage( - Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
' . Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSET'), - 'error' - ); - - return false; - } - - // Is the max upload size too small in php.ini? - if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_INI_SIZE)) - { - Factory::getApplication()->enqueueMessage( - Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
' . Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZE'), - 'error' - ); - - return false; - } - - // Check if there was a different problem uploading the file. - if ($userfile['error'] || $userfile['size'] < 1) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR'), 'error'); - - return false; - } - - // Build the appropriate paths. - $config = Factory::getApplication()->getConfig(); - $tmp_dest = $config->get('tmp_path') . '/' . $userfile['name']; - $tmp_src = $userfile['tmp_name']; - - // Move uploaded file. - File::upload($tmp_src, $tmp_dest, false, true); - - // Unpack the downloaded package file. - $package = InstallerHelper::unpack($tmp_dest, true); - - return $package; - } - - /** - * Install an extension from a directory - * - * @return array Package details or false on failure - * - * @since 1.5 - */ - protected function _getPackageFromFolder() - { - $input = Factory::getApplication()->input; - - // Get the path to the package to install. - $p_dir = $input->getString('install_directory'); - $p_dir = Path::clean($p_dir); - - // Did you give us a valid directory? - if (!is_dir($p_dir)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_PLEASE_ENTER_A_PACKAGE_DIRECTORY'), 'error'); - - return false; - } - - // Detect the package type - $type = InstallerHelper::detectType($p_dir); - - // Did you give us a valid package? - if (!$type) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_PATH_DOES_NOT_HAVE_A_VALID_PACKAGE'), 'error'); - } - - $package['packagefile'] = null; - $package['extractdir'] = null; - $package['dir'] = $p_dir; - $package['type'] = $type; - - return $package; - } - - /** - * Install an extension from a URL. - * - * @return bool|array Package details or false on failure. - * - * @since 1.5 - */ - protected function _getPackageFromUrl() - { - $input = Factory::getApplication()->input; - - // Get the URL of the package to install. - $url = $input->getString('install_url'); - - // Did you give us a URL? - if (!$url) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_ENTER_A_URL'), 'error'); - - return false; - } - - // We only allow http & https here - $uri = new Uri($url); - - if (!in_array($uri->getScheme(), ['http', 'https'])) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_INVALID_URL_SCHEME'), 'error'); - - return false; - } - - // Handle updater XML file case: - if (preg_match('/\.xml\s*$/', $url)) - { - $update = new Update; - $update->loadFromXml($url); - $package_url = trim($update->get('downloadurl', false)->_data); - - if ($package_url) - { - $url = $package_url; - } - - unset($update); - } - - // Download the package at the URL given. - $p_file = InstallerHelper::downloadPackage($url); - - // Was the package downloaded? - if (!$p_file) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_INVALID_URL'), 'error'); - - return false; - } - - $tmp_dest = Factory::getApplication()->get('tmp_path'); - - // Unpack the downloaded package file. - $package = InstallerHelper::unpack($tmp_dest . '/' . $p_file, true); - - return $package; - } + /** + * @var \Joomla\CMS\Table\Table Table object + */ + protected $_table = null; + + /** + * @var string URL + */ + protected $_url = null; + + /** + * Model context string. + * + * @var string + */ + protected $_context = 'com_installer.install'; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + $this->setState('message', $app->getUserState('com_installer.message')); + $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); + $app->setUserState('com_installer.message', ''); + $app->setUserState('com_installer.extension_message', ''); + + parent::populateState(); + } + + /** + * Install an extension from either folder, URL or upload. + * + * @return boolean + * + * @since 1.5 + */ + public function install() + { + $this->setState('action', 'install'); + + $app = Factory::getApplication(); + + // Load installer plugins for assistance if required: + PluginHelper::importPlugin('installer'); + + $package = null; + + // This event allows an input pre-treatment, a custom pre-packing or custom installation. + // (e.g. from a \JSON description). + $results = $app->triggerEvent('onInstallerBeforeInstallation', array($this, &$package)); + + if (in_array(true, $results, true)) { + return true; + } + + if (in_array(false, $results, true)) { + return false; + } + + $installType = $app->input->getWord('installtype'); + $installLang = $app->input->getWord('package'); + + if ($package === null) { + switch ($installType) { + case 'folder': + // Remember the 'Install from Directory' path. + $app->getUserStateFromRequest($this->_context . '.install_directory', 'install_directory'); + $package = $this->_getPackageFromFolder(); + break; + + case 'upload': + $package = $this->_getPackageFromUpload(); + break; + + case 'url': + $package = $this->_getPackageFromUrl(); + break; + + default: + $app->setUserState('com_installer.message', Text::_('COM_INSTALLER_NO_INSTALL_TYPE_FOUND')); + + return false; + } + } + + // This event allows a custom installation of the package or a customization of the package: + $results = $app->triggerEvent('onInstallerBeforeInstaller', array($this, &$package)); + + if (in_array(true, $results, true)) { + return true; + } + + if (in_array(false, $results, true)) { + if (in_array($installType, array('upload', 'url'))) { + InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); + } + + return false; + } + + // Check if package was uploaded successfully. + if (!\is_array($package)) { + $app->enqueueMessage(Text::_('COM_INSTALLER_UNABLE_TO_FIND_INSTALL_PACKAGE'), 'error'); + + return false; + } + + // Get an installer instance. + $installer = Installer::getInstance(); + + /* + * Check for a Joomla core package. + * To do this we need to set the source path to find the manifest (the same first step as Installer::install()) + * + * This must be done before the unpacked check because InstallerHelper::detectType() returns a boolean false since the manifest + * can't be found in the expected location. + */ + if (isset($package['dir']) && is_dir($package['dir'])) { + $installer->setPath('source', $package['dir']); + + if (!$installer->findManifest()) { + // If a manifest isn't found at the source, this may be a Joomla package; check the package directory for the Joomla manifest + if (file_exists($package['dir'] . '/administrator/manifests/files/joomla.xml')) { + // We have a Joomla package + if (in_array($installType, array('upload', 'url'))) { + InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); + } + + $app->enqueueMessage( + Text::sprintf('COM_INSTALLER_UNABLE_TO_INSTALL_JOOMLA_PACKAGE', Route::_('index.php?option=com_joomlaupdate')), + 'warning' + ); + + return false; + } + } + } + + // Was the package unpacked? + if (empty($package['type'])) { + if (in_array($installType, array('upload', 'url'))) { + InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); + } + + $app->enqueueMessage(Text::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST'), 'error'); + + return false; + } + + // Install the package. + if (!$installer->install($package['dir'])) { + // There was an error installing the package. + $msg = Text::sprintf('COM_INSTALLER_INSTALL_ERROR', Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type']))); + $result = false; + $msgType = 'error'; + } else { + // Package installed successfully. + $msg = Text::sprintf('COM_INSTALLER_INSTALL_SUCCESS', Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($installLang . $package['type']))); + $result = true; + $msgType = 'message'; + } + + // This event allows a custom a post-flight: + $app->triggerEvent('onInstallerAfterInstaller', array($this, &$package, $installer, &$result, &$msg)); + + // Set some model state values. + $app->enqueueMessage($msg, $msgType); + $this->setState('name', $installer->get('name')); + $this->setState('result', $result); + $app->setUserState('com_installer.message', $installer->message); + $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); + $app->setUserState('com_installer.redirect_url', $installer->get('redirect_url')); + + // Cleanup the install files. + if (!is_file($package['packagefile'])) { + $package['packagefile'] = $app->get('tmp_path') . '/' . $package['packagefile']; + } + + InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); + + // Clear the cached extension data and menu cache + $this->cleanCache('_system'); + $this->cleanCache('com_modules'); + $this->cleanCache('com_plugins'); + $this->cleanCache('mod_menu'); + + return $result; + } + + /** + * Works out an installation package from a HTTP upload. + * + * @return mixed Package definition or false on failure. + */ + protected function _getPackageFromUpload() + { + // Get the uploaded file information. + $input = Factory::getApplication()->input; + + // Do not change the filter type 'raw'. We need this to let files containing PHP code to upload. See \JInputFiles::get. + $userfile = $input->files->get('install_package', null, 'raw'); + + // Make sure that file uploads are enabled in php. + if (!(bool) ini_get('file_uploads')) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLFILE'), 'error'); + + return false; + } + + // Make sure that zlib is loaded so that the package can be unpacked. + if (!extension_loaded('zlib')) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLZLIB'), 'error'); + + return false; + } + + // If there is no uploaded file, we have a problem... + if (!is_array($userfile)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_NO_FILE_SELECTED'), 'error'); + + return false; + } + + // Is the PHP tmp directory missing? + if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_NO_TMP_DIR)) { + Factory::getApplication()->enqueueMessage( + Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
' . Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSET'), + 'error' + ); + + return false; + } + + // Is the max upload size too small in php.ini? + if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_INI_SIZE)) { + Factory::getApplication()->enqueueMessage( + Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
' . Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZE'), + 'error' + ); + + return false; + } + + // Check if there was a different problem uploading the file. + if ($userfile['error'] || $userfile['size'] < 1) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR'), 'error'); + + return false; + } + + // Build the appropriate paths. + $config = Factory::getApplication()->getConfig(); + $tmp_dest = $config->get('tmp_path') . '/' . $userfile['name']; + $tmp_src = $userfile['tmp_name']; + + // Move uploaded file. + File::upload($tmp_src, $tmp_dest, false, true); + + // Unpack the downloaded package file. + $package = InstallerHelper::unpack($tmp_dest, true); + + return $package; + } + + /** + * Install an extension from a directory + * + * @return array Package details or false on failure + * + * @since 1.5 + */ + protected function _getPackageFromFolder() + { + $input = Factory::getApplication()->input; + + // Get the path to the package to install. + $p_dir = $input->getString('install_directory'); + $p_dir = Path::clean($p_dir); + + // Did you give us a valid directory? + if (!is_dir($p_dir)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_PLEASE_ENTER_A_PACKAGE_DIRECTORY'), 'error'); + + return false; + } + + // Detect the package type + $type = InstallerHelper::detectType($p_dir); + + // Did you give us a valid package? + if (!$type) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_PATH_DOES_NOT_HAVE_A_VALID_PACKAGE'), 'error'); + } + + $package['packagefile'] = null; + $package['extractdir'] = null; + $package['dir'] = $p_dir; + $package['type'] = $type; + + return $package; + } + + /** + * Install an extension from a URL. + * + * @return bool|array Package details or false on failure. + * + * @since 1.5 + */ + protected function _getPackageFromUrl() + { + $input = Factory::getApplication()->input; + + // Get the URL of the package to install. + $url = $input->getString('install_url'); + + // Did you give us a URL? + if (!$url) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_ENTER_A_URL'), 'error'); + + return false; + } + + // We only allow http & https here + $uri = new Uri($url); + + if (!in_array($uri->getScheme(), ['http', 'https'])) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_INVALID_URL_SCHEME'), 'error'); + + return false; + } + + // Handle updater XML file case: + if (preg_match('/\.xml\s*$/', $url)) { + $update = new Update(); + $update->loadFromXml($url); + $package_url = trim($update->get('downloadurl', false)->_data); + + if ($package_url) { + $url = $package_url; + } + + unset($update); + } + + // Download the package at the URL given. + $p_file = InstallerHelper::downloadPackage($url); + + // Was the package downloaded? + if (!$p_file) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_INSTALL_INVALID_URL'), 'error'); + + return false; + } + + $tmp_dest = Factory::getApplication()->get('tmp_path'); + + // Unpack the downloaded package file. + $package = InstallerHelper::unpack($tmp_dest . '/' . $p_file, true); + + return $package; + } } diff --git a/administrator/components/com_installer/src/Model/InstallerModel.php b/administrator/components/com_installer/src/Model/InstallerModel.php index cf1a350f7d16b..fdf78e5b23f55 100644 --- a/administrator/components/com_installer/src/Model/InstallerModel.php +++ b/administrator/components/com_installer/src/Model/InstallerModel.php @@ -1,4 +1,5 @@ getState('list.ordering', 'name'); - $listDirn = $this->getState('list.direction', 'asc'); - - // Replace slashes so preg_match will work - $search = $this->getState('filter.search'); - $search = str_replace('/', ' ', $search); - $db = $this->getDatabase(); - - // Define which fields have to be processed in a custom way because of translation. - $customOrderFields = array('name', 'client_translated', 'type_translated', 'folder_translated', 'creationDate'); - - // Process searching, ordering and pagination for fields that need to be translated. - if (in_array($listOrder, $customOrderFields) || (!empty($search) && stripos($search, 'id:') !== 0)) - { - // Get results from database and translate them. - $db->setQuery($query); - $result = $db->loadObjectList(); - $this->translate($result); - - // Process searching. - if (!empty($search) && stripos($search, 'id:') !== 0) - { - $escapedSearchString = $this->refineSearchStringToRegex($search, '/'); - - // By default search only the extension name field. - $searchFields = array('name'); - - // If in update sites view search also in the update site name field. - if ($this instanceof UpdatesitesModel) - { - $searchFields[] = 'update_site_name'; - } - - foreach ($result as $i => $item) - { - // Check if search string exists in any of the fields to be searched. - $found = 0; - - foreach ($searchFields as $key => $field) - { - if (!$found && preg_match('/' . $escapedSearchString . '/i', $item->{$field})) - { - $found = 1; - } - } - - // If search string was not found in any of the fields searched remove it from results array. - if (!$found) - { - unset($result[$i]); - } - } - } - - // Process ordering. - // Sort array object by selected ordering and selected direction. Sort is case insensitive and using locale sorting. - $result = ArrayHelper::sortObjects($result, $listOrder, strtolower($listDirn) == 'desc' ? -1 : 1, false, true); - - // Process pagination. - $total = count($result); - $this->cache[$this->getStoreId('getTotal')] = $total; - - if ($total <= $limitstart) - { - $limitstart = 0; - $this->setState('list.limitstart', 0); - } - - return array_slice($result, $limitstart, $limit ?: null); - } - - // Process searching, ordering and pagination for regular database fields. - $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); - $result = parent::_getList($query, $limitstart, $limit); - $this->translate($result); - - return $result; - } - - /** - * Translate a list of objects - * - * @param array $items The array of objects - * - * @return array The array of translated objects - */ - protected function translate(&$items) - { - $lang = Factory::getLanguage(); - - foreach ($items as &$item) - { - if (strlen($item->manifest_cache) && $data = json_decode($item->manifest_cache)) - { - foreach ($data as $key => $value) - { - if ($key == 'type') - { - // Ignore the type field - continue; - } - - $item->$key = $value; - } - } - - $item->author_info = @$item->authorEmail . '
' . @$item->authorUrl; - $item->client = Text::_([0 => 'JSITE', 1 => 'JADMINISTRATOR', 3 => 'JAPI'][$item->client_id] ?? 'JSITE'); - $item->client_translated = $item->client; - $item->type_translated = Text::_('COM_INSTALLER_TYPE_' . strtoupper($item->type)); - $item->folder_translated = @$item->folder ? $item->folder : Text::_('COM_INSTALLER_TYPE_NONAPPLICABLE'); - - $path = $item->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE; - - switch ($item->type) - { - case 'component': - $extension = $item->element; - $source = JPATH_ADMINISTRATOR . '/components/' . $extension; - $lang->load("$extension.sys", JPATH_ADMINISTRATOR) || $lang->load("$extension.sys", $source); - break; - case 'file': - $extension = 'files_' . $item->element; - $lang->load("$extension.sys", JPATH_SITE); - break; - case 'library': - $parts = explode('/', $item->element); - $vendor = (isset($parts[1]) ? $parts[0] : null); - $extension = 'lib_' . ($vendor ? implode('_', $parts) : $item->element); - - if (!$lang->load("$extension.sys", $path)) - { - $source = $path . '/libraries/' . ($vendor ? $vendor . '/' . $parts[1] : $item->element); - $lang->load("$extension.sys", $source); - } - break; - case 'module': - $extension = $item->element; - $source = $path . '/modules/' . $extension; - $lang->load("$extension.sys", $path) || $lang->load("$extension.sys", $source); - break; - case 'plugin': - $extension = 'plg_' . $item->folder . '_' . $item->element; - $source = JPATH_PLUGINS . '/' . $item->folder . '/' . $item->element; - $lang->load("$extension.sys", JPATH_ADMINISTRATOR) || $lang->load("$extension.sys", $source); - break; - case 'template': - $extension = 'tpl_' . $item->element; - $source = $path . '/templates/' . $item->element; - $lang->load("$extension.sys", $path) || $lang->load("$extension.sys", $source); - break; - case 'package': - default: - $extension = $item->element; - $lang->load("$extension.sys", JPATH_SITE); - break; - } - - // Translate the extension name if possible - $item->name = Text::_($item->name); - - settype($item->description, 'string'); - - if (!in_array($item->type, array('language'))) - { - $item->description = Text::_($item->description); - } - } - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\ListModel + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'name', + 'client_id', + 'client', 'client_translated', + 'enabled', + 'type', 'type_translated', + 'folder', 'folder_translated', + 'extension_id', + 'creationDate', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Returns an object list + * + * @param DatabaseQuery $query The query + * @param int $limitstart Offset + * @param int $limit The number of records + * + * @return array + */ + protected function _getList($query, $limitstart = 0, $limit = 0) + { + $listOrder = $this->getState('list.ordering', 'name'); + $listDirn = $this->getState('list.direction', 'asc'); + + // Replace slashes so preg_match will work + $search = $this->getState('filter.search'); + $search = str_replace('/', ' ', $search); + $db = $this->getDatabase(); + + // Define which fields have to be processed in a custom way because of translation. + $customOrderFields = array('name', 'client_translated', 'type_translated', 'folder_translated', 'creationDate'); + + // Process searching, ordering and pagination for fields that need to be translated. + if (in_array($listOrder, $customOrderFields) || (!empty($search) && stripos($search, 'id:') !== 0)) { + // Get results from database and translate them. + $db->setQuery($query); + $result = $db->loadObjectList(); + $this->translate($result); + + // Process searching. + if (!empty($search) && stripos($search, 'id:') !== 0) { + $escapedSearchString = $this->refineSearchStringToRegex($search, '/'); + + // By default search only the extension name field. + $searchFields = array('name'); + + // If in update sites view search also in the update site name field. + if ($this instanceof UpdatesitesModel) { + $searchFields[] = 'update_site_name'; + } + + foreach ($result as $i => $item) { + // Check if search string exists in any of the fields to be searched. + $found = 0; + + foreach ($searchFields as $key => $field) { + if (!$found && preg_match('/' . $escapedSearchString . '/i', $item->{$field})) { + $found = 1; + } + } + + // If search string was not found in any of the fields searched remove it from results array. + if (!$found) { + unset($result[$i]); + } + } + } + + // Process ordering. + // Sort array object by selected ordering and selected direction. Sort is case insensitive and using locale sorting. + $result = ArrayHelper::sortObjects($result, $listOrder, strtolower($listDirn) == 'desc' ? -1 : 1, false, true); + + // Process pagination. + $total = count($result); + $this->cache[$this->getStoreId('getTotal')] = $total; + + if ($total <= $limitstart) { + $limitstart = 0; + $this->setState('list.limitstart', 0); + } + + return array_slice($result, $limitstart, $limit ?: null); + } + + // Process searching, ordering and pagination for regular database fields. + $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); + $result = parent::_getList($query, $limitstart, $limit); + $this->translate($result); + + return $result; + } + + /** + * Translate a list of objects + * + * @param array $items The array of objects + * + * @return array The array of translated objects + */ + protected function translate(&$items) + { + $lang = Factory::getLanguage(); + + foreach ($items as &$item) { + if (strlen($item->manifest_cache) && $data = json_decode($item->manifest_cache)) { + foreach ($data as $key => $value) { + if ($key == 'type') { + // Ignore the type field + continue; + } + + $item->$key = $value; + } + } + + $item->author_info = @$item->authorEmail . '
' . @$item->authorUrl; + $item->client = Text::_([0 => 'JSITE', 1 => 'JADMINISTRATOR', 3 => 'JAPI'][$item->client_id] ?? 'JSITE'); + $item->client_translated = $item->client; + $item->type_translated = Text::_('COM_INSTALLER_TYPE_' . strtoupper($item->type)); + $item->folder_translated = @$item->folder ? $item->folder : Text::_('COM_INSTALLER_TYPE_NONAPPLICABLE'); + + $path = $item->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE; + + switch ($item->type) { + case 'component': + $extension = $item->element; + $source = JPATH_ADMINISTRATOR . '/components/' . $extension; + $lang->load("$extension.sys", JPATH_ADMINISTRATOR) || $lang->load("$extension.sys", $source); + break; + case 'file': + $extension = 'files_' . $item->element; + $lang->load("$extension.sys", JPATH_SITE); + break; + case 'library': + $parts = explode('/', $item->element); + $vendor = (isset($parts[1]) ? $parts[0] : null); + $extension = 'lib_' . ($vendor ? implode('_', $parts) : $item->element); + + if (!$lang->load("$extension.sys", $path)) { + $source = $path . '/libraries/' . ($vendor ? $vendor . '/' . $parts[1] : $item->element); + $lang->load("$extension.sys", $source); + } + break; + case 'module': + $extension = $item->element; + $source = $path . '/modules/' . $extension; + $lang->load("$extension.sys", $path) || $lang->load("$extension.sys", $source); + break; + case 'plugin': + $extension = 'plg_' . $item->folder . '_' . $item->element; + $source = JPATH_PLUGINS . '/' . $item->folder . '/' . $item->element; + $lang->load("$extension.sys", JPATH_ADMINISTRATOR) || $lang->load("$extension.sys", $source); + break; + case 'template': + $extension = 'tpl_' . $item->element; + $source = $path . '/templates/' . $item->element; + $lang->load("$extension.sys", $path) || $lang->load("$extension.sys", $source); + break; + case 'package': + default: + $extension = $item->element; + $lang->load("$extension.sys", JPATH_SITE); + break; + } + + // Translate the extension name if possible + $item->name = Text::_($item->name); + + settype($item->description, 'string'); + + if (!in_array($item->type, array('language'))) { + $item->description = Text::_($item->description); + } + } + } } diff --git a/administrator/components/com_installer/src/Model/LanguagesModel.php b/administrator/components/com_installer/src/Model/LanguagesModel.php index 12bdde329517f..7a902f41aecba 100644 --- a/administrator/components/com_installer/src/Model/LanguagesModel.php +++ b/administrator/components/com_installer/src/Model/LanguagesModel.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('us.location')) - ->from($db->quoteName('#__extensions', 'e')) - ->where($db->quoteName('e.type') . ' = ' . $db->quote('package')) - ->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_en-GB')) - ->where($db->quoteName('e.client_id') . ' = 0') - ->join( - 'LEFT', $db->quoteName('#__update_sites_extensions', 'use') - . ' ON ' . $db->quoteName('use.extension_id') . ' = ' . $db->quoteName('e.extension_id') - ) - ->join( - 'LEFT', $db->quoteName('#__update_sites', 'us') - . ' ON ' . $db->quoteName('us.update_site_id') . ' = ' . $db->quoteName('use.update_site_id') - ); - - return $db->setQuery($query)->loadResult(); - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 3.7.0 - */ - public function getItems() - { - // Get a storage key. - $store = $this->getStoreId(); - - // Try to load the data from internal storage. - if (isset($this->cache[$store])) - { - return $this->cache[$store]; - } - - try - { - // Load the list items and add the items to the internal cache. - $this->cache[$store] = $this->getLanguages(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return $this->cache[$store]; - } - - /** - * Gets an array of objects from the updatesite. - * - * @return object[] An array of results. - * - * @since 3.0 - * @throws \RuntimeException - */ - protected function getLanguages() - { - $updateSite = $this->getUpdateSite(); - - // Check whether the updateserver is found - if (empty($updateSite)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_WARNING_NO_LANGUAGES_UPDATESERVER'), 'warning'); - - return; - } - - try - { - $response = HttpFactory::getHttp()->get($updateSite); - } - catch (\RuntimeException $e) - { - $response = null; - } - - if ($response === null || $response->code !== 200) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_ERROR_CANT_CONNECT_TO_UPDATESERVER', $updateSite), 'error'); - - return; - } - - $updateSiteXML = simplexml_load_string($response->body); - - if (!$updateSiteXML) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_ERROR_CANT_RETRIEVE_XML', $updateSite), 'error'); - - return; - } - - $languages = array(); - $search = strtolower($this->getState('filter.search')); - - foreach ($updateSiteXML->extension as $extension) - { - $language = new \stdClass; - - foreach ($extension->attributes() as $key => $value) - { - $language->$key = (string) $value; - } - - if ($search) - { - if (strpos(strtolower($language->name), $search) === false - && strpos(strtolower($language->element), $search) === false) - { - continue; - } - } - - $languages[$language->name] = $language; - } - - // Workaround for php 5.3 - $that = $this; - - // Sort the array by value of subarray - usort( - $languages, - function ($a, $b) use ($that) - { - $ordering = $that->getState('list.ordering'); - - if (strtolower($that->getState('list.direction')) === 'asc') - { - return StringHelper::strcmp($a->$ordering, $b->$ordering); - } - else - { - return StringHelper::strcmp($b->$ordering, $a->$ordering); - } - } - ); - - // Count the non-paginated list - $this->languageCount = count($languages); - $limit = ($this->getState('list.limit') > 0) ? $this->getState('list.limit') : $this->languageCount; - - return array_slice($languages, $this->getStart(), $limit); - } - - /** - * Returns a record count for the updatesite. - * - * @param \Joomla\Database\DatabaseQuery|string $query The query. - * - * @return integer Number of rows for query. - * - * @since 3.7.0 - */ - protected function _getListCount($query) - { - return $this->languageCount; - } - - /** - * Method to get a store id based on model configuration state. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 2.5.7 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - - return parent::getStoreId($id); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering list order - * @param string $direction direction in the list - * - * @return void - * - * @since 2.5.7 - */ - protected function populateState($ordering = 'name', $direction = 'asc') - { - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - - $this->setState('extension_message', Factory::getApplication()->getUserState('com_installer.extension_message')); - - parent::populateState($ordering, $direction); - } - - /** - * Method to compare two languages in order to sort them. - * - * @param object $lang1 The first language. - * @param object $lang2 The second language. - * - * @return integer - * - * @since 3.7.0 - */ - protected function compareLanguages($lang1, $lang2) - { - return strcmp($lang1->name, $lang2->name); - } + /** + * Language count + * + * @var integer + * @since 3.7.0 + */ + private $languageCount; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\ListModel + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'name', + 'element', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Get the Update Site + * + * @since 3.7.0 + * + * @return string The URL of the Accredited Languagepack Updatesite XML + */ + private function getUpdateSite() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('us.location')) + ->from($db->quoteName('#__extensions', 'e')) + ->where($db->quoteName('e.type') . ' = ' . $db->quote('package')) + ->where($db->quoteName('e.element') . ' = ' . $db->quote('pkg_en-GB')) + ->where($db->quoteName('e.client_id') . ' = 0') + ->join( + 'LEFT', + $db->quoteName('#__update_sites_extensions', 'use') + . ' ON ' . $db->quoteName('use.extension_id') . ' = ' . $db->quoteName('e.extension_id') + ) + ->join( + 'LEFT', + $db->quoteName('#__update_sites', 'us') + . ' ON ' . $db->quoteName('us.update_site_id') . ' = ' . $db->quoteName('use.update_site_id') + ); + + return $db->setQuery($query)->loadResult(); + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 3.7.0 + */ + public function getItems() + { + // Get a storage key. + $store = $this->getStoreId(); + + // Try to load the data from internal storage. + if (isset($this->cache[$store])) { + return $this->cache[$store]; + } + + try { + // Load the list items and add the items to the internal cache. + $this->cache[$store] = $this->getLanguages(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return $this->cache[$store]; + } + + /** + * Gets an array of objects from the updatesite. + * + * @return object[] An array of results. + * + * @since 3.0 + * @throws \RuntimeException + */ + protected function getLanguages() + { + $updateSite = $this->getUpdateSite(); + + // Check whether the updateserver is found + if (empty($updateSite)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_WARNING_NO_LANGUAGES_UPDATESERVER'), 'warning'); + + return; + } + + try { + $response = HttpFactory::getHttp()->get($updateSite); + } catch (\RuntimeException $e) { + $response = null; + } + + if ($response === null || $response->code !== 200) { + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_ERROR_CANT_CONNECT_TO_UPDATESERVER', $updateSite), 'error'); + + return; + } + + $updateSiteXML = simplexml_load_string($response->body); + + if (!$updateSiteXML) { + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_ERROR_CANT_RETRIEVE_XML', $updateSite), 'error'); + + return; + } + + $languages = array(); + $search = strtolower($this->getState('filter.search')); + + foreach ($updateSiteXML->extension as $extension) { + $language = new \stdClass(); + + foreach ($extension->attributes() as $key => $value) { + $language->$key = (string) $value; + } + + if ($search) { + if ( + strpos(strtolower($language->name), $search) === false + && strpos(strtolower($language->element), $search) === false + ) { + continue; + } + } + + $languages[$language->name] = $language; + } + + // Workaround for php 5.3 + $that = $this; + + // Sort the array by value of subarray + usort( + $languages, + function ($a, $b) use ($that) { + $ordering = $that->getState('list.ordering'); + + if (strtolower($that->getState('list.direction')) === 'asc') { + return StringHelper::strcmp($a->$ordering, $b->$ordering); + } else { + return StringHelper::strcmp($b->$ordering, $a->$ordering); + } + } + ); + + // Count the non-paginated list + $this->languageCount = count($languages); + $limit = ($this->getState('list.limit') > 0) ? $this->getState('list.limit') : $this->languageCount; + + return array_slice($languages, $this->getStart(), $limit); + } + + /** + * Returns a record count for the updatesite. + * + * @param \Joomla\Database\DatabaseQuery|string $query The query. + * + * @return integer Number of rows for query. + * + * @since 3.7.0 + */ + protected function _getListCount($query) + { + return $this->languageCount; + } + + /** + * Method to get a store id based on model configuration state. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 2.5.7 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + + return parent::getStoreId($id); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering list order + * @param string $direction direction in the list + * + * @return void + * + * @since 2.5.7 + */ + protected function populateState($ordering = 'name', $direction = 'asc') + { + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + + $this->setState('extension_message', Factory::getApplication()->getUserState('com_installer.extension_message')); + + parent::populateState($ordering, $direction); + } + + /** + * Method to compare two languages in order to sort them. + * + * @param object $lang1 The first language. + * @param object $lang2 The second language. + * + * @return integer + * + * @since 3.7.0 + */ + protected function compareLanguages($lang1, $lang2) + { + return strcmp($lang1->name, $lang2->name); + } } diff --git a/administrator/components/com_installer/src/Model/ManageModel.php b/administrator/components/com_installer/src/Model/ManageModel.php index 6832a5e01bd14..7c74397f25b23 100644 --- a/administrator/components/com_installer/src/Model/ManageModel.php +++ b/administrator/components/com_installer/src/Model/ManageModel.php @@ -1,4 +1,5 @@ setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); - $this->setState('filter.package_id', $this->getUserStateFromRequest($this->context . '.filter.package_id', 'filter_package_id', null, 'int')); - $this->setState('filter.status', $this->getUserStateFromRequest($this->context . '.filter.status', 'filter_status', '', 'string')); - $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); - $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); - $this->setState('filter.core', $this->getUserStateFromRequest($this->context . '.filter.core', 'filter_core', '', 'string')); - - $this->setState('message', $app->getUserState('com_installer.message')); - $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); - $app->setUserState('com_installer.message', ''); - $app->setUserState('com_installer.extension_message', ''); - - parent::populateState($ordering, $direction); - } - - /** - * Enable/Disable an extension. - * - * @param array $eid Extension ids to un/publish - * @param int $value Publish value - * - * @return boolean True on success - * - * @throws \Exception - * - * @since 1.5 - */ - public function publish(&$eid = array(), $value = 1) - { - if (!Factory::getUser()->authorise('core.edit.state', 'com_installer')) - { - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); - - return false; - } - - $result = true; - - /* - * Ensure eid is an array of extension ids - * @todo: If it isn't an array do we want to set an error and fail? - */ - if (!is_array($eid)) - { - $eid = array($eid); - } - - // Get a table object for the extension type - $table = new Extension($this->getDatabase()); - - // Enable the extension in the table and store it in the database - foreach ($eid as $i => $id) - { - $table->load($id); - - if ($table->type == 'template') - { - $style = new StyleTable($this->getDatabase()); - - if ($style->load(array('template' => $table->element, 'client_id' => $table->client_id, 'home' => 1))) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_ERROR_DISABLE_DEFAULT_TEMPLATE_NOT_PERMITTED'), 'notice'); - unset($eid[$i]); - continue; - } - - // Parent template cannot be disabled if there are children - if ($style->load(['parent' => $table->element, 'client_id' => $table->client_id])) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_ERROR_DISABLE_PARENT_TEMPLATE_NOT_PERMITTED'), 'notice'); - unset($eid[$i]); - continue; - } - } - - if ($table->protected == 1) - { - $result = false; - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); - } - else - { - $table->enabled = $value; - } - - $context = $this->option . '.' . $this->name; - - PluginHelper::importPlugin('extension'); - Factory::getApplication()->triggerEvent('onExtensionChangeState', array($context, $eid, $value)); - - if (!$table->store()) - { - $this->setError($table->getError()); - $result = false; - } - } - - // Clear the cached extension data and menu cache - $this->cleanCache('_system'); - $this->cleanCache('com_modules'); - $this->cleanCache('mod_menu'); - - return $result; - } - - /** - * Refreshes the cached manifest information for an extension. - * - * @param int|int[] $eid extension identifier (key in #__extensions) - * - * @return boolean result of refresh - * - * @since 1.6 - */ - public function refresh($eid) - { - if (!is_array($eid)) - { - $eid = array($eid => 0); - } - - // Get an installer object for the extension type - $installer = Installer::getInstance(); - $result = 0; - - // Uninstall the chosen extensions - foreach ($eid as $id) - { - $result |= $installer->refreshManifestCache($id); - } - - return $result; - } - - /** - * Remove (uninstall) an extension - * - * @param array $eid An array of identifiers - * - * @return boolean True on success - * - * @throws \Exception - * - * @since 1.5 - */ - public function remove($eid = array()) - { - if (!Factory::getUser()->authorise('core.delete', 'com_installer')) - { - Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); - - return false; - } - - /* - * Ensure eid is an array of extension ids in the form id => client_id - * @todo: If it isn't an array do we want to set an error and fail? - */ - if (!is_array($eid)) - { - $eid = array($eid => 0); - } - - // Get an installer object for the extension type - $installer = Installer::getInstance(); - $row = new \Joomla\CMS\Table\Extension($this->getDatabase()); - - // Uninstall the chosen extensions - $msgs = array(); - $result = false; - - foreach ($eid as $id) - { - $id = trim($id); - $row->load($id); - $result = false; - - // Do not allow to uninstall locked extensions. - if ((int) $row->locked === 1) - { - $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR_LOCKED_EXTENSION', $row->name, $id); - - continue; - } - - $langstring = 'COM_INSTALLER_TYPE_TYPE_' . strtoupper($row->type); - $rowtype = Text::_($langstring); - - if (strpos($rowtype, $langstring) !== false) - { - $rowtype = $row->type; - } - - if ($row->type) - { - $result = $installer->uninstall($row->type, $id); - - // Build an array of extensions that failed to uninstall - if ($result === false) - { - // There was an error in uninstalling the package - $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR', $rowtype); - - continue; - } - - // Package uninstalled successfully - $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_SUCCESS', $rowtype); - $result = true; - - continue; - } - - // There was an error in uninstalling the package - $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR', $rowtype); - } - - $msg = implode('
', $msgs); - $app = Factory::getApplication(); - $app->enqueueMessage($msg); - $this->setState('action', 'remove'); - $this->setState('name', $installer->get('name')); - $app->setUserState('com_installer.message', $installer->message); - $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); - - // Clear the cached extension data and menu cache - $this->cleanCache('_system'); - $this->cleanCache('com_modules'); - $this->cleanCache('com_plugins'); - $this->cleanCache('mod_menu'); - - return $result; - } - - /** - * Method to get the database query - * - * @return DatabaseQuery The database query - * - * @since 1.6 - */ - protected function getListQuery() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('*') - ->select('2*protected+(1-protected)*enabled AS status') - ->from('#__extensions') - ->where('state = 0'); - - // Process select filters. - $status = $this->getState('filter.status', ''); - $type = $this->getState('filter.type'); - $clientId = $this->getState('filter.client_id', ''); - $folder = $this->getState('filter.folder'); - $core = $this->getState('filter.core', ''); - $packageId = $this->getState('filter.package_id', ''); - - if ($status !== '') - { - if ($status === '2') - { - $query->where('protected = 1'); - } - elseif ($status === '3') - { - $query->where('protected = 0'); - } - else - { - $status = (int) $status; - $query->where($db->quoteName('protected') . ' = 0') - ->where($db->quoteName('enabled') . ' = :status') - ->bind(':status', $status, ParameterType::INTEGER); - } - } - - if ($type) - { - $query->where($db->quoteName('type') . ' = :type') - ->bind(':type', $type); - } - - if ($clientId !== '') - { - $clientId = (int) $clientId; - $query->where($db->quoteName('client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); - } - - if ($packageId !== '') - { - $packageId = (int) $packageId; - $query->where( - '((' . $db->quoteName('package_id') . ' = :packageId1) OR ' - . '(' . $db->quoteName('extension_id') . ' = :packageId2))' - ) - ->bind([':packageId1',':packageId2'], $packageId, ParameterType::INTEGER); - } - - if ($folder) - { - $folder = $folder === '*' ? '' : $folder; - $query->where($db->quoteName('folder') . ' = :folder') - ->bind(':folder', $folder); - } - - // Filter by core extensions. - if ($core === '1' || $core === '0') - { - $coreExtensionIds = ExtensionHelper::getCoreExtensionIds(); - $method = $core === '1' ? 'whereIn' : 'whereNotIn'; - $query->$method($db->quoteName('extension_id'), $coreExtensionIds); - } - - // Process search filter (extension id). - $search = $this->getState('filter.search'); - - if (!empty($search) && stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('extension_id') . ' = :eid') - ->bind(':eid', $ids, ParameterType::INTEGER); - } - - // Note: The search for name, ordering and pagination are processed by the parent InstallerModel class (in extension.php). - - return $query; - } - - /** - * Load the changelog details for a given extension. - * - * @param integer $eid The extension ID - * @param string $source The view the changelog is for, this is used to determine which version number to show - * - * @return string The output to show in the modal. - * - * @since 4.0.0 - */ - public function loadChangelog($eid, $source) - { - // Get the changelog URL - $eid = (int) $eid; - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select( - $db->quoteName( - [ - 'extensions.element', - 'extensions.type', - 'extensions.folder', - 'extensions.changelogurl', - 'extensions.manifest_cache', - 'extensions.client_id' - ] - ) - ) - ->select($db->quoteName('updates.version', 'updateVersion')) - ->from($db->quoteName('#__extensions', 'extensions')) - ->join( - 'LEFT', - $db->quoteName('#__updates', 'updates'), - $db->quoteName('updates.extension_id') . ' = ' . $db->quoteName('extensions.extension_id') - ) - ->where($db->quoteName('extensions.extension_id') . ' = :eid') - ->bind(':eid', $eid, ParameterType::INTEGER); - $db->setQuery($query); - - $extensions = $db->loadObjectList(); - $this->translate($extensions); - $extension = array_shift($extensions); - - if (!$extension->changelogurl) - { - return ''; - } - - $changelog = new Changelog; - $changelog->setVersion($source === 'manage' ? $extension->version : $extension->updateVersion); - $changelog->loadFromXml($extension->changelogurl); - - // Read all the entries - $entries = array( - 'security' => array(), - 'fix' => array(), - 'addition' => array(), - 'change' => array(), - 'remove' => array(), - 'language' => array(), - 'note' => array() - ); - - array_walk( - $entries, - function (&$value, $name) use ($changelog) { - if ($field = $changelog->get($name)) - { - $value = $changelog->get($name)->data; - } - } - ); - - $layout = new FileLayout('joomla.installer.changelog'); - $output = $layout->render($entries); - - return $output; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\ListModel + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'status', + 'name', + 'client_id', + 'client', 'client_translated', + 'type', 'type_translated', + 'folder', 'folder_translated', + 'package_id', + 'extension_id', + 'creationDate', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @throws \Exception + * + * @since 1.6 + */ + protected function populateState($ordering = 'name', $direction = 'asc') + { + $app = Factory::getApplication(); + + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); + $this->setState('filter.package_id', $this->getUserStateFromRequest($this->context . '.filter.package_id', 'filter_package_id', null, 'int')); + $this->setState('filter.status', $this->getUserStateFromRequest($this->context . '.filter.status', 'filter_status', '', 'string')); + $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); + $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); + $this->setState('filter.core', $this->getUserStateFromRequest($this->context . '.filter.core', 'filter_core', '', 'string')); + + $this->setState('message', $app->getUserState('com_installer.message')); + $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); + $app->setUserState('com_installer.message', ''); + $app->setUserState('com_installer.extension_message', ''); + + parent::populateState($ordering, $direction); + } + + /** + * Enable/Disable an extension. + * + * @param array $eid Extension ids to un/publish + * @param int $value Publish value + * + * @return boolean True on success + * + * @throws \Exception + * + * @since 1.5 + */ + public function publish(&$eid = array(), $value = 1) + { + if (!Factory::getUser()->authorise('core.edit.state', 'com_installer')) { + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); + + return false; + } + + $result = true; + + /* + * Ensure eid is an array of extension ids + * @todo: If it isn't an array do we want to set an error and fail? + */ + if (!is_array($eid)) { + $eid = array($eid); + } + + // Get a table object for the extension type + $table = new Extension($this->getDatabase()); + + // Enable the extension in the table and store it in the database + foreach ($eid as $i => $id) { + $table->load($id); + + if ($table->type == 'template') { + $style = new StyleTable($this->getDatabase()); + + if ($style->load(array('template' => $table->element, 'client_id' => $table->client_id, 'home' => 1))) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_ERROR_DISABLE_DEFAULT_TEMPLATE_NOT_PERMITTED'), 'notice'); + unset($eid[$i]); + continue; + } + + // Parent template cannot be disabled if there are children + if ($style->load(['parent' => $table->element, 'client_id' => $table->client_id])) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_ERROR_DISABLE_PARENT_TEMPLATE_NOT_PERMITTED'), 'notice'); + unset($eid[$i]); + continue; + } + } + + if ($table->protected == 1) { + $result = false; + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); + } else { + $table->enabled = $value; + } + + $context = $this->option . '.' . $this->name; + + PluginHelper::importPlugin('extension'); + Factory::getApplication()->triggerEvent('onExtensionChangeState', array($context, $eid, $value)); + + if (!$table->store()) { + $this->setError($table->getError()); + $result = false; + } + } + + // Clear the cached extension data and menu cache + $this->cleanCache('_system'); + $this->cleanCache('com_modules'); + $this->cleanCache('mod_menu'); + + return $result; + } + + /** + * Refreshes the cached manifest information for an extension. + * + * @param int|int[] $eid extension identifier (key in #__extensions) + * + * @return boolean result of refresh + * + * @since 1.6 + */ + public function refresh($eid) + { + if (!is_array($eid)) { + $eid = array($eid => 0); + } + + // Get an installer object for the extension type + $installer = Installer::getInstance(); + $result = 0; + + // Uninstall the chosen extensions + foreach ($eid as $id) { + $result |= $installer->refreshManifestCache($id); + } + + return $result; + } + + /** + * Remove (uninstall) an extension + * + * @param array $eid An array of identifiers + * + * @return boolean True on success + * + * @throws \Exception + * + * @since 1.5 + */ + public function remove($eid = array()) + { + if (!Factory::getUser()->authorise('core.delete', 'com_installer')) { + Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); + + return false; + } + + /* + * Ensure eid is an array of extension ids in the form id => client_id + * @todo: If it isn't an array do we want to set an error and fail? + */ + if (!is_array($eid)) { + $eid = array($eid => 0); + } + + // Get an installer object for the extension type + $installer = Installer::getInstance(); + $row = new \Joomla\CMS\Table\Extension($this->getDatabase()); + + // Uninstall the chosen extensions + $msgs = array(); + $result = false; + + foreach ($eid as $id) { + $id = trim($id); + $row->load($id); + $result = false; + + // Do not allow to uninstall locked extensions. + if ((int) $row->locked === 1) { + $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR_LOCKED_EXTENSION', $row->name, $id); + + continue; + } + + $langstring = 'COM_INSTALLER_TYPE_TYPE_' . strtoupper($row->type); + $rowtype = Text::_($langstring); + + if (strpos($rowtype, $langstring) !== false) { + $rowtype = $row->type; + } + + if ($row->type) { + $result = $installer->uninstall($row->type, $id); + + // Build an array of extensions that failed to uninstall + if ($result === false) { + // There was an error in uninstalling the package + $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR', $rowtype); + + continue; + } + + // Package uninstalled successfully + $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_SUCCESS', $rowtype); + $result = true; + + continue; + } + + // There was an error in uninstalling the package + $msgs[] = Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR', $rowtype); + } + + $msg = implode('
', $msgs); + $app = Factory::getApplication(); + $app->enqueueMessage($msg); + $this->setState('action', 'remove'); + $this->setState('name', $installer->get('name')); + $app->setUserState('com_installer.message', $installer->message); + $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); + + // Clear the cached extension data and menu cache + $this->cleanCache('_system'); + $this->cleanCache('com_modules'); + $this->cleanCache('com_plugins'); + $this->cleanCache('mod_menu'); + + return $result; + } + + /** + * Method to get the database query + * + * @return DatabaseQuery The database query + * + * @since 1.6 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('*') + ->select('2*protected+(1-protected)*enabled AS status') + ->from('#__extensions') + ->where('state = 0'); + + // Process select filters. + $status = $this->getState('filter.status', ''); + $type = $this->getState('filter.type'); + $clientId = $this->getState('filter.client_id', ''); + $folder = $this->getState('filter.folder'); + $core = $this->getState('filter.core', ''); + $packageId = $this->getState('filter.package_id', ''); + + if ($status !== '') { + if ($status === '2') { + $query->where('protected = 1'); + } elseif ($status === '3') { + $query->where('protected = 0'); + } else { + $status = (int) $status; + $query->where($db->quoteName('protected') . ' = 0') + ->where($db->quoteName('enabled') . ' = :status') + ->bind(':status', $status, ParameterType::INTEGER); + } + } + + if ($type) { + $query->where($db->quoteName('type') . ' = :type') + ->bind(':type', $type); + } + + if ($clientId !== '') { + $clientId = (int) $clientId; + $query->where($db->quoteName('client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); + } + + if ($packageId !== '') { + $packageId = (int) $packageId; + $query->where( + '((' . $db->quoteName('package_id') . ' = :packageId1) OR ' + . '(' . $db->quoteName('extension_id') . ' = :packageId2))' + ) + ->bind([':packageId1',':packageId2'], $packageId, ParameterType::INTEGER); + } + + if ($folder) { + $folder = $folder === '*' ? '' : $folder; + $query->where($db->quoteName('folder') . ' = :folder') + ->bind(':folder', $folder); + } + + // Filter by core extensions. + if ($core === '1' || $core === '0') { + $coreExtensionIds = ExtensionHelper::getCoreExtensionIds(); + $method = $core === '1' ? 'whereIn' : 'whereNotIn'; + $query->$method($db->quoteName('extension_id'), $coreExtensionIds); + } + + // Process search filter (extension id). + $search = $this->getState('filter.search'); + + if (!empty($search) && stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('extension_id') . ' = :eid') + ->bind(':eid', $ids, ParameterType::INTEGER); + } + + // Note: The search for name, ordering and pagination are processed by the parent InstallerModel class (in extension.php). + + return $query; + } + + /** + * Load the changelog details for a given extension. + * + * @param integer $eid The extension ID + * @param string $source The view the changelog is for, this is used to determine which version number to show + * + * @return string The output to show in the modal. + * + * @since 4.0.0 + */ + public function loadChangelog($eid, $source) + { + // Get the changelog URL + $eid = (int) $eid; + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + $db->quoteName( + [ + 'extensions.element', + 'extensions.type', + 'extensions.folder', + 'extensions.changelogurl', + 'extensions.manifest_cache', + 'extensions.client_id' + ] + ) + ) + ->select($db->quoteName('updates.version', 'updateVersion')) + ->from($db->quoteName('#__extensions', 'extensions')) + ->join( + 'LEFT', + $db->quoteName('#__updates', 'updates'), + $db->quoteName('updates.extension_id') . ' = ' . $db->quoteName('extensions.extension_id') + ) + ->where($db->quoteName('extensions.extension_id') . ' = :eid') + ->bind(':eid', $eid, ParameterType::INTEGER); + $db->setQuery($query); + + $extensions = $db->loadObjectList(); + $this->translate($extensions); + $extension = array_shift($extensions); + + if (!$extension->changelogurl) { + return ''; + } + + $changelog = new Changelog(); + $changelog->setVersion($source === 'manage' ? $extension->version : $extension->updateVersion); + $changelog->loadFromXml($extension->changelogurl); + + // Read all the entries + $entries = array( + 'security' => array(), + 'fix' => array(), + 'addition' => array(), + 'change' => array(), + 'remove' => array(), + 'language' => array(), + 'note' => array() + ); + + array_walk( + $entries, + function (&$value, $name) use ($changelog) { + if ($field = $changelog->get($name)) { + $value = $changelog->get($name)->data; + } + } + ); + + $layout = new FileLayout('joomla.installer.changelog'); + $output = $layout->render($entries); + + return $output; + } } diff --git a/administrator/components/com_installer/src/Model/UpdateModel.php b/administrator/components/com_installer/src/Model/UpdateModel.php index 85d1ae353738d..998756e54ac65 100644 --- a/administrator/components/com_installer/src/Model/UpdateModel.php +++ b/administrator/components/com_installer/src/Model/UpdateModel.php @@ -1,4 +1,5 @@ setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); - $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); - $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); - - $app = Factory::getApplication(); - $this->setState('message', $app->getUserState('com_installer.message')); - $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); - $app->setUserState('com_installer.message', ''); - $app->setUserState('com_installer.extension_message', ''); - - parent::populateState($ordering, $direction); - } - - /** - * Method to get the database query - * - * @return \Joomla\Database\DatabaseQuery The database query - * - * @since 1.6 - */ - protected function getListQuery() - { - $db = $this->getDatabase(); - - // Grab updates ignoring new installs - $query = $db->getQuery(true) - ->select('u.*') - ->select($db->quoteName('e.manifest_cache')) - ->from($db->quoteName('#__updates', 'u')) - ->join( - 'LEFT', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('u.extension_id') - ) - ->where($db->quoteName('u.extension_id') . ' != 0'); - - // Process select filters. - $clientId = $this->getState('filter.client_id'); - $type = $this->getState('filter.type'); - $folder = $this->getState('filter.folder'); - $extensionId = $this->getState('filter.extension_id'); - - if ($type) - { - $query->where($db->quoteName('u.type') . ' = :type') - ->bind(':type', $type); - } - - if ($clientId != '') - { - $clientId = (int) $clientId; - $query->where($db->quoteName('u.client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); - } - - if ($folder != '' && in_array($type, array('plugin', 'library', ''))) - { - $folder = $folder === '*' ? '' : $folder; - $query->where($db->quoteName('u.folder') . ' = :folder') - ->bind(':folder', $folder); - } - - if ($extensionId) - { - $extensionId = (int) $extensionId; - $query->where($db->quoteName('u.extension_id') . ' = :extensionid') - ->bind(':extensionid', $extensionId, ParameterType::INTEGER); - } - else - { - $eid = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; - $query->where($db->quoteName('u.extension_id') . ' != 0') - ->where($db->quoteName('u.extension_id') . ' != :eid') - ->bind(':eid', $eid, ParameterType::INTEGER); - } - - // Process search filter. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'eid:') !== false) - { - $sid = (int) substr($search, 4); - $query->where($db->quoteName('u.extension_id') . ' = :sid') - ->bind(':sid', $sid, ParameterType::INTEGER); - } - else - { - if (stripos($search, 'uid:') !== false) - { - $suid = (int) substr($search, 4); - $query->where($db->quoteName('u.update_site_id') . ' = :suid') - ->bind(':suid', $suid, ParameterType::INTEGER); - } - elseif (stripos($search, 'id:') !== false) - { - $uid = (int) substr($search, 3); - $query->where($db->quoteName('u.update_id') . ' = :uid') - ->bind(':uid', $uid, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where($db->quoteName('u.name') . ' LIKE :search') - ->bind(':search', $search); - } - } - } - - return $query; - } - - /** - * Translate a list of objects - * - * @param array $items The array of objects - * - * @return array The array of translated objects - * - * @since 3.5 - */ - protected function translate(&$items) - { - foreach ($items as &$item) - { - $item->client_translated = Text::_([0 => 'JSITE', 1 => 'JADMINISTRATOR', 3 => 'JAPI'][$item->client_id] ?? 'JSITE'); - $manifest = json_decode($item->manifest_cache); - $item->current_version = $manifest->version ?? Text::_('JLIB_UNKNOWN'); - $item->description = $item->description !== '' ? $item->description : Text::_('COM_INSTALLER_MSG_UPDATE_NODESC'); - $item->type_translated = Text::_('COM_INSTALLER_TYPE_' . strtoupper($item->type)); - $item->folder_translated = $item->folder ?: Text::_('COM_INSTALLER_TYPE_NONAPPLICABLE'); - $item->install_type = $item->extension_id ? Text::_('COM_INSTALLER_MSG_UPDATE_UPDATE') : Text::_('COM_INSTALLER_NEW_INSTALL'); - } - - return $items; - } - - /** - * Returns an object list - * - * @param DatabaseQuery $query The query - * @param int $limitstart Offset - * @param int $limit The number of records - * - * @return array - * - * @since 3.5 - */ - protected function _getList($query, $limitstart = 0, $limit = 0) - { - $db = $this->getDatabase(); - $listOrder = $this->getState('list.ordering', 'u.name'); - $listDirn = $this->getState('list.direction', 'asc'); - - // Process ordering. - if (in_array($listOrder, array('client_translated', 'folder_translated', 'type_translated'))) - { - $db->setQuery($query); - $result = $db->loadObjectList(); - $this->translate($result); - $result = ArrayHelper::sortObjects($result, $listOrder, strtolower($listDirn) === 'desc' ? -1 : 1, true, true); - $total = count($result); - - if ($total < $limitstart) - { - $limitstart = 0; - $this->setState('list.start', 0); - } - - return array_slice($result, $limitstart, $limit ?: null); - } - else - { - $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); - - $result = parent::_getList($query, $limitstart, $limit); - $this->translate($result); - - return $result; - } - } - - /** - * Get the count of disabled update sites - * - * @return integer - * - * @since 3.4 - */ - public function getDisabledUpdateSites() - { - $db = $this->getDatabase(); - - $query = $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName('#__update_sites')) - ->where($db->quoteName('enabled') . ' = 0'); - - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Finds updates for an extension. - * - * @param int $eid Extension identifier to look for - * @param int $cacheTimeout Cache timeout - * @param int $minimumStability Minimum stability for updates {@see Updater} (0=dev, 1=alpha, 2=beta, 3=rc, 4=stable) - * - * @return boolean Result - * - * @since 1.6 - */ - public function findUpdates($eid = 0, $cacheTimeout = 0, $minimumStability = Updater::STABILITY_STABLE) - { - Updater::getInstance()->findUpdates($eid, $cacheTimeout, $minimumStability); - - return true; - } - - /** - * Removes all of the updates from the table. - * - * @return boolean result of operation - * - * @since 1.6 - */ - public function purge() - { - $db = $this->getDatabase(); - - try - { - $db->truncateTable('#__updates'); - } - catch (ExecutionFailureException $e) - { - $this->_message = Text::_('JLIB_INSTALLER_FAILED_TO_PURGE_UPDATES'); - - return false; - } - - // Reset the last update check timestamp - $query = $db->getQuery(true) - ->update($db->quoteName('#__update_sites')) - ->set($db->quoteName('last_check_timestamp') . ' = ' . $db->quote(0)); - $db->setQuery($query); - $db->execute(); - - // Clear the administrator cache - $this->cleanCache('_system'); - - $this->_message = Text::_('JLIB_INSTALLER_PURGED_UPDATES'); - - return true; - } - - /** - * Update function. - * - * Sets the "result" state with the result of the operation. - * - * @param int[] $uids List of updates to apply - * @param int $minimumStability The minimum allowed stability for installed updates {@see Updater} - * - * @return void - * - * @since 1.6 - */ - public function update($uids, $minimumStability = Updater::STABILITY_STABLE) - { - $result = true; - - foreach ($uids as $uid) - { - $update = new Update; - $instance = new \Joomla\CMS\Table\Update($this->getDatabase()); - - if (!$instance->load($uid)) - { - // Update no longer available, maybe already updated by a package. - continue; - } - - $update->loadFromXml($instance->detailsurl, $minimumStability); - - // Find and use extra_query from update_site if available - $updateSiteInstance = new \Joomla\CMS\Table\UpdateSite($this->getDatabase()); - $updateSiteInstance->load($instance->update_site_id); - - if ($updateSiteInstance->extra_query) - { - $update->set('extra_query', $updateSiteInstance->extra_query); - } - - $this->preparePreUpdate($update, $instance); - - // Install sets state and enqueues messages - $res = $this->install($update); - - if ($res) - { - $instance->delete($uid); - } - - $result = $res & $result; - } - - // Clear the cached extension data and menu cache - $this->cleanCache('_system'); - $this->cleanCache('com_modules'); - $this->cleanCache('com_plugins'); - $this->cleanCache('mod_menu'); - - // Set the final state - $this->setState('result', $result); - } - - /** - * Handles the actual update installation. - * - * @param Update $update An update definition - * - * @return boolean Result of install - * - * @since 1.6 - */ - private function install($update) - { - // Load overrides plugin. - PluginHelper::importPlugin('installer'); - - $app = Factory::getApplication(); - - if (!isset($update->get('downloadurl')->_data)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_INVALID_EXTENSION_UPDATE'), 'error'); - - return false; - } - - $url = trim($update->downloadurl->_data); - $sources = $update->get('downloadSources', array()); - - if ($extra_query = $update->get('extra_query')) - { - $url .= (strpos($url, '?') === false) ? '?' : '&'; - $url .= $extra_query; - } - - $mirror = 0; - - while (!($p_file = InstallerHelper::downloadPackage($url)) && isset($sources[$mirror])) - { - $name = $sources[$mirror]; - $url = trim($name->url); - - if ($extra_query) - { - $url .= (strpos($url, '?') === false) ? '?' : '&'; - $url .= $extra_query; - } - - $mirror++; - } - - // Was the package downloaded? - if (!$p_file) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_PACKAGE_DOWNLOAD_FAILED', $url), 'error'); - - return false; - } - - $config = $app->getConfig(); - $tmp_dest = $config->get('tmp_path'); - - // Unpack the downloaded package file - $package = InstallerHelper::unpack($tmp_dest . '/' . $p_file); - - if (empty($package)) - { - $app->enqueueMessage(Text::sprintf('COM_INSTALLER_UNPACK_ERROR', $p_file), 'error'); - - return false; - } - - // Get an installer instance - $installer = Installer::getInstance(); - $update->set('type', $package['type']); - - // Check the package - $check = InstallerHelper::isChecksumValid($package['packagefile'], $update); - - if ($check === InstallerHelper::HASH_NOT_VALIDATED) - { - $app->enqueueMessage(Text::_('COM_INSTALLER_INSTALL_CHECKSUM_WRONG'), 'error'); - - return false; - } - - if ($check === InstallerHelper::HASH_NOT_PROVIDED) - { - $app->enqueueMessage(Text::_('COM_INSTALLER_INSTALL_CHECKSUM_WARNING'), 'warning'); - } - - // Install the package - if (!$installer->update($package['dir'])) - { - // There was an error updating the package - $app->enqueueMessage( - Text::sprintf('COM_INSTALLER_MSG_UPDATE_ERROR', - Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type'])) - ), 'error' - ); - $result = false; - } - else - { - // Package updated successfully - $app->enqueueMessage( - Text::sprintf('COM_INSTALLER_MSG_UPDATE_SUCCESS', - Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type'])) - ), 'success' - ); - $result = true; - } - - // Quick change - $this->type = $package['type']; - - // @todo: Reconfigure this code when you have more battery life left - $this->setState('name', $installer->get('name')); - $this->setState('result', $result); - $app->setUserState('com_installer.message', $installer->message); - $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); - - // Cleanup the install files - if (!is_file($package['packagefile'])) - { - $package['packagefile'] = $config->get('tmp_path') . '/' . $package['packagefile']; - } - - InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); - - return $result; - } - - /** - * Method to get the row form. - * - * @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 Form|bool A Form object on success, false on failure - * - * @since 2.5.2 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - Form::addFormPath(JPATH_COMPONENT . '/models/forms'); - Form::addFieldPath(JPATH_COMPONENT . '/models/fields'); - $form = Form::getInstance('com_installer.update', 'update', array('load_data' => $loadData)); - - // Check for an error. - if ($form == false) - { - $this->setError($form->getMessage()); - - return false; - } - - // Check the session for previously entered form data. - $data = $this->loadFormData(); - - // Bind the form data if present. - if (!empty($data)) - { - $form->bind($data); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 2.5.2 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState($this->context, array()); - - return $data; - } - - /** - * Method to add parameters to the update - * - * @param Update $update An update definition - * @param \Joomla\CMS\Table\Update $table The update instance from the database - * - * @return void - * - * @since 3.7.0 - */ - protected function preparePreUpdate($update, $table) - { - switch ($table->type) - { - // Components could have a helper which adds additional data - case 'component': - $ename = str_replace('com_', '', $table->element); - $fname = $ename . '.php'; - $cname = ucfirst($ename) . 'Helper'; - - $path = JPATH_ADMINISTRATOR . '/components/' . $table->element . '/helpers/' . $fname; - - if (File::exists($path)) - { - require_once $path; - - if (class_exists($cname) && is_callable(array($cname, 'prepareUpdate'))) - { - call_user_func_array(array($cname, 'prepareUpdate'), array(&$update, &$table)); - } - } - - break; - - // Modules could have a helper which adds additional data - case 'module': - $cname = str_replace('_', '', $table->element) . 'Helper'; - $path = ($table->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE) . '/modules/' . $table->element . '/helper.php'; - - if (File::exists($path)) - { - require_once $path; - - if (class_exists($cname) && is_callable(array($cname, 'prepareUpdate'))) - { - call_user_func_array(array($cname, 'prepareUpdate'), array(&$update, &$table)); - } - } - - break; - - // If we have a plugin, we can use the plugin trigger "onInstallerBeforePackageDownload" - // But we should make sure, that our plugin is loaded, so we don't need a second "installer" plugin - case 'plugin': - $cname = str_replace('plg_', '', $table->element); - PluginHelper::importPlugin($table->folder, $cname); - break; - } - } - - /** - * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. - * - * @return DatabaseQuery - * - * @since 4.0.0 - */ - protected function getEmptyStateQuery() - { - $query = parent::getEmptyStateQuery(); - - $query->where($this->getDatabase()->quoteName('extension_id') . ' != 0'); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\ListModel + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'name', 'u.name', + 'client_id', 'u.client_id', 'client_translated', + 'type', 'u.type', 'type_translated', + 'folder', 'u.folder', 'folder_translated', + 'extension_id', 'u.extension_id', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'u.name', $direction = 'asc') + { + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.client_id', $this->getUserStateFromRequest($this->context . '.filter.client_id', 'filter_client_id', null, 'int')); + $this->setState('filter.type', $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string')); + $this->setState('filter.folder', $this->getUserStateFromRequest($this->context . '.filter.folder', 'filter_folder', '', 'string')); + + $app = Factory::getApplication(); + $this->setState('message', $app->getUserState('com_installer.message')); + $this->setState('extension_message', $app->getUserState('com_installer.extension_message')); + $app->setUserState('com_installer.message', ''); + $app->setUserState('com_installer.extension_message', ''); + + parent::populateState($ordering, $direction); + } + + /** + * Method to get the database query + * + * @return \Joomla\Database\DatabaseQuery The database query + * + * @since 1.6 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + + // Grab updates ignoring new installs + $query = $db->getQuery(true) + ->select('u.*') + ->select($db->quoteName('e.manifest_cache')) + ->from($db->quoteName('#__updates', 'u')) + ->join( + 'LEFT', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('u.extension_id') + ) + ->where($db->quoteName('u.extension_id') . ' != 0'); + + // Process select filters. + $clientId = $this->getState('filter.client_id'); + $type = $this->getState('filter.type'); + $folder = $this->getState('filter.folder'); + $extensionId = $this->getState('filter.extension_id'); + + if ($type) { + $query->where($db->quoteName('u.type') . ' = :type') + ->bind(':type', $type); + } + + if ($clientId != '') { + $clientId = (int) $clientId; + $query->where($db->quoteName('u.client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); + } + + if ($folder != '' && in_array($type, array('plugin', 'library', ''))) { + $folder = $folder === '*' ? '' : $folder; + $query->where($db->quoteName('u.folder') . ' = :folder') + ->bind(':folder', $folder); + } + + if ($extensionId) { + $extensionId = (int) $extensionId; + $query->where($db->quoteName('u.extension_id') . ' = :extensionid') + ->bind(':extensionid', $extensionId, ParameterType::INTEGER); + } else { + $eid = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; + $query->where($db->quoteName('u.extension_id') . ' != 0') + ->where($db->quoteName('u.extension_id') . ' != :eid') + ->bind(':eid', $eid, ParameterType::INTEGER); + } + + // Process search filter. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'eid:') !== false) { + $sid = (int) substr($search, 4); + $query->where($db->quoteName('u.extension_id') . ' = :sid') + ->bind(':sid', $sid, ParameterType::INTEGER); + } else { + if (stripos($search, 'uid:') !== false) { + $suid = (int) substr($search, 4); + $query->where($db->quoteName('u.update_site_id') . ' = :suid') + ->bind(':suid', $suid, ParameterType::INTEGER); + } elseif (stripos($search, 'id:') !== false) { + $uid = (int) substr($search, 3); + $query->where($db->quoteName('u.update_id') . ' = :uid') + ->bind(':uid', $uid, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where($db->quoteName('u.name') . ' LIKE :search') + ->bind(':search', $search); + } + } + } + + return $query; + } + + /** + * Translate a list of objects + * + * @param array $items The array of objects + * + * @return array The array of translated objects + * + * @since 3.5 + */ + protected function translate(&$items) + { + foreach ($items as &$item) { + $item->client_translated = Text::_([0 => 'JSITE', 1 => 'JADMINISTRATOR', 3 => 'JAPI'][$item->client_id] ?? 'JSITE'); + $manifest = json_decode($item->manifest_cache); + $item->current_version = $manifest->version ?? Text::_('JLIB_UNKNOWN'); + $item->description = $item->description !== '' ? $item->description : Text::_('COM_INSTALLER_MSG_UPDATE_NODESC'); + $item->type_translated = Text::_('COM_INSTALLER_TYPE_' . strtoupper($item->type)); + $item->folder_translated = $item->folder ?: Text::_('COM_INSTALLER_TYPE_NONAPPLICABLE'); + $item->install_type = $item->extension_id ? Text::_('COM_INSTALLER_MSG_UPDATE_UPDATE') : Text::_('COM_INSTALLER_NEW_INSTALL'); + } + + return $items; + } + + /** + * Returns an object list + * + * @param DatabaseQuery $query The query + * @param int $limitstart Offset + * @param int $limit The number of records + * + * @return array + * + * @since 3.5 + */ + protected function _getList($query, $limitstart = 0, $limit = 0) + { + $db = $this->getDatabase(); + $listOrder = $this->getState('list.ordering', 'u.name'); + $listDirn = $this->getState('list.direction', 'asc'); + + // Process ordering. + if (in_array($listOrder, array('client_translated', 'folder_translated', 'type_translated'))) { + $db->setQuery($query); + $result = $db->loadObjectList(); + $this->translate($result); + $result = ArrayHelper::sortObjects($result, $listOrder, strtolower($listDirn) === 'desc' ? -1 : 1, true, true); + $total = count($result); + + if ($total < $limitstart) { + $limitstart = 0; + $this->setState('list.start', 0); + } + + return array_slice($result, $limitstart, $limit ?: null); + } else { + $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); + + $result = parent::_getList($query, $limitstart, $limit); + $this->translate($result); + + return $result; + } + } + + /** + * Get the count of disabled update sites + * + * @return integer + * + * @since 3.4 + */ + public function getDisabledUpdateSites() + { + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__update_sites')) + ->where($db->quoteName('enabled') . ' = 0'); + + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Finds updates for an extension. + * + * @param int $eid Extension identifier to look for + * @param int $cacheTimeout Cache timeout + * @param int $minimumStability Minimum stability for updates {@see Updater} (0=dev, 1=alpha, 2=beta, 3=rc, 4=stable) + * + * @return boolean Result + * + * @since 1.6 + */ + public function findUpdates($eid = 0, $cacheTimeout = 0, $minimumStability = Updater::STABILITY_STABLE) + { + Updater::getInstance()->findUpdates($eid, $cacheTimeout, $minimumStability); + + return true; + } + + /** + * Removes all of the updates from the table. + * + * @return boolean result of operation + * + * @since 1.6 + */ + public function purge() + { + $db = $this->getDatabase(); + + try { + $db->truncateTable('#__updates'); + } catch (ExecutionFailureException $e) { + $this->_message = Text::_('JLIB_INSTALLER_FAILED_TO_PURGE_UPDATES'); + + return false; + } + + // Reset the last update check timestamp + $query = $db->getQuery(true) + ->update($db->quoteName('#__update_sites')) + ->set($db->quoteName('last_check_timestamp') . ' = ' . $db->quote(0)); + $db->setQuery($query); + $db->execute(); + + // Clear the administrator cache + $this->cleanCache('_system'); + + $this->_message = Text::_('JLIB_INSTALLER_PURGED_UPDATES'); + + return true; + } + + /** + * Update function. + * + * Sets the "result" state with the result of the operation. + * + * @param int[] $uids List of updates to apply + * @param int $minimumStability The minimum allowed stability for installed updates {@see Updater} + * + * @return void + * + * @since 1.6 + */ + public function update($uids, $minimumStability = Updater::STABILITY_STABLE) + { + $result = true; + + foreach ($uids as $uid) { + $update = new Update(); + $instance = new \Joomla\CMS\Table\Update($this->getDatabase()); + + if (!$instance->load($uid)) { + // Update no longer available, maybe already updated by a package. + continue; + } + + $update->loadFromXml($instance->detailsurl, $minimumStability); + + // Find and use extra_query from update_site if available + $updateSiteInstance = new \Joomla\CMS\Table\UpdateSite($this->getDatabase()); + $updateSiteInstance->load($instance->update_site_id); + + if ($updateSiteInstance->extra_query) { + $update->set('extra_query', $updateSiteInstance->extra_query); + } + + $this->preparePreUpdate($update, $instance); + + // Install sets state and enqueues messages + $res = $this->install($update); + + if ($res) { + $instance->delete($uid); + } + + $result = $res & $result; + } + + // Clear the cached extension data and menu cache + $this->cleanCache('_system'); + $this->cleanCache('com_modules'); + $this->cleanCache('com_plugins'); + $this->cleanCache('mod_menu'); + + // Set the final state + $this->setState('result', $result); + } + + /** + * Handles the actual update installation. + * + * @param Update $update An update definition + * + * @return boolean Result of install + * + * @since 1.6 + */ + private function install($update) + { + // Load overrides plugin. + PluginHelper::importPlugin('installer'); + + $app = Factory::getApplication(); + + if (!isset($update->get('downloadurl')->_data)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_INVALID_EXTENSION_UPDATE'), 'error'); + + return false; + } + + $url = trim($update->downloadurl->_data); + $sources = $update->get('downloadSources', array()); + + if ($extra_query = $update->get('extra_query')) { + $url .= (strpos($url, '?') === false) ? '?' : '&'; + $url .= $extra_query; + } + + $mirror = 0; + + while (!($p_file = InstallerHelper::downloadPackage($url)) && isset($sources[$mirror])) { + $name = $sources[$mirror]; + $url = trim($name->url); + + if ($extra_query) { + $url .= (strpos($url, '?') === false) ? '?' : '&'; + $url .= $extra_query; + } + + $mirror++; + } + + // Was the package downloaded? + if (!$p_file) { + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_INSTALLER_PACKAGE_DOWNLOAD_FAILED', $url), 'error'); + + return false; + } + + $config = $app->getConfig(); + $tmp_dest = $config->get('tmp_path'); + + // Unpack the downloaded package file + $package = InstallerHelper::unpack($tmp_dest . '/' . $p_file); + + if (empty($package)) { + $app->enqueueMessage(Text::sprintf('COM_INSTALLER_UNPACK_ERROR', $p_file), 'error'); + + return false; + } + + // Get an installer instance + $installer = Installer::getInstance(); + $update->set('type', $package['type']); + + // Check the package + $check = InstallerHelper::isChecksumValid($package['packagefile'], $update); + + if ($check === InstallerHelper::HASH_NOT_VALIDATED) { + $app->enqueueMessage(Text::_('COM_INSTALLER_INSTALL_CHECKSUM_WRONG'), 'error'); + + return false; + } + + if ($check === InstallerHelper::HASH_NOT_PROVIDED) { + $app->enqueueMessage(Text::_('COM_INSTALLER_INSTALL_CHECKSUM_WARNING'), 'warning'); + } + + // Install the package + if (!$installer->update($package['dir'])) { + // There was an error updating the package + $app->enqueueMessage( + Text::sprintf( + 'COM_INSTALLER_MSG_UPDATE_ERROR', + Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type'])) + ), + 'error' + ); + $result = false; + } else { + // Package updated successfully + $app->enqueueMessage( + Text::sprintf( + 'COM_INSTALLER_MSG_UPDATE_SUCCESS', + Text::_('COM_INSTALLER_TYPE_TYPE_' . strtoupper($package['type'])) + ), + 'success' + ); + $result = true; + } + + // Quick change + $this->type = $package['type']; + + // @todo: Reconfigure this code when you have more battery life left + $this->setState('name', $installer->get('name')); + $this->setState('result', $result); + $app->setUserState('com_installer.message', $installer->message); + $app->setUserState('com_installer.extension_message', $installer->get('extension_message')); + + // Cleanup the install files + if (!is_file($package['packagefile'])) { + $package['packagefile'] = $config->get('tmp_path') . '/' . $package['packagefile']; + } + + InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); + + return $result; + } + + /** + * Method to get the row form. + * + * @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 Form|bool A Form object on success, false on failure + * + * @since 2.5.2 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + Form::addFormPath(JPATH_COMPONENT . '/models/forms'); + Form::addFieldPath(JPATH_COMPONENT . '/models/fields'); + $form = Form::getInstance('com_installer.update', 'update', array('load_data' => $loadData)); + + // Check for an error. + if ($form == false) { + $this->setError($form->getMessage()); + + return false; + } + + // Check the session for previously entered form data. + $data = $this->loadFormData(); + + // Bind the form data if present. + if (!empty($data)) { + $form->bind($data); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 2.5.2 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState($this->context, array()); + + return $data; + } + + /** + * Method to add parameters to the update + * + * @param Update $update An update definition + * @param \Joomla\CMS\Table\Update $table The update instance from the database + * + * @return void + * + * @since 3.7.0 + */ + protected function preparePreUpdate($update, $table) + { + switch ($table->type) { + // Components could have a helper which adds additional data + case 'component': + $ename = str_replace('com_', '', $table->element); + $fname = $ename . '.php'; + $cname = ucfirst($ename) . 'Helper'; + + $path = JPATH_ADMINISTRATOR . '/components/' . $table->element . '/helpers/' . $fname; + + if (File::exists($path)) { + require_once $path; + + if (class_exists($cname) && is_callable(array($cname, 'prepareUpdate'))) { + call_user_func_array(array($cname, 'prepareUpdate'), array(&$update, &$table)); + } + } + + break; + + // Modules could have a helper which adds additional data + case 'module': + $cname = str_replace('_', '', $table->element) . 'Helper'; + $path = ($table->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE) . '/modules/' . $table->element . '/helper.php'; + + if (File::exists($path)) { + require_once $path; + + if (class_exists($cname) && is_callable(array($cname, 'prepareUpdate'))) { + call_user_func_array(array($cname, 'prepareUpdate'), array(&$update, &$table)); + } + } + + break; + + // If we have a plugin, we can use the plugin trigger "onInstallerBeforePackageDownload" + // But we should make sure, that our plugin is loaded, so we don't need a second "installer" plugin + case 'plugin': + $cname = str_replace('plg_', '', $table->element); + PluginHelper::importPlugin($table->folder, $cname); + break; + } + } + + /** + * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. + * + * @return DatabaseQuery + * + * @since 4.0.0 + */ + protected function getEmptyStateQuery() + { + $query = parent::getEmptyStateQuery(); + + $query->where($this->getDatabase()->quoteName('extension_id') . ' != 0'); + + return $query; + } } diff --git a/administrator/components/com_installer/src/Model/UpdatesiteModel.php b/administrator/components/com_installer/src/Model/UpdatesiteModel.php index e9def0c2f6c68..51ad16a2fa4a5 100644 --- a/administrator/components/com_installer/src/Model/UpdatesiteModel.php +++ b/administrator/components/com_installer/src/Model/UpdatesiteModel.php @@ -1,4 +1,5 @@ loadForm('com_installer.updatesite', 'updatesite', ['control' => 'jform', 'load_data' => $loadData]); - - if (empty($form)) - { - return false; - } - - return $form; - } - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 4.0.0 - */ - protected function loadFormData() - { - $data = $this->getItem(); - $this->preprocessData('com_installer.updatesite', $data); - - return $data; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return CMSObject|boolean Object on success, false on failure. - * - * @since 4.0.0 - */ - public function getItem($pk = null) - { - $item = parent::getItem($pk); - - $db = $this->getDatabase(); - $updateSiteId = (int) $item->get('update_site_id'); - $query = $db->getQuery(true) - ->select( - $db->quoteName( - [ - 'update_sites.extra_query', - 'extensions.type', - 'extensions.element', - 'extensions.folder', - 'extensions.client_id', - 'extensions.checked_out' - ] - ) - ) - ->from($db->quoteName('#__update_sites', 'update_sites')) - ->join( - 'INNER', - $db->quoteName('#__update_sites_extensions', 'update_sites_extensions'), - $db->quoteName('update_sites_extensions.update_site_id') . ' = ' . $db->quoteName('update_sites.update_site_id') - ) - ->join( - 'INNER', - $db->quoteName('#__extensions', 'extensions'), - $db->quoteName('extensions.extension_id') . ' = ' . $db->quoteName('update_sites_extensions.extension_id') - ) - ->where($db->quoteName('update_sites.update_site_id') . ' = :updatesiteid') - ->bind(':updatesiteid', $updateSiteId, ParameterType::INTEGER); - - $db->setQuery($query); - $extension = new CMSObject($db->loadAssoc()); - - $downloadKey = InstallerHelper::getDownloadKey($extension); - - $item->set('extra_query', $downloadKey['value'] ?? ''); - $item->set('downloadIdPrefix', $downloadKey['prefix'] ?? ''); - $item->set('downloadIdSuffix', $downloadKey['suffix'] ?? ''); - - return $item; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success, False on error. - * - * @since 4.0.0 - */ - public function save($data): bool - { - // Apply the extra_query. Always empty when saving a free extension's update site. - if (isset($data['extra_query'])) - { - $data['extra_query'] = $data['downloadIdPrefix'] . $data['extra_query'] . $data['downloadIdSuffix']; - } - - // Force Joomla to recheck for updates - $data['last_check_timestamp'] = 0; - - $result = parent::save($data); - - if (!$result) - { - return $result; - } - - // Delete update records forcing Joomla to fetch them again, applying the new extra_query. - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__updates')) - ->where($db->quoteName('update_site_id') . ' = :updateSiteId'); - $query->bind(':updateSiteId', $data['update_site_id'], ParameterType::INTEGER); - - try - { - $db->setQuery($query)->execute(); - } - catch (Exception $e) - { - // No problem if this fails for any reason. - } - - return true; - } + /** + * The type alias for this content type. + * + * @var string + * @since 4.0.0 + */ + public $typeAlias = 'com_installer.updatesite'; + + /** + * Method to get the row form. + * + * @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 Form|boolean A Form object on success, false on failure + * + * @throws Exception + * + * @since 4.0.0 + */ + public function getForm($data = [], $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_installer.updatesite', 'updatesite', ['control' => 'jform', 'load_data' => $loadData]); + + if (empty($form)) { + return false; + } + + return $form; + } + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 4.0.0 + */ + protected function loadFormData() + { + $data = $this->getItem(); + $this->preprocessData('com_installer.updatesite', $data); + + return $data; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return CMSObject|boolean Object on success, false on failure. + * + * @since 4.0.0 + */ + public function getItem($pk = null) + { + $item = parent::getItem($pk); + + $db = $this->getDatabase(); + $updateSiteId = (int) $item->get('update_site_id'); + $query = $db->getQuery(true) + ->select( + $db->quoteName( + [ + 'update_sites.extra_query', + 'extensions.type', + 'extensions.element', + 'extensions.folder', + 'extensions.client_id', + 'extensions.checked_out' + ] + ) + ) + ->from($db->quoteName('#__update_sites', 'update_sites')) + ->join( + 'INNER', + $db->quoteName('#__update_sites_extensions', 'update_sites_extensions'), + $db->quoteName('update_sites_extensions.update_site_id') . ' = ' . $db->quoteName('update_sites.update_site_id') + ) + ->join( + 'INNER', + $db->quoteName('#__extensions', 'extensions'), + $db->quoteName('extensions.extension_id') . ' = ' . $db->quoteName('update_sites_extensions.extension_id') + ) + ->where($db->quoteName('update_sites.update_site_id') . ' = :updatesiteid') + ->bind(':updatesiteid', $updateSiteId, ParameterType::INTEGER); + + $db->setQuery($query); + $extension = new CMSObject($db->loadAssoc()); + + $downloadKey = InstallerHelper::getDownloadKey($extension); + + $item->set('extra_query', $downloadKey['value'] ?? ''); + $item->set('downloadIdPrefix', $downloadKey['prefix'] ?? ''); + $item->set('downloadIdSuffix', $downloadKey['suffix'] ?? ''); + + return $item; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success, False on error. + * + * @since 4.0.0 + */ + public function save($data): bool + { + // Apply the extra_query. Always empty when saving a free extension's update site. + if (isset($data['extra_query'])) { + $data['extra_query'] = $data['downloadIdPrefix'] . $data['extra_query'] . $data['downloadIdSuffix']; + } + + // Force Joomla to recheck for updates + $data['last_check_timestamp'] = 0; + + $result = parent::save($data); + + if (!$result) { + return $result; + } + + // Delete update records forcing Joomla to fetch them again, applying the new extra_query. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__updates')) + ->where($db->quoteName('update_site_id') . ' = :updateSiteId'); + $query->bind(':updateSiteId', $data['update_site_id'], ParameterType::INTEGER); + + try { + $db->setQuery($query)->execute(); + } catch (Exception $e) { + // No problem if this fails for any reason. + } + + return true; + } } diff --git a/administrator/components/com_installer/src/Model/UpdatesitesModel.php b/administrator/components/com_installer/src/Model/UpdatesitesModel.php index 4be1b8c62079d..21658f6b416dd 100644 --- a/administrator/components/com_installer/src/Model/UpdatesitesModel.php +++ b/administrator/components/com_installer/src/Model/UpdatesitesModel.php @@ -1,4 +1,5 @@ authorise('core.edit.state', 'com_installer')) - { - throw new Exception(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 403); - } - - $result = true; - - // Ensure eid is an array of extension ids - if (!is_array($eid)) - { - $eid = [$eid]; - } - - // Get a table object for the extension type - $table = new UpdateSiteTable($this->getDatabase()); - - // Enable the update site in the table and store it in the database - foreach ($eid as $i => $id) - { - $table->load($id); - $table->enabled = $value; - - if (!$table->store()) - { - $this->setError($table->getError()); - $result = false; - } - } - - return $result; - } - - /** - * Deletes an update site. - * - * @param array $ids Extension ids to delete. - * - * @return void - * - * @throws Exception on ACL error - * @since 3.6 - * - */ - public function delete($ids = []) - { - if (!Factory::getUser()->authorise('core.delete', 'com_installer')) - { - throw new Exception(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 403); - } - - // Ensure eid is an array of extension ids - if (!is_array($ids)) - { - $ids = [$ids]; - } - - $db = $this->getDatabase(); - $app = Factory::getApplication(); - - $count = 0; - - // Gets the update site names. - $query = $db->getQuery(true) - ->select($db->quoteName(['update_site_id', 'name'])) - ->from($db->quoteName('#__update_sites')) - ->whereIn($db->quoteName('update_site_id'), $ids); - $db->setQuery($query); - $updateSitesNames = $db->loadObjectList('update_site_id'); - - // Gets Joomla core update sites Ids. - $joomlaUpdateSitesIds = $this->getJoomlaUpdateSitesIds(0); - - // Enable the update site in the table and store it in the database - foreach ($ids as $i => $id) - { - // Don't allow to delete Joomla Core update sites. - if (in_array((int) $id, $joomlaUpdateSitesIds)) - { - $app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_UPDATESITES_DELETE_CANNOT_DELETE', $updateSitesNames[$id]->name), 'error'); - continue; - } - - // Delete the update site from all tables. - try - { - $id = (int) $id; - $query = $db->getQuery(true) - ->delete($db->quoteName('#__update_sites')) - ->where($db->quoteName('update_site_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - $query = $db->getQuery(true) - ->delete($db->quoteName('#__update_sites_extensions')) - ->where($db->quoteName('update_site_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - $query = $db->getQuery(true) - ->delete($db->quoteName('#__updates')) - ->where($db->quoteName('update_site_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - $count++; - } - catch (RuntimeException $e) - { - $app->enqueueMessage( - Text::sprintf( - 'COM_INSTALLER_MSG_UPDATESITES_DELETE_ERROR', - $updateSitesNames[$id]->name, $e->getMessage() - ), 'error' - ); - } - } - - if ($count > 0) - { - $app->enqueueMessage(Text::plural('COM_INSTALLER_MSG_UPDATESITES_N_DELETE_UPDATESITES_DELETED', $count), 'message'); - } - } - - /** - * Fetch the Joomla update sites ids. - * - * @param integer $column Column to return. 0 for update site ids, 1 for extension ids. - * - * @return array Array with joomla core update site ids. - * - * @since 3.6.0 - */ - protected function getJoomlaUpdateSitesIds($column = 0) - { - $db = $this->getDatabase(); - - // Fetch the Joomla core update sites ids and their extension ids. We search for all except the core joomla extension with update sites. - $query = $db->getQuery(true) - ->select($db->quoteName(['use.update_site_id', 'e.extension_id'])) - ->from($db->quoteName('#__update_sites_extensions', 'use')) - ->join( - 'LEFT', - $db->quoteName('#__update_sites', 'us'), - $db->quoteName('us.update_site_id') . ' = ' . $db->quoteName('use.update_site_id') - ) - ->join( - 'LEFT', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id') - ) - ->where('(' - . '(' . $db->quoteName('e.type') . ' = ' . $db->quote('file') . - ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quote('joomla') . ')' - . ' OR (' . $db->quoteName('e.type') . ' = ' . $db->quote('package') . ' AND ' . $db->quoteName('e.element') - . ' = ' . $db->quote('pkg_en-GB') . ') OR (' . $db->quoteName('e.type') . ' = ' . $db->quote('component') - . ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quote('com_joomlaupdate') . ')' - . ')' - ); - - $db->setQuery($query); - - return $db->loadColumn($column); - } - - /** - * Rebuild update sites tables. - * - * @return void - * - * @throws Exception on ACL error - * @since 3.6 - * - */ - public function rebuild(): void - { - if (!Factory::getUser()->authorise('core.admin', 'com_installer')) - { - throw new Exception(Text::_('COM_INSTALLER_MSG_UPDATESITES_REBUILD_NOT_PERMITTED'), 403); - } - - $db = $this->getDatabase(); - $app = Factory::getApplication(); - - // Check if Joomla Extension plugin is enabled. - if (!PluginHelper::isEnabled('extension', 'joomla')) - { - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('element') . ' = ' . $db->quote('joomla')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('extension')); - $db->setQuery($query); - - $pluginId = (int) $db->loadResult(); - - $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . $pluginId); - $app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_UPDATESITES_REBUILD_EXTENSION_PLUGIN_NOT_ENABLED', $link), 'error'); - - return; - } - - $clients = [JPATH_SITE, JPATH_ADMINISTRATOR, JPATH_API]; - $extensionGroupFolders = ['components', 'modules', 'plugins', 'templates', 'language', 'manifests']; - - $pathsToSearch = []; - - // Identifies which folders to search for manifest files. - foreach ($clients as $clientPath) - { - foreach ($extensionGroupFolders as $extensionGroupFolderName) - { - // Components, modules, plugins, templates, languages and manifest (files, libraries, etc) - if ($extensionGroupFolderName !== 'plugins') - { - foreach (glob($clientPath . '/' . $extensionGroupFolderName . '/*', GLOB_NOSORT | GLOB_ONLYDIR) as $extensionFolderPath) - { - $pathsToSearch[] = $extensionFolderPath; - } - } - else - { - // Plugins (another directory level is needed) - foreach (glob($clientPath . '/' . $extensionGroupFolderName . '/*', - GLOB_NOSORT | GLOB_ONLYDIR - ) as $pluginGroupFolderPath) - { - foreach (glob($pluginGroupFolderPath . '/*', GLOB_NOSORT | GLOB_ONLYDIR) as $extensionFolderPath) - { - $pathsToSearch[] = $extensionFolderPath; - } - } - } - } - } - - // Gets Joomla core update sites Ids. - $joomlaUpdateSitesIds = $this->getJoomlaUpdateSitesIds(0); - - // First backup any custom extra_query for the sites - $query = $db->getQuery(true) - ->select('TRIM(' . $db->quoteName('location') . ') AS ' . $db->quoteName('location') . ', ' . $db->quoteName('extra_query')) - ->from($db->quoteName('#__update_sites')); - $db->setQuery($query); - $backupExtraQuerys = $db->loadAssocList('location'); - - // Delete from all tables (except joomla core update sites). - $query = $db->getQuery(true) - ->delete($db->quoteName('#__update_sites')) - ->whereNotIn($db->quoteName('update_site_id'), $joomlaUpdateSitesIds); - $db->setQuery($query); - $db->execute(); - - $query = $db->getQuery(true) - ->delete($db->quoteName('#__update_sites_extensions')) - ->whereNotIn($db->quoteName('update_site_id'), $joomlaUpdateSitesIds); - $db->setQuery($query); - $db->execute(); - - $query = $db->getQuery(true) - ->delete($db->quoteName('#__updates')) - ->whereNotIn($db->quoteName('update_site_id'), $joomlaUpdateSitesIds); - $db->setQuery($query); - $db->execute(); - - $count = 0; - - // Gets Joomla core extension Ids. - $joomlaCoreExtensionIds = $this->getJoomlaUpdateSitesIds(1); - - // Search for updateservers in manifest files inside the folders to search. - foreach ($pathsToSearch as $extensionFolderPath) - { - $tmpInstaller = new Installer; - $tmpInstaller->setDatabase($this->getDatabase()); - - $tmpInstaller->setPath('source', $extensionFolderPath); - - // Main folder manifests (higher priority) - $parentXmlfiles = Folder::files($tmpInstaller->getPath('source'), '.xml$', false, true); - - // Search for children manifests (lower priority) - $allXmlFiles = Folder::files($tmpInstaller->getPath('source'), '.xml$', 1, true); - - // Create a unique array of files ordered by priority - $xmlfiles = array_unique(array_merge($parentXmlfiles, $allXmlFiles)); - - if (!empty($xmlfiles)) - { - foreach ($xmlfiles as $file) - { - // Is it a valid Joomla installation manifest file? - $manifest = $tmpInstaller->isManifest($file); - - if ($manifest !== null) - { - /** - * Search if the extension exists in the extensions table. Excluding Joomla - * core extensions and discovered but not yet installed extensions. - */ - - $name = (string) $manifest->name; - $pkgName = (string) $manifest->packagename; - $type = (string) $manifest['type']; - - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where( - [ - $db->quoteName('type') . ' = :type', - $db->quoteName('state') . ' != -1', - ] - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('name') . ' = :name', - $db->quoteName('name') . ' = :pkgname', - ], - 'OR' - ) - ->whereNotIn($db->quoteName('extension_id'), $joomlaCoreExtensionIds) - ->bind(':name', $name) - ->bind(':pkgname', $pkgName) - ->bind(':type', $type); - $db->setQuery($query); - - $eid = (int) $db->loadResult(); - - if ($eid && $manifest->updateservers) - { - // Set the manifest object and path - $tmpInstaller->manifest = $manifest; - $tmpInstaller->setPath('manifest', $file); - - // Remove last extra_query as we are in a foreach - $tmpInstaller->extraQuery = ''; - - if ($tmpInstaller->manifest->updateservers - && $tmpInstaller->manifest->updateservers->server - && isset($backupExtraQuerys[trim((string) $tmpInstaller->manifest->updateservers->server)])) - { - $tmpInstaller->extraQuery = $backupExtraQuerys[trim((string) $tmpInstaller->manifest->updateservers->server)]['extra_query']; - } - - // Load the extension plugin (if not loaded yet). - PluginHelper::importPlugin('extension', 'joomla'); - - // Fire the onExtensionAfterUpdate - $app->triggerEvent('onExtensionAfterUpdate', ['installer' => $tmpInstaller, 'eid' => $eid]); - - $count++; - } - } - } - } - } - - if ($count > 0) - { - $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_UPDATESITES_REBUILD_SUCCESS'), 'message'); - } - else - { - $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_UPDATESITES_REBUILD_MESSAGE'), 'message'); - } - - // Flush the system cache to ensure extra_query is correctly loaded next time. - $this->cleanCache('_system'); - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 4.0.0 - */ - public function getItems() - { - $items = parent::getItems(); - - array_walk($items, - static function ($item) { - $data = new CMSObject($item); - $item->downloadKey = InstallerHelper::getDownloadKey($data); - } - ); - - return $items; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 3.4 - */ - protected function populateState($ordering = 'name', $direction = 'asc') - { - // Load the filter state. - $stateKeys = [ - 'search' => 'string', - 'client_id' => 'int', - 'enabled' => 'string', - 'type' => 'string', - 'folder' => 'string', - 'supported' => 'int', - ]; - - foreach ($stateKeys as $key => $filterType) - { - $stateKey = 'filter.' . $key; - - switch ($filterType) - { - case 'int': - case 'bool': - $default = null; - break; - - default: - $default = ''; - break; - } - - $stateValue = $this->getUserStateFromRequest( - $this->context . '.' . $stateKey, 'filter_' . $key, $default, $filterType - ); - - $this->setState($stateKey, $stateValue); - } - - parent::populateState($ordering, $direction); - } - - protected function getStoreId($id = '') - { - $id .= ':' . $this->getState('search'); - $id .= ':' . $this->getState('client_id'); - $id .= ':' . $this->getState('enabled'); - $id .= ':' . $this->getState('type'); - $id .= ':' . $this->getState('folder'); - $id .= ':' . $this->getState('supported'); - - return parent::getStoreId($id); - } - - /** - * Method to get the database query - * - * @return \Joomla\Database\DatabaseQuery The database query - * - * @since 3.4 - */ - protected function getListQuery() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select( - $db->quoteName( - [ - 's.update_site_id', - 's.name', - 's.type', - 's.location', - 's.enabled', - 's.checked_out', - 's.checked_out_time', - 's.extra_query', - 'e.extension_id', - 'e.name', - 'e.type', - 'e.element', - 'e.folder', - 'e.client_id', - 'e.state', - 'e.manifest_cache', - 'u.name' - ], - [ - 'update_site_id', - 'update_site_name', - 'update_site_type', - 'location', - 'enabled', - 'checked_out', - 'checked_out_time', - 'extra_query', - 'extension_id', - 'name', - 'type', - 'element', - 'folder', - 'client_id', - 'state', - 'manifest_cache', - 'editor' - ] - ) - ) - ->from($db->quoteName('#__update_sites', 's')) - ->join( - 'INNER', - $db->quoteName('#__update_sites_extensions', 'se'), - $db->quoteName('se.update_site_id') . ' = ' . $db->quoteName('s.update_site_id') - ) - ->join( - 'INNER', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('se.extension_id') - ) - ->join( - 'LEFT', - $db->quoteName('#__users', 'u'), - $db->quoteName('s.checked_out') . ' = ' . $db->quoteName('u.id') - ) - ->where($db->quoteName('state') . ' = 0'); - - // Process select filters. - $supported = $this->getState('filter.supported'); - $enabled = $this->getState('filter.enabled'); - $type = $this->getState('filter.type'); - $clientId = $this->getState('filter.client_id'); - $folder = $this->getState('filter.folder'); - - if ($enabled !== '') - { - $enabled = (int) $enabled; - $query->where($db->quoteName('s.enabled') . ' = :enabled') - ->bind(':enabled', $enabled, ParameterType::INTEGER); - } - - if ($type) - { - $query->where($db->quoteName('e.type') . ' = :type') - ->bind(':type', $type); - } - - if ($clientId !== null && $clientId !== '') - { - $clientId = (int) $clientId; - $query->where($db->quoteName('e.client_id') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - } - - if ($folder !== '' && in_array($type, ['plugin', 'library', ''], true)) - { - $folderForBinding = $folder === '*' ? '' : $folder; - $query->where($db->quoteName('e.folder') . ' = :folder') - ->bind(':folder', $folderForBinding); - } - - // Process search filter (update site id). - $search = $this->getState('filter.search'); - - if (!empty($search) && stripos($search, 'id:') === 0) - { - $uid = (int) substr($search, 3); - $query->where($db->quoteName('s.update_site_id') . ' = :siteId') - ->bind(':siteId', $uid, ParameterType::INTEGER); - } - - if (is_numeric($supported)) - { - switch ($supported) - { - // Show Update Sites which support Download Keys - case 1: - $supportedIDs = InstallerHelper::getDownloadKeySupportedSites($enabled); - break; - - // Show Update Sites which are missing Download Keys - case -1: - $supportedIDs = InstallerHelper::getDownloadKeyExistsSites(false, $enabled); - break; - - // Show Update Sites which have valid Download Keys - case 2: - $supportedIDs = InstallerHelper::getDownloadKeyExistsSites(true, $enabled); - break; - } - - if (!empty($supportedIDs)) - { - // Don't remove array_values(). whereIn expect a zero-based array. - $query->whereIn($db->qn('s.update_site_id'), array_values($supportedIDs)); - } - else - { - // In case of an empty list of IDs we apply a fake filter to effectively return no data - $query->where($db->qn('s.update_site_id') . ' = 0'); - } - } - - /** - * Note: The search for name, ordering and pagination are processed by the parent InstallerModel class (in - * extension.php). - */ - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @since 1.6 + * @see \Joomla\CMS\MVC\Model\ListModel + */ + public function __construct($config = [], MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = [ + 'update_site_name', + 'name', + 'client_id', + 'client', + 'client_translated', + 'status', + 'type', + 'type_translated', + 'folder', + 'folder_translated', + 'update_site_id', + 'enabled', + 'supported' + ]; + } + + parent::__construct($config, $factory); + } + + /** + * Enable/Disable an extension. + * + * @param array $eid Extension ids to un/publish + * @param int $value Publish value + * + * @return boolean True on success + * + * @throws Exception on ACL error + * @since 3.4 + * + */ + public function publish(&$eid = [], $value = 1) + { + if (!Factory::getUser()->authorise('core.edit.state', 'com_installer')) { + throw new Exception(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 403); + } + + $result = true; + + // Ensure eid is an array of extension ids + if (!is_array($eid)) { + $eid = [$eid]; + } + + // Get a table object for the extension type + $table = new UpdateSiteTable($this->getDatabase()); + + // Enable the update site in the table and store it in the database + foreach ($eid as $i => $id) { + $table->load($id); + $table->enabled = $value; + + if (!$table->store()) { + $this->setError($table->getError()); + $result = false; + } + } + + return $result; + } + + /** + * Deletes an update site. + * + * @param array $ids Extension ids to delete. + * + * @return void + * + * @throws Exception on ACL error + * @since 3.6 + * + */ + public function delete($ids = []) + { + if (!Factory::getUser()->authorise('core.delete', 'com_installer')) { + throw new Exception(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 403); + } + + // Ensure eid is an array of extension ids + if (!is_array($ids)) { + $ids = [$ids]; + } + + $db = $this->getDatabase(); + $app = Factory::getApplication(); + + $count = 0; + + // Gets the update site names. + $query = $db->getQuery(true) + ->select($db->quoteName(['update_site_id', 'name'])) + ->from($db->quoteName('#__update_sites')) + ->whereIn($db->quoteName('update_site_id'), $ids); + $db->setQuery($query); + $updateSitesNames = $db->loadObjectList('update_site_id'); + + // Gets Joomla core update sites Ids. + $joomlaUpdateSitesIds = $this->getJoomlaUpdateSitesIds(0); + + // Enable the update site in the table and store it in the database + foreach ($ids as $i => $id) { + // Don't allow to delete Joomla Core update sites. + if (in_array((int) $id, $joomlaUpdateSitesIds)) { + $app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_UPDATESITES_DELETE_CANNOT_DELETE', $updateSitesNames[$id]->name), 'error'); + continue; + } + + // Delete the update site from all tables. + try { + $id = (int) $id; + $query = $db->getQuery(true) + ->delete($db->quoteName('#__update_sites')) + ->where($db->quoteName('update_site_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__update_sites_extensions')) + ->where($db->quoteName('update_site_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__updates')) + ->where($db->quoteName('update_site_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + $count++; + } catch (RuntimeException $e) { + $app->enqueueMessage( + Text::sprintf( + 'COM_INSTALLER_MSG_UPDATESITES_DELETE_ERROR', + $updateSitesNames[$id]->name, + $e->getMessage() + ), + 'error' + ); + } + } + + if ($count > 0) { + $app->enqueueMessage(Text::plural('COM_INSTALLER_MSG_UPDATESITES_N_DELETE_UPDATESITES_DELETED', $count), 'message'); + } + } + + /** + * Fetch the Joomla update sites ids. + * + * @param integer $column Column to return. 0 for update site ids, 1 for extension ids. + * + * @return array Array with joomla core update site ids. + * + * @since 3.6.0 + */ + protected function getJoomlaUpdateSitesIds($column = 0) + { + $db = $this->getDatabase(); + + // Fetch the Joomla core update sites ids and their extension ids. We search for all except the core joomla extension with update sites. + $query = $db->getQuery(true) + ->select($db->quoteName(['use.update_site_id', 'e.extension_id'])) + ->from($db->quoteName('#__update_sites_extensions', 'use')) + ->join( + 'LEFT', + $db->quoteName('#__update_sites', 'us'), + $db->quoteName('us.update_site_id') . ' = ' . $db->quoteName('use.update_site_id') + ) + ->join( + 'LEFT', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('use.extension_id') + ) + ->where('(' + . '(' . $db->quoteName('e.type') . ' = ' . $db->quote('file') . + ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quote('joomla') . ')' + . ' OR (' . $db->quoteName('e.type') . ' = ' . $db->quote('package') . ' AND ' . $db->quoteName('e.element') + . ' = ' . $db->quote('pkg_en-GB') . ') OR (' . $db->quoteName('e.type') . ' = ' . $db->quote('component') + . ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quote('com_joomlaupdate') . ')' + . ')'); + + $db->setQuery($query); + + return $db->loadColumn($column); + } + + /** + * Rebuild update sites tables. + * + * @return void + * + * @throws Exception on ACL error + * @since 3.6 + * + */ + public function rebuild(): void + { + if (!Factory::getUser()->authorise('core.admin', 'com_installer')) { + throw new Exception(Text::_('COM_INSTALLER_MSG_UPDATESITES_REBUILD_NOT_PERMITTED'), 403); + } + + $db = $this->getDatabase(); + $app = Factory::getApplication(); + + // Check if Joomla Extension plugin is enabled. + if (!PluginHelper::isEnabled('extension', 'joomla')) { + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('element') . ' = ' . $db->quote('joomla')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('extension')); + $db->setQuery($query); + + $pluginId = (int) $db->loadResult(); + + $link = Route::_('index.php?option=com_plugins&task=plugin.edit&extension_id=' . $pluginId); + $app->enqueueMessage(Text::sprintf('COM_INSTALLER_MSG_UPDATESITES_REBUILD_EXTENSION_PLUGIN_NOT_ENABLED', $link), 'error'); + + return; + } + + $clients = [JPATH_SITE, JPATH_ADMINISTRATOR, JPATH_API]; + $extensionGroupFolders = ['components', 'modules', 'plugins', 'templates', 'language', 'manifests']; + + $pathsToSearch = []; + + // Identifies which folders to search for manifest files. + foreach ($clients as $clientPath) { + foreach ($extensionGroupFolders as $extensionGroupFolderName) { + // Components, modules, plugins, templates, languages and manifest (files, libraries, etc) + if ($extensionGroupFolderName !== 'plugins') { + foreach (glob($clientPath . '/' . $extensionGroupFolderName . '/*', GLOB_NOSORT | GLOB_ONLYDIR) as $extensionFolderPath) { + $pathsToSearch[] = $extensionFolderPath; + } + } else { + // Plugins (another directory level is needed) + foreach ( + glob( + $clientPath . '/' . $extensionGroupFolderName . '/*', + GLOB_NOSORT | GLOB_ONLYDIR + ) as $pluginGroupFolderPath + ) { + foreach (glob($pluginGroupFolderPath . '/*', GLOB_NOSORT | GLOB_ONLYDIR) as $extensionFolderPath) { + $pathsToSearch[] = $extensionFolderPath; + } + } + } + } + } + + // Gets Joomla core update sites Ids. + $joomlaUpdateSitesIds = $this->getJoomlaUpdateSitesIds(0); + + // First backup any custom extra_query for the sites + $query = $db->getQuery(true) + ->select('TRIM(' . $db->quoteName('location') . ') AS ' . $db->quoteName('location') . ', ' . $db->quoteName('extra_query')) + ->from($db->quoteName('#__update_sites')); + $db->setQuery($query); + $backupExtraQuerys = $db->loadAssocList('location'); + + // Delete from all tables (except joomla core update sites). + $query = $db->getQuery(true) + ->delete($db->quoteName('#__update_sites')) + ->whereNotIn($db->quoteName('update_site_id'), $joomlaUpdateSitesIds); + $db->setQuery($query); + $db->execute(); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__update_sites_extensions')) + ->whereNotIn($db->quoteName('update_site_id'), $joomlaUpdateSitesIds); + $db->setQuery($query); + $db->execute(); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__updates')) + ->whereNotIn($db->quoteName('update_site_id'), $joomlaUpdateSitesIds); + $db->setQuery($query); + $db->execute(); + + $count = 0; + + // Gets Joomla core extension Ids. + $joomlaCoreExtensionIds = $this->getJoomlaUpdateSitesIds(1); + + // Search for updateservers in manifest files inside the folders to search. + foreach ($pathsToSearch as $extensionFolderPath) { + $tmpInstaller = new Installer(); + $tmpInstaller->setDatabase($this->getDatabase()); + + $tmpInstaller->setPath('source', $extensionFolderPath); + + // Main folder manifests (higher priority) + $parentXmlfiles = Folder::files($tmpInstaller->getPath('source'), '.xml$', false, true); + + // Search for children manifests (lower priority) + $allXmlFiles = Folder::files($tmpInstaller->getPath('source'), '.xml$', 1, true); + + // Create a unique array of files ordered by priority + $xmlfiles = array_unique(array_merge($parentXmlfiles, $allXmlFiles)); + + if (!empty($xmlfiles)) { + foreach ($xmlfiles as $file) { + // Is it a valid Joomla installation manifest file? + $manifest = $tmpInstaller->isManifest($file); + + if ($manifest !== null) { + /** + * Search if the extension exists in the extensions table. Excluding Joomla + * core extensions and discovered but not yet installed extensions. + */ + + $name = (string) $manifest->name; + $pkgName = (string) $manifest->packagename; + $type = (string) $manifest['type']; + + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where( + [ + $db->quoteName('type') . ' = :type', + $db->quoteName('state') . ' != -1', + ] + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('name') . ' = :name', + $db->quoteName('name') . ' = :pkgname', + ], + 'OR' + ) + ->whereNotIn($db->quoteName('extension_id'), $joomlaCoreExtensionIds) + ->bind(':name', $name) + ->bind(':pkgname', $pkgName) + ->bind(':type', $type); + $db->setQuery($query); + + $eid = (int) $db->loadResult(); + + if ($eid && $manifest->updateservers) { + // Set the manifest object and path + $tmpInstaller->manifest = $manifest; + $tmpInstaller->setPath('manifest', $file); + + // Remove last extra_query as we are in a foreach + $tmpInstaller->extraQuery = ''; + + if ( + $tmpInstaller->manifest->updateservers + && $tmpInstaller->manifest->updateservers->server + && isset($backupExtraQuerys[trim((string) $tmpInstaller->manifest->updateservers->server)]) + ) { + $tmpInstaller->extraQuery = $backupExtraQuerys[trim((string) $tmpInstaller->manifest->updateservers->server)]['extra_query']; + } + + // Load the extension plugin (if not loaded yet). + PluginHelper::importPlugin('extension', 'joomla'); + + // Fire the onExtensionAfterUpdate + $app->triggerEvent('onExtensionAfterUpdate', ['installer' => $tmpInstaller, 'eid' => $eid]); + + $count++; + } + } + } + } + } + + if ($count > 0) { + $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_UPDATESITES_REBUILD_SUCCESS'), 'message'); + } else { + $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_UPDATESITES_REBUILD_MESSAGE'), 'message'); + } + + // Flush the system cache to ensure extra_query is correctly loaded next time. + $this->cleanCache('_system'); + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 4.0.0 + */ + public function getItems() + { + $items = parent::getItems(); + + array_walk( + $items, + static function ($item) { + $data = new CMSObject($item); + $item->downloadKey = InstallerHelper::getDownloadKey($data); + } + ); + + return $items; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.4 + */ + protected function populateState($ordering = 'name', $direction = 'asc') + { + // Load the filter state. + $stateKeys = [ + 'search' => 'string', + 'client_id' => 'int', + 'enabled' => 'string', + 'type' => 'string', + 'folder' => 'string', + 'supported' => 'int', + ]; + + foreach ($stateKeys as $key => $filterType) { + $stateKey = 'filter.' . $key; + + switch ($filterType) { + case 'int': + case 'bool': + $default = null; + break; + + default: + $default = ''; + break; + } + + $stateValue = $this->getUserStateFromRequest( + $this->context . '.' . $stateKey, + 'filter_' . $key, + $default, + $filterType + ); + + $this->setState($stateKey, $stateValue); + } + + parent::populateState($ordering, $direction); + } + + protected function getStoreId($id = '') + { + $id .= ':' . $this->getState('search'); + $id .= ':' . $this->getState('client_id'); + $id .= ':' . $this->getState('enabled'); + $id .= ':' . $this->getState('type'); + $id .= ':' . $this->getState('folder'); + $id .= ':' . $this->getState('supported'); + + return parent::getStoreId($id); + } + + /** + * Method to get the database query + * + * @return \Joomla\Database\DatabaseQuery The database query + * + * @since 3.4 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + $db->quoteName( + [ + 's.update_site_id', + 's.name', + 's.type', + 's.location', + 's.enabled', + 's.checked_out', + 's.checked_out_time', + 's.extra_query', + 'e.extension_id', + 'e.name', + 'e.type', + 'e.element', + 'e.folder', + 'e.client_id', + 'e.state', + 'e.manifest_cache', + 'u.name' + ], + [ + 'update_site_id', + 'update_site_name', + 'update_site_type', + 'location', + 'enabled', + 'checked_out', + 'checked_out_time', + 'extra_query', + 'extension_id', + 'name', + 'type', + 'element', + 'folder', + 'client_id', + 'state', + 'manifest_cache', + 'editor' + ] + ) + ) + ->from($db->quoteName('#__update_sites', 's')) + ->join( + 'INNER', + $db->quoteName('#__update_sites_extensions', 'se'), + $db->quoteName('se.update_site_id') . ' = ' . $db->quoteName('s.update_site_id') + ) + ->join( + 'INNER', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('se.extension_id') + ) + ->join( + 'LEFT', + $db->quoteName('#__users', 'u'), + $db->quoteName('s.checked_out') . ' = ' . $db->quoteName('u.id') + ) + ->where($db->quoteName('state') . ' = 0'); + + // Process select filters. + $supported = $this->getState('filter.supported'); + $enabled = $this->getState('filter.enabled'); + $type = $this->getState('filter.type'); + $clientId = $this->getState('filter.client_id'); + $folder = $this->getState('filter.folder'); + + if ($enabled !== '') { + $enabled = (int) $enabled; + $query->where($db->quoteName('s.enabled') . ' = :enabled') + ->bind(':enabled', $enabled, ParameterType::INTEGER); + } + + if ($type) { + $query->where($db->quoteName('e.type') . ' = :type') + ->bind(':type', $type); + } + + if ($clientId !== null && $clientId !== '') { + $clientId = (int) $clientId; + $query->where($db->quoteName('e.client_id') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + } + + if ($folder !== '' && in_array($type, ['plugin', 'library', ''], true)) { + $folderForBinding = $folder === '*' ? '' : $folder; + $query->where($db->quoteName('e.folder') . ' = :folder') + ->bind(':folder', $folderForBinding); + } + + // Process search filter (update site id). + $search = $this->getState('filter.search'); + + if (!empty($search) && stripos($search, 'id:') === 0) { + $uid = (int) substr($search, 3); + $query->where($db->quoteName('s.update_site_id') . ' = :siteId') + ->bind(':siteId', $uid, ParameterType::INTEGER); + } + + if (is_numeric($supported)) { + switch ($supported) { + // Show Update Sites which support Download Keys + case 1: + $supportedIDs = InstallerHelper::getDownloadKeySupportedSites($enabled); + break; + + // Show Update Sites which are missing Download Keys + case -1: + $supportedIDs = InstallerHelper::getDownloadKeyExistsSites(false, $enabled); + break; + + // Show Update Sites which have valid Download Keys + case 2: + $supportedIDs = InstallerHelper::getDownloadKeyExistsSites(true, $enabled); + break; + } + + if (!empty($supportedIDs)) { + // Don't remove array_values(). whereIn expect a zero-based array. + $query->whereIn($db->qn('s.update_site_id'), array_values($supportedIDs)); + } else { + // In case of an empty list of IDs we apply a fake filter to effectively return no data + $query->where($db->qn('s.update_site_id') . ' = 0'); + } + } + + /** + * Note: The search for name, ordering and pagination are processed by the parent InstallerModel class (in + * extension.php). + */ + + return $query; + } } diff --git a/administrator/components/com_installer/src/Model/WarningsModel.php b/administrator/components/com_installer/src/Model/WarningsModel.php index ff3d20e5d1ce5..8b5f5c0d9beed 100644 --- a/administrator/components/com_installer/src/Model/WarningsModel.php +++ b/administrator/components/com_installer/src/Model/WarningsModel.php @@ -1,4 +1,5 @@ Text::_('COM_INSTALLER_MSG_WARNINGS_FILEUPLOADSDISABLED'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_FILEUPLOADISDISABLEDDESC'), - ]; - } - - $upload_dir = ini_get('upload_tmp_dir'); - - if (!$upload_dir) - { - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSET'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSETDESC'), - ]; - } - elseif (!is_writable($upload_dir)) - { - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTWRITEABLE'), - 'description' => Text::sprintf('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTWRITEABLEDESC', $upload_dir), - ]; - } - - $tmp_path = Factory::getApplication()->get('tmp_path'); - - if (!$tmp_path) - { - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSET'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSETDESC'), - ]; - } - elseif (!is_writable($tmp_path)) - { - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLE'), - 'description' => Text::sprintf('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLEDESC', $tmp_path), - ]; - } - - $memory_limit = $this->return_bytes(ini_get('memory_limit')); - - if ($memory_limit > -1) - { - if ($memory_limit < $minLimit) - { - // 16MB - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_LOWMEMORYWARN'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_LOWMEMORYDESC'), - ]; - } - elseif ($memory_limit < ($minLimit * 1.5)) - { - // 24MB - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_MEDMEMORYWARN'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_MEDMEMORYDESC'), - ]; - } - } - - $post_max_size = $this->return_bytes(ini_get('post_max_size')); - $upload_max_filesize = $this->return_bytes(ini_get('upload_max_filesize')); - - if ($post_max_size > 0 && $post_max_size < $upload_max_filesize) - { - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_UPLOADBIGGERTHANPOST'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_UPLOADBIGGERTHANPOSTDESC'), - ]; - } - - if ($post_max_size > 0 && $post_max_size < $minLimit) - { - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLPOSTSIZE'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLPOSTSIZEDESC'), - ]; - } - - if ($upload_max_filesize > 0 && $upload_max_filesize < $minLimit) - { - $messages[] = [ - 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZE'), - 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZEDESC'), - ]; - } - - return $messages; - } + /** + * Extension Type + * @var string + */ + public $type = 'warnings'; + + /** + * Return the byte value of a particular string. + * + * @param string $val String optionally with G, M or K suffix + * + * @return integer size in bytes + * + * @since 1.6 + */ + public function return_bytes($val) + { + if (empty($val)) { + return 0; + } + + $val = trim($val); + + preg_match('#([0-9]+)[\s]*([a-z]+)#i', $val, $matches); + + $last = ''; + + if (isset($matches[2])) { + $last = $matches[2]; + } + + if (isset($matches[1])) { + $val = (int) $matches[1]; + } + + switch (strtolower($last)) { + case 'g': + case 'gb': + $val *= (1024 * 1024 * 1024); + break; + case 'm': + case 'mb': + $val *= (1024 * 1024); + break; + case 'k': + case 'kb': + $val *= 1024; + break; + } + + return (int) $val; + } + + /** + * Load the data. + * + * @return array Messages + * + * @since 1.6 + */ + public function getItems() + { + static $messages; + + if ($messages) { + return $messages; + } + + $messages = []; + + // 16MB + $minLimit = 16 * 1024 * 1024; + + $file_uploads = ini_get('file_uploads'); + + if (!$file_uploads) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_FILEUPLOADSDISABLED'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_FILEUPLOADISDISABLEDDESC'), + ]; + } + + $upload_dir = ini_get('upload_tmp_dir'); + + if (!$upload_dir) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSET'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSETDESC'), + ]; + } elseif (!is_writable($upload_dir)) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTWRITEABLE'), + 'description' => Text::sprintf('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTWRITEABLEDESC', $upload_dir), + ]; + } + + $tmp_path = Factory::getApplication()->get('tmp_path'); + + if (!$tmp_path) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSET'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTSETDESC'), + ]; + } elseif (!is_writable($tmp_path)) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLE'), + 'description' => Text::sprintf('COM_INSTALLER_MSG_WARNINGS_JOOMLATMPNOTWRITEABLEDESC', $tmp_path), + ]; + } + + $memory_limit = $this->return_bytes(ini_get('memory_limit')); + + if ($memory_limit > -1) { + if ($memory_limit < $minLimit) { + // 16MB + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_LOWMEMORYWARN'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_LOWMEMORYDESC'), + ]; + } elseif ($memory_limit < ($minLimit * 1.5)) { + // 24MB + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_MEDMEMORYWARN'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_MEDMEMORYDESC'), + ]; + } + } + + $post_max_size = $this->return_bytes(ini_get('post_max_size')); + $upload_max_filesize = $this->return_bytes(ini_get('upload_max_filesize')); + + if ($post_max_size > 0 && $post_max_size < $upload_max_filesize) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_UPLOADBIGGERTHANPOST'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_UPLOADBIGGERTHANPOSTDESC'), + ]; + } + + if ($post_max_size > 0 && $post_max_size < $minLimit) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLPOSTSIZE'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLPOSTSIZEDESC'), + ]; + } + + if ($upload_max_filesize > 0 && $upload_max_filesize < $minLimit) { + $messages[] = [ + 'message' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZE'), + 'description' => Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZEDESC'), + ]; + } + + return $messages; + } } diff --git a/administrator/components/com_installer/src/Service/HTML/Manage.php b/administrator/components/com_installer/src/Service/HTML/Manage.php index 35a10d69450d1..0c9ec3883a3ea 100644 --- a/administrator/components/com_installer/src/Service/HTML/Manage.php +++ b/administrator/components/com_installer/src/Service/HTML/Manage.php @@ -1,4 +1,5 @@ array( - '', - 'COM_INSTALLER_EXTENSION_PROTECTED', - '', - 'COM_INSTALLER_EXTENSION_PROTECTED', - true, - 'protected', - 'protected', - ), - 1 => array( - 'unpublish', - 'COM_INSTALLER_EXTENSION_ENABLED', - 'COM_INSTALLER_EXTENSION_DISABLE', - 'COM_INSTALLER_EXTENSION_ENABLED', - true, - 'publish', - 'publish', - ), - 0 => array( - 'publish', - 'COM_INSTALLER_EXTENSION_DISABLED', - 'COM_INSTALLER_EXTENSION_ENABLE', - 'COM_INSTALLER_EXTENSION_DISABLED', - true, - 'unpublish', - 'unpublish', - ), - ); + /** + * Returns a published state on a grid. + * + * @param integer $value The state value. + * @param integer $i The row index. + * @param boolean $enabled An optional setting for access control on the action. + * @param string $checkbox An optional prefix for checkboxes. + * + * @return string The Html code + * + * @see JHtmlJGrid::state + * + * @since 2.5 + */ + public function state($value, $i, $enabled = true, $checkbox = 'cb') + { + $states = array( + 2 => array( + '', + 'COM_INSTALLER_EXTENSION_PROTECTED', + '', + 'COM_INSTALLER_EXTENSION_PROTECTED', + true, + 'protected', + 'protected', + ), + 1 => array( + 'unpublish', + 'COM_INSTALLER_EXTENSION_ENABLED', + 'COM_INSTALLER_EXTENSION_DISABLE', + 'COM_INSTALLER_EXTENSION_ENABLED', + true, + 'publish', + 'publish', + ), + 0 => array( + 'publish', + 'COM_INSTALLER_EXTENSION_DISABLED', + 'COM_INSTALLER_EXTENSION_ENABLE', + 'COM_INSTALLER_EXTENSION_DISABLED', + true, + 'unpublish', + 'unpublish', + ), + ); - return HTMLHelper::_('jgrid.state', $states, $value, $i, 'manage.', $enabled, true, $checkbox); - } + return HTMLHelper::_('jgrid.state', $states, $value, $i, 'manage.', $enabled, true, $checkbox); + } } diff --git a/administrator/components/com_installer/src/Service/HTML/Updatesites.php b/administrator/components/com_installer/src/Service/HTML/Updatesites.php index 8f917c2c9841f..ad90d1e369801 100644 --- a/administrator/components/com_installer/src/Service/HTML/Updatesites.php +++ b/administrator/components/com_installer/src/Service/HTML/Updatesites.php @@ -1,4 +1,5 @@ array( - 'unpublish', - 'COM_INSTALLER_UPDATESITE_ENABLED', - 'COM_INSTALLER_UPDATESITE_DISABLE', - 'COM_INSTALLER_UPDATESITE_ENABLED', - true, - 'publish', - 'publish', - ), - 0 => array( - 'publish', - 'COM_INSTALLER_UPDATESITE_DISABLED', - 'COM_INSTALLER_UPDATESITE_ENABLE', - 'COM_INSTALLER_UPDATESITE_DISABLED', - true, - 'unpublish', - 'unpublish', - ), - ); + /** + * Returns a published state on a grid. + * + * @param integer $value The state value. + * @param integer $i The row index. + * @param boolean $enabled An optional setting for access control on the action. + * @param string $checkbox An optional prefix for checkboxes. + * + * @return string The HTML code + * + * @see JHtmlJGrid::state() + * @since 3.5 + */ + public function state($value, $i, $enabled = true, $checkbox = 'cb') + { + $states = array( + 1 => array( + 'unpublish', + 'COM_INSTALLER_UPDATESITE_ENABLED', + 'COM_INSTALLER_UPDATESITE_DISABLE', + 'COM_INSTALLER_UPDATESITE_ENABLED', + true, + 'publish', + 'publish', + ), + 0 => array( + 'publish', + 'COM_INSTALLER_UPDATESITE_DISABLED', + 'COM_INSTALLER_UPDATESITE_ENABLE', + 'COM_INSTALLER_UPDATESITE_DISABLED', + true, + 'unpublish', + 'unpublish', + ), + ); - return HTMLHelper::_('jgrid.state', $states, $value, $i, 'updatesites.', $enabled, true, $checkbox); - } + return HTMLHelper::_('jgrid.state', $states, $value, $i, 'updatesites.', $enabled, true, $checkbox); + } } diff --git a/administrator/components/com_installer/src/Table/UpdatesiteTable.php b/administrator/components/com_installer/src/Table/UpdatesiteTable.php index 57a5f8784afa6..ee6460f99eca9 100644 --- a/administrator/components/com_installer/src/Table/UpdatesiteTable.php +++ b/administrator/components/com_installer/src/Table/UpdatesiteTable.php @@ -1,4 +1,5 @@ typeAlias = 'com_installer.downloadkey'; + /** + * Constructor + * + * @param DatabaseDriver $db Database connector object + * + * @since 4.0.0 + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = 'com_installer.downloadkey'; - parent::__construct('#__update_sites', 'update_site_id', $db); - } + parent::__construct('#__update_sites', 'update_site_id', $db); + } } diff --git a/administrator/components/com_installer/src/View/Database/HtmlView.php b/administrator/components/com_installer/src/View/Database/HtmlView.php index 772e86d40b6d2..15a73675b3e3c 100644 --- a/administrator/components/com_installer/src/View/Database/HtmlView.php +++ b/administrator/components/com_installer/src/View/Database/HtmlView.php @@ -1,4 +1,5 @@ getModel(); + // Get data from the model. + /** @var DatabaseModel $model */ + $model = $this->getModel(); - try - { - $this->changeSet = $model->getItems(); - } - catch (\Exception $exception) - { - $app->enqueueMessage($exception->getMessage(), 'error'); - } + try { + $this->changeSet = $model->getItems(); + } catch (\Exception $exception) { + $app->enqueueMessage($exception->getMessage(), 'error'); + } - $this->errorCount = $model->getErrorCount(); - $this->pagination = $model->getPagination(); - $this->filterForm = $model->getFilterForm(); - $this->activeFilters = $model->getActiveFilters(); + $this->errorCount = $model->getErrorCount(); + $this->pagination = $model->getPagination(); + $this->filterForm = $model->getFilterForm(); + $this->activeFilters = $model->getActiveFilters(); - if ($this->changeSet) - { - ($this->errorCount === 0) - ? $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DATABASE_CORE_OK'), 'info') - : $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DATABASE_CORE_ERRORS'), 'warning'); - } + if ($this->changeSet) { + ($this->errorCount === 0) + ? $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DATABASE_CORE_OK'), 'info') + : $app->enqueueMessage(Text::_('COM_INSTALLER_MSG_DATABASE_CORE_ERRORS'), 'warning'); + } - parent::display($tpl); - } + parent::display($tpl); + } - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - /* - * Set toolbar items for the page. - */ - ToolbarHelper::custom('database.fix', 'refresh', '', 'COM_INSTALLER_TOOLBAR_DATABASE_FIX', true); - ToolbarHelper::divider(); - parent::addToolbar(); - ToolbarHelper::help('Information:_Database'); - } + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + /* + * Set toolbar items for the page. + */ + ToolbarHelper::custom('database.fix', 'refresh', '', 'COM_INSTALLER_TOOLBAR_DATABASE_FIX', true); + ToolbarHelper::divider(); + parent::addToolbar(); + ToolbarHelper::help('Information:_Database'); + } } diff --git a/administrator/components/com_installer/src/View/Discover/HtmlView.php b/administrator/components/com_installer/src/View/Discover/HtmlView.php index c923170296983..48fd330b11bcf 100644 --- a/administrator/components/com_installer/src/View/Discover/HtmlView.php +++ b/administrator/components/com_installer/src/View/Discover/HtmlView.php @@ -1,4 +1,5 @@ getModel()->checkExtensions()) - { - $this->getModel()->discover(); - } + /** + * Display the view. + * + * @param string $tpl Template + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + // Run discover from the model. + if (!$this->getModel()->checkExtensions()) { + $this->getModel()->discover(); + } - // Get data from the model. - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); + // Get data from the model. + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); - if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } + if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } - parent::display($tpl); - } + parent::display($tpl); + } - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.1 - */ - protected function addToolbar() - { - /* - * Set toolbar items for the page. - */ - if (!$this->isEmptyState) - { - ToolbarHelper::custom('discover.install', 'upload', '', 'JTOOLBAR_INSTALL', true); - } + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.1 + */ + protected function addToolbar() + { + /* + * Set toolbar items for the page. + */ + if (!$this->isEmptyState) { + ToolbarHelper::custom('discover.install', 'upload', '', 'JTOOLBAR_INSTALL', true); + } - ToolbarHelper::custom('discover.refresh', 'refresh', '', 'COM_INSTALLER_TOOLBAR_DISCOVER', false); - ToolbarHelper::divider(); + ToolbarHelper::custom('discover.refresh', 'refresh', '', 'COM_INSTALLER_TOOLBAR_DISCOVER', false); + ToolbarHelper::divider(); - parent::addToolbar(); + parent::addToolbar(); - ToolbarHelper::help('Extensions:_Discover'); - } + ToolbarHelper::help('Extensions:_Discover'); + } } diff --git a/administrator/components/com_installer/src/View/Install/HtmlView.php b/administrator/components/com_installer/src/View/Install/HtmlView.php index acd570b4c3d78..e2549474542c8 100644 --- a/administrator/components/com_installer/src/View/Install/HtmlView.php +++ b/administrator/components/com_installer/src/View/Install/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser()->authorise('core.admin')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + /** + * Display the view + * + * @param string $tpl Template + * + * @return void + * + * @since 1.5 + */ + public function display($tpl = null) + { + if (!$this->getCurrentUser()->authorise('core.admin')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } - $paths = new \stdClass; - $paths->first = ''; + $paths = new \stdClass(); + $paths->first = ''; - $this->paths = &$paths; + $this->paths = &$paths; - PluginHelper::importPlugin('installer'); + PluginHelper::importPlugin('installer'); - parent::display($tpl); - } + parent::display($tpl); + } - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - if (ContentHelper::getActions('com_installer')->get('core.manage')) - { - ToolbarHelper::link('index.php?option=com_installer&view=manage', 'COM_INSTALLER_TOOLBAR_MANAGE', 'list'); - ToolbarHelper::divider(); - } + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + if (ContentHelper::getActions('com_installer')->get('core.manage')) { + ToolbarHelper::link('index.php?option=com_installer&view=manage', 'COM_INSTALLER_TOOLBAR_MANAGE', 'list'); + ToolbarHelper::divider(); + } - parent::addToolbar(); + parent::addToolbar(); - ToolbarHelper::help('Extensions:_Install'); - } + ToolbarHelper::help('Extensions:_Install'); + } } diff --git a/administrator/components/com_installer/src/View/Installer/HtmlView.php b/administrator/components/com_installer/src/View/Installer/HtmlView.php index bf3cbb9b938a8..a930799cc43f8 100644 --- a/administrator/components/com_installer/src/View/Installer/HtmlView.php +++ b/administrator/components/com_installer/src/View/Installer/HtmlView.php @@ -1,4 +1,5 @@ _addPath('template', $this->_basePath . '/tmpl/installer'); - $this->_addPath('template', JPATH_THEMES . '/' . Factory::getApplication()->getTemplate() . '/html/com_installer/installer'); - } + $this->_addPath('template', $this->_basePath . '/tmpl/installer'); + $this->_addPath('template', JPATH_THEMES . '/' . Factory::getApplication()->getTemplate() . '/html/com_installer/installer'); + } - /** - * Display the view. - * - * @param string $tpl Template - * - * @return void - * - * @since 1.5 - */ - public function display($tpl = null) - { - // Get data from the model. - $state = $this->get('State'); + /** + * Display the view. + * + * @param string $tpl Template + * + * @return void + * + * @since 1.5 + */ + public function display($tpl = null) + { + // Get data from the model. + $state = $this->get('State'); - // Are there messages to display? - $showMessage = false; + // Are there messages to display? + $showMessage = false; - if (is_object($state)) - { - $message1 = $state->get('message'); - $message2 = $state->get('extension_message'); - $showMessage = ($message1 || $message2); - } + if (is_object($state)) { + $message1 = $state->get('message'); + $message2 = $state->get('extension_message'); + $showMessage = ($message1 || $message2); + } - $this->showMessage = $showMessage; - $this->state = &$state; + $this->showMessage = $showMessage; + $this->state = &$state; - $this->addToolbar(); - parent::display($tpl); - } + $this->addToolbar(); + parent::display($tpl); + } - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_installer'); - ToolbarHelper::title(Text::_('COM_INSTALLER_HEADER_' . strtoupper($this->getName())), 'puzzle-piece install'); + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_installer'); + ToolbarHelper::title(Text::_('COM_INSTALLER_HEADER_' . strtoupper($this->getName())), 'puzzle-piece install'); - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_installer'); - ToolbarHelper::divider(); - } - } + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_installer'); + ToolbarHelper::divider(); + } + } } diff --git a/administrator/components/com_installer/src/View/Languages/HtmlView.php b/administrator/components/com_installer/src/View/Languages/HtmlView.php index 41766b6330663..eb041e0b912fb 100644 --- a/administrator/components/com_installer/src/View/Languages/HtmlView.php +++ b/administrator/components/com_installer/src/View/Languages/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser()->authorise('core.admin')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + /** + * Display the view. + * + * @param null $tpl template to display + * + * @return mixed|void + */ + public function display($tpl = null) + { + if (!$this->getCurrentUser()->authorise('core.admin')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } - // Get data from the model. - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->installedLang = LanguageHelper::getInstalledLanguages(); + // Get data from the model. + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->installedLang = LanguageHelper::getInstalledLanguages(); - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } - parent::display($tpl); - } + parent::display($tpl); + } - /** - * Add the page title and toolbar. - * - * @return void - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_installer'); - ToolbarHelper::title(Text::_('COM_INSTALLER_HEADER_' . $this->getName()), 'puzzle-piece install'); + /** + * Add the page title and toolbar. + * + * @return void + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_installer'); + ToolbarHelper::title(Text::_('COM_INSTALLER_HEADER_' . $this->getName()), 'puzzle-piece install'); - if ($canDo->get('core.admin')) - { - parent::addToolbar(); + if ($canDo->get('core.admin')) { + parent::addToolbar(); - ToolbarHelper::help('Extensions:_Languages'); - } - } + ToolbarHelper::help('Extensions:_Languages'); + } + } } diff --git a/administrator/components/com_installer/src/View/Manage/HtmlView.php b/administrator/components/com_installer/src/View/Manage/HtmlView.php index 3262bae2834ae..aa83e462d1823 100644 --- a/administrator/components/com_installer/src/View/Manage/HtmlView.php +++ b/administrator/components/com_installer/src/View/Manage/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); + /** + * Display the view. + * + * @param string $tpl Template + * + * @return mixed|void + * + * @since 1.6 + */ + public function display($tpl = null) + { + // Get data from the model. + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } - // Display the view. - parent::display($tpl); - } + // Display the view. + parent::display($tpl); + } - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $toolbar = Toolbar::getInstance('toolbar'); - $canDo = ContentHelper::getActions('com_installer'); + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $toolbar = Toolbar::getInstance('toolbar'); + $canDo = ContentHelper::getActions('com_installer'); - if ($canDo->get('core.edit.state')) - { - $toolbar->publish('manage.publish') - ->text('JTOOLBAR_ENABLE') - ->listCheck(true); - $toolbar->unpublish('manage.unpublish') - ->text('JTOOLBAR_DISABLE') - ->listCheck(true); - $toolbar->divider(); - } + if ($canDo->get('core.edit.state')) { + $toolbar->publish('manage.publish') + ->text('JTOOLBAR_ENABLE') + ->listCheck(true); + $toolbar->unpublish('manage.unpublish') + ->text('JTOOLBAR_DISABLE') + ->listCheck(true); + $toolbar->divider(); + } - $toolbar->standardButton('refresh') - ->text('JTOOLBAR_REFRESH_CACHE') - ->task('manage.refresh') - ->listCheck(true); - $toolbar->divider(); + $toolbar->standardButton('refresh') + ->text('JTOOLBAR_REFRESH_CACHE') + ->task('manage.refresh') + ->listCheck(true); + $toolbar->divider(); - if ($canDo->get('core.delete')) - { - $toolbar->delete('manage.remove') - ->text('JTOOLBAR_UNINSTALL') - ->message('COM_INSTALLER_CONFIRM_UNINSTALL') - ->listCheck(true); - $toolbar->divider(); - } + if ($canDo->get('core.delete')) { + $toolbar->delete('manage.remove') + ->text('JTOOLBAR_UNINSTALL') + ->message('COM_INSTALLER_CONFIRM_UNINSTALL') + ->listCheck(true); + $toolbar->divider(); + } - if ($canDo->get('core.manage')) - { - ToolbarHelper::link('index.php?option=com_installer&view=install', 'COM_INSTALLER_TOOLBAR_INSTALL_EXTENSIONS', 'upload'); - $toolbar->divider(); - } + if ($canDo->get('core.manage')) { + ToolbarHelper::link('index.php?option=com_installer&view=install', 'COM_INSTALLER_TOOLBAR_INSTALL_EXTENSIONS', 'upload'); + $toolbar->divider(); + } - parent::addToolbar(); - $toolbar->help('Extensions:_Manage'); - } + parent::addToolbar(); + $toolbar->help('Extensions:_Manage'); + } } diff --git a/administrator/components/com_installer/src/View/Update/HtmlView.php b/administrator/components/com_installer/src/View/Update/HtmlView.php index 2f1b991527d43..ccbd53c708dca 100644 --- a/administrator/components/com_installer/src/View/Update/HtmlView.php +++ b/administrator/components/com_installer/src/View/Update/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - $paths = new \stdClass; - $paths->first = ''; - - $this->paths = &$paths; - - if (count($this->items) === 0 && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - else - { - Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_WARNINGS_UPDATE_NOTICE'), 'warning'); - } - - // Find if there are any updates which require but are missing a Download Key - if (!class_exists('Joomla\Component\Installer\Administrator\Helper\InstallerHelper')) - { - require_once JPATH_COMPONENT_ADMINISTRATOR . '/Helper/InstallerHelper.php'; - } - - $mappingCallback = function ($item) { - $dlkeyInfo = CmsInstallerHelper::getDownloadKey(new CMSObject($item)); - $item->isMissingDownloadKey = $dlkeyInfo['supported'] && !$dlkeyInfo['valid']; - - if ($item->isMissingDownloadKey) - { - $this->missingDownloadKeys++; - } - - return $item; - }; - $this->items = array_map($mappingCallback, $this->items); - - if ($this->missingDownloadKeys) - { - $url = 'index.php?option=com_installer&view=updatesites&filter[supported]=-1'; - $msg = Text::plural('COM_INSTALLER_UPDATE_MISSING_DOWNLOADKEY_LABEL_N', $this->missingDownloadKeys, $url); - Factory::getApplication()->enqueueMessage($msg, CMSApplication::MSG_WARNING); - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - if (false === $this->isEmptyState) - { - ToolbarHelper::custom('update.update', 'upload', '', 'COM_INSTALLER_TOOLBAR_UPDATE', true); - } - - ToolbarHelper::custom('update.find', 'refresh', '', 'COM_INSTALLER_TOOLBAR_FIND_UPDATES', false); - ToolbarHelper::divider(); - - parent::addToolbar(); - ToolbarHelper::help('Extensions:_Update'); - } + /** + * List of update items. + * + * @var array + */ + protected $items; + + /** + * List pagination. + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * How many updates require but are missing Download Keys + * + * @var integer + * @since 4.0.0 + */ + protected $missingDownloadKeys = 0; + + /** + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view. + * + * @param string $tpl Template + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + // Get data from the model. + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + $paths = new \stdClass(); + $paths->first = ''; + + $this->paths = &$paths; + + if (count($this->items) === 0 && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } else { + Factory::getApplication()->enqueueMessage(Text::_('COM_INSTALLER_MSG_WARNINGS_UPDATE_NOTICE'), 'warning'); + } + + // Find if there are any updates which require but are missing a Download Key + if (!class_exists('Joomla\Component\Installer\Administrator\Helper\InstallerHelper')) { + require_once JPATH_COMPONENT_ADMINISTRATOR . '/Helper/InstallerHelper.php'; + } + + $mappingCallback = function ($item) { + $dlkeyInfo = CmsInstallerHelper::getDownloadKey(new CMSObject($item)); + $item->isMissingDownloadKey = $dlkeyInfo['supported'] && !$dlkeyInfo['valid']; + + if ($item->isMissingDownloadKey) { + $this->missingDownloadKeys++; + } + + return $item; + }; + $this->items = array_map($mappingCallback, $this->items); + + if ($this->missingDownloadKeys) { + $url = 'index.php?option=com_installer&view=updatesites&filter[supported]=-1'; + $msg = Text::plural('COM_INSTALLER_UPDATE_MISSING_DOWNLOADKEY_LABEL_N', $this->missingDownloadKeys, $url); + Factory::getApplication()->enqueueMessage($msg, CMSApplication::MSG_WARNING); + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + if (false === $this->isEmptyState) { + ToolbarHelper::custom('update.update', 'upload', '', 'COM_INSTALLER_TOOLBAR_UPDATE', true); + } + + ToolbarHelper::custom('update.find', 'refresh', '', 'COM_INSTALLER_TOOLBAR_FIND_UPDATES', false); + ToolbarHelper::divider(); + + parent::addToolbar(); + ToolbarHelper::help('Extensions:_Update'); + } } diff --git a/administrator/components/com_installer/src/View/Updatesite/HtmlView.php b/administrator/components/com_installer/src/View/Updatesite/HtmlView.php index eff7ee4eeff4d..c02fc9e656f67 100644 --- a/administrator/components/com_installer/src/View/Updatesite/HtmlView.php +++ b/administrator/components/com_installer/src/View/Updatesite/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->form = $model->getForm(); - $this->item = $model->getItem(); - - // Remove the extra_query field if it's a free download extension - $dlidSupportingSites = InstallerHelper::getDownloadKeySupportedSites(false); - $update_site_id = $this->item->get('update_site_id'); - - if (!in_array($update_site_id, $dlidSupportingSites)) - { - $this->form->removeField('extra_query'); - } - - // Check for errors. - if (count($errors = $model->getErrors())) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - * - * @throws \Exception - */ - protected function addToolbar(): void - { - $app = Factory::getApplication(); - $app->input->set('hidemainmenu', true); - - $user = $app->getIdentity(); - $userId = $user->id; - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out === $userId); - - // Since we don't track these assets at the item level, use the category id. - $canDo = ContentHelper::getActions('com_installer', 'updatesite'); - - ToolbarHelper::title(Text::_('COM_INSTALLER_UPDATESITE_EDIT_TITLE'), 'address contact'); - - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit'); - $toolbarButtons = []; - - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable && $this->form->getField('extra_query')) - { - $toolbarButtons[] = ['apply', 'updatesite.apply']; - $toolbarButtons[] = ['save', 'updatesite.save']; - } - - ToolbarHelper::saveGroup($toolbarButtons); - - ToolbarHelper::cancel('updatesite.cancel', 'JTOOLBAR_CLOSE'); - - ToolbarHelper::help('Edit_Update_Site'); - } + /** + * The Form object + * + * @var Form + * + * @since 4.0.0 + */ + protected $form; + + /** + * The active item + * + * @var object + * + * @since 4.0.0 + */ + protected $item; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + * + * @throws \Exception + */ + public function display($tpl = null): void + { + /** @var UpdatesiteModel $model */ + $model = $this->getModel(); + $this->form = $model->getForm(); + $this->item = $model->getItem(); + + // Remove the extra_query field if it's a free download extension + $dlidSupportingSites = InstallerHelper::getDownloadKeySupportedSites(false); + $update_site_id = $this->item->get('update_site_id'); + + if (!in_array($update_site_id, $dlidSupportingSites)) { + $this->form->removeField('extra_query'); + } + + // Check for errors. + if (count($errors = $model->getErrors())) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + * + * @throws \Exception + */ + protected function addToolbar(): void + { + $app = Factory::getApplication(); + $app->input->set('hidemainmenu', true); + + $user = $app->getIdentity(); + $userId = $user->id; + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out === $userId); + + // Since we don't track these assets at the item level, use the category id. + $canDo = ContentHelper::getActions('com_installer', 'updatesite'); + + ToolbarHelper::title(Text::_('COM_INSTALLER_UPDATESITE_EDIT_TITLE'), 'address contact'); + + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit'); + $toolbarButtons = []; + + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable && $this->form->getField('extra_query')) { + $toolbarButtons[] = ['apply', 'updatesite.apply']; + $toolbarButtons[] = ['save', 'updatesite.save']; + } + + ToolbarHelper::saveGroup($toolbarButtons); + + ToolbarHelper::cancel('updatesite.cancel', 'JTOOLBAR_CLOSE'); + + ToolbarHelper::help('Edit_Update_Site'); + } } diff --git a/administrator/components/com_installer/src/View/Updatesites/HtmlView.php b/administrator/components/com_installer/src/View/Updatesites/HtmlView.php index 3041f780a7dba..7268e5f4c4d52 100644 --- a/administrator/components/com_installer/src/View/Updatesites/HtmlView.php +++ b/administrator/components/com_installer/src/View/Updatesites/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->items = $model->getItems(); - $this->pagination = $model->getPagination(); - $this->filterForm = $model->getFilterForm(); - $this->activeFilters = $model->getActiveFilters(); - - // Check for errors. - if (count($errors = $model->getErrors())) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Display the view - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.4 - */ - protected function addToolbar(): void - { - $canDo = ContentHelper::getActions('com_installer'); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - if ($canDo->get('core.edit.state')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('updatesites.publish', 'JTOOLBAR_ENABLE')->listCheck(true); - $childBar->unpublish('updatesites.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); - - if ($canDo->get('core.delete')) - { - $childBar->delete('updatesites.delete')->listCheck(true); - } - - $childBar->checkin('updatesites.checkin')->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::custom('updatesites.rebuild', 'refresh', '', 'JTOOLBAR_REBUILD', false); - } - - parent::addToolbar(); - - ToolbarHelper::help('Extensions:_Update_Sites'); - } + /** + * The search tools form + * + * @var Form + * @since 3.4 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 3.4 + */ + public $activeFilters = []; + + /** + * List of updatesites + * + * @var \stdClass[] + * @since 3.4 + */ + protected $items; + + /** + * Pagination object + * + * @var Pagination + * @since 3.4 + */ + protected $pagination; + + /** + * Display the view + * + * @param string $tpl Template + * + * @return mixed|void + * + * @since 3.4 + * + * @throws \Exception on errors + */ + public function display($tpl = null): void + { + /** @var UpdatesitesModel $model */ + $model = $this->getModel(); + $this->items = $model->getItems(); + $this->pagination = $model->getPagination(); + $this->filterForm = $model->getFilterForm(); + $this->activeFilters = $model->getActiveFilters(); + + // Check for errors. + if (count($errors = $model->getErrors())) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Display the view + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.4 + */ + protected function addToolbar(): void + { + $canDo = ContentHelper::getActions('com_installer'); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + if ($canDo->get('core.edit.state')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('updatesites.publish', 'JTOOLBAR_ENABLE')->listCheck(true); + $childBar->unpublish('updatesites.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); + + if ($canDo->get('core.delete')) { + $childBar->delete('updatesites.delete')->listCheck(true); + } + + $childBar->checkin('updatesites.checkin')->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::custom('updatesites.rebuild', 'refresh', '', 'JTOOLBAR_REBUILD', false); + } + + parent::addToolbar(); + + ToolbarHelper::help('Extensions:_Update_Sites'); + } } diff --git a/administrator/components/com_installer/src/View/Warnings/HtmlView.php b/administrator/components/com_installer/src/View/Warnings/HtmlView.php index 6452040816bc1..e1abfb064c2bb 100644 --- a/administrator/components/com_installer/src/View/Warnings/HtmlView.php +++ b/administrator/components/com_installer/src/View/Warnings/HtmlView.php @@ -1,4 +1,5 @@ messages = $this->get('Items'); - - if (!\count($this->messages)) - { - $this->setLayout('emptystate'); - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - parent::addToolbar(); - - ToolbarHelper::help('Information:_Warnings'); - } + /** + * Display the view + * + * @param string $tpl Template + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->messages = $this->get('Items'); + + if (!\count($this->messages)) { + $this->setLayout('emptystate'); + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + parent::addToolbar(); + + ToolbarHelper::help('Information:_Warnings'); + } } diff --git a/administrator/components/com_installer/tmpl/database/default.php b/administrator/components/com_installer/tmpl/database/default.php index ff27f5666d25c..ac7d87af469eb 100644 --- a/administrator/components/com_installer/tmpl/database/default.php +++ b/administrator/components/com_installer/tmpl/database/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirection = $this->escape($this->state->get('list.direction')); ?>
-
-
-
-
- $this)); ?> - changeSet)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - changeSet as $i => $item) : ?> - - manifest_cache); ?> + +
+
+
+ $this)); ?> + changeSet)) : ?> +
+ + +
+ +
- , - , - -
- - - - - - - - - - - - - - - - - -
+ + + + + + + + + + + + + + + + changeSet as $i => $item) : ?> + + manifest_cache); ?> - - - - - - - - - - - - - -
+ , + , + +
+ + + + + + + + + + + + + + + + + +
- extension_id, false, 'cid', 'cb', $extension->name); ?> - - name; ?> -
- description); ?> -
-
- client_translated; ?> - - type_translated; ?> - - - - - - - version_id; ?> - - version; ?> - - folder_translated; ?> - - extension_id; ?> -
+ + + extension_id, false, 'cid', 'cb', $extension->name); ?> + + + name; ?> +
+ description); ?> +
+ + + client_translated; ?> + + + type_translated; ?> + + + + + + + + + version_id; ?> + + + version; ?> + + + folder_translated; ?> + + + extension_id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
-
-
-
+ + + + +
+ + + diff --git a/administrator/components/com_installer/tmpl/discover/default.php b/administrator/components/com_installer/tmpl/discover/default.php index c01ea5f9324fd..c2b850b8a35a4 100644 --- a/administrator/components/com_installer/tmpl/discover/default.php +++ b/administrator/components/com_installer/tmpl/discover/default.php @@ -1,4 +1,5 @@ escape($this->state->get('list.direction')); ?>
-
-
-
-
- showMessage) : ?> - loadTemplate('message'); ?> - - $this)); ?> - items)) : ?> -
- - -
-
- - -
- - - - - - - - - - - - - - - - - - items as $i => $item) : ?> - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - - - -
- extension_id, false, 'cid', 'cb', $item->name); ?> - - name; ?> -
description; ?>
-
- client_translated; ?> - - type_translated; ?> - - version) ? $item->version : ' '; ?> - - creationDate) ? $item->creationDate : ' '; ?> - - author) ? $item->author : ' '; ?> - - folder_translated; ?> - - extension_id; ?> -
+ +
+
+
+ showMessage) : ?> + loadTemplate('message'); ?> + + $this)); ?> + items)) : ?> +
+ + +
+
+ + +
+ + + + + + + + + + + + + + + + + + items as $i => $item) : ?> + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + + + +
+ extension_id, false, 'cid', 'cb', $item->name); ?> + + name; ?> +
description; ?>
+
+ client_translated; ?> + + type_translated; ?> + + version) ? $item->version : ' '; ?> + + creationDate) ? $item->creationDate : ' '; ?> + + author) ? $item->author : ' '; ?> + + folder_translated; ?> + + extension_id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
-
-
- + + + + +
+
+
+
diff --git a/administrator/components/com_installer/tmpl/discover/emptystate.php b/administrator/components/com_installer/tmpl/discover/emptystate.php index 547c2b77acfb1..0d56305b7ea34 100644 --- a/administrator/components/com_installer/tmpl/discover/emptystate.php +++ b/administrator/components/com_installer/tmpl/discover/emptystate.php @@ -1,4 +1,5 @@ 'COM_INSTALLER', - 'formURL' => 'index.php?option=com_installer&task=discover.refresh', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Extensions:_Discover', - 'icon' => 'icon-puzzle-piece install', - 'createURL' => 'index.php?option=com_installer&task=discover.refresh&' . Session::getFormToken() . '=1', - 'content' => Text::_('COM_INSTALLER_MSG_DISCOVER_DESCRIPTION'), - 'title' => Text::_('COM_INSTALLER_EMPTYSTATE_DISCOVER_TITLE'), - 'btnadd' => Text::_('COM_INSTALLER_EMPTYSTATE_DISCOVER_BUTTON_ADD'), + 'textPrefix' => 'COM_INSTALLER', + 'formURL' => 'index.php?option=com_installer&task=discover.refresh', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Extensions:_Discover', + 'icon' => 'icon-puzzle-piece install', + 'createURL' => 'index.php?option=com_installer&task=discover.refresh&' . Session::getFormToken() . '=1', + 'content' => Text::_('COM_INSTALLER_MSG_DISCOVER_DESCRIPTION'), + 'title' => Text::_('COM_INSTALLER_EMPTYSTATE_DISCOVER_TITLE'), + 'btnadd' => Text::_('COM_INSTALLER_EMPTYSTATE_DISCOVER_BUTTON_ADD'), ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_installer/tmpl/install/default.php b/administrator/components/com_installer/tmpl/install/default.php index 151bf9fa384c0..1c2b115aa7477 100644 --- a/administrator/components/com_installer/tmpl/install/default.php +++ b/administrator/components/com_installer/tmpl/install/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('core') - ->usePreset('com_installer.installer') - ->useScript('webcomponent.core-loader'); + ->usePreset('com_installer.installer') + ->useScript('webcomponent.core-loader'); $app = Factory::getApplication(); $tabs = $app->triggerEvent('onInstallerAddInstallationTab', []); @@ -34,42 +35,42 @@ ?>
-
- - showMessage) : ?> - loadTemplate('message'); ?> - + + + showMessage) : ?> + loadTemplate('message'); ?> + -
-
-
- -
- - -
- +
+
+
+ +
+ + +
+ - - $tabs[0]['name'] ?? '', 'recall' => true, 'breakpoint' => 768]); ?> - - - -
- -
- - + + $tabs[0]['name'] ?? '', 'recall' => true, 'breakpoint' => 768]); ?> + + + +
+ +
+ + - - + + - - - -
-
-
- + + + +
+
+
+
diff --git a/administrator/components/com_installer/tmpl/installer/default_message.php b/administrator/components/com_installer/tmpl/installer/default_message.php index b0e8170be42d1..3d1c78d6aa87b 100644 --- a/administrator/components/com_installer/tmpl/installer/default_message.php +++ b/administrator/components/com_installer/tmpl/installer/default_message.php @@ -1,4 +1,5 @@ -
- -
+
+ +
-
- -
+
+ +
diff --git a/administrator/components/com_installer/tmpl/languages/default.php b/administrator/components/com_installer/tmpl/languages/default.php index e14f69e07d226..e17347af3646b 100644 --- a/administrator/components/com_installer/tmpl/languages/default.php +++ b/administrator/components/com_installer/tmpl/languages/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); ?>
-
-
-
-
- $this, 'options' => array('filterButton' => false))); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - getShortVersion()); - $i = 0; - foreach ($this->items as $language) : - preg_match('#^pkg_([a-z]{2,3}-[A-Z]{2})$#', $language->element, $element); - $language->code = $element[1]; - ?> - - - - - - - - - - -
- , - , - -
- - - - - - - -
- installedLang[0][$language->code]) || isset($this->installedLang[1][$language->code])) ? 'REINSTALL' : 'INSTALL'; ?> - installedLang[0][$language->code]) || isset($this->installedLang[1][$language->code])) ? 'btn btn-success btn-sm' : 'btn btn-primary btn-sm'; ?> - detailsurl . '\'; Joomla.submitbutton(\'install.install\');'; ?> - - - name; ?> - - code; ?> - - - - version, $minorVersion) !== 0 || strpos($language->version, $currentShortVersion) !== 0) : ?> - version; ?> - - - - version; ?> - - - detailsurl; ?> -
+ +
+
+
+ $this, 'options' => array('filterButton' => false))); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + getShortVersion()); + $i = 0; + foreach ($this->items as $language) : + preg_match('#^pkg_([a-z]{2,3}-[A-Z]{2})$#', $language->element, $element); + $language->code = $element[1]; + ?> + + + + + + + + + + +
+ , + , + +
+ + + + + + + +
+ installedLang[0][$language->code]) || isset($this->installedLang[1][$language->code])) ? 'REINSTALL' : 'INSTALL'; ?> + installedLang[0][$language->code]) || isset($this->installedLang[1][$language->code])) ? 'btn btn-success btn-sm' : 'btn btn-primary btn-sm'; ?> + detailsurl . '\'; Joomla.submitbutton(\'install.install\');'; ?> + + + name; ?> + + code; ?> + + + + version, $minorVersion) !== 0 || strpos($language->version, $currentShortVersion) !== 0) : ?> + version; ?> + + + + version; ?> + + + detailsurl; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - - - - - -
-
-
- + + + + + + + + +
+
+
+
diff --git a/administrator/components/com_installer/tmpl/manage/default.php b/administrator/components/com_installer/tmpl/manage/default.php index 90a939b7fbd13..52fb80188299d 100644 --- a/administrator/components/com_installer/tmpl/manage/default.php +++ b/administrator/components/com_installer/tmpl/manage/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('com_installer.changelog') - ->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('table.columns') + ->useScript('multiselect'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); ?>
-
-
-
-
- showMessage) : ?> - loadTemplate('message'); ?> - - $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - - - - - items as $i => $item) : ?> - - - - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - - - - - - - - - -
- extension_id, false, 'cid', 'cb', $item->name); ?> - - element) : ?> - X - - status, $i, $item->status < 2, 'cb'); ?> - - - name; ?> - - - client_translated; ?> - - type_translated; ?> - - version)) : ?> - changelogurl)) : ?> - - version?> - - extension_id, - array( - 'title' => Text::sprintf('COM_INSTALLER_CHANGELOG_TITLE', $item->name, $item->version), - ), - '' - ); - ?> - - version; ?> - - - - creationDate)) : ?> - creationDate, $createdDateFormat); - } - catch (Exception $e) { - echo $item->creationDate; - }?> - - - - - author) ? $item->author : ' '; ?> - - folder_translated; ?> - - locked ? Text::_('JYES') : Text::_('JNO'); ?> - - package_id ?: ' '; ?> - - extension_id; ?> -
+ +
+
+
+ showMessage) : ?> + loadTemplate('message'); ?> + + $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + items as $i => $item) : ?> + + + + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ extension_id, false, 'cid', 'cb', $item->name); ?> + + element) : ?> + X + + status, $i, $item->status < 2, 'cb'); ?> + + + name; ?> + + + client_translated; ?> + + type_translated; ?> + + version)) : ?> + changelogurl)) : ?> + + version?> + + extension_id, + array( + 'title' => Text::sprintf('COM_INSTALLER_CHANGELOG_TITLE', $item->name, $item->version), + ), + '' + ); + ?> + + version; ?> + + + + creationDate)) : ?> + creationDate, $createdDateFormat); + } catch (Exception $e) { + echo $item->creationDate; + }?> + + + + + author) ? $item->author : ' '; ?> + + folder_translated; ?> + + locked ? Text::_('JYES') : Text::_('JNO'); ?> + + package_id ?: ' '; ?> + + extension_id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
-
-
- + + + + +
+
+
+
diff --git a/administrator/components/com_installer/tmpl/update/default.php b/administrator/components/com_installer/tmpl/update/default.php index 9122e871137d0..ea0fe5681717a 100644 --- a/administrator/components/com_installer/tmpl/update/default.php +++ b/administrator/components/com_installer/tmpl/update/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('multiselect') - ->useScript('com_installer.changelog'); + ->useScript('com_installer.changelog'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); ?>
-
-
-
-
- showMessage) : ?> - loadTemplate('message'); ?> - - $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - items as $i => $item): ?> - - - - - - - - + + + + + +
- , - , - -
- - - - - - - - - - - - - - - - - -
- isMissingDownloadKey): ?> - - - update_id, false, 'cid', 'cb', $item->name); ?> - - - escape($item->name); ?> - -
- detailsurl; ?> - infourl)) : ?> -
- escape(trim($item->infourl)); ?> - -
- isMissingDownloadKey): ?> - update_site_id; ?> - - -
- client_translated; ?> - - type_translated; ?> - - current_version; ?> - - version; ?> - - changelogurl)) : ?> - - - - extension_id, - array( - 'title' => Text::sprintf('COM_INSTALLER_CHANGELOG_TITLE', $item->name, $item->version), - ), - '' - ); - ?> - - - - + +
+
+
+ showMessage) : ?> + loadTemplate('message'); ?> + + $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + items as $i => $item) : ?> + + + + + + + + - - - - - -
+ , + , + +
+ + + + + + + + + + + + + + + + + +
+ isMissingDownloadKey) : ?> + + + update_id, false, 'cid', 'cb', $item->name); ?> + + + escape($item->name); ?> + +
+ detailsurl; ?> + infourl)) : ?> +
+ escape(trim($item->infourl)); ?> + +
+ isMissingDownloadKey) : ?> + update_site_id; ?> + + +
+ client_translated; ?> + + type_translated; ?> + + current_version; ?> + + version; ?> + + changelogurl)) : ?> + + + + extension_id, + array( + 'title' => Text::sprintf('COM_INSTALLER_CHANGELOG_TITLE', $item->name, $item->version), + ), + '' + ); + ?> + + + + - - - folder_translated; ?> - - install_type; ?> -
+ +
+ folder_translated; ?> + + install_type; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
-
-
-
+ + + + +
+ + + diff --git a/administrator/components/com_installer/tmpl/update/emptystate.php b/administrator/components/com_installer/tmpl/update/emptystate.php index 923e561332e76..172cfd663eaad 100644 --- a/administrator/components/com_installer/tmpl/update/emptystate.php +++ b/administrator/components/com_installer/tmpl/update/emptystate.php @@ -1,4 +1,5 @@ 'COM_INSTALLER', - 'formURL' => 'index.php?option=com_installer&view=update', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Extensions:_Update', - 'icon' => 'icon-puzzle-piece install', + 'textPrefix' => 'COM_INSTALLER', + 'formURL' => 'index.php?option=com_installer&view=update', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Extensions:_Update', + 'icon' => 'icon-puzzle-piece install', ]; $user = Factory::getApplication()->getIdentity(); -if ($user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) -{ - $displayData['createURL'] = 'index.php?option=com_installer&task=update.find&' . Session::getFormToken() . '=1'; +if ($user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')) > 0) { + $displayData['createURL'] = 'index.php?option=com_installer&task=update.find&' . Session::getFormToken() . '=1'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_installer/tmpl/updatesite/edit.php b/administrator/components/com_installer/tmpl/updatesite/edit.php index 25bce98728730..0c252686195b4 100644 --- a/administrator/components/com_installer/tmpl/updatesite/edit.php +++ b/administrator/components/com_installer/tmpl/updatesite/edit.php @@ -1,4 +1,5 @@

item->name; ?>

- form->renderFieldset('updateSite'); ?> - - + form->renderFieldset('updateSite'); ?> + +
diff --git a/administrator/components/com_installer/tmpl/updatesites/default.php b/administrator/components/com_installer/tmpl/updatesites/default.php index ec4d156125b72..79e90252f8130 100644 --- a/administrator/components/com_installer/tmpl/updatesites/default.php +++ b/administrator/components/com_installer/tmpl/updatesites/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getApplication()->getIdentity(); $userId = $user->get('id'); @@ -26,136 +27,139 @@ $listDirn = $this->escape($this->state->get('list.direction')); ?>
-
-
-
-
- $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - items as $i => $item) : - $canCheckin = $user->authorise('core.manage', 'com_checkin') - || $item->checked_out === $userId - || is_null($item->checked_out); - $canEdit = $user->authorise('core.edit', 'com_installer'); - ?> - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - -
- update_site_id, false, 'cid', 'cb', $item->update_site_name); ?> - - element) : ?> - X - - enabled, $i, $item->enabled < 2, 'cb'); ?> - - - checked_out) : ?> - editor, $item->checked_out_time, 'updatesites.', $canCheckin); ?> - - - - update_site_name); ?> - - - update_site_name); ?> - - -
- downloadKey['valid']) : ?> - - - - downloadKey['value']; ?> - downloadKey['supported']) : ?> - - - - - -
-
- - name; ?> - - - - client_translated; ?> - - type_translated; ?> - - folder_translated; ?> - - update_site_id; ?> -
+ +
+
+
+ $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + items as $i => $item) : + $canCheckin = $user->authorise('core.manage', 'com_checkin') + || $item->checked_out === $userId + || is_null($item->checked_out); + $canEdit = $user->authorise('core.edit', 'com_installer'); + ?> + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + +
+ update_site_id, false, 'cid', 'cb', $item->update_site_name); ?> + + element) : ?> + X + + enabled, $i, $item->enabled < 2, 'cb'); ?> + + + checked_out) : ?> + editor, $item->checked_out_time, 'updatesites.', $canCheckin); ?> + + + + update_site_name); ?> + + + update_site_name); ?> + + +
+ downloadKey['valid']) : ?> + + + + downloadKey['value']; ?> + downloadKey['supported']) : ?> + + + + + +
+
+ + name; ?> + + + + client_translated; ?> + + type_translated; ?> + + folder_translated; ?> + + update_site_id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
-
-
- + + + + +
+
+
+
diff --git a/administrator/components/com_installer/tmpl/warnings/default.php b/administrator/components/com_installer/tmpl/warnings/default.php index bcf4c662ac9ab..8bd5bc8a8d52f 100644 --- a/administrator/components/com_installer/tmpl/warnings/default.php +++ b/administrator/components/com_installer/tmpl/warnings/default.php @@ -1,4 +1,5 @@
-
-
-
-
- messages)) : ?> - messages as $message) : ?> -
-

- - - -

-

-
- -
-

- - - -

-

-
- -
- - - -
- -
- - -
-
-
-
-
+
+
+
+
+ messages)) : ?> + messages as $message) : ?> +
+

+ + + +

+

+
+ +
+

+ + + +

+

+
+ +
+ + + +
+ +
+ + +
+
+
+
+
diff --git a/administrator/components/com_installer/tmpl/warnings/emptystate.php b/administrator/components/com_installer/tmpl/warnings/emptystate.php index 03ebbd1bc86b2..82397e9b1b6a7 100644 --- a/administrator/components/com_installer/tmpl/warnings/emptystate.php +++ b/administrator/components/com_installer/tmpl/warnings/emptystate.php @@ -1,4 +1,5 @@ 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Information:_Warnings', - 'icon' => 'icon-puzzle-piece install', - 'title' => Text::_('COM_INSTALLER_MSG_WARNINGS_NONE'), - 'content' => '', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Information:_Warnings', + 'icon' => 'icon-puzzle-piece install', + 'title' => Text::_('COM_INSTALLER_MSG_WARNINGS_NONE'), + 'content' => '', ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_joomlaupdate/extract.php b/administrator/components/com_joomlaupdate/extract.php index 1ea4d93c8a799..2ca3730f8e19b 100644 --- a/administrator/components/com_joomlaupdate/extract.php +++ b/administrator/components/com_joomlaupdate/extract.php @@ -1,10 +1,13 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ /** @@ -42,1732 +45,1632 @@ */ class ZIPExtraction { - /** - * How much data to read at once when processing files - * - * @var int - * @since 4.0.4 - */ - private const CHUNK_SIZE = 524288; - - /** - * Maximum execution time (seconds). - * - * Each page load will take at most this much time. Please note that if the ZIP archive contains fairly large, - * compressed files we may overshoot this time since we can't interrupt the decompression. This should not be an - * issue in the context of updating Joomla as the ZIP archive contains fairly small files. - * - * If this is too low it will cause too many requests to hit the server, potentially triggering a DoS protection and - * causing the extraction to fail. If this is too big the extraction will not be as verbose and the user might think - * something is broken. A value between 3 and 7 seconds is, therefore, recommended. - * - * @var int - * @since 4.0.4 - */ - private const MAX_EXEC_TIME = 4; - - /** - * Run-time execution bias (percentage points). - * - * We evaluate the time remaining on the timer before processing each file on the ZIP archive. If we have already - * consumed at least this much percentage of the MAX_EXEC_TIME we will stop processing the archive in this page - * load, return the result to the client and wait for it to call us again so we can resume the extraction. - * - * This becomes important when the MAX_EXEC_TIME is close to the PHP, PHP-FPM or Apache timeout on the server - * (whichever is lowest) and there are fairly large files in the backup archive. If we start extracting a large, - * compressed file close to a hard server timeout it's possible that we will overshoot that hard timeout and see the - * extraction failing. - * - * Since Joomla Update is used to extract a ZIP archive with many small files we can keep at a fairly high 90% - * without much fear that something will break. - * - * Example: if MAX_EXEC_TIME is 10 seconds and RUNTIME_BIAS is 80 each page load will take between 80% and 100% of - * the MAX_EXEC_TIME, i.e. anywhere between 8 and 10 seconds. - * - * Lower values make it less likely to overshoot MAX_EXEC_TIME when extracting large files. - * - * @var int - * @since 4.0.4 - */ - private const RUNTIME_BIAS = 90; - - /** - * Minimum execution time (seconds). - * - * A request cannot take less than this many seconds. If it does, we add “dead time” (sleep) where the script does - * nothing except wait. This is essentially a rate limiting feature to avoid hitting a server-side DoS protection - * which could be triggered if we ended up sending too many requests in a limited amount of time. - * - * This should normally be less than MAX_EXEC * (RUNTIME_BIAS / 100). Values between that and MAX_EXEC_TIME have the - * effect of almost always adding dead time in each request, unless a really large file is being extracted from the - * ZIP archive. Values larger than MAX_EXEC will always add dead time to the request. This can be useful to - * artificially reduce the CPU usage limit. Some servers might kill the request if they see a sustained CPU usage - * spike over a short period of time. - * - * The chosen value of 3 seconds belongs to the first category, essentially making sure that we have a decent rate - * limiting without annoying the user too much but also catering for the most badly configured of shared - * hosting. It's a happy medium which works for the majority (~90%) of commercial servers out there. - * - * @var int - * @since 4.0.4 - */ - private const MIN_EXEC_TIME = 3; - - /** - * Internal state when extracting files: we need to be initialised - * - * @var int - * @since 4.0.4 - */ - private const AK_STATE_INITIALIZE = -1; - - /** - * Internal state when extracting files: no file currently being extracted - * - * @var int - * @since 4.0.4 - */ - private const AK_STATE_NOFILE = 0; - - /** - * Internal state when extracting files: reading the file header - * - * @var int - * @since 4.0.4 - */ - private const AK_STATE_HEADER = 1; - - /** - * Internal state when extracting files: reading file data - * - * @var int - * @since 4.0.4 - */ - private const AK_STATE_DATA = 2; - - /** - * Internal state when extracting files: file data has been read thoroughly - * - * @var int - * @since 4.0.4 - */ - private const AK_STATE_DATAREAD = 3; - - /** - * Internal state when extracting files: post-processing the file - * - * @var int - * @since 4.0.4 - */ - private const AK_STATE_POSTPROC = 4; - - /** - * Internal state when extracting files: done with this file - * - * @var int - * @since 4.0.4 - */ - private const AK_STATE_DONE = 5; - - /** - * Internal state when extracting files: finished extracting the ZIP file - * - * @var int - * @since 4.0.4 - */ - private const AK_STATE_FINISHED = 999; - - /** - * Internal logging level: debug - * - * @var int - * @since 4.0.4 - */ - private const LOG_DEBUG = 1; - - /** - * Internal logging level: information - * - * @var int - * @since 4.0.4 - */ - private const LOG_INFO = 10; - - /** - * Internal logging level: warning - * - * @var int - * @since 4.0.4 - */ - private const LOG_WARNING = 50; - - /** - * Internal logging level: error - * - * @var int - * @since 4.0.4 - */ - private const LOG_ERROR = 90; - - /** - * Singleton instance - * - * @var null|self - * @since 4.0.4 - */ - private static $instance = null; - - /** - * Debug log file pointer resource - * - * @var null|resource|boolean - * @since 4.0.4 - */ - private static $logFP = null; - - /** - * Debug log filename - * - * @var null|string - * @since 4.0.4 - */ - private static $logFilePath = null; - - /** - * The total size of the ZIP archive - * - * @var integer - * @since 4.0.4 - */ - public $totalSize = 0; - - /** - * Which files to skip - * - * @var array - * @since 4.0.4 - */ - public $skipFiles = []; - - /** - * Current tally of compressed size read - * - * @var integer - * @since 4.0.4 - */ - public $compressedTotal = 0; - - /** - * Current tally of bytes written to disk - * - * @var integer - * @since 4.0.4 - */ - public $uncompressedTotal = 0; - - /** - * Current tally of files extracted - * - * @var integer - * @since 4.0.4 - */ - public $filesProcessed = 0; - - /** - * Maximum execution time allowance per step - * - * @var integer - * @since 4.0.4 - */ - private $maxExecTime = null; - - /** - * Timestamp of execution start - * - * @var integer - * @since 4.0.4 - */ - private $startTime; - - /** - * The last error message - * - * @var string|null - * @since 4.0.4 - */ - private $lastErrorMessage = null; - - /** - * Archive filename - * - * @var string - * @since 4.0.4 - */ - private $filename = null; - - /** - * Current archive part number - * - * @var boolean - * @since 4.0.4 - */ - private $archiveFileIsBeingRead = false; - - /** - * The offset inside the current part - * - * @var integer - * @since 4.0.4 - */ - private $currentOffset = 0; - - /** - * Absolute path to prepend to extracted files - * - * @var string - * @since 4.0.4 - */ - private $addPath = ''; - - /** - * File pointer to the current archive part file - * - * @var resource|null - * @since 4.0.4 - */ - private $fp = null; - - /** - * Run state when processing the current archive file - * - * @var integer - * @since 4.0.4 - */ - private $runState = self::AK_STATE_INITIALIZE; - - /** - * File header data, as read by the readFileHeader() method - * - * @var stdClass - * @since 4.0.4 - */ - private $fileHeader = null; - - /** - * How much of the uncompressed data we've read so far - * - * @var integer - * @since 4.0.4 - */ - private $dataReadLength = 0; - - /** - * Unwritable files in these directories are always ignored and do not cause errors when not - * extracted. - * - * @var array - * @since 4.0.4 - */ - private $ignoreDirectories = []; - - /** - * Internal flag, set when the ZIP file has a data descriptor (which we will be ignoring) - * - * @var boolean - * @since 4.0.4 - */ - private $expectDataDescriptor = false; - - /** - * The UNIX last modification timestamp of the file last extracted - * - * @var integer - * @since 4.0.4 - */ - private $lastExtractedFileTimestamp = 0; - - /** - * The file path of the file last extracted - * - * @var string - * @since 4.0.4 - */ - private $lastExtractedFilename = null; - - /** - * Public constructor. - * - * Sets up the internal timer. - * - * @since 4.0.4 - */ - public function __construct() - { - $this->setupMaxExecTime(); - - // Initialize start time - $this->startTime = microtime(true); - } - - /** - * Singleton implementation. - * - * @return static - * @since 4.0.4 - */ - public static function getInstance(): self - { - if (is_null(self::$instance)) - { - self::$instance = new self; - } - - return self::$instance; - } - - /** - * Returns a serialised copy of the object. - * - * This is different to calling serialise() directly. This operates on a copy of the object which undergoes a - * call to shutdown() first so any open files are closed first. - * - * @return string The serialised data, potentially base64 encoded. - * @since 4.0.4 - */ - public static function getSerialised(): string - { - $clone = clone self::getInstance(); - $clone->shutdown(); - $serialized = serialize($clone); - - return (function_exists('base64_encode') && function_exists('base64_decode')) ? base64_encode($serialized) : $serialized; - } - - /** - * Restores a serialised instance into the singleton implementation and returns it. - * - * If the serialised data is corrupt it will return null. - * - * @param string $serialised The serialised data, potentially base64 encoded, to deserialize. - * - * @return static|null The instance of the object, NULL if it cannot be deserialised. - * @since 4.0.4 - */ - public static function unserialiseInstance(string $serialised): ?self - { - if (function_exists('base64_encode') && function_exists('base64_decode')) - { - $serialised = base64_decode($serialised); - } - - $instance = @unserialize($serialised, [ - 'allowed_classes' => [ - self::class, - stdClass::class, - ], - ] - ); - - if (($instance === false) || !is_object($instance) || !($instance instanceof self)) - { - return null; - } - - self::$instance = $instance; - - return self::$instance; - } - - /** - * Wakeup function, called whenever the class is deserialized. - * - * This method does the following: - * - Restart the timer. - * - Reopen the archive file, if one is defined. - * - Seek to the correct offset of the file. - * - * @return void - * @since 4.0.4 - * @internal - */ - public function __wakeup(): void - { - // Reset the timer when deserializing the object. - $this->startTime = microtime(true); - - if (!$this->archiveFileIsBeingRead) - { - return; - } - - $this->fp = @fopen($this->filename, 'rb'); - - if ((is_resource($this->fp)) && ($this->currentOffset > 0)) - { - @fseek($this->fp, $this->currentOffset); - } - } - - /** - * Enforce the minimum execution time. - * - * @return void - * @since 4.0.4 - */ - public function enforceMinimumExecutionTime() - { - $elapsed = $this->getRunningTime() * 1000; - $minExecTime = 1000.0 * min(1, (min(self::MIN_EXEC_TIME, $this->getPhpMaxExecTime()) - 1)); - - // Only run a sleep delay if we haven't reached the minimum execution time - if (($minExecTime <= $elapsed) || ($elapsed <= 0)) - { - return; - } - - $sleepMillisec = intval($minExecTime - $elapsed); - - /** - * If we need to sleep for more than 1 second we should be using sleep() or time_sleep_until() to prevent high - * CPU usage, also because some OS might not support sleeping for over 1 second using these functions. In all - * other cases we will try to use usleep or time_nanosleep instead. - */ - $longSleep = $sleepMillisec > 1000; - $miniSleepSupported = function_exists('usleep') || function_exists('time_nanosleep'); - - if (!$longSleep && $miniSleepSupported) - { - if (function_exists('usleep') && ($sleepMillisec < 1000)) - { - usleep(1000 * $sleepMillisec); - - return; - } - - if (function_exists('time_nanosleep') && ($sleepMillisec < 1000)) - { - time_nanosleep(0, 1000000 * $sleepMillisec); - - return; - } - } - - if (function_exists('sleep')) - { - sleep(ceil($sleepMillisec / 1000)); - - return; - } - - if (function_exists('time_sleep_until')) - { - time_sleep_until(time() + ceil($sleepMillisec / 1000)); - } - } - - /** - * Set the filepath to the ZIP archive which will be extracted. - * - * @param string $value The filepath to the archive. Only LOCAL files are allowed! - * - * @return void - * @since 4.0.4 - */ - public function setFilename(string $value) - { - // Security check: disallow remote filenames - if (!empty($value) && strpos($value, '://') !== false) - { - $this->setError('Invalid archive location'); - - return; - } - - $this->filename = $value; - $this->initializeLog(dirname($this->filename)); - } - - /** - * Sets the path to prefix all extracted files with. Essentially, where the archive will be extracted to. - * - * @param string $addPath The path where the archive will be extracted. - * - * @return void - * @since 4.0.4 - */ - public function setAddPath(string $addPath): void - { - $this->addPath = $addPath; - $this->addPath = str_replace('\\', '/', $this->addPath); - $this->addPath = rtrim($this->addPath, '/'); - - if (!empty($this->addPath)) - { - $this->addPath .= '/'; - } - } - - /** - * Set the list of files to skip when extracting the ZIP file. - * - * @param array $skipFiles A list of files to skip when extracting the ZIP archive - * - * @return void - * @since 4.0.4 - */ - public function setSkipFiles(array $skipFiles): void - { - $this->skipFiles = array_values($skipFiles); - } - - /** - * Set the directories to skip over when extracting the ZIP archive - * - * @param array $ignoreDirectories The list of directories to ignore. - * - * @return void - * @since 4.0.4 - */ - public function setIgnoreDirectories(array $ignoreDirectories): void - { - $this->ignoreDirectories = array_values($ignoreDirectories); - } - - /** - * Prepares for the archive extraction - * - * @return void - * @since 4.0.4 - */ - public function initialize(): void - { - $this->debugMsg(sprintf('Initializing extraction. Filepath: %s', $this->filename)); - $this->totalSize = @filesize($this->filename) ?: 0; - $this->archiveFileIsBeingRead = false; - $this->currentOffset = 0; - $this->runState = self::AK_STATE_NOFILE; - - $this->readArchiveHeader(); - - if (!empty($this->getError())) - { - $this->debugMsg(sprintf('Error: %s', $this->getError()), self::LOG_ERROR); - - return; - } - - $this->archiveFileIsBeingRead = true; - $this->runState = self::AK_STATE_NOFILE; - - $this->debugMsg('Setting state to NOFILE', self::LOG_DEBUG); - } - - /** - * Executes a step of the archive extraction - * - * @return boolean True if we are done extracting or an error occurred - * @since 4.0.4 - */ - public function step(): bool - { - $status = true; - - $this->debugMsg('Starting a new step', self::LOG_INFO); - - while ($status && ($this->getTimeLeft() > 0)) - { - switch ($this->runState) - { - case self::AK_STATE_INITIALIZE: - $this->debugMsg('Current run state: INITIALIZE', self::LOG_DEBUG); - $this->initialize(); - break; - - case self::AK_STATE_NOFILE: - $this->debugMsg('Current run state: NOFILE', self::LOG_DEBUG); - $status = $this->readFileHeader(); - - if ($status) - { - $this->debugMsg('Found file header; updating number of files processed and bytes in/out', self::LOG_DEBUG); - - // Update running tallies when we start extracting a file - $this->filesProcessed++; - $this->compressedTotal += array_key_exists('compressed', get_object_vars($this->fileHeader)) - ? $this->fileHeader->compressed : 0; - $this->uncompressedTotal += $this->fileHeader->uncompressed; - } - - break; - - case self::AK_STATE_HEADER: - case self::AK_STATE_DATA: - $runStateHuman = $this->runState === self::AK_STATE_HEADER ? 'HEADER' : 'DATA'; - $this->debugMsg(sprintf('Current run state: %s', $runStateHuman), self::LOG_DEBUG); - - $status = $this->processFileData(); - break; - - case self::AK_STATE_DATAREAD: - case self::AK_STATE_POSTPROC: - $runStateHuman = $this->runState === self::AK_STATE_DATAREAD ? 'DATAREAD' : 'POSTPROC'; - $this->debugMsg(sprintf('Current run state: %s', $runStateHuman), self::LOG_DEBUG); - - $this->setLastExtractedFileTimestamp($this->fileHeader->timestamp); - $this->processLastExtractedFile(); - - $status = true; - $this->runState = self::AK_STATE_DONE; - break; - - case self::AK_STATE_DONE: - default: - $this->debugMsg('Current run state: DONE', self::LOG_DEBUG); - $this->runState = self::AK_STATE_NOFILE; - - break; - - case self::AK_STATE_FINISHED: - $this->debugMsg('Current run state: FINISHED', self::LOG_DEBUG); - $status = false; - break; - } - - if ($this->getTimeLeft() <= 0) - { - $this->debugMsg('Ran out of time; the step will break.'); - } - elseif (!$status) - { - $this->debugMsg('Last step status is false; the step will break.'); - } - } - - $error = $this->getError() ?? null; - - if (!empty($error)) - { - $this->debugMsg(sprintf('Step failed with error: %s', $error), self::LOG_ERROR); - } - - // Did we just finish or run into an error? - if (!empty($error) || $this->runState === self::AK_STATE_FINISHED) - { - $this->debugMsg('Returning true (must stop running) from step()', self::LOG_DEBUG); - - // Reset internal state, prevents __wakeup from trying to open a non-existent file - $this->archiveFileIsBeingRead = false; - - return true; - } - - $this->debugMsg('Returning false (must continue running) from step()', self::LOG_DEBUG); - - return false; - } - - /** - * Get the most recent error message - * - * @return string|null The message string, null if there's no error - * @since 4.0.4 - */ - public function getError(): ?string - { - return $this->lastErrorMessage; - } - - /** - * Gets the number of seconds left, before we hit the "must break" threshold - * - * @return float - * @since 4.0.4 - */ - private function getTimeLeft(): float - { - return $this->maxExecTime - $this->getRunningTime(); - } - - /** - * Gets the time elapsed since object creation/unserialization, effectively how - * long Akeeba Engine has been processing data - * - * @return float - * @since 4.0.4 - */ - private function getRunningTime(): float - { - return microtime(true) - $this->startTime; - } - - /** - * Process the last extracted file or directory - * - * This invalidates OPcache for .php files. Also applies the correct permissions and timestamp. - * - * @return void - * @since 4.0.4 - */ - private function processLastExtractedFile(): void - { - $this->debugMsg(sprintf('Processing last extracted entity: %s', $this->lastExtractedFilename), self::LOG_DEBUG); - - if (@is_file($this->lastExtractedFilename)) - { - @chmod($this->lastExtractedFilename, 0644); - - clearFileInOPCache($this->lastExtractedFilename); - } - else - { - @chmod($this->lastExtractedFilename, 0755); - } - - if ($this->lastExtractedFileTimestamp > 0) - { - @touch($this->lastExtractedFilename, $this->lastExtractedFileTimestamp); - } - } - - /** - * Set the last extracted filename - * - * @param string|null $lastExtractedFilename The last extracted filename - * - * @return void - * @since 4.0.4 - */ - private function setLastExtractedFilename(?string $lastExtractedFilename): void - { - $this->lastExtractedFilename = $lastExtractedFilename; - } - - /** - * Set the last modification UNIX timestamp for the last extracted file - * - * @param int $lastExtractedFileTimestamp The timestamp - * - * @return void - * @since 4.0.4 - */ - private function setLastExtractedFileTimestamp(int $lastExtractedFileTimestamp): void - { - $this->lastExtractedFileTimestamp = $lastExtractedFileTimestamp; - } - - /** - * Sleep function, called whenever the class is serialized - * - * @return void - * @since 4.0.4 - * @internal - */ - private function shutdown(): void - { - if (is_resource(self::$logFP)) - { - @fclose(self::$logFP); - } - - if (!is_resource($this->fp)) - { - return; - } - - $this->currentOffset = @ftell($this->fp); - - @fclose($this->fp); - } - - /** - * Unicode-safe binary data length - * - * @param string|null $string The binary data to get the length for - * - * @return integer - * @since 4.0.4 - */ - private function binStringLength(?string $string): int - { - if (is_null($string)) - { - return 0; - } - - if (function_exists('mb_strlen')) - { - return mb_strlen($string, '8bit') ?: 0; - } - - return strlen($string) ?: 0; - } - - /** - * Add an error message - * - * @param string $error Error message - * - * @return void - * @since 4.0.4 - */ - private function setError(string $error): void - { - $this->lastErrorMessage = $error; - } - - /** - * Reads data from the archive. - * - * @param resource $fp The file pointer to read data from - * @param int|null $length The volume of data to read, in bytes - * - * @return string The data read from the file - * @since 4.0.4 - */ - private function fread($fp, ?int $length = null): string - { - $readLength = (is_numeric($length) && ($length > 0)) ? $length : PHP_INT_MAX; - $data = fread($fp, $readLength); - - if ($data === false) - { - $this->debugMsg('No more data could be read from the file', self::LOG_WARNING); - - $data = ''; - } - - return $data; - } - - /** - * Read the header of the archive, making sure it's a valid ZIP file. - * - * @return void - * @since 4.0.4 - */ - private function readArchiveHeader(): void - { - $this->debugMsg('Reading the archive header.', self::LOG_DEBUG); - - // Open the first part - $this->openArchiveFile(); - - // Fail for unreadable files - if ($this->fp === false) - { - return; - } - - // Read the header data. - $sigBinary = fread($this->fp, 4); - $headerData = unpack('Vsig', $sigBinary); - - // We only support single part ZIP files - if ($headerData['sig'] != 0x04034b50) - { - $this->setError('The archive file is corrupt: bad header'); - - return; - } - - // Roll back the file pointer - fseek($this->fp, -4, SEEK_CUR); - - $this->currentOffset = @ftell($this->fp); - $this->dataReadLength = 0; - - } - - /** - * Concrete classes must use this method to read the file header - * - * @return boolean True if reading the file was successful, false if an error occurred or we - * reached end of archive. - * @since 4.0.4 - */ - private function readFileHeader(): bool - { - $this->debugMsg('Reading the file entry header.', self::LOG_DEBUG); - - if (!is_resource($this->fp)) - { - $this->setError('The archive is not open for reading.'); - - return false; - } - - // Unexpected end of file - if ($this->isEOF()) - { - $this->debugMsg('EOF when reading file header data', self::LOG_WARNING); - $this->setError('The archive is corrupt or truncated'); - - return false; - } - - $this->currentOffset = ftell($this->fp); - - if ($this->expectDataDescriptor) - { - $this->debugMsg('Expecting data descriptor (bit 3 of general purpose flag was set).', self::LOG_DEBUG); - - /** - * The last file had bit 3 of the general purpose bit flag set. This means that we have a 12 byte data - * descriptor we need to skip. To make things worse, there might also be a 4 byte optional data descriptor - * header (0x08074b50). - */ - $junk = @fread($this->fp, 4); - $junk = unpack('Vsig', $junk); - $readLength = ($junk['sig'] == 0x08074b50) ? 12 : 8; - $junk = @fread($this->fp, $readLength); - - // And check for EOF, too - if ($this->isEOF()) - { - $this->debugMsg('EOF when reading data descriptor', self::LOG_WARNING); - $this->setError('The archive is corrupt or truncated'); - - return false; - } - } - - // Get and decode Local File Header - $headerBinary = fread($this->fp, 30); - $format = 'Vsig/C2ver/vbitflag/vcompmethod/vlastmodtime/vlastmoddate/Vcrc/Vcompsize/' - . 'Vuncomp/vfnamelen/veflen'; - $headerData = unpack($format, $headerBinary); - - // Check signature - if (!($headerData['sig'] == 0x04034b50)) - { - // The signature is not the one used for files. Is this a central directory record (i.e. we're done)? - if ($headerData['sig'] == 0x02014b50) - { - $this->debugMsg('Found Central Directory header; the extraction is complete', self::LOG_DEBUG); - - // End of ZIP file detected. We'll just skip to the end of file... - @fseek($this->fp, 0, SEEK_END); - $this->runState = self::AK_STATE_FINISHED; - - return false; - } - - $this->setError('The archive file is corrupt or truncated'); - - return false; - } - - // If bit 3 of the bitflag is set, expectDataDescriptor is true - $this->expectDataDescriptor = ($headerData['bitflag'] & 4) == 4; - $this->fileHeader = new stdClass; - $this->fileHeader->timestamp = 0; - - // Read the last modified date and time - $lastmodtime = $headerData['lastmodtime']; - $lastmoddate = $headerData['lastmoddate']; - - if ($lastmoddate && $lastmodtime) - { - $vHour = ($lastmodtime & 0xF800) >> 11; - $vMInute = ($lastmodtime & 0x07E0) >> 5; - $vSeconds = ($lastmodtime & 0x001F) * 2; - $vYear = (($lastmoddate & 0xFE00) >> 9) + 1980; - $vMonth = ($lastmoddate & 0x01E0) >> 5; - $vDay = $lastmoddate & 0x001F; - - $this->fileHeader->timestamp = @mktime($vHour, $vMInute, $vSeconds, $vMonth, $vDay, $vYear); - } - - $isBannedFile = false; - - $this->fileHeader->compressed = $headerData['compsize']; - $this->fileHeader->uncompressed = $headerData['uncomp']; - $nameFieldLength = $headerData['fnamelen']; - $extraFieldLength = $headerData['eflen']; - - // Read filename field - $this->fileHeader->file = fread($this->fp, $nameFieldLength); - - // Read extra field if present - if ($extraFieldLength > 0) - { - $extrafield = fread($this->fp, $extraFieldLength); - } - - // Decide filetype -- Check for directories - $this->fileHeader->type = 'file'; - - if (strrpos($this->fileHeader->file, '/') == strlen($this->fileHeader->file) - 1) - { - $this->fileHeader->type = 'dir'; - } - - // Decide filetype -- Check for symbolic links - if (($headerData['ver1'] == 10) && ($headerData['ver2'] == 3)) - { - $this->fileHeader->type = 'link'; - } - - switch ($headerData['compmethod']) - { - case 0: - $this->fileHeader->compression = 'none'; - break; - case 8: - $this->fileHeader->compression = 'gzip'; - break; - default: - $messageTemplate = 'This script cannot handle ZIP compression method %d. ' - . 'Only 0 (no compression) and 8 (DEFLATE, gzip) can be handled.'; - $actualMessage = sprintf($messageTemplate, $headerData['compmethod']); - $this->setError($actualMessage); - - return false; - break; - } - - // Find hard-coded banned files - if ((basename($this->fileHeader->file) == ".") || (basename($this->fileHeader->file) == "..")) - { - $isBannedFile = true; - } - - // Also try to find banned files passed in class configuration - if ((count($this->skipFiles) > 0) && in_array($this->fileHeader->file, $this->skipFiles)) - { - $isBannedFile = true; - } - - // If we have a banned file, let's skip it - if ($isBannedFile) - { - $debugMessage = sprintf('Current entity (%s) is banned from extraction and will be skipped over.', $this->fileHeader->file); - $this->debugMsg($debugMessage, self::LOG_DEBUG); - - // Advance the file pointer, skipping exactly the size of the compressed data - $seekleft = $this->fileHeader->compressed; - - while ($seekleft > 0) - { - // Ensure that we can seek past archive part boundaries - $curSize = @filesize($this->filename); - $curPos = @ftell($this->fp); - $canSeek = $curSize - $curPos; - $canSeek = ($canSeek > $seekleft) ? $seekleft : $canSeek; - @fseek($this->fp, $canSeek, SEEK_CUR); - $seekleft -= $canSeek; - - if ($seekleft) - { - $this->setError('The archive is corrupt or truncated'); - - return false; - } - } - - $this->currentOffset = @ftell($this->fp); - $this->runState = self::AK_STATE_DONE; - - return true; - } - - // Last chance to prepend a path to the filename - if (!empty($this->addPath)) - { - $this->fileHeader->file = $this->addPath . $this->fileHeader->file; - } - - // Get the translated path name - if ($this->fileHeader->type == 'file') - { - $this->fileHeader->realFile = $this->fileHeader->file; - $this->setLastExtractedFilename($this->fileHeader->file); - } - elseif ($this->fileHeader->type == 'dir') - { - $this->fileHeader->timestamp = 0; - - $dir = $this->fileHeader->file; - - if (!@is_dir($dir)) - { - mkdir($dir, 0755, true); - } - - $this->setLastExtractedFilename(null); - } - else - { - // Symlink; do not post-process - $this->fileHeader->timestamp = 0; - $this->setLastExtractedFilename(null); - } - - $this->createDirectory(); - - // Header is read - $this->runState = self::AK_STATE_HEADER; - - return true; - } - - /** - * Creates the directory this file points to - * - * @return void - * @since 4.0.4 - */ - private function createDirectory(): void - { - // Do we need to create a directory? - if (empty($this->fileHeader->realFile)) - { - $this->fileHeader->realFile = $this->fileHeader->file; - } - - $lastSlash = strrpos($this->fileHeader->realFile, '/'); - $dirName = substr($this->fileHeader->realFile, 0, $lastSlash); - $perms = 0755; - $ignore = $this->isIgnoredDirectory($dirName); - - if (@is_dir($dirName)) - { - return; - } - - if ((@mkdir($dirName, $perms, true) === false) && (!$ignore)) - { - $this->setError(sprintf('Could not create %s folder', $dirName)); - } - - } - - /** - * Concrete classes must use this method to process file data. It must set $runState to self::AK_STATE_DATAREAD when - * it's finished processing the file data. - * - * @return boolean True if processing the file data was successful, false if an error occurred - * @since 4.0.4 - */ - private function processFileData(): bool - { - switch ($this->fileHeader->type) - { - case 'dir': - $this->debugMsg('Extracting entity of type Directory', self::LOG_DEBUG); - - return $this->processTypeDir(); - break; - - case 'link': - $this->debugMsg('Extracting entity of type Symbolic Link', self::LOG_DEBUG); - - return $this->processTypeLink(); - break; - - case 'file': - switch ($this->fileHeader->compression) - { - case 'none': - $this->debugMsg('Extracting entity of type File (Stored)', self::LOG_DEBUG); - - return $this->processTypeFileUncompressed(); - break; - - case 'gzip': - case 'bzip2': - $this->debugMsg('Extracting entity of type File (Compressed)', self::LOG_DEBUG); - - return $this->processTypeFileCompressed(); - break; - - case 'default': - $this->setError(sprintf('Unknown compression type %s.', $this->fileHeader->compression)); - - return false; - break; - } - break; - } - - $this->setError(sprintf('Unknown entry type %s.', $this->fileHeader->type)); - - return false; - } - - /** - * Opens the next part file for reading - * - * @return void - * @since 4.0.4 - */ - private function openArchiveFile(): void - { - $this->debugMsg('Opening archive file for reading', self::LOG_DEBUG); - - if ($this->archiveFileIsBeingRead) - { - return; - } - - if (is_resource($this->fp)) - { - @fclose($this->fp); - } - - $this->fp = @fopen($this->filename, 'rb'); - - if ($this->fp === false) - { - $message = 'Could not open archive for reading. Check that the file exists, is ' - . 'readable by the web server and is not in a directory made out of reach by chroot, ' - . 'open_basedir restrictions or any other restriction put in place by your host.'; - $this->setError($message); - - return; - } - - fseek($this->fp, 0); - $this->currentOffset = 0; - - } - - /** - * Returns true if we have reached the end of file - * - * @return boolean True if we have reached End Of File - * @since 4.0.4 - */ - private function isEOF(): bool - { - /** - * feof() will return false if the file pointer is exactly at the last byte of the file. However, this is a - * condition we want to treat as a proper EOF for the purpose of extracting a ZIP file. Hence the second part - * after the logical OR. - */ - return @feof($this->fp) || (@ftell($this->fp) > @filesize($this->filename)); - } - - /** - * Handles the permissions of the parent directory to a file and the file itself to make it writeable. - * - * @param string $path A path to a file - * - * @return void - * @since 4.0.4 - */ - private function setCorrectPermissions(string $path): void - { - static $rootDir = null; - - if (is_null($rootDir)) - { - $rootDir = rtrim($this->addPath, '/\\'); - } - - $directory = rtrim(dirname($path), '/\\'); - - // Is this an unwritable directory? - if (($directory != $rootDir) && !is_writeable($directory)) - { - @chmod($directory, 0755); - } - - @chmod($path, 0644); - } - - /** - * Is this file or directory contained in a directory we've decided to ignore - * write errors for? This is useful to let the extraction work despite write - * errors in the log, logs and tmp directories which MIGHT be used by the system - * on some low quality hosts and Plesk-powered hosts. - * - * @param string $shortFilename The relative path of the file/directory in the package - * - * @return boolean True if it belongs in an ignored directory - * @since 4.0.4 - */ - private function isIgnoredDirectory(string $shortFilename): bool - { - $check = substr($shortFilename, -1) == '/' ? rtrim($shortFilename, '/') : dirname($shortFilename); - - return in_array($check, $this->ignoreDirectories); - } - - /** - * Process the file data of a directory entry - * - * @return boolean - * @since 4.0.4 - */ - private function processTypeDir(): bool - { - // Directory entries do not have file data, therefore we're done processing the entry. - $this->runState = self::AK_STATE_DATAREAD; - - return true; - } - - /** - * Process the file data of a link entry - * - * @return boolean - * @since 4.0.4 - */ - private function processTypeLink(): bool - { - $toReadBytes = 0; - $leftBytes = $this->fileHeader->compressed; - $data = ''; - - while ($leftBytes > 0) - { - $toReadBytes = min($leftBytes, self::CHUNK_SIZE); - $mydata = $this->fread($this->fp, $toReadBytes); - $reallyReadBytes = $this->binStringLength($mydata); - $data .= $mydata; - $leftBytes -= $reallyReadBytes; - - if ($reallyReadBytes < $toReadBytes) - { - // We read less than requested! - if ($this->isEOF()) - { - $this->debugMsg('EOF when reading symlink data', self::LOG_WARNING); - $this->setError('The archive file is corrupt or truncated'); - - return false; - } - } - } - - $filename = $this->fileHeader->realFile ?? $this->fileHeader->file; - - // Try to remove an existing file or directory by the same name - if (file_exists($filename)) - { - clearFileInOPCache($filename); - @unlink($filename); - @rmdir($filename); - } - - // Remove any trailing slash - if (substr($filename, -1) == '/') - { - $filename = substr($filename, 0, -1); - } - - // Create the symlink - @symlink($data, $filename); - - $this->runState = self::AK_STATE_DATAREAD; - - // No matter if the link was created! - return true; - } - - /** - * Processes an uncompressed (stored) file - * - * @return boolean - * @since 4.0.4 - */ - private function processTypeFileUncompressed(): bool - { - // Uncompressed files are being processed in small chunks, to avoid timeouts - if ($this->dataReadLength == 0) - { - // Before processing file data, ensure permissions are adequate - $this->setCorrectPermissions($this->fileHeader->file); - } - - // Open the output file - $ignore = $this->isIgnoredDirectory($this->fileHeader->file); - - $writeMode = ($this->dataReadLength == 0) ? 'wb' : 'ab'; - $outfp = @fopen($this->fileHeader->realFile, $writeMode); - - // Can we write to the file? - if (($outfp === false) && (!$ignore)) - { - // An error occurred - $this->setError(sprintf('Could not open %s for writing.', $this->fileHeader->realFile)); - - return false; - } - - // Does the file have any data, at all? - if ($this->fileHeader->compressed == 0) - { - // No file data! - if (is_resource($outfp)) - { - @fclose($outfp); - } - - $this->debugMsg('Zero byte Stored file; no data will be read', self::LOG_DEBUG); - - $this->runState = self::AK_STATE_DATAREAD; - - return true; - } - - $leftBytes = $this->fileHeader->compressed - $this->dataReadLength; - - // Loop while there's data to read and enough time to do it - while (($leftBytes > 0) && ($this->getTimeLeft() > 0)) - { - $toReadBytes = min($leftBytes, self::CHUNK_SIZE); - $data = $this->fread($this->fp, $toReadBytes); - $reallyReadBytes = $this->binStringLength($data); - $leftBytes -= $reallyReadBytes; - $this->dataReadLength += $reallyReadBytes; - - if ($reallyReadBytes < $toReadBytes) - { - // We read less than requested! Why? Did we hit local EOF? - if ($this->isEOF()) - { - // Nope. The archive is corrupt - $this->debugMsg('EOF when reading stored file data', self::LOG_WARNING); - $this->setError('The archive file is corrupt or truncated'); - - return false; - } - } - - if (is_resource($outfp)) - { - @fwrite($outfp, $data); - } - - if ($this->getTimeLeft()) - { - $this->debugMsg('Out of time; will resume extraction in the next step', self::LOG_DEBUG); - } - } - - // Close the file pointer - if (is_resource($outfp)) - { - @fclose($outfp); - } - - // Was this a pre-timeout bail out? - if ($leftBytes > 0) - { - $this->debugMsg(sprintf('We have %d bytes left to extract in the next step', $leftBytes), self::LOG_DEBUG); - $this->runState = self::AK_STATE_DATA; - - return true; - } - - // Oh! We just finished! - $this->runState = self::AK_STATE_DATAREAD; - $this->dataReadLength = 0; - - return true; - } - - /** - * Processes a compressed file - * - * @return boolean - * @since 4.0.4 - */ - private function processTypeFileCompressed(): bool - { - // Before processing file data, ensure permissions are adequate - $this->setCorrectPermissions($this->fileHeader->file); - - // Open the output file - $outfp = @fopen($this->fileHeader->realFile, 'wb'); - - // Can we write to the file? - $ignore = $this->isIgnoredDirectory($this->fileHeader->file); - - if (($outfp === false) && (!$ignore)) - { - // An error occurred - $this->setError(sprintf('Could not open %s for writing.', $this->fileHeader->realFile)); - - return false; - } - - // Does the file have any data, at all? - if ($this->fileHeader->compressed == 0) - { - $this->debugMsg('Zero byte Compressed file; no data will be read', self::LOG_DEBUG); - - // No file data! - if (is_resource($outfp)) - { - @fclose($outfp); - } - - $this->runState = self::AK_STATE_DATAREAD; - - return true; - } - - // Simple compressed files are processed as a whole; we can't do chunk processing - $zipData = $this->fread($this->fp, $this->fileHeader->compressed); - - while ($this->binStringLength($zipData) < $this->fileHeader->compressed) - { - // End of local file before reading all data? - if ($this->isEOF()) - { - $this->debugMsg('EOF reading compressed data', self::LOG_WARNING); - $this->setError('The archive file is corrupt or truncated'); - - return false; - } - } - - switch ($this->fileHeader->compression) - { - case 'gzip': - /** @noinspection PhpComposerExtensionStubsInspection */ - $unzipData = gzinflate($zipData); - break; - - case 'bzip2': - /** @noinspection PhpComposerExtensionStubsInspection */ - $unzipData = bzdecompress($zipData); - break; - - default: - $this->setError(sprintf('Unknown compression method %s', $this->fileHeader->compression)); - - return false; - break; - } - - unset($zipData); - - // Write to the file. - if (is_resource($outfp)) - { - @fwrite($outfp, $unzipData, $this->fileHeader->uncompressed); - @fclose($outfp); - } - - unset($unzipData); - - $this->runState = self::AK_STATE_DATAREAD; - - return true; - } - - /** - * Set up the maximum execution time - * - * @return void - * @since 4.0.4 - */ - private function setupMaxExecTime(): void - { - $configMaxTime = self::MAX_EXEC_TIME; - $bias = self::RUNTIME_BIAS / 100; - $this->maxExecTime = min($this->getPhpMaxExecTime(), $configMaxTime) * $bias; - } - - /** - * Get the PHP maximum execution time. - * - * If it's not defined or it's zero (infinite) we use a fake value of 10 seconds. - * - * @return integer - * @since 4.0.4 - */ - private function getPhpMaxExecTime(): int - { - if (!@function_exists('ini_get')) - { - return 10; - } - - $phpMaxTime = @ini_get("maximum_execution_time"); - $phpMaxTime = (!is_numeric($phpMaxTime) ? 10 : @intval($phpMaxTime)) ?: 10; - - return max(1, $phpMaxTime); - } - - /** - * Write a message to the debug error log - * - * @param string $message The message to log - * @param int $priority The message's log priority - * - * @return void - * @since 4.0.4 - */ - private function debugMsg(string $message, int $priority = self::LOG_INFO): void - { - if (!defined('_JOOMLA_UPDATE_DEBUG')) - { - return; - } - - if (!is_resource(self::$logFP) && !is_bool(self::$logFP)) - { - self::$logFP = @fopen(self::$logFilePath, 'at'); - } - - if (!is_resource(self::$logFP)) - { - return; - } - - switch ($priority) - { - case self::LOG_DEBUG: - $priorityString = 'DEBUG'; - break; - - case self::LOG_INFO: - $priorityString = 'INFO'; - break; - - case self::LOG_WARNING: - $priorityString = 'WARNING'; - break; - - case self::LOG_ERROR: - $priorityString = 'ERROR'; - break; - } - - fputs(self::$logFP, sprintf('%s | %7s | %s' . "\r\n", gmdate('Y-m-d H:i:s'), $priorityString, $message)); - } - - /** - * Initialise the debug log file - * - * @param string $logPath The path where the log file will be written to - * - * @return void - * @since 4.0.4 - */ - private function initializeLog(string $logPath): void - { - if (!defined('_JOOMLA_UPDATE_DEBUG')) - { - return; - } - - $logPath = $logPath ?: dirname($this->filename); - $logFile = rtrim($logPath, '/' . DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'joomla_update.txt'; - - self::$logFilePath = $logFile; - } + /** + * How much data to read at once when processing files + * + * @var int + * @since 4.0.4 + */ + private const CHUNK_SIZE = 524288; + + /** + * Maximum execution time (seconds). + * + * Each page load will take at most this much time. Please note that if the ZIP archive contains fairly large, + * compressed files we may overshoot this time since we can't interrupt the decompression. This should not be an + * issue in the context of updating Joomla as the ZIP archive contains fairly small files. + * + * If this is too low it will cause too many requests to hit the server, potentially triggering a DoS protection and + * causing the extraction to fail. If this is too big the extraction will not be as verbose and the user might think + * something is broken. A value between 3 and 7 seconds is, therefore, recommended. + * + * @var int + * @since 4.0.4 + */ + private const MAX_EXEC_TIME = 4; + + /** + * Run-time execution bias (percentage points). + * + * We evaluate the time remaining on the timer before processing each file on the ZIP archive. If we have already + * consumed at least this much percentage of the MAX_EXEC_TIME we will stop processing the archive in this page + * load, return the result to the client and wait for it to call us again so we can resume the extraction. + * + * This becomes important when the MAX_EXEC_TIME is close to the PHP, PHP-FPM or Apache timeout on the server + * (whichever is lowest) and there are fairly large files in the backup archive. If we start extracting a large, + * compressed file close to a hard server timeout it's possible that we will overshoot that hard timeout and see the + * extraction failing. + * + * Since Joomla Update is used to extract a ZIP archive with many small files we can keep at a fairly high 90% + * without much fear that something will break. + * + * Example: if MAX_EXEC_TIME is 10 seconds and RUNTIME_BIAS is 80 each page load will take between 80% and 100% of + * the MAX_EXEC_TIME, i.e. anywhere between 8 and 10 seconds. + * + * Lower values make it less likely to overshoot MAX_EXEC_TIME when extracting large files. + * + * @var int + * @since 4.0.4 + */ + private const RUNTIME_BIAS = 90; + + /** + * Minimum execution time (seconds). + * + * A request cannot take less than this many seconds. If it does, we add “dead time” (sleep) where the script does + * nothing except wait. This is essentially a rate limiting feature to avoid hitting a server-side DoS protection + * which could be triggered if we ended up sending too many requests in a limited amount of time. + * + * This should normally be less than MAX_EXEC * (RUNTIME_BIAS / 100). Values between that and MAX_EXEC_TIME have the + * effect of almost always adding dead time in each request, unless a really large file is being extracted from the + * ZIP archive. Values larger than MAX_EXEC will always add dead time to the request. This can be useful to + * artificially reduce the CPU usage limit. Some servers might kill the request if they see a sustained CPU usage + * spike over a short period of time. + * + * The chosen value of 3 seconds belongs to the first category, essentially making sure that we have a decent rate + * limiting without annoying the user too much but also catering for the most badly configured of shared + * hosting. It's a happy medium which works for the majority (~90%) of commercial servers out there. + * + * @var int + * @since 4.0.4 + */ + private const MIN_EXEC_TIME = 3; + + /** + * Internal state when extracting files: we need to be initialised + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_INITIALIZE = -1; + + /** + * Internal state when extracting files: no file currently being extracted + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_NOFILE = 0; + + /** + * Internal state when extracting files: reading the file header + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_HEADER = 1; + + /** + * Internal state when extracting files: reading file data + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_DATA = 2; + + /** + * Internal state when extracting files: file data has been read thoroughly + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_DATAREAD = 3; + + /** + * Internal state when extracting files: post-processing the file + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_POSTPROC = 4; + + /** + * Internal state when extracting files: done with this file + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_DONE = 5; + + /** + * Internal state when extracting files: finished extracting the ZIP file + * + * @var int + * @since 4.0.4 + */ + private const AK_STATE_FINISHED = 999; + + /** + * Internal logging level: debug + * + * @var int + * @since 4.0.4 + */ + private const LOG_DEBUG = 1; + + /** + * Internal logging level: information + * + * @var int + * @since 4.0.4 + */ + private const LOG_INFO = 10; + + /** + * Internal logging level: warning + * + * @var int + * @since 4.0.4 + */ + private const LOG_WARNING = 50; + + /** + * Internal logging level: error + * + * @var int + * @since 4.0.4 + */ + private const LOG_ERROR = 90; + + /** + * Singleton instance + * + * @var null|self + * @since 4.0.4 + */ + private static $instance = null; + + /** + * Debug log file pointer resource + * + * @var null|resource|boolean + * @since 4.0.4 + */ + private static $logFP = null; + + /** + * Debug log filename + * + * @var null|string + * @since 4.0.4 + */ + private static $logFilePath = null; + + /** + * The total size of the ZIP archive + * + * @var integer + * @since 4.0.4 + */ + public $totalSize = 0; + + /** + * Which files to skip + * + * @var array + * @since 4.0.4 + */ + public $skipFiles = []; + + /** + * Current tally of compressed size read + * + * @var integer + * @since 4.0.4 + */ + public $compressedTotal = 0; + + /** + * Current tally of bytes written to disk + * + * @var integer + * @since 4.0.4 + */ + public $uncompressedTotal = 0; + + /** + * Current tally of files extracted + * + * @var integer + * @since 4.0.4 + */ + public $filesProcessed = 0; + + /** + * Maximum execution time allowance per step + * + * @var integer + * @since 4.0.4 + */ + private $maxExecTime = null; + + /** + * Timestamp of execution start + * + * @var integer + * @since 4.0.4 + */ + private $startTime; + + /** + * The last error message + * + * @var string|null + * @since 4.0.4 + */ + private $lastErrorMessage = null; + + /** + * Archive filename + * + * @var string + * @since 4.0.4 + */ + private $filename = null; + + /** + * Current archive part number + * + * @var boolean + * @since 4.0.4 + */ + private $archiveFileIsBeingRead = false; + + /** + * The offset inside the current part + * + * @var integer + * @since 4.0.4 + */ + private $currentOffset = 0; + + /** + * Absolute path to prepend to extracted files + * + * @var string + * @since 4.0.4 + */ + private $addPath = ''; + + /** + * File pointer to the current archive part file + * + * @var resource|null + * @since 4.0.4 + */ + private $fp = null; + + /** + * Run state when processing the current archive file + * + * @var integer + * @since 4.0.4 + */ + private $runState = self::AK_STATE_INITIALIZE; + + /** + * File header data, as read by the readFileHeader() method + * + * @var stdClass + * @since 4.0.4 + */ + private $fileHeader = null; + + /** + * How much of the uncompressed data we've read so far + * + * @var integer + * @since 4.0.4 + */ + private $dataReadLength = 0; + + /** + * Unwritable files in these directories are always ignored and do not cause errors when not + * extracted. + * + * @var array + * @since 4.0.4 + */ + private $ignoreDirectories = []; + + /** + * Internal flag, set when the ZIP file has a data descriptor (which we will be ignoring) + * + * @var boolean + * @since 4.0.4 + */ + private $expectDataDescriptor = false; + + /** + * The UNIX last modification timestamp of the file last extracted + * + * @var integer + * @since 4.0.4 + */ + private $lastExtractedFileTimestamp = 0; + + /** + * The file path of the file last extracted + * + * @var string + * @since 4.0.4 + */ + private $lastExtractedFilename = null; + + /** + * Public constructor. + * + * Sets up the internal timer. + * + * @since 4.0.4 + */ + public function __construct() + { + $this->setupMaxExecTime(); + + // Initialize start time + $this->startTime = microtime(true); + } + + /** + * Singleton implementation. + * + * @return static + * @since 4.0.4 + */ + public static function getInstance(): self + { + if (is_null(self::$instance)) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Returns a serialised copy of the object. + * + * This is different to calling serialise() directly. This operates on a copy of the object which undergoes a + * call to shutdown() first so any open files are closed first. + * + * @return string The serialised data, potentially base64 encoded. + * @since 4.0.4 + */ + public static function getSerialised(): string + { + $clone = clone self::getInstance(); + $clone->shutdown(); + $serialized = serialize($clone); + + return (function_exists('base64_encode') && function_exists('base64_decode')) ? base64_encode($serialized) : $serialized; + } + + /** + * Restores a serialised instance into the singleton implementation and returns it. + * + * If the serialised data is corrupt it will return null. + * + * @param string $serialised The serialised data, potentially base64 encoded, to deserialize. + * + * @return static|null The instance of the object, NULL if it cannot be deserialised. + * @since 4.0.4 + */ + public static function unserialiseInstance(string $serialised): ?self + { + if (function_exists('base64_encode') && function_exists('base64_decode')) { + $serialised = base64_decode($serialised); + } + + $instance = @unserialize($serialised, [ + 'allowed_classes' => [ + self::class, + stdClass::class, + ], + ]); + + if (($instance === false) || !is_object($instance) || !($instance instanceof self)) { + return null; + } + + self::$instance = $instance; + + return self::$instance; + } + + /** + * Wakeup function, called whenever the class is deserialized. + * + * This method does the following: + * - Restart the timer. + * - Reopen the archive file, if one is defined. + * - Seek to the correct offset of the file. + * + * @return void + * @since 4.0.4 + * @internal + */ + public function __wakeup(): void + { + // Reset the timer when deserializing the object. + $this->startTime = microtime(true); + + if (!$this->archiveFileIsBeingRead) { + return; + } + + $this->fp = @fopen($this->filename, 'rb'); + + if ((is_resource($this->fp)) && ($this->currentOffset > 0)) { + @fseek($this->fp, $this->currentOffset); + } + } + + /** + * Enforce the minimum execution time. + * + * @return void + * @since 4.0.4 + */ + public function enforceMinimumExecutionTime() + { + $elapsed = $this->getRunningTime() * 1000; + $minExecTime = 1000.0 * min(1, (min(self::MIN_EXEC_TIME, $this->getPhpMaxExecTime()) - 1)); + + // Only run a sleep delay if we haven't reached the minimum execution time + if (($minExecTime <= $elapsed) || ($elapsed <= 0)) { + return; + } + + $sleepMillisec = intval($minExecTime - $elapsed); + + /** + * If we need to sleep for more than 1 second we should be using sleep() or time_sleep_until() to prevent high + * CPU usage, also because some OS might not support sleeping for over 1 second using these functions. In all + * other cases we will try to use usleep or time_nanosleep instead. + */ + $longSleep = $sleepMillisec > 1000; + $miniSleepSupported = function_exists('usleep') || function_exists('time_nanosleep'); + + if (!$longSleep && $miniSleepSupported) { + if (function_exists('usleep') && ($sleepMillisec < 1000)) { + usleep(1000 * $sleepMillisec); + + return; + } + + if (function_exists('time_nanosleep') && ($sleepMillisec < 1000)) { + time_nanosleep(0, 1000000 * $sleepMillisec); + + return; + } + } + + if (function_exists('sleep')) { + sleep(ceil($sleepMillisec / 1000)); + + return; + } + + if (function_exists('time_sleep_until')) { + time_sleep_until(time() + ceil($sleepMillisec / 1000)); + } + } + + /** + * Set the filepath to the ZIP archive which will be extracted. + * + * @param string $value The filepath to the archive. Only LOCAL files are allowed! + * + * @return void + * @since 4.0.4 + */ + public function setFilename(string $value) + { + // Security check: disallow remote filenames + if (!empty($value) && strpos($value, '://') !== false) { + $this->setError('Invalid archive location'); + + return; + } + + $this->filename = $value; + $this->initializeLog(dirname($this->filename)); + } + + /** + * Sets the path to prefix all extracted files with. Essentially, where the archive will be extracted to. + * + * @param string $addPath The path where the archive will be extracted. + * + * @return void + * @since 4.0.4 + */ + public function setAddPath(string $addPath): void + { + $this->addPath = $addPath; + $this->addPath = str_replace('\\', '/', $this->addPath); + $this->addPath = rtrim($this->addPath, '/'); + + if (!empty($this->addPath)) { + $this->addPath .= '/'; + } + } + + /** + * Set the list of files to skip when extracting the ZIP file. + * + * @param array $skipFiles A list of files to skip when extracting the ZIP archive + * + * @return void + * @since 4.0.4 + */ + public function setSkipFiles(array $skipFiles): void + { + $this->skipFiles = array_values($skipFiles); + } + + /** + * Set the directories to skip over when extracting the ZIP archive + * + * @param array $ignoreDirectories The list of directories to ignore. + * + * @return void + * @since 4.0.4 + */ + public function setIgnoreDirectories(array $ignoreDirectories): void + { + $this->ignoreDirectories = array_values($ignoreDirectories); + } + + /** + * Prepares for the archive extraction + * + * @return void + * @since 4.0.4 + */ + public function initialize(): void + { + $this->debugMsg(sprintf('Initializing extraction. Filepath: %s', $this->filename)); + $this->totalSize = @filesize($this->filename) ?: 0; + $this->archiveFileIsBeingRead = false; + $this->currentOffset = 0; + $this->runState = self::AK_STATE_NOFILE; + + $this->readArchiveHeader(); + + if (!empty($this->getError())) { + $this->debugMsg(sprintf('Error: %s', $this->getError()), self::LOG_ERROR); + + return; + } + + $this->archiveFileIsBeingRead = true; + $this->runState = self::AK_STATE_NOFILE; + + $this->debugMsg('Setting state to NOFILE', self::LOG_DEBUG); + } + + /** + * Executes a step of the archive extraction + * + * @return boolean True if we are done extracting or an error occurred + * @since 4.0.4 + */ + public function step(): bool + { + $status = true; + + $this->debugMsg('Starting a new step', self::LOG_INFO); + + while ($status && ($this->getTimeLeft() > 0)) { + switch ($this->runState) { + case self::AK_STATE_INITIALIZE: + $this->debugMsg('Current run state: INITIALIZE', self::LOG_DEBUG); + $this->initialize(); + break; + + case self::AK_STATE_NOFILE: + $this->debugMsg('Current run state: NOFILE', self::LOG_DEBUG); + $status = $this->readFileHeader(); + + if ($status) { + $this->debugMsg('Found file header; updating number of files processed and bytes in/out', self::LOG_DEBUG); + + // Update running tallies when we start extracting a file + $this->filesProcessed++; + $this->compressedTotal += array_key_exists('compressed', get_object_vars($this->fileHeader)) + ? $this->fileHeader->compressed : 0; + $this->uncompressedTotal += $this->fileHeader->uncompressed; + } + + break; + + case self::AK_STATE_HEADER: + case self::AK_STATE_DATA: + $runStateHuman = $this->runState === self::AK_STATE_HEADER ? 'HEADER' : 'DATA'; + $this->debugMsg(sprintf('Current run state: %s', $runStateHuman), self::LOG_DEBUG); + + $status = $this->processFileData(); + break; + + case self::AK_STATE_DATAREAD: + case self::AK_STATE_POSTPROC: + $runStateHuman = $this->runState === self::AK_STATE_DATAREAD ? 'DATAREAD' : 'POSTPROC'; + $this->debugMsg(sprintf('Current run state: %s', $runStateHuman), self::LOG_DEBUG); + + $this->setLastExtractedFileTimestamp($this->fileHeader->timestamp); + $this->processLastExtractedFile(); + + $status = true; + $this->runState = self::AK_STATE_DONE; + break; + + case self::AK_STATE_DONE: + default: + $this->debugMsg('Current run state: DONE', self::LOG_DEBUG); + $this->runState = self::AK_STATE_NOFILE; + + break; + + case self::AK_STATE_FINISHED: + $this->debugMsg('Current run state: FINISHED', self::LOG_DEBUG); + $status = false; + break; + } + + if ($this->getTimeLeft() <= 0) { + $this->debugMsg('Ran out of time; the step will break.'); + } elseif (!$status) { + $this->debugMsg('Last step status is false; the step will break.'); + } + } + + $error = $this->getError() ?? null; + + if (!empty($error)) { + $this->debugMsg(sprintf('Step failed with error: %s', $error), self::LOG_ERROR); + } + + // Did we just finish or run into an error? + if (!empty($error) || $this->runState === self::AK_STATE_FINISHED) { + $this->debugMsg('Returning true (must stop running) from step()', self::LOG_DEBUG); + + // Reset internal state, prevents __wakeup from trying to open a non-existent file + $this->archiveFileIsBeingRead = false; + + return true; + } + + $this->debugMsg('Returning false (must continue running) from step()', self::LOG_DEBUG); + + return false; + } + + /** + * Get the most recent error message + * + * @return string|null The message string, null if there's no error + * @since 4.0.4 + */ + public function getError(): ?string + { + return $this->lastErrorMessage; + } + + /** + * Gets the number of seconds left, before we hit the "must break" threshold + * + * @return float + * @since 4.0.4 + */ + private function getTimeLeft(): float + { + return $this->maxExecTime - $this->getRunningTime(); + } + + /** + * Gets the time elapsed since object creation/unserialization, effectively how + * long Akeeba Engine has been processing data + * + * @return float + * @since 4.0.4 + */ + private function getRunningTime(): float + { + return microtime(true) - $this->startTime; + } + + /** + * Process the last extracted file or directory + * + * This invalidates OPcache for .php files. Also applies the correct permissions and timestamp. + * + * @return void + * @since 4.0.4 + */ + private function processLastExtractedFile(): void + { + $this->debugMsg(sprintf('Processing last extracted entity: %s', $this->lastExtractedFilename), self::LOG_DEBUG); + + if (@is_file($this->lastExtractedFilename)) { + @chmod($this->lastExtractedFilename, 0644); + + clearFileInOPCache($this->lastExtractedFilename); + } else { + @chmod($this->lastExtractedFilename, 0755); + } + + if ($this->lastExtractedFileTimestamp > 0) { + @touch($this->lastExtractedFilename, $this->lastExtractedFileTimestamp); + } + } + + /** + * Set the last extracted filename + * + * @param string|null $lastExtractedFilename The last extracted filename + * + * @return void + * @since 4.0.4 + */ + private function setLastExtractedFilename(?string $lastExtractedFilename): void + { + $this->lastExtractedFilename = $lastExtractedFilename; + } + + /** + * Set the last modification UNIX timestamp for the last extracted file + * + * @param int $lastExtractedFileTimestamp The timestamp + * + * @return void + * @since 4.0.4 + */ + private function setLastExtractedFileTimestamp(int $lastExtractedFileTimestamp): void + { + $this->lastExtractedFileTimestamp = $lastExtractedFileTimestamp; + } + + /** + * Sleep function, called whenever the class is serialized + * + * @return void + * @since 4.0.4 + * @internal + */ + private function shutdown(): void + { + if (is_resource(self::$logFP)) { + @fclose(self::$logFP); + } + + if (!is_resource($this->fp)) { + return; + } + + $this->currentOffset = @ftell($this->fp); + + @fclose($this->fp); + } + + /** + * Unicode-safe binary data length + * + * @param string|null $string The binary data to get the length for + * + * @return integer + * @since 4.0.4 + */ + private function binStringLength(?string $string): int + { + if (is_null($string)) { + return 0; + } + + if (function_exists('mb_strlen')) { + return mb_strlen($string, '8bit') ?: 0; + } + + return strlen($string) ?: 0; + } + + /** + * Add an error message + * + * @param string $error Error message + * + * @return void + * @since 4.0.4 + */ + private function setError(string $error): void + { + $this->lastErrorMessage = $error; + } + + /** + * Reads data from the archive. + * + * @param resource $fp The file pointer to read data from + * @param int|null $length The volume of data to read, in bytes + * + * @return string The data read from the file + * @since 4.0.4 + */ + private function fread($fp, ?int $length = null): string + { + $readLength = (is_numeric($length) && ($length > 0)) ? $length : PHP_INT_MAX; + $data = fread($fp, $readLength); + + if ($data === false) { + $this->debugMsg('No more data could be read from the file', self::LOG_WARNING); + + $data = ''; + } + + return $data; + } + + /** + * Read the header of the archive, making sure it's a valid ZIP file. + * + * @return void + * @since 4.0.4 + */ + private function readArchiveHeader(): void + { + $this->debugMsg('Reading the archive header.', self::LOG_DEBUG); + + // Open the first part + $this->openArchiveFile(); + + // Fail for unreadable files + if ($this->fp === false) { + return; + } + + // Read the header data. + $sigBinary = fread($this->fp, 4); + $headerData = unpack('Vsig', $sigBinary); + + // We only support single part ZIP files + if ($headerData['sig'] != 0x04034b50) { + $this->setError('The archive file is corrupt: bad header'); + + return; + } + + // Roll back the file pointer + fseek($this->fp, -4, SEEK_CUR); + + $this->currentOffset = @ftell($this->fp); + $this->dataReadLength = 0; + } + + /** + * Concrete classes must use this method to read the file header + * + * @return boolean True if reading the file was successful, false if an error occurred or we + * reached end of archive. + * @since 4.0.4 + */ + private function readFileHeader(): bool + { + $this->debugMsg('Reading the file entry header.', self::LOG_DEBUG); + + if (!is_resource($this->fp)) { + $this->setError('The archive is not open for reading.'); + + return false; + } + + // Unexpected end of file + if ($this->isEOF()) { + $this->debugMsg('EOF when reading file header data', self::LOG_WARNING); + $this->setError('The archive is corrupt or truncated'); + + return false; + } + + $this->currentOffset = ftell($this->fp); + + if ($this->expectDataDescriptor) { + $this->debugMsg('Expecting data descriptor (bit 3 of general purpose flag was set).', self::LOG_DEBUG); + + /** + * The last file had bit 3 of the general purpose bit flag set. This means that we have a 12 byte data + * descriptor we need to skip. To make things worse, there might also be a 4 byte optional data descriptor + * header (0x08074b50). + */ + $junk = @fread($this->fp, 4); + $junk = unpack('Vsig', $junk); + $readLength = ($junk['sig'] == 0x08074b50) ? 12 : 8; + $junk = @fread($this->fp, $readLength); + + // And check for EOF, too + if ($this->isEOF()) { + $this->debugMsg('EOF when reading data descriptor', self::LOG_WARNING); + $this->setError('The archive is corrupt or truncated'); + + return false; + } + } + + // Get and decode Local File Header + $headerBinary = fread($this->fp, 30); + $format = 'Vsig/C2ver/vbitflag/vcompmethod/vlastmodtime/vlastmoddate/Vcrc/Vcompsize/' + . 'Vuncomp/vfnamelen/veflen'; + $headerData = unpack($format, $headerBinary); + + // Check signature + if (!($headerData['sig'] == 0x04034b50)) { + // The signature is not the one used for files. Is this a central directory record (i.e. we're done)? + if ($headerData['sig'] == 0x02014b50) { + $this->debugMsg('Found Central Directory header; the extraction is complete', self::LOG_DEBUG); + + // End of ZIP file detected. We'll just skip to the end of file... + @fseek($this->fp, 0, SEEK_END); + $this->runState = self::AK_STATE_FINISHED; + + return false; + } + + $this->setError('The archive file is corrupt or truncated'); + + return false; + } + + // If bit 3 of the bitflag is set, expectDataDescriptor is true + $this->expectDataDescriptor = ($headerData['bitflag'] & 4) == 4; + $this->fileHeader = new stdClass(); + $this->fileHeader->timestamp = 0; + + // Read the last modified date and time + $lastmodtime = $headerData['lastmodtime']; + $lastmoddate = $headerData['lastmoddate']; + + if ($lastmoddate && $lastmodtime) { + $vHour = ($lastmodtime & 0xF800) >> 11; + $vMInute = ($lastmodtime & 0x07E0) >> 5; + $vSeconds = ($lastmodtime & 0x001F) * 2; + $vYear = (($lastmoddate & 0xFE00) >> 9) + 1980; + $vMonth = ($lastmoddate & 0x01E0) >> 5; + $vDay = $lastmoddate & 0x001F; + + $this->fileHeader->timestamp = @mktime($vHour, $vMInute, $vSeconds, $vMonth, $vDay, $vYear); + } + + $isBannedFile = false; + + $this->fileHeader->compressed = $headerData['compsize']; + $this->fileHeader->uncompressed = $headerData['uncomp']; + $nameFieldLength = $headerData['fnamelen']; + $extraFieldLength = $headerData['eflen']; + + // Read filename field + $this->fileHeader->file = fread($this->fp, $nameFieldLength); + + // Read extra field if present + if ($extraFieldLength > 0) { + $extrafield = fread($this->fp, $extraFieldLength); + } + + // Decide filetype -- Check for directories + $this->fileHeader->type = 'file'; + + if (strrpos($this->fileHeader->file, '/') == strlen($this->fileHeader->file) - 1) { + $this->fileHeader->type = 'dir'; + } + + // Decide filetype -- Check for symbolic links + if (($headerData['ver1'] == 10) && ($headerData['ver2'] == 3)) { + $this->fileHeader->type = 'link'; + } + + switch ($headerData['compmethod']) { + case 0: + $this->fileHeader->compression = 'none'; + break; + case 8: + $this->fileHeader->compression = 'gzip'; + break; + default: + $messageTemplate = 'This script cannot handle ZIP compression method %d. ' + . 'Only 0 (no compression) and 8 (DEFLATE, gzip) can be handled.'; + $actualMessage = sprintf($messageTemplate, $headerData['compmethod']); + $this->setError($actualMessage); + + return false; + break; + } + + // Find hard-coded banned files + if ((basename($this->fileHeader->file) == ".") || (basename($this->fileHeader->file) == "..")) { + $isBannedFile = true; + } + + // Also try to find banned files passed in class configuration + if ((count($this->skipFiles) > 0) && in_array($this->fileHeader->file, $this->skipFiles)) { + $isBannedFile = true; + } + + // If we have a banned file, let's skip it + if ($isBannedFile) { + $debugMessage = sprintf('Current entity (%s) is banned from extraction and will be skipped over.', $this->fileHeader->file); + $this->debugMsg($debugMessage, self::LOG_DEBUG); + + // Advance the file pointer, skipping exactly the size of the compressed data + $seekleft = $this->fileHeader->compressed; + + while ($seekleft > 0) { + // Ensure that we can seek past archive part boundaries + $curSize = @filesize($this->filename); + $curPos = @ftell($this->fp); + $canSeek = $curSize - $curPos; + $canSeek = ($canSeek > $seekleft) ? $seekleft : $canSeek; + @fseek($this->fp, $canSeek, SEEK_CUR); + $seekleft -= $canSeek; + + if ($seekleft) { + $this->setError('The archive is corrupt or truncated'); + + return false; + } + } + + $this->currentOffset = @ftell($this->fp); + $this->runState = self::AK_STATE_DONE; + + return true; + } + + // Last chance to prepend a path to the filename + if (!empty($this->addPath)) { + $this->fileHeader->file = $this->addPath . $this->fileHeader->file; + } + + // Get the translated path name + if ($this->fileHeader->type == 'file') { + $this->fileHeader->realFile = $this->fileHeader->file; + $this->setLastExtractedFilename($this->fileHeader->file); + } elseif ($this->fileHeader->type == 'dir') { + $this->fileHeader->timestamp = 0; + + $dir = $this->fileHeader->file; + + if (!@is_dir($dir)) { + mkdir($dir, 0755, true); + } + + $this->setLastExtractedFilename(null); + } else { + // Symlink; do not post-process + $this->fileHeader->timestamp = 0; + $this->setLastExtractedFilename(null); + } + + $this->createDirectory(); + + // Header is read + $this->runState = self::AK_STATE_HEADER; + + return true; + } + + /** + * Creates the directory this file points to + * + * @return void + * @since 4.0.4 + */ + private function createDirectory(): void + { + // Do we need to create a directory? + if (empty($this->fileHeader->realFile)) { + $this->fileHeader->realFile = $this->fileHeader->file; + } + + $lastSlash = strrpos($this->fileHeader->realFile, '/'); + $dirName = substr($this->fileHeader->realFile, 0, $lastSlash); + $perms = 0755; + $ignore = $this->isIgnoredDirectory($dirName); + + if (@is_dir($dirName)) { + return; + } + + if ((@mkdir($dirName, $perms, true) === false) && (!$ignore)) { + $this->setError(sprintf('Could not create %s folder', $dirName)); + } + } + + /** + * Concrete classes must use this method to process file data. It must set $runState to self::AK_STATE_DATAREAD when + * it's finished processing the file data. + * + * @return boolean True if processing the file data was successful, false if an error occurred + * @since 4.0.4 + */ + private function processFileData(): bool + { + switch ($this->fileHeader->type) { + case 'dir': + $this->debugMsg('Extracting entity of type Directory', self::LOG_DEBUG); + + return $this->processTypeDir(); + break; + + case 'link': + $this->debugMsg('Extracting entity of type Symbolic Link', self::LOG_DEBUG); + + return $this->processTypeLink(); + break; + + case 'file': + switch ($this->fileHeader->compression) { + case 'none': + $this->debugMsg('Extracting entity of type File (Stored)', self::LOG_DEBUG); + + return $this->processTypeFileUncompressed(); + break; + + case 'gzip': + case 'bzip2': + $this->debugMsg('Extracting entity of type File (Compressed)', self::LOG_DEBUG); + + return $this->processTypeFileCompressed(); + break; + + case 'default': + $this->setError(sprintf('Unknown compression type %s.', $this->fileHeader->compression)); + + return false; + break; + } + break; + } + + $this->setError(sprintf('Unknown entry type %s.', $this->fileHeader->type)); + + return false; + } + + /** + * Opens the next part file for reading + * + * @return void + * @since 4.0.4 + */ + private function openArchiveFile(): void + { + $this->debugMsg('Opening archive file for reading', self::LOG_DEBUG); + + if ($this->archiveFileIsBeingRead) { + return; + } + + if (is_resource($this->fp)) { + @fclose($this->fp); + } + + $this->fp = @fopen($this->filename, 'rb'); + + if ($this->fp === false) { + $message = 'Could not open archive for reading. Check that the file exists, is ' + . 'readable by the web server and is not in a directory made out of reach by chroot, ' + . 'open_basedir restrictions or any other restriction put in place by your host.'; + $this->setError($message); + + return; + } + + fseek($this->fp, 0); + $this->currentOffset = 0; + } + + /** + * Returns true if we have reached the end of file + * + * @return boolean True if we have reached End Of File + * @since 4.0.4 + */ + private function isEOF(): bool + { + /** + * feof() will return false if the file pointer is exactly at the last byte of the file. However, this is a + * condition we want to treat as a proper EOF for the purpose of extracting a ZIP file. Hence the second part + * after the logical OR. + */ + return @feof($this->fp) || (@ftell($this->fp) > @filesize($this->filename)); + } + + /** + * Handles the permissions of the parent directory to a file and the file itself to make it writeable. + * + * @param string $path A path to a file + * + * @return void + * @since 4.0.4 + */ + private function setCorrectPermissions(string $path): void + { + static $rootDir = null; + + if (is_null($rootDir)) { + $rootDir = rtrim($this->addPath, '/\\'); + } + + $directory = rtrim(dirname($path), '/\\'); + + // Is this an unwritable directory? + if (($directory != $rootDir) && !is_writeable($directory)) { + @chmod($directory, 0755); + } + + @chmod($path, 0644); + } + + /** + * Is this file or directory contained in a directory we've decided to ignore + * write errors for? This is useful to let the extraction work despite write + * errors in the log, logs and tmp directories which MIGHT be used by the system + * on some low quality hosts and Plesk-powered hosts. + * + * @param string $shortFilename The relative path of the file/directory in the package + * + * @return boolean True if it belongs in an ignored directory + * @since 4.0.4 + */ + private function isIgnoredDirectory(string $shortFilename): bool + { + $check = substr($shortFilename, -1) == '/' ? rtrim($shortFilename, '/') : dirname($shortFilename); + + return in_array($check, $this->ignoreDirectories); + } + + /** + * Process the file data of a directory entry + * + * @return boolean + * @since 4.0.4 + */ + private function processTypeDir(): bool + { + // Directory entries do not have file data, therefore we're done processing the entry. + $this->runState = self::AK_STATE_DATAREAD; + + return true; + } + + /** + * Process the file data of a link entry + * + * @return boolean + * @since 4.0.4 + */ + private function processTypeLink(): bool + { + $toReadBytes = 0; + $leftBytes = $this->fileHeader->compressed; + $data = ''; + + while ($leftBytes > 0) { + $toReadBytes = min($leftBytes, self::CHUNK_SIZE); + $mydata = $this->fread($this->fp, $toReadBytes); + $reallyReadBytes = $this->binStringLength($mydata); + $data .= $mydata; + $leftBytes -= $reallyReadBytes; + + if ($reallyReadBytes < $toReadBytes) { + // We read less than requested! + if ($this->isEOF()) { + $this->debugMsg('EOF when reading symlink data', self::LOG_WARNING); + $this->setError('The archive file is corrupt or truncated'); + + return false; + } + } + } + + $filename = $this->fileHeader->realFile ?? $this->fileHeader->file; + + // Try to remove an existing file or directory by the same name + if (file_exists($filename)) { + clearFileInOPCache($filename); + @unlink($filename); + @rmdir($filename); + } + + // Remove any trailing slash + if (substr($filename, -1) == '/') { + $filename = substr($filename, 0, -1); + } + + // Create the symlink + @symlink($data, $filename); + + $this->runState = self::AK_STATE_DATAREAD; + + // No matter if the link was created! + return true; + } + + /** + * Processes an uncompressed (stored) file + * + * @return boolean + * @since 4.0.4 + */ + private function processTypeFileUncompressed(): bool + { + // Uncompressed files are being processed in small chunks, to avoid timeouts + if ($this->dataReadLength == 0) { + // Before processing file data, ensure permissions are adequate + $this->setCorrectPermissions($this->fileHeader->file); + } + + // Open the output file + $ignore = $this->isIgnoredDirectory($this->fileHeader->file); + + $writeMode = ($this->dataReadLength == 0) ? 'wb' : 'ab'; + $outfp = @fopen($this->fileHeader->realFile, $writeMode); + + // Can we write to the file? + if (($outfp === false) && (!$ignore)) { + // An error occurred + $this->setError(sprintf('Could not open %s for writing.', $this->fileHeader->realFile)); + + return false; + } + + // Does the file have any data, at all? + if ($this->fileHeader->compressed == 0) { + // No file data! + if (is_resource($outfp)) { + @fclose($outfp); + } + + $this->debugMsg('Zero byte Stored file; no data will be read', self::LOG_DEBUG); + + $this->runState = self::AK_STATE_DATAREAD; + + return true; + } + + $leftBytes = $this->fileHeader->compressed - $this->dataReadLength; + + // Loop while there's data to read and enough time to do it + while (($leftBytes > 0) && ($this->getTimeLeft() > 0)) { + $toReadBytes = min($leftBytes, self::CHUNK_SIZE); + $data = $this->fread($this->fp, $toReadBytes); + $reallyReadBytes = $this->binStringLength($data); + $leftBytes -= $reallyReadBytes; + $this->dataReadLength += $reallyReadBytes; + + if ($reallyReadBytes < $toReadBytes) { + // We read less than requested! Why? Did we hit local EOF? + if ($this->isEOF()) { + // Nope. The archive is corrupt + $this->debugMsg('EOF when reading stored file data', self::LOG_WARNING); + $this->setError('The archive file is corrupt or truncated'); + + return false; + } + } + + if (is_resource($outfp)) { + @fwrite($outfp, $data); + } + + if ($this->getTimeLeft()) { + $this->debugMsg('Out of time; will resume extraction in the next step', self::LOG_DEBUG); + } + } + + // Close the file pointer + if (is_resource($outfp)) { + @fclose($outfp); + } + + // Was this a pre-timeout bail out? + if ($leftBytes > 0) { + $this->debugMsg(sprintf('We have %d bytes left to extract in the next step', $leftBytes), self::LOG_DEBUG); + $this->runState = self::AK_STATE_DATA; + + return true; + } + + // Oh! We just finished! + $this->runState = self::AK_STATE_DATAREAD; + $this->dataReadLength = 0; + + return true; + } + + /** + * Processes a compressed file + * + * @return boolean + * @since 4.0.4 + */ + private function processTypeFileCompressed(): bool + { + // Before processing file data, ensure permissions are adequate + $this->setCorrectPermissions($this->fileHeader->file); + + // Open the output file + $outfp = @fopen($this->fileHeader->realFile, 'wb'); + + // Can we write to the file? + $ignore = $this->isIgnoredDirectory($this->fileHeader->file); + + if (($outfp === false) && (!$ignore)) { + // An error occurred + $this->setError(sprintf('Could not open %s for writing.', $this->fileHeader->realFile)); + + return false; + } + + // Does the file have any data, at all? + if ($this->fileHeader->compressed == 0) { + $this->debugMsg('Zero byte Compressed file; no data will be read', self::LOG_DEBUG); + + // No file data! + if (is_resource($outfp)) { + @fclose($outfp); + } + + $this->runState = self::AK_STATE_DATAREAD; + + return true; + } + + // Simple compressed files are processed as a whole; we can't do chunk processing + $zipData = $this->fread($this->fp, $this->fileHeader->compressed); + + while ($this->binStringLength($zipData) < $this->fileHeader->compressed) { + // End of local file before reading all data? + if ($this->isEOF()) { + $this->debugMsg('EOF reading compressed data', self::LOG_WARNING); + $this->setError('The archive file is corrupt or truncated'); + + return false; + } + } + + switch ($this->fileHeader->compression) { + case 'gzip': + /** @noinspection PhpComposerExtensionStubsInspection */ + $unzipData = gzinflate($zipData); + break; + + case 'bzip2': + /** @noinspection PhpComposerExtensionStubsInspection */ + $unzipData = bzdecompress($zipData); + break; + + default: + $this->setError(sprintf('Unknown compression method %s', $this->fileHeader->compression)); + + return false; + break; + } + + unset($zipData); + + // Write to the file. + if (is_resource($outfp)) { + @fwrite($outfp, $unzipData, $this->fileHeader->uncompressed); + @fclose($outfp); + } + + unset($unzipData); + + $this->runState = self::AK_STATE_DATAREAD; + + return true; + } + + /** + * Set up the maximum execution time + * + * @return void + * @since 4.0.4 + */ + private function setupMaxExecTime(): void + { + $configMaxTime = self::MAX_EXEC_TIME; + $bias = self::RUNTIME_BIAS / 100; + $this->maxExecTime = min($this->getPhpMaxExecTime(), $configMaxTime) * $bias; + } + + /** + * Get the PHP maximum execution time. + * + * If it's not defined or it's zero (infinite) we use a fake value of 10 seconds. + * + * @return integer + * @since 4.0.4 + */ + private function getPhpMaxExecTime(): int + { + if (!@function_exists('ini_get')) { + return 10; + } + + $phpMaxTime = @ini_get("maximum_execution_time"); + $phpMaxTime = (!is_numeric($phpMaxTime) ? 10 : @intval($phpMaxTime)) ?: 10; + + return max(1, $phpMaxTime); + } + + /** + * Write a message to the debug error log + * + * @param string $message The message to log + * @param int $priority The message's log priority + * + * @return void + * @since 4.0.4 + */ + private function debugMsg(string $message, int $priority = self::LOG_INFO): void + { + if (!defined('_JOOMLA_UPDATE_DEBUG')) { + return; + } + + if (!is_resource(self::$logFP) && !is_bool(self::$logFP)) { + self::$logFP = @fopen(self::$logFilePath, 'at'); + } + + if (!is_resource(self::$logFP)) { + return; + } + + switch ($priority) { + case self::LOG_DEBUG: + $priorityString = 'DEBUG'; + break; + + case self::LOG_INFO: + $priorityString = 'INFO'; + break; + + case self::LOG_WARNING: + $priorityString = 'WARNING'; + break; + + case self::LOG_ERROR: + $priorityString = 'ERROR'; + break; + } + + fputs(self::$logFP, sprintf('%s | %7s | %s' . "\r\n", gmdate('Y-m-d H:i:s'), $priorityString, $message)); + } + + /** + * Initialise the debug log file + * + * @param string $logPath The path where the log file will be written to + * + * @return void + * @since 4.0.4 + */ + private function initializeLog(string $logPath): void + { + if (!defined('_JOOMLA_UPDATE_DEBUG')) { + return; + } + + $logPath = $logPath ?: dirname($this->filename); + $logFile = rtrim($logPath, '/' . DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'joomla_update.txt'; + + self::$logFilePath = $logFile; + } } // Skip over the mini-controller for testing purposes -if (defined('_JOOMLA_UPDATE_TESTING')) -{ - return; +if (defined('_JOOMLA_UPDATE_TESTING')) { + return; } /** @@ -1782,21 +1685,19 @@ private function initializeLog(string $logPath): void */ function clearFileInOPCache(string $file): bool { - static $hasOpCache = null; + static $hasOpCache = null; - if (is_null($hasOpCache)) - { - $hasOpCache = ini_get('opcache.enable') - && function_exists('opcache_invalidate') - && (!ini_get('opcache.restrict_api') || stripos(realpath($_SERVER['SCRIPT_FILENAME']), ini_get('opcache.restrict_api')) === 0); - } + if (is_null($hasOpCache)) { + $hasOpCache = ini_get('opcache.enable') + && function_exists('opcache_invalidate') + && (!ini_get('opcache.restrict_api') || stripos(realpath($_SERVER['SCRIPT_FILENAME']), ini_get('opcache.restrict_api')) === 0); + } - if ($hasOpCache && (strtolower(substr($file, -4)) === '.php')) - { - return opcache_invalidate($file, true); - } + if ($hasOpCache && (strtolower(substr($file, -4)) === '.php')) { + return opcache_invalidate($file, true); + } - return false; + return false; } /** @@ -1817,28 +1718,25 @@ function clearFileInOPCache(string $file): bool */ function timingSafeEquals($known, $user) { - if (function_exists('hash_equals')) - { - return hash_equals($known, $user); - } + if (function_exists('hash_equals')) { + return hash_equals($known, $user); + } - $safeLen = strlen($known); - $userLen = strlen($user); + $safeLen = strlen($known); + $userLen = strlen($user); - if ($userLen != $safeLen) - { - return false; - } + if ($userLen != $safeLen) { + return false; + } - $result = 0; + $result = 0; - for ($i = 0; $i < $userLen; $i++) - { - $result |= (ord($known[$i]) ^ ord($user[$i])); - } + for ($i = 0; $i < $userLen; $i++) { + $result |= (ord($known[$i]) ^ ord($user[$i])); + } - // They are only identical strings if $result is exactly 0... - return $result === 0; + // They are only identical strings if $result is exactly 0... + return $result === 0; } /** @@ -1850,93 +1748,86 @@ function timingSafeEquals($known, $user) */ function getConfiguration(): ?array { - // Make sure the locale is correct for basename() to work - if (function_exists('setlocale')) - { - @setlocale(LC_ALL, 'en_US.UTF8'); - } - - // Require update.php or fail - $setupFile = __DIR__ . '/update.php'; - - if (!file_exists($setupFile)) - { - return null; - } - - /** - * If the setup file was created more than 1.5 hours ago we can assume that it's stale and someone forgot to - * remove it from the server. - * - * This prevents brute force attacks against the randomly generated password. Even a simple 8 character simple - * alphanum (a-z, 0-9) password yields over 2.8e12 permutation. Assuming a very fast server which can - * serve 100 requests to extract.php per second and an easy to attack password requiring going over just 1% of - * the search space it'd still take over 282 million seconds to brute force it. Our limit is more than 4 orders - * of magnitude lower than this best practical case scenario, giving us adequate protection against all but the - * luckiest attacker (spoiler alert: the mathematics of probabilities say you're not gonna get too lucky). - * - * It is still advisable to remove the update.php file once you are done with the extraction. This check - * here is only meant as a failsafe in case of a server error during the extraction and subsequent lack of user - * action to remove the update.php file from their server. - */ - clearstatcache(true); - $setupFileCreationTime = filectime($setupFile); - - if (abs(time() - $setupFileCreationTime) > 5400) - { - return null; - } - - // Load update.php. It pulls a variable named $restoration_setup into the local scope. - clearFileInOPCache($setupFile); - - require_once $setupFile; - - /** @var array $extractionSetup */ - - // The file exists but no configuration is present? - if (empty($extractionSetup ?? null) || !is_array($extractionSetup)) - { - return null; - } - - /** - * Immediately reject any attempt to run extract.php without a password. - * - * Doing that is a GRAVE SECURITY RISK. It makes it trivial to hack a site. Therefore we are preventing this script - * to run without a password. - */ - $password = $extractionSetup['security.password'] ?? null; - $userPassword = $_REQUEST['password'] ?? ''; - $userPassword = !is_string($userPassword) ? '' : trim($userPassword); - - if (empty($password) || !is_string($password) || (trim($password) == '') || (strlen(trim($password)) < 32)) - { - return null; - } - - // Timing-safe password comparison. See http://blog.ircmaxell.com/2014/11/its-all-about-time.html - if (!timingSafeEquals($password, $userPassword)) - { - return null; - } - - // An "instance" variable will resume the engine from the serialised instance - $serialized = $_REQUEST['instance'] ?? null; - - if (!is_null($serialized) && empty(ZIPExtraction::unserialiseInstance($serialized))) - { - // The serialised instance is corrupt or someone tries to trick us. YOU SHALL NOT PASS! - return null; - } - - return $extractionSetup; + // Make sure the locale is correct for basename() to work + if (function_exists('setlocale')) { + @setlocale(LC_ALL, 'en_US.UTF8'); + } + + // Require update.php or fail + $setupFile = __DIR__ . '/update.php'; + + if (!file_exists($setupFile)) { + return null; + } + + /** + * If the setup file was created more than 1.5 hours ago we can assume that it's stale and someone forgot to + * remove it from the server. + * + * This prevents brute force attacks against the randomly generated password. Even a simple 8 character simple + * alphanum (a-z, 0-9) password yields over 2.8e12 permutation. Assuming a very fast server which can + * serve 100 requests to extract.php per second and an easy to attack password requiring going over just 1% of + * the search space it'd still take over 282 million seconds to brute force it. Our limit is more than 4 orders + * of magnitude lower than this best practical case scenario, giving us adequate protection against all but the + * luckiest attacker (spoiler alert: the mathematics of probabilities say you're not gonna get too lucky). + * + * It is still advisable to remove the update.php file once you are done with the extraction. This check + * here is only meant as a failsafe in case of a server error during the extraction and subsequent lack of user + * action to remove the update.php file from their server. + */ + clearstatcache(true); + $setupFileCreationTime = filectime($setupFile); + + if (abs(time() - $setupFileCreationTime) > 5400) { + return null; + } + + // Load update.php. It pulls a variable named $restoration_setup into the local scope. + clearFileInOPCache($setupFile); + + require_once $setupFile; + + /** @var array $extractionSetup */ + + // The file exists but no configuration is present? + if (empty($extractionSetup ?? null) || !is_array($extractionSetup)) { + return null; + } + + /** + * Immediately reject any attempt to run extract.php without a password. + * + * Doing that is a GRAVE SECURITY RISK. It makes it trivial to hack a site. Therefore we are preventing this script + * to run without a password. + */ + $password = $extractionSetup['security.password'] ?? null; + $userPassword = $_REQUEST['password'] ?? ''; + $userPassword = !is_string($userPassword) ? '' : trim($userPassword); + + if (empty($password) || !is_string($password) || (trim($password) == '') || (strlen(trim($password)) < 32)) { + return null; + } + + // Timing-safe password comparison. See http://blog.ircmaxell.com/2014/11/its-all-about-time.html + if (!timingSafeEquals($password, $userPassword)) { + return null; + } + + // An "instance" variable will resume the engine from the serialised instance + $serialized = $_REQUEST['instance'] ?? null; + + if (!is_null($serialized) && empty(ZIPExtraction::unserialiseInstance($serialized))) { + // The serialised instance is corrupt or someone tries to trick us. YOU SHALL NOT PASS! + return null; + } + + return $extractionSetup; } // Import configuration $retArray = [ - 'status' => true, - 'message' => null, + 'status' => true, + 'message' => null, ]; $configuration = getConfiguration(); @@ -1950,12 +1841,11 @@ function getConfiguration(): ?array */ function setLongTimeout() { - if (!function_exists('ini_set')) - { - return; - } + if (!function_exists('ini_set')) { + return; + } - ini_set('max_execution_time', 3600); + ini_set('max_execution_time', 3600); } /** @@ -1966,128 +1856,114 @@ function setLongTimeout() */ function setHugeMemoryLimit() { - if (!function_exists('ini_set')) - { - return; - } + if (!function_exists('ini_set')) { + return; + } - ini_set('memory_limit', 1073741824); + ini_set('memory_limit', 1073741824); } -if ($enabled) -{ - // Try to set a very large memory and timeout limit - setLongTimeout(); - setHugeMemoryLimit(); - - $sourcePath = $configuration['setup.sourcepath'] ?? ''; - $sourceFile = $configuration['setup.sourcefile'] ?? ''; - $destDir = ($configuration['setup.destdir'] ?? null) ?: __DIR__; - $basePath = rtrim(str_replace('\\', '/', __DIR__), '/'); - $basePath = empty($basePath) ? $basePath : ($basePath . '/'); - $sourceFile = (empty($sourcePath) ? '' : (rtrim($sourcePath, '/\\') . '/')) . $sourceFile; - $engine = ZIPExtraction::getInstance(); - - $engine->setFilename($sourceFile); - $engine->setAddPath($destDir); - $skipFiles = [ - 'administrator/components/com_joomlaupdate/restoration.php', - 'administrator/components/com_joomlaupdate/update.php', - ]; - - if (defined('_JOOMLA_UPDATE_DEBUG')) - { - $skipFiles[] = 'administrator/components/com_joomlaupdate/extract.php'; - } - - $engine->setSkipFiles($skipFiles - ); - $engine->setIgnoreDirectories([ - 'tmp', 'administrator/logs', - ] - ); - - $task = $_REQUEST['task'] ?? null; - - switch ($task) - { - case 'startExtract': - case 'stepExtract': - $done = $engine->step(); - $error = $engine->getError(); - - if ($error != '') - { - $retArray['status'] = false; - $retArray['done'] = true; - $retArray['message'] = $error; - } - elseif ($done) - { - $retArray['files'] = $engine->filesProcessed; - $retArray['bytesIn'] = $engine->compressedTotal; - $retArray['bytesOut'] = $engine->uncompressedTotal; - $retArray['percent'] = 100; - $retArray['status'] = true; - $retArray['done'] = true; - - $retArray['percent'] = min($retArray['percent'], 100); - } - else - { - $retArray['files'] = $engine->filesProcessed; - $retArray['bytesIn'] = $engine->compressedTotal; - $retArray['bytesOut'] = $engine->uncompressedTotal; - $retArray['percent'] = ($engine->totalSize > 0) ? (100 * $engine->compressedTotal / $engine->totalSize) : 0; - $retArray['status'] = true; - $retArray['done'] = false; - $retArray['instance'] = ZIPExtraction::getSerialised(); - } - - $engine->enforceMinimumExecutionTime(); - - break; - - case 'finalizeUpdate': - $root = $configuration['setup.destdir'] ?? ''; - - // Remove update.php - clearFileInOPCache($basePath . 'update.php'); - @unlink($basePath . 'update.php'); - - // Import a custom finalisation file - $filename = dirname(__FILE__) . '/finalisation.php'; - - if (file_exists($filename)) - { - clearFileInOPCache($filename); - - include_once $filename; - } - - // Run a custom finalisation script - if (function_exists('finalizeUpdate')) - { - finalizeUpdate($root, $basePath); - } - - $engine->enforceMinimumExecutionTime(); - - break; - - default: - // Invalid task! - $enabled = false; - break; - } +if ($enabled) { + // Try to set a very large memory and timeout limit + setLongTimeout(); + setHugeMemoryLimit(); + + $sourcePath = $configuration['setup.sourcepath'] ?? ''; + $sourceFile = $configuration['setup.sourcefile'] ?? ''; + $destDir = ($configuration['setup.destdir'] ?? null) ?: __DIR__; + $basePath = rtrim(str_replace('\\', '/', __DIR__), '/'); + $basePath = empty($basePath) ? $basePath : ($basePath . '/'); + $sourceFile = (empty($sourcePath) ? '' : (rtrim($sourcePath, '/\\') . '/')) . $sourceFile; + $engine = ZIPExtraction::getInstance(); + + $engine->setFilename($sourceFile); + $engine->setAddPath($destDir); + $skipFiles = [ + 'administrator/components/com_joomlaupdate/restoration.php', + 'administrator/components/com_joomlaupdate/update.php', + ]; + + if (defined('_JOOMLA_UPDATE_DEBUG')) { + $skipFiles[] = 'administrator/components/com_joomlaupdate/extract.php'; + } + + $engine->setSkipFiles($skipFiles); + $engine->setIgnoreDirectories([ + 'tmp', 'administrator/logs', + ]); + + $task = $_REQUEST['task'] ?? null; + + switch ($task) { + case 'startExtract': + case 'stepExtract': + $done = $engine->step(); + $error = $engine->getError(); + + if ($error != '') { + $retArray['status'] = false; + $retArray['done'] = true; + $retArray['message'] = $error; + } elseif ($done) { + $retArray['files'] = $engine->filesProcessed; + $retArray['bytesIn'] = $engine->compressedTotal; + $retArray['bytesOut'] = $engine->uncompressedTotal; + $retArray['percent'] = 100; + $retArray['status'] = true; + $retArray['done'] = true; + + $retArray['percent'] = min($retArray['percent'], 100); + } else { + $retArray['files'] = $engine->filesProcessed; + $retArray['bytesIn'] = $engine->compressedTotal; + $retArray['bytesOut'] = $engine->uncompressedTotal; + $retArray['percent'] = ($engine->totalSize > 0) ? (100 * $engine->compressedTotal / $engine->totalSize) : 0; + $retArray['status'] = true; + $retArray['done'] = false; + $retArray['instance'] = ZIPExtraction::getSerialised(); + } + + $engine->enforceMinimumExecutionTime(); + + break; + + case 'finalizeUpdate': + $root = $configuration['setup.destdir'] ?? ''; + + // Remove update.php + clearFileInOPCache($basePath . 'update.php'); + @unlink($basePath . 'update.php'); + + // Import a custom finalisation file + $filename = dirname(__FILE__) . '/finalisation.php'; + + if (file_exists($filename)) { + clearFileInOPCache($filename); + + include_once $filename; + } + + // Run a custom finalisation script + if (function_exists('finalizeUpdate')) { + finalizeUpdate($root, $basePath); + } + + $engine->enforceMinimumExecutionTime(); + + break; + + default: + // Invalid task! + $enabled = false; + break; + } } // This could happen even if $enabled was true, e.g. if we were asked for an invalid task. -if (!$enabled) -{ - // Maybe we weren't authorized or the task was invalid? - $retArray['status'] = false; - $retArray['message'] = 'Invalid login'; +if (!$enabled) { + // Maybe we weren't authorized or the task was invalid? + $retArray['status'] = false; + $retArray['message'] = 'Invalid login'; } // JSON encode the message diff --git a/administrator/components/com_joomlaupdate/finalisation.php b/administrator/components/com_joomlaupdate/finalisation.php index 8319203fca08e..697eacff3c585 100644 --- a/administrator/components/com_joomlaupdate/finalisation.php +++ b/administrator/components/com_joomlaupdate/finalisation.php @@ -1,4 +1,5 @@ deleteUnexistingFiles(); - } + // Remove obsolete files - prevents errors occurring in some system plugins + if (class_exists('JoomlaInstallerScript')) { + (new JoomlaInstallerScript())->deleteUnexistingFiles(); + } - /** - * Remove autoload_psr4.php so that namespace map is re-generated on the next request. This is needed - * when there are new classes added to extensions on new Joomla! release. - */ - $namespaceMapFile = JPATH_ROOT . '/administrator/cache/autoload_psr4.php'; + /** + * Remove autoload_psr4.php so that namespace map is re-generated on the next request. This is needed + * when there are new classes added to extensions on new Joomla! release. + */ + $namespaceMapFile = JPATH_ROOT . '/administrator/cache/autoload_psr4.php'; - if (\Joomla\CMS\Filesystem\File::exists($namespaceMapFile)) - { - \Joomla\CMS\Filesystem\File::delete($namespaceMapFile); - } - } - } + if (\Joomla\CMS\Filesystem\File::exists($namespaceMapFile)) { + \Joomla\CMS\Filesystem\File::delete($namespaceMapFile); + } + } + } } namespace Joomla\CMS\Filesystem { - // Fake the File class - if (!class_exists('\Joomla\CMS\Filesystem\File')) - { - /** - * File mock class - * - * @since 3.5.1 - */ - abstract class File - { - /** - * Proxies checking a file exists to the native php version - * - * @param string $fileName The path to the file to be checked - * - * @return boolean - * - * @since 3.5.1 - */ - public static function exists(string $fileName): bool - { - return @file_exists($fileName); - } - - /** - * Delete a file and invalidate the PHP OPcache - * - * @param string $fileName The path to the file to be deleted - * - * @return boolean - * - * @since 3.5.1 - */ - public static function delete(string $fileName): bool - { - self::invalidateFileCache($fileName); + // Fake the File class + if (!class_exists('\Joomla\CMS\Filesystem\File')) { + /** + * File mock class + * + * @since 3.5.1 + */ + abstract class File + { + /** + * Proxies checking a file exists to the native php version + * + * @param string $fileName The path to the file to be checked + * + * @return boolean + * + * @since 3.5.1 + */ + public static function exists(string $fileName): bool + { + return @file_exists($fileName); + } - return @unlink($fileName); - } + /** + * Delete a file and invalidate the PHP OPcache + * + * @param string $fileName The path to the file to be deleted + * + * @return boolean + * + * @since 3.5.1 + */ + public static function delete(string $fileName): bool + { + self::invalidateFileCache($fileName); - /** - * Rename a file and invalidate the PHP OPcache - * - * @param string $src The path to the source file - * @param string $dest The path to the destination file - * - * @return boolean True on success - * - * @since 4.0.1 - */ - public static function move(string $src, string $dest): bool - { - self::invalidateFileCache($src); + return @unlink($fileName); + } - $result = @rename($src, $dest); + /** + * Rename a file and invalidate the PHP OPcache + * + * @param string $src The path to the source file + * @param string $dest The path to the destination file + * + * @return boolean True on success + * + * @since 4.0.1 + */ + public static function move(string $src, string $dest): bool + { + self::invalidateFileCache($src); - if ($result) - { - self::invalidateFileCache($dest); - } + $result = @rename($src, $dest); - return $result; - } + if ($result) { + self::invalidateFileCache($dest); + } - /** - * Invalidate opcache for a newly written/deleted file immediately, if opcache* functions exist and if this was a PHP file. - * - * @param string $filepath The path to the file just written to, to flush from opcache - * @param boolean $force If set to true, the script will be invalidated regardless of whether invalidation is necessary - * - * @return boolean TRUE if the opcode cache for script was invalidated/nothing to invalidate, - * or FALSE if the opcode cache is disabled or other conditions returning - * FALSE from opcache_invalidate (like file not found). - * - * @since 4.0.2 - */ - public static function invalidateFileCache($filepath, $force = true) - { - return \clearFileInOPCache($filepath); - } + return $result; + } - } - } + /** + * Invalidate opcache for a newly written/deleted file immediately, if opcache* functions exist and if this was a PHP file. + * + * @param string $filepath The path to the file just written to, to flush from opcache + * @param boolean $force If set to true, the script will be invalidated regardless of whether invalidation is necessary + * + * @return boolean TRUE if the opcode cache for script was invalidated/nothing to invalidate, + * or FALSE if the opcode cache is disabled or other conditions returning + * FALSE from opcache_invalidate (like file not found). + * + * @since 4.0.2 + */ + public static function invalidateFileCache($filepath, $force = true) + { + return \clearFileInOPCache($filepath); + } + } + } - // Fake the Folder class, mapping it to Restore's post-processing class - if (!class_exists('\Joomla\CMS\Filesystem\Folder')) - { - /** - * Folder mock class - * - * @since 3.5.1 - */ - abstract class Folder - { - /** - * Proxies checking a folder exists to the native php version - * - * @param string $folderName The path to the folder to be checked - * - * @return boolean - * - * @since 3.5.1 - */ - public static function exists(string $folderName): bool - { - return @is_dir($folderName); - } + // Fake the Folder class, mapping it to Restore's post-processing class + if (!class_exists('\Joomla\CMS\Filesystem\Folder')) { + /** + * Folder mock class + * + * @since 3.5.1 + */ + abstract class Folder + { + /** + * Proxies checking a folder exists to the native php version + * + * @param string $folderName The path to the folder to be checked + * + * @return boolean + * + * @since 3.5.1 + */ + public static function exists(string $folderName): bool + { + return @is_dir($folderName); + } - /** - * Delete a folder recursively and invalidate the PHP OPcache - * - * @param string $folderName The path to the folder to be deleted - * - * @return boolean - * - * @since 3.5.1 - */ - public static function delete(string $folderName): bool - { - if (substr($folderName, -1) == '/') - { - $folderName = substr($folderName, 0, -1); - } + /** + * Delete a folder recursively and invalidate the PHP OPcache + * + * @param string $folderName The path to the folder to be deleted + * + * @return boolean + * + * @since 3.5.1 + */ + public static function delete(string $folderName): bool + { + if (substr($folderName, -1) == '/') { + $folderName = substr($folderName, 0, -1); + } - if (!@file_exists($folderName) || !@is_dir($folderName) || !is_readable($folderName)) - { - return false; - } + if (!@file_exists($folderName) || !@is_dir($folderName) || !is_readable($folderName)) { + return false; + } - $di = new \DirectoryIterator($folderName); + $di = new \DirectoryIterator($folderName); - /** @var \DirectoryIterator $item */ - foreach ($di as $item) - { - if ($item->isDot()) - { - continue; - } + /** @var \DirectoryIterator $item */ + foreach ($di as $item) { + if ($item->isDot()) { + continue; + } - if ($item->isDir()) - { - $status = self::delete($item->getPathname()); + if ($item->isDir()) { + $status = self::delete($item->getPathname()); - if (!$status) - { - return false; - } + if (!$status) { + return false; + } - continue; - } + continue; + } - \clearFileInOPCache($item->getPathname()); + \clearFileInOPCache($item->getPathname()); - @unlink($item->getPathname()); - } + @unlink($item->getPathname()); + } - return @rmdir($folderName); - } - } - } + return @rmdir($folderName); + } + } + } } namespace Joomla\CMS\Language { - // Fake the Text class - we aren't going to show errors to people anyhow - if (!class_exists('\Joomla\CMS\Language\Text')) - { - /** - * Text mock class - * - * @since 3.5.1 - */ - abstract class Text - { - /** - * No need for translations in a non-interactive script, so always return an empty string here - * - * @param string $text A language constant - * - * @return string - * - * @since 3.5.1 - */ - public static function sprintf(string $text): string - { - return ''; - } - } - } + // Fake the Text class - we aren't going to show errors to people anyhow + if (!class_exists('\Joomla\CMS\Language\Text')) { + /** + * Text mock class + * + * @since 3.5.1 + */ + abstract class Text + { + /** + * No need for translations in a non-interactive script, so always return an empty string here + * + * @param string $text A language constant + * + * @return string + * + * @since 3.5.1 + */ + public static function sprintf(string $text): string + { + return ''; + } + } + } } diff --git a/administrator/components/com_joomlaupdate/restore_finalisation.php b/administrator/components/com_joomlaupdate/restore_finalisation.php index f2cdb41545e29..ffdaf06fed873 100644 --- a/administrator/components/com_joomlaupdate/restore_finalisation.php +++ b/administrator/components/com_joomlaupdate/restore_finalisation.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Joomlaupdate')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Joomlaupdate')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Joomlaupdate')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Joomlaupdate')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_joomlaupdate/src/Controller/DisplayController.php b/administrator/components/com_joomlaupdate/src/Controller/DisplayController.php index 75534fc13bc21..19a9848956fc2 100644 --- a/administrator/components/com_joomlaupdate/src/Controller/DisplayController.php +++ b/administrator/components/com_joomlaupdate/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ app->getDocument(); - - // Set the default view name and format from the Request. - $vName = $this->input->get('view', 'Joomlaupdate'); - $vFormat = $document->getType(); - $lName = $this->input->get('layout', 'default', 'string'); - - // Get and render the view. - if ($view = $this->getView($vName, $vFormat)) - { - // Only super user can access file upload - if ($view == 'upload' && !$this->app->getIdentity()->authorise('core.admin', 'com_joomlaupdate')) - { - $this->app->redirect(Route::_('index.php?option=com_joomlaupdate', true)); - } - - // Get the model for the view. - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - /** @var ?\Joomla\Component\Installer\Administrator\Model\WarningsModel $warningsModel */ - $warningsModel = $this->app->bootComponent('com_installer') - ->getMVCFactory()->createModel('Warnings', 'Administrator', ['ignore_request' => true]); - - if ($warningsModel !== null) - { - $view->setModel($warningsModel, false); - } - - // Perform update source preference check and refresh update information. - $model->applyUpdateSite(); - $model->refreshUpdates(); - - // Push the model into the view (as default). - $view->setModel($model, true); - $view->setLayout($lName); - - // Push document object into the view. - $view->document = $document; - $view->display(); - } - - return $this; - } - - /** - * Provide the data for a badge in a menu item via JSON - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function getMenuBadgeData() - { - if (!$this->app->getIdentity()->authorise('core.manage', 'com_joomlaupdate')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } - - $model = $this->getModel('Update'); - - $model->refreshUpdates(); - - $joomlaUpdate = $model->getUpdateInformation(); - - $hasUpdate = $joomlaUpdate['hasUpdate'] ? $joomlaUpdate['latest'] : ''; - - echo new JsonResponse($hasUpdate); - } + /** + * Method to display a view. + * + * @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()}. + * + * @return static This object to support chaining. + * + * @since 2.5.4 + */ + public function display($cachable = false, $urlparams = false) + { + // Get the document object. + $document = $this->app->getDocument(); + + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'Joomlaupdate'); + $vFormat = $document->getType(); + $lName = $this->input->get('layout', 'default', 'string'); + + // Get and render the view. + if ($view = $this->getView($vName, $vFormat)) { + // Only super user can access file upload + if ($view == 'upload' && !$this->app->getIdentity()->authorise('core.admin', 'com_joomlaupdate')) { + $this->app->redirect(Route::_('index.php?option=com_joomlaupdate', true)); + } + + // Get the model for the view. + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + /** @var ?\Joomla\Component\Installer\Administrator\Model\WarningsModel $warningsModel */ + $warningsModel = $this->app->bootComponent('com_installer') + ->getMVCFactory()->createModel('Warnings', 'Administrator', ['ignore_request' => true]); + + if ($warningsModel !== null) { + $view->setModel($warningsModel, false); + } + + // Perform update source preference check and refresh update information. + $model->applyUpdateSite(); + $model->refreshUpdates(); + + // Push the model into the view (as default). + $view->setModel($model, true); + $view->setLayout($lName); + + // Push document object into the view. + $view->document = $document; + $view->display(); + } + + return $this; + } + + /** + * Provide the data for a badge in a menu item via JSON + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function getMenuBadgeData() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_joomlaupdate')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } + + $model = $this->getModel('Update'); + + $model->refreshUpdates(); + + $joomlaUpdate = $model->getUpdateInformation(); + + $hasUpdate = $joomlaUpdate['hasUpdate'] ? $joomlaUpdate['latest'] : ''; + + echo new JsonResponse($hasUpdate); + } } diff --git a/administrator/components/com_joomlaupdate/src/Controller/UpdateController.php b/administrator/components/com_joomlaupdate/src/Controller/UpdateController.php index 5aa701148261d..744166f7da9aa 100644 --- a/administrator/components/com_joomlaupdate/src/Controller/UpdateController.php +++ b/administrator/components/com_joomlaupdate/src/Controller/UpdateController.php @@ -1,4 +1,5 @@ checkToken(); - - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'joomla_update.php'; - Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); - $user = $this->app->getIdentity(); - - try - { - Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_START', $user->id, $user->name, \JVERSION), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - $result = $model->download(); - $file = $result['basename']; - - $message = null; - $messageType = null; - - // The validation was not successful so abort. - if ($result['check'] === false) - { - $message = Text::_('COM_JOOMLAUPDATE_VIEW_UPDATE_CHECKSUM_WRONG'); - $messageType = 'error'; - $url = 'index.php?option=com_joomlaupdate'; - - $this->app->setUserState('com_joomlaupdate.file', null); - $this->setRedirect($url, $message, $messageType); - - try - { - Log::add($message, Log::ERROR, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - return; - } - - if ($file) - { - $this->app->setUserState('com_joomlaupdate.file', $file); - $url = 'index.php?option=com_joomlaupdate&task=update.install&' . $this->app->getSession()->getFormToken() . '=1'; - - try - { - Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_FILE', $file), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - } - else - { - $this->app->setUserState('com_joomlaupdate.file', null); - $url = 'index.php?option=com_joomlaupdate'; - $message = Text::_('COM_JOOMLAUPDATE_VIEW_UPDATE_DOWNLOADFAILED'); - $messageType = 'error'; - } - - $this->setRedirect($url, $message, $messageType); - } - - /** - * Start the installation of the new Joomla! version - * - * @return void - * - * @since 2.5.4 - */ - public function install() - { - $this->checkToken('get'); - $this->app->setUserState('com_joomlaupdate.oldversion', JVERSION); - - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'joomla_update.php'; - Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); - - try - { - Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_INSTALL'), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - $file = $this->app->getUserState('com_joomlaupdate.file', null); - $model->createRestorationFile($file); - - $this->display(); - } - - /** - * Finalise the upgrade by running the necessary scripts - * - * @return void - * - * @since 2.5.4 - */ - public function finalise() - { - /* - * Finalize with login page. Used for pre-token check versions - * to allow updates without problems but with a maximum of security. - */ - if (!Session::checkToken('get')) - { - $this->setRedirect('index.php?option=com_joomlaupdate&view=update&layout=finaliseconfirm'); - - return; - } - - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'joomla_update.php'; - Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); - - try - { - Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_FINALISE'), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - $model->finaliseUpgrade(); - - $url = 'index.php?option=com_joomlaupdate&task=update.cleanup&' . Session::getFormToken() . '=1'; - $this->setRedirect($url); - } - - /** - * Clean up after ourselves - * - * @return void - * - * @since 2.5.4 - */ - public function cleanup() - { - /* - * Cleanup with login page. Used for pre-token check versions to be able to update - * from =< 3.2.7 to allow updates without problems but with a maximum of security. - */ - if (!Session::checkToken('get')) - { - $this->setRedirect('index.php?option=com_joomlaupdate&view=update&layout=finaliseconfirm'); - - return; - } - - $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; - $options['text_file'] = 'joomla_update.php'; - Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); - - try - { - Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_CLEANUP'), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - $model->cleanUp(); - - $url = 'index.php?option=com_joomlaupdate&view=joomlaupdate&layout=complete'; - $this->setRedirect($url); - - try - { - Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_COMPLETE', \JVERSION), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - } - - /** - * Purges updates. - * - * @return void - * - * @since 3.0 - */ - public function purge() - { - // Check for request forgeries - $this->checkToken('request'); - - // Purge updates - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - $model->purge(); - - $url = 'index.php?option=com_joomlaupdate'; - $this->setRedirect($url, $model->_message); - } - - /** - * Uploads an update package to the temporary directory, under a random name - * - * @return void - * - * @since 3.6.0 - */ - public function upload() - { - // Check for request forgeries - $this->checkToken(); - - // Did a non Super User tried to upload something (a.k.a. pathetic hacking attempt)? - $this->app->getIdentity()->authorise('core.admin') or jexit(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')); - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - try - { - $model->upload(); - } - catch (\RuntimeException $e) - { - $url = 'index.php?option=com_joomlaupdate'; - $this->setRedirect($url, $e->getMessage(), 'error'); - - return; - } - - $token = Session::getFormToken(); - $url = 'index.php?option=com_joomlaupdate&task=update.captive&' . $token . '=1'; - $this->setRedirect($url); - } - - /** - * Checks there is a valid update package and redirects to the captive view for super admin authentication. - * - * @return void - * - * @since 3.6.0 - */ - public function captive() - { - // Check for request forgeries - $this->checkToken('get'); - - // Did a non Super User tried to upload something (a.k.a. pathetic hacking attempt)? - if (!$this->app->getIdentity()->authorise('core.admin')) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); - } - - // Do I really have an update package? - $tempFile = $this->app->getUserState('com_joomlaupdate.temp_file', null); - - if (empty($tempFile) || !File::exists($tempFile)) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); - } - - $this->input->set('view', 'upload'); - $this->input->set('layout', 'captive'); - - $this->display(); - } - - /** - * Checks the admin has super administrator privileges and then proceeds with the update. - * - * @return void - * - * @since 3.6.0 - */ - public function confirm() - { - // Check for request forgeries - $this->checkToken(); - - // Did a non Super User tried to upload something (a.k.a. pathetic hacking attempt)? - if (!$this->app->getIdentity()->authorise('core.admin')) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); - } - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - // Get the captive file before the session resets - $tempFile = $this->app->getUserState('com_joomlaupdate.temp_file', null); - - // Do I really have an update package? - if (!$model->captiveFileExists()) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); - } - - // Try to log in - $credentials = array( - 'username' => $this->input->post->get('username', '', 'username'), - 'password' => $this->input->post->get('passwd', '', 'raw'), - 'secretkey' => $this->input->post->get('secretkey', '', 'raw'), - ); - - $result = $model->captiveLogin($credentials); - - if (!$result) - { - $model->removePackageFiles(); - - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); - } - - // Set the update source in the session - $this->app->setUserState('com_joomlaupdate.file', basename($tempFile)); - - try - { - Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_FILE', $tempFile), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - // Redirect to the actual update page - $url = 'index.php?option=com_joomlaupdate&task=update.install&' . Session::getFormToken() . '=1'; - $this->setRedirect($url); - } - - /** - * Method to display a view. - * - * @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()}. - * - * @return static This object to support chaining. - * - * @since 2.5.4 - */ - public function display($cachable = false, $urlparams = array()) - { - // Get the document object. - $document = $this->app->getDocument(); - - // Set the default view name and format from the Request. - $vName = $this->input->get('view', 'update'); - $vFormat = $document->getType(); - $lName = $this->input->get('layout', 'default', 'string'); - - // Get and render the view. - if ($view = $this->getView($vName, $vFormat)) - { - // Get the model for the view. - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - // Push the model into the view (as default). - $view->setModel($model, true); - $view->setLayout($lName); - - // Push document object into the view. - $view->document = $document; - $view->display(); - } - - return $this; - } - - /** - * Checks the admin has super administrator privileges and then proceeds with the final & cleanup steps. - * - * @return void - * - * @since 3.6.3 - */ - public function finaliseconfirm() - { - // Check for request forgeries - $this->checkToken(); - - // Did a non Super User try do this? - if (!$this->app->getIdentity()->authorise('core.admin')) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); - } - - // Get the model - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - // Try to log in - $credentials = array( - 'username' => $this->input->post->get('username', '', 'username'), - 'password' => $this->input->post->get('passwd', '', 'raw'), - 'secretkey' => $this->input->post->get('secretkey', '', 'raw'), - ); - - $result = $model->captiveLogin($credentials); - - // The login fails? - if (!$result) - { - $this->setMessage(Text::_('JGLOBAL_AUTH_INVALID_PASS'), 'warning'); - $this->setRedirect('index.php?option=com_joomlaupdate&view=update&layout=finaliseconfirm'); - - return; - } - - // Redirect back to the actual finalise page - $this->setRedirect('index.php?option=com_joomlaupdate&task=update.finalise&' . Session::getFormToken() . '=1'); - } - - /** - * Fetch Extension update XML proxy. Used to prevent Access-Control-Allow-Origin errors. - * Prints a JSON string. - * Called from JS. - * - * @since 3.10.0 - * @deprecated 5.0 Use batchextensioncompatibility instead. - * - * @return void - */ - public function fetchExtensionCompatibility() - { - $extensionID = $this->input->get('extension-id', '', 'DEFAULT'); - $joomlaTargetVersion = $this->input->get('joomla-target-version', '', 'DEFAULT'); - $joomlaCurrentVersion = $this->input->get('joomla-current-version', '', JVERSION); - $extensionVersion = $this->input->get('extension-version', '', 'DEFAULT'); - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - $upgradeCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaTargetVersion); - $currentCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaCurrentVersion); - $upgradeUpdateVersion = false; - $currentUpdateVersion = false; - - $upgradeWarning = 0; - - if ($upgradeCompatibilityStatus->state == 1 && !empty($upgradeCompatibilityStatus->compatibleVersions)) - { - $upgradeUpdateVersion = end($upgradeCompatibilityStatus->compatibleVersions); - } - - if ($currentCompatibilityStatus->state == 1 && !empty($currentCompatibilityStatus->compatibleVersions)) - { - $currentUpdateVersion = end($currentCompatibilityStatus->compatibleVersions); - } - - if ($upgradeUpdateVersion !== false) - { - $upgradeOldestVersion = $upgradeCompatibilityStatus->compatibleVersions[0]; - - if ($currentUpdateVersion !== false) - { - // If there are updates compatible with both CMS versions use these - $bothCompatibleVersions = array_values( - array_intersect($upgradeCompatibilityStatus->compatibleVersions, $currentCompatibilityStatus->compatibleVersions) - ); - - if (!empty($bothCompatibleVersions)) - { - $upgradeOldestVersion = $bothCompatibleVersions[0]; - $upgradeUpdateVersion = end($bothCompatibleVersions); - } - } - - if (version_compare($upgradeOldestVersion, $extensionVersion, '>')) - { - // Installed version is empty or older than the oldest compatible update: Update required - $resultGroup = 2; - } - else - { - // Current version is compatible - $resultGroup = 3; - } - - if ($currentUpdateVersion !== false && version_compare($upgradeUpdateVersion, $currentUpdateVersion, '<')) - { - // Special case warning when version compatible with target is lower than current - $upgradeWarning = 2; - } - } - elseif ($currentUpdateVersion !== false) - { - // No compatible version for target version but there is a compatible version for current version - $resultGroup = 1; - } - else - { - // No update server available - $resultGroup = 1; - } - - // Do we need to capture - $combinedCompatibilityStatus = array( - 'upgradeCompatibilityStatus' => (object) array( - 'state' => $upgradeCompatibilityStatus->state, - 'compatibleVersion' => $upgradeUpdateVersion - ), - 'currentCompatibilityStatus' => (object) array( - 'state' => $currentCompatibilityStatus->state, - 'compatibleVersion' => $currentUpdateVersion - ), - 'resultGroup' => $resultGroup, - 'upgradeWarning' => $upgradeWarning, - ); - - $this->app = Factory::getApplication(); - $this->app->mimeType = 'application/json'; - $this->app->charSet = 'utf-8'; - $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); - $this->app->sendHeaders(); - - try - { - echo new JsonResponse($combinedCompatibilityStatus); - } - catch (\Exception $e) - { - echo $e; - } - - $this->app->close(); - } - - /** - * Determines the compatibility information for a number of extensions. - * - * Called by the Joomla Update JavaScript (PreUpdateChecker.checkNextChunk). - * - * @return void - * @since 4.2.0 - * - */ - public function batchextensioncompatibility() - { - $joomlaTargetVersion = $this->input->post->get('joomla-target-version', '', 'DEFAULT'); - $joomlaCurrentVersion = $this->input->post->get('joomla-current-version', JVERSION); - $extensionInformation = $this->input->post->get('extensions', []); - - /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ - $model = $this->getModel('Update'); - - $extensionResults = []; - $leftover = []; - $startTime = microtime(true); - - foreach ($extensionInformation as $information) - { - // Only process an extension if we have spent less than 5 seconds already - $currentTime = microtime(true); - - if ($currentTime - $startTime > 5.0) - { - $leftover[] = $information; - - continue; - } - - // Get the extension information and fetch its compatibility information - $extensionID = $information['eid'] ?: ''; - $extensionVersion = $information['version'] ?: ''; - $upgradeCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaTargetVersion); - $currentCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaCurrentVersion); - $upgradeUpdateVersion = false; - $currentUpdateVersion = false; - $upgradeWarning = 0; - - if ($upgradeCompatibilityStatus->state == 1 && !empty($upgradeCompatibilityStatus->compatibleVersions)) - { - $upgradeUpdateVersion = end($upgradeCompatibilityStatus->compatibleVersions); - } - - if ($currentCompatibilityStatus->state == 1 && !empty($currentCompatibilityStatus->compatibleVersions)) - { - $currentUpdateVersion = end($currentCompatibilityStatus->compatibleVersions); - } - - if ($upgradeUpdateVersion !== false) - { - $upgradeOldestVersion = $upgradeCompatibilityStatus->compatibleVersions[0]; - - if ($currentUpdateVersion !== false) - { - // If there are updates compatible with both CMS versions use these - $bothCompatibleVersions = array_values( - array_intersect($upgradeCompatibilityStatus->compatibleVersions, $currentCompatibilityStatus->compatibleVersions) - ); - - if (!empty($bothCompatibleVersions)) - { - $upgradeOldestVersion = $bothCompatibleVersions[0]; - $upgradeUpdateVersion = end($bothCompatibleVersions); - } - } - - if (version_compare($upgradeOldestVersion, $extensionVersion, '>')) - { - // Installed version is empty or older than the oldest compatible update: Update required - $resultGroup = 2; - } - else - { - // Current version is compatible - $resultGroup = 3; - } - - if ($currentUpdateVersion !== false && version_compare($upgradeUpdateVersion, $currentUpdateVersion, '<')) - { - // Special case warning when version compatible with target is lower than current - $upgradeWarning = 2; - } - } - elseif ($currentUpdateVersion !== false) - { - // No compatible version for target version but there is a compatible version for current version - $resultGroup = 1; - } - else - { - // No update server available - $resultGroup = 1; - } - - // Do we need to capture - $extensionResults[] = [ - 'id' => $extensionID, - 'upgradeCompatibilityStatus' => (object) [ - 'state' => $upgradeCompatibilityStatus->state, - 'compatibleVersion' => $upgradeUpdateVersion - ], - 'currentCompatibilityStatus' => (object) [ - 'state' => $currentCompatibilityStatus->state, - 'compatibleVersion' => $currentUpdateVersion - ], - 'resultGroup' => $resultGroup, - 'upgradeWarning' => $upgradeWarning, - ]; - } - - $this->app->mimeType = 'application/json'; - $this->app->charSet = 'utf-8'; - $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); - $this->app->sendHeaders(); - - try - { - $return = [ - 'compatibility' => $extensionResults, - 'extensions' => $leftover, - ]; - - echo new JsonResponse($return); - } - catch (\Exception $e) - { - echo $e; - } - - $this->app->close(); - } - - /** - * Fetch and report updates in \JSON format, for AJAX requests - * - * @return void - * - * @since 3.10.10 - */ - public function ajax() - { - if (!Session::checkToken('get')) - { - $this->app->setHeader('status', 403, true); - $this->app->sendHeaders(); - echo Text::_('JINVALID_TOKEN_NOTICE'); - $this->app->close(); - } - - /** @var UpdateModel $model */ - $model = $this->getModel('Update'); - $updateInfo = $model->getUpdateInformation(); - - $update = []; - $update[] = ['version' => $updateInfo['latest']]; - - echo json_encode($update); - - $this->app->close(); - } + /** + * Performs the download of the update package + * + * @return void + * + * @since 2.5.4 + */ + public function download() + { + $this->checkToken(); + + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'joomla_update.php'; + Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); + $user = $this->app->getIdentity(); + + try { + Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_START', $user->id, $user->name, \JVERSION), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + $result = $model->download(); + $file = $result['basename']; + + $message = null; + $messageType = null; + + // The validation was not successful so abort. + if ($result['check'] === false) { + $message = Text::_('COM_JOOMLAUPDATE_VIEW_UPDATE_CHECKSUM_WRONG'); + $messageType = 'error'; + $url = 'index.php?option=com_joomlaupdate'; + + $this->app->setUserState('com_joomlaupdate.file', null); + $this->setRedirect($url, $message, $messageType); + + try { + Log::add($message, Log::ERROR, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + + return; + } + + if ($file) { + $this->app->setUserState('com_joomlaupdate.file', $file); + $url = 'index.php?option=com_joomlaupdate&task=update.install&' . $this->app->getSession()->getFormToken() . '=1'; + + try { + Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_FILE', $file), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + } else { + $this->app->setUserState('com_joomlaupdate.file', null); + $url = 'index.php?option=com_joomlaupdate'; + $message = Text::_('COM_JOOMLAUPDATE_VIEW_UPDATE_DOWNLOADFAILED'); + $messageType = 'error'; + } + + $this->setRedirect($url, $message, $messageType); + } + + /** + * Start the installation of the new Joomla! version + * + * @return void + * + * @since 2.5.4 + */ + public function install() + { + $this->checkToken('get'); + $this->app->setUserState('com_joomlaupdate.oldversion', JVERSION); + + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'joomla_update.php'; + Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); + + try { + Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_INSTALL'), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + $file = $this->app->getUserState('com_joomlaupdate.file', null); + $model->createRestorationFile($file); + + $this->display(); + } + + /** + * Finalise the upgrade by running the necessary scripts + * + * @return void + * + * @since 2.5.4 + */ + public function finalise() + { + /* + * Finalize with login page. Used for pre-token check versions + * to allow updates without problems but with a maximum of security. + */ + if (!Session::checkToken('get')) { + $this->setRedirect('index.php?option=com_joomlaupdate&view=update&layout=finaliseconfirm'); + + return; + } + + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'joomla_update.php'; + Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); + + try { + Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_FINALISE'), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + $model->finaliseUpgrade(); + + $url = 'index.php?option=com_joomlaupdate&task=update.cleanup&' . Session::getFormToken() . '=1'; + $this->setRedirect($url); + } + + /** + * Clean up after ourselves + * + * @return void + * + * @since 2.5.4 + */ + public function cleanup() + { + /* + * Cleanup with login page. Used for pre-token check versions to be able to update + * from =< 3.2.7 to allow updates without problems but with a maximum of security. + */ + if (!Session::checkToken('get')) { + $this->setRedirect('index.php?option=com_joomlaupdate&view=update&layout=finaliseconfirm'); + + return; + } + + $options['format'] = '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}'; + $options['text_file'] = 'joomla_update.php'; + Log::addLogger($options, Log::INFO, array('Update', 'databasequery', 'jerror')); + + try { + Log::add(Text::_('COM_JOOMLAUPDATE_UPDATE_LOG_CLEANUP'), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + $model->cleanUp(); + + $url = 'index.php?option=com_joomlaupdate&view=joomlaupdate&layout=complete'; + $this->setRedirect($url); + + try { + Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_COMPLETE', \JVERSION), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + } + + /** + * Purges updates. + * + * @return void + * + * @since 3.0 + */ + public function purge() + { + // Check for request forgeries + $this->checkToken('request'); + + // Purge updates + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + $model->purge(); + + $url = 'index.php?option=com_joomlaupdate'; + $this->setRedirect($url, $model->_message); + } + + /** + * Uploads an update package to the temporary directory, under a random name + * + * @return void + * + * @since 3.6.0 + */ + public function upload() + { + // Check for request forgeries + $this->checkToken(); + + // Did a non Super User tried to upload something (a.k.a. pathetic hacking attempt)? + $this->app->getIdentity()->authorise('core.admin') or jexit(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN')); + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + try { + $model->upload(); + } catch (\RuntimeException $e) { + $url = 'index.php?option=com_joomlaupdate'; + $this->setRedirect($url, $e->getMessage(), 'error'); + + return; + } + + $token = Session::getFormToken(); + $url = 'index.php?option=com_joomlaupdate&task=update.captive&' . $token . '=1'; + $this->setRedirect($url); + } + + /** + * Checks there is a valid update package and redirects to the captive view for super admin authentication. + * + * @return void + * + * @since 3.6.0 + */ + public function captive() + { + // Check for request forgeries + $this->checkToken('get'); + + // Did a non Super User tried to upload something (a.k.a. pathetic hacking attempt)? + if (!$this->app->getIdentity()->authorise('core.admin')) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + // Do I really have an update package? + $tempFile = $this->app->getUserState('com_joomlaupdate.temp_file', null); + + if (empty($tempFile) || !File::exists($tempFile)) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + $this->input->set('view', 'upload'); + $this->input->set('layout', 'captive'); + + $this->display(); + } + + /** + * Checks the admin has super administrator privileges and then proceeds with the update. + * + * @return void + * + * @since 3.6.0 + */ + public function confirm() + { + // Check for request forgeries + $this->checkToken(); + + // Did a non Super User tried to upload something (a.k.a. pathetic hacking attempt)? + if (!$this->app->getIdentity()->authorise('core.admin')) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + // Get the captive file before the session resets + $tempFile = $this->app->getUserState('com_joomlaupdate.temp_file', null); + + // Do I really have an update package? + if (!$model->captiveFileExists()) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + // Try to log in + $credentials = array( + 'username' => $this->input->post->get('username', '', 'username'), + 'password' => $this->input->post->get('passwd', '', 'raw'), + 'secretkey' => $this->input->post->get('secretkey', '', 'raw'), + ); + + $result = $model->captiveLogin($credentials); + + if (!$result) { + $model->removePackageFiles(); + + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + // Set the update source in the session + $this->app->setUserState('com_joomlaupdate.file', basename($tempFile)); + + try { + Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_FILE', $tempFile), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + + // Redirect to the actual update page + $url = 'index.php?option=com_joomlaupdate&task=update.install&' . Session::getFormToken() . '=1'; + $this->setRedirect($url); + } + + /** + * Method to display a view. + * + * @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()}. + * + * @return static This object to support chaining. + * + * @since 2.5.4 + */ + public function display($cachable = false, $urlparams = array()) + { + // Get the document object. + $document = $this->app->getDocument(); + + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'update'); + $vFormat = $document->getType(); + $lName = $this->input->get('layout', 'default', 'string'); + + // Get and render the view. + if ($view = $this->getView($vName, $vFormat)) { + // Get the model for the view. + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + // Push the model into the view (as default). + $view->setModel($model, true); + $view->setLayout($lName); + + // Push document object into the view. + $view->document = $document; + $view->display(); + } + + return $this; + } + + /** + * Checks the admin has super administrator privileges and then proceeds with the final & cleanup steps. + * + * @return void + * + * @since 3.6.3 + */ + public function finaliseconfirm() + { + // Check for request forgeries + $this->checkToken(); + + // Did a non Super User try do this? + if (!$this->app->getIdentity()->authorise('core.admin')) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + // Get the model + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + // Try to log in + $credentials = array( + 'username' => $this->input->post->get('username', '', 'username'), + 'password' => $this->input->post->get('passwd', '', 'raw'), + 'secretkey' => $this->input->post->get('secretkey', '', 'raw'), + ); + + $result = $model->captiveLogin($credentials); + + // The login fails? + if (!$result) { + $this->setMessage(Text::_('JGLOBAL_AUTH_INVALID_PASS'), 'warning'); + $this->setRedirect('index.php?option=com_joomlaupdate&view=update&layout=finaliseconfirm'); + + return; + } + + // Redirect back to the actual finalise page + $this->setRedirect('index.php?option=com_joomlaupdate&task=update.finalise&' . Session::getFormToken() . '=1'); + } + + /** + * Fetch Extension update XML proxy. Used to prevent Access-Control-Allow-Origin errors. + * Prints a JSON string. + * Called from JS. + * + * @since 3.10.0 + * @deprecated 5.0 Use batchextensioncompatibility instead. + * + * @return void + */ + public function fetchExtensionCompatibility() + { + $extensionID = $this->input->get('extension-id', '', 'DEFAULT'); + $joomlaTargetVersion = $this->input->get('joomla-target-version', '', 'DEFAULT'); + $joomlaCurrentVersion = $this->input->get('joomla-current-version', '', JVERSION); + $extensionVersion = $this->input->get('extension-version', '', 'DEFAULT'); + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + $upgradeCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaTargetVersion); + $currentCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaCurrentVersion); + $upgradeUpdateVersion = false; + $currentUpdateVersion = false; + + $upgradeWarning = 0; + + if ($upgradeCompatibilityStatus->state == 1 && !empty($upgradeCompatibilityStatus->compatibleVersions)) { + $upgradeUpdateVersion = end($upgradeCompatibilityStatus->compatibleVersions); + } + + if ($currentCompatibilityStatus->state == 1 && !empty($currentCompatibilityStatus->compatibleVersions)) { + $currentUpdateVersion = end($currentCompatibilityStatus->compatibleVersions); + } + + if ($upgradeUpdateVersion !== false) { + $upgradeOldestVersion = $upgradeCompatibilityStatus->compatibleVersions[0]; + + if ($currentUpdateVersion !== false) { + // If there are updates compatible with both CMS versions use these + $bothCompatibleVersions = array_values( + array_intersect($upgradeCompatibilityStatus->compatibleVersions, $currentCompatibilityStatus->compatibleVersions) + ); + + if (!empty($bothCompatibleVersions)) { + $upgradeOldestVersion = $bothCompatibleVersions[0]; + $upgradeUpdateVersion = end($bothCompatibleVersions); + } + } + + if (version_compare($upgradeOldestVersion, $extensionVersion, '>')) { + // Installed version is empty or older than the oldest compatible update: Update required + $resultGroup = 2; + } else { + // Current version is compatible + $resultGroup = 3; + } + + if ($currentUpdateVersion !== false && version_compare($upgradeUpdateVersion, $currentUpdateVersion, '<')) { + // Special case warning when version compatible with target is lower than current + $upgradeWarning = 2; + } + } elseif ($currentUpdateVersion !== false) { + // No compatible version for target version but there is a compatible version for current version + $resultGroup = 1; + } else { + // No update server available + $resultGroup = 1; + } + + // Do we need to capture + $combinedCompatibilityStatus = array( + 'upgradeCompatibilityStatus' => (object) array( + 'state' => $upgradeCompatibilityStatus->state, + 'compatibleVersion' => $upgradeUpdateVersion + ), + 'currentCompatibilityStatus' => (object) array( + 'state' => $currentCompatibilityStatus->state, + 'compatibleVersion' => $currentUpdateVersion + ), + 'resultGroup' => $resultGroup, + 'upgradeWarning' => $upgradeWarning, + ); + + $this->app = Factory::getApplication(); + $this->app->mimeType = 'application/json'; + $this->app->charSet = 'utf-8'; + $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); + $this->app->sendHeaders(); + + try { + echo new JsonResponse($combinedCompatibilityStatus); + } catch (\Exception $e) { + echo $e; + } + + $this->app->close(); + } + + /** + * Determines the compatibility information for a number of extensions. + * + * Called by the Joomla Update JavaScript (PreUpdateChecker.checkNextChunk). + * + * @return void + * @since 4.2.0 + * + */ + public function batchextensioncompatibility() + { + $joomlaTargetVersion = $this->input->post->get('joomla-target-version', '', 'DEFAULT'); + $joomlaCurrentVersion = $this->input->post->get('joomla-current-version', JVERSION); + $extensionInformation = $this->input->post->get('extensions', []); + + /** @var \Joomla\Component\Joomlaupdate\Administrator\Model\UpdateModel $model */ + $model = $this->getModel('Update'); + + $extensionResults = []; + $leftover = []; + $startTime = microtime(true); + + foreach ($extensionInformation as $information) { + // Only process an extension if we have spent less than 5 seconds already + $currentTime = microtime(true); + + if ($currentTime - $startTime > 5.0) { + $leftover[] = $information; + + continue; + } + + // Get the extension information and fetch its compatibility information + $extensionID = $information['eid'] ?: ''; + $extensionVersion = $information['version'] ?: ''; + $upgradeCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaTargetVersion); + $currentCompatibilityStatus = $model->fetchCompatibility($extensionID, $joomlaCurrentVersion); + $upgradeUpdateVersion = false; + $currentUpdateVersion = false; + $upgradeWarning = 0; + + if ($upgradeCompatibilityStatus->state == 1 && !empty($upgradeCompatibilityStatus->compatibleVersions)) { + $upgradeUpdateVersion = end($upgradeCompatibilityStatus->compatibleVersions); + } + + if ($currentCompatibilityStatus->state == 1 && !empty($currentCompatibilityStatus->compatibleVersions)) { + $currentUpdateVersion = end($currentCompatibilityStatus->compatibleVersions); + } + + if ($upgradeUpdateVersion !== false) { + $upgradeOldestVersion = $upgradeCompatibilityStatus->compatibleVersions[0]; + + if ($currentUpdateVersion !== false) { + // If there are updates compatible with both CMS versions use these + $bothCompatibleVersions = array_values( + array_intersect($upgradeCompatibilityStatus->compatibleVersions, $currentCompatibilityStatus->compatibleVersions) + ); + + if (!empty($bothCompatibleVersions)) { + $upgradeOldestVersion = $bothCompatibleVersions[0]; + $upgradeUpdateVersion = end($bothCompatibleVersions); + } + } + + if (version_compare($upgradeOldestVersion, $extensionVersion, '>')) { + // Installed version is empty or older than the oldest compatible update: Update required + $resultGroup = 2; + } else { + // Current version is compatible + $resultGroup = 3; + } + + if ($currentUpdateVersion !== false && version_compare($upgradeUpdateVersion, $currentUpdateVersion, '<')) { + // Special case warning when version compatible with target is lower than current + $upgradeWarning = 2; + } + } elseif ($currentUpdateVersion !== false) { + // No compatible version for target version but there is a compatible version for current version + $resultGroup = 1; + } else { + // No update server available + $resultGroup = 1; + } + + // Do we need to capture + $extensionResults[] = [ + 'id' => $extensionID, + 'upgradeCompatibilityStatus' => (object) [ + 'state' => $upgradeCompatibilityStatus->state, + 'compatibleVersion' => $upgradeUpdateVersion + ], + 'currentCompatibilityStatus' => (object) [ + 'state' => $currentCompatibilityStatus->state, + 'compatibleVersion' => $currentUpdateVersion + ], + 'resultGroup' => $resultGroup, + 'upgradeWarning' => $upgradeWarning, + ]; + } + + $this->app->mimeType = 'application/json'; + $this->app->charSet = 'utf-8'; + $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet); + $this->app->sendHeaders(); + + try { + $return = [ + 'compatibility' => $extensionResults, + 'extensions' => $leftover, + ]; + + echo new JsonResponse($return); + } catch (\Exception $e) { + echo $e; + } + + $this->app->close(); + } + + /** + * Fetch and report updates in \JSON format, for AJAX requests + * + * @return void + * + * @since 3.10.10 + */ + public function ajax() + { + if (!Session::checkToken('get')) { + $this->app->setHeader('status', 403, true); + $this->app->sendHeaders(); + echo Text::_('JINVALID_TOKEN_NOTICE'); + $this->app->close(); + } + + /** @var UpdateModel $model */ + $model = $this->getModel('Update'); + $updateInfo = $model->getUpdateInformation(); + + $update = []; + $update[] = ['version' => $updateInfo['latest']]; + + echo json_encode($update); + + $this->app->close(); + } } diff --git a/administrator/components/com_joomlaupdate/src/Dispatcher/Dispatcher.php b/administrator/components/com_joomlaupdate/src/Dispatcher/Dispatcher.php index 5857cd7c68861..9229d0a78254f 100644 --- a/administrator/components/com_joomlaupdate/src/Dispatcher/Dispatcher.php +++ b/administrator/components/com_joomlaupdate/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.admin')) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + /** + * Joomla Update is checked for global core.admin rights - not the usual core.manage for the component + * + * @return void + */ + 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.admin')) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php index a1a271503e761..84045ba3c8600 100644 --- a/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php +++ b/administrator/components/com_joomlaupdate/src/Model/UpdateModel.php @@ -1,4 +1,5 @@ get('updatesource', 'nochange')) - { - // "Minor & Patch Release for Current version AND Next Major Release". - case 'next': - $updateURL = 'https://update.joomla.org/core/sts/list_sts.xml'; - break; - - // "Testing" - case 'testing': - $updateURL = 'https://update.joomla.org/core/test/list_test.xml'; - break; - - // "Custom" - // @todo: check if the customurl is valid and not just "not empty". - case 'custom': - if (trim($params->get('customurl', '')) != '') - { - $updateURL = trim($params->get('customurl', '')); - } - else - { - Factory::getApplication()->enqueueMessage(Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_CUSTOM_ERROR'), 'error'); - - return; - } - break; - - /** - * "Minor & Patch Release for Current version (recommended and default)". - * The commented "case" below are for documenting where 'default' and legacy options falls - * case 'default': - * case 'lts': - * case 'sts': (It's shown as "Default" because that option does not exist any more) - * case 'nochange': - */ - default: - $updateURL = 'https://update.joomla.org/core/list.xml'; - } - - $id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('us') . '.*') - ->from($db->quoteName('#__update_sites_extensions', 'map')) - ->join( - 'INNER', - $db->quoteName('#__update_sites', 'us'), - $db->quoteName('us.update_site_id') . ' = ' . $db->quoteName('map.update_site_id') - ) - ->where($db->quoteName('map.extension_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $update_site = $db->loadObject(); - - if ($update_site->location != $updateURL) - { - // Modify the database record. - $update_site->last_check_timestamp = 0; - $update_site->location = $updateURL; - $db->updateObject('#__update_sites', $update_site, 'update_site_id'); - - // Remove cached updates. - $query->clear() - ->delete($db->quoteName('#__updates')) - ->where($db->quoteName('extension_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - } - } - - /** - * Makes sure that the Joomla! update cache is up-to-date. - * - * @param boolean $force Force reload, ignoring the cache timeout. - * - * @return void - * - * @since 2.5.4 - */ - public function refreshUpdates($force = false) - { - if ($force) - { - $cache_timeout = 0; - } - else - { - $update_params = ComponentHelper::getParams('com_installer'); - $cache_timeout = (int) $update_params->get('cachetimeout', 6); - $cache_timeout = 3600 * $cache_timeout; - } - - $updater = Updater::getInstance(); - $minimumStability = Updater::STABILITY_STABLE; - $comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate'); - - if (in_array($comJoomlaupdateParams->get('updatesource', 'nochange'), array('testing', 'custom'))) - { - $minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE); - } - - $reflection = new \ReflectionObject($updater); - $reflectionMethod = $reflection->getMethod('findUpdates'); - $methodParameters = $reflectionMethod->getParameters(); - - if (count($methodParameters) >= 4) - { - // Reinstall support is available in Updater - $updater->findUpdates(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id, $cache_timeout, $minimumStability, true); - } - else - { - $updater->findUpdates(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id, $cache_timeout, $minimumStability); - } - } - - /** - * Makes sure that the Joomla! Update Component Update is in the database and check if there is a new version. - * - * @return boolean True if there is an update else false - * - * @since 4.0.0 - */ - public function getCheckForSelfUpdate() - { - $db = $this->getDatabase(); - - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('element') . ' = ' . $db->quote('com_joomlaupdate')); - $db->setQuery($query); - - try - { - // Get the component extension ID - $joomlaUpdateComponentId = $db->loadResult(); - } - catch (\RuntimeException $e) - { - // Something is wrong here! - $joomlaUpdateComponentId = 0; - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - // Try the update only if we have an extension id - if ($joomlaUpdateComponentId != 0) - { - // Always force to check for an update! - $cache_timeout = 0; - - $updater = Updater::getInstance(); - $updater->findUpdates($joomlaUpdateComponentId, $cache_timeout, Updater::STABILITY_STABLE); - - // Fetch the update information from the database. - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__updates')) - ->where($db->quoteName('extension_id') . ' = :id') - ->bind(':id', $joomlaUpdateComponentId, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $joomlaUpdateComponentObject = $db->loadObject(); - } - catch (\RuntimeException $e) - { - // Something is wrong here! - $joomlaUpdateComponentObject = null; - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - return !empty($joomlaUpdateComponentObject); - } - - return false; - } - - /** - * Returns an array with the Joomla! update information. - * - * @return array - * - * @since 2.5.4 - */ - public function getUpdateInformation() - { - if ($this->updateInformation) - { - return $this->updateInformation; - } - - // Initialise the return array. - $this->updateInformation = array( - 'installed' => \JVERSION, - 'latest' => null, - 'object' => null, - 'hasUpdate' => false, - 'current' => JVERSION // This is deprecated please use 'installed' or JVERSION directly - ); - - // Fetch the update information from the database. - $id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__updates')) - ->where($db->quoteName('extension_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $updateObject = $db->loadObject(); - - if (is_null($updateObject)) - { - // We have not found any update in the database - we seem to be running the latest version. - $this->updateInformation['latest'] = \JVERSION; - - return $this->updateInformation; - } - - // Check whether this is a valid update or not - if (version_compare($updateObject->version, JVERSION, '<')) - { - // This update points to an outdated version. We should not offer to update to this. - $this->updateInformation['latest'] = JVERSION; - - return $this->updateInformation; - } - - $minimumStability = Updater::STABILITY_STABLE; - $comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate'); - - if (in_array($comJoomlaupdateParams->get('updatesource', 'nochange'), array('testing', 'custom'))) - { - $minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE); - } - - // Fetch the full update details from the update details URL. - $update = new Update; - $update->loadFromXml($updateObject->detailsurl, $minimumStability); - - // Make sure we use the current information we got from the detailsurl - $this->updateInformation['object'] = $update; - $this->updateInformation['latest'] = $updateObject->version; - - // Check whether this is an update or not. - if (version_compare($this->updateInformation['latest'], JVERSION, '>')) - { - $this->updateInformation['hasUpdate'] = true; - } - - return $this->updateInformation; - } - - /** - * Removes all of the updates from the table and enable all update streams. - * - * @return boolean Result of operation. - * - * @since 3.0 - */ - public function purge() - { - $db = $this->getDatabase(); - - // Modify the database record - $update_site = new \stdClass; - $update_site->last_check_timestamp = 0; - $update_site->enabled = 1; - $update_site->update_site_id = 1; - $db->updateObject('#__update_sites', $update_site, 'update_site_id'); - - $query = $db->getQuery(true) - ->delete($db->quoteName('#__updates')) - ->where($db->quoteName('update_site_id') . ' = 1'); - $db->setQuery($query); - - if ($db->execute()) - { - $this->_message = Text::_('COM_JOOMLAUPDATE_CHECKED_UPDATES'); - - return true; - } - else - { - $this->_message = Text::_('COM_JOOMLAUPDATE_FAILED_TO_CHECK_UPDATES'); - - return false; - } - } - - /** - * Downloads the update package to the site. - * - * @return array - * - * @since 2.5.4 - */ - public function download() - { - $updateInfo = $this->getUpdateInformation(); - $packageURL = trim($updateInfo['object']->downloadurl->_data); - $sources = $updateInfo['object']->get('downloadSources', array()); - - // We have to manually follow the redirects here so we set the option to false. - $httpOptions = new Registry; - $httpOptions->set('follow_location', false); - - try - { - $head = HttpFactory::getHttp($httpOptions)->head($packageURL); - } - catch (\RuntimeException $e) - { - // Passing false here -> download failed message - $response['basename'] = false; - - return $response; - } - - // Follow the Location headers until the actual download URL is known - while (isset($head->headers['location'])) - { - $packageURL = (string) $head->headers['location'][0]; - - try - { - $head = HttpFactory::getHttp($httpOptions)->head($packageURL); - } - catch (\RuntimeException $e) - { - // Passing false here -> download failed message - $response['basename'] = false; - - return $response; - } - } - - // Remove protocol, path and query string from URL - $basename = basename($packageURL); - - if (strpos($basename, '?') !== false) - { - $basename = substr($basename, 0, strpos($basename, '?')); - } - - // Find the path to the temp directory and the local package. - $tempdir = (string) InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ) - ->clean(Factory::getApplication()->get('tmp_path'), 'path'); - $target = $tempdir . '/' . $basename; - $response = []; - - // Do we have a cached file? - $exists = File::exists($target); - - if (!$exists) - { - // Not there, let's fetch it. - $mirror = 0; - - while (!($download = $this->downloadPackage($packageURL, $target)) && isset($sources[$mirror])) - { - $name = $sources[$mirror]; - $packageURL = trim($name->url); - $mirror++; - } - - $response['basename'] = $download; - } - else - { - // Is it a 0-byte file? If so, re-download please. - $filesize = @filesize($target); - - if (empty($filesize)) - { - $mirror = 0; - - while (!($download = $this->downloadPackage($packageURL, $target)) && isset($sources[$mirror])) - { - $name = $sources[$mirror]; - $packageURL = trim($name->url); - $mirror++; - } - - $response['basename'] = $download; - } - - // Yes, it's there, skip downloading. - $response['basename'] = $basename; - } - - $response['check'] = $this->isChecksumValid($target, $updateInfo['object']); - - return $response; - } - - /** - * Return the result of the checksum of a package with the SHA256/SHA384/SHA512 tags in the update server manifest - * - * @param string $packagefile Location of the package to be installed - * @param Update $updateObject The Update Object - * - * @return boolean False in case the validation did not work; true in any other case. - * - * @note This method has been forked from (JInstallerHelper::isChecksumValid) so it - * does not depend on an up-to-date InstallerHelper at the update time - * - * @since 3.9.0 - */ - private function isChecksumValid($packagefile, $updateObject) - { - $hashes = array('sha256', 'sha384', 'sha512'); - - foreach ($hashes as $hash) - { - if ($updateObject->get($hash, false)) - { - $hashPackage = hash_file($hash, $packagefile); - $hashRemote = $updateObject->$hash->_data; - - if ($hashPackage !== $hashRemote) - { - // Return false in case the hash did not match - return false; - } - } - } - - // Well nothing was provided or all worked - return true; - } - - /** - * Downloads a package file to a specific directory - * - * @param string $url The URL to download from - * @param string $target The directory to store the file - * - * @return boolean True on success - * - * @since 2.5.4 - */ - protected function downloadPackage($url, $target) - { - try - { - Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_URL', $url), Log::INFO, 'Update'); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - // Make sure the target does not exist. - File::delete($target); - - // Download the package - try - { - $result = HttpFactory::getHttp([], ['curl', 'stream'])->get($url); - } - catch (\RuntimeException $e) - { - return false; - } - - if (!$result || ($result->code != 200 && $result->code != 310)) - { - return false; - } - - // Write the file to disk - File::write($target, $result->body); - - return basename($target); - } - - /** - * Backwards compatibility. Use createUpdateFile() instead. - * - * @param null $basename The basename of the file to create - * - * @return boolean - * @since 2.5.1 - * @deprecated 5.0 - */ - public function createRestorationFile($basename = null): bool - { - return $this->createUpdateFile($basename); - } - - /** - * Create the update.php file and trigger onJoomlaBeforeUpdate event. - * - * The onJoomlaBeforeUpdate event stores the core files for which overrides have been defined. - * This will be compared in the onJoomlaAfterUpdate event with the current filesystem state, - * thereby determining how many and which overrides need to be checked and possibly updated - * after Joomla installed an update. - * - * @param string $basename Optional base path to the file. - * - * @return boolean True if successful; false otherwise. - * - * @since 2.5.4 - */ - public function createUpdateFile($basename = null): bool - { - // Load overrides plugin. - PluginHelper::importPlugin('installer'); - - // Get a password - $password = UserHelper::genRandomPassword(32); - $app = Factory::getApplication(); - - // Trigger event before joomla update. - $app->triggerEvent('onJoomlaBeforeUpdate'); - - // Get the absolute path to site's root. - $siteroot = JPATH_SITE; - - // If the package name is not specified, get it from the update info. - if (empty($basename)) - { - $updateInfo = $this->getUpdateInformation(); - $packageURL = $updateInfo['object']->downloadurl->_data; - $basename = basename($packageURL); - } - - // Get the package name. - $config = $app->getConfig(); - $tempdir = $config->get('tmp_path'); - $file = $tempdir . '/' . $basename; - - $filesize = @filesize($file); - $app->setUserState('com_joomlaupdate.password', $password); - $app->setUserState('com_joomlaupdate.filesize', $filesize); - - $data = "get('updatesource', 'nochange')) { + // "Minor & Patch Release for Current version AND Next Major Release". + case 'next': + $updateURL = 'https://update.joomla.org/core/sts/list_sts.xml'; + break; + + // "Testing" + case 'testing': + $updateURL = 'https://update.joomla.org/core/test/list_test.xml'; + break; + + // "Custom" + // @todo: check if the customurl is valid and not just "not empty". + case 'custom': + if (trim($params->get('customurl', '')) != '') { + $updateURL = trim($params->get('customurl', '')); + } else { + Factory::getApplication()->enqueueMessage(Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_CUSTOM_ERROR'), 'error'); + + return; + } + break; + + /** + * "Minor & Patch Release for Current version (recommended and default)". + * The commented "case" below are for documenting where 'default' and legacy options falls + * case 'default': + * case 'lts': + * case 'sts': (It's shown as "Default" because that option does not exist any more) + * case 'nochange': + */ + default: + $updateURL = 'https://update.joomla.org/core/list.xml'; + } + + $id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('us') . '.*') + ->from($db->quoteName('#__update_sites_extensions', 'map')) + ->join( + 'INNER', + $db->quoteName('#__update_sites', 'us'), + $db->quoteName('us.update_site_id') . ' = ' . $db->quoteName('map.update_site_id') + ) + ->where($db->quoteName('map.extension_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $update_site = $db->loadObject(); + + if ($update_site->location != $updateURL) { + // Modify the database record. + $update_site->last_check_timestamp = 0; + $update_site->location = $updateURL; + $db->updateObject('#__update_sites', $update_site, 'update_site_id'); + + // Remove cached updates. + $query->clear() + ->delete($db->quoteName('#__updates')) + ->where($db->quoteName('extension_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + } + } + + /** + * Makes sure that the Joomla! update cache is up-to-date. + * + * @param boolean $force Force reload, ignoring the cache timeout. + * + * @return void + * + * @since 2.5.4 + */ + public function refreshUpdates($force = false) + { + if ($force) { + $cache_timeout = 0; + } else { + $update_params = ComponentHelper::getParams('com_installer'); + $cache_timeout = (int) $update_params->get('cachetimeout', 6); + $cache_timeout = 3600 * $cache_timeout; + } + + $updater = Updater::getInstance(); + $minimumStability = Updater::STABILITY_STABLE; + $comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate'); + + if (in_array($comJoomlaupdateParams->get('updatesource', 'nochange'), array('testing', 'custom'))) { + $minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE); + } + + $reflection = new \ReflectionObject($updater); + $reflectionMethod = $reflection->getMethod('findUpdates'); + $methodParameters = $reflectionMethod->getParameters(); + + if (count($methodParameters) >= 4) { + // Reinstall support is available in Updater + $updater->findUpdates(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id, $cache_timeout, $minimumStability, true); + } else { + $updater->findUpdates(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id, $cache_timeout, $minimumStability); + } + } + + /** + * Makes sure that the Joomla! Update Component Update is in the database and check if there is a new version. + * + * @return boolean True if there is an update else false + * + * @since 4.0.0 + */ + public function getCheckForSelfUpdate() + { + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' = ' . $db->quote('com_joomlaupdate')); + $db->setQuery($query); + + try { + // Get the component extension ID + $joomlaUpdateComponentId = $db->loadResult(); + } catch (\RuntimeException $e) { + // Something is wrong here! + $joomlaUpdateComponentId = 0; + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + // Try the update only if we have an extension id + if ($joomlaUpdateComponentId != 0) { + // Always force to check for an update! + $cache_timeout = 0; + + $updater = Updater::getInstance(); + $updater->findUpdates($joomlaUpdateComponentId, $cache_timeout, Updater::STABILITY_STABLE); + + // Fetch the update information from the database. + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__updates')) + ->where($db->quoteName('extension_id') . ' = :id') + ->bind(':id', $joomlaUpdateComponentId, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $joomlaUpdateComponentObject = $db->loadObject(); + } catch (\RuntimeException $e) { + // Something is wrong here! + $joomlaUpdateComponentObject = null; + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + return !empty($joomlaUpdateComponentObject); + } + + return false; + } + + /** + * Returns an array with the Joomla! update information. + * + * @return array + * + * @since 2.5.4 + */ + public function getUpdateInformation() + { + if ($this->updateInformation) { + return $this->updateInformation; + } + + // Initialise the return array. + $this->updateInformation = array( + 'installed' => \JVERSION, + 'latest' => null, + 'object' => null, + 'hasUpdate' => false, + 'current' => JVERSION // This is deprecated please use 'installed' or JVERSION directly + ); + + // Fetch the update information from the database. + $id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__updates')) + ->where($db->quoteName('extension_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $updateObject = $db->loadObject(); + + if (is_null($updateObject)) { + // We have not found any update in the database - we seem to be running the latest version. + $this->updateInformation['latest'] = \JVERSION; + + return $this->updateInformation; + } + + // Check whether this is a valid update or not + if (version_compare($updateObject->version, JVERSION, '<')) { + // This update points to an outdated version. We should not offer to update to this. + $this->updateInformation['latest'] = JVERSION; + + return $this->updateInformation; + } + + $minimumStability = Updater::STABILITY_STABLE; + $comJoomlaupdateParams = ComponentHelper::getParams('com_joomlaupdate'); + + if (in_array($comJoomlaupdateParams->get('updatesource', 'nochange'), array('testing', 'custom'))) { + $minimumStability = $comJoomlaupdateParams->get('minimum_stability', Updater::STABILITY_STABLE); + } + + // Fetch the full update details from the update details URL. + $update = new Update(); + $update->loadFromXml($updateObject->detailsurl, $minimumStability); + + // Make sure we use the current information we got from the detailsurl + $this->updateInformation['object'] = $update; + $this->updateInformation['latest'] = $updateObject->version; + + // Check whether this is an update or not. + if (version_compare($this->updateInformation['latest'], JVERSION, '>')) { + $this->updateInformation['hasUpdate'] = true; + } + + return $this->updateInformation; + } + + /** + * Removes all of the updates from the table and enable all update streams. + * + * @return boolean Result of operation. + * + * @since 3.0 + */ + public function purge() + { + $db = $this->getDatabase(); + + // Modify the database record + $update_site = new \stdClass(); + $update_site->last_check_timestamp = 0; + $update_site->enabled = 1; + $update_site->update_site_id = 1; + $db->updateObject('#__update_sites', $update_site, 'update_site_id'); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__updates')) + ->where($db->quoteName('update_site_id') . ' = 1'); + $db->setQuery($query); + + if ($db->execute()) { + $this->_message = Text::_('COM_JOOMLAUPDATE_CHECKED_UPDATES'); + + return true; + } else { + $this->_message = Text::_('COM_JOOMLAUPDATE_FAILED_TO_CHECK_UPDATES'); + + return false; + } + } + + /** + * Downloads the update package to the site. + * + * @return array + * + * @since 2.5.4 + */ + public function download() + { + $updateInfo = $this->getUpdateInformation(); + $packageURL = trim($updateInfo['object']->downloadurl->_data); + $sources = $updateInfo['object']->get('downloadSources', array()); + + // We have to manually follow the redirects here so we set the option to false. + $httpOptions = new Registry(); + $httpOptions->set('follow_location', false); + + try { + $head = HttpFactory::getHttp($httpOptions)->head($packageURL); + } catch (\RuntimeException $e) { + // Passing false here -> download failed message + $response['basename'] = false; + + return $response; + } + + // Follow the Location headers until the actual download URL is known + while (isset($head->headers['location'])) { + $packageURL = (string) $head->headers['location'][0]; + + try { + $head = HttpFactory::getHttp($httpOptions)->head($packageURL); + } catch (\RuntimeException $e) { + // Passing false here -> download failed message + $response['basename'] = false; + + return $response; + } + } + + // Remove protocol, path and query string from URL + $basename = basename($packageURL); + + if (strpos($basename, '?') !== false) { + $basename = substr($basename, 0, strpos($basename, '?')); + } + + // Find the path to the temp directory and the local package. + $tempdir = (string) InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ) + ->clean(Factory::getApplication()->get('tmp_path'), 'path'); + $target = $tempdir . '/' . $basename; + $response = []; + + // Do we have a cached file? + $exists = File::exists($target); + + if (!$exists) { + // Not there, let's fetch it. + $mirror = 0; + + while (!($download = $this->downloadPackage($packageURL, $target)) && isset($sources[$mirror])) { + $name = $sources[$mirror]; + $packageURL = trim($name->url); + $mirror++; + } + + $response['basename'] = $download; + } else { + // Is it a 0-byte file? If so, re-download please. + $filesize = @filesize($target); + + if (empty($filesize)) { + $mirror = 0; + + while (!($download = $this->downloadPackage($packageURL, $target)) && isset($sources[$mirror])) { + $name = $sources[$mirror]; + $packageURL = trim($name->url); + $mirror++; + } + + $response['basename'] = $download; + } + + // Yes, it's there, skip downloading. + $response['basename'] = $basename; + } + + $response['check'] = $this->isChecksumValid($target, $updateInfo['object']); + + return $response; + } + + /** + * Return the result of the checksum of a package with the SHA256/SHA384/SHA512 tags in the update server manifest + * + * @param string $packagefile Location of the package to be installed + * @param Update $updateObject The Update Object + * + * @return boolean False in case the validation did not work; true in any other case. + * + * @note This method has been forked from (JInstallerHelper::isChecksumValid) so it + * does not depend on an up-to-date InstallerHelper at the update time + * + * @since 3.9.0 + */ + private function isChecksumValid($packagefile, $updateObject) + { + $hashes = array('sha256', 'sha384', 'sha512'); + + foreach ($hashes as $hash) { + if ($updateObject->get($hash, false)) { + $hashPackage = hash_file($hash, $packagefile); + $hashRemote = $updateObject->$hash->_data; + + if ($hashPackage !== $hashRemote) { + // Return false in case the hash did not match + return false; + } + } + } + + // Well nothing was provided or all worked + return true; + } + + /** + * Downloads a package file to a specific directory + * + * @param string $url The URL to download from + * @param string $target The directory to store the file + * + * @return boolean True on success + * + * @since 2.5.4 + */ + protected function downloadPackage($url, $target) + { + try { + Log::add(Text::sprintf('COM_JOOMLAUPDATE_UPDATE_LOG_URL', $url), Log::INFO, 'Update'); + } catch (\RuntimeException $exception) { + // Informational log only + } + + // Make sure the target does not exist. + File::delete($target); + + // Download the package + try { + $result = HttpFactory::getHttp([], ['curl', 'stream'])->get($url); + } catch (\RuntimeException $e) { + return false; + } + + if (!$result || ($result->code != 200 && $result->code != 310)) { + return false; + } + + // Write the file to disk + File::write($target, $result->body); + + return basename($target); + } + + /** + * Backwards compatibility. Use createUpdateFile() instead. + * + * @param null $basename The basename of the file to create + * + * @return boolean + * @since 2.5.1 + * @deprecated 5.0 + */ + public function createRestorationFile($basename = null): bool + { + return $this->createUpdateFile($basename); + } + + /** + * Create the update.php file and trigger onJoomlaBeforeUpdate event. + * + * The onJoomlaBeforeUpdate event stores the core files for which overrides have been defined. + * This will be compared in the onJoomlaAfterUpdate event with the current filesystem state, + * thereby determining how many and which overrides need to be checked and possibly updated + * after Joomla installed an update. + * + * @param string $basename Optional base path to the file. + * + * @return boolean True if successful; false otherwise. + * + * @since 2.5.4 + */ + public function createUpdateFile($basename = null): bool + { + // Load overrides plugin. + PluginHelper::importPlugin('installer'); + + // Get a password + $password = UserHelper::genRandomPassword(32); + $app = Factory::getApplication(); + + // Trigger event before joomla update. + $app->triggerEvent('onJoomlaBeforeUpdate'); + + // Get the absolute path to site's root. + $siteroot = JPATH_SITE; + + // If the package name is not specified, get it from the update info. + if (empty($basename)) { + $updateInfo = $this->getUpdateInformation(); + $packageURL = $updateInfo['object']->downloadurl->_data; + $basename = basename($packageURL); + } + + // Get the package name. + $config = $app->getConfig(); + $tempdir = $config->get('tmp_path'); + $file = $tempdir . '/' . $basename; + + $filesize = @filesize($file); + $app->setUserState('com_joomlaupdate.password', $password); + $app->setUserState('com_joomlaupdate.filesize', $filesize); + + $data = " '$password', 'setup.sourcefile' => '$file', 'setup.destdir' => '$siteroot', ENDDATA; - $data .= '];'; - - // Remove the old file, if it's there... - $configpath = JPATH_COMPONENT_ADMINISTRATOR . '/update.php'; - - if (File::exists($configpath)) - { - if (!File::delete($configpath)) - { - File::invalidateFileCache($configpath); - @unlink($configpath); - } - } - - // Write new file. First try with File. - $result = File::write($configpath, $data); - - // In case File used FTP but direct access could help. - if (!$result) - { - if (function_exists('file_put_contents')) - { - $result = @file_put_contents($configpath, $data); - - if ($result !== false) - { - $result = true; - } - } - else - { - $fp = @fopen($configpath, 'wt'); - - if ($fp !== false) - { - $result = @fwrite($fp, $data); - - if ($result !== false) - { - $result = true; - } - - @fclose($fp); - } - } - } - - return $result; - } - - /** - * Finalise the upgrade. - * - * This method will do the following: - * * Run the schema update SQL files. - * * Run the Joomla post-update script. - * * Update the manifest cache and #__extensions entry for Joomla itself. - * - * It performs essentially the same function as InstallerFile::install() without the file copy. - * - * @return boolean True on success. - * - * @since 2.5.4 - */ - public function finaliseUpgrade() - { - $installer = Installer::getInstance(); - - $manifest = $installer->isManifest(JPATH_MANIFESTS . '/files/joomla.xml'); - - if ($manifest === false) - { - $installer->abort(Text::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST')); - - return false; - } - - $installer->manifest = $manifest; - - $installer->setUpgrade(true); - $installer->setOverwrite(true); - - $installer->extension = new \Joomla\CMS\Table\Extension($this->getDatabase()); - $installer->extension->load(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id); - - $installer->setAdapter($installer->extension->type); - - $installer->setPath('manifest', JPATH_MANIFESTS . '/files/joomla.xml'); - $installer->setPath('source', JPATH_MANIFESTS . '/files'); - $installer->setPath('extension_root', JPATH_ROOT); - - // Run the script file. - \JLoader::register('JoomlaInstallerScript', JPATH_ADMINISTRATOR . '/components/com_admin/script.php'); - - $manifestClass = new \JoomlaInstallerScript; - - ob_start(); - ob_implicit_flush(false); - - if ($manifestClass && method_exists($manifestClass, 'preflight')) - { - if ($manifestClass->preflight('update', $installer) === false) - { - $installer->abort( - Text::sprintf( - 'JLIB_INSTALLER_ABORT_INSTALL_CUSTOM_INSTALL_FAILURE', - Text::_('JLIB_INSTALLER_INSTALL') - ) - ); - - return false; - } - } - - // Create msg object; first use here. - $msg = ob_get_contents(); - ob_end_clean(); - - // Get a database connector object. - $db = $this->getDatabase(); - - /* - * Check to see if a file extension by the same name is already installed. - * If it is, then update the table because if the files aren't there - * we can assume that it was (badly) uninstalled. - * If it isn't, add an entry to extensions. - */ - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('file')) - ->where($db->quoteName('element') . ' = ' . $db->quote('joomla')); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - // Install failed, roll back changes. - $installer->abort( - Text::sprintf('JLIB_INSTALLER_ABORT_FILE_ROLLBACK', Text::_('JLIB_INSTALLER_UPDATE'), $e->getMessage()) - ); - - return false; - } - - $id = $db->loadResult(); - $row = new \Joomla\CMS\Table\Extension($this->getDatabase()); - - if ($id) - { - // Load the entry and update the manifest_cache. - $row->load($id); - - // Update name. - $row->set('name', 'files_joomla'); - - // Update manifest. - $row->manifest_cache = $installer->generateManifestCache(); - - if (!$row->store()) - { - // Install failed, roll back changes. - $installer->abort( - Text::sprintf('JLIB_INSTALLER_ABORT_FILE_ROLLBACK', Text::_('JLIB_INSTALLER_UPDATE'), $row->getError()) - ); - - return false; - } - } - else - { - // Add an entry to the extension table with a whole heap of defaults. - $row->set('name', 'files_joomla'); - $row->set('type', 'file'); - $row->set('element', 'joomla'); - - // There is no folder for files so leave it blank. - $row->set('folder', ''); - $row->set('enabled', 1); - $row->set('protected', 0); - $row->set('access', 0); - $row->set('client_id', 0); - $row->set('params', ''); - $row->set('manifest_cache', $installer->generateManifestCache()); - - if (!$row->store()) - { - // Install failed, roll back changes. - $installer->abort(Text::sprintf('JLIB_INSTALLER_ABORT_FILE_INSTALL_ROLLBACK', $row->getError())); - - return false; - } - - // Set the insert id. - $row->set('extension_id', $db->insertid()); - - // Since we have created a module item, we add it to the installation step stack - // so that if we have to rollback the changes we can undo it. - $installer->pushStep(array('type' => 'extension', 'extension_id' => $row->extension_id)); - } - - $result = $installer->parseSchemaUpdates($manifest->update->schemas, $row->extension_id); - - if ($result === false) - { - // Install failed, rollback changes (message already logged by the installer). - $installer->abort(); - - return false; - } - - // Reinitialise the installer's extensions table's properties. - $installer->extension->getFields(true); - - // Start Joomla! 1.6. - ob_start(); - ob_implicit_flush(false); - - if ($manifestClass && method_exists($manifestClass, 'update')) - { - if ($manifestClass->update($installer) === false) - { - // Install failed, rollback changes. - $installer->abort( - Text::sprintf( - 'JLIB_INSTALLER_ABORT_INSTALL_CUSTOM_INSTALL_FAILURE', - Text::_('JLIB_INSTALLER_INSTALL') - ) - ); - - return false; - } - } - - // Append messages. - $msg .= ob_get_contents(); - ob_end_clean(); - - // Clobber any possible pending updates. - $update = new \Joomla\CMS\Table\Update($this->getDatabase()); - $uid = $update->find( - array('element' => 'joomla', 'type' => 'file', 'client_id' => '0', 'folder' => '') - ); - - if ($uid) - { - $update->delete($uid); - } - - // And now we run the postflight. - ob_start(); - ob_implicit_flush(false); - - if ($manifestClass && method_exists($manifestClass, 'postflight')) - { - $manifestClass->postflight('update', $installer); - } - - // Append messages. - $msg .= ob_get_contents(); - ob_end_clean(); - - if ($msg != '') - { - $installer->set('extension_message', $msg); - } - - // Refresh versionable assets cache. - Factory::getApplication()->flushAssets(); - - return true; - } - - /** - * Removes the extracted package file and trigger onJoomlaAfterUpdate event. - * - * The onJoomlaAfterUpdate event compares the stored list of files previously overridden with - * the updated core files, finding out which files have changed during the update, thereby - * determining how many and which override files need to be checked and possibly updated after - * the Joomla update. - * - * @return void - * - * @since 2.5.4 - */ - public function cleanUp() - { - // Load overrides plugin. - PluginHelper::importPlugin('installer'); - - $app = Factory::getApplication(); - - // Trigger event after joomla update. - $app->triggerEvent('onJoomlaAfterUpdate'); - - // Remove the update package. - $tempdir = $app->get('tmp_path'); - - $file = $app->getUserState('com_joomlaupdate.file', null); - File::delete($tempdir . '/' . $file); - - // Remove the update.php file used in Joomla 4.0.3 and later. - if (File::exists(JPATH_COMPONENT_ADMINISTRATOR . '/update.php')) - { - File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/update.php'); - } - - // Remove the legacy restoration.php file (when updating from Joomla 4.0.2 and earlier). - if (File::exists(JPATH_COMPONENT_ADMINISTRATOR . '/restoration.php')) - { - File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/restoration.php'); - } - - // Remove the legacy restore_finalisation.php file used in Joomla 4.0.2 and earlier. - if (File::exists(JPATH_COMPONENT_ADMINISTRATOR . '/restore_finalisation.php')) - { - File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/restore_finalisation.php'); - } - - // Remove joomla.xml from the site's root. - if (File::exists(JPATH_ROOT . '/joomla.xml')) - { - File::delete(JPATH_ROOT . '/joomla.xml'); - } - - // Unset the update filename from the session. - $app = Factory::getApplication(); - $app->setUserState('com_joomlaupdate.file', null); - $oldVersion = $app->getUserState('com_joomlaupdate.oldversion'); - - // Trigger event after joomla update. - $app->triggerEvent('onJoomlaAfterUpdate', array($oldVersion)); - $app->setUserState('com_joomlaupdate.oldversion', null); - } - - /** - * Uploads what is presumably an update ZIP file under a mangled name in the temporary directory. - * - * @return void - * - * @since 3.6.0 - */ - public function upload() - { - // Get the uploaded file information. - $input = Factory::getApplication()->input; - - // Do not change the filter type 'raw'. We need this to let files containing PHP code to upload. See \JInputFiles::get. - $userfile = $input->files->get('install_package', null, 'raw'); - - // Make sure that file uploads are enabled in php. - if (!(bool) ini_get('file_uploads')) - { - throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLFILE'), 500); - } - - // Make sure that zlib is loaded so that the package can be unpacked. - if (!extension_loaded('zlib')) - { - throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLZLIB'), 500); - } - - // If there is no uploaded file, we have a problem... - if (!is_array($userfile)) - { - throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_NO_FILE_SELECTED'), 500); - } - - // Is the PHP tmp directory missing? - if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_NO_TMP_DIR)) - { - throw new \RuntimeException( - Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
' . - Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSET'), - 500 - ); - } - - // Is the max upload size too small in php.ini? - if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_INI_SIZE)) - { - throw new \RuntimeException( - Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
' . Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZE'), - 500 - ); - } - - // Check if there was a different problem uploading the file. - if ($userfile['error'] || $userfile['size'] < 1) - { - throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR'), 500); - } - - // Build the appropriate paths. - $tmp_dest = tempnam(Factory::getApplication()->get('tmp_path'), 'ju'); - $tmp_src = $userfile['tmp_name']; - - // Move uploaded file. - $result = File::upload($tmp_src, $tmp_dest, false, true); - - if (!$result) - { - throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR'), 500); - } - - Factory::getApplication()->setUserState('com_joomlaupdate.temp_file', $tmp_dest); - } - - /** - * Checks the super admin credentials are valid for the currently logged in users - * - * @param array $credentials The credentials to authenticate the user with - * - * @return boolean - * - * @since 3.6.0 - */ - public function captiveLogin($credentials) - { - // Make sure the username matches - $username = $credentials['username'] ?? null; - $user = Factory::getUser(); - - if (strtolower($user->username) != strtolower($username)) - { - return false; - } - - // Make sure the user is authorised - if (!$user->authorise('core.admin')) - { - return false; - } - - // Get the global Authentication object. - $authenticate = Authentication::getInstance(); - $response = $authenticate->authenticate($credentials); - - if ($response->status !== Authentication::STATUS_SUCCESS) - { - return false; - } - - return true; - } - - /** - * Does the captive (temporary) file we uploaded before still exist? - * - * @return boolean - * - * @since 3.6.0 - */ - public function captiveFileExists() - { - $file = Factory::getApplication()->getUserState('com_joomlaupdate.temp_file', null); - - if (empty($file) || !File::exists($file)) - { - return false; - } - - return true; - } - - /** - * Remove the captive (temporary) file we uploaded before and the . - * - * @return void - * - * @since 3.6.0 - */ - public function removePackageFiles() - { - $files = array( - Factory::getApplication()->getUserState('com_joomlaupdate.temp_file', null), - Factory::getApplication()->getUserState('com_joomlaupdate.file', null), - ); - - foreach ($files as $file) - { - if ($file !== null && File::exists($file)) - { - File::delete($file); - } - } - } - - /** - * Gets PHP options. - * @todo: Outsource, build common code base for pre install and pre update check - * - * @return array Array of PHP config options - * - * @since 3.10.0 - */ - public function getPhpOptions() - { - $options = array(); - - /* - * Check the PHP Version. It is already checked in Update. - * A Joomla! Update which is not supported by current PHP - * version is not shown. So this check is actually unnecessary. - */ - $option = new \stdClass; - $option->label = Text::sprintf('INSTL_PHP_VERSION_NEWER', $this->getTargetMinimumPHPVersion()); - $option->state = $this->isPhpVersionSupported(); - $option->notice = null; - $options[] = $option; - - // Check for zlib support. - $option = new \stdClass; - $option->label = Text::_('INSTL_ZLIB_COMPRESSION_SUPPORT'); - $option->state = extension_loaded('zlib'); - $option->notice = null; - $options[] = $option; - - // Check for XML support. - $option = new \stdClass; - $option->label = Text::_('INSTL_XML_SUPPORT'); - $option->state = extension_loaded('xml'); - $option->notice = null; - $options[] = $option; - - // Check for mbstring options. - if (extension_loaded('mbstring')) - { - // Check for default MB language. - $option = new \stdClass; - $option->label = Text::_('INSTL_MB_LANGUAGE_IS_DEFAULT'); - $option->state = strtolower(ini_get('mbstring.language')) === 'neutral'; - $option->notice = $option->state ? null : Text::_('INSTL_NOTICEMBLANGNOTDEFAULT'); - $options[] = $option; - - // Check for MB function overload. - $option = new \stdClass; - $option->label = Text::_('INSTL_MB_STRING_OVERLOAD_OFF'); - $option->state = ini_get('mbstring.func_overload') == 0; - $option->notice = $option->state ? null : Text::_('INSTL_NOTICEMBSTRINGOVERLOAD'); - $options[] = $option; - } - - // Check for a missing native parse_ini_file implementation. - $option = new \stdClass; - $option->label = Text::_('INSTL_PARSE_INI_FILE_AVAILABLE'); - $option->state = $this->getIniParserAvailability(); - $option->notice = null; - $options[] = $option; - - // Check for missing native json_encode / json_decode support. - $option = new \stdClass; - $option->label = Text::_('INSTL_JSON_SUPPORT_AVAILABLE'); - $option->state = function_exists('json_encode') && function_exists('json_decode'); - $option->notice = null; - $options[] = $option; - $updateInformation = $this->getUpdateInformation(); - - // Check if configured database is compatible with the next major version of Joomla - $nextMajorVersion = Version::MAJOR_VERSION + 1; - - if (version_compare($updateInformation['latest'], (string) $nextMajorVersion, '>=')) - { - $option = new \stdClass; - $option->label = Text::sprintf('INSTL_DATABASE_SUPPORTED', $this->getConfiguredDatabaseType()); - $option->state = $this->isDatabaseTypeSupported(); - $option->notice = null; - $options[] = $option; - } - - // Check if database structure is up to date - $option = new \stdClass; - $option->label = Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_DATABASE_STRUCTURE_TITLE'); - $option->state = $this->getDatabaseSchemaCheck(); - $option->notice = $option->state ? null : Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_DATABASE_STRUCTURE_NOTICE'); - $options[] = $option; - - return $options; - } - - /** - * Gets PHP Settings. - * @todo: Outsource, build common code base for pre install and pre update check - * - * @return array - * - * @since 3.10.0 - */ - public function getPhpSettings() - { - $settings = array(); - - // Check for display errors. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_DISPLAY_ERRORS'); - $setting->state = (bool) ini_get('display_errors'); - $setting->recommended = false; - $settings[] = $setting; - - // Check for file uploads. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_FILE_UPLOADS'); - $setting->state = (bool) ini_get('file_uploads'); - $setting->recommended = true; - $settings[] = $setting; - - // Check for output buffering. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_OUTPUT_BUFFERING'); - $setting->state = (int) ini_get('output_buffering') !== 0; - $setting->recommended = false; - $settings[] = $setting; - - // Check for session auto-start. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_SESSION_AUTO_START'); - $setting->state = (bool) ini_get('session.auto_start'); - $setting->recommended = false; - $settings[] = $setting; - - // Check for native ZIP support. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_ZIP_SUPPORT_AVAILABLE'); - $setting->state = function_exists('zip_open') && function_exists('zip_read'); - $setting->recommended = true; - $settings[] = $setting; - - // Check for GD support - $setting = new \stdClass; - $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'GD'); - $setting->state = extension_loaded('gd'); - $setting->recommended = true; - $settings[] = $setting; - - // Check for iconv support - $setting = new \stdClass; - $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'iconv'); - $setting->state = function_exists('iconv'); - $setting->recommended = true; - $settings[] = $setting; - - // Check for intl support - $setting = new \stdClass; - $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'intl'); - $setting->state = function_exists('transliterator_transliterate'); - $setting->recommended = true; - $settings[] = $setting; - - return $settings; - } - - /** - * Returns the configured database type id (mysqli or sqlsrv or ...) - * - * @return string - * - * @since 3.10.0 - */ - private function getConfiguredDatabaseType() - { - return Factory::getApplication()->get('dbtype'); - } - - /** - * Returns true, if J! version is < 4 or current configured - * database type is compatible with the update. - * - * @return boolean - * - * @since 3.10.0 - */ - public function isDatabaseTypeSupported() - { - $updateInformation = $this->getUpdateInformation(); - $nextMajorVersion = Version::MAJOR_VERSION + 1; - - // Check if configured database is compatible with Joomla 4 - if (version_compare($updateInformation['latest'], (string) $nextMajorVersion, '>=')) - { - $unsupportedDatabaseTypes = array('sqlsrv', 'sqlazure'); - $currentDatabaseType = $this->getConfiguredDatabaseType(); - - return !in_array($currentDatabaseType, $unsupportedDatabaseTypes); - } - - return true; - } - - - /** - * Returns true, if current installed php version is compatible with the update. - * - * @return boolean - * - * @since 3.10.0 - */ - public function isPhpVersionSupported() - { - return version_compare(PHP_VERSION, $this->getTargetMinimumPHPVersion(), '>='); - } - - /** - * Returns the PHP minimum version for the update. - * Returns JOOMLA_MINIMUM_PHP, if there is no information given. - * - * @return string - * - * @since 3.10.0 - */ - private function getTargetMinimumPHPVersion() - { - $updateInformation = $this->getUpdateInformation(); - - return isset($updateInformation['object']->php_minimum) ? - $updateInformation['object']->php_minimum->_data : - JOOMLA_MINIMUM_PHP; - } - - /** - * Checks the availability of the parse_ini_file and parse_ini_string functions. - * @todo: Outsource, build common code base for pre install and pre update check - * - * @return boolean True if the method exists. - * - * @since 3.10.0 - */ - public function getIniParserAvailability() - { - $disabledFunctions = ini_get('disable_functions'); - - if (!empty($disabledFunctions)) - { - // Attempt to detect them in the PHP INI disable_functions variable. - $disabledFunctions = explode(',', trim($disabledFunctions)); - $numberOfDisabledFunctions = count($disabledFunctions); - - for ($i = 0; $i < $numberOfDisabledFunctions; $i++) - { - $disabledFunctions[$i] = trim($disabledFunctions[$i]); - } - - $result = !in_array('parse_ini_string', $disabledFunctions); - } - else - { - // Attempt to detect their existence; even pure PHP implementations of them will trigger a positive response, though. - $result = function_exists('parse_ini_string'); - } - - return $result; - } - - - /** - * Check if database structure is up to date - * - * @return boolean True if ok, false if not. - * - * @since 3.10.0 - */ - private function getDatabaseSchemaCheck(): bool - { - $mvcFactory = $this->bootComponent('com_installer')->getMVCFactory(); - - /** @var \Joomla\Component\Installer\Administrator\Model\DatabaseModel $model */ - $model = $mvcFactory->createModel('Database', 'Administrator'); - - // Check if no default text filters found - if (!$model->getDefaultTextFilters()) - { - return false; - } - - $coreExtensionInfo = \Joomla\CMS\Extension\ExtensionHelper::getExtensionRecord('joomla', 'file'); - $cache = new \Joomla\Registry\Registry($coreExtensionInfo->manifest_cache); - - $updateVersion = $cache->get('version'); - - // Check if database update version does not match CMS version - if (version_compare($updateVersion, JVERSION) != 0) - { - return false; - } - - // Ensure we only get information for core - $model->setState('filter.extension_id', $coreExtensionInfo->extension_id); - - // We're filtering by a single extension which must always exist - so can safely access this through - // element 0 of the array - $changeInformation = $model->getItems()[0]; - - // Check if schema errors found - if ($changeInformation['errorsCount'] !== 0) - { - return false; - } - - // Check if database schema version does not match CMS version - if ($model->getSchemaVersion($coreExtensionInfo->extension_id) != $changeInformation['schema']) - { - return false; - } - - // No database problems found - return true; - } - - /** - * Gets an array containing all installed extensions, that are not core extensions. - * - * @return array name,version,updateserver - * - * @since 3.10.0 - */ - public function getNonCoreExtensions() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->select( - [ - $db->quoteName('ex.name'), - $db->quoteName('ex.extension_id'), - $db->quoteName('ex.manifest_cache'), - $db->quoteName('ex.type'), - $db->quoteName('ex.folder'), - $db->quoteName('ex.element'), - $db->quoteName('ex.client_id'), - ] - ) - ->from($db->quoteName('#__extensions', 'ex')) - ->where($db->quoteName('ex.package_id') . ' = 0') - ->whereNotIn($db->quoteName('ex.extension_id'), ExtensionHelper::getCoreExtensionIds()); - - $db->setQuery($query); - $rows = $db->loadObjectList(); - - foreach ($rows as $extension) - { - $decode = json_decode($extension->manifest_cache); - - // Remove unused fields so they do not cause javascript errors during pre-update check - unset($decode->description); - unset($decode->copyright); - unset($decode->creationDate); - - $this->translateExtensionName($extension); - $extension->version - = isset($decode->version) ? $decode->version : Text::_('COM_JOOMLAUPDATE_PREUPDATE_UNKNOWN_EXTENSION_MANIFESTCACHE_VERSION'); - unset($extension->manifest_cache); - $extension->manifest_cache = $decode; - } - - return $rows; - } - - /** - * Gets an array containing all installed and enabled plugins, that are not core plugins. - * - * @param array $folderFilter Limit the list of plugins to a specific set of folder values - * - * @return array name,version,updateserver - * - * @since 3.10.0 - */ - public function getNonCorePlugins($folderFilter = ['system','user','authentication','actionlog','multifactorauth']) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->select( - $db->qn('ex.name') . ', ' . - $db->qn('ex.extension_id') . ', ' . - $db->qn('ex.manifest_cache') . ', ' . - $db->qn('ex.type') . ', ' . - $db->qn('ex.folder') . ', ' . - $db->qn('ex.element') . ', ' . - $db->qn('ex.client_id') . ', ' . - $db->qn('ex.package_id') - )->from( - $db->qn('#__extensions', 'ex') - )->where( - $db->qn('ex.type') . ' = ' . $db->quote('plugin') - )->where( - $db->qn('ex.enabled') . ' = 1' - )->whereNotIn( - $db->quoteName('ex.extension_id'), ExtensionHelper::getCoreExtensionIds() - ); - - if (count($folderFilter) > 0) - { - $folderFilter = array_map(array($db, 'quote'), $folderFilter); - - $query->where($db->qn('folder') . ' IN (' . implode(',', $folderFilter) . ')'); - } - - $db->setQuery($query); - $rows = $db->loadObjectList(); - - foreach ($rows as $plugin) - { - $decode = json_decode($plugin->manifest_cache); - - // Remove unused fields so they do not cause javascript errors during pre-update check - unset($decode->description); - unset($decode->copyright); - unset($decode->creationDate); - - $this->translateExtensionName($plugin); - $plugin->version = $decode->version ?? Text::_('COM_JOOMLAUPDATE_PREUPDATE_UNKNOWN_EXTENSION_MANIFESTCACHE_VERSION'); - unset($plugin->manifest_cache); - $plugin->manifest_cache = $decode; - } - - return $rows; - } - - /** - * Called by controller's fetchExtensionCompatibility, which is called via AJAX. - * - * @param string $extensionID The ID of the checked extension - * @param string $joomlaTargetVersion Target version of Joomla - * - * @return object - * - * @since 3.10.0 - */ - public function fetchCompatibility($extensionID, $joomlaTargetVersion) - { - $updateSites = $this->getUpdateSitesInfo($extensionID); - - if (empty($updateSites)) - { - return (object) array('state' => 2); - } - - foreach ($updateSites as $updateSite) - { - if ($updateSite['type'] === 'collection') - { - $updateFileUrls = $this->getCollectionDetailsUrls($updateSite, $joomlaTargetVersion); - - foreach ($updateFileUrls as $updateFileUrl) - { - $compatibleVersions = $this->checkCompatibility($updateFileUrl, $joomlaTargetVersion); - - // Return the compatible versions - return (object) array('state' => 1, 'compatibleVersions' => $compatibleVersions); - } - } - else - { - $compatibleVersions = $this->checkCompatibility($updateSite['location'], $joomlaTargetVersion); - - // Return the compatible versions - return (object) array('state' => 1, 'compatibleVersions' => $compatibleVersions); - } - } - - // In any other case we mark this extension as not compatible - return (object) array('state' => 0); - } - - /** - * Returns records with update sites and extension information for a given extension ID. - * - * @param int $extensionID The extension ID - * - * @return array - * - * @since 3.10.0 - */ - private function getUpdateSitesInfo($extensionID) - { - $id = (int) $extensionID; - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->select( - $db->qn('us.type') . ', ' . - $db->qn('us.location') . ', ' . - $db->qn('e.element') . ' AS ' . $db->qn('ext_element') . ', ' . - $db->qn('e.type') . ' AS ' . $db->qn('ext_type') . ', ' . - $db->qn('e.folder') . ' AS ' . $db->qn('ext_folder') - ) - ->from($db->quoteName('#__update_sites', 'us')) - ->join( - 'LEFT', - $db->quoteName('#__update_sites_extensions', 'ue'), - $db->quoteName('ue.update_site_id') . ' = ' . $db->quoteName('us.update_site_id') - ) - ->join( - 'LEFT', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('ue.extension_id') - ) - ->where($db->quoteName('e.extension_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - - $db->setQuery($query); - - $result = $db->loadAssocList(); - - if (!is_array($result)) - { - return array(); - } - - return $result; - } - - /** - * Method to get details URLs from a collection update site for given extension and Joomla target version. - * - * @param array $updateSiteInfo The update site and extension information record to process - * @param string $joomlaTargetVersion The Joomla! version to test against, - * - * @return array An array of URLs. - * - * @since 3.10.0 - */ - private function getCollectionDetailsUrls($updateSiteInfo, $joomlaTargetVersion) - { - $return = array(); - - $http = new Http; - - try - { - $response = $http->get($updateSiteInfo['location']); - } - catch (\RuntimeException $e) - { - $response = null; - } - - if ($response === null || $response->code !== 200) - { - return $return; - } - - $updateSiteXML = simplexml_load_string($response->body); - - foreach ($updateSiteXML->extension as $extension) - { - $attribs = new \stdClass; - - $attribs->element = ''; - $attribs->type = ''; - $attribs->folder = ''; - $attribs->targetplatformversion = ''; - - foreach ($extension->attributes() as $key => $value) - { - $attribs->$key = (string) $value; - } - - if ($attribs->element === $updateSiteInfo['ext_element'] - && $attribs->type === $updateSiteInfo['ext_type'] - && $attribs->folder === $updateSiteInfo['ext_folder'] - && preg_match('/^' . $attribs->targetplatformversion . '/', $joomlaTargetVersion)) - { - $return[] = (string) $extension['detailsurl']; - } - } - - return $return; - } - - /** - * Method to check non core extensions for compatibility. - * - * @param string $updateFileUrl The items update XML url. - * @param string $joomlaTargetVersion The Joomla! version to test against - * - * @return array An array of strings with compatible version numbers - * - * @since 3.10.0 - */ - private function checkCompatibility($updateFileUrl, $joomlaTargetVersion) - { - $minimumStability = ComponentHelper::getParams('com_installer')->get('minimum_stability', Updater::STABILITY_STABLE); - - $update = new Update; - $update->set('jversion.full', $joomlaTargetVersion); - $update->loadFromXml($updateFileUrl, $minimumStability); - - $compatibleVersions = $update->get('compatibleVersions'); - - // Check if old version of the updater library - if (!isset($compatibleVersions)) - { - $downloadUrl = $update->get('downloadurl'); - $updateVersion = $update->get('version'); - - return empty($downloadUrl) || empty($downloadUrl->_data) || empty($updateVersion) ? array() : array($updateVersion->_data); - } - - usort($compatibleVersions, 'version_compare'); - - return $compatibleVersions; - } - - /** - * Translates an extension name - * - * @param object &$item The extension of which the name needs to be translated - * - * @return void - * - * @since 3.10.0 - */ - protected function translateExtensionName(&$item) - { - // @todo: Cleanup duplicated code. from com_installer/models/extension.php - $lang = Factory::getLanguage(); - $path = $item->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE; - - $extension = $item->element; - $source = JPATH_SITE; - - switch ($item->type) - { - case 'component': - $extension = $item->element; - $source = $path . '/components/' . $extension; - break; - case 'module': - $extension = $item->element; - $source = $path . '/modules/' . $extension; - break; - case 'file': - $extension = 'files_' . $item->element; - break; - case 'library': - $extension = 'lib_' . $item->element; - break; - case 'plugin': - $extension = 'plg_' . $item->folder . '_' . $item->element; - $source = JPATH_PLUGINS . '/' . $item->folder . '/' . $item->element; - break; - case 'template': - $extension = 'tpl_' . $item->element; - $source = $path . '/templates/' . $item->element; - } - - $lang->load("$extension.sys", JPATH_ADMINISTRATOR) - || $lang->load("$extension.sys", $source); - $lang->load($extension, JPATH_ADMINISTRATOR) - || $lang->load($extension, $source); - - // Translate the extension name if possible - $item->name = strip_tags(Text::_($item->name)); - } - - /** - * Checks whether a given template is active - * - * @param string $template The template name to be checked - * - * @return boolean - * - * @since 3.10.4 - */ - public function isTemplateActive($template) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->select( - $db->qn( - array( - 'id', - 'home' - ) - ) - )->from( - $db->qn('#__template_styles') - )->where( - $db->qn('template') . ' = :template' - )->bind(':template', $template, ParameterType::STRING); - - $templates = $db->setQuery($query)->loadObjectList(); - - $home = array_filter( - $templates, - function ($value) - { - return $value->home > 0; - } - ); - - $ids = ArrayHelper::getColumn($templates, 'id'); - - $menu = false; - - if (count($ids)) - { - $query = $db->getQuery(true); - - $query->select( - 'COUNT(*)' - )->from( - $db->qn('#__menu') - )->whereIn( - $db->qn('template_style_id'), $ids - ); - - $menu = $db->setQuery($query)->loadResult() > 0; - } - - return $home || $menu; - } + $data .= '];'; + + // Remove the old file, if it's there... + $configpath = JPATH_COMPONENT_ADMINISTRATOR . '/update.php'; + + if (File::exists($configpath)) { + if (!File::delete($configpath)) { + File::invalidateFileCache($configpath); + @unlink($configpath); + } + } + + // Write new file. First try with File. + $result = File::write($configpath, $data); + + // In case File used FTP but direct access could help. + if (!$result) { + if (function_exists('file_put_contents')) { + $result = @file_put_contents($configpath, $data); + + if ($result !== false) { + $result = true; + } + } else { + $fp = @fopen($configpath, 'wt'); + + if ($fp !== false) { + $result = @fwrite($fp, $data); + + if ($result !== false) { + $result = true; + } + + @fclose($fp); + } + } + } + + return $result; + } + + /** + * Finalise the upgrade. + * + * This method will do the following: + * * Run the schema update SQL files. + * * Run the Joomla post-update script. + * * Update the manifest cache and #__extensions entry for Joomla itself. + * + * It performs essentially the same function as InstallerFile::install() without the file copy. + * + * @return boolean True on success. + * + * @since 2.5.4 + */ + public function finaliseUpgrade() + { + $installer = Installer::getInstance(); + + $manifest = $installer->isManifest(JPATH_MANIFESTS . '/files/joomla.xml'); + + if ($manifest === false) { + $installer->abort(Text::_('JLIB_INSTALLER_ABORT_DETECTMANIFEST')); + + return false; + } + + $installer->manifest = $manifest; + + $installer->setUpgrade(true); + $installer->setOverwrite(true); + + $installer->extension = new \Joomla\CMS\Table\Extension($this->getDatabase()); + $installer->extension->load(ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id); + + $installer->setAdapter($installer->extension->type); + + $installer->setPath('manifest', JPATH_MANIFESTS . '/files/joomla.xml'); + $installer->setPath('source', JPATH_MANIFESTS . '/files'); + $installer->setPath('extension_root', JPATH_ROOT); + + // Run the script file. + \JLoader::register('JoomlaInstallerScript', JPATH_ADMINISTRATOR . '/components/com_admin/script.php'); + + $manifestClass = new \JoomlaInstallerScript(); + + ob_start(); + ob_implicit_flush(false); + + if ($manifestClass && method_exists($manifestClass, 'preflight')) { + if ($manifestClass->preflight('update', $installer) === false) { + $installer->abort( + Text::sprintf( + 'JLIB_INSTALLER_ABORT_INSTALL_CUSTOM_INSTALL_FAILURE', + Text::_('JLIB_INSTALLER_INSTALL') + ) + ); + + return false; + } + } + + // Create msg object; first use here. + $msg = ob_get_contents(); + ob_end_clean(); + + // Get a database connector object. + $db = $this->getDatabase(); + + /* + * Check to see if a file extension by the same name is already installed. + * If it is, then update the table because if the files aren't there + * we can assume that it was (badly) uninstalled. + * If it isn't, add an entry to extensions. + */ + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('file')) + ->where($db->quoteName('element') . ' = ' . $db->quote('joomla')); + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + // Install failed, roll back changes. + $installer->abort( + Text::sprintf('JLIB_INSTALLER_ABORT_FILE_ROLLBACK', Text::_('JLIB_INSTALLER_UPDATE'), $e->getMessage()) + ); + + return false; + } + + $id = $db->loadResult(); + $row = new \Joomla\CMS\Table\Extension($this->getDatabase()); + + if ($id) { + // Load the entry and update the manifest_cache. + $row->load($id); + + // Update name. + $row->set('name', 'files_joomla'); + + // Update manifest. + $row->manifest_cache = $installer->generateManifestCache(); + + if (!$row->store()) { + // Install failed, roll back changes. + $installer->abort( + Text::sprintf('JLIB_INSTALLER_ABORT_FILE_ROLLBACK', Text::_('JLIB_INSTALLER_UPDATE'), $row->getError()) + ); + + return false; + } + } else { + // Add an entry to the extension table with a whole heap of defaults. + $row->set('name', 'files_joomla'); + $row->set('type', 'file'); + $row->set('element', 'joomla'); + + // There is no folder for files so leave it blank. + $row->set('folder', ''); + $row->set('enabled', 1); + $row->set('protected', 0); + $row->set('access', 0); + $row->set('client_id', 0); + $row->set('params', ''); + $row->set('manifest_cache', $installer->generateManifestCache()); + + if (!$row->store()) { + // Install failed, roll back changes. + $installer->abort(Text::sprintf('JLIB_INSTALLER_ABORT_FILE_INSTALL_ROLLBACK', $row->getError())); + + return false; + } + + // Set the insert id. + $row->set('extension_id', $db->insertid()); + + // Since we have created a module item, we add it to the installation step stack + // so that if we have to rollback the changes we can undo it. + $installer->pushStep(array('type' => 'extension', 'extension_id' => $row->extension_id)); + } + + $result = $installer->parseSchemaUpdates($manifest->update->schemas, $row->extension_id); + + if ($result === false) { + // Install failed, rollback changes (message already logged by the installer). + $installer->abort(); + + return false; + } + + // Reinitialise the installer's extensions table's properties. + $installer->extension->getFields(true); + + // Start Joomla! 1.6. + ob_start(); + ob_implicit_flush(false); + + if ($manifestClass && method_exists($manifestClass, 'update')) { + if ($manifestClass->update($installer) === false) { + // Install failed, rollback changes. + $installer->abort( + Text::sprintf( + 'JLIB_INSTALLER_ABORT_INSTALL_CUSTOM_INSTALL_FAILURE', + Text::_('JLIB_INSTALLER_INSTALL') + ) + ); + + return false; + } + } + + // Append messages. + $msg .= ob_get_contents(); + ob_end_clean(); + + // Clobber any possible pending updates. + $update = new \Joomla\CMS\Table\Update($this->getDatabase()); + $uid = $update->find( + array('element' => 'joomla', 'type' => 'file', 'client_id' => '0', 'folder' => '') + ); + + if ($uid) { + $update->delete($uid); + } + + // And now we run the postflight. + ob_start(); + ob_implicit_flush(false); + + if ($manifestClass && method_exists($manifestClass, 'postflight')) { + $manifestClass->postflight('update', $installer); + } + + // Append messages. + $msg .= ob_get_contents(); + ob_end_clean(); + + if ($msg != '') { + $installer->set('extension_message', $msg); + } + + // Refresh versionable assets cache. + Factory::getApplication()->flushAssets(); + + return true; + } + + /** + * Removes the extracted package file and trigger onJoomlaAfterUpdate event. + * + * The onJoomlaAfterUpdate event compares the stored list of files previously overridden with + * the updated core files, finding out which files have changed during the update, thereby + * determining how many and which override files need to be checked and possibly updated after + * the Joomla update. + * + * @return void + * + * @since 2.5.4 + */ + public function cleanUp() + { + // Load overrides plugin. + PluginHelper::importPlugin('installer'); + + $app = Factory::getApplication(); + + // Trigger event after joomla update. + $app->triggerEvent('onJoomlaAfterUpdate'); + + // Remove the update package. + $tempdir = $app->get('tmp_path'); + + $file = $app->getUserState('com_joomlaupdate.file', null); + File::delete($tempdir . '/' . $file); + + // Remove the update.php file used in Joomla 4.0.3 and later. + if (File::exists(JPATH_COMPONENT_ADMINISTRATOR . '/update.php')) { + File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/update.php'); + } + + // Remove the legacy restoration.php file (when updating from Joomla 4.0.2 and earlier). + if (File::exists(JPATH_COMPONENT_ADMINISTRATOR . '/restoration.php')) { + File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/restoration.php'); + } + + // Remove the legacy restore_finalisation.php file used in Joomla 4.0.2 and earlier. + if (File::exists(JPATH_COMPONENT_ADMINISTRATOR . '/restore_finalisation.php')) { + File::delete(JPATH_COMPONENT_ADMINISTRATOR . '/restore_finalisation.php'); + } + + // Remove joomla.xml from the site's root. + if (File::exists(JPATH_ROOT . '/joomla.xml')) { + File::delete(JPATH_ROOT . '/joomla.xml'); + } + + // Unset the update filename from the session. + $app = Factory::getApplication(); + $app->setUserState('com_joomlaupdate.file', null); + $oldVersion = $app->getUserState('com_joomlaupdate.oldversion'); + + // Trigger event after joomla update. + $app->triggerEvent('onJoomlaAfterUpdate', array($oldVersion)); + $app->setUserState('com_joomlaupdate.oldversion', null); + } + + /** + * Uploads what is presumably an update ZIP file under a mangled name in the temporary directory. + * + * @return void + * + * @since 3.6.0 + */ + public function upload() + { + // Get the uploaded file information. + $input = Factory::getApplication()->input; + + // Do not change the filter type 'raw'. We need this to let files containing PHP code to upload. See \JInputFiles::get. + $userfile = $input->files->get('install_package', null, 'raw'); + + // Make sure that file uploads are enabled in php. + if (!(bool) ini_get('file_uploads')) { + throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLFILE'), 500); + } + + // Make sure that zlib is loaded so that the package can be unpacked. + if (!extension_loaded('zlib')) { + throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLZLIB'), 500); + } + + // If there is no uploaded file, we have a problem... + if (!is_array($userfile)) { + throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_NO_FILE_SELECTED'), 500); + } + + // Is the PHP tmp directory missing? + if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_NO_TMP_DIR)) { + throw new \RuntimeException( + Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
' . + Text::_('COM_INSTALLER_MSG_WARNINGS_PHPUPLOADNOTSET'), + 500 + ); + } + + // Is the max upload size too small in php.ini? + if ($userfile['error'] && ($userfile['error'] == UPLOAD_ERR_INI_SIZE)) { + throw new \RuntimeException( + Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR') . '
' . Text::_('COM_INSTALLER_MSG_WARNINGS_SMALLUPLOADSIZE'), + 500 + ); + } + + // Check if there was a different problem uploading the file. + if ($userfile['error'] || $userfile['size'] < 1) { + throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR'), 500); + } + + // Build the appropriate paths. + $tmp_dest = tempnam(Factory::getApplication()->get('tmp_path'), 'ju'); + $tmp_src = $userfile['tmp_name']; + + // Move uploaded file. + $result = File::upload($tmp_src, $tmp_dest, false, true); + + if (!$result) { + throw new \RuntimeException(Text::_('COM_INSTALLER_MSG_INSTALL_WARNINSTALLUPLOADERROR'), 500); + } + + Factory::getApplication()->setUserState('com_joomlaupdate.temp_file', $tmp_dest); + } + + /** + * Checks the super admin credentials are valid for the currently logged in users + * + * @param array $credentials The credentials to authenticate the user with + * + * @return boolean + * + * @since 3.6.0 + */ + public function captiveLogin($credentials) + { + // Make sure the username matches + $username = $credentials['username'] ?? null; + $user = Factory::getUser(); + + if (strtolower($user->username) != strtolower($username)) { + return false; + } + + // Make sure the user is authorised + if (!$user->authorise('core.admin')) { + return false; + } + + // Get the global Authentication object. + $authenticate = Authentication::getInstance(); + $response = $authenticate->authenticate($credentials); + + if ($response->status !== Authentication::STATUS_SUCCESS) { + return false; + } + + return true; + } + + /** + * Does the captive (temporary) file we uploaded before still exist? + * + * @return boolean + * + * @since 3.6.0 + */ + public function captiveFileExists() + { + $file = Factory::getApplication()->getUserState('com_joomlaupdate.temp_file', null); + + if (empty($file) || !File::exists($file)) { + return false; + } + + return true; + } + + /** + * Remove the captive (temporary) file we uploaded before and the . + * + * @return void + * + * @since 3.6.0 + */ + public function removePackageFiles() + { + $files = array( + Factory::getApplication()->getUserState('com_joomlaupdate.temp_file', null), + Factory::getApplication()->getUserState('com_joomlaupdate.file', null), + ); + + foreach ($files as $file) { + if ($file !== null && File::exists($file)) { + File::delete($file); + } + } + } + + /** + * Gets PHP options. + * @todo: Outsource, build common code base for pre install and pre update check + * + * @return array Array of PHP config options + * + * @since 3.10.0 + */ + public function getPhpOptions() + { + $options = array(); + + /* + * Check the PHP Version. It is already checked in Update. + * A Joomla! Update which is not supported by current PHP + * version is not shown. So this check is actually unnecessary. + */ + $option = new \stdClass(); + $option->label = Text::sprintf('INSTL_PHP_VERSION_NEWER', $this->getTargetMinimumPHPVersion()); + $option->state = $this->isPhpVersionSupported(); + $option->notice = null; + $options[] = $option; + + // Check for zlib support. + $option = new \stdClass(); + $option->label = Text::_('INSTL_ZLIB_COMPRESSION_SUPPORT'); + $option->state = extension_loaded('zlib'); + $option->notice = null; + $options[] = $option; + + // Check for XML support. + $option = new \stdClass(); + $option->label = Text::_('INSTL_XML_SUPPORT'); + $option->state = extension_loaded('xml'); + $option->notice = null; + $options[] = $option; + + // Check for mbstring options. + if (extension_loaded('mbstring')) { + // Check for default MB language. + $option = new \stdClass(); + $option->label = Text::_('INSTL_MB_LANGUAGE_IS_DEFAULT'); + $option->state = strtolower(ini_get('mbstring.language')) === 'neutral'; + $option->notice = $option->state ? null : Text::_('INSTL_NOTICEMBLANGNOTDEFAULT'); + $options[] = $option; + + // Check for MB function overload. + $option = new \stdClass(); + $option->label = Text::_('INSTL_MB_STRING_OVERLOAD_OFF'); + $option->state = ini_get('mbstring.func_overload') == 0; + $option->notice = $option->state ? null : Text::_('INSTL_NOTICEMBSTRINGOVERLOAD'); + $options[] = $option; + } + + // Check for a missing native parse_ini_file implementation. + $option = new \stdClass(); + $option->label = Text::_('INSTL_PARSE_INI_FILE_AVAILABLE'); + $option->state = $this->getIniParserAvailability(); + $option->notice = null; + $options[] = $option; + + // Check for missing native json_encode / json_decode support. + $option = new \stdClass(); + $option->label = Text::_('INSTL_JSON_SUPPORT_AVAILABLE'); + $option->state = function_exists('json_encode') && function_exists('json_decode'); + $option->notice = null; + $options[] = $option; + $updateInformation = $this->getUpdateInformation(); + + // Check if configured database is compatible with the next major version of Joomla + $nextMajorVersion = Version::MAJOR_VERSION + 1; + + if (version_compare($updateInformation['latest'], (string) $nextMajorVersion, '>=')) { + $option = new \stdClass(); + $option->label = Text::sprintf('INSTL_DATABASE_SUPPORTED', $this->getConfiguredDatabaseType()); + $option->state = $this->isDatabaseTypeSupported(); + $option->notice = null; + $options[] = $option; + } + + // Check if database structure is up to date + $option = new \stdClass(); + $option->label = Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_DATABASE_STRUCTURE_TITLE'); + $option->state = $this->getDatabaseSchemaCheck(); + $option->notice = $option->state ? null : Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_DATABASE_STRUCTURE_NOTICE'); + $options[] = $option; + + return $options; + } + + /** + * Gets PHP Settings. + * @todo: Outsource, build common code base for pre install and pre update check + * + * @return array + * + * @since 3.10.0 + */ + public function getPhpSettings() + { + $settings = array(); + + // Check for display errors. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_DISPLAY_ERRORS'); + $setting->state = (bool) ini_get('display_errors'); + $setting->recommended = false; + $settings[] = $setting; + + // Check for file uploads. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_FILE_UPLOADS'); + $setting->state = (bool) ini_get('file_uploads'); + $setting->recommended = true; + $settings[] = $setting; + + // Check for output buffering. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_OUTPUT_BUFFERING'); + $setting->state = (int) ini_get('output_buffering') !== 0; + $setting->recommended = false; + $settings[] = $setting; + + // Check for session auto-start. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_SESSION_AUTO_START'); + $setting->state = (bool) ini_get('session.auto_start'); + $setting->recommended = false; + $settings[] = $setting; + + // Check for native ZIP support. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_ZIP_SUPPORT_AVAILABLE'); + $setting->state = function_exists('zip_open') && function_exists('zip_read'); + $setting->recommended = true; + $settings[] = $setting; + + // Check for GD support + $setting = new \stdClass(); + $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'GD'); + $setting->state = extension_loaded('gd'); + $setting->recommended = true; + $settings[] = $setting; + + // Check for iconv support + $setting = new \stdClass(); + $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'iconv'); + $setting->state = function_exists('iconv'); + $setting->recommended = true; + $settings[] = $setting; + + // Check for intl support + $setting = new \stdClass(); + $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'intl'); + $setting->state = function_exists('transliterator_transliterate'); + $setting->recommended = true; + $settings[] = $setting; + + return $settings; + } + + /** + * Returns the configured database type id (mysqli or sqlsrv or ...) + * + * @return string + * + * @since 3.10.0 + */ + private function getConfiguredDatabaseType() + { + return Factory::getApplication()->get('dbtype'); + } + + /** + * Returns true, if J! version is < 4 or current configured + * database type is compatible with the update. + * + * @return boolean + * + * @since 3.10.0 + */ + public function isDatabaseTypeSupported() + { + $updateInformation = $this->getUpdateInformation(); + $nextMajorVersion = Version::MAJOR_VERSION + 1; + + // Check if configured database is compatible with Joomla 4 + if (version_compare($updateInformation['latest'], (string) $nextMajorVersion, '>=')) { + $unsupportedDatabaseTypes = array('sqlsrv', 'sqlazure'); + $currentDatabaseType = $this->getConfiguredDatabaseType(); + + return !in_array($currentDatabaseType, $unsupportedDatabaseTypes); + } + + return true; + } + + + /** + * Returns true, if current installed php version is compatible with the update. + * + * @return boolean + * + * @since 3.10.0 + */ + public function isPhpVersionSupported() + { + return version_compare(PHP_VERSION, $this->getTargetMinimumPHPVersion(), '>='); + } + + /** + * Returns the PHP minimum version for the update. + * Returns JOOMLA_MINIMUM_PHP, if there is no information given. + * + * @return string + * + * @since 3.10.0 + */ + private function getTargetMinimumPHPVersion() + { + $updateInformation = $this->getUpdateInformation(); + + return isset($updateInformation['object']->php_minimum) ? + $updateInformation['object']->php_minimum->_data : + JOOMLA_MINIMUM_PHP; + } + + /** + * Checks the availability of the parse_ini_file and parse_ini_string functions. + * @todo: Outsource, build common code base for pre install and pre update check + * + * @return boolean True if the method exists. + * + * @since 3.10.0 + */ + public function getIniParserAvailability() + { + $disabledFunctions = ini_get('disable_functions'); + + if (!empty($disabledFunctions)) { + // Attempt to detect them in the PHP INI disable_functions variable. + $disabledFunctions = explode(',', trim($disabledFunctions)); + $numberOfDisabledFunctions = count($disabledFunctions); + + for ($i = 0; $i < $numberOfDisabledFunctions; $i++) { + $disabledFunctions[$i] = trim($disabledFunctions[$i]); + } + + $result = !in_array('parse_ini_string', $disabledFunctions); + } else { + // Attempt to detect their existence; even pure PHP implementations of them will trigger a positive response, though. + $result = function_exists('parse_ini_string'); + } + + return $result; + } + + + /** + * Check if database structure is up to date + * + * @return boolean True if ok, false if not. + * + * @since 3.10.0 + */ + private function getDatabaseSchemaCheck(): bool + { + $mvcFactory = $this->bootComponent('com_installer')->getMVCFactory(); + + /** @var \Joomla\Component\Installer\Administrator\Model\DatabaseModel $model */ + $model = $mvcFactory->createModel('Database', 'Administrator'); + + // Check if no default text filters found + if (!$model->getDefaultTextFilters()) { + return false; + } + + $coreExtensionInfo = \Joomla\CMS\Extension\ExtensionHelper::getExtensionRecord('joomla', 'file'); + $cache = new \Joomla\Registry\Registry($coreExtensionInfo->manifest_cache); + + $updateVersion = $cache->get('version'); + + // Check if database update version does not match CMS version + if (version_compare($updateVersion, JVERSION) != 0) { + return false; + } + + // Ensure we only get information for core + $model->setState('filter.extension_id', $coreExtensionInfo->extension_id); + + // We're filtering by a single extension which must always exist - so can safely access this through + // element 0 of the array + $changeInformation = $model->getItems()[0]; + + // Check if schema errors found + if ($changeInformation['errorsCount'] !== 0) { + return false; + } + + // Check if database schema version does not match CMS version + if ($model->getSchemaVersion($coreExtensionInfo->extension_id) != $changeInformation['schema']) { + return false; + } + + // No database problems found + return true; + } + + /** + * Gets an array containing all installed extensions, that are not core extensions. + * + * @return array name,version,updateserver + * + * @since 3.10.0 + */ + public function getNonCoreExtensions() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select( + [ + $db->quoteName('ex.name'), + $db->quoteName('ex.extension_id'), + $db->quoteName('ex.manifest_cache'), + $db->quoteName('ex.type'), + $db->quoteName('ex.folder'), + $db->quoteName('ex.element'), + $db->quoteName('ex.client_id'), + ] + ) + ->from($db->quoteName('#__extensions', 'ex')) + ->where($db->quoteName('ex.package_id') . ' = 0') + ->whereNotIn($db->quoteName('ex.extension_id'), ExtensionHelper::getCoreExtensionIds()); + + $db->setQuery($query); + $rows = $db->loadObjectList(); + + foreach ($rows as $extension) { + $decode = json_decode($extension->manifest_cache); + + // Remove unused fields so they do not cause javascript errors during pre-update check + unset($decode->description); + unset($decode->copyright); + unset($decode->creationDate); + + $this->translateExtensionName($extension); + $extension->version + = isset($decode->version) ? $decode->version : Text::_('COM_JOOMLAUPDATE_PREUPDATE_UNKNOWN_EXTENSION_MANIFESTCACHE_VERSION'); + unset($extension->manifest_cache); + $extension->manifest_cache = $decode; + } + + return $rows; + } + + /** + * Gets an array containing all installed and enabled plugins, that are not core plugins. + * + * @param array $folderFilter Limit the list of plugins to a specific set of folder values + * + * @return array name,version,updateserver + * + * @since 3.10.0 + */ + public function getNonCorePlugins($folderFilter = ['system','user','authentication','actionlog','multifactorauth']) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select( + $db->qn('ex.name') . ', ' . + $db->qn('ex.extension_id') . ', ' . + $db->qn('ex.manifest_cache') . ', ' . + $db->qn('ex.type') . ', ' . + $db->qn('ex.folder') . ', ' . + $db->qn('ex.element') . ', ' . + $db->qn('ex.client_id') . ', ' . + $db->qn('ex.package_id') + )->from( + $db->qn('#__extensions', 'ex') + )->where( + $db->qn('ex.type') . ' = ' . $db->quote('plugin') + )->where( + $db->qn('ex.enabled') . ' = 1' + )->whereNotIn( + $db->quoteName('ex.extension_id'), + ExtensionHelper::getCoreExtensionIds() + ); + + if (count($folderFilter) > 0) { + $folderFilter = array_map(array($db, 'quote'), $folderFilter); + + $query->where($db->qn('folder') . ' IN (' . implode(',', $folderFilter) . ')'); + } + + $db->setQuery($query); + $rows = $db->loadObjectList(); + + foreach ($rows as $plugin) { + $decode = json_decode($plugin->manifest_cache); + + // Remove unused fields so they do not cause javascript errors during pre-update check + unset($decode->description); + unset($decode->copyright); + unset($decode->creationDate); + + $this->translateExtensionName($plugin); + $plugin->version = $decode->version ?? Text::_('COM_JOOMLAUPDATE_PREUPDATE_UNKNOWN_EXTENSION_MANIFESTCACHE_VERSION'); + unset($plugin->manifest_cache); + $plugin->manifest_cache = $decode; + } + + return $rows; + } + + /** + * Called by controller's fetchExtensionCompatibility, which is called via AJAX. + * + * @param string $extensionID The ID of the checked extension + * @param string $joomlaTargetVersion Target version of Joomla + * + * @return object + * + * @since 3.10.0 + */ + public function fetchCompatibility($extensionID, $joomlaTargetVersion) + { + $updateSites = $this->getUpdateSitesInfo($extensionID); + + if (empty($updateSites)) { + return (object) array('state' => 2); + } + + foreach ($updateSites as $updateSite) { + if ($updateSite['type'] === 'collection') { + $updateFileUrls = $this->getCollectionDetailsUrls($updateSite, $joomlaTargetVersion); + + foreach ($updateFileUrls as $updateFileUrl) { + $compatibleVersions = $this->checkCompatibility($updateFileUrl, $joomlaTargetVersion); + + // Return the compatible versions + return (object) array('state' => 1, 'compatibleVersions' => $compatibleVersions); + } + } else { + $compatibleVersions = $this->checkCompatibility($updateSite['location'], $joomlaTargetVersion); + + // Return the compatible versions + return (object) array('state' => 1, 'compatibleVersions' => $compatibleVersions); + } + } + + // In any other case we mark this extension as not compatible + return (object) array('state' => 0); + } + + /** + * Returns records with update sites and extension information for a given extension ID. + * + * @param int $extensionID The extension ID + * + * @return array + * + * @since 3.10.0 + */ + private function getUpdateSitesInfo($extensionID) + { + $id = (int) $extensionID; + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select( + $db->qn('us.type') . ', ' . + $db->qn('us.location') . ', ' . + $db->qn('e.element') . ' AS ' . $db->qn('ext_element') . ', ' . + $db->qn('e.type') . ' AS ' . $db->qn('ext_type') . ', ' . + $db->qn('e.folder') . ' AS ' . $db->qn('ext_folder') + ) + ->from($db->quoteName('#__update_sites', 'us')) + ->join( + 'LEFT', + $db->quoteName('#__update_sites_extensions', 'ue'), + $db->quoteName('ue.update_site_id') . ' = ' . $db->quoteName('us.update_site_id') + ) + ->join( + 'LEFT', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('ue.extension_id') + ) + ->where($db->quoteName('e.extension_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + $db->setQuery($query); + + $result = $db->loadAssocList(); + + if (!is_array($result)) { + return array(); + } + + return $result; + } + + /** + * Method to get details URLs from a collection update site for given extension and Joomla target version. + * + * @param array $updateSiteInfo The update site and extension information record to process + * @param string $joomlaTargetVersion The Joomla! version to test against, + * + * @return array An array of URLs. + * + * @since 3.10.0 + */ + private function getCollectionDetailsUrls($updateSiteInfo, $joomlaTargetVersion) + { + $return = array(); + + $http = new Http(); + + try { + $response = $http->get($updateSiteInfo['location']); + } catch (\RuntimeException $e) { + $response = null; + } + + if ($response === null || $response->code !== 200) { + return $return; + } + + $updateSiteXML = simplexml_load_string($response->body); + + foreach ($updateSiteXML->extension as $extension) { + $attribs = new \stdClass(); + + $attribs->element = ''; + $attribs->type = ''; + $attribs->folder = ''; + $attribs->targetplatformversion = ''; + + foreach ($extension->attributes() as $key => $value) { + $attribs->$key = (string) $value; + } + + if ( + $attribs->element === $updateSiteInfo['ext_element'] + && $attribs->type === $updateSiteInfo['ext_type'] + && $attribs->folder === $updateSiteInfo['ext_folder'] + && preg_match('/^' . $attribs->targetplatformversion . '/', $joomlaTargetVersion) + ) { + $return[] = (string) $extension['detailsurl']; + } + } + + return $return; + } + + /** + * Method to check non core extensions for compatibility. + * + * @param string $updateFileUrl The items update XML url. + * @param string $joomlaTargetVersion The Joomla! version to test against + * + * @return array An array of strings with compatible version numbers + * + * @since 3.10.0 + */ + private function checkCompatibility($updateFileUrl, $joomlaTargetVersion) + { + $minimumStability = ComponentHelper::getParams('com_installer')->get('minimum_stability', Updater::STABILITY_STABLE); + + $update = new Update(); + $update->set('jversion.full', $joomlaTargetVersion); + $update->loadFromXml($updateFileUrl, $minimumStability); + + $compatibleVersions = $update->get('compatibleVersions'); + + // Check if old version of the updater library + if (!isset($compatibleVersions)) { + $downloadUrl = $update->get('downloadurl'); + $updateVersion = $update->get('version'); + + return empty($downloadUrl) || empty($downloadUrl->_data) || empty($updateVersion) ? array() : array($updateVersion->_data); + } + + usort($compatibleVersions, 'version_compare'); + + return $compatibleVersions; + } + + /** + * Translates an extension name + * + * @param object &$item The extension of which the name needs to be translated + * + * @return void + * + * @since 3.10.0 + */ + protected function translateExtensionName(&$item) + { + // @todo: Cleanup duplicated code. from com_installer/models/extension.php + $lang = Factory::getLanguage(); + $path = $item->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE; + + $extension = $item->element; + $source = JPATH_SITE; + + switch ($item->type) { + case 'component': + $extension = $item->element; + $source = $path . '/components/' . $extension; + break; + case 'module': + $extension = $item->element; + $source = $path . '/modules/' . $extension; + break; + case 'file': + $extension = 'files_' . $item->element; + break; + case 'library': + $extension = 'lib_' . $item->element; + break; + case 'plugin': + $extension = 'plg_' . $item->folder . '_' . $item->element; + $source = JPATH_PLUGINS . '/' . $item->folder . '/' . $item->element; + break; + case 'template': + $extension = 'tpl_' . $item->element; + $source = $path . '/templates/' . $item->element; + } + + $lang->load("$extension.sys", JPATH_ADMINISTRATOR) + || $lang->load("$extension.sys", $source); + $lang->load($extension, JPATH_ADMINISTRATOR) + || $lang->load($extension, $source); + + // Translate the extension name if possible + $item->name = strip_tags(Text::_($item->name)); + } + + /** + * Checks whether a given template is active + * + * @param string $template The template name to be checked + * + * @return boolean + * + * @since 3.10.4 + */ + public function isTemplateActive($template) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select( + $db->qn( + array( + 'id', + 'home' + ) + ) + )->from( + $db->qn('#__template_styles') + )->where( + $db->qn('template') . ' = :template' + )->bind(':template', $template, ParameterType::STRING); + + $templates = $db->setQuery($query)->loadObjectList(); + + $home = array_filter( + $templates, + function ($value) { + return $value->home > 0; + } + ); + + $ids = ArrayHelper::getColumn($templates, 'id'); + + $menu = false; + + if (count($ids)) { + $query = $db->getQuery(true); + + $query->select( + 'COUNT(*)' + )->from( + $db->qn('#__menu') + )->whereIn( + $db->qn('template_style_id'), + $ids + ); + + $menu = $db->setQuery($query)->loadResult() > 0; + } + + return $home || $menu; + } } diff --git a/administrator/components/com_joomlaupdate/src/View/Joomlaupdate/HtmlView.php b/administrator/components/com_joomlaupdate/src/View/Joomlaupdate/HtmlView.php index 554c8c01968fc..5b583ac453c96 100644 --- a/administrator/components/com_joomlaupdate/src/View/Joomlaupdate/HtmlView.php +++ b/administrator/components/com_joomlaupdate/src/View/Joomlaupdate/HtmlView.php @@ -1,4 +1,5 @@ updateInfo = $this->get('UpdateInformation'); - $this->selfUpdateAvailable = $this->get('CheckForSelfUpdate'); - - // Get results of pre update check evaluations - $model = $this->getModel(); - $this->phpOptions = $this->get('PhpOptions'); - $this->phpSettings = $this->get('PhpSettings'); - $this->nonCoreExtensions = $this->get('NonCoreExtensions'); - $this->isDefaultBackendTemplate = (bool) $model->isTemplateActive($this->defaultBackendTemplate); - $nextMajorVersion = Version::MAJOR_VERSION + 1; - - // The critical plugins check is only available for major updates. - if (version_compare($this->updateInfo['latest'], (string) $nextMajorVersion, '>=')) - { - $this->nonCoreCriticalPlugins = $this->get('NonCorePlugins'); - } - - // Set to true if a required PHP option is not ok - $isCritical = false; - - foreach ($this->phpOptions as $option) - { - if (!$option->state) - { - $isCritical = true; - break; - } - } - - $this->state = $this->get('State'); - - $hasUpdate = !empty($this->updateInfo['hasUpdate']); - $hasDownload = isset($this->updateInfo['object']->downloadurl->_data); - - // Fresh update, show it - if ($this->getLayout() == 'complete') - { - // Complete message, nothing to do here - } - // There is an update for the updater itself. So we have to update it first - elseif ($this->selfUpdateAvailable) - { - $this->setLayout('selfupdate'); - } - elseif (!$hasDownload || !$hasUpdate) - { - // Could be that we have a download file but no update, so we offer a re-install - if ($hasDownload) - { - // We can reinstall if we have a URL but no update - $this->setLayout('reinstall'); - } - // No download available - else - { - if ($hasUpdate) - { - $this->messagePrefix = '_NODOWNLOAD'; - } - - $this->setLayout('noupdate'); - } - } - // Here we have now two options: preupdatecheck or update - elseif ($this->getLayout() != 'update' && ($isCritical || $this->shouldDisplayPreUpdateCheck())) - { - $this->setLayout('preupdatecheck'); - } - else - { - $this->setLayout('update'); - } - - if (in_array($this->getLayout(), ['preupdatecheck', 'update', 'upload'])) - { - $language = Factory::getLanguage(); - $language->load('com_installer', JPATH_ADMINISTRATOR, 'en-GB', false, true); - $language->load('com_installer', JPATH_ADMINISTRATOR, null, true); - - Factory::getApplication()->enqueueMessage(Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATE_NOTICE'), 'notice'); - } - - $params = ComponentHelper::getParams('com_joomlaupdate'); - - switch ($params->get('updatesource', 'default')) - { - // "Minor & Patch Release for Current version AND Next Major Release". - case 'next': - $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_NEXT'; - $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_NEXT'); - break; - - // "Testing" - case 'testing': - $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_TESTING'; - $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_TESTING'); - break; - - // "Custom" - case 'custom': - $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_CUSTOM'; - $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_CUSTOM'); - break; - - /** - * "Minor & Patch Release for Current version (recommended and default)". - * The commented "case" below are for documenting where 'default' and legacy options falls - * case 'default': - * case 'sts': - * case 'lts': - * case 'nochange': - */ - default: - $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_DEFAULT'; - $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_DEFAULT'); - } - - $this->noVersionCheck = $params->get('versioncheck', 1) == 0; - $this->noBackupCheck = $params->get('backupcheck', 1) == 0; - - // Remove temporary files - $this->getModel()->removePackageFiles(); - - $this->addToolbar(); - - // Render the view. - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - // Set the toolbar information. - ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_OVERVIEW'), 'joomla install'); - - if (in_array($this->getLayout(), ['update', 'complete'])) - { - $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; - - ToolbarHelper::link('index.php?option=com_joomlaupdate', 'JTOOLBAR_BACK', $arrow); - - ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_TAB_UPLOAD'), 'joomla install'); - } - elseif (!$this->selfUpdateAvailable) - { - ToolbarHelper::custom('update.purge', 'loop', '', 'COM_JOOMLAUPDATE_TOOLBAR_CHECK', false); - } - - // Add toolbar buttons. - if ($this->getCurrentUser()->authorise('core.admin')) - { - ToolbarHelper::preferences('com_joomlaupdate'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Joomla_Update'); - } - - /** - * Returns true, if the pre update check should be displayed. - * - * @return boolean - * - * @since 3.10.0 - */ - public function shouldDisplayPreUpdateCheck() - { - // When the download URL is not found there is no core upgrade path - if (!isset($this->updateInfo['object']->downloadurl->_data)) - { - return false; - } - - $nextMinor = Version::MAJOR_VERSION . '.' . (Version::MINOR_VERSION + 1); - - // Show only when we found a download URL, we have an update and when we update to the next minor or greater. - return $this->updateInfo['hasUpdate'] - && version_compare($this->updateInfo['latest'], $nextMinor, '>='); - } + /** + * An array with the Joomla! update information. + * + * @var array + * + * @since 3.6.0 + */ + protected $updateInfo = null; + + /** + * PHP options. + * + * @var array Array of PHP config options + * + * @since 3.10.0 + */ + protected $phpOptions = null; + + /** + * PHP settings. + * + * @var array Array of PHP settings + * + * @since 3.10.0 + */ + protected $phpSettings = null; + + /** + * Non Core Extensions. + * + * @var array Array of Non-Core-Extensions + * + * @since 3.10.0 + */ + protected $nonCoreExtensions = null; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 4.0.0 + */ + protected $state; + + /** + * Flag if the update component itself has to be updated + * + * @var boolean True when update is available otherwise false + * + * @since 4.0.0 + */ + protected $selfUpdateAvailable = false; + + /** + * The default admin template for the major version of Joomla that should be used when + * upgrading to the next major version of Joomla + * + * @var string + * + * @since 4.0.0 + */ + protected $defaultBackendTemplate = 'atum'; + + /** + * Flag if default backend template is being used + * + * @var boolean True when default backend template is being used + * + * @since 4.0.0 + */ + protected $isDefaultBackendTemplate = false; + + /** + * A special prefix used for the emptystate layout variable + * + * @var string The prefix + * + * @since 4.0.0 + */ + protected $messagePrefix = ''; + + /** + * List of non core critical plugins + * + * @var \stdClass[] + * @since 4.0.0 + */ + protected $nonCoreCriticalPlugins = []; + + /** + * Should I disable the confirmation checkbox for pre-update extension version checks? + * + * @var boolean + * @since 4.2.0 + */ + protected $noVersionCheck = false; + + /** + * Should I disable the confirmation checkbox for taking a backup before updating? + * + * @var boolean + * @since 4.2.0 + */ + protected $noBackupCheck = false; + + /** + * Renders the view + * + * @param string $tpl Template name + * + * @return void + * + * @since 2.5.4 + */ + public function display($tpl = null) + { + $this->updateInfo = $this->get('UpdateInformation'); + $this->selfUpdateAvailable = $this->get('CheckForSelfUpdate'); + + // Get results of pre update check evaluations + $model = $this->getModel(); + $this->phpOptions = $this->get('PhpOptions'); + $this->phpSettings = $this->get('PhpSettings'); + $this->nonCoreExtensions = $this->get('NonCoreExtensions'); + $this->isDefaultBackendTemplate = (bool) $model->isTemplateActive($this->defaultBackendTemplate); + $nextMajorVersion = Version::MAJOR_VERSION + 1; + + // The critical plugins check is only available for major updates. + if (version_compare($this->updateInfo['latest'], (string) $nextMajorVersion, '>=')) { + $this->nonCoreCriticalPlugins = $this->get('NonCorePlugins'); + } + + // Set to true if a required PHP option is not ok + $isCritical = false; + + foreach ($this->phpOptions as $option) { + if (!$option->state) { + $isCritical = true; + break; + } + } + + $this->state = $this->get('State'); + + $hasUpdate = !empty($this->updateInfo['hasUpdate']); + $hasDownload = isset($this->updateInfo['object']->downloadurl->_data); + + // Fresh update, show it + if ($this->getLayout() == 'complete') { + // Complete message, nothing to do here + } elseif ($this->selfUpdateAvailable) { + // There is an update for the updater itself. So we have to update it first + $this->setLayout('selfupdate'); + } elseif (!$hasDownload || !$hasUpdate) { + // Could be that we have a download file but no update, so we offer a re-install + if ($hasDownload) { + // We can reinstall if we have a URL but no update + $this->setLayout('reinstall'); + } else { + // No download available + if ($hasUpdate) { + $this->messagePrefix = '_NODOWNLOAD'; + } + + $this->setLayout('noupdate'); + } + } elseif ($this->getLayout() != 'update' && ($isCritical || $this->shouldDisplayPreUpdateCheck())) { + // Here we have now two options: preupdatecheck or update + $this->setLayout('preupdatecheck'); + } else { + $this->setLayout('update'); + } + + if (in_array($this->getLayout(), ['preupdatecheck', 'update', 'upload'])) { + $language = Factory::getLanguage(); + $language->load('com_installer', JPATH_ADMINISTRATOR, 'en-GB', false, true); + $language->load('com_installer', JPATH_ADMINISTRATOR, null, true); + + Factory::getApplication()->enqueueMessage(Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATE_NOTICE'), 'notice'); + } + + $params = ComponentHelper::getParams('com_joomlaupdate'); + + switch ($params->get('updatesource', 'default')) { + // "Minor & Patch Release for Current version AND Next Major Release". + case 'next': + $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_NEXT'; + $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_NEXT'); + break; + + // "Testing" + case 'testing': + $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_TESTING'; + $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_TESTING'); + break; + + // "Custom" + case 'custom': + $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_CUSTOM'; + $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_CUSTOM'); + break; + + /** + * "Minor & Patch Release for Current version (recommended and default)". + * The commented "case" below are for documenting where 'default' and legacy options falls + * case 'default': + * case 'sts': + * case 'lts': + * case 'nochange': + */ + default: + $this->langKey = 'COM_JOOMLAUPDATE_VIEW_DEFAULT_UPDATES_INFO_DEFAULT'; + $this->updateSourceKey = Text::_('COM_JOOMLAUPDATE_CONFIG_UPDATESOURCE_DEFAULT'); + } + + $this->noVersionCheck = $params->get('versioncheck', 1) == 0; + $this->noBackupCheck = $params->get('backupcheck', 1) == 0; + + // Remove temporary files + $this->getModel()->removePackageFiles(); + + $this->addToolbar(); + + // Render the view. + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + // Set the toolbar information. + ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_OVERVIEW'), 'joomla install'); + + if (in_array($this->getLayout(), ['update', 'complete'])) { + $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; + + ToolbarHelper::link('index.php?option=com_joomlaupdate', 'JTOOLBAR_BACK', $arrow); + + ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_TAB_UPLOAD'), 'joomla install'); + } elseif (!$this->selfUpdateAvailable) { + ToolbarHelper::custom('update.purge', 'loop', '', 'COM_JOOMLAUPDATE_TOOLBAR_CHECK', false); + } + + // Add toolbar buttons. + if ($this->getCurrentUser()->authorise('core.admin')) { + ToolbarHelper::preferences('com_joomlaupdate'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Joomla_Update'); + } + + /** + * Returns true, if the pre update check should be displayed. + * + * @return boolean + * + * @since 3.10.0 + */ + public function shouldDisplayPreUpdateCheck() + { + // When the download URL is not found there is no core upgrade path + if (!isset($this->updateInfo['object']->downloadurl->_data)) { + return false; + } + + $nextMinor = Version::MAJOR_VERSION . '.' . (Version::MINOR_VERSION + 1); + + // Show only when we found a download URL, we have an update and when we update to the next minor or greater. + return $this->updateInfo['hasUpdate'] + && version_compare($this->updateInfo['latest'], $nextMinor, '>='); + } } diff --git a/administrator/components/com_joomlaupdate/src/View/Update/HtmlView.php b/administrator/components/com_joomlaupdate/src/View/Update/HtmlView.php index 1d8471e4a825d..607f62460b7d5 100644 --- a/administrator/components/com_joomlaupdate/src/View/Update/HtmlView.php +++ b/administrator/components/com_joomlaupdate/src/View/Update/HtmlView.php @@ -1,4 +1,5 @@ input->set('hidemainmenu', true); + /** + * Renders the view. + * + * @param string $tpl Template name. + * + * @return void + */ + public function display($tpl = null) + { + Factory::getApplication()->input->set('hidemainmenu', true); - // Set the toolbar information. - ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_OVERVIEW'), 'sync install'); + // Set the toolbar information. + ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_OVERVIEW'), 'sync install'); - // Render the view. - parent::display($tpl); - } + // Render the view. + parent::display($tpl); + } } diff --git a/administrator/components/com_joomlaupdate/src/View/Upload/HtmlView.php b/administrator/components/com_joomlaupdate/src/View/Upload/HtmlView.php index f172fc7900273..3823a40c342e6 100644 --- a/administrator/components/com_joomlaupdate/src/View/Upload/HtmlView.php +++ b/administrator/components/com_joomlaupdate/src/View/Upload/HtmlView.php @@ -1,4 +1,5 @@ load('com_installer', JPATH_ADMINISTRATOR, 'en-GB', false, true); - $language->load('com_installer', JPATH_ADMINISTRATOR, null, true); + /** + * Renders the view. + * + * @param string $tpl Template name. + * + * @return void + * + * @since 3.6.0 + */ + public function display($tpl = null) + { + // Load com_installer's language + $language = Factory::getLanguage(); + $language->load('com_installer', JPATH_ADMINISTRATOR, 'en-GB', false, true); + $language->load('com_installer', JPATH_ADMINISTRATOR, null, true); - $this->updateInfo = $this->get('UpdateInformation'); - $this->selfUpdateAvailable = $this->get('CheckForSelfUpdate'); + $this->updateInfo = $this->get('UpdateInformation'); + $this->selfUpdateAvailable = $this->get('CheckForSelfUpdate'); - if ($this->getLayout() !== 'captive') - { - $this->warnings = $this->get('Items', 'warnings'); - } + if ($this->getLayout() !== 'captive') { + $this->warnings = $this->get('Items', 'warnings'); + } - $params = ComponentHelper::getParams('com_joomlaupdate'); - $this->noBackupCheck = $params->get('backupcheck', 1) == 0; + $params = ComponentHelper::getParams('com_joomlaupdate'); + $this->noBackupCheck = $params->get('backupcheck', 1) == 0; - $this->addToolbar(); + $this->addToolbar(); - // Render the view. - parent::display($tpl); - } + // Render the view. + parent::display($tpl); + } - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - // Set the toolbar information. - ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_OVERVIEW'), 'sync install'); + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + // Set the toolbar information. + ToolbarHelper::title(Text::_('COM_JOOMLAUPDATE_OVERVIEW'), 'sync install'); - $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; - ToolbarHelper::link('index.php?option=com_joomlaupdate&' . ($this->getLayout() == 'captive' ? 'view=upload' : ''), 'JTOOLBAR_BACK', $arrow); - ToolbarHelper::divider(); - ToolbarHelper::help('Joomla_Update'); - } + $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; + ToolbarHelper::link('index.php?option=com_joomlaupdate&' . ($this->getLayout() == 'captive' ? 'view=upload' : ''), 'JTOOLBAR_BACK', $arrow); + ToolbarHelper::divider(); + ToolbarHelper::help('Joomla_Update'); + } } diff --git a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/complete.php b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/complete.php index 01a8d4eb7fe1e..e168c2697e00c 100644 --- a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/complete.php +++ b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/complete.php @@ -1,4 +1,5 @@
-

-
-
- - -
-
+

+
+
+ + +
+
- - + +
diff --git a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/noupdate.php b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/noupdate.php index ec72b53384e73..8a16e3ca46d86 100644 --- a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/noupdate.php +++ b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/noupdate.php @@ -1,4 +1,5 @@ 'COM_JOOMLAUPDATE' . $this->messagePrefix, - 'content' => Text::sprintf($this->langKey, $this->updateSourceKey), - 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', - 'icon' => 'icon-loop joomlaupdate', - 'createURL' => 'index.php?option=com_joomlaupdate&task=update.purge&' . Session::getFormToken() . '=1' + 'textPrefix' => 'COM_JOOMLAUPDATE' . $this->messagePrefix, + 'content' => Text::sprintf($this->langKey, $this->updateSourceKey), + 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', + 'icon' => 'icon-loop joomlaupdate', + 'createURL' => 'index.php?option=com_joomlaupdate&task=update.purge&' . Session::getFormToken() . '=1' ]; -if (Factory::getApplication()->getIdentity()->authorise('core.admin', 'com_joomlaupdate')) -{ - $displayData['formAppend'] = ''; +if (Factory::getApplication()->getIdentity()->authorise('core.admin', 'com_joomlaupdate')) { + $displayData['formAppend'] = ''; } if (isset($this->updateInfo['object']) && isset($this->updateInfo['object']->get('infourl')->_data)) : - $displayData['content'] .= '
' . HTMLHelper::_('link', - $this->updateInfo['object']->get('infourl')->_data, - Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_INFOURL'), - [ - 'target' => '_blank', - 'rel' => 'noopener noreferrer', - 'title' => isset($this->updateInfo['object']->get('infourl')->title) ? Text::sprintf('JBROWSERTARGET_NEW_TITLE', $this->updateInfo['object']->get('infourl')->title) : '' - ] - ); + $displayData['content'] .= '
' . HTMLHelper::_( + 'link', + $this->updateInfo['object']->get('infourl')->_data, + Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_INFOURL'), + [ + 'target' => '_blank', + 'rel' => 'noopener noreferrer', + 'title' => isset($this->updateInfo['object']->get('infourl')->title) ? Text::sprintf('JBROWSERTARGET_NEW_TITLE', $this->updateInfo['object']->get('infourl')->title) : '' + ] + ); endif; $content = LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/preupdatecheck.php b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/preupdatecheck.php index c64638bf382a7..58da0e0224a2c 100644 --- a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/preupdatecheck.php +++ b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/preupdatecheck.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('core') - ->useScript('com_joomlaupdate.default') - ->useScript('bootstrap.popover') - ->useScript('bootstrap.tab'); + ->useScript('com_joomlaupdate.default') + ->useScript('bootstrap.popover') + ->useScript('bootstrap.tab'); // Text::script doesn't have a sprintf equivalent so work around this $this->document->addScriptOptions('nonCoreCriticalPlugins', $this->nonCoreCriticalPlugins); @@ -50,36 +51,36 @@ Text::script('JLIB_JS_AJAX_ERROR_TIMEOUT'); $compatibilityTypes = array( - 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_RUNNING_PRE_UPDATE_CHECKS' => array( - 'class' => 'info', - 'icon' => 'hourglass fa-spin', - 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_RUNNING_PRE_UPDATE_CHECKS_NOTES', - 'group' => 0, - ), - 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_REQUIRING_UPDATES_TO_BE_COMPATIBLE' => array( - 'class' => 'danger', - 'icon' => 'times', - 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_REQUIRING_UPDATES_TO_BE_COMPATIBLE_NOTES', - 'group' => 2, - ), - 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PRE_UPDATE_CHECKS_FAILED' => array( - 'class' => 'warning', - 'icon' => 'exclamation-triangle', - 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PRE_UPDATE_CHECKS_FAILED_NOTES', - 'group' => 4, - ), - 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_UPDATE_SERVER_OFFERS_NO_COMPATIBLE_VERSION' => array( - 'class' => 'warning', - 'icon' => 'exclamation-triangle', - 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_UPDATE_SERVER_OFFERS_NO_COMPATIBLE_VERSION_NOTES', - 'group' => 1, - ), - 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PROBABLY_COMPATIBLE' => array( - 'class' => 'success', - 'icon' => 'check', - 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PROBABLY_COMPATIBLE_NOTES', - 'group' => 3, - ), + 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_RUNNING_PRE_UPDATE_CHECKS' => array( + 'class' => 'info', + 'icon' => 'hourglass fa-spin', + 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_RUNNING_PRE_UPDATE_CHECKS_NOTES', + 'group' => 0, + ), + 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_REQUIRING_UPDATES_TO_BE_COMPATIBLE' => array( + 'class' => 'danger', + 'icon' => 'times', + 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_REQUIRING_UPDATES_TO_BE_COMPATIBLE_NOTES', + 'group' => 2, + ), + 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PRE_UPDATE_CHECKS_FAILED' => array( + 'class' => 'warning', + 'icon' => 'exclamation-triangle', + 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PRE_UPDATE_CHECKS_FAILED_NOTES', + 'group' => 4, + ), + 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_UPDATE_SERVER_OFFERS_NO_COMPATIBLE_VERSION' => array( + 'class' => 'warning', + 'icon' => 'exclamation-triangle', + 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_UPDATE_SERVER_OFFERS_NO_COMPATIBLE_VERSION_NOTES', + 'group' => 1, + ), + 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PROBABLY_COMPATIBLE' => array( + 'class' => 'success', + 'icon' => 'check', + 'notes' => 'COM_JOOMLAUPDATE_VIEW_DEFAULT_EXTENSIONS_PROBABLY_COMPATIBLE_NOTES', + 'group' => 3, + ), ); $latestJoomlaVersion = $this->updateInfo['latest']; @@ -87,286 +88,284 @@ $updatePossible = true; -if (version_compare($this->updateInfo['latest'], Version::MAJOR_VERSION + 1, '>=') && $this->isDefaultBackendTemplate === false) -{ - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'COM_JOOMLAUPDATE_VIEW_DEFAULT_NON_CORE_BACKEND_TEMPLATE_USED_NOTICE', - ucfirst($this->defaultBackendTemplate) - ), - 'info' - ); +if (version_compare($this->updateInfo['latest'], Version::MAJOR_VERSION + 1, '>=') && $this->isDefaultBackendTemplate === false) { + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'COM_JOOMLAUPDATE_VIEW_DEFAULT_NON_CORE_BACKEND_TEMPLATE_USED_NOTICE', + ucfirst($this->defaultBackendTemplate) + ), + 'info' + ); } ?>
-

- updateInfo['latest']); ?> -

-

- -

- -
- +

+ updateInfo['latest']); ?> +

+

+ +

-
-
-

- -

-
- - - - - - - - - - phpOptions as $option) : ?> - - - - - - -
- -
- - - -
- label; ?> - notice) : ?> -
- notice; ?> -
- -
- - state ? 'JYES' : 'JNO'); ?> - -
-
-
- -
-

- -

-
-
-

- -

-
-
- -
-
-
-
+
+ - +
+
+

+ +

+
+ + + + + + + + + + phpOptions as $option) : ?> + + + + + + +
+ +
+ + + +
+ label; ?> + notice) : ?> +
+ notice; ?> +
+ +
+ + state ? 'JYES' : 'JNO'); ?> + +
+
+
+ +
+

+ +

+
+
+

+ +

+
+
+ +
+
+
+
- nonCoreExtensions)) : ?> -
- $data) : ?> -
-

- - - 0) : ?> - - -

+ -
- - - - - - - - - - - - - - - nonCoreExtensions as $extension) : ?> - - - - - - - - - - -
- -
- - - -
- name; ?> - - type)); ?> -
-
-
- -
- -
- - -
- -
-
-
+ nonCoreExtensions)) : ?> +
+ $data) : ?> +
+

+ + + 0) : ?> + + +

- +
+ + + + + + + + + + + + + + + nonCoreExtensions as $extension) : ?> + + + + + + + + + + +
+ +
+ + + +
+ name; ?> + + type)); ?> +
+
+
+ +
+ +
+ + +
+ +
+
+
-
+ + - noVersionCheck): ?> -
-
- - -
-
- + noVersionCheck) : ?> +
+
+ + +
+
+ - -
- + + + -
- - -
+
+ + +
- authorise('core.admin')) : ?> -
- - - -
- + authorise('core.admin')) : ?> +
+ + + +
+
diff --git a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/reinstall.php b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/reinstall.php index e6e93402d4ec2..57a5ed36dc833 100644 --- a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/reinstall.php +++ b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/reinstall.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('core') - ->useScript('com_joomlaupdate.default') - ->useScript('bootstrap.popover'); + ->useScript('com_joomlaupdate.default') + ->useScript('bootstrap.popover'); $uploadLink = 'index.php?option=com_joomlaupdate&view=upload'; $displayData = [ - 'textPrefix' => 'COM_JOOMLAUPDATE_REINSTALL', - 'content' => Text::sprintf($this->langKey, $this->updateSourceKey), - 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', - 'icon' => 'icon-loop joomlaupdate', - 'createURL' => '#' + 'textPrefix' => 'COM_JOOMLAUPDATE_REINSTALL', + 'content' => Text::sprintf($this->langKey, $this->updateSourceKey), + 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', + 'icon' => 'icon-loop joomlaupdate', + 'createURL' => '#' ]; if (isset($this->updateInfo['object']) && isset($this->updateInfo['object']->get('infourl')->_data)) : - $displayData['content'] .= '
' . HTMLHelper::_('link', - $this->updateInfo['object']->get('infourl')->_data, - Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_INFOURL'), - [ - 'target' => '_blank', - 'rel' => 'noopener noreferrer', - 'title' => isset($this->updateInfo['object']->get('infourl')->title) ? Text::sprintf('JBROWSERTARGET_NEW_TITLE', $this->updateInfo['object']->get('infourl')->title) : '' - ] - ); + $displayData['content'] .= '
' . HTMLHelper::_( + 'link', + $this->updateInfo['object']->get('infourl')->_data, + Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_INFOURL'), + [ + 'target' => '_blank', + 'rel' => 'noopener noreferrer', + 'title' => isset($this->updateInfo['object']->get('infourl')->title) ? Text::sprintf('JBROWSERTARGET_NEW_TITLE', $this->updateInfo['object']->get('infourl')->title) : '' + ] + ); endif; if (Factory::getApplication()->getIdentity()->authorise('core.admin', 'com_joomlaupdate')) : - $displayData['formAppend'] = ''; + $displayData['formAppend'] = ''; endif; echo '
'; diff --git a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/selfupdate.php b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/selfupdate.php index 0ca24eb8efa62..52044361cb0b2 100644 --- a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/selfupdate.php +++ b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/selfupdate.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Layout\LayoutHelper; $displayData = [ - 'textPrefix' => 'COM_JOOMLAUPDATE_SELF', - 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', - 'icon' => 'icon-loop joomlaupdate', - 'createURL' => 'index.php?option=com_installer&view=update' + 'textPrefix' => 'COM_JOOMLAUPDATE_SELF', + 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', + 'icon' => 'icon-loop joomlaupdate', + 'createURL' => 'index.php?option=com_installer&view=update' ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/update.php b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/update.php index 258fdc97cc655..1ee49824663b7 100644 --- a/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/update.php +++ b/administrator/components/com_joomlaupdate/tmpl/joomlaupdate/update.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('core') - ->useScript('com_joomlaupdate.default') - ->useScript('bootstrap.popover'); + ->useScript('com_joomlaupdate.default') + ->useScript('bootstrap.popover'); $uploadLink = 'index.php?option=com_joomlaupdate&view=upload'; $displayData = [ - 'textPrefix' => 'COM_JOOMLAUPDATE_UPDATE', - 'title' => Text::sprintf('COM_JOOMLAUPDATE_UPDATE_EMPTYSTATE_TITLE', $this->escape($this->updateInfo['latest'])), - 'content' => Text::sprintf($this->langKey, $this->updateSourceKey), - 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', - 'icon' => 'icon-loop joomlaupdate', - 'createURL' => '#' + 'textPrefix' => 'COM_JOOMLAUPDATE_UPDATE', + 'title' => Text::sprintf('COM_JOOMLAUPDATE_UPDATE_EMPTYSTATE_TITLE', $this->escape($this->updateInfo['latest'])), + 'content' => Text::sprintf($this->langKey, $this->updateSourceKey), + 'formURL' => 'index.php?option=com_joomlaupdate&view=joomlaupdate', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Updating_from_an_existing_version', + 'icon' => 'icon-loop joomlaupdate', + 'createURL' => '#' ]; if (isset($this->updateInfo['object']) && isset($this->updateInfo['object']->get('infourl')->_data)) : - $displayData['content'] .= '
' . HTMLHelper::_('link', - $this->updateInfo['object']->get('infourl')->_data, - Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_INFOURL'), - [ - 'target' => '_blank', - 'rel' => 'noopener noreferrer', - 'title' => isset($this->updateInfo['object']->get('infourl')->title) ? Text::sprintf('JBROWSERTARGET_NEW_TITLE', $this->updateInfo['object']->get('infourl')->title) : '' - ] - ); + $displayData['content'] .= '
' . HTMLHelper::_( + 'link', + $this->updateInfo['object']->get('infourl')->_data, + Text::_('COM_JOOMLAUPDATE_VIEW_DEFAULT_INFOURL'), + [ + 'target' => '_blank', + 'rel' => 'noopener noreferrer', + 'title' => isset($this->updateInfo['object']->get('infourl')->title) ? Text::sprintf('JBROWSERTARGET_NEW_TITLE', $this->updateInfo['object']->get('infourl')->title) : '' + ] + ); endif; // Confirm backup and check @@ -57,7 +59,7 @@
'; if (Factory::getApplication()->getIdentity()->authorise('core.admin', 'com_joomlaupdate')) : - $displayData['formAppend'] = ''; + $displayData['formAppend'] = ''; endif; echo '
'; diff --git a/administrator/components/com_joomlaupdate/tmpl/update/default.php b/administrator/components/com_joomlaupdate/tmpl/update/default.php index ff76166341672..0375bd95a1da6 100644 --- a/administrator/components/com_joomlaupdate/tmpl/update/default.php +++ b/administrator/components/com_joomlaupdate/tmpl/update/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('core') - ->useScript('com_joomlaupdate.admin-update') - ->useScript('bootstrap.modal'); + ->useScript('com_joomlaupdate.admin-update') + ->useScript('bootstrap.modal'); Text::script('COM_JOOMLAUPDATE_ERRORMODAL_HEAD_FORBIDDEN'); Text::script('COM_JOOMLAUPDATE_ERRORMODAL_BODY_FORBIDDEN'); @@ -45,90 +46,90 @@ $returnUrl = 'index.php?option=com_joomlaupdate&task=update.finalise&' . Factory::getSession()->getFormToken() . '=1'; $this->document->addScriptOptions( - 'joomlaupdate', - [ - 'password' => $password, - 'totalsize' => $filesize, - 'ajax_url' => $ajaxUrl, - 'return_url' => $returnUrl, - ] + 'joomlaupdate', + [ + 'password' => $password, + 'totalsize' => $filesize, + 'ajax_url' => $ajaxUrl, + 'return_url' => $returnUrl, + ] ); $helpUrl = Help::createUrl('JHELP_COMPONENTS_JOOMLA_UPDATE', false); ?>
- -

-
-

- -

-
-
-
-
-
-
-
- - - -
-
- - - -
-
- - - -
-
-
-
+ +

+
+

+ +

+
+
+
+
+
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
-
-

-
-
-
- -
+
+

+
+
+
+ +
diff --git a/administrator/components/com_joomlaupdate/tmpl/update/finaliseconfirm.php b/administrator/components/com_joomlaupdate/tmpl/update/finaliseconfirm.php index 32cf3dde1cb68..261863f052dbc 100644 --- a/administrator/components/com_joomlaupdate/tmpl/update/finaliseconfirm.php +++ b/administrator/components/com_joomlaupdate/tmpl/update/finaliseconfirm.php @@ -1,4 +1,5 @@
-

- -

-

- get('sitename')); ?> -

+

+ +

+

+ get('sitename')); ?> +


-
- -
-
-
- - - - - -
-
-
-
-
-
- - - - - -
-
-
-
-
-
- - - - -
-
-
+
+ +
+
+
+ + + + + +
+
+
+
+
+
+ + + + + +
+
+
+
+
+
+ + + + +
+
+
- - - -
+ + + +
diff --git a/administrator/components/com_joomlaupdate/tmpl/upload/captive.php b/administrator/components/com_joomlaupdate/tmpl/upload/captive.php index b3c030d177478..15de15ee2f445 100644 --- a/administrator/components/com_joomlaupdate/tmpl/upload/captive.php +++ b/administrator/components/com_joomlaupdate/tmpl/upload/captive.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('core') - ->useScript('jquery') - ->useScript('form.validate') - ->useScript('keepalive') - ->useScript('field.passwordview'); + ->useScript('jquery') + ->useScript('form.validate') + ->useScript('keepalive') + ->useScript('field.passwordview'); Text::script('JSHOWPASSWORD'); Text::script('JHIDEPASSWORD'); ?>
-

- -

-

- get('sitename')); ?> -

+

+ +

+

+ get('sitename')); ?> +


-
- -
-
-
- - - - - -
-
-
-
-
-
- - -
-
-
-
-
- - - - -
-
+
+ +
+
+
+ + + + + +
+
+
+
+
+
+ + +
+
+
+
+
+ + + + +
+
- - - -
+ + + +
diff --git a/administrator/components/com_joomlaupdate/tmpl/upload/default.php b/administrator/components/com_joomlaupdate/tmpl/upload/default.php index c0f7837f3ffb2..319c22d2a7c6e 100644 --- a/administrator/components/com_joomlaupdate/tmpl/upload/default.php +++ b/administrator/components/com_joomlaupdate/tmpl/upload/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('core') - ->useScript('com_joomlaupdate.default') - ->useScript('bootstrap.popover'); + ->useScript('com_joomlaupdate.default') + ->useScript('bootstrap.popover'); Text::script('COM_INSTALLER_MSG_INSTALL_PLEASE_SELECT_A_PACKAGE', true); Text::script('COM_INSTALLER_MSG_WARNINGS_UPLOADFILETOOBIG', true); @@ -33,69 +34,69 @@
- - - updateInfo['object']) && ($this->updateInfo['object'] instanceof Update)) : ?> -

- - updateInfo['object']->downloadurl->_data); ?> - + + + updateInfo['object']) && ($this->updateInfo['object'] instanceof Update)) : ?> +

+ + updateInfo['object']->downloadurl->_data); ?> +
warnings)) : ?> -

- warnings as $warning) : ?> -
-

- - - -

-

-
- -
-

- - - -

-

-
+

+ warnings as $warning) : ?> +
+

+ + + +

+

+
+ +
+

+ + + +

+

+
-
- - - - - - -
- - -
- -
- noBackupCheck ? 'checked' : '' ?>> - -
- - - - - - +
+ + + + + + +
+ + +
+ +
+ noBackupCheck ? 'checked' : '' ?>> + +
+ + + + + +
diff --git a/administrator/components/com_languages/services/provider.php b/administrator/components/com_languages/services/provider.php index c405c69c847df..b47d560cb2285 100644 --- a/administrator/components/com_languages/services/provider.php +++ b/administrator/components/com_languages/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Languages')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Languages')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Languages')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Languages')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new LanguagesComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new LanguagesComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_languages/src/Controller/DisplayController.php b/administrator/components/com_languages/src/Controller/DisplayController.php index 87d538a3ad59d..c22fc13a59986 100644 --- a/administrator/components/com_languages/src/Controller/DisplayController.php +++ b/administrator/components/com_languages/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', $this->default_view); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); - - // Check for edit form. - if ($view == 'language' && $layout == 'edit' && !$this->checkEditId('com_languages.edit.language', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_languages&view=languages', false)); - - return false; - } - - return parent::display(); - } + /** + * @var string The default view. + * @since 1.6 + */ + protected $default_view = 'installed'; + + /** + * Method to display a view. + * + * @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()}. + * + * @return static|boolean This object to support chaining or false on failure. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $view = $this->input->get('view', $this->default_view); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); + + // Check for edit form. + if ($view == 'language' && $layout == 'edit' && !$this->checkEditId('com_languages.edit.language', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_languages&view=languages', false)); + + return false; + } + + return parent::display(); + } } diff --git a/administrator/components/com_languages/src/Controller/InstalledController.php b/administrator/components/com_languages/src/Controller/InstalledController.php index 67b26265b7bfb..994ae67b6dfa9 100644 --- a/administrator/components/com_languages/src/Controller/InstalledController.php +++ b/administrator/components/com_languages/src/Controller/InstalledController.php @@ -1,4 +1,5 @@ checkToken(); - - $cid = (string) $this->input->get('cid', '', 'string'); - $model = $this->getModel('installed'); - - if ($model->publish($cid)) - { - // Switching to the new administrator language for the message - if ($model->getState('client_id') == 1) - { - $language = Factory::getLanguage(); - $newLang = Language::getInstance($cid); - Factory::$language = $newLang; - Factory::getApplication()->loadLanguage($language = $newLang); - $newLang->load('com_languages', JPATH_ADMINISTRATOR); - } - - if (Multilanguage::isEnabled() && $model->getState('client_id') == 0) - { - $msg = Text::_('COM_LANGUAGES_MSG_DEFAULT_MULTILANG_SAVED'); - $type = 'message'; - } - else - { - $msg = Text::_('COM_LANGUAGES_MSG_DEFAULT_LANGUAGE_SAVED'); - $type = 'message'; - } - } - else - { - $msg = $model->getError(); - $type = 'error'; - } - - $clientId = $model->getState('client_id'); - $this->setRedirect('index.php?option=com_languages&view=installed&client=' . $clientId, $msg, $type); - } - - /** - * Task to switch the administrator language. - * - * @return void - */ - public function switchAdminLanguage() - { - // Check for request forgeries. - $this->checkToken(); - - $cid = (string) $this->input->get('cid', '', 'string'); - $model = $this->getModel('installed'); - - // Fetching the language name from the langmetadata.xml or xx-XX.xml respectively. - $file = JPATH_ADMINISTRATOR . '/language/' . $cid . '/langmetadata.xml'; - - if (!is_file($file)) - { - $file = JPATH_ADMINISTRATOR . '/language/' . $cid . '/' . $cid . '.xml'; - } - - $info = LanguageHelper::parseXMLLanguageFile($file); - - if ($model->switchAdminLanguage($cid)) - { - // Switching to the new language for the message - $languageName = $info['nativeName']; - $language = Factory::getLanguage(); - $newLang = Language::getInstance($cid); - Factory::$language = $newLang; - Factory::getApplication()->loadLanguage($language = $newLang); - $newLang->load('com_languages', JPATH_ADMINISTRATOR); - - $msg = Text::sprintf('COM_LANGUAGES_MSG_SWITCH_ADMIN_LANGUAGE_SUCCESS', $languageName); - $type = 'message'; - } - else - { - $msg = $model->getError(); - $type = 'error'; - } - - $this->setRedirect('index.php?option=com_languages&view=installed', $msg, $type); - } + /** + * Task to set the default language. + * + * @return void + */ + public function setDefault() + { + // Check for request forgeries. + $this->checkToken(); + + $cid = (string) $this->input->get('cid', '', 'string'); + $model = $this->getModel('installed'); + + if ($model->publish($cid)) { + // Switching to the new administrator language for the message + if ($model->getState('client_id') == 1) { + $language = Factory::getLanguage(); + $newLang = Language::getInstance($cid); + Factory::$language = $newLang; + Factory::getApplication()->loadLanguage($language = $newLang); + $newLang->load('com_languages', JPATH_ADMINISTRATOR); + } + + if (Multilanguage::isEnabled() && $model->getState('client_id') == 0) { + $msg = Text::_('COM_LANGUAGES_MSG_DEFAULT_MULTILANG_SAVED'); + $type = 'message'; + } else { + $msg = Text::_('COM_LANGUAGES_MSG_DEFAULT_LANGUAGE_SAVED'); + $type = 'message'; + } + } else { + $msg = $model->getError(); + $type = 'error'; + } + + $clientId = $model->getState('client_id'); + $this->setRedirect('index.php?option=com_languages&view=installed&client=' . $clientId, $msg, $type); + } + + /** + * Task to switch the administrator language. + * + * @return void + */ + public function switchAdminLanguage() + { + // Check for request forgeries. + $this->checkToken(); + + $cid = (string) $this->input->get('cid', '', 'string'); + $model = $this->getModel('installed'); + + // Fetching the language name from the langmetadata.xml or xx-XX.xml respectively. + $file = JPATH_ADMINISTRATOR . '/language/' . $cid . '/langmetadata.xml'; + + if (!is_file($file)) { + $file = JPATH_ADMINISTRATOR . '/language/' . $cid . '/' . $cid . '.xml'; + } + + $info = LanguageHelper::parseXMLLanguageFile($file); + + if ($model->switchAdminLanguage($cid)) { + // Switching to the new language for the message + $languageName = $info['nativeName']; + $language = Factory::getLanguage(); + $newLang = Language::getInstance($cid); + Factory::$language = $newLang; + Factory::getApplication()->loadLanguage($language = $newLang); + $newLang->load('com_languages', JPATH_ADMINISTRATOR); + + $msg = Text::sprintf('COM_LANGUAGES_MSG_SWITCH_ADMIN_LANGUAGE_SUCCESS', $languageName); + $type = 'message'; + } else { + $msg = $model->getError(); + $type = 'error'; + } + + $this->setRedirect('index.php?option=com_languages&view=installed', $msg, $type); + } } diff --git a/administrator/components/com_languages/src/Controller/LanguageController.php b/administrator/components/com_languages/src/Controller/LanguageController.php index fadee0f8b483d..fb99bcd2e833a 100644 --- a/administrator/components/com_languages/src/Controller/LanguageController.php +++ b/administrator/components/com_languages/src/Controller/LanguageController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * 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 object The model. + * + * @since 1.6 + */ + public function getModel($name = 'Language', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_languages/src/Controller/OverrideController.php b/administrator/components/com_languages/src/Controller/OverrideController.php index 7f29c944d53f9..a6980f3df8899 100644 --- a/administrator/components/com_languages/src/Controller/OverrideController.php +++ b/administrator/components/com_languages/src/Controller/OverrideController.php @@ -1,4 +1,5 @@ app->allowCache(false); - - $cid = (array) $this->input->post->get('cid', array(), 'string'); - $context = "$this->option.edit.$this->context"; - - // Get the constant name. - $recordId = (count($cid) ? $cid[0] : $this->input->get('id')); - - // Access check. - if (!$this->allowEdit()) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); - - return; - } - - $this->app->setUserState($context . '.data', null); - $this->setRedirect('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, 'id')); - } - - /** - * Method to save an override. - * - * @param string $key The name of the primary key of the URL variable (not used here). - * @param string $urlVar The name of the URL variable if different from the primary key (not used here). - * - * @return void - * - * @since 2.5 - */ - public function save($key = null, $urlVar = null) - { - // Check for request forgeries. - $this->checkToken(); - - $app = $this->app; - $model = $this->getModel(); - $data = $this->input->post->get('jform', array(), 'array'); - $context = "$this->option.edit.$this->context"; - $task = $this->getTask(); - - $recordId = $this->input->get('id'); - $data['id'] = $recordId; - - // Access check. - if (!$this->allowSave($data, 'id')) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); - - return; - } - - // Validate the posted data. - $form = $model->getForm($data, false); - - if (!$form) - { - $app->enqueueMessage($model->getError(), 'error'); - - return; - } - - // Test whether the data is valid. - $validData = $model->validate($form, $data); - - // Check for validation errors. - if ($validData === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Save the data in the session. - $app->setUserState($context . '.data', $data); - - // Redirect back to the edit screen. - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, 'id'), false) - ); - - return; - } - - // Attempt to save the data. - if (!$model->save($validData)) - { - // Save the data in the session. - $app->setUserState($context . '.data', $validData); - - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, 'id'), false) - ); - - return; - } - - // Add message of success. - $this->setMessage(Text::_('COM_LANGUAGES_VIEW_OVERRIDE_SAVE_SUCCESS')); - - // Redirect the user and adjust session state based on the chosen task. - switch ($task) - { - case 'apply': - // Set the record data in the session. - $app->setUserState($context . '.data', null); - - // Redirect back to the edit screen - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($validData['key'], 'id'), false) - ); - break; - - case 'save2new': - // Clear the record id and data from the session. - $app->setUserState($context . '.data', null); - - // Redirect back to the edit screen - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend(null, 'id'), false) - ); - break; - - default: - // Clear the record id and data from the session. - $app->setUserState($context . '.data', null); - - // Redirect to the list screen. - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); - break; - } - } - - /** - * Method to cancel an edit. - * - * @param string $key The name of the primary key of the URL variable (not used here). - * - * @return void - * - * @since 2.5 - */ - public function cancel($key = null) - { - $this->checkToken(); - - $context = "$this->option.edit.$this->context"; - - $this->app->setUserState($context . '.data', null); - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); - } + /** + * Method to edit an existing override. + * + * @param string $key The name of the primary key of the URL variable (not used here). + * @param string $urlVar The name of the URL variable if different from the primary key (not used here). + * + * @return void + * + * @since 2.5 + */ + public function edit($key = null, $urlVar = null) + { + // Do not cache the response to this, its a redirect + $this->app->allowCache(false); + + $cid = (array) $this->input->post->get('cid', array(), 'string'); + $context = "$this->option.edit.$this->context"; + + // Get the constant name. + $recordId = (count($cid) ? $cid[0] : $this->input->get('id')); + + // Access check. + if (!$this->allowEdit()) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); + + return; + } + + $this->app->setUserState($context . '.data', null); + $this->setRedirect('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, 'id')); + } + + /** + * Method to save an override. + * + * @param string $key The name of the primary key of the URL variable (not used here). + * @param string $urlVar The name of the URL variable if different from the primary key (not used here). + * + * @return void + * + * @since 2.5 + */ + public function save($key = null, $urlVar = null) + { + // Check for request forgeries. + $this->checkToken(); + + $app = $this->app; + $model = $this->getModel(); + $data = $this->input->post->get('jform', array(), 'array'); + $context = "$this->option.edit.$this->context"; + $task = $this->getTask(); + + $recordId = $this->input->get('id'); + $data['id'] = $recordId; + + // Access check. + if (!$this->allowSave($data, 'id')) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); + + return; + } + + // Validate the posted data. + $form = $model->getForm($data, false); + + if (!$form) { + $app->enqueueMessage($model->getError(), 'error'); + + return; + } + + // Test whether the data is valid. + $validData = $model->validate($form, $data); + + // Check for validation errors. + if ($validData === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Save the data in the session. + $app->setUserState($context . '.data', $data); + + // Redirect back to the edit screen. + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, 'id'), false) + ); + + return; + } + + // Attempt to save the data. + if (!$model->save($validData)) { + // Save the data in the session. + $app->setUserState($context . '.data', $validData); + + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId, 'id'), false) + ); + + return; + } + + // Add message of success. + $this->setMessage(Text::_('COM_LANGUAGES_VIEW_OVERRIDE_SAVE_SUCCESS')); + + // Redirect the user and adjust session state based on the chosen task. + switch ($task) { + case 'apply': + // Set the record data in the session. + $app->setUserState($context . '.data', null); + + // Redirect back to the edit screen + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($validData['key'], 'id'), false) + ); + break; + + case 'save2new': + // Clear the record id and data from the session. + $app->setUserState($context . '.data', null); + + // Redirect back to the edit screen + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend(null, 'id'), false) + ); + break; + + default: + // Clear the record id and data from the session. + $app->setUserState($context . '.data', null); + + // Redirect to the list screen. + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); + break; + } + } + + /** + * Method to cancel an edit. + * + * @param string $key The name of the primary key of the URL variable (not used here). + * + * @return void + * + * @since 2.5 + */ + public function cancel($key = null) + { + $this->checkToken(); + + $context = "$this->option.edit.$this->context"; + + $this->app->setUserState($context . '.data', null); + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false)); + } } diff --git a/administrator/components/com_languages/src/Controller/OverridesController.php b/administrator/components/com_languages/src/Controller/OverridesController.php index a3edb544ffc74..6e5e839677db9 100644 --- a/administrator/components/com_languages/src/Controller/OverridesController.php +++ b/administrator/components/com_languages/src/Controller/OverridesController.php @@ -1,4 +1,5 @@ checkToken(); + /** + * Method for deleting one or more overrides. + * + * @return void + * + * @since 2.5 + */ + public function delete() + { + // Check for request forgeries. + $this->checkToken(); - // Get items to delete from the request. - $cid = (array) $this->input->get('cid', array(), 'string'); + // Get items to delete from the request. + $cid = (array) $this->input->get('cid', array(), 'string'); - // Remove zero values resulting from input filter - $cid = array_filter($cid); + // Remove zero values resulting from input filter + $cid = array_filter($cid); - if (empty($cid)) - { - $this->setMessage(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), 'warning'); - } - else - { - // Get the model. - $model = $this->getModel('overrides'); + if (empty($cid)) { + $this->setMessage(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), 'warning'); + } else { + // Get the model. + $model = $this->getModel('overrides'); - // Remove the items. - if ($model->delete($cid)) - { - $this->setMessage(Text::plural($this->text_prefix . '_N_ITEMS_DELETED', count($cid))); - } - else - { - $this->setMessage($model->getError(), 'error'); - } - } + // Remove the items. + if ($model->delete($cid)) { + $this->setMessage(Text::plural($this->text_prefix . '_N_ITEMS_DELETED', count($cid))); + } else { + $this->setMessage($model->getError(), 'error'); + } + } - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false)); - } + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_list, false)); + } - /** - * Method to purge the overrider table. - * - * @return void - * - * @since 3.4.2 - */ - public function purge() - { - // Check for request forgeries. - $this->checkToken(); + /** + * Method to purge the overrider table. + * + * @return void + * + * @since 3.4.2 + */ + public function purge() + { + // Check for request forgeries. + $this->checkToken(); - /** @var \Joomla\Component\Languages\Administrator\Model\OverridesModel $model */ - $model = $this->getModel('overrides'); - $model->purge(); - $this->setRedirect(Route::_('index.php?option=com_languages&view=overrides', false)); - } + /** @var \Joomla\Component\Languages\Administrator\Model\OverridesModel $model */ + $model = $this->getModel('overrides'); + $model->purge(); + $this->setRedirect(Route::_('index.php?option=com_languages&view=overrides', false)); + } } diff --git a/administrator/components/com_languages/src/Controller/StringsController.php b/administrator/components/com_languages/src/Controller/StringsController.php index 36de315c64218..c44c1d9e8d3e4 100644 --- a/administrator/components/com_languages/src/Controller/StringsController.php +++ b/administrator/components/com_languages/src/Controller/StringsController.php @@ -1,4 +1,5 @@ getModel('strings')->refresh()); - } + /** + * Method for refreshing the cache in the database with the known language strings + * + * @return void + * + * @since 2.5 + */ + public function refresh() + { + echo new JsonResponse($this->getModel('strings')->refresh()); + } - /** - * Method for searching language strings - * - * @return void - * - * @since 2.5 - */ - public function search() - { - echo new JsonResponse($this->getModel('strings')->search()); - } + /** + * Method for searching language strings + * + * @return void + * + * @since 2.5 + */ + public function search() + { + echo new JsonResponse($this->getModel('strings')->search()); + } } diff --git a/administrator/components/com_languages/src/Extension/LanguagesComponent.php b/administrator/components/com_languages/src/Extension/LanguagesComponent.php index 0621719ae1307..9b5218ea4a3c9 100644 --- a/administrator/components/com_languages/src/Extension/LanguagesComponent.php +++ b/administrator/components/com_languages/src/Extension/LanguagesComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('languages', new Languages); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('languages', new Languages()); + } } diff --git a/administrator/components/com_languages/src/Field/LanguageclientField.php b/administrator/components/com_languages/src/Field/LanguageclientField.php index 612f49be02413..2ddda6dfeb9e8 100644 --- a/administrator/components/com_languages/src/Field/LanguageclientField.php +++ b/administrator/components/com_languages/src/Field/LanguageclientField.php @@ -1,4 +1,5 @@ cache)) - { - return $this->cache; - } + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.9.0 + */ + protected function getOptions() + { + // Try to load the data from our mini-cache. + if (!empty($this->cache)) { + return $this->cache; + } - // Get all languages of frontend and backend. - $languages = array(); - $site_languages = LanguageHelper::getKnownLanguages(JPATH_SITE); - $admin_languages = LanguageHelper::getKnownLanguages(JPATH_ADMINISTRATOR); + // Get all languages of frontend and backend. + $languages = array(); + $site_languages = LanguageHelper::getKnownLanguages(JPATH_SITE); + $admin_languages = LanguageHelper::getKnownLanguages(JPATH_ADMINISTRATOR); - // Create a single array of them. - foreach ($site_languages as $tag => $language) - { - $languages[$tag . '0'] = Text::sprintf('COM_LANGUAGES_VIEW_OVERRIDES_LANGUAGES_BOX_ITEM', $language['name'], Text::_('JSITE')); - } + // Create a single array of them. + foreach ($site_languages as $tag => $language) { + $languages[$tag . '0'] = Text::sprintf('COM_LANGUAGES_VIEW_OVERRIDES_LANGUAGES_BOX_ITEM', $language['name'], Text::_('JSITE')); + } - foreach ($admin_languages as $tag => $language) - { - $languages[$tag . '1'] = Text::sprintf('COM_LANGUAGES_VIEW_OVERRIDES_LANGUAGES_BOX_ITEM', $language['name'], Text::_('JADMINISTRATOR')); - } + foreach ($admin_languages as $tag => $language) { + $languages[$tag . '1'] = Text::sprintf('COM_LANGUAGES_VIEW_OVERRIDES_LANGUAGES_BOX_ITEM', $language['name'], Text::_('JADMINISTRATOR')); + } - // Sort it by language tag and by client after that. - ksort($languages); + // Sort it by language tag and by client after that. + ksort($languages); - // Add the languages to the internal cache. - $this->cache = array_merge(parent::getOptions(), $languages); + // Add the languages to the internal cache. + $this->cache = array_merge(parent::getOptions(), $languages); - return $this->cache; - } + return $this->cache; + } } diff --git a/administrator/components/com_languages/src/Helper/LanguagesHelper.php b/administrator/components/com_languages/src/Helper/LanguagesHelper.php index acf03db636b8e..3e0f43c94e970 100644 --- a/administrator/components/com_languages/src/Helper/LanguagesHelper.php +++ b/administrator/components/com_languages/src/Helper/LanguagesHelper.php @@ -1,4 +1,5 @@ clean($value, 'cmd')); - } + return strtoupper($filter->clean($value, 'cmd')); + } - /** - * Filter method for language strings. - * This method will be called by \JForm while filtering the form data. - * - * @param string $value The language string to filter. - * - * @return string The filtered language string. - * - * @since 2.5 - */ - public static function filterText($value) - { - $filter = InputFilter::getInstance([], [], InputFilter::ONLY_BLOCK_DEFINED_TAGS, InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES); + /** + * Filter method for language strings. + * This method will be called by \JForm while filtering the form data. + * + * @param string $value The language string to filter. + * + * @return string The filtered language string. + * + * @since 2.5 + */ + public static function filterText($value) + { + $filter = InputFilter::getInstance([], [], InputFilter::ONLY_BLOCK_DEFINED_TAGS, InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES); - return $filter->clean($value); - } + return $filter->clean($value); + } } diff --git a/administrator/components/com_languages/src/Helper/MultilangstatusHelper.php b/administrator/components/com_languages/src/Helper/MultilangstatusHelper.php index ba2e534f7d088..8884f44bb9755 100644 --- a/administrator/components/com_languages/src/Helper/MultilangstatusHelper.php +++ b/administrator/components/com_languages/src/Helper/MultilangstatusHelper.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName('#__menu')) - ->where( - [ - $db->quoteName('home') . ' = 1', - $db->quoteName('published') . ' = 1', - $db->quoteName('client_id') . ' = 0', - ] - ); - - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Method to get the number of published language switcher modules. - * - * @return integer - */ - public static function getLangswitchers() - { - // Check if switcher is published. - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName('#__modules')) - ->where( - [ - $db->quoteName('module') . ' = ' . $db->quote('mod_languages'), - $db->quoteName('published') . ' = 1', - $db->quoteName('client_id') . ' = 0', - ] - ); - - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Method to return a list of published content languages. - * - * @return array of language objects. - */ - public static function getContentlangs() - { - // Check for published Content Languages. - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('lang_code'), - $db->quoteName('published'), - $db->quoteName('sef'), - ] - ) - ->from($db->quoteName('#__languages')); - - $db->setQuery($query); - - return $db->loadObjectList(); - } - - /** - * Method to return combined language status. - * - * @return array of language objects. - */ - public static function getStatus() - { - // Check for combined status. - $db = Factory::getDbo(); - $query = $db->getQuery(true); - - // Select all fields from the languages table. - $query->select( - [ - $db->quoteName('a') . '.*', - $db->quoteName('a.published'), - $db->quoteName('a.lang_code'), - $db->quoteName('e.enabled'), - $db->quoteName('e.element'), - $db->quoteName('l.home'), - $db->quoteName('l.published', 'home_published'), - ] - ) - ->from($db->quoteName('#__languages', 'a')) - ->join( - 'LEFT', - $db->quoteName('#__menu', 'l'), - $db->quoteName('l.language') . ' = ' . $db->quoteName('a.lang_code') - . ' AND ' . $db->quoteName('l.home') . ' = 1 AND ' . $db->quoteName('l.language') . ' <> ' . $db->quote('*') - ) - ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.element') . ' = ' . $db->quoteName('a.lang_code')) - ->where( - [ - $db->quoteName('e.client_id') . ' = 0', - $db->quoteName('e.enabled') . ' = 1', - $db->quoteName('e.state') . ' = 0', - ] - ); - - $db->setQuery($query); - - return $db->loadObjectList(); - } - - /** - * Method to return a list of contact objects. - * - * @return array of contact objects. - */ - public static function getContacts() - { - $db = Factory::getDbo(); - $languages = count(LanguageHelper::getLanguages()); - - // Get the number of contact with all as language - $alang = $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName('#__contact_details', 'cd')) - ->where( - [ - $db->quoteName('cd.user_id') . ' = ' . $db->quoteName('u.id'), - $db->quoteName('cd.published') . ' = 1', - $db->quoteName('cd.language') . ' = ' . $db->quote('*'), - ] - ); - - // Get the number of languages for the contact - $slang = $db->getQuery(true) - ->select('COUNT(DISTINCT ' . $db->quoteName('l.lang_code') . ')') - ->from($db->quoteName('#__languages', 'l')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd'), $db->quoteName('cd.language') . ' = ' . $db->quoteName('l.lang_code')) - ->where( - [ - $db->quoteName('cd.user_id') . ' = ' . $db->quoteName('u.id'), - $db->quoteName('cd.published') . ' = 1', - $db->quoteName('l.published') . ' = 1', - ] - ); - - // Get the number of multiple contact/language - $mlang = $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName('#__languages', 'l')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd'), $db->quoteName('cd.language') . ' = ' . $db->quoteName('l.lang_code')) - ->where( - [ - $db->quoteName('cd.user_id') . ' = ' . $db->quoteName('u.id'), - $db->quoteName('cd.published') . ' = 1', - $db->quoteName('l.published') . ' = 1', - ] - ) - ->group($db->quoteName('l.lang_code')) - ->having('COUNT(*) > 1'); - - // Get the contacts - $subQuery = $db->getQuery(true) - ->select('1') - ->from($db->quoteName('#__content', 'c')) - ->where($db->quoteName('c.created_by') . ' = ' . $db->quoteName('u.id')); - - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('u.name'), - '(' . $alang . ') AS ' . $db->quoteName('alang'), - '(' . $slang . ') AS ' . $db->quoteName('slang'), - '(' . $mlang . ') AS ' . $db->quoteName('mlang'), - ] - ) - ->from($db->quoteName('#__users', 'u')) - ->join('LEFT', $db->quoteName('#__contact_details', 'cd'), $db->quoteName('cd.user_id') . ' = ' . $db->quoteName('u.id')) - ->where('EXISTS (' . $subQuery . ')') - ->group( - [ - $db->quoteName('u.id'), - $db->quoteName('u.name'), - ] - ); - - $db->setQuery($query); - $warnings = $db->loadObjectList(); - - foreach ($warnings as $index => $warn) - { - if ($warn->alang == 1 && $warn->slang == 0) - { - unset($warnings[$index]); - } - - if ($warn->alang == 0 && $warn->slang == 0 && empty($warn->mlang)) - { - unset($warnings[$index]); - } - - if ($warn->alang == 0 && $warn->slang == $languages && empty($warn->mlang)) - { - unset($warnings[$index]); - } - } - - return $warnings; - } - - /** - * Method to get the status of the module displaying the menutype of the default Home page set to All languages. - * - * @return boolean True if the module is published, false otherwise. - * - * @since 3.7.0 - */ - public static function getDefaultHomeModule() - { - // Find Default Home menutype. - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('menutype')) - ->from($db->quoteName('#__menu')) - ->where( - [ - $db->quoteName('home') . ' = 1', - $db->quoteName('published') . ' = 1', - $db->quoteName('client_id') . ' = 0', - $db->quoteName('language') . ' = ' . $db->quote('*'), - ] - ); - - $db->setQuery($query); - - $menutype = $db->loadResult(); - - // Get published site menu modules titles. - $query->clear() - ->select($db->quoteName('title')) - ->from($db->quoteName('#__modules')) - ->where( - [ - $db->quoteName('module') . ' = ' . $db->quote('mod_menu'), - $db->quoteName('published') . ' = 1', - $db->quoteName('client_id') . ' = 0', - ] - ); - - $db->setQuery($query); - - $menutitles = $db->loadColumn(); - - // Do we have a published menu module displaying the default Home menu item set to all languages? - foreach ($menutitles as $menutitle) - { - $module = self::getModule('mod_menu', $menutitle); - $moduleParams = new Registry($module->params); - $param = $moduleParams->get('menutype', ''); - - if ($param && $param != $menutype) - { - continue; - } - - return true; - } - } - - /** - * Get module by name - * - * @param string $moduleName The name of the module - * @param string $instanceTitle The title of the module, optional - * - * @return \stdClass The Module object - * - * @since 3.7.0 - */ - public static function getModule($moduleName, $instanceTitle = null) - { - $db = Factory::getDbo(); - - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('id'), - $db->quoteName('title'), - $db->quoteName('module'), - $db->quoteName('position'), - $db->quoteName('content'), - $db->quoteName('showtitle'), - $db->quoteName('params'), - ] - ) - ->from($db->quoteName('#__modules')) - ->where( - [ - $db->quoteName('module') . ' = :module', - $db->quoteName('published') . ' = 1', - $db->quoteName('client_id') . ' = 0', - ] - ) - ->bind(':module', $moduleName); - - if ($instanceTitle) - { - $query->where($db->quoteName('title') . ' = :title') - ->bind(':title', $instanceTitle); - } - - $db->setQuery($query); - - try - { - $modules = $db->loadObject(); - } - catch (\RuntimeException $e) - { - Log::add(Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $e->getMessage()), Log::WARNING, 'jerror'); - } - - return $modules; - } + /** + * Method to get the number of published home pages. + * + * @return integer + */ + public static function getHomes() + { + // Check for multiple Home pages. + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__menu')) + ->where( + [ + $db->quoteName('home') . ' = 1', + $db->quoteName('published') . ' = 1', + $db->quoteName('client_id') . ' = 0', + ] + ); + + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Method to get the number of published language switcher modules. + * + * @return integer + */ + public static function getLangswitchers() + { + // Check if switcher is published. + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__modules')) + ->where( + [ + $db->quoteName('module') . ' = ' . $db->quote('mod_languages'), + $db->quoteName('published') . ' = 1', + $db->quoteName('client_id') . ' = 0', + ] + ); + + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Method to return a list of published content languages. + * + * @return array of language objects. + */ + public static function getContentlangs() + { + // Check for published Content Languages. + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('lang_code'), + $db->quoteName('published'), + $db->quoteName('sef'), + ] + ) + ->from($db->quoteName('#__languages')); + + $db->setQuery($query); + + return $db->loadObjectList(); + } + + /** + * Method to return combined language status. + * + * @return array of language objects. + */ + public static function getStatus() + { + // Check for combined status. + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + // Select all fields from the languages table. + $query->select( + [ + $db->quoteName('a') . '.*', + $db->quoteName('a.published'), + $db->quoteName('a.lang_code'), + $db->quoteName('e.enabled'), + $db->quoteName('e.element'), + $db->quoteName('l.home'), + $db->quoteName('l.published', 'home_published'), + ] + ) + ->from($db->quoteName('#__languages', 'a')) + ->join( + 'LEFT', + $db->quoteName('#__menu', 'l'), + $db->quoteName('l.language') . ' = ' . $db->quoteName('a.lang_code') + . ' AND ' . $db->quoteName('l.home') . ' = 1 AND ' . $db->quoteName('l.language') . ' <> ' . $db->quote('*') + ) + ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.element') . ' = ' . $db->quoteName('a.lang_code')) + ->where( + [ + $db->quoteName('e.client_id') . ' = 0', + $db->quoteName('e.enabled') . ' = 1', + $db->quoteName('e.state') . ' = 0', + ] + ); + + $db->setQuery($query); + + return $db->loadObjectList(); + } + + /** + * Method to return a list of contact objects. + * + * @return array of contact objects. + */ + public static function getContacts() + { + $db = Factory::getDbo(); + $languages = count(LanguageHelper::getLanguages()); + + // Get the number of contact with all as language + $alang = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__contact_details', 'cd')) + ->where( + [ + $db->quoteName('cd.user_id') . ' = ' . $db->quoteName('u.id'), + $db->quoteName('cd.published') . ' = 1', + $db->quoteName('cd.language') . ' = ' . $db->quote('*'), + ] + ); + + // Get the number of languages for the contact + $slang = $db->getQuery(true) + ->select('COUNT(DISTINCT ' . $db->quoteName('l.lang_code') . ')') + ->from($db->quoteName('#__languages', 'l')) + ->join('LEFT', $db->quoteName('#__contact_details', 'cd'), $db->quoteName('cd.language') . ' = ' . $db->quoteName('l.lang_code')) + ->where( + [ + $db->quoteName('cd.user_id') . ' = ' . $db->quoteName('u.id'), + $db->quoteName('cd.published') . ' = 1', + $db->quoteName('l.published') . ' = 1', + ] + ); + + // Get the number of multiple contact/language + $mlang = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__languages', 'l')) + ->join('LEFT', $db->quoteName('#__contact_details', 'cd'), $db->quoteName('cd.language') . ' = ' . $db->quoteName('l.lang_code')) + ->where( + [ + $db->quoteName('cd.user_id') . ' = ' . $db->quoteName('u.id'), + $db->quoteName('cd.published') . ' = 1', + $db->quoteName('l.published') . ' = 1', + ] + ) + ->group($db->quoteName('l.lang_code')) + ->having('COUNT(*) > 1'); + + // Get the contacts + $subQuery = $db->getQuery(true) + ->select('1') + ->from($db->quoteName('#__content', 'c')) + ->where($db->quoteName('c.created_by') . ' = ' . $db->quoteName('u.id')); + + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('u.name'), + '(' . $alang . ') AS ' . $db->quoteName('alang'), + '(' . $slang . ') AS ' . $db->quoteName('slang'), + '(' . $mlang . ') AS ' . $db->quoteName('mlang'), + ] + ) + ->from($db->quoteName('#__users', 'u')) + ->join('LEFT', $db->quoteName('#__contact_details', 'cd'), $db->quoteName('cd.user_id') . ' = ' . $db->quoteName('u.id')) + ->where('EXISTS (' . $subQuery . ')') + ->group( + [ + $db->quoteName('u.id'), + $db->quoteName('u.name'), + ] + ); + + $db->setQuery($query); + $warnings = $db->loadObjectList(); + + foreach ($warnings as $index => $warn) { + if ($warn->alang == 1 && $warn->slang == 0) { + unset($warnings[$index]); + } + + if ($warn->alang == 0 && $warn->slang == 0 && empty($warn->mlang)) { + unset($warnings[$index]); + } + + if ($warn->alang == 0 && $warn->slang == $languages && empty($warn->mlang)) { + unset($warnings[$index]); + } + } + + return $warnings; + } + + /** + * Method to get the status of the module displaying the menutype of the default Home page set to All languages. + * + * @return boolean True if the module is published, false otherwise. + * + * @since 3.7.0 + */ + public static function getDefaultHomeModule() + { + // Find Default Home menutype. + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('menutype')) + ->from($db->quoteName('#__menu')) + ->where( + [ + $db->quoteName('home') . ' = 1', + $db->quoteName('published') . ' = 1', + $db->quoteName('client_id') . ' = 0', + $db->quoteName('language') . ' = ' . $db->quote('*'), + ] + ); + + $db->setQuery($query); + + $menutype = $db->loadResult(); + + // Get published site menu modules titles. + $query->clear() + ->select($db->quoteName('title')) + ->from($db->quoteName('#__modules')) + ->where( + [ + $db->quoteName('module') . ' = ' . $db->quote('mod_menu'), + $db->quoteName('published') . ' = 1', + $db->quoteName('client_id') . ' = 0', + ] + ); + + $db->setQuery($query); + + $menutitles = $db->loadColumn(); + + // Do we have a published menu module displaying the default Home menu item set to all languages? + foreach ($menutitles as $menutitle) { + $module = self::getModule('mod_menu', $menutitle); + $moduleParams = new Registry($module->params); + $param = $moduleParams->get('menutype', ''); + + if ($param && $param != $menutype) { + continue; + } + + return true; + } + } + + /** + * Get module by name + * + * @param string $moduleName The name of the module + * @param string $instanceTitle The title of the module, optional + * + * @return \stdClass The Module object + * + * @since 3.7.0 + */ + public static function getModule($moduleName, $instanceTitle = null) + { + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('id'), + $db->quoteName('title'), + $db->quoteName('module'), + $db->quoteName('position'), + $db->quoteName('content'), + $db->quoteName('showtitle'), + $db->quoteName('params'), + ] + ) + ->from($db->quoteName('#__modules')) + ->where( + [ + $db->quoteName('module') . ' = :module', + $db->quoteName('published') . ' = 1', + $db->quoteName('client_id') . ' = 0', + ] + ) + ->bind(':module', $moduleName); + + if ($instanceTitle) { + $query->where($db->quoteName('title') . ' = :title') + ->bind(':title', $instanceTitle); + } + + $db->setQuery($query); + + try { + $modules = $db->loadObject(); + } catch (\RuntimeException $e) { + Log::add(Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $e->getMessage()), Log::WARNING, 'jerror'); + } + + return $modules; + } } diff --git a/administrator/components/com_languages/src/Model/InstalledModel.php b/administrator/components/com_languages/src/Model/InstalledModel.php index 0660c31dba961..f98308279e9d7 100644 --- a/administrator/components/com_languages/src/Model/InstalledModel.php +++ b/administrator/components/com_languages/src/Model/InstalledModel.php @@ -1,4 +1,5 @@ setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - - // Special case for client id. - $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); - $clientId = (!in_array($clientId, array (0, 1))) ? 0 : $clientId; - $this->setState('client_id', $clientId); - - // Load the parameters. - $params = ComponentHelper::getParams('com_languages'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('client_id'); - $id .= ':' . $this->getState('filter.search'); - - return parent::getStoreId($id); - } - - /** - * Method to get the client object. - * - * @return object - * - * @since 1.6 - */ - public function getClient() - { - return ApplicationHelper::getClientInfo($this->getState('client_id', 0)); - } - - /** - * Method to get the option. - * - * @return object - * - * @since 1.6 - */ - public function getOption() - { - $option = $this->getState('option'); - - return $option; - } - - /** - * Method to get Languages item data. - * - * @return array - * - * @since 1.6 - */ - public function getData() - { - // Fetch language data if not fetched yet. - if (is_null($this->data)) - { - $this->data = array(); - - $isCurrentLanguageRtl = Factory::getLanguage()->isRtl(); - $params = ComponentHelper::getParams('com_languages'); - $installedLanguages = LanguageHelper::getInstalledLanguages(null, true, true, null, null, null); - - // Compute all the languages. - foreach ($installedLanguages as $clientId => $languages) - { - $defaultLanguage = $params->get(ApplicationHelper::getClientInfo($clientId)->name, 'en-GB'); - - foreach ($languages as $lang) - { - $row = new \stdClass; - $row->language = $lang->element; - $row->name = $lang->metadata['name']; - $row->nativeName = $lang->metadata['nativeName'] ?? '-'; - $row->client_id = (int) $lang->client_id; - $row->extension_id = (int) $lang->extension_id; - $row->author = $lang->manifest['author']; - $row->creationDate = $lang->manifest['creationDate']; - $row->authorEmail = $lang->manifest['authorEmail']; - $row->version = $lang->manifest['version']; - $row->published = $defaultLanguage === $row->language ? 1 : 0; - $row->checked_out = null; - - // Fix wrongly set parentheses in RTL languages - if ($isCurrentLanguageRtl) - { - $row->name = html_entity_decode($row->name . '‎', ENT_QUOTES, 'UTF-8'); - $row->nativeName = html_entity_decode($row->nativeName . '‎', ENT_QUOTES, 'UTF-8'); - } - - $this->data[] = $row; - } - } - } - - $installedLanguages = array_merge($this->data); - - // Process filters. - $clientId = (int) $this->getState('client_id'); - $search = $this->getState('filter.search'); - - foreach ($installedLanguages as $key => $installedLanguage) - { - // Filter by client id. - if (in_array($clientId, array(0, 1))) - { - if ($installedLanguage->client_id !== $clientId) - { - unset($installedLanguages[$key]); - continue; - } - } - - // Filter by search term. - if (!empty($search)) - { - if (stripos($installedLanguage->name, $search) === false - && stripos($installedLanguage->nativeName, $search) === false - && stripos($installedLanguage->language, $search) === false) - { - unset($installedLanguages[$key]); - } - } - } - - // Process ordering. - $listOrder = $this->getState('list.ordering', 'name'); - $listDirn = $this->getState('list.direction', 'ASC'); - $installedLanguages = ArrayHelper::sortObjects($installedLanguages, $listOrder, strtolower($listDirn) === 'desc' ? -1 : 1, true, true); - - // Process pagination. - $limit = (int) $this->getState('list.limit', 25); - - // Sets the total for pagination. - $this->total = count($installedLanguages); - - if ($limit !== 0) - { - $start = (int) $this->getState('list.start', 0); - - return array_slice($installedLanguages, $start, $limit); - } - - return $installedLanguages; - } - - /** - * Method to get the total number of Languages items. - * - * @return integer - * - * @since 1.6 - */ - public function getTotal() - { - if (is_null($this->total)) - { - $this->getData(); - } - - return $this->total; - } - - /** - * Method to set the default language. - * - * @param integer $cid Id of the language to publish. - * - * @return boolean - * - * @since 1.6 - */ - public function publish($cid) - { - if ($cid) - { - $client = $this->getClient(); - - $params = ComponentHelper::getParams('com_languages'); - $params->set($client->name, $cid); - - $table = Table::getInstance('extension', 'Joomla\\CMS\\Table\\'); - $id = $table->find(array('element' => 'com_languages')); - - // Load. - if (!$table->load($id)) - { - $this->setError($table->getError()); - - return false; - } - - $table->params = (string) $params; - - // Pre-save checks. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Save the changes. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - } - else - { - $this->setError(Text::_('COM_LANGUAGES_ERR_NO_LANGUAGE_SELECTED')); - - return false; - } - - // Clean the cache of com_languages and component cache. - $this->cleanCache(); - $this->cleanCache('_system'); - - return true; - } - - /** - * Method to get the folders. - * - * @return array Languages folders. - * - * @since 1.6 - */ - protected function getFolders() - { - if (is_null($this->folders)) - { - $path = $this->getPath(); - $this->folders = Folder::folders($path, '.', false, false, array('.svn', 'CVS', '.DS_Store', '__MACOSX', 'pdf_fonts', 'overrides')); - } - - return $this->folders; - } - - /** - * Method to get the path. - * - * @return string The path to the languages folders. - * - * @since 1.6 - */ - protected function getPath() - { - if (is_null($this->path)) - { - $client = $this->getClient(); - $this->path = LanguageHelper::getLanguagePath($client->path); - } - - return $this->path; - } - - /** - * Method to switch the administrator language. - * - * @param string $cid The language tag. - * - * @return boolean - * - * @since 3.5 - */ - public function switchAdminLanguage($cid) - { - if ($cid) - { - $client = $this->getClient(); - - if ($client->name == 'administrator') - { - Factory::getApplication()->setUserState('application.lang', $cid); - } - } - else - { - Factory::getApplication()->enqueueMessage(Text::_('COM_LANGUAGES_ERR_NO_LANGUAGE_SELECTED'), 'error'); - - return false; - } - - return true; - } + /** + * @var object user object + */ + protected $user = null; + + /** + * @var string option name + */ + protected $option = null; + + /** + * @var array languages description + */ + protected $data = null; + + /** + * @var integer total number of languages + */ + protected $total = null; + + /** + * @var string language path + */ + protected $path = null; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'name', + 'nativeName', + 'language', + 'author', + 'published', + 'version', + 'creationDate', + 'author', + 'authorEmail', + 'extension_id', + 'client_id', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'name', $direction = 'asc') + { + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + + // Special case for client id. + $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); + $clientId = (!in_array($clientId, array (0, 1))) ? 0 : $clientId; + $this->setState('client_id', $clientId); + + // Load the parameters. + $params = ComponentHelper::getParams('com_languages'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('client_id'); + $id .= ':' . $this->getState('filter.search'); + + return parent::getStoreId($id); + } + + /** + * Method to get the client object. + * + * @return object + * + * @since 1.6 + */ + public function getClient() + { + return ApplicationHelper::getClientInfo($this->getState('client_id', 0)); + } + + /** + * Method to get the option. + * + * @return object + * + * @since 1.6 + */ + public function getOption() + { + $option = $this->getState('option'); + + return $option; + } + + /** + * Method to get Languages item data. + * + * @return array + * + * @since 1.6 + */ + public function getData() + { + // Fetch language data if not fetched yet. + if (is_null($this->data)) { + $this->data = array(); + + $isCurrentLanguageRtl = Factory::getLanguage()->isRtl(); + $params = ComponentHelper::getParams('com_languages'); + $installedLanguages = LanguageHelper::getInstalledLanguages(null, true, true, null, null, null); + + // Compute all the languages. + foreach ($installedLanguages as $clientId => $languages) { + $defaultLanguage = $params->get(ApplicationHelper::getClientInfo($clientId)->name, 'en-GB'); + + foreach ($languages as $lang) { + $row = new \stdClass(); + $row->language = $lang->element; + $row->name = $lang->metadata['name']; + $row->nativeName = $lang->metadata['nativeName'] ?? '-'; + $row->client_id = (int) $lang->client_id; + $row->extension_id = (int) $lang->extension_id; + $row->author = $lang->manifest['author']; + $row->creationDate = $lang->manifest['creationDate']; + $row->authorEmail = $lang->manifest['authorEmail']; + $row->version = $lang->manifest['version']; + $row->published = $defaultLanguage === $row->language ? 1 : 0; + $row->checked_out = null; + + // Fix wrongly set parentheses in RTL languages + if ($isCurrentLanguageRtl) { + $row->name = html_entity_decode($row->name . '‎', ENT_QUOTES, 'UTF-8'); + $row->nativeName = html_entity_decode($row->nativeName . '‎', ENT_QUOTES, 'UTF-8'); + } + + $this->data[] = $row; + } + } + } + + $installedLanguages = array_merge($this->data); + + // Process filters. + $clientId = (int) $this->getState('client_id'); + $search = $this->getState('filter.search'); + + foreach ($installedLanguages as $key => $installedLanguage) { + // Filter by client id. + if (in_array($clientId, array(0, 1))) { + if ($installedLanguage->client_id !== $clientId) { + unset($installedLanguages[$key]); + continue; + } + } + + // Filter by search term. + if (!empty($search)) { + if ( + stripos($installedLanguage->name, $search) === false + && stripos($installedLanguage->nativeName, $search) === false + && stripos($installedLanguage->language, $search) === false + ) { + unset($installedLanguages[$key]); + } + } + } + + // Process ordering. + $listOrder = $this->getState('list.ordering', 'name'); + $listDirn = $this->getState('list.direction', 'ASC'); + $installedLanguages = ArrayHelper::sortObjects($installedLanguages, $listOrder, strtolower($listDirn) === 'desc' ? -1 : 1, true, true); + + // Process pagination. + $limit = (int) $this->getState('list.limit', 25); + + // Sets the total for pagination. + $this->total = count($installedLanguages); + + if ($limit !== 0) { + $start = (int) $this->getState('list.start', 0); + + return array_slice($installedLanguages, $start, $limit); + } + + return $installedLanguages; + } + + /** + * Method to get the total number of Languages items. + * + * @return integer + * + * @since 1.6 + */ + public function getTotal() + { + if (is_null($this->total)) { + $this->getData(); + } + + return $this->total; + } + + /** + * Method to set the default language. + * + * @param integer $cid Id of the language to publish. + * + * @return boolean + * + * @since 1.6 + */ + public function publish($cid) + { + if ($cid) { + $client = $this->getClient(); + + $params = ComponentHelper::getParams('com_languages'); + $params->set($client->name, $cid); + + $table = Table::getInstance('extension', 'Joomla\\CMS\\Table\\'); + $id = $table->find(array('element' => 'com_languages')); + + // Load. + if (!$table->load($id)) { + $this->setError($table->getError()); + + return false; + } + + $table->params = (string) $params; + + // Pre-save checks. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Save the changes. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + } else { + $this->setError(Text::_('COM_LANGUAGES_ERR_NO_LANGUAGE_SELECTED')); + + return false; + } + + // Clean the cache of com_languages and component cache. + $this->cleanCache(); + $this->cleanCache('_system'); + + return true; + } + + /** + * Method to get the folders. + * + * @return array Languages folders. + * + * @since 1.6 + */ + protected function getFolders() + { + if (is_null($this->folders)) { + $path = $this->getPath(); + $this->folders = Folder::folders($path, '.', false, false, array('.svn', 'CVS', '.DS_Store', '__MACOSX', 'pdf_fonts', 'overrides')); + } + + return $this->folders; + } + + /** + * Method to get the path. + * + * @return string The path to the languages folders. + * + * @since 1.6 + */ + protected function getPath() + { + if (is_null($this->path)) { + $client = $this->getClient(); + $this->path = LanguageHelper::getLanguagePath($client->path); + } + + return $this->path; + } + + /** + * Method to switch the administrator language. + * + * @param string $cid The language tag. + * + * @return boolean + * + * @since 3.5 + */ + public function switchAdminLanguage($cid) + { + if ($cid) { + $client = $this->getClient(); + + if ($client->name == 'administrator') { + Factory::getApplication()->setUserState('application.lang', $cid); + } + } else { + Factory::getApplication()->enqueueMessage(Text::_('COM_LANGUAGES_ERR_NO_LANGUAGE_SELECTED'), 'error'); + + return false; + } + + return true; + } } diff --git a/administrator/components/com_languages/src/Model/LanguageModel.php b/administrator/components/com_languages/src/Model/LanguageModel.php index c9b953e22afbf..5c4690bd31bcb 100644 --- a/administrator/components/com_languages/src/Model/LanguageModel.php +++ b/administrator/components/com_languages/src/Model/LanguageModel.php @@ -1,4 +1,5 @@ 'onExtensionAfterSave', - 'event_before_save' => 'onExtensionBeforeSave', - 'events_map' => array( - 'save' => 'extension' - ) - ), $config - ); - - parent::__construct($config, $factory); - } - - /** - * Override to get the table. - * - * @param string $name Name of the table. - * @param string $prefix Table name prefix. - * @param array $options Array of options. - * - * @return Table - * - * @since 1.6 - */ - public function getTable($name = '', $prefix = '', $options = array()) - { - return Table::getInstance('Language', 'Joomla\\CMS\\Table\\'); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - */ - protected function populateState() - { - $app = Factory::getApplication(); - $params = ComponentHelper::getParams('com_languages'); - - // Load the User state. - $langId = $app->input->getInt('lang_id'); - $this->setState('language.id', $langId); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Method to get a member item. - * - * @param integer $langId The id of the member to get. - * - * @return mixed User data object on success, false on failure. - * - * @since 1.0 - */ - public function getItem($langId = null) - { - $langId = (!empty($langId)) ? $langId : (int) $this->getState('language.id'); - - // Get a member row instance. - $table = $this->getTable(); - - // Attempt to load the row. - $return = $table->load($langId); - - // Check for a table object error. - if ($return === false && $table->getError()) - { - $this->setError($table->getError()); - - return false; - } - - // Set a valid accesslevel in case '0' is stored due to a bug in the installation SQL (was fixed with PR 2714). - if ($table->access == '0') - { - $table->access = (int) Factory::getApplication()->get('access'); - } - - $properties = $table->getProperties(1); - $value = ArrayHelper::toObject($properties, CMSObject::class); - - return $value; - } - - /** - * Method to get the group form. - * - * @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 \Joomla\CMS\Form\Form|bool A Form object on success, false on failure. - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_languages.language', 'language', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_languages.edit.language.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_languages.language', $data); - - return $data; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $langId = (!empty($data['lang_id'])) ? $data['lang_id'] : (int) $this->getState('language.id'); - $isNew = true; - - PluginHelper::importPlugin($this->events_map['save']); - - $table = $this->getTable(); - $context = $this->option . '.' . $this->name; - - // Load the row if saving an existing item. - if ($langId > 0) - { - $table->load($langId); - $isNew = false; - } - - // Prevent white spaces, including East Asian double bytes. - $spaces = array('/\xE3\x80\x80/', ' '); - - $data['lang_code'] = str_replace($spaces, '', $data['lang_code']); - - // Prevent saving an incorrect language tag - if (!preg_match('#\b([a-z]{2,3})[-]([A-Z]{2})\b#', $data['lang_code'])) - { - $this->setError(Text::_('COM_LANGUAGES_ERROR_LANG_TAG')); - - return false; - } - - $data['sef'] = str_replace($spaces, '', $data['sef']); - $data['sef'] = ApplicationHelper::stringURLSafe($data['sef']); - - // Prevent saving an empty url language code - if ($data['sef'] === '') - { - $this->setError(Text::_('COM_LANGUAGES_ERROR_SEF')); - - return false; - } - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, $isNew)); - - // Check the event responses. - if (in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - // Store the data. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the after save event. - Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, $isNew)); - - $this->setState('language.id', $table->lang_id); - - // Clean the cache. - $this->cleanCache(); - - return true; - } - - /** - * Custom clean cache method. - * - * @param string $group Optional cache group name. - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('_system'); - parent::cleanCache('com_languages'); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + $config = array_merge( + array( + 'event_after_save' => 'onExtensionAfterSave', + 'event_before_save' => 'onExtensionBeforeSave', + 'events_map' => array( + 'save' => 'extension' + ) + ), + $config + ); + + parent::__construct($config, $factory); + } + + /** + * Override to get the table. + * + * @param string $name Name of the table. + * @param string $prefix Table name prefix. + * @param array $options Array of options. + * + * @return Table + * + * @since 1.6 + */ + public function getTable($name = '', $prefix = '', $options = array()) + { + return Table::getInstance('Language', 'Joomla\\CMS\\Table\\'); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + $params = ComponentHelper::getParams('com_languages'); + + // Load the User state. + $langId = $app->input->getInt('lang_id'); + $this->setState('language.id', $langId); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Method to get a member item. + * + * @param integer $langId The id of the member to get. + * + * @return mixed User data object on success, false on failure. + * + * @since 1.0 + */ + public function getItem($langId = null) + { + $langId = (!empty($langId)) ? $langId : (int) $this->getState('language.id'); + + // Get a member row instance. + $table = $this->getTable(); + + // Attempt to load the row. + $return = $table->load($langId); + + // Check for a table object error. + if ($return === false && $table->getError()) { + $this->setError($table->getError()); + + return false; + } + + // Set a valid accesslevel in case '0' is stored due to a bug in the installation SQL (was fixed with PR 2714). + if ($table->access == '0') { + $table->access = (int) Factory::getApplication()->get('access'); + } + + $properties = $table->getProperties(1); + $value = ArrayHelper::toObject($properties, CMSObject::class); + + return $value; + } + + /** + * Method to get the group form. + * + * @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 \Joomla\CMS\Form\Form|bool A Form object on success, false on failure. + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_languages.language', 'language', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_languages.edit.language.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_languages.language', $data); + + return $data; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $langId = (!empty($data['lang_id'])) ? $data['lang_id'] : (int) $this->getState('language.id'); + $isNew = true; + + PluginHelper::importPlugin($this->events_map['save']); + + $table = $this->getTable(); + $context = $this->option . '.' . $this->name; + + // Load the row if saving an existing item. + if ($langId > 0) { + $table->load($langId); + $isNew = false; + } + + // Prevent white spaces, including East Asian double bytes. + $spaces = array('/\xE3\x80\x80/', ' '); + + $data['lang_code'] = str_replace($spaces, '', $data['lang_code']); + + // Prevent saving an incorrect language tag + if (!preg_match('#\b([a-z]{2,3})[-]([A-Z]{2})\b#', $data['lang_code'])) { + $this->setError(Text::_('COM_LANGUAGES_ERROR_LANG_TAG')); + + return false; + } + + $data['sef'] = str_replace($spaces, '', $data['sef']); + $data['sef'] = ApplicationHelper::stringURLSafe($data['sef']); + + // Prevent saving an empty url language code + if ($data['sef'] === '') { + $this->setError(Text::_('COM_LANGUAGES_ERROR_SEF')); + + return false; + } + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, $isNew)); + + // Check the event responses. + if (in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + // Store the data. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the after save event. + Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, $isNew)); + + $this->setState('language.id', $table->lang_id); + + // Clean the cache. + $this->cleanCache(); + + return true; + } + + /** + * Custom clean cache method. + * + * @param string $group Optional cache group name. + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('_system'); + parent::cleanCache('com_languages'); + } } diff --git a/administrator/components/com_languages/src/Model/LanguagesModel.php b/administrator/components/com_languages/src/Model/LanguagesModel.php index d6b415f5ab0ae..a089f259aea9b 100644 --- a/administrator/components/com_languages/src/Model/LanguagesModel.php +++ b/administrator/components/com_languages/src/Model/LanguagesModel.php @@ -1,4 +1,5 @@ setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.published'); - - return parent::getStoreId($id); - } - - /** - * Method to build an SQL query to load the list data. - * - * @return string An SQL query - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select all fields from the languages table. - $query->select( - $this->getState('list.select', - [ - $db->quoteName('a') . '.*', - ] - ) - ) - ->select( - [ - $db->quoteName('l.home'), - $db->quoteName('ag.title', 'access_level'), - ] - ) - ->from($db->quoteName('#__languages', 'a')) - ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) - ->join( - 'LEFT', - $db->quoteName('#__menu', 'l'), - $db->quoteName('l.language') . ' = ' . $db->quoteName('a.lang_code') - . ' AND ' . $db->quoteName('l.home') . ' = 1 AND ' . $db->quoteName('l.language') . ' <> ' . $db->quote('*') - ); - - // Filter on the published state. - $published = (string) $this->getState('filter.published'); - - if (is_numeric($published)) - { - $published = (int) $published; - $query->where($db->quoteName('a.published') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->where($db->quoteName('a.published') . ' IN (0, 1)'); - } - - // Filter by search in title. - if ($search = $this->getState('filter.search')) - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where($db->quoteName('a.title') . ' LIKE :search') - ->bind(':search', $search); - } - - // Filter by access level. - if ($access = (int) $this->getState('filter.access')) - { - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - - // Add the list ordering clause. - $query->order($db->quoteName($db->escape($this->getState('list.ordering', 'a.ordering'))) - . ' ' . $db->escape($this->getState('list.direction', 'ASC')) - ); - - return $query; - } - - /** - * Set the published language(s). - * - * @param array $cid An array of language IDs. - * @param integer $value The value of the published state. - * - * @return boolean True on success, false otherwise. - * - * @since 1.6 - */ - public function setPublished($cid, $value = 0) - { - return Table::getInstance('Language', 'Joomla\\CMS\\Table\\')->publish($cid, $value); - } - - /** - * Method to delete records. - * - * @param array $pks An array of item primary keys. - * - * @return boolean Returns true on success, false on failure. - * - * @since 1.6 - */ - public function delete($pks) - { - // Sanitize the array. - $pks = (array) $pks; - - // Get a row instance. - $table = Table::getInstance('Language', 'Joomla\\CMS\\Table\\'); - - // Iterate the items to delete each one. - foreach ($pks as $itemId) - { - if (!$table->delete((int) $itemId)) - { - $this->setError($table->getError()); - - return false; - } - } - - // Clean the cache. - $this->cleanCache(); - - return true; - } - - /** - * Custom clean cache method, 2 places for 2 clients. - * - * @param string $group Optional cache group name. - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('_system'); - parent::cleanCache('com_languages'); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'lang_id', 'a.lang_id', + 'lang_code', 'a.lang_code', + 'title', 'a.title', + 'title_native', 'a.title_native', + 'sef', 'a.sef', + 'image', 'a.image', + 'published', 'a.published', + 'ordering', 'a.ordering', + 'access', 'a.access', 'access_level', + 'home', 'l.home', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.ordering', $direction = 'asc') + { + // Load the parameters. + $params = ComponentHelper::getParams('com_languages'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.published'); + + return parent::getStoreId($id); + } + + /** + * Method to build an SQL query to load the list data. + * + * @return string An SQL query + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select all fields from the languages table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a') . '.*', + ] + ) + ) + ->select( + [ + $db->quoteName('l.home'), + $db->quoteName('ag.title', 'access_level'), + ] + ) + ->from($db->quoteName('#__languages', 'a')) + ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) + ->join( + 'LEFT', + $db->quoteName('#__menu', 'l'), + $db->quoteName('l.language') . ' = ' . $db->quoteName('a.lang_code') + . ' AND ' . $db->quoteName('l.home') . ' = 1 AND ' . $db->quoteName('l.language') . ' <> ' . $db->quote('*') + ); + + // Filter on the published state. + $published = (string) $this->getState('filter.published'); + + if (is_numeric($published)) { + $published = (int) $published; + $query->where($db->quoteName('a.published') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->where($db->quoteName('a.published') . ' IN (0, 1)'); + } + + // Filter by search in title. + if ($search = $this->getState('filter.search')) { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where($db->quoteName('a.title') . ' LIKE :search') + ->bind(':search', $search); + } + + // Filter by access level. + if ($access = (int) $this->getState('filter.access')) { + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + + // Add the list ordering clause. + $query->order($db->quoteName($db->escape($this->getState('list.ordering', 'a.ordering'))) + . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Set the published language(s). + * + * @param array $cid An array of language IDs. + * @param integer $value The value of the published state. + * + * @return boolean True on success, false otherwise. + * + * @since 1.6 + */ + public function setPublished($cid, $value = 0) + { + return Table::getInstance('Language', 'Joomla\\CMS\\Table\\')->publish($cid, $value); + } + + /** + * Method to delete records. + * + * @param array $pks An array of item primary keys. + * + * @return boolean Returns true on success, false on failure. + * + * @since 1.6 + */ + public function delete($pks) + { + // Sanitize the array. + $pks = (array) $pks; + + // Get a row instance. + $table = Table::getInstance('Language', 'Joomla\\CMS\\Table\\'); + + // Iterate the items to delete each one. + foreach ($pks as $itemId) { + if (!$table->delete((int) $itemId)) { + $this->setError($table->getError()); + + return false; + } + } + + // Clean the cache. + $this->cleanCache(); + + return true; + } + + /** + * Custom clean cache method, 2 places for 2 clients. + * + * @param string $group Optional cache group name. + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('_system'); + parent::cleanCache('com_languages'); + } } diff --git a/administrator/components/com_languages/src/Model/OverrideModel.php b/administrator/components/com_languages/src/Model/OverrideModel.php index 7fc3ab3427f1e..862becb9a1237 100644 --- a/administrator/components/com_languages/src/Model/OverrideModel.php +++ b/administrator/components/com_languages/src/Model/OverrideModel.php @@ -1,4 +1,5 @@ loadForm('com_languages.override', 'override', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - $client = $this->getState('filter.client', 'site'); - $language = $this->getState('filter.language', 'en-GB'); - $langName = Language::getInstance($language)->getName(); - - if (!$langName) - { - // If a language only exists in frontend, its metadata cannot be - // loaded in backend at the moment, so fall back to the language tag. - $langName = $language; - } - - $form->setValue('client', null, Text::_('COM_LANGUAGES_VIEW_OVERRIDE_CLIENT_' . strtoupper($client))); - $form->setValue('language', null, Text::sprintf('COM_LANGUAGES_VIEW_OVERRIDE_LANGUAGE', $langName, $language)); - $form->setValue('file', null, Path::clean(constant('JPATH_' . strtoupper($client)) . '/language/overrides/' . $language . '.override.ini')); - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 2.5 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_languages.edit.override.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_languages.override', $data); - - return $data; - } - - /** - * Method to get a single record. - * - * @param string $pk The key name. - * - * @return mixed Object on success, false otherwise. - * - * @since 2.5 - */ - public function getItem($pk = null) - { - $input = Factory::getApplication()->input; - $pk = !empty($pk) ? $pk : $input->get('id'); - $fileName = constant('JPATH_' . strtoupper($this->getState('filter.client'))) - . '/language/overrides/' . $this->getState('filter.language', 'en-GB') . '.override.ini'; - $strings = LanguageHelper::parseIniFile($fileName); - - $result = new \stdClass; - $result->key = ''; - $result->override = ''; - - if (isset($strings[$pk])) - { - $result->key = $pk; - $result->override = $strings[$pk]; - } - - $oppositeFileName = constant('JPATH_' . strtoupper($this->getState('filter.client') == 'site' ? 'administrator' : 'site')) - . '/language/overrides/' . $this->getState('filter.language', 'en-GB') . '.override.ini'; - $oppositeStrings = LanguageHelper::parseIniFile($oppositeFileName); - $result->both = isset($oppositeStrings[$pk]) && ($oppositeStrings[$pk] == $strings[$pk]); - - return $result; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * @param boolean $oppositeClient Indicates whether the override should not be created for the current client. - * - * @return boolean True on success, false otherwise. - * - * @since 2.5 - */ - public function save($data, $oppositeClient = false) - { - $app = Factory::getApplication(); - - if ($app->isClient('api')) - { - $client = $this->getState('filter.client'); - $language = $this->getState('filter.language'); - } - else - { - $client = $app->getUserState('com_languages.overrides.filter.client', 0); - $language = $app->getUserState('com_languages.overrides.filter.language', 'en-GB'); - } - - // If the override should be created for both. - if ($oppositeClient) - { - $client = 1 - $client; - } - - // Return false if the constant is a reserved word, i.e. YES, NO, NULL, FALSE, ON, OFF, NONE, TRUE - $reservedWords = array('YES', 'NO', 'NULL', 'FALSE', 'ON', 'OFF', 'NONE', 'TRUE'); - - if (in_array($data['key'], $reservedWords)) - { - $this->setError(Text::_('COM_LANGUAGES_OVERRIDE_ERROR_RESERVED_WORDS')); - - return false; - } - - $client = $client ? 'administrator' : 'site'; - - // Parse the override.ini file in order to get the keys and strings. - $fileName = constant('JPATH_' . strtoupper($client)) . '/language/overrides/' . $language . '.override.ini'; - $strings = LanguageHelper::parseIniFile($fileName); - - if (isset($strings[$data['id']])) - { - // If an existent string was edited check whether - // the name of the constant is still the same. - if ($data['key'] == $data['id']) - { - // If yes, simply override it. - $strings[$data['key']] = $data['override']; - } - else - { - // If no, delete the old string and prepend the new one. - unset($strings[$data['id']]); - $strings = array($data['key'] => $data['override']) + $strings; - } - } - else - { - // If it is a new override simply prepend it. - $strings = array($data['key'] => $data['override']) + $strings; - } - - // Write override.ini file with the strings. - if (LanguageHelper::saveToIniFile($fileName, $strings) === false) - { - return false; - } - - // If the override should be stored for both clients save - // it also for the other one and prevent endless recursion. - if (isset($data['both']) && $data['both'] && !$oppositeClient) - { - return $this->save($data, true); - } - - return true; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 2.5 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - if ($app->isClient('api')) - { - return; - } - - $client = $app->getUserStateFromRequest('com_languages.overrides.filter.client', 'filter_client', 0, 'int') ? 'administrator' : 'site'; - $this->setState('filter.client', $client); - - $language = $app->getUserStateFromRequest('com_languages.overrides.filter.language', 'filter_language', 'en-GB', 'cmd'); - $this->setState('filter.language', $language); - } + /** + * Method to get the record form. + * + * @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 \Joomla\CMS\Form\Form|bool A Form object on success, false on failure. + * + * @since 2.5 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_languages.override', 'override', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + $client = $this->getState('filter.client', 'site'); + $language = $this->getState('filter.language', 'en-GB'); + $langName = Language::getInstance($language)->getName(); + + if (!$langName) { + // If a language only exists in frontend, its metadata cannot be + // loaded in backend at the moment, so fall back to the language tag. + $langName = $language; + } + + $form->setValue('client', null, Text::_('COM_LANGUAGES_VIEW_OVERRIDE_CLIENT_' . strtoupper($client))); + $form->setValue('language', null, Text::sprintf('COM_LANGUAGES_VIEW_OVERRIDE_LANGUAGE', $langName, $language)); + $form->setValue('file', null, Path::clean(constant('JPATH_' . strtoupper($client)) . '/language/overrides/' . $language . '.override.ini')); + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 2.5 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_languages.edit.override.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_languages.override', $data); + + return $data; + } + + /** + * Method to get a single record. + * + * @param string $pk The key name. + * + * @return mixed Object on success, false otherwise. + * + * @since 2.5 + */ + public function getItem($pk = null) + { + $input = Factory::getApplication()->input; + $pk = !empty($pk) ? $pk : $input->get('id'); + $fileName = constant('JPATH_' . strtoupper($this->getState('filter.client'))) + . '/language/overrides/' . $this->getState('filter.language', 'en-GB') . '.override.ini'; + $strings = LanguageHelper::parseIniFile($fileName); + + $result = new \stdClass(); + $result->key = ''; + $result->override = ''; + + if (isset($strings[$pk])) { + $result->key = $pk; + $result->override = $strings[$pk]; + } + + $oppositeFileName = constant('JPATH_' . strtoupper($this->getState('filter.client') == 'site' ? 'administrator' : 'site')) + . '/language/overrides/' . $this->getState('filter.language', 'en-GB') . '.override.ini'; + $oppositeStrings = LanguageHelper::parseIniFile($oppositeFileName); + $result->both = isset($oppositeStrings[$pk]) && ($oppositeStrings[$pk] == $strings[$pk]); + + return $result; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * @param boolean $oppositeClient Indicates whether the override should not be created for the current client. + * + * @return boolean True on success, false otherwise. + * + * @since 2.5 + */ + public function save($data, $oppositeClient = false) + { + $app = Factory::getApplication(); + + if ($app->isClient('api')) { + $client = $this->getState('filter.client'); + $language = $this->getState('filter.language'); + } else { + $client = $app->getUserState('com_languages.overrides.filter.client', 0); + $language = $app->getUserState('com_languages.overrides.filter.language', 'en-GB'); + } + + // If the override should be created for both. + if ($oppositeClient) { + $client = 1 - $client; + } + + // Return false if the constant is a reserved word, i.e. YES, NO, NULL, FALSE, ON, OFF, NONE, TRUE + $reservedWords = array('YES', 'NO', 'NULL', 'FALSE', 'ON', 'OFF', 'NONE', 'TRUE'); + + if (in_array($data['key'], $reservedWords)) { + $this->setError(Text::_('COM_LANGUAGES_OVERRIDE_ERROR_RESERVED_WORDS')); + + return false; + } + + $client = $client ? 'administrator' : 'site'; + + // Parse the override.ini file in order to get the keys and strings. + $fileName = constant('JPATH_' . strtoupper($client)) . '/language/overrides/' . $language . '.override.ini'; + $strings = LanguageHelper::parseIniFile($fileName); + + if (isset($strings[$data['id']])) { + // If an existent string was edited check whether + // the name of the constant is still the same. + if ($data['key'] == $data['id']) { + // If yes, simply override it. + $strings[$data['key']] = $data['override']; + } else { + // If no, delete the old string and prepend the new one. + unset($strings[$data['id']]); + $strings = array($data['key'] => $data['override']) + $strings; + } + } else { + // If it is a new override simply prepend it. + $strings = array($data['key'] => $data['override']) + $strings; + } + + // Write override.ini file with the strings. + if (LanguageHelper::saveToIniFile($fileName, $strings) === false) { + return false; + } + + // If the override should be stored for both clients save + // it also for the other one and prevent endless recursion. + if (isset($data['both']) && $data['both'] && !$oppositeClient) { + return $this->save($data, true); + } + + return true; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 2.5 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + if ($app->isClient('api')) { + return; + } + + $client = $app->getUserStateFromRequest('com_languages.overrides.filter.client', 'filter_client', 0, 'int') ? 'administrator' : 'site'; + $this->setState('filter.client', $client); + + $language = $app->getUserStateFromRequest('com_languages.overrides.filter.language', 'filter_language', 'en-GB', 'cmd'); + $this->setState('filter.language', $language); + } } diff --git a/administrator/components/com_languages/src/Model/OverridesModel.php b/administrator/components/com_languages/src/Model/OverridesModel.php index fbb95e7d7a055..670ad4f4af486 100644 --- a/administrator/components/com_languages/src/Model/OverridesModel.php +++ b/administrator/components/com_languages/src/Model/OverridesModel.php @@ -1,4 +1,5 @@ getStoreId(); - - // Try to load the data from internal storage. - if (!empty($this->cache[$store])) - { - return $this->cache[$store]; - } - - $client = strtoupper($this->getState('filter.client')); - - // Parse the override.ini file in order to get the keys and strings. - $fileName = constant('JPATH_' . $client) . '/language/overrides/' . $this->getState('filter.language') . '.override.ini'; - $strings = LanguageHelper::parseIniFile($fileName); - - // Delete the override.ini file if empty. - if (file_exists($fileName) && $strings === array()) - { - File::delete($fileName); - } - - // Filter the loaded strings according to the search box. - $search = $this->getState('filter.search'); - - if ($search != '') - { - $search = preg_quote($search, '~'); - $matchvals = preg_grep('~' . $search . '~i', $strings); - $matchkeys = array_intersect_key($strings, array_flip(preg_grep('~' . $search . '~i', array_keys($strings)))); - $strings = array_merge($matchvals, $matchkeys); - } - - // Consider the ordering - if ($this->getState('list.ordering') == 'text') - { - if (strtoupper($this->getState('list.direction')) == 'DESC') - { - arsort($strings); - } - else - { - asort($strings); - } - } - else - { - if (strtoupper($this->getState('list.direction')) == 'DESC') - { - krsort($strings); - } - else - { - ksort($strings); - } - } - - // Consider the pagination. - if (!$all && $this->getState('list.limit') && $this->getTotal() > $this->getState('list.limit')) - { - $strings = array_slice($strings, $this->getStart(), $this->getState('list.limit'), true); - } - - // Add the items to the internal cache. - $this->cache[$store] = $strings; - - return $this->cache[$store]; - } - - /** - * Method to get the total number of overrides. - * - * @return integer The total number of overrides. - * - * @since 2.5 - */ - public function getTotal() - { - // Get a storage key. - $store = $this->getStoreId('getTotal'); - - // Try to load the data from internal storage - if (!empty($this->cache[$store])) - { - return $this->cache[$store]; - } - - // Add the total to the internal cache. - $this->cache[$store] = count($this->getOverrides(true)); - - return $this->cache[$store]; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 2.5 - */ - protected function populateState($ordering = 'key', $direction = 'asc') - { - // We call populate state first so that we can then set the filter.client and filter.language properties in afterwards - parent::populateState($ordering, $direction); - - $app = Factory::getApplication(); - - if ($app->isClient('api')) - { - return; - } - - $language_client = $this->getUserStateFromRequest('com_languages.overrides.language_client', 'language_client', '', 'cmd'); - $client = substr($language_client, -1); - $language = substr($language_client, 0, -1); - - // Sets the search filter. - $search = $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search'); - $this->setState('filter.search', $search); - - $this->setState('language_client', $language . $client); - $this->setState('filter.client', $client ? 'administrator' : 'site'); - $this->setState('filter.language', $language); - - // Add the 'language_client' value to the session to display a message if none selected - $app->setUserState('com_languages.overrides.language_client', $language . $client); - - // Add filters to the session because they won't be stored there by 'getUserStateFromRequest' if they aren't in the current request. - $app->setUserState('com_languages.overrides.filter.client', $client); - $app->setUserState('com_languages.overrides.filter.language', $language); - } - - /** - * Method to delete one or more overrides. - * - * @param array $cids Array of keys to delete. - * - * @return integer Number of successfully deleted overrides, boolean false if an error occurred. - * - * @since 2.5 - */ - public function delete($cids) - { - // Check permissions first. - if (!Factory::getUser()->authorise('core.delete', 'com_languages')) - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED')); - - return false; - } - - $app = Factory::getApplication(); - - if ($app->isClient('api')) - { - $cids = (array) $cids; - $client = $this->getState('filter.client'); - } - else - { - $filterclient = Factory::getApplication()->getUserState('com_languages.overrides.filter.client'); - $client = $filterclient == 0 ? 'site' : 'administrator'; - } - - // Parse the override.ini file in order to get the keys and strings. - $fileName = constant('JPATH_' . strtoupper($client)) . '/language/overrides/' . $this->getState('filter.language') . '.override.ini'; - $strings = LanguageHelper::parseIniFile($fileName); - - // Unset strings that shall be deleted - foreach ($cids as $key) - { - if (isset($strings[$key])) - { - unset($strings[$key]); - } - } - - // Write override.ini file with the strings. - if (LanguageHelper::saveToIniFile($fileName, $strings) === false) - { - return false; - } - - $this->cleanCache(); - - return count($cids); - } - - /** - * Removes all of the cached strings from the table. - * - * @return boolean result of operation - * - * @since 3.4.2 - */ - public function purge() - { - $db = $this->getDatabase(); - - // Note: TRUNCATE is a DDL operation - // This may or may not mean depending on your database - try - { - $db->truncateTable('#__overrider'); - } - catch (\RuntimeException $e) - { - return $e; - } - - Factory::getApplication()->enqueueMessage(Text::_('COM_LANGUAGES_VIEW_OVERRIDES_PURGE_SUCCESS')); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 2.5 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'key', + 'text', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Retrieves the overrides data + * + * @param boolean $all True if all overrides shall be returned without considering pagination, defaults to false + * + * @return array Array of objects containing the overrides of the override.ini file + * + * @since 2.5 + */ + public function getOverrides($all = false) + { + // Get a storage key. + $store = $this->getStoreId(); + + // Try to load the data from internal storage. + if (!empty($this->cache[$store])) { + return $this->cache[$store]; + } + + $client = strtoupper($this->getState('filter.client')); + + // Parse the override.ini file in order to get the keys and strings. + $fileName = constant('JPATH_' . $client) . '/language/overrides/' . $this->getState('filter.language') . '.override.ini'; + $strings = LanguageHelper::parseIniFile($fileName); + + // Delete the override.ini file if empty. + if (file_exists($fileName) && $strings === array()) { + File::delete($fileName); + } + + // Filter the loaded strings according to the search box. + $search = $this->getState('filter.search'); + + if ($search != '') { + $search = preg_quote($search, '~'); + $matchvals = preg_grep('~' . $search . '~i', $strings); + $matchkeys = array_intersect_key($strings, array_flip(preg_grep('~' . $search . '~i', array_keys($strings)))); + $strings = array_merge($matchvals, $matchkeys); + } + + // Consider the ordering + if ($this->getState('list.ordering') == 'text') { + if (strtoupper($this->getState('list.direction')) == 'DESC') { + arsort($strings); + } else { + asort($strings); + } + } else { + if (strtoupper($this->getState('list.direction')) == 'DESC') { + krsort($strings); + } else { + ksort($strings); + } + } + + // Consider the pagination. + if (!$all && $this->getState('list.limit') && $this->getTotal() > $this->getState('list.limit')) { + $strings = array_slice($strings, $this->getStart(), $this->getState('list.limit'), true); + } + + // Add the items to the internal cache. + $this->cache[$store] = $strings; + + return $this->cache[$store]; + } + + /** + * Method to get the total number of overrides. + * + * @return integer The total number of overrides. + * + * @since 2.5 + */ + public function getTotal() + { + // Get a storage key. + $store = $this->getStoreId('getTotal'); + + // Try to load the data from internal storage + if (!empty($this->cache[$store])) { + return $this->cache[$store]; + } + + // Add the total to the internal cache. + $this->cache[$store] = count($this->getOverrides(true)); + + return $this->cache[$store]; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 2.5 + */ + protected function populateState($ordering = 'key', $direction = 'asc') + { + // We call populate state first so that we can then set the filter.client and filter.language properties in afterwards + parent::populateState($ordering, $direction); + + $app = Factory::getApplication(); + + if ($app->isClient('api')) { + return; + } + + $language_client = $this->getUserStateFromRequest('com_languages.overrides.language_client', 'language_client', '', 'cmd'); + $client = substr($language_client, -1); + $language = substr($language_client, 0, -1); + + // Sets the search filter. + $search = $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search'); + $this->setState('filter.search', $search); + + $this->setState('language_client', $language . $client); + $this->setState('filter.client', $client ? 'administrator' : 'site'); + $this->setState('filter.language', $language); + + // Add the 'language_client' value to the session to display a message if none selected + $app->setUserState('com_languages.overrides.language_client', $language . $client); + + // Add filters to the session because they won't be stored there by 'getUserStateFromRequest' if they aren't in the current request. + $app->setUserState('com_languages.overrides.filter.client', $client); + $app->setUserState('com_languages.overrides.filter.language', $language); + } + + /** + * Method to delete one or more overrides. + * + * @param array $cids Array of keys to delete. + * + * @return integer Number of successfully deleted overrides, boolean false if an error occurred. + * + * @since 2.5 + */ + public function delete($cids) + { + // Check permissions first. + if (!Factory::getUser()->authorise('core.delete', 'com_languages')) { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED')); + + return false; + } + + $app = Factory::getApplication(); + + if ($app->isClient('api')) { + $cids = (array) $cids; + $client = $this->getState('filter.client'); + } else { + $filterclient = Factory::getApplication()->getUserState('com_languages.overrides.filter.client'); + $client = $filterclient == 0 ? 'site' : 'administrator'; + } + + // Parse the override.ini file in order to get the keys and strings. + $fileName = constant('JPATH_' . strtoupper($client)) . '/language/overrides/' . $this->getState('filter.language') . '.override.ini'; + $strings = LanguageHelper::parseIniFile($fileName); + + // Unset strings that shall be deleted + foreach ($cids as $key) { + if (isset($strings[$key])) { + unset($strings[$key]); + } + } + + // Write override.ini file with the strings. + if (LanguageHelper::saveToIniFile($fileName, $strings) === false) { + return false; + } + + $this->cleanCache(); + + return count($cids); + } + + /** + * Removes all of the cached strings from the table. + * + * @return boolean result of operation + * + * @since 3.4.2 + */ + public function purge() + { + $db = $this->getDatabase(); + + // Note: TRUNCATE is a DDL operation + // This may or may not mean depending on your database + try { + $db->truncateTable('#__overrider'); + } catch (\RuntimeException $e) { + return $e; + } + + Factory::getApplication()->enqueueMessage(Text::_('COM_LANGUAGES_VIEW_OVERRIDES_PURGE_SUCCESS')); + } } diff --git a/administrator/components/com_languages/src/Model/StringsModel.php b/administrator/components/com_languages/src/Model/StringsModel.php index fe1e991ddc82c..7641a68ba668f 100644 --- a/administrator/components/com_languages/src/Model/StringsModel.php +++ b/administrator/components/com_languages/src/Model/StringsModel.php @@ -1,4 +1,5 @@ getDatabase(); - - $app->setUserState('com_languages.overrides.cachedtime', null); - - // Empty the database cache first. - try - { - $db->truncateTable('#__overrider'); - } - catch (\RuntimeException $e) - { - return $e; - } - - // Create the insert query. - $query = $db->getQuery(true) - ->insert($db->quoteName('#__overrider')) - ->columns( - [ - $db->quoteName('constant'), - $db->quoteName('string'), - $db->quoteName('file'), - ] - ); - - // Initialize some variables. - $client = $app->getUserState('com_languages.overrides.filter.client', 'site') ? 'administrator' : 'site'; - $language = $app->getUserState('com_languages.overrides.filter.language', 'en-GB'); - - $base = constant('JPATH_' . strtoupper($client)); - $path = $base . '/language/' . $language; - - $files = array(); - - // Parse common language directory. - if (is_dir($path)) - { - $files = Folder::files($path, '.*ini$', false, true); - } - - // Parse language directories of components. - $files = array_merge($files, Folder::files($base . '/components', '.*ini$', 3, true)); - - // Parse language directories of modules. - $files = array_merge($files, Folder::files($base . '/modules', '.*ini$', 3, true)); - - // Parse language directories of templates. - $files = array_merge($files, Folder::files($base . '/templates', '.*ini$', 3, true)); - - // Parse language directories of plugins. - $files = array_merge($files, Folder::files(JPATH_PLUGINS, '.*ini$', 4, true)); - - // Parse all found ini files and add the strings to the database cache. - foreach ($files as $file) - { - // Only process if language file is for selected language - if (strpos($file, $language, strlen($base)) === false) - { - continue; - } - - $strings = LanguageHelper::parseIniFile($file); - - if ($strings) - { - $file = Path::clean($file); - - $query->clear('values') - ->clear('bounded'); - - foreach ($strings as $key => $string) - { - $query->values(implode(',', $query->bindArray([$key, $string, $file], ParameterType::STRING))); - } - - try - { - $db->setQuery($query); - $db->execute(); - } - catch (\RuntimeException $e) - { - return $e; - } - } - } - - // Update the cached time. - $app->setUserState('com_languages.overrides.cachedtime.' . $client . '.' . $language, time()); - - return true; - } - - /** - * Method for searching language strings. - * - * @return array|\Exception Array of results on success, \Exception object otherwise. - * - * @since 2.5 - */ - public function search() - { - $results = array(); - $input = Factory::getApplication()->input; - $filter = InputFilter::getInstance(); - $db = $this->getDatabase(); - $searchTerm = $input->getString('searchstring'); - - $limitstart = $input->getInt('more'); - - try - { - $searchstring = '%' . $filter->clean($searchTerm, 'TRIM') . '%'; - - // Create the search query. - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('constant'), - $db->quoteName('string'), - $db->quoteName('file'), - ] - ) - ->from($db->quoteName('#__overrider')); - - if ($input->get('searchtype') === 'constant') - { - $query->where($db->quoteName('constant') . ' LIKE :search'); - } - else - { - $query->where($db->quoteName('string') . ' LIKE :search'); - } - - $query->bind(':search', $searchstring); - - // Consider the limitstart according to the 'more' parameter and load the results. - $query->setLimit(10, $limitstart); - $db->setQuery($query); - $results['results'] = $db->loadObjectList(); - - // Check whether there are more results than already loaded. - $query->clear('select') - ->clear('limit') - ->select('COUNT(' . $db->quoteName('id') . ')'); - $db->setQuery($query); - - if ($db->loadResult() > $limitstart + 10) - { - // If this is set a 'More Results' link will be displayed in the view. - $results['more'] = $limitstart + 10; - } - } - catch (\RuntimeException $e) - { - return $e; - } - - return $results; - } + /** + * Method for refreshing the cache in the database with the known language strings. + * + * @return boolean|\Exception True on success, \Exception object otherwise. + * + * @since 2.5 + */ + public function refresh() + { + $app = Factory::getApplication(); + $db = $this->getDatabase(); + + $app->setUserState('com_languages.overrides.cachedtime', null); + + // Empty the database cache first. + try { + $db->truncateTable('#__overrider'); + } catch (\RuntimeException $e) { + return $e; + } + + // Create the insert query. + $query = $db->getQuery(true) + ->insert($db->quoteName('#__overrider')) + ->columns( + [ + $db->quoteName('constant'), + $db->quoteName('string'), + $db->quoteName('file'), + ] + ); + + // Initialize some variables. + $client = $app->getUserState('com_languages.overrides.filter.client', 'site') ? 'administrator' : 'site'; + $language = $app->getUserState('com_languages.overrides.filter.language', 'en-GB'); + + $base = constant('JPATH_' . strtoupper($client)); + $path = $base . '/language/' . $language; + + $files = array(); + + // Parse common language directory. + if (is_dir($path)) { + $files = Folder::files($path, '.*ini$', false, true); + } + + // Parse language directories of components. + $files = array_merge($files, Folder::files($base . '/components', '.*ini$', 3, true)); + + // Parse language directories of modules. + $files = array_merge($files, Folder::files($base . '/modules', '.*ini$', 3, true)); + + // Parse language directories of templates. + $files = array_merge($files, Folder::files($base . '/templates', '.*ini$', 3, true)); + + // Parse language directories of plugins. + $files = array_merge($files, Folder::files(JPATH_PLUGINS, '.*ini$', 4, true)); + + // Parse all found ini files and add the strings to the database cache. + foreach ($files as $file) { + // Only process if language file is for selected language + if (strpos($file, $language, strlen($base)) === false) { + continue; + } + + $strings = LanguageHelper::parseIniFile($file); + + if ($strings) { + $file = Path::clean($file); + + $query->clear('values') + ->clear('bounded'); + + foreach ($strings as $key => $string) { + $query->values(implode(',', $query->bindArray([$key, $string, $file], ParameterType::STRING))); + } + + try { + $db->setQuery($query); + $db->execute(); + } catch (\RuntimeException $e) { + return $e; + } + } + } + + // Update the cached time. + $app->setUserState('com_languages.overrides.cachedtime.' . $client . '.' . $language, time()); + + return true; + } + + /** + * Method for searching language strings. + * + * @return array|\Exception Array of results on success, \Exception object otherwise. + * + * @since 2.5 + */ + public function search() + { + $results = array(); + $input = Factory::getApplication()->input; + $filter = InputFilter::getInstance(); + $db = $this->getDatabase(); + $searchTerm = $input->getString('searchstring'); + + $limitstart = $input->getInt('more'); + + try { + $searchstring = '%' . $filter->clean($searchTerm, 'TRIM') . '%'; + + // Create the search query. + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('constant'), + $db->quoteName('string'), + $db->quoteName('file'), + ] + ) + ->from($db->quoteName('#__overrider')); + + if ($input->get('searchtype') === 'constant') { + $query->where($db->quoteName('constant') . ' LIKE :search'); + } else { + $query->where($db->quoteName('string') . ' LIKE :search'); + } + + $query->bind(':search', $searchstring); + + // Consider the limitstart according to the 'more' parameter and load the results. + $query->setLimit(10, $limitstart); + $db->setQuery($query); + $results['results'] = $db->loadObjectList(); + + // Check whether there are more results than already loaded. + $query->clear('select') + ->clear('limit') + ->select('COUNT(' . $db->quoteName('id') . ')'); + $db->setQuery($query); + + if ($db->loadResult() > $limitstart + 10) { + // If this is set a 'More Results' link will be displayed in the view. + $results['more'] = $limitstart + 10; + } + } catch (\RuntimeException $e) { + return $e; + } + + return $results; + } } diff --git a/administrator/components/com_languages/src/Service/HTML/Languages.php b/administrator/components/com_languages/src/Service/HTML/Languages.php index b4b4e423ebda1..3792cb50af6bf 100644 --- a/administrator/components/com_languages/src/Service/HTML/Languages.php +++ b/administrator/components/com_languages/src/Service/HTML/Languages.php @@ -1,4 +1,5 @@ '; - } + /** + * Method to generate an input radio button. + * + * @param integer $rowNum The row number. + * @param string $language Language tag. + * + * @return string HTML code. + */ + public function id($rowNum, $language) + { + return ''; + } - /** - * Method to generate an array of clients. - * - * @return array of client objects. - */ - public function clients() - { - return array( - HTMLHelper::_('select.option', 0, Text::_('JSITE')), - HTMLHelper::_('select.option', 1, Text::_('JADMINISTRATOR')) - ); - } + /** + * Method to generate an array of clients. + * + * @return array of client objects. + */ + public function clients() + { + return array( + HTMLHelper::_('select.option', 0, Text::_('JSITE')), + HTMLHelper::_('select.option', 1, Text::_('JADMINISTRATOR')) + ); + } - /** - * Returns an array of published state filter options. - * - * @return string The HTML code for the select tag. - * - * @since 1.6 - */ - public function publishedOptions() - { - // Build the active state filter options. - $options = array(); - $options[] = HTMLHelper::_('select.option', '1', 'JPUBLISHED'); - $options[] = HTMLHelper::_('select.option', '0', 'JUNPUBLISHED'); - $options[] = HTMLHelper::_('select.option', '-2', 'JTRASHED'); - $options[] = HTMLHelper::_('select.option', '*', 'JALL'); + /** + * Returns an array of published state filter options. + * + * @return string The HTML code for the select tag. + * + * @since 1.6 + */ + public function publishedOptions() + { + // Build the active state filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '1', 'JPUBLISHED'); + $options[] = HTMLHelper::_('select.option', '0', 'JUNPUBLISHED'); + $options[] = HTMLHelper::_('select.option', '-2', 'JTRASHED'); + $options[] = HTMLHelper::_('select.option', '*', 'JALL'); - return $options; - } + return $options; + } } diff --git a/administrator/components/com_languages/src/View/Installed/HtmlView.php b/administrator/components/com_languages/src/View/Installed/HtmlView.php index dc657fed998e5..fe8cae53eac71 100644 --- a/administrator/components/com_languages/src/View/Installed/HtmlView.php +++ b/administrator/components/com_languages/src/View/Installed/HtmlView.php @@ -1,4 +1,5 @@ option = $this->get('Option'); - $this->pagination = $this->get('Pagination'); - $this->rows = $this->get('Data'); - $this->total = $this->get('Total'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_languages'); - - if ((int) $this->state->get('client_id') === 1) - { - ToolbarHelper::title(Text::_('COM_LANGUAGES_VIEW_INSTALLED_ADMIN_TITLE'), 'comments langmanager'); - } - else - { - ToolbarHelper::title(Text::_('COM_LANGUAGES_VIEW_INSTALLED_SITE_TITLE'), 'comments langmanager'); - } - - if ($canDo->get('core.edit.state')) - { - ToolbarHelper::makeDefault('installed.setDefault'); - ToolbarHelper::divider(); - } - - if ($canDo->get('core.admin')) - { - // Add install languages link to the lang installer component. - $bar = Toolbar::getInstance('toolbar'); - - // Switch administrator language - if ($this->state->get('client_id', 0) == 1) - { - ToolbarHelper::custom('installed.switchadminlanguage', 'refresh', '', 'COM_LANGUAGES_SWITCH_ADMIN', true); - ToolbarHelper::divider(); - } - - $bar->appendButton('Link', 'upload', 'COM_LANGUAGES_INSTALL', 'index.php?option=com_installer&view=languages'); - ToolbarHelper::divider(); - - ToolbarHelper::preferences('com_languages'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Languages:_Installed'); - } + /** + * Option (component) name + * + * @var string + */ + protected $option = null; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * Languages information + * + * @var array + */ + protected $rows = null; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 4.0.0 + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse. + * + * @return void + */ + public function display($tpl = null) + { + $this->option = $this->get('Option'); + $this->pagination = $this->get('Pagination'); + $this->rows = $this->get('Data'); + $this->total = $this->get('Total'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_languages'); + + if ((int) $this->state->get('client_id') === 1) { + ToolbarHelper::title(Text::_('COM_LANGUAGES_VIEW_INSTALLED_ADMIN_TITLE'), 'comments langmanager'); + } else { + ToolbarHelper::title(Text::_('COM_LANGUAGES_VIEW_INSTALLED_SITE_TITLE'), 'comments langmanager'); + } + + if ($canDo->get('core.edit.state')) { + ToolbarHelper::makeDefault('installed.setDefault'); + ToolbarHelper::divider(); + } + + if ($canDo->get('core.admin')) { + // Add install languages link to the lang installer component. + $bar = Toolbar::getInstance('toolbar'); + + // Switch administrator language + if ($this->state->get('client_id', 0) == 1) { + ToolbarHelper::custom('installed.switchadminlanguage', 'refresh', '', 'COM_LANGUAGES_SWITCH_ADMIN', true); + ToolbarHelper::divider(); + } + + $bar->appendButton('Link', 'upload', 'COM_LANGUAGES_INSTALL', 'index.php?option=com_installer&view=languages'); + ToolbarHelper::divider(); + + ToolbarHelper::preferences('com_languages'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Languages:_Installed'); + } } diff --git a/administrator/components/com_languages/src/View/Language/HtmlView.php b/administrator/components/com_languages/src/View/Language/HtmlView.php index 60abd3670b7e7..12bf59a544d8d 100644 --- a/administrator/components/com_languages/src/View/Language/HtmlView.php +++ b/administrator/components/com_languages/src/View/Language/HtmlView.php @@ -1,4 +1,5 @@ item = $this->get('Item'); - $this->form = $this->get('Form'); - $this->state = $this->get('State'); - $this->canDo = ContentHelper::getActions('com_languages'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', 1); - $isNew = empty($this->item->lang_id); - $canDo = $this->canDo; - - ToolbarHelper::title( - Text::_($isNew ? 'COM_LANGUAGES_VIEW_LANGUAGE_EDIT_NEW_TITLE' : 'COM_LANGUAGES_VIEW_LANGUAGE_EDIT_EDIT_TITLE'), 'comments-2 langmanager' - ); - - $toolbarButtons = []; - - if (($isNew && $canDo->get('core.create')) || (!$isNew && $canDo->get('core.edit'))) - { - ToolbarHelper::apply('language.apply'); - - $toolbarButtons[] = ['save', 'language.save']; - } - - // If an existing item, can save to a copy only if we have create rights. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'language.save2new']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if ($isNew) - { - ToolbarHelper::cancel('language.cancel'); - } - else - { - ToolbarHelper::cancel('language.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Languages:_Edit_Content_Language'); - } + /** + * The active item + * + * @var object + */ + public $item; + + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + public $form; + + /** + * The model state + * + * @var CMSObject + */ + public $state; + + /** + * The actions the user is authorised to perform + * + * @var CMSObject + * + * @since 4.0.0 + */ + protected $canDo; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse. + * + * @return void + */ + public function display($tpl = null) + { + $this->item = $this->get('Item'); + $this->form = $this->get('Form'); + $this->state = $this->get('State'); + $this->canDo = ContentHelper::getActions('com_languages'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', 1); + $isNew = empty($this->item->lang_id); + $canDo = $this->canDo; + + ToolbarHelper::title( + Text::_($isNew ? 'COM_LANGUAGES_VIEW_LANGUAGE_EDIT_NEW_TITLE' : 'COM_LANGUAGES_VIEW_LANGUAGE_EDIT_EDIT_TITLE'), + 'comments-2 langmanager' + ); + + $toolbarButtons = []; + + if (($isNew && $canDo->get('core.create')) || (!$isNew && $canDo->get('core.edit'))) { + ToolbarHelper::apply('language.apply'); + + $toolbarButtons[] = ['save', 'language.save']; + } + + // If an existing item, can save to a copy only if we have create rights. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'language.save2new']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if ($isNew) { + ToolbarHelper::cancel('language.cancel'); + } else { + ToolbarHelper::cancel('language.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Languages:_Edit_Content_Language'); + } } diff --git a/administrator/components/com_languages/src/View/Languages/HtmlView.php b/administrator/components/com_languages/src/View/Languages/HtmlView.php index 8233611961c92..22cc8c2c8a793 100644 --- a/administrator/components/com_languages/src/View/Languages/HtmlView.php +++ b/administrator/components/com_languages/src/View/Languages/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar(): void - { - $canDo = ContentHelper::getActions('com_languages'); - - ToolbarHelper::title(Text::_('COM_LANGUAGES_VIEW_LANGUAGES_TITLE'), 'comments langmanager'); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('language.add'); - } - - if ($canDo->get('core.edit.state')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('languages.publish')->listCheck(true); - $childBar->unpublish('languages.unpublish')->listCheck(true); - - if ($this->state->get('filter.published') != -2) - { - $childBar->trash('languages.trash')->listCheck(true); - } - } - - if ($this->state->get('filter.published') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('languages.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin')) - { - // Add install languages link to the lang installer component. - $bar = Toolbar::getInstance('toolbar'); - $bar->appendButton('Link', 'upload', 'COM_LANGUAGES_INSTALL', 'index.php?option=com_installer&view=languages'); - ToolbarHelper::divider(); - - ToolbarHelper::preferences('com_languages'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Languages:_Content'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 4.0.0 + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar(): void + { + $canDo = ContentHelper::getActions('com_languages'); + + ToolbarHelper::title(Text::_('COM_LANGUAGES_VIEW_LANGUAGES_TITLE'), 'comments langmanager'); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('language.add'); + } + + if ($canDo->get('core.edit.state')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('languages.publish')->listCheck(true); + $childBar->unpublish('languages.unpublish')->listCheck(true); + + if ($this->state->get('filter.published') != -2) { + $childBar->trash('languages.trash')->listCheck(true); + } + } + + if ($this->state->get('filter.published') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('languages.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin')) { + // Add install languages link to the lang installer component. + $bar = Toolbar::getInstance('toolbar'); + $bar->appendButton('Link', 'upload', 'COM_LANGUAGES_INSTALL', 'index.php?option=com_installer&view=languages'); + ToolbarHelper::divider(); + + ToolbarHelper::preferences('com_languages'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Languages:_Content'); + } } diff --git a/administrator/components/com_languages/src/View/Multilangstatus/HtmlView.php b/administrator/components/com_languages/src/View/Multilangstatus/HtmlView.php index d083e24f92d6f..2f0d44be606bf 100644 --- a/administrator/components/com_languages/src/View/Multilangstatus/HtmlView.php +++ b/administrator/components/com_languages/src/View/Multilangstatus/HtmlView.php @@ -1,4 +1,5 @@ homes = MultilangstatusHelper::getHomes(); - $this->language_filter = Multilanguage::isEnabled(); - $this->switchers = MultilangstatusHelper::getLangswitchers(); - $this->listUsersError = MultilangstatusHelper::getContacts(); - $this->contentlangs = MultilangstatusHelper::getContentlangs(); - $this->site_langs = LanguageHelper::getInstalledLanguages(0); - $this->statuses = MultilangstatusHelper::getStatus(); - $this->homepages = Multilanguage::getSiteHomePages(); - $this->defaultHome = MultilangstatusHelper::getDefaultHomeModule(); - $this->default_lang = ComponentHelper::getParams('com_languages')->get('site', 'en-GB'); + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse. + * + * @return void + */ + public function display($tpl = null) + { + $this->homes = MultilangstatusHelper::getHomes(); + $this->language_filter = Multilanguage::isEnabled(); + $this->switchers = MultilangstatusHelper::getLangswitchers(); + $this->listUsersError = MultilangstatusHelper::getContacts(); + $this->contentlangs = MultilangstatusHelper::getContentlangs(); + $this->site_langs = LanguageHelper::getInstalledLanguages(0); + $this->statuses = MultilangstatusHelper::getStatus(); + $this->homepages = Multilanguage::getSiteHomePages(); + $this->defaultHome = MultilangstatusHelper::getDefaultHomeModule(); + $this->default_lang = ComponentHelper::getParams('com_languages')->get('site', 'en-GB'); - parent::display($tpl); - } + parent::display($tpl); + } } diff --git a/administrator/components/com_languages/src/View/Override/HtmlView.php b/administrator/components/com_languages/src/View/Override/HtmlView.php index a89fc5b559616..310733f1dcee0 100644 --- a/administrator/components/com_languages/src/View/Override/HtmlView.php +++ b/administrator/components/com_languages/src/View/Override/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - $app = Factory::getApplication(); - - $languageClient = $app->getUserStateFromRequest('com_languages.overrides.language_client', 'language_client'); - - if ($languageClient == null) - { - $app->enqueueMessage(Text::_('COM_LANGUAGES_OVERRIDE_FIRST_SELECT_MESSAGE'), 'warning'); - - $app->redirect('index.php?option=com_languages&view=overrides'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors)); - } - - // Check whether the cache has to be refreshed. - $cached_time = Factory::getApplication()->getUserState( - 'com_languages.overrides.cachedtime.' . $this->state->get('filter.client') . '.' . $this->state->get('filter.language'), - 0 - ); - - if (time() - $cached_time > 60 * 5) - { - $this->state->set('cache_expired', true); - } - - // Add strings for translations in \Javascript. - Text::script('COM_LANGUAGES_VIEW_OVERRIDE_NO_RESULTS'); - Text::script('COM_LANGUAGES_VIEW_OVERRIDE_REQUEST_ERROR'); - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Adds the page title and toolbar. - * - * @return void - * - * @since 2.5 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $canDo = ContentHelper::getActions('com_languages'); - - ToolbarHelper::title(Text::_('COM_LANGUAGES_VIEW_OVERRIDE_EDIT_TITLE'), 'comments langmanager'); - - $toolbarButtons = []; - - if ($canDo->get('core.edit')) - { - ToolbarHelper::apply('override.apply'); - - $toolbarButtons[] = ['save', 'override.save']; - } - - // This component does not support Save as Copy. - if ($canDo->get('core.edit') && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'override.save2new']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->key)) - { - ToolbarHelper::cancel('override.cancel'); - } - else - { - ToolbarHelper::cancel('override.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Languages:_Edit_Override'); - } + /** + * The form to use for the view. + * + * @var object + * @since 2.5 + */ + protected $form; + + /** + * The item to edit. + * + * @var object + * @since 2.5 + */ + protected $item; + + /** + * The model state. + * + * @var object + * @since 2.5 + */ + protected $state; + + /** + * Displays the view. + * + * @param string $tpl The name of the template file to parse + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + $app = Factory::getApplication(); + + $languageClient = $app->getUserStateFromRequest('com_languages.overrides.language_client', 'language_client'); + + if ($languageClient == null) { + $app->enqueueMessage(Text::_('COM_LANGUAGES_OVERRIDE_FIRST_SELECT_MESSAGE'), 'warning'); + + $app->redirect('index.php?option=com_languages&view=overrides'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors)); + } + + // Check whether the cache has to be refreshed. + $cached_time = Factory::getApplication()->getUserState( + 'com_languages.overrides.cachedtime.' . $this->state->get('filter.client') . '.' . $this->state->get('filter.language'), + 0 + ); + + if (time() - $cached_time > 60 * 5) { + $this->state->set('cache_expired', true); + } + + // Add strings for translations in \Javascript. + Text::script('COM_LANGUAGES_VIEW_OVERRIDE_NO_RESULTS'); + Text::script('COM_LANGUAGES_VIEW_OVERRIDE_REQUEST_ERROR'); + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Adds the page title and toolbar. + * + * @return void + * + * @since 2.5 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $canDo = ContentHelper::getActions('com_languages'); + + ToolbarHelper::title(Text::_('COM_LANGUAGES_VIEW_OVERRIDE_EDIT_TITLE'), 'comments langmanager'); + + $toolbarButtons = []; + + if ($canDo->get('core.edit')) { + ToolbarHelper::apply('override.apply'); + + $toolbarButtons[] = ['save', 'override.save']; + } + + // This component does not support Save as Copy. + if ($canDo->get('core.edit') && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'override.save2new']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->key)) { + ToolbarHelper::cancel('override.cancel'); + } else { + ToolbarHelper::cancel('override.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Languages:_Edit_Override'); + } } diff --git a/administrator/components/com_languages/src/View/Overrides/HtmlView.php b/administrator/components/com_languages/src/View/Overrides/HtmlView.php index 1795f4a2fcaed..c7ea570a22d63 100644 --- a/administrator/components/com_languages/src/View/Overrides/HtmlView.php +++ b/administrator/components/com_languages/src/View/Overrides/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->items = $this->get('Overrides'); - $this->languages = $this->get('Languages'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors)); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Adds the page title and toolbar. - * - * @return void - * - * @since 2.5 - */ - protected function addToolbar() - { - // Get the results for each action - $canDo = ContentHelper::getActions('com_languages'); - - ToolbarHelper::title(Text::_('COM_LANGUAGES_VIEW_OVERRIDES_TITLE'), 'comments langmanager'); - - if ($canDo->get('core.create')) - { - ToolbarHelper::addNew('override.add'); - } - - if ($canDo->get('core.delete') && $this->pagination->total) - { - ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'overrides.delete', 'JTOOLBAR_DELETE'); - } - - if ($this->getCurrentUser()->authorise('core.admin')) - { - ToolbarHelper::custom('overrides.purge', 'refresh', '', 'COM_LANGUAGES_VIEW_OVERRIDES_PURGE', false); - } - - if ($canDo->get('core.admin')) - { - ToolbarHelper::preferences('com_languages'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Languages:_Overrides'); - } + /** + * The items to list. + * + * @var array + * @since 2.5 + */ + protected $items; + + /** + * The pagination object. + * + * @var object + * @since 2.5 + */ + protected $pagination; + + /** + * The model state. + * + * @var object + * @since 2.5 + */ + protected $state; + + /** + * An array containing all frontend and backend languages + * + * @var array + * @since 4.0.0 + */ + protected $languages; + + /** + * Displays the view. + * + * @param string $tpl The name of the template file to parse. + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->items = $this->get('Overrides'); + $this->languages = $this->get('Languages'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors)); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Adds the page title and toolbar. + * + * @return void + * + * @since 2.5 + */ + protected function addToolbar() + { + // Get the results for each action + $canDo = ContentHelper::getActions('com_languages'); + + ToolbarHelper::title(Text::_('COM_LANGUAGES_VIEW_OVERRIDES_TITLE'), 'comments langmanager'); + + if ($canDo->get('core.create')) { + ToolbarHelper::addNew('override.add'); + } + + if ($canDo->get('core.delete') && $this->pagination->total) { + ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'overrides.delete', 'JTOOLBAR_DELETE'); + } + + if ($this->getCurrentUser()->authorise('core.admin')) { + ToolbarHelper::custom('overrides.purge', 'refresh', '', 'COM_LANGUAGES_VIEW_OVERRIDES_PURGE', false); + } + + if ($canDo->get('core.admin')) { + ToolbarHelper::preferences('com_languages'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Languages:_Overrides'); + } } diff --git a/administrator/components/com_languages/tmpl/installed/default.php b/administrator/components/com_languages/tmpl/installed/default.php index fc58225bfd116..10eb42adb6a75 100644 --- a/administrator/components/com_languages/tmpl/installed/default.php +++ b/administrator/components/com_languages/tmpl/installed/default.php @@ -1,4 +1,5 @@ escape($this->state->get('list.direction')); ?>
-
-
-
- $this)); ?> - rows)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - - getShortVersion()); - foreach ($this->rows as $i => $row) : - $canCreate = $user->authorise('core.create', 'com_languages'); - $canEdit = $user->authorise('core.edit', 'com_languages'); - $canChange = $user->authorise('core.edit.state', 'com_languages'); - ?> - - - - - - - - - - - - - - -
- , - , - -
-   - - - - - - - - - - - - - - - - - - -
- language); ?> - - - - escape($row->nativeName); ?> - - escape($row->language); ?> - - published, $i, 'installed.', !$row->published && $canChange); ?> - - - - version, $minorVersion) !== 0 || strpos($row->version, $currentShortVersion) !== 0) : ?> - version; ?> - - version; ?> - - - escape($row->creationDate); ?> - - escape($row->author); ?> - - escape($row->authorEmail)); ?> - - escape($row->extension_id); ?> -
+
+
+
+ $this)); ?> + rows)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + + getShortVersion()); + foreach ($this->rows as $i => $row) : + $canCreate = $user->authorise('core.create', 'com_languages'); + $canEdit = $user->authorise('core.edit', 'com_languages'); + $canChange = $user->authorise('core.edit.state', 'com_languages'); + ?> + + + + + + + + + + + + + + +
+ , + , + +
+   + + + + + + + + + + + + + + + + + + +
+ language); ?> + + + + escape($row->nativeName); ?> + + escape($row->language); ?> + + published, $i, 'installed.', !$row->published && $canChange); ?> + + + + version, $minorVersion) !== 0 || strpos($row->version, $currentShortVersion) !== 0) : ?> + version; ?> + + version; ?> + + + escape($row->creationDate); ?> + + escape($row->author); ?> + + escape($row->authorEmail)); ?> + + escape($row->extension_id); ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
-
-
+ + + + +
+
+
diff --git a/administrator/components/com_languages/tmpl/language/edit.php b/administrator/components/com_languages/tmpl/language/edit.php index 21999ae811e22..0f2cf809a22a2 100644 --- a/administrator/components/com_languages/tmpl/language/edit.php +++ b/administrator/components/com_languages/tmpl/language/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_languages.admin-language-edit-change-flag'); + ->useScript('form.validate') + ->useScript('com_languages.admin-language-edit-change-flag'); ?> @@ -25,61 +26,61 @@
- 'details', 'recall' => true, 'breakpoint' => 768]); ?> + 'details', 'recall' => true, 'breakpoint' => 768]); ?> - -
- -
- form->renderField('title'); ?> - form->renderField('title_native'); ?> - form->renderField('lang_code'); ?> - form->renderField('sef'); ?> -
-
- form->getLabel('image'); ?> -
-
- form->getInput('image'); ?> - - form->getValue('image') . '.gif', $this->form->getValue('image'), null, true); ?> - -
-
- canDo->get('core.edit.state')) : ?> - form->renderField('published'); ?> - + +
+ +
+ form->renderField('title'); ?> + form->renderField('title_native'); ?> + form->renderField('lang_code'); ?> + form->renderField('sef'); ?> +
+
+ form->getLabel('image'); ?> +
+
+ form->getInput('image'); ?> + + form->getValue('image') . '.gif', $this->form->getValue('image'), null, true); ?> + +
+
+ canDo->get('core.edit.state')) : ?> + form->renderField('published'); ?> + - form->renderField('access'); ?> - form->renderField('description'); ?> - form->renderField('lang_id'); ?> -
-
- + form->renderField('access'); ?> + form->renderField('description'); ?> + form->renderField('lang_id'); ?> +
+
+ - -
-
-
- -
- form->renderFieldset('site_name'); ?> -
-
-
-
-
- -
- form->renderFieldset('metadata'); ?> -
-
-
-
- + +
+
+
+ +
+ form->renderFieldset('site_name'); ?> +
+
+
+
+
+ +
+ form->renderFieldset('metadata'); ?> +
+
+
+
+ - + - - + +
diff --git a/administrator/components/com_languages/tmpl/languages/default.php b/administrator/components/com_languages/tmpl/languages/default.php index d41787d26e461..131e65d5f1cb2 100644 --- a/administrator/components/com_languages/tmpl/languages/default.php +++ b/administrator/components/com_languages/tmpl/languages/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); $saveOrder = $listOrder == 'a.ordering'; -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_languages&task=languages.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_languages&task=languages.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
-
-
-
- $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction=""> - items as $i => $item) : - $canCreate = $user->authorise('core.create', 'com_languages'); - $canEdit = $user->authorise('core.edit', 'com_languages'); - $canChange = $user->authorise('core.edit.state', 'com_languages'); - ?> - - - + + + + + + + + + + + + +
- , - , - -
- - - - - - - - - - - - - - - - - - - - - -
- lang_id, false, 'cid', 'cb', $item->title); ?> - - +
+
+ $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction=""> + items as $i => $item) : + $canCreate = $user->authorise('core.create', 'com_languages'); + $canEdit = $user->authorise('core.edit', 'com_languages'); + $canChange = $user->authorise('core.edit.state', 'com_languages'); + ?> + + + - - - - - - - - - - - - -
+ , + , + +
+ + + + + + + + + + + + + + + + + + + + + +
+ lang_id, false, 'cid', 'cb', $item->title); ?> + + - - - - - - - - - - - published, $i, 'languages.', $canChange); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - - - escape($item->title_native); ?> - - escape($item->lang_code); ?> - - escape($item->sef); ?> - - image) : ?> - image . '.gif', $item->image, array('class'=>'me-1'), true); ?>escape($item->image); ?> - - - - - escape($item->access_level); ?> - - home == '1') ? Text::_('JYES') : Text::_('JNO'); ?> - - escape($item->lang_id); ?> -
+ if (!$saveOrder) : + $disabledLabel = Text::_('JORDERINGDISABLED'); + $disableClassName = 'inactive'; + endif; ?> + + + + + + + + + +
+ published, $i, 'languages.', $canChange); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + + + escape($item->title_native); ?> + + escape($item->lang_code); ?> + + escape($item->sef); ?> + + image) : ?> + image . '.gif', $item->image, array('class' => 'me-1'), true); ?>escape($item->image); ?> + + + + + escape($item->access_level); ?> + + home == '1') ? Text::_('JYES') : Text::_('JNO'); ?> + + escape($item->lang_id); ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
-
-
+ + + + +
+ + diff --git a/administrator/components/com_languages/tmpl/multilangstatus/default.php b/administrator/components/com_languages/tmpl/multilangstatus/default.php index 270845822397c..00ab90ae7648b 100644 --- a/administrator/components/com_languages/tmpl/multilangstatus/default.php +++ b/administrator/components/com_languages/tmpl/multilangstatus/default.php @@ -1,4 +1,5 @@ homepages, 'language'); ?>
- language_filter && $this->switchers == 0) : ?> - homes == 1) : ?> -
- - - -
- -
- - - -
- - - default_lang, $content_languages)) : ?> -
- - - default_lang); ?> -
- - contentlangs as $contentlang) : ?> - lang_code == $this->default_lang && $contentlang->published != 1) : ?> -
- - - default_lang); ?> -
- - - - defaultHome == true) : ?> -
- - - -
- - statuses as $status) : ?> - - lang_code && $status->published == 1 && $status->home_published != 1) : ?> -
- - - lang_code, $status->lang_code); ?> -
- - - lang_code && $status->published == 0 && $status->home_published != 1) : ?> -
- - - lang_code, $status->lang_code); ?> -
- - - -
- - - -
- - -
- - - -
- - contentlangs as $contentlang) : ?> - lang_code, $this->homepages) && (!array_key_exists($contentlang->lang_code, $this->site_langs) || $contentlang->published != 1)) : ?> -
- - - lang_code); ?> -
- - lang_code, $this->site_langs)) : ?> -
- - - lang_code); ?> -
- - published == -2) : ?> -
- - - lang_code); ?> -
- - sef)) : ?> -
- - - lang_code); ?> -
- - - listUsersError) : ?> -
- - - -
    - listUsersError as $user) : ?> -
  • - name); ?> -
  • - -
-
- - - - -
- - - -
- - - - - - - - - - - - - - - + language_filter && $this->switchers == 0) : ?> + homes == 1) : ?> +
+ + + +
+ +
+ + + +
+ + + default_lang, $content_languages)) : ?> +
+ + + default_lang); ?> +
+ + contentlangs as $contentlang) : ?> + lang_code == $this->default_lang && $contentlang->published != 1) : ?> +
+ + + default_lang); ?> +
+ + + + defaultHome == true) : ?> +
+ + + +
+ + statuses as $status) : ?> + + lang_code && $status->published == 1 && $status->home_published != 1) : ?> +
+ + + lang_code, $status->lang_code); ?> +
+ + + lang_code && $status->published == 0 && $status->home_published != 1) : ?> +
+ + + lang_code, $status->lang_code); ?> +
+ + + +
+ + + +
+ + +
+ + + +
+ + contentlangs as $contentlang) : ?> + lang_code, $this->homepages) && (!array_key_exists($contentlang->lang_code, $this->site_langs) || $contentlang->published != 1)) : ?> +
+ + + lang_code); ?> +
+ + lang_code, $this->site_langs)) : ?> +
+ + + lang_code); ?> +
+ + published == -2) : ?> +
+ + + lang_code); ?> +
+ + sef)) : ?> +
+ + + lang_code); ?> +
+ + + listUsersError) : ?> +
+ + + +
    + listUsersError as $user) : ?> +
  • + name); ?> +
  • + +
+
+ + + + +
+ + + +
+ + +
- - - -
- - - language_filter) : ?> - - - - -
+ + + + + + + + + + + + - - - - - - - - - -
+ + + +
+ + + language_filter) : ?> + + + + +
- - - switchers != 0) : ?> - switchers; ?> - - - -
- homes > 1) : ?> - - - - - - homes > 1) : ?> - homes; ?> - - - -
- - - - - - - - - - - - statuses as $status) : ?> - element) : ?> - - - - - element) : ?> - - - - - - - - - - - contentlangs as $contentlang) : ?> - lang_code, $this->site_langs)) : ?> - - - - - - - - - - - - - - - - - - - - -
- - - - - - - -
- element; ?> - - - - - - - lang_code && $status->published == 1) : ?> - - - lang_code && $status->published == 0) : ?> - - - lang_code && $status->published == -2) : ?> - - - - - - - - home_published == 1) : ?> - - - home_published == 0) : ?> - - - home_published == -2) : ?> - - - - - - -
- lang_code; ?> - - - - - published == 1) : ?> - - - published == 0 && array_key_exists($contentlang->lang_code, $this->homepages)) : ?> - - - published == -2 && array_key_exists($contentlang->lang_code, $this->homepages)) : ?> - - - - - lang_code, $this->homepages)) : ?> - - - - - - -
- - - - - - - - - - -
- + + + + + + switchers != 0) : ?> + switchers; ?> + + + + + + + + homes > 1) : ?> + + + + + + + homes > 1) : ?> + homes; ?> + + + + + + + + + + + + + + + + + + + statuses as $status) : ?> + element) : ?> + + + + + element) : ?> + + + + + + + + + + + contentlangs as $contentlang) : ?> + lang_code, $this->site_langs)) : ?> + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+ element; ?> + + + + + + + lang_code && $status->published == 1) : ?> + + + lang_code && $status->published == 0) : ?> + + + lang_code && $status->published == -2) : ?> + + + + + + + + home_published == 1) : ?> + + + home_published == 0) : ?> + + + home_published == -2) : ?> + + + + + + +
+ lang_code; ?> + + + + + published == 1) : ?> + + + published == 0 && array_key_exists($contentlang->lang_code, $this->homepages)) : ?> + + + published == -2 && array_key_exists($contentlang->lang_code, $this->homepages)) : ?> + + + + + lang_code, $this->homepages)) : ?> + + + + + + +
+ + + + + + + + + + +
+
diff --git a/administrator/components/com_languages/tmpl/override/edit.php b/administrator/components/com_languages/tmpl/override/edit.php index 929c4cb36c117..fbad39d067997 100644 --- a/administrator/components/com_languages/tmpl/override/edit.php +++ b/administrator/components/com_languages/tmpl/override/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->usePreset('com_languages.overrider') - ->useScript('com_languages.admin-override-edit-refresh-searchstring'); + ->useScript('form.validate') + ->usePreset('com_languages.overrider') + ->useScript('com_languages.admin-override-edit-refresh-searchstring'); ?>
-
-
-
- item->key) ? Text::_('COM_LANGUAGES_VIEW_OVERRIDE_EDIT_NEW_OVERRIDE_LEGEND') : Text::_('COM_LANGUAGES_VIEW_OVERRIDE_EDIT_EDIT_OVERRIDE_LEGEND'); ?> -
- form->renderField('language'); ?> - form->renderField('client'); ?> - form->renderField('key'); ?> - form->renderField('override'); ?> +
+
+
+ item->key) ? Text::_('COM_LANGUAGES_VIEW_OVERRIDE_EDIT_NEW_OVERRIDE_LEGEND') : Text::_('COM_LANGUAGES_VIEW_OVERRIDE_EDIT_EDIT_OVERRIDE_LEGEND'); ?> +
+ form->renderField('language'); ?> + form->renderField('client'); ?> + form->renderField('key'); ?> + form->renderField('override'); ?> - state->get('filter.client') == 'administrator') : ?> - form->renderField('both'); ?> - + state->get('filter.client') == 'administrator') : ?> + form->renderField('both'); ?> + - form->renderField('file'); ?> -
-
-
+ form->renderField('file'); ?> +
+
+
-
- +
+ -
- -
- - - -
+
+ +
+ + + +
- - + + - -
-
+ +
+
diff --git a/administrator/components/com_languages/tmpl/overrides/default.php b/administrator/components/com_languages/tmpl/overrides/default.php index 7b4081d370457..954a765486f3e 100644 --- a/administrator/components/com_languages/tmpl/overrides/default.php +++ b/administrator/components/com_languages/tmpl/overrides/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $client = $this->state->get('filter.client') == 'site' ? Text::_('JSITE') : Text::_('JADMINISTRATOR'); $language = $this->state->get('filter.language'); @@ -28,93 +29,92 @@ $oppositeClient = $this->state->get('filter.client') == 'administrator' ? Text::_('JSITE') : Text::_('JADMINISTRATOR'); $oppositeFilename = constant('JPATH_' . strtoupper($this->state->get('filter.client') === 'site' ? 'administrator' : 'site')) - . '/language/overrides/' . $this->state->get('filter.language', 'en-GB') . '.override.ini'; + . '/language/overrides/' . $this->state->get('filter.language', 'en-GB') . '.override.ini'; $oppositeStrings = LanguageHelper::parseIniFile($oppositeFilename); ?>
-
-
-
- $this, 'options' => ['selectorFieldName' => 'language_client'])); ?> -
- items)) : ?> -
- - -
- - - - - - - - - - - - - - authorise('core.edit', 'com_languages'); ?> - - items as $key => $text) : ?> - - - - - - - - - - -
- , - , - -
- - - - - - - - - -
- - - - - escape($key); ?> - - escape($key); ?> - - - escape($text), 200); ?> - - - - - -
+
+
+
+ $this, 'options' => ['selectorFieldName' => 'language_client'])); ?> +
+ items)) : ?> +
+ + +
+ + + + + + + + + + + + + + authorise('core.edit', 'com_languages'); ?> + + items as $key => $text) : ?> + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + +
+ + + + + escape($key); ?> + + escape($key); ?> + + + escape($text), 200); ?> + + + + + +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
-
-
+ + + +
+
+
diff --git a/administrator/components/com_login/services/provider.php b/administrator/components/com_login/services/provider.php index bf55bc6227c68..eb064b3428dff 100644 --- a/administrator/components/com_login/services/provider.php +++ b/administrator/components/com_login/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Login')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Login')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Login')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Login')); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_login/src/Controller/DisplayController.php b/administrator/components/com_login/src/Controller/DisplayController.php index db93e6ebdb584..890d43c78e3ec 100644 --- a/administrator/components/com_login/src/Controller/DisplayController.php +++ b/administrator/components/com_login/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->set('view', 'login'); - $this->input->set('layout', 'default'); - - // For non-html formats we do not have login view, so just display 403 instead - if ($this->input->get('format', 'html') !== 'html') - { - throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - /** - * To prevent clickjacking, only allow the login form to be used inside a frame in the same origin. - * So send a X-Frame-Options HTTP Header with the SAMEORIGIN value. - * - * @link https://www.owasp.org/index.php/Clickjacking_Defense_Cheat_Sheet - * @link https://tools.ietf.org/html/rfc7034 - */ - $this->app->setHeader('X-Frame-Options', 'SAMEORIGIN'); - - return parent::display(); - } - - /** - * Method to log in a user. - * - * @return void - */ - public function login() - { - // Check for request forgeries. - $this->checkToken(); - - $app = $this->app; - - $model = $this->getModel('login'); - $credentials = $model->getState('credentials'); - $return = $model->getState('return'); - - $app->login($credentials, array('action' => 'core.login.admin')); - - if (Uri::isInternal($return) && strpos($return, 'tmpl=component') === false) - { - $app->redirect($return); - } - else - { - $app->redirect('index.php'); - } - } - - /** - * Method to log out a user. - * - * @return void - */ - public function logout() - { - $this->checkToken('request'); - - $app = $this->app; - - $userid = $this->input->getInt('uid', null); - - if ($app->get('shared_session', '0')) - { - $clientid = null; - } - else - { - $clientid = $userid ? 0 : 1; - } - - $options = array( - 'clientid' => $clientid, - ); - - $result = $app->logout($userid, $options); - - if (!($result instanceof \Exception)) - { - $model = $this->getModel('login'); - $return = $model->getState('return'); - - // Only redirect to an internal URL. - if (Uri::isInternal($return)) - { - $app->redirect($return); - } - } - - parent::display(); - } + /** + * Method to display a view. + * + * @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()}. + * + * @return static This object to support chaining. + * + * @since 1.5 + * @throws \Exception + */ + public function display($cachable = false, $urlparams = false) + { + /* + * Special treatment is required for this component, as this view may be called + * after a session timeout. We must reset the view and layout prior to display + * otherwise an error will occur. + */ + $this->input->set('view', 'login'); + $this->input->set('layout', 'default'); + + // For non-html formats we do not have login view, so just display 403 instead + if ($this->input->get('format', 'html') !== 'html') { + throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + /** + * To prevent clickjacking, only allow the login form to be used inside a frame in the same origin. + * So send a X-Frame-Options HTTP Header with the SAMEORIGIN value. + * + * @link https://www.owasp.org/index.php/Clickjacking_Defense_Cheat_Sheet + * @link https://tools.ietf.org/html/rfc7034 + */ + $this->app->setHeader('X-Frame-Options', 'SAMEORIGIN'); + + return parent::display(); + } + + /** + * Method to log in a user. + * + * @return void + */ + public function login() + { + // Check for request forgeries. + $this->checkToken(); + + $app = $this->app; + + $model = $this->getModel('login'); + $credentials = $model->getState('credentials'); + $return = $model->getState('return'); + + $app->login($credentials, array('action' => 'core.login.admin')); + + if (Uri::isInternal($return) && strpos($return, 'tmpl=component') === false) { + $app->redirect($return); + } else { + $app->redirect('index.php'); + } + } + + /** + * Method to log out a user. + * + * @return void + */ + public function logout() + { + $this->checkToken('request'); + + $app = $this->app; + + $userid = $this->input->getInt('uid', null); + + if ($app->get('shared_session', '0')) { + $clientid = null; + } else { + $clientid = $userid ? 0 : 1; + } + + $options = array( + 'clientid' => $clientid, + ); + + $result = $app->logout($userid, $options); + + if (!($result instanceof \Exception)) { + $model = $this->getModel('login'); + $return = $model->getState('return'); + + // Only redirect to an internal URL. + if (Uri::isInternal($return)) { + $app->redirect($return); + } + } + + parent::display(); + } } diff --git a/administrator/components/com_login/src/Dispatcher/Dispatcher.php b/administrator/components/com_login/src/Dispatcher/Dispatcher.php index f580d9d185382..2ca2893b37531 100644 --- a/administrator/components/com_login/src/Dispatcher/Dispatcher.php +++ b/administrator/components/com_login/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ input->get('task'); - - if ($task != 'login' && $task != 'logout') - { - $this->input->set('task', ''); - } - - // Reset controller name - $this->input->set('controller', null); - - parent::dispatch(); - } - - /** - * com_login does not require check permission, so we override checkAccess method and have it empty - * - * @return void - */ - protected function checkAccess() - { - } + /** + * Dispatch a controller task. + * + * @return void + * + * @since 4.0.0 + */ + public function dispatch() + { + // Only accept two values login and logout for `task` + $task = $this->input->get('task'); + + if ($task != 'login' && $task != 'logout') { + $this->input->set('task', ''); + } + + // Reset controller name + $this->input->set('controller', null); + + parent::dispatch(); + } + + /** + * com_login does not require check permission, so we override checkAccess method and have it empty + * + * @return void + */ + protected function checkAccess() + { + } } diff --git a/administrator/components/com_login/src/Model/LoginModel.php b/administrator/components/com_login/src/Model/LoginModel.php index 59377adbd106d..ffc371c68f438 100644 --- a/administrator/components/com_login/src/Model/LoginModel.php +++ b/administrator/components/com_login/src/Model/LoginModel.php @@ -1,4 +1,5 @@ input->getInputForRequestMethod(); - - $credentials = array( - 'username' => $input->get('username', '', 'USERNAME'), - 'password' => $input->get('passwd', '', 'RAW'), - 'secretkey' => $input->get('secretkey', '', 'RAW'), - ); - - $this->setState('credentials', $credentials); - - // Check for return URL from the request first. - if ($return = $input->get('return', '', 'BASE64')) - { - $return = base64_decode($return); - - if (!Uri::isInternal($return)) - { - $return = ''; - } - } - - // Set the return URL if empty. - if (empty($return)) - { - $return = 'index.php'; - } - - $this->setState('return', $return); - } - - /** - * Get the administrator login module by name (real, eg 'login' or folder, eg 'mod_login'). - * - * @param string $name The name of the module. - * @param string $title The title of the module, optional. - * - * @return object The Module object. - * - * @since 1.7.0 - */ - public static function getLoginModule($name = 'mod_login', $title = null) - { - $result = null; - $modules = self::_load($name); - $total = count($modules); - - for ($i = 0; $i < $total; $i++) - { - // Match the title if we're looking for a specific instance of the module. - if (!$title || $modules[$i]->title == $title) - { - $result = $modules[$i]; - break; - } - } - - // If we didn't find it, and the name is mod_something, create a dummy object. - if (is_null($result) && substr($name, 0, 4) == 'mod_') - { - $result = new \stdClass; - $result->id = 0; - $result->title = ''; - $result->module = $name; - $result->position = ''; - $result->content = ''; - $result->showtitle = 0; - $result->control = ''; - $result->params = ''; - $result->user = 0; - } - - return $result; - } - - /** - * Load login modules. - * - * Note that we load regardless of state or access level since access - * for public is the only thing that makes sense since users are not logged in - * and the module lets them log in. - * This is put in as a failsafe to avoid super user lock out caused by an unpublished - * login module or by a module set to have a viewing access level that is not Public. - * - * @param string $module The name of the module. - * - * @return array - * - * @since 1.7.0 - */ - protected static function _load($module) - { - static $clean; - - if (isset($clean)) - { - return $clean; - } - - $app = Factory::getApplication(); - $lang = Factory::getLanguage()->getTag(); - $clientId = (int) $app->getClientId(); - - /** @var \Joomla\CMS\Cache\Controller\CallbackController $cache */ - $cache = Factory::getCache('com_modules', 'callback'); - - $loader = function () use ($app, $lang, $module) { - $db = Factory::getDbo(); - - $query = $db->getQuery(true) - ->select( - $db->quoteName( - [ - 'm.id', - 'm.title', - 'm.module', - 'm.position', - 'm.showtitle', - 'm.params' - ] - ) - ) - ->from($db->quoteName('#__modules', 'm')) - ->where($db->quoteName('m.module') . ' = :module') - ->where($db->quoteName('m.client_id') . ' = 1') - ->join( - 'LEFT', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.element') . ' = ' . $db->quoteName('m.module') . - ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('m.client_id') - ) - ->where($db->quoteName('e.enabled') . ' = 1') - ->bind(':module', $module); - - // Filter by language. - if ($app->isClient('site') && $app->getLanguageFilter()) - { - $query->whereIn($db->quoteName('m.language'), [$lang, '*']); - } - - $query->order('m.position, m.ordering'); - - // Set the query. - $db->setQuery($query); - - return $db->loadObjectList(); - }; - - try - { - return $clean = $cache->get($loader, array(), md5(serialize(array($clientId, $lang)))); - } - catch (CacheExceptionInterface $cacheException) - { - try - { - return $loader(); - } - catch (ExecutionFailureException $databaseException) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $databaseException->getMessage()), - 'error' - ); - - return array(); - } - } - catch (ExecutionFailureException $databaseException) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $databaseException->getMessage()), 'error'); - - return array(); - } - } + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $input = Factory::getApplication()->input->getInputForRequestMethod(); + + $credentials = array( + 'username' => $input->get('username', '', 'USERNAME'), + 'password' => $input->get('passwd', '', 'RAW'), + 'secretkey' => $input->get('secretkey', '', 'RAW'), + ); + + $this->setState('credentials', $credentials); + + // Check for return URL from the request first. + if ($return = $input->get('return', '', 'BASE64')) { + $return = base64_decode($return); + + if (!Uri::isInternal($return)) { + $return = ''; + } + } + + // Set the return URL if empty. + if (empty($return)) { + $return = 'index.php'; + } + + $this->setState('return', $return); + } + + /** + * Get the administrator login module by name (real, eg 'login' or folder, eg 'mod_login'). + * + * @param string $name The name of the module. + * @param string $title The title of the module, optional. + * + * @return object The Module object. + * + * @since 1.7.0 + */ + public static function getLoginModule($name = 'mod_login', $title = null) + { + $result = null; + $modules = self::_load($name); + $total = count($modules); + + for ($i = 0; $i < $total; $i++) { + // Match the title if we're looking for a specific instance of the module. + if (!$title || $modules[$i]->title == $title) { + $result = $modules[$i]; + break; + } + } + + // If we didn't find it, and the name is mod_something, create a dummy object. + if (is_null($result) && substr($name, 0, 4) == 'mod_') { + $result = new \stdClass(); + $result->id = 0; + $result->title = ''; + $result->module = $name; + $result->position = ''; + $result->content = ''; + $result->showtitle = 0; + $result->control = ''; + $result->params = ''; + $result->user = 0; + } + + return $result; + } + + /** + * Load login modules. + * + * Note that we load regardless of state or access level since access + * for public is the only thing that makes sense since users are not logged in + * and the module lets them log in. + * This is put in as a failsafe to avoid super user lock out caused by an unpublished + * login module or by a module set to have a viewing access level that is not Public. + * + * @param string $module The name of the module. + * + * @return array + * + * @since 1.7.0 + */ + protected static function _load($module) + { + static $clean; + + if (isset($clean)) { + return $clean; + } + + $app = Factory::getApplication(); + $lang = Factory::getLanguage()->getTag(); + $clientId = (int) $app->getClientId(); + + /** @var \Joomla\CMS\Cache\Controller\CallbackController $cache */ + $cache = Factory::getCache('com_modules', 'callback'); + + $loader = function () use ($app, $lang, $module) { + $db = Factory::getDbo(); + + $query = $db->getQuery(true) + ->select( + $db->quoteName( + [ + 'm.id', + 'm.title', + 'm.module', + 'm.position', + 'm.showtitle', + 'm.params' + ] + ) + ) + ->from($db->quoteName('#__modules', 'm')) + ->where($db->quoteName('m.module') . ' = :module') + ->where($db->quoteName('m.client_id') . ' = 1') + ->join( + 'LEFT', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.element') . ' = ' . $db->quoteName('m.module') . + ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('m.client_id') + ) + ->where($db->quoteName('e.enabled') . ' = 1') + ->bind(':module', $module); + + // Filter by language. + if ($app->isClient('site') && $app->getLanguageFilter()) { + $query->whereIn($db->quoteName('m.language'), [$lang, '*']); + } + + $query->order('m.position, m.ordering'); + + // Set the query. + $db->setQuery($query); + + return $db->loadObjectList(); + }; + + try { + return $clean = $cache->get($loader, array(), md5(serialize(array($clientId, $lang)))); + } catch (CacheExceptionInterface $cacheException) { + try { + return $loader(); + } catch (ExecutionFailureException $databaseException) { + Factory::getApplication()->enqueueMessage( + Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $databaseException->getMessage()), + 'error' + ); + + return array(); + } + } catch (ExecutionFailureException $databaseException) { + Factory::getApplication()->enqueueMessage(Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $databaseException->getMessage()), 'error'); + + return array(); + } + } } diff --git a/administrator/components/com_login/src/View/Login/HtmlView.php b/administrator/components/com_login/src/View/Login/HtmlView.php index 40bcab2f8ae89..380e8debdbc0f 100644 --- a/administrator/components/com_login/src/View/Login/HtmlView.php +++ b/administrator/components/com_login/src/View/Login/HtmlView.php @@ -1,4 +1,5 @@ module != 'mod_login'){ - echo ModuleHelper::renderModule($module, array('id' => 'section-box')); + if ($module->module != 'mod_login') { + echo ModuleHelper::renderModule($module, array('id' => 'section-box')); + } } diff --git a/administrator/components/com_mails/services/provider.php b/administrator/components/com_mails/services/provider.php index 7790122707ca7..794bdfe532688 100644 --- a/administrator/components/com_mails/services/provider.php +++ b/administrator/components/com_mails/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Mails')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Mails')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Mails')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Mails')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_mails/src/Controller/DisplayController.php b/administrator/components/com_mails/src/Controller/DisplayController.php index b6cd1e43eea00..508aacf22acd0 100644 --- a/administrator/components/com_mails/src/Controller/DisplayController.php +++ b/administrator/components/com_mails/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'templates'); - $layout = $this->input->get('layout', ''); - $id = $this->input->getString('template_id'); - - // Check for edit form. - if ($view == 'template' && $layout == 'edit' && !$this->checkEditId('com_mails.edit.template', $id)) - { - // Somehow the person just went to the form - we don't allow that. - $this->setMessage(Text::sprintf('COM_MAILS_ERROR_UNHELD_ID', $id), 'error'); - $this->setRedirect(Route::_('index.php?option=com_mails&view=templates', false)); - - return false; - } - - return parent::display(); - } + /** + * The default view. + * + * @var string + * @since 4.0.0 + */ + protected $default_view = 'templates'; + + /** + * Method to display a view. + * + * @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()}. + * + * @return BaseController|boolean This object to support chaining. + * + * @since 4.0.0 + */ + public function display($cachable = false, $urlparams = array()) + { + $view = $this->input->get('view', 'templates'); + $layout = $this->input->get('layout', ''); + $id = $this->input->getString('template_id'); + + // Check for edit form. + if ($view == 'template' && $layout == 'edit' && !$this->checkEditId('com_mails.edit.template', $id)) { + // Somehow the person just went to the form - we don't allow that. + $this->setMessage(Text::sprintf('COM_MAILS_ERROR_UNHELD_ID', $id), 'error'); + $this->setRedirect(Route::_('index.php?option=com_mails&view=templates', false)); + + return false; + } + + return parent::display(); + } } diff --git a/administrator/components/com_mails/src/Controller/TemplateController.php b/administrator/components/com_mails/src/Controller/TemplateController.php index 2b980daaf07de..8696bbcb92194 100644 --- a/administrator/components/com_mails/src/Controller/TemplateController.php +++ b/administrator/components/com_mails/src/Controller/TemplateController.php @@ -1,4 +1,5 @@ view_item = 'template'; - $this->view_list = 'templates'; - } - - /** - * Method to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowAdd($data = []) - { - return false; - } - - /** - * Method to edit an existing record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key - * (sometimes required to avoid router collisions). - * - * @return boolean True if access level check and checkout passes, false otherwise. - * - * @since 4.0.0 - */ - public function edit($key = null, $urlVar = null) - { - // Do not cache the response to this, its a redirect, and mod_expires and google chrome browser bugs cache it forever! - $this->app->allowCache(false); - - $context = "$this->option.edit.$this->context"; - - // Get the previous record id (if any) and the current record id. - $template_id = $this->input->getCmd('template_id'); - $language = $this->input->getCmd('language'); - - // Access check. - if (!$this->allowEdit(array('template_id' => $template_id, 'language' => $language), $template_id)) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(), false - ) - ); - - return false; - } - - // Check-out succeeded, push the new record id into the session. - $this->holdEditId($context, $template_id . '.' . $language); - $this->app->setUserState($context . '.data', null); - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_item - . $this->getRedirectToItemAppend(array($template_id, $language), 'template_id'), false - ) - ); - - return true; - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param string[] $recordId The primary key id for the item in the first element and the language of the - * mail template in the second key. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $language = array_pop($recordId); - $return = parent::getRedirectToItemAppend(array_pop($recordId), $urlVar); - $return .= '&language=' . $language; - - return $return; - } - - /** - * Method to save a record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean True if successful, false otherwise. - * - * @since 4.0.0 - */ - public function save($key = null, $urlVar = null) - { - // Check for request forgeries. - $this->checkToken(); - - /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ - $model = $this->getModel(); - $data = $this->input->post->get('jform', array(), 'array'); - $context = "$this->option.edit.$this->context"; - $task = $this->getTask(); - - $recordId = $this->input->getCmd('template_id'); - $language = $this->input->getCmd('language'); - - // Populate the row id from the session. - $data['template_id'] = $recordId; - $data['language'] = $language; - - // Access check. - if (!$this->allowSave($data, 'template_id')) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(), false - ) - ); - - return false; - } - - // Validate the posted data. - // Sometimes the form needs some posted data, such as for plugins and modules. - $form = $model->getForm($data, false); - - if (!$form) - { - $this->app->enqueueMessage($model->getError(), 'error'); - - return false; - } - - // Send an object which can be modified through the plugin event - $objData = (object) $data; - $this->app->triggerEvent( - 'onContentNormaliseRequestData', - array($this->option . '.' . $this->context, $objData, $form) - ); - $data = (array) $objData; - - // Test whether the data is valid. - $validData = $model->validate($form, $data); - - // Check for validation errors. - if ($validData === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $this->app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Save the data in the session. - $this->app->setUserState($context . '.data', $data); - - // Redirect back to the edit screen. - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_item - . $this->getRedirectToItemAppend(array($recordId, $language), 'template_id'), false - ) - ); - - return false; - } - - // Attempt to save the data. - if (!$model->save($validData)) - { - // Save the data in the session. - $this->app->setUserState($context . '.data', $validData); - - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_item - . $this->getRedirectToItemAppend(array($recordId, $language), 'template_id'), false - ) - ); - - return false; - } - - $langKey = $this->text_prefix . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS'; - $prefix = Factory::getLanguage()->hasKey($langKey) ? $this->text_prefix : 'COM_MAILS'; - - $this->setMessage(Text::_($prefix . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS')); - - // Redirect the user and adjust session state based on the chosen task. - switch ($task) - { - case 'apply': - // Set the record data in the session. - $this->holdEditId($context, $recordId); - $this->app->setUserState($context . '.data', null); - - // Redirect back to the edit screen. - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_item - . $this->getRedirectToItemAppend(array($recordId, $language), 'template_id'), false - ) - ); - break; - - default: - // Clear the record id and data from the session. - $this->releaseEditId($context, $recordId); - $this->app->setUserState($context . '.data', null); - - $url = 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(); - - // Check if there is a return value - $return = $this->input->get('return', null, 'base64'); - - if (!is_null($return) && Uri::isInternal(base64_decode($return))) - { - $url = base64_decode($return); - } - - // Redirect to the list screen. - $this->setRedirect(Route::_($url, false)); - break; - } - - // Invoke the postSave method to allow for the child class to access the model. - $this->postSaveHook($model, $validData); - - return true; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * 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 Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->view_item = 'template'; + $this->view_list = 'templates'; + } + + /** + * Method to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowAdd($data = []) + { + return false; + } + + /** + * Method to edit an existing record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key + * (sometimes required to avoid router collisions). + * + * @return boolean True if access level check and checkout passes, false otherwise. + * + * @since 4.0.0 + */ + public function edit($key = null, $urlVar = null) + { + // Do not cache the response to this, its a redirect, and mod_expires and google chrome browser bugs cache it forever! + $this->app->allowCache(false); + + $context = "$this->option.edit.$this->context"; + + // Get the previous record id (if any) and the current record id. + $template_id = $this->input->getCmd('template_id'); + $language = $this->input->getCmd('language'); + + // Access check. + if (!$this->allowEdit(array('template_id' => $template_id, 'language' => $language), $template_id)) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . $this->getRedirectToListAppend(), + false + ) + ); + + return false; + } + + // Check-out succeeded, push the new record id into the session. + $this->holdEditId($context, $template_id . '.' . $language); + $this->app->setUserState($context . '.data', null); + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_item + . $this->getRedirectToItemAppend(array($template_id, $language), 'template_id'), + false + ) + ); + + return true; + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param string[] $recordId The primary key id for the item in the first element and the language of the + * mail template in the second key. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $language = array_pop($recordId); + $return = parent::getRedirectToItemAppend(array_pop($recordId), $urlVar); + $return .= '&language=' . $language; + + return $return; + } + + /** + * Method to save a record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 4.0.0 + */ + public function save($key = null, $urlVar = null) + { + // Check for request forgeries. + $this->checkToken(); + + /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ + $model = $this->getModel(); + $data = $this->input->post->get('jform', array(), 'array'); + $context = "$this->option.edit.$this->context"; + $task = $this->getTask(); + + $recordId = $this->input->getCmd('template_id'); + $language = $this->input->getCmd('language'); + + // Populate the row id from the session. + $data['template_id'] = $recordId; + $data['language'] = $language; + + // Access check. + if (!$this->allowSave($data, 'template_id')) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . $this->getRedirectToListAppend(), + false + ) + ); + + return false; + } + + // Validate the posted data. + // Sometimes the form needs some posted data, such as for plugins and modules. + $form = $model->getForm($data, false); + + if (!$form) { + $this->app->enqueueMessage($model->getError(), 'error'); + + return false; + } + + // Send an object which can be modified through the plugin event + $objData = (object) $data; + $this->app->triggerEvent( + 'onContentNormaliseRequestData', + array($this->option . '.' . $this->context, $objData, $form) + ); + $data = (array) $objData; + + // Test whether the data is valid. + $validData = $model->validate($form, $data); + + // Check for validation errors. + if ($validData === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $this->app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Save the data in the session. + $this->app->setUserState($context . '.data', $data); + + // Redirect back to the edit screen. + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_item + . $this->getRedirectToItemAppend(array($recordId, $language), 'template_id'), + false + ) + ); + + return false; + } + + // Attempt to save the data. + if (!$model->save($validData)) { + // Save the data in the session. + $this->app->setUserState($context . '.data', $validData); + + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_item + . $this->getRedirectToItemAppend(array($recordId, $language), 'template_id'), + false + ) + ); + + return false; + } + + $langKey = $this->text_prefix . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS'; + $prefix = Factory::getLanguage()->hasKey($langKey) ? $this->text_prefix : 'COM_MAILS'; + + $this->setMessage(Text::_($prefix . ($recordId === 0 && $this->app->isClient('site') ? '_SUBMIT' : '') . '_SAVE_SUCCESS')); + + // Redirect the user and adjust session state based on the chosen task. + switch ($task) { + case 'apply': + // Set the record data in the session. + $this->holdEditId($context, $recordId); + $this->app->setUserState($context . '.data', null); + + // Redirect back to the edit screen. + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_item + . $this->getRedirectToItemAppend(array($recordId, $language), 'template_id'), + false + ) + ); + break; + + default: + // Clear the record id and data from the session. + $this->releaseEditId($context, $recordId); + $this->app->setUserState($context . '.data', null); + + $url = 'index.php?option=' . $this->option . '&view=' . $this->view_list + . $this->getRedirectToListAppend(); + + // Check if there is a return value + $return = $this->input->get('return', null, 'base64'); + + if (!is_null($return) && Uri::isInternal(base64_decode($return))) { + $url = base64_decode($return); + } + + // Redirect to the list screen. + $this->setRedirect(Route::_($url, false)); + break; + } + + // Invoke the postSave method to allow for the child class to access the model. + $this->postSaveHook($model, $validData); + + return true; + } } diff --git a/administrator/components/com_mails/src/Helper/MailsHelper.php b/administrator/components/com_mails/src/Helper/MailsHelper.php index 87aff21a2eba1..a740988dcfa05 100644 --- a/administrator/components/com_mails/src/Helper/MailsHelper.php +++ b/administrator/components/com_mails/src/Helper/MailsHelper.php @@ -1,4 +1,5 @@ triggerEvent('onMailBeforeTagsRendering', array($mail->template_id, &$mail)); - - if (!isset($mail->params['tags']) || !count($mail->params['tags'])) - { - return ''; - } - - $html = '
    '; - - foreach ($mail->params['tags'] as $tag) - { - $html .= '
  • ' - . '' . $tag . '' - . '
  • '; - } - - $html .= '
'; - - return $html; - } - - /** - * Load the translation files for an extension - * - * @param string $extension Extension name - * - * @return void - * - * @since 4.0.0 - */ - public static function loadTranslationFiles($extension) - { - static $cache = array(); - - $extension = strtolower($extension); - - if (isset($cache[$extension])) - { - return; - } - - $lang = Factory::getLanguage(); - $source = ''; - - switch (substr($extension, 0, 3)) - { - case 'com': - default: - $source = JPATH_ADMINISTRATOR . '/components/' . $extension; - break; - - case 'mod': - $source = JPATH_SITE . '/modules/' . $extension; - break; - - case 'plg': - $parts = explode('_', $extension, 3); - - if (count($parts) > 2) - { - $source = JPATH_PLUGINS . '/' . $parts[1] . '/' . $parts[2]; - } - break; - } - - $lang->load($extension, JPATH_ADMINISTRATOR) - || $lang->load($extension, $source); - - if (!$lang->hasKey(strtoupper($extension))) - { - $lang->load($extension . '.sys', JPATH_ADMINISTRATOR) - || $lang->load($extension . '.sys', $source); - } - - $cache[$extension] = true; - } + /** + * Display a clickable list of tags for a mail template + * + * @param object $mail Row of the mail template. + * @param string $fieldname Name of the target field. + * + * @return string List of tags that can be inserted into a field. + * + * @since 4.0.0 + */ + public static function mailtags($mail, $fieldname) + { + Factory::getApplication()->triggerEvent('onMailBeforeTagsRendering', array($mail->template_id, &$mail)); + + if (!isset($mail->params['tags']) || !count($mail->params['tags'])) { + return ''; + } + + $html = '
    '; + + foreach ($mail->params['tags'] as $tag) { + $html .= '
  • ' + . '' . $tag . '' + . '
  • '; + } + + $html .= '
'; + + return $html; + } + + /** + * Load the translation files for an extension + * + * @param string $extension Extension name + * + * @return void + * + * @since 4.0.0 + */ + public static function loadTranslationFiles($extension) + { + static $cache = array(); + + $extension = strtolower($extension); + + if (isset($cache[$extension])) { + return; + } + + $lang = Factory::getLanguage(); + $source = ''; + + switch (substr($extension, 0, 3)) { + case 'com': + default: + $source = JPATH_ADMINISTRATOR . '/components/' . $extension; + break; + + case 'mod': + $source = JPATH_SITE . '/modules/' . $extension; + break; + + case 'plg': + $parts = explode('_', $extension, 3); + + if (count($parts) > 2) { + $source = JPATH_PLUGINS . '/' . $parts[1] . '/' . $parts[2]; + } + break; + } + + $lang->load($extension, JPATH_ADMINISTRATOR) + || $lang->load($extension, $source); + + if (!$lang->hasKey(strtoupper($extension))) { + $lang->load($extension . '.sys', JPATH_ADMINISTRATOR) + || $lang->load($extension . '.sys', $source); + } + + $cache[$extension] = true; + } } diff --git a/administrator/components/com_mails/src/Model/TemplateModel.php b/administrator/components/com_mails/src/Model/TemplateModel.php index fb5e2146e06f2..35c35c818912e 100644 --- a/administrator/components/com_mails/src/Model/TemplateModel.php +++ b/administrator/components/com_mails/src/Model/TemplateModel.php @@ -1,4 +1,5 @@ loadForm('com_mails.template', 'template', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - $params = ComponentHelper::getParams('com_mails'); - - if ($params->get('mail_style', 'plaintext') == 'plaintext') - { - $form->removeField('htmlbody'); - } - - if ($params->get('mail_style', 'plaintext') == 'html') - { - $form->removeField('body'); - } - - if (!$params->get('alternative_mailconfig', '0')) - { - $form->removeField('alternative_mailconfig', 'params'); - $form->removeField('mailfrom', 'params'); - $form->removeField('fromname', 'params'); - $form->removeField('replyto', 'params'); - $form->removeField('replytoname', 'params'); - $form->removeField('mailer', 'params'); - $form->removeField('sendmail', 'params'); - $form->removeField('smtphost', 'params'); - $form->removeField('smtpport', 'params'); - $form->removeField('smtpsecure', 'params'); - $form->removeField('smtpauth', 'params'); - $form->removeField('smtpuser', 'params'); - $form->removeField('smtppass', 'params'); - } - - if (!$params->get('copy_mails')) - { - $form->removeField('copyto', 'params'); - } - - if (!trim($params->get('attachment_folder', ''))) - { - $form->removeField('attachments'); - - return $form; - } - - try - { - $attachmentPath = rtrim(Path::check(JPATH_ROOT . '/' . $params->get('attachment_folder')), \DIRECTORY_SEPARATOR); - } - catch (\Exception $e) - { - $attachmentPath = ''; - } - - if (!$attachmentPath || $attachmentPath === Path::clean(JPATH_ROOT) || !is_dir($attachmentPath)) - { - $form->removeField('attachments'); - - return $form; - } - - $field = $form->getField('attachments'); - $subform = new \SimpleXMLElement($field->formsource); - $files = $subform->xpath('field[@name="file"]'); - $files[0]->addAttribute('directory', $attachmentPath); - $form->load('
' - . str_replace('', '', $subform->asXML()) - . '
' - ); - - return $form; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return CMSObject|boolean Object on success, false on failure. - * - * @since 4.0.0 - */ - public function getItem($pk = null) - { - $templateId = $this->getState($this->getName() . '.template_id'); - $language = $this->getState($this->getName() . '.language'); - $table = $this->getTable('Template', 'Table'); - - if ($templateId != '' && $language != '') - { - // Attempt to load the row. - $return = $table->load(array('template_id' => $templateId, 'language' => $language)); - - // Check for a table object error. - if ($return === false && $table->getError()) - { - $this->setError($table->getError()); - - return false; - } - } - - // Convert to the CMSObject before adding other data. - $properties = $table->getProperties(1); - $item = ArrayHelper::toObject($properties, CMSObject::class); - - if (property_exists($item, 'params')) - { - $registry = new Registry($item->params); - $item->params = $registry->toArray(); - } - - if (!$item->template_id) - { - $item->template_id = $templateId; - } - - if (!$item->language) - { - $item->language = $language; - } - - return $item; - } - - /** - * Get the master data for a mail template. - * - * @param integer $pk The id of the primary key. - * - * @return CMSObject|boolean Object on success, false on failure. - * - * @since 4.0.0 - */ - public function getMaster($pk = null) - { - $template_id = $this->getState($this->getName() . '.template_id'); - $table = $this->getTable('Template', 'Table'); - - if ($template_id != '') - { - // Attempt to load the row. - $return = $table->load(array('template_id' => $template_id, 'language' => '')); - - // Check for a table object error. - if ($return === false && $table->getError()) - { - $this->setError($table->getError()); - - return false; - } - } - - // Convert to the CMSObject before adding other data. - $properties = $table->getProperties(1); - $item = ArrayHelper::toObject($properties, CMSObject::class); - - if (property_exists($item, 'params')) - { - $registry = new Registry($item->params); - $item->params = $registry->toArray(); - } - - return $item; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 4.0.0 - * @throws \Exception - */ - public function getTable($name = 'Template', $prefix = 'Administrator', $options = array()) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 4.0.0 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $app = Factory::getApplication(); - $data = $app->getUserState('com_mails.edit.template.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_mails.template', $data); - - return $data; - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @since 4.0.0 - */ - public function validate($form, $data, $group = null) - { - $validLanguages = LanguageHelper::getContentLanguages(array(0, 1)); - - if (!array_key_exists($data['language'], $validLanguages)) - { - $this->setError(Text::_('COM_MAILS_FIELD_LANGUAGE_CODE_INVALID')); - - return false; - } - - return parent::validate($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success, False on error. - * - * @since 4.0.0 - */ - public function save($data) - { - $table = $this->getTable(); - $context = $this->option . '.' . $this->name; - - $key = $table->getKeyName(); - $template_id = (!empty($data['template_id'])) ? $data['template_id'] : $this->getState($this->getName() . '.template_id'); - $language = (!empty($data['language'])) ? $data['language'] : $this->getState($this->getName() . '.language'); - $isNew = true; - - // Include the plugins for the save events. - \Joomla\CMS\Plugin\PluginHelper::importPlugin($this->events_map['save']); - - // Allow an exception to be thrown. - try - { - // Load the row if saving an existing record. - $table->load(array('template_id' => $template_id, 'language' => $language)); - - if ($table->subject) - { - $isNew = false; - } - - // Load the default row - $table->load(array('template_id' => $template_id, 'language' => '')); - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Prepare the row for saving - $this->prepareTable($table); - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, $table, $isNew, $data)); - - if (in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - // Store the data. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Clean the cache. - $this->cleanCache(); - - // Trigger the after save event. - Factory::getApplication()->triggerEvent($this->event_after_save, array($context, $table, $isNew, $data)); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - $this->setState($this->getName() . '.new', $isNew); - - return true; - } - - /** - * Prepare and sanitise the table data prior to saving. - * - * @param Table $table A reference to a Table object. - * - * @return void - * - * @since 4.0.0 - */ - protected function prepareTable($table) - { - - } - - /** - * Stock method to auto-populate the model state. - * - * @return void - * - * @since 4.0.0 - */ - protected function populateState() - { - parent::populateState(); - - $template_id = Factory::getApplication()->input->getCmd('template_id'); - $this->setState($this->getName() . '.template_id', $template_id); - - $language = Factory::getApplication()->input->getCmd('language'); - $this->setState($this->getName() . '.language', $language); - } + /** + * The prefix to use with controller messages. + * + * @var string + * @since 4.0.0 + */ + protected $text_prefix = 'COM_MAILS'; + + /** + * The type alias for this content type (for example, 'com_content.article'). + * + * @var string + * @since 4.0.0 + */ + public $typeAlias = 'com_mails.template'; + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 4.0.0 + */ + protected function canDelete($record) + { + return false; + } + + /** + * Method to get the record form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return \Joomla\CMS\Form\Form|bool A JForm object on success, false on failure + * + * @since 4.0.0 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_mails.template', 'template', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + $params = ComponentHelper::getParams('com_mails'); + + if ($params->get('mail_style', 'plaintext') == 'plaintext') { + $form->removeField('htmlbody'); + } + + if ($params->get('mail_style', 'plaintext') == 'html') { + $form->removeField('body'); + } + + if (!$params->get('alternative_mailconfig', '0')) { + $form->removeField('alternative_mailconfig', 'params'); + $form->removeField('mailfrom', 'params'); + $form->removeField('fromname', 'params'); + $form->removeField('replyto', 'params'); + $form->removeField('replytoname', 'params'); + $form->removeField('mailer', 'params'); + $form->removeField('sendmail', 'params'); + $form->removeField('smtphost', 'params'); + $form->removeField('smtpport', 'params'); + $form->removeField('smtpsecure', 'params'); + $form->removeField('smtpauth', 'params'); + $form->removeField('smtpuser', 'params'); + $form->removeField('smtppass', 'params'); + } + + if (!$params->get('copy_mails')) { + $form->removeField('copyto', 'params'); + } + + if (!trim($params->get('attachment_folder', ''))) { + $form->removeField('attachments'); + + return $form; + } + + try { + $attachmentPath = rtrim(Path::check(JPATH_ROOT . '/' . $params->get('attachment_folder')), \DIRECTORY_SEPARATOR); + } catch (\Exception $e) { + $attachmentPath = ''; + } + + if (!$attachmentPath || $attachmentPath === Path::clean(JPATH_ROOT) || !is_dir($attachmentPath)) { + $form->removeField('attachments'); + + return $form; + } + + $field = $form->getField('attachments'); + $subform = new \SimpleXMLElement($field->formsource); + $files = $subform->xpath('field[@name="file"]'); + $files[0]->addAttribute('directory', $attachmentPath); + $form->load('
' + . str_replace('', '', $subform->asXML()) + . '
'); + + return $form; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return CMSObject|boolean Object on success, false on failure. + * + * @since 4.0.0 + */ + public function getItem($pk = null) + { + $templateId = $this->getState($this->getName() . '.template_id'); + $language = $this->getState($this->getName() . '.language'); + $table = $this->getTable('Template', 'Table'); + + if ($templateId != '' && $language != '') { + // Attempt to load the row. + $return = $table->load(array('template_id' => $templateId, 'language' => $language)); + + // Check for a table object error. + if ($return === false && $table->getError()) { + $this->setError($table->getError()); + + return false; + } + } + + // Convert to the CMSObject before adding other data. + $properties = $table->getProperties(1); + $item = ArrayHelper::toObject($properties, CMSObject::class); + + if (property_exists($item, 'params')) { + $registry = new Registry($item->params); + $item->params = $registry->toArray(); + } + + if (!$item->template_id) { + $item->template_id = $templateId; + } + + if (!$item->language) { + $item->language = $language; + } + + return $item; + } + + /** + * Get the master data for a mail template. + * + * @param integer $pk The id of the primary key. + * + * @return CMSObject|boolean Object on success, false on failure. + * + * @since 4.0.0 + */ + public function getMaster($pk = null) + { + $template_id = $this->getState($this->getName() . '.template_id'); + $table = $this->getTable('Template', 'Table'); + + if ($template_id != '') { + // Attempt to load the row. + $return = $table->load(array('template_id' => $template_id, 'language' => '')); + + // Check for a table object error. + if ($return === false && $table->getError()) { + $this->setError($table->getError()); + + return false; + } + } + + // Convert to the CMSObject before adding other data. + $properties = $table->getProperties(1); + $item = ArrayHelper::toObject($properties, CMSObject::class); + + if (property_exists($item, 'params')) { + $registry = new Registry($item->params); + $item->params = $registry->toArray(); + } + + return $item; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 4.0.0 + * @throws \Exception + */ + public function getTable($name = 'Template', $prefix = 'Administrator', $options = array()) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 4.0.0 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $app = Factory::getApplication(); + $data = $app->getUserState('com_mails.edit.template.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_mails.template', $data); + + return $data; + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @since 4.0.0 + */ + public function validate($form, $data, $group = null) + { + $validLanguages = LanguageHelper::getContentLanguages(array(0, 1)); + + if (!array_key_exists($data['language'], $validLanguages)) { + $this->setError(Text::_('COM_MAILS_FIELD_LANGUAGE_CODE_INVALID')); + + return false; + } + + return parent::validate($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success, False on error. + * + * @since 4.0.0 + */ + public function save($data) + { + $table = $this->getTable(); + $context = $this->option . '.' . $this->name; + + $key = $table->getKeyName(); + $template_id = (!empty($data['template_id'])) ? $data['template_id'] : $this->getState($this->getName() . '.template_id'); + $language = (!empty($data['language'])) ? $data['language'] : $this->getState($this->getName() . '.language'); + $isNew = true; + + // Include the plugins for the save events. + \Joomla\CMS\Plugin\PluginHelper::importPlugin($this->events_map['save']); + + // Allow an exception to be thrown. + try { + // Load the row if saving an existing record. + $table->load(array('template_id' => $template_id, 'language' => $language)); + + if ($table->subject) { + $isNew = false; + } + + // Load the default row + $table->load(array('template_id' => $template_id, 'language' => '')); + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Prepare the row for saving + $this->prepareTable($table); + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, $table, $isNew, $data)); + + if (in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + // Store the data. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Clean the cache. + $this->cleanCache(); + + // Trigger the after save event. + Factory::getApplication()->triggerEvent($this->event_after_save, array($context, $table, $isNew, $data)); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + $this->setState($this->getName() . '.new', $isNew); + + return true; + } + + /** + * Prepare and sanitise the table data prior to saving. + * + * @param Table $table A reference to a Table object. + * + * @return void + * + * @since 4.0.0 + */ + protected function prepareTable($table) + { + } + + /** + * Stock method to auto-populate the model state. + * + * @return void + * + * @since 4.0.0 + */ + protected function populateState() + { + parent::populateState(); + + $template_id = Factory::getApplication()->input->getCmd('template_id'); + $this->setState($this->getName() . '.template_id', $template_id); + + $language = Factory::getApplication()->input->getCmd('language'); + $this->setState($this->getName() . '.language', $language); + } } diff --git a/administrator/components/com_mails/src/Model/TemplatesModel.php b/administrator/components/com_mails/src/Model/TemplatesModel.php index 3e415ca54ed81..038174350e802 100644 --- a/administrator/components/com_mails/src/Model/TemplatesModel.php +++ b/administrator/components/com_mails/src/Model/TemplatesModel.php @@ -1,4 +1,5 @@ setState('params', $params); - - // List state information. - parent::populateState('a.template_id', 'asc'); - } - - /** - * Get a list of mail templates - * - * @return array - * - * @since 4.0.0 - */ - public function getItems() - { - $items = parent::getItems(); - $id = ''; - - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('language')) - ->from($db->quoteName('#__mail_templates')) - ->where($db->quoteName('template_id') . ' = :id') - ->where($db->quoteName('language') . ' != ' . $db->quote('')) - ->order($db->quoteName('language') . ' ASC') - ->bind(':id', $id); - - foreach ($items as $item) - { - $id = $item->template_id; - $db->setQuery($query); - $item->languages = $db->loadColumn(); - } - - return $items; - } - - /** - * Build an SQL query to load the list data. - * - * @return QueryInterface - * - * @since 4.0.0 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - $db->quoteName('a') . '.*' - ) - ); - $query->from($db->quoteName('#__mail_templates', 'a')) - ->where($db->quoteName('a.language') . ' = ' . $db->quote('')); - - // Filter by search in title. - if ($search = trim($this->getState('filter.search', ''))) - { - if (stripos($search, 'id:') === 0) - { - $search = substr($search, 3); - $query->where($db->quoteName('a.template_id') . ' = :search') - ->bind(':search', $search); - } - else - { - $search = '%' . str_replace(' ', '%', $search) . '%'; - $query->where( - '(' . $db->quoteName('a.template_id') . ' LIKE :search1' - . ' OR ' . $db->quoteName('a.subject') . ' LIKE :search2' - . ' OR ' . $db->quoteName('a.body') . ' LIKE :search3' - . ' OR ' . $db->quoteName('a.htmlbody') . ' LIKE :search4)' - ) - ->bind([':search1', ':search2', ':search3', ':search4'], $search); - } - } - - // Filter on the extension. - if ($extension = $this->getState('filter.extension')) - { - $query->where($db->quoteName('a.extension') . ' = :extension') - ->bind(':extension', $extension); - } - else - { - // Only show mail template from enabled extensions - $subQuery = $db->getQuery(true) - ->select($db->quoteName('name')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('enabled') . ' = 1'); - - $query->where($db->quoteName('a.extension') . ' IN(' . $subQuery . ')'); - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->join( - 'INNER', - $db->quoteName('#__mail_templates', 'b'), - $db->quoteName('b.template_id') . ' = ' . $db->quoteName('a.template_id') - . ' AND ' . $db->quoteName('b.language') . ' = :language' - ) - ->bind(':language', $language); - } - - // Add the list ordering clause - $listOrdering = $this->state->get('list.ordering', 'a.template_id'); - $orderDirn = $this->state->get('list.direction', 'ASC'); - - $query->order($db->escape($listOrdering) . ' ' . $db->escape($orderDirn)); - - return $query; - } - - /** - * Get list of extensions which are using mail templates - * - * @return array - * - * @since 4.0.0 - */ - public function getExtensions() - { - $db = $this->getDatabase(); - $subQuery = $db->getQuery(true) - ->select($db->quoteName('name')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('enabled') . ' = 1'); - - $query = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('extension')) - ->from($db->quoteName('#__mail_templates')) - ->where($db->quoteName('extension') . ' IN (' . $subQuery . ')'); - $db->setQuery($query); - - return $db->loadColumn(); - } - - /** - * Get a list of the current content languages - * - * @return array - * - * @since 4.0.0 - */ - public function getLanguages() - { - return LanguageHelper::getContentLanguages(array(0,1)); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 4.0.0 + * @throws \Exception + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'template_id', 'a.template_id', + 'language', 'a.language', + 'subject', 'a.subject', + 'body', 'a.body', + 'htmlbody', 'a.htmlbody', + 'extension' + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 4.0.0 + */ + protected function populateState($ordering = null, $direction = null) + { + // Load the parameters. + $params = ComponentHelper::getParams('com_mails'); + $this->setState('params', $params); + + // List state information. + parent::populateState('a.template_id', 'asc'); + } + + /** + * Get a list of mail templates + * + * @return array + * + * @since 4.0.0 + */ + public function getItems() + { + $items = parent::getItems(); + $id = ''; + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('language')) + ->from($db->quoteName('#__mail_templates')) + ->where($db->quoteName('template_id') . ' = :id') + ->where($db->quoteName('language') . ' != ' . $db->quote('')) + ->order($db->quoteName('language') . ' ASC') + ->bind(':id', $id); + + foreach ($items as $item) { + $id = $item->template_id; + $db->setQuery($query); + $item->languages = $db->loadColumn(); + } + + return $items; + } + + /** + * Build an SQL query to load the list data. + * + * @return QueryInterface + * + * @since 4.0.0 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + $db->quoteName('a') . '.*' + ) + ); + $query->from($db->quoteName('#__mail_templates', 'a')) + ->where($db->quoteName('a.language') . ' = ' . $db->quote('')); + + // Filter by search in title. + if ($search = trim($this->getState('filter.search', ''))) { + if (stripos($search, 'id:') === 0) { + $search = substr($search, 3); + $query->where($db->quoteName('a.template_id') . ' = :search') + ->bind(':search', $search); + } else { + $search = '%' . str_replace(' ', '%', $search) . '%'; + $query->where( + '(' . $db->quoteName('a.template_id') . ' LIKE :search1' + . ' OR ' . $db->quoteName('a.subject') . ' LIKE :search2' + . ' OR ' . $db->quoteName('a.body') . ' LIKE :search3' + . ' OR ' . $db->quoteName('a.htmlbody') . ' LIKE :search4)' + ) + ->bind([':search1', ':search2', ':search3', ':search4'], $search); + } + } + + // Filter on the extension. + if ($extension = $this->getState('filter.extension')) { + $query->where($db->quoteName('a.extension') . ' = :extension') + ->bind(':extension', $extension); + } else { + // Only show mail template from enabled extensions + $subQuery = $db->getQuery(true) + ->select($db->quoteName('name')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('enabled') . ' = 1'); + + $query->where($db->quoteName('a.extension') . ' IN(' . $subQuery . ')'); + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->join( + 'INNER', + $db->quoteName('#__mail_templates', 'b'), + $db->quoteName('b.template_id') . ' = ' . $db->quoteName('a.template_id') + . ' AND ' . $db->quoteName('b.language') . ' = :language' + ) + ->bind(':language', $language); + } + + // Add the list ordering clause + $listOrdering = $this->state->get('list.ordering', 'a.template_id'); + $orderDirn = $this->state->get('list.direction', 'ASC'); + + $query->order($db->escape($listOrdering) . ' ' . $db->escape($orderDirn)); + + return $query; + } + + /** + * Get list of extensions which are using mail templates + * + * @return array + * + * @since 4.0.0 + */ + public function getExtensions() + { + $db = $this->getDatabase(); + $subQuery = $db->getQuery(true) + ->select($db->quoteName('name')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('enabled') . ' = 1'); + + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('extension')) + ->from($db->quoteName('#__mail_templates')) + ->where($db->quoteName('extension') . ' IN (' . $subQuery . ')'); + $db->setQuery($query); + + return $db->loadColumn(); + } + + /** + * Get a list of the current content languages + * + * @return array + * + * @since 4.0.0 + */ + public function getLanguages() + { + return LanguageHelper::getContentLanguages(array(0,1)); + } } diff --git a/administrator/components/com_mails/src/Table/TemplateTable.php b/administrator/components/com_mails/src/Table/TemplateTable.php index c4c8f2f5b2792..acc262d854628 100644 --- a/administrator/components/com_mails/src/Table/TemplateTable.php +++ b/administrator/components/com_mails/src/Table/TemplateTable.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->item = $this->get('Item'); - $this->master = $this->get('Master'); - $this->form = $this->get('Form'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - list($component, $template_id) = explode('.', $this->item->template_id, 2); - $fields = array('subject', 'body', 'htmlbody'); - $this->templateData = array(); - $language = Factory::getLanguage(); - $language->load($component, JPATH_SITE, $this->item->language, true); - $language->load($component, JPATH_SITE . '/components/' . $component, $this->item->language, true); - $language->load($component, JPATH_ADMINISTRATOR, $this->item->language, true); - $language->load($component, JPATH_ADMINISTRATOR . '/components/' . $component, $this->item->language, true); - - $this->master->subject = Text::_($this->master->subject); - $this->master->body = Text::_($this->master->body); - - if ($this->master->htmlbody) - { - $this->master->htmlbody = Text::_($this->master->htmlbody); - } - else - { - $this->master->htmlbody = nl2br($this->master->body, false); - } - - $this->templateData = [ - 'subject' => $this->master->subject, - 'body' => $this->master->body, - 'htmlbody' => $this->master->htmlbody, - ]; - - foreach ($fields as $field) - { - if (is_null($this->item->$field) || $this->item->$field == '') - { - $this->item->$field = $this->master->$field; - $this->form->setValue($field, null, $this->item->$field); - } - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - $toolbar = Toolbar::getInstance(); - - ToolbarHelper::title( - Text::_('COM_MAILS_PAGE_EDIT_MAIL'), - 'pencil-2 article-add' - ); - - $saveGroup = $toolbar->dropdownButton('save-group'); - - $saveGroup->configure( - function (Toolbar $childBar) - { - $childBar->apply('template.apply'); - $childBar->save('template.save'); - } - ); - - $toolbar->cancel('template.cancel', 'JTOOLBAR_CLOSE'); - - $toolbar->divider(); - $toolbar->help('Mail_Template:_Edit'); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var CMSObject + */ + protected $item; + + /** + * The model state + * + * @var object + */ + protected $state; + + /** + * The template data + * + * @var array + */ + protected $templateData; + + /** + * Master data for the mail template + * + * @var CMSObject + */ + protected $master; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->item = $this->get('Item'); + $this->master = $this->get('Master'); + $this->form = $this->get('Form'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + list($component, $template_id) = explode('.', $this->item->template_id, 2); + $fields = array('subject', 'body', 'htmlbody'); + $this->templateData = array(); + $language = Factory::getLanguage(); + $language->load($component, JPATH_SITE, $this->item->language, true); + $language->load($component, JPATH_SITE . '/components/' . $component, $this->item->language, true); + $language->load($component, JPATH_ADMINISTRATOR, $this->item->language, true); + $language->load($component, JPATH_ADMINISTRATOR . '/components/' . $component, $this->item->language, true); + + $this->master->subject = Text::_($this->master->subject); + $this->master->body = Text::_($this->master->body); + + if ($this->master->htmlbody) { + $this->master->htmlbody = Text::_($this->master->htmlbody); + } else { + $this->master->htmlbody = nl2br($this->master->body, false); + } + + $this->templateData = [ + 'subject' => $this->master->subject, + 'body' => $this->master->body, + 'htmlbody' => $this->master->htmlbody, + ]; + + foreach ($fields as $field) { + if (is_null($this->item->$field) || $this->item->$field == '') { + $this->item->$field = $this->master->$field; + $this->form->setValue($field, null, $this->item->$field); + } + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + $toolbar = Toolbar::getInstance(); + + ToolbarHelper::title( + Text::_('COM_MAILS_PAGE_EDIT_MAIL'), + 'pencil-2 article-add' + ); + + $saveGroup = $toolbar->dropdownButton('save-group'); + + $saveGroup->configure( + function (Toolbar $childBar) { + $childBar->apply('template.apply'); + $childBar->save('template.save'); + } + ); + + $toolbar->cancel('template.cancel', 'JTOOLBAR_CLOSE'); + + $toolbar->divider(); + $toolbar->help('Mail_Template:_Edit'); + } } diff --git a/administrator/components/com_mails/src/View/Templates/HtmlView.php b/administrator/components/com_mails/src/View/Templates/HtmlView.php index f70abbb4604b1..649c0503387e0 100644 --- a/administrator/components/com_mails/src/View/Templates/HtmlView.php +++ b/administrator/components/com_mails/src/View/Templates/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->languages = $this->get('Languages'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $extensions = $this->get('Extensions'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Find and set site default language - $defaultLanguageTag = ComponentHelper::getParams('com_languages')->get('site'); - - foreach ($this->languages as $tag => $language) - { - if ($tag === $defaultLanguageTag) - { - $this->defaultLanguage = $language; - break; - } - } - - foreach ($extensions as $extension) - { - MailsHelper::loadTranslationFiles($extension); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - $user = $this->getCurrentUser(); - - ToolbarHelper::title(Text::_('COM_MAILS_MAILS_TITLE'), 'envelope'); - - if ($user->authorise('core.admin', 'com_mails') || $user->authorise('core.options', 'com_mails')) - { - $toolbar->preferences('com_mails'); - } - - $toolbar->help('Mail_Templates'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * An array of installed languages + * + * @var array + */ + protected $languages; + + /** + * Site default language + * + * @var \stdClass + */ + protected $defaultLanguage; + + /** + * The pagination object + * + * @var Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var Form + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + */ + public $activeFilters; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->languages = $this->get('Languages'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $extensions = $this->get('Extensions'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Find and set site default language + $defaultLanguageTag = ComponentHelper::getParams('com_languages')->get('site'); + + foreach ($this->languages as $tag => $language) { + if ($tag === $defaultLanguageTag) { + $this->defaultLanguage = $language; + break; + } + } + + foreach ($extensions as $extension) { + MailsHelper::loadTranslationFiles($extension); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + $user = $this->getCurrentUser(); + + ToolbarHelper::title(Text::_('COM_MAILS_MAILS_TITLE'), 'envelope'); + + if ($user->authorise('core.admin', 'com_mails') || $user->authorise('core.options', 'com_mails')) { + $toolbar->preferences('com_mails'); + } + + $toolbar->help('Mail_Templates'); + } } diff --git a/administrator/components/com_mails/tmpl/template/edit.php b/administrator/components/com_mails/tmpl/template/edit.php index f25a8e15f78e2..b0f14e322bb2f 100644 --- a/administrator/components/com_mails/tmpl/template/edit.php +++ b/administrator/components/com_mails/tmpl/template/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_mails.admin-email-template-edit'); + ->useScript('form.validate') + ->useScript('com_mails.admin-email-template-edit'); $this->useCoreUI = true; @@ -36,84 +37,84 @@ ?>
-
- 'general', 'recall' => true, 'breakpoint' => 768]); ?> - - -
-
-

- escape($this->item->language); ?> -

-
- escape($this->master->template_id); ?> -
-

-
-
- -
-
- form->renderField('subject'); ?> -
-
- -
-
- form->getField('body')) : ?> -
-
- form->renderField('body'); ?> -
-
- -
-

- master, 'body'); ?> -
-
-
- - - form->getField('htmlbody')) : ?> -
-
- form->renderField('htmlbody'); ?> -
-
- -
-

- master, 'htmlbody'); ?> -
-
-
- - - form->getField('attachments')) : ?> -
-
- form->renderField('attachments'); ?> -
-
- - - - - form->getFieldset('basic'))) : ?> - - - - -
- form->renderField('template_id'); ?> - form->renderField('language'); ?> - - - +
+ 'general', 'recall' => true, 'breakpoint' => 768]); ?> + + +
+
+

- escape($this->item->language); ?> +

+
+ escape($this->master->template_id); ?> +
+

+
+
+ +
+
+ form->renderField('subject'); ?> +
+
+ +
+
+ form->getField('body')) : ?> +
+
+ form->renderField('body'); ?> +
+
+ +
+

+ master, 'body'); ?> +
+
+
+ + + form->getField('htmlbody')) : ?> +
+
+ form->renderField('htmlbody'); ?> +
+
+ +
+

+ master, 'htmlbody'); ?> +
+
+
+ + + form->getField('attachments')) : ?> +
+
+ form->renderField('attachments'); ?> +
+
+ + + + + form->getFieldset('basic'))) : ?> + + + + +
+ form->renderField('template_id'); ?> + form->renderField('language'); ?> + + +
diff --git a/administrator/components/com_mails/tmpl/templates/default.php b/administrator/components/com_mails/tmpl/templates/default.php index d0c3d023b9a08..17e770ba763ea 100644 --- a/administrator/components/com_mails/tmpl/templates/default.php +++ b/administrator/components/com_mails/tmpl/templates/default.php @@ -1,4 +1,5 @@ escape($this->state->get('list.direction')); ?>
-
-
-
- $this)); - ?> - items)) : ?> -
- - -
- - - - - - - - languages) > 1) : ?> - - - - - - - - items as $i => $item) : - list($component, $sub_id) = explode('.', $item->template_id, 2); - $sub_id = str_replace('.', '_', $sub_id); - ?> - - - - languages) > 1) : ?> - - - - - - - -
- , - , - -
- - - - - - - - - -
- - - - - - - - - - - template_id; ?> -
+
+
+
+ $this)); + ?> + items)) : ?> +
+ + +
+ + + + + + + + languages) > 1) : ?> + + + + + + + + items as $i => $item) : + list($component, $sub_id) = explode('.', $item->template_id, 2); + $sub_id = str_replace('.', '_', $sub_id); + ?> + + + + languages) > 1) : ?> + + + + + + + +
+ , + , + +
+ + + + + + + + + +
+ + + + + + + + + + + template_id; ?> +
- - pagination->getListFooter(); ?> - + + pagination->getListFooter(); ?> + - - - -
-
-
+ + + +
+
+
diff --git a/administrator/components/com_media/helpers/media.php b/administrator/components/com_media/helpers/media.php index 9896de4ab32eb..f43d71e963bd7 100644 --- a/administrator/components/com_media/helpers/media.php +++ b/administrator/components/com_media/helpers/media.php @@ -1,13 +1,14 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt - */ -defined('_JEXEC') or die; + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + */ use Joomla\CMS\Object\CMSObject; @@ -18,33 +19,31 @@ */ abstract class MediaHelper { - /** - * Generates the URL to the object in the action logs component - * - * @param string $contentType The content type - * @param integer $id The integer id - * @param CMSObject $mediaObject The media object being uploaded - * - * @return string The link for the action log - * - * @since 3.9.27 - */ - public static function getContentTypeLink($contentType, $id, CMSObject $mediaObject) - { - if ($contentType === 'com_media.file') - { - return ''; - } + /** + * Generates the URL to the object in the action logs component + * + * @param string $contentType The content type + * @param integer $id The integer id + * @param CMSObject $mediaObject The media object being uploaded + * + * @return string The link for the action log + * + * @since 3.9.27 + */ + public static function getContentTypeLink($contentType, $id, CMSObject $mediaObject) + { + if ($contentType === 'com_media.file') { + return ''; + } - $link = 'index.php?option=com_media'; - $adapter = $mediaObject->get('adapter'); - $uploadedPath = $mediaObject->get('path'); + $link = 'index.php?option=com_media'; + $adapter = $mediaObject->get('adapter'); + $uploadedPath = $mediaObject->get('path'); - if (!empty($adapter) && !empty($uploadedPath)) - { - $link = $link . '&path=' . $adapter . ':' . $uploadedPath; - } + if (!empty($adapter) && !empty($uploadedPath)) { + $link = $link . '&path=' . $adapter . ':' . $uploadedPath; + } - return $link; - } + return $link; + } } diff --git a/administrator/components/com_media/layouts/toolbar/create-folder.php b/administrator/components/com_media/layouts/toolbar/create-folder.php index d5de1dd66c960..52ddda3bbac77 100644 --- a/administrator/components/com_media/layouts/toolbar/create-folder.php +++ b/administrator/components/com_media/layouts/toolbar/create-folder.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useScript('webcomponent.toolbar-button'); + ->useScript('webcomponent.toolbar-button'); $title = Text::_('COM_MEDIA_CREATE_NEW_FOLDER'); ?> - + diff --git a/administrator/components/com_media/layouts/toolbar/delete.php b/administrator/components/com_media/layouts/toolbar/delete.php index 2fc8cccbc0150..392e0e1f22c83 100644 --- a/administrator/components/com_media/layouts/toolbar/delete.php +++ b/administrator/components/com_media/layouts/toolbar/delete.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useScript('webcomponent.toolbar-button'); + ->useScript('webcomponent.toolbar-button'); $title = Text::_('JTOOLBAR_DELETE'); ?> - + diff --git a/administrator/components/com_media/layouts/toolbar/upload.php b/administrator/components/com_media/layouts/toolbar/upload.php index 988542e2cf3a9..11691117f375c 100644 --- a/administrator/components/com_media/layouts/toolbar/upload.php +++ b/administrator/components/com_media/layouts/toolbar/upload.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useScript('webcomponent.toolbar-button'); + ->useScript('webcomponent.toolbar-button'); $title = Text::_('JTOOLBAR_UPLOAD'); ?> - + diff --git a/administrator/components/com_media/services/provider.php b/administrator/components/com_media/services/provider.php index 53a08de23fd86..dbab9ac0dd137 100644 --- a/administrator/components/com_media/services/provider.php +++ b/administrator/components/com_media/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Media')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Media')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Media')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Media')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_media/src/Adapter/AdapterInterface.php b/administrator/components/com_media/src/Adapter/AdapterInterface.php index a752e347bb232..3c820c522ba3b 100644 --- a/administrator/components/com_media/src/Adapter/AdapterInterface.php +++ b/administrator/components/com_media/src/Adapter/AdapterInterface.php @@ -1,4 +1,5 @@ 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(Text::_('JINVALID_TOKEN_NOTICE'), 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(Text::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 (InvalidPathException $e) - { - $this->sendResponse($e, 400); - } - 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/cassiopeia - * /api/files/sampledata/cassiopeia - * - 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/cassiopeia&search=nasa5 - * /api/files/sampledata/cassiopeia?search=nasa5 - * To look up in same working directory set flag recursive=0 - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia&search=nasa5&recursive=0 - * /api/files/sampledata/cassiopeia?search=nasa5&recursive=0 - * - GET file information for a specific file: - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg - * /api/files/sampledata/cassiopeia/test.jpg - * - GET a temporary URL to a given file - * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg&url=1&temp=1 - * /api/files/sampledata/cassiopeia/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/cassiopeia/test.jpg&url=1 - * /api/files/sampledata/cassiopeia/test.jpg&url=1 - * - * @return array The data to send with the response - * - * @since 4.0.0 - * @throws \Exception - */ - public function getFiles() - { - // Grab options - $options = []; - $options['url'] = $this->input->getBool('url', false); - $options['search'] = $this->input->getString('search', ''); - $options['recursive'] = $this->input->getBool('recursive', true); - $options['content'] = $this->input->getBool('content', false); - - 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/cassiopeia/test - * /api/files/sampledata/cassiopeia/test - * - DELETE an existing file in a specific folder: - * index.php?option=com_media&task=api.files&path=/sampledata/cassiopeia/test.jpg - * /api/files/sampledata/cassiopeia/test.jpg - * - * @return null - * - * @since 4.0.0 - * @throws \Exception - */ - public function deleteFiles() - { - if (!$this->app->getIdentity()->authorise('core.delete', 'com_media')) - { - throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 403); - } - - $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/cassiopeia - * /api/files/sampledata/cassiopeia - * - * 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 4.0.0 - * @throws \Exception - */ - public function postFiles() - { - if (!$this->app->getIdentity()->authorise('core.create', 'com_media')) - { - throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED'), 403); - } - - $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); - - if ($mediaContent) - { - $this->checkContent(); - - // A file needs to be created - $name = $this->getModel()->createFile($adapter, $name, $path, $mediaContent, $override); - } - else - { - // A file needs to be created - $name = $this->getModel()->createFolder($adapter, $name, $path, $override); - } - - $options = []; - $options['url'] = $this->input->getBool('url', false); - - return $this->getModel()->getFile($adapter, $path . '/' . $name, $options); - } - - /** - * 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/cassiopeia/test.jpg - * /api/files/sampledata/cassiopeia/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/cassiopeia/test.jpg - * /api/files/sampledata/cassiopeia/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/cassiopeia/test.jpg - * /api/files/sampledata/cassiopeia/test.jpg - * - * JSON body: - * { - * "newPath" : "/path/to/destination", - * "move" : "0" - * } - * - * @return array The data to send with the response - * - * @since 4.0.0 - * @throws \Exception - */ - public function putFiles() - { - if (!$this->app->getIdentity()->authorise('core.edit', 'com_media')) - { - throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 403); - } - - $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(); - - $this->getModel()->updateFile($adapter, $name, str_replace($name, '', $path), $mediaContent); - } - - if ($newPath != null && $newPath !== $adapter . ':' . $path) - { - list($destinationAdapter, $destinationPath) = explode(':', $newPath, 2); - - if ($move) - { - $destinationPath = $this->getModel()->move($adapter, $path, $destinationPath, false); - } - else - { - $destinationPath = $this->getModel()->copy($adapter, $path, $destinationPath, false); - } - - $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 integer $responseCode The response code - * - * @return void - * - * @since 4.0.0 - */ - private function sendResponse($data = null, int $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 BaseModel|boolean Model object on success; otherwise false on failure. - * - * @since 4.0.0 - */ - public function getModel($name = 'Api', $prefix = 'Administrator', $config = []) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Performs various checks if it is allowed to save the content. - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - private function checkContent() - { - $helper = new MediaHelper; - $contentLength = $this->input->server->getInt('CONTENT_LENGTH'); - $params = ComponentHelper::getParams('com_media'); - $paramsUploadMaxsize = $params->get('upload_maxsize', 0) * 1024 * 1024; - $uploadMaxFilesize = $helper->toBytes(ini_get('upload_max_filesize')); - $postMaxSize = $helper->toBytes(ini_get('post_max_size')); - $memoryLimit = $helper->toBytes(ini_get('memory_limit')); - - if (($paramsUploadMaxsize > 0 && $contentLength > $paramsUploadMaxsize) - || ($uploadMaxFilesize > 0 && $contentLength > $uploadMaxFilesize) - || ($postMaxSize > 0 && $contentLength > $postMaxSize) - || ($memoryLimit > -1 && $contentLength > $memoryLimit) - ) - { - throw new \Exception(Text::_('COM_MEDIA_ERROR_WARNFILETOOLARGE'), 403); - } - } - - /** - * Get the Adapter. - * - * @return string - * - * @since 4.0.0 - */ - private function getAdapter() - { - $parts = explode(':', $this->input->getString('path', ''), 2); - - if (count($parts) < 1) - { - return null; - } - - return $parts[0]; - } - - /** - * Get the Path. - * - * @return string - * - * @since 4.0.0 - */ - private function getPath() - { - $parts = explode(':', $this->input->getString('path', ''), 2); - - if (count($parts) < 2) - { - return null; - } - - return $parts[1]; - } + /** + * Execute a task by triggering a method in the derived class. + * + * @param string $task The task to perform. If no matching task is found, the '__default' task is executed, if defined. + * + * @return mixed The value returned by the called method. + * + * @since 4.0.0 + * @throws \Exception + */ + public function execute($task) + { + $method = $this->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(Text::_('JINVALID_TOKEN_NOTICE'), 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(Text::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 (InvalidPathException $e) { + $this->sendResponse($e, 400); + } 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/cassiopeia + * /api/files/sampledata/cassiopeia + * - 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/cassiopeia&search=nasa5 + * /api/files/sampledata/cassiopeia?search=nasa5 + * To look up in same working directory set flag recursive=0 + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia&search=nasa5&recursive=0 + * /api/files/sampledata/cassiopeia?search=nasa5&recursive=0 + * - GET file information for a specific file: + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg + * /api/files/sampledata/cassiopeia/test.jpg + * - GET a temporary URL to a given file + * index.php?option=com_media&task=api.files&format=json&path=/sampledata/cassiopeia/test.jpg&url=1&temp=1 + * /api/files/sampledata/cassiopeia/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/cassiopeia/test.jpg&url=1 + * /api/files/sampledata/cassiopeia/test.jpg&url=1 + * + * @return array The data to send with the response + * + * @since 4.0.0 + * @throws \Exception + */ + public function getFiles() + { + // Grab options + $options = []; + $options['url'] = $this->input->getBool('url', false); + $options['search'] = $this->input->getString('search', ''); + $options['recursive'] = $this->input->getBool('recursive', true); + $options['content'] = $this->input->getBool('content', false); + + 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/cassiopeia/test + * /api/files/sampledata/cassiopeia/test + * - DELETE an existing file in a specific folder: + * index.php?option=com_media&task=api.files&path=/sampledata/cassiopeia/test.jpg + * /api/files/sampledata/cassiopeia/test.jpg + * + * @return null + * + * @since 4.0.0 + * @throws \Exception + */ + public function deleteFiles() + { + if (!$this->app->getIdentity()->authorise('core.delete', 'com_media')) { + throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 403); + } + + $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/cassiopeia + * /api/files/sampledata/cassiopeia + * + * 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 4.0.0 + * @throws \Exception + */ + public function postFiles() + { + if (!$this->app->getIdentity()->authorise('core.create', 'com_media')) { + throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED'), 403); + } + + $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); + + if ($mediaContent) { + $this->checkContent(); + + // A file needs to be created + $name = $this->getModel()->createFile($adapter, $name, $path, $mediaContent, $override); + } else { + // A file needs to be created + $name = $this->getModel()->createFolder($adapter, $name, $path, $override); + } + + $options = []; + $options['url'] = $this->input->getBool('url', false); + + return $this->getModel()->getFile($adapter, $path . '/' . $name, $options); + } + + /** + * 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/cassiopeia/test.jpg + * /api/files/sampledata/cassiopeia/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/cassiopeia/test.jpg + * /api/files/sampledata/cassiopeia/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/cassiopeia/test.jpg + * /api/files/sampledata/cassiopeia/test.jpg + * + * JSON body: + * { + * "newPath" : "/path/to/destination", + * "move" : "0" + * } + * + * @return array The data to send with the response + * + * @since 4.0.0 + * @throws \Exception + */ + public function putFiles() + { + if (!$this->app->getIdentity()->authorise('core.edit', 'com_media')) { + throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 403); + } + + $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(); + + $this->getModel()->updateFile($adapter, $name, str_replace($name, '', $path), $mediaContent); + } + + if ($newPath != null && $newPath !== $adapter . ':' . $path) { + list($destinationAdapter, $destinationPath) = explode(':', $newPath, 2); + + if ($move) { + $destinationPath = $this->getModel()->move($adapter, $path, $destinationPath, false); + } else { + $destinationPath = $this->getModel()->copy($adapter, $path, $destinationPath, false); + } + + $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 integer $responseCode The response code + * + * @return void + * + * @since 4.0.0 + */ + private function sendResponse($data = null, int $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 BaseModel|boolean Model object on success; otherwise false on failure. + * + * @since 4.0.0 + */ + public function getModel($name = 'Api', $prefix = 'Administrator', $config = []) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Performs various checks if it is allowed to save the content. + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + private function checkContent() + { + $helper = new MediaHelper(); + $contentLength = $this->input->server->getInt('CONTENT_LENGTH'); + $params = ComponentHelper::getParams('com_media'); + $paramsUploadMaxsize = $params->get('upload_maxsize', 0) * 1024 * 1024; + $uploadMaxFilesize = $helper->toBytes(ini_get('upload_max_filesize')); + $postMaxSize = $helper->toBytes(ini_get('post_max_size')); + $memoryLimit = $helper->toBytes(ini_get('memory_limit')); + + if ( + ($paramsUploadMaxsize > 0 && $contentLength > $paramsUploadMaxsize) + || ($uploadMaxFilesize > 0 && $contentLength > $uploadMaxFilesize) + || ($postMaxSize > 0 && $contentLength > $postMaxSize) + || ($memoryLimit > -1 && $contentLength > $memoryLimit) + ) { + throw new \Exception(Text::_('COM_MEDIA_ERROR_WARNFILETOOLARGE'), 403); + } + } + + /** + * Get the Adapter. + * + * @return string + * + * @since 4.0.0 + */ + private function getAdapter() + { + $parts = explode(':', $this->input->getString('path', ''), 2); + + if (count($parts) < 1) { + return null; + } + + return $parts[0]; + } + + /** + * Get the Path. + * + * @return string + * + * @since 4.0.0 + */ + private function getPath() + { + $parts = explode(':', $this->input->getString('path', ''), 2); + + if (count($parts) < 2) { + return null; + } + + return $parts[1]; + } } diff --git a/administrator/components/com_media/src/Controller/DisplayController.php b/administrator/components/com_media/src/Controller/DisplayController.php index 1afaa99e974bd..af55d78a6fa91 100644 --- a/administrator/components/com_media/src/Controller/DisplayController.php +++ b/administrator/components/com_media/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ 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!'); - } - - // Check if the plugin is disabled, if so redirect to control panel - if (!PluginHelper::isEnabled('filesystem', $pluginName)) - { - throw new \Exception('Plugin ' . $pluginName . ' is disabled.'); - } - - // 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) $this->app->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 = $eventResults['action'] ?? null; - - // If there are any messages display them - if (isset($eventResults['message'])) - { - $message = $eventResults['message']; - $messageType = ($eventResults['message_type'] ?? ''); - - $this->app->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(Route::_('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(Route::_('index.php', false)); - break; - - // Redirect browser to Media Manager - case 'media-manager': - default: - $this->setRedirect(Route::_('index.php?option=com_media&view=media', false)); - } - } - catch (\Exception $e) - { - // Display any error - $this->app->enqueueMessage($e->getMessage(), 'error'); - $this->setRedirect(Route::_('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 4.0.0 - */ - private function containsPlugin($plugins, $pluginName) - { - foreach ($plugins as $plugin) - { - if ($plugin->name == $pluginName) - { - return true; - } - } - - return false; - } + /** + * Handles an OAuth Callback request for a specified plugin. + * + * URLs containing [sitename]/administrator/index.php?option=com_media&task=plugin.oauthcallback + * &plugin=[plugin_name] + * + * will be handled by this endpoint. + * It will select the plugin specified by plugin_name and pass all the data received from the provider + * + * @return void + * + * @since 4.0.0 + */ + public function oauthcallback() + { + try { + // Load plugin names + $pluginName = $this->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!'); + } + + // Check if the plugin is disabled, if so redirect to control panel + if (!PluginHelper::isEnabled('filesystem', $pluginName)) { + throw new \Exception('Plugin ' . $pluginName . ' is disabled.'); + } + + // 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) $this->app->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 = $eventResults['action'] ?? null; + + // If there are any messages display them + if (isset($eventResults['message'])) { + $message = $eventResults['message']; + $messageType = ($eventResults['message_type'] ?? ''); + + $this->app->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(Route::_('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(Route::_('index.php', false)); + break; + + // Redirect browser to Media Manager + case 'media-manager': + default: + $this->setRedirect(Route::_('index.php?option=com_media&view=media', false)); + } + } catch (\Exception $e) { + // Display any error + $this->app->enqueueMessage($e->getMessage(), 'error'); + $this->setRedirect(Route::_('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 4.0.0 + */ + private function containsPlugin($plugins, $pluginName) + { + foreach ($plugins as $plugin) { + if ($plugin->name == $pluginName) { + return true; + } + } + + return false; + } } diff --git a/administrator/components/com_media/src/Dispatcher/Dispatcher.php b/administrator/components/com_media/src/Dispatcher/Dispatcher.php index f405c09d95a08..04e5a0537a2bf 100644 --- a/administrator/components/com_media/src/Dispatcher/Dispatcher.php +++ b/administrator/components/com_media/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->getIdentity(); - $asset = $this->input->get('asset'); - $author = $this->input->get('author'); + /** + * Method to check component access permission + * + * @since 4.0.0 + * + * @return void + */ + protected function checkAccess() + { + $user = $this->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 NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + // 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 NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/administrator/components/com_media/src/Event/AbstractMediaItemValidationEvent.php b/administrator/components/com_media/src/Event/AbstractMediaItemValidationEvent.php index 83f2921865cf6..87274f8f7af9b 100644 --- a/administrator/components/com_media/src/Event/AbstractMediaItemValidationEvent.php +++ b/administrator/components/com_media/src/Event/AbstractMediaItemValidationEvent.php @@ -1,4 +1,5 @@ type) || ($item->type !== 'dir' && $item->type !== 'file')) - { - throw new \BadMethodCallException("Property 'type' of argument 'item' of event {$this->name} has a wrong item. Valid: 'dir' or 'file'"); - } + /** + * Validate $item to have all attributes with a valid type. + * + * Properties validated: + * - type: The type can be file or dir + * - name: The name of the item + * - path: The relative path to the root + * - extension: The file extension + * - size: The size of the file + * - create_date: The date created + * - modified_date: The date modified + * - mime_type: The mime type + * - width: The width, when available + * - height: The height, when available + * + * Properties generated: + * - created_date_formatted: DATE_FORMAT_LC5 formatted string based on create_date + * - modified_date_formatted: DATE_FORMAT_LC5 formatted string based on modified_date + * + * @param \stdClass $item The item to set + * + * @return void + * + * @since 4.1.0 + * + * @throws \BadMethodCallException + */ + protected function validate(\stdClass $item): void + { + // Only "dir" or "file" is allowed + if (!isset($item->type) || ($item->type !== 'dir' && $item->type !== 'file')) { + throw new \BadMethodCallException("Property 'type' of argument 'item' of event {$this->name} has a wrong item. Valid: 'dir' or 'file'"); + } - // Non empty string - if (empty($item->name) || !is_string($item->name)) - { - throw new \BadMethodCallException("Property 'name' of argument 'item' of event {$this->name} has a wrong item. Valid: non empty string"); - } + // Non empty string + if (empty($item->name) || !is_string($item->name)) { + throw new \BadMethodCallException("Property 'name' of argument 'item' of event {$this->name} has a wrong item. Valid: non empty string"); + } - // Non empty string - if (empty($item->path) || !is_string($item->path)) - { - throw new \BadMethodCallException("Property 'path' of argument 'item' of event {$this->name} has a wrong item. Valid: non empty string"); - } + // Non empty string + if (empty($item->path) || !is_string($item->path)) { + throw new \BadMethodCallException("Property 'path' of argument 'item' of event {$this->name} has a wrong item. Valid: non empty string"); + } - // A string - if ($item->type === 'file' && (!isset($item->extension) || !is_string($item->extension))) - { - throw new \BadMethodCallException("Property 'extension' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); - } + // A string + if ($item->type === 'file' && (!isset($item->extension) || !is_string($item->extension))) { + throw new \BadMethodCallException("Property 'extension' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); + } - // An empty string or an integer - if (!isset($item->size) || - (!is_integer($item->size) && !is_string($item->size)) || - (is_string($item->size) && $item->size !== '') - ) - { - throw new \BadMethodCallException("Property 'size' of argument 'item' of event {$this->name} has a wrong item. Valid: empty string or integer"); - } + // An empty string or an integer + if ( + !isset($item->size) || + (!is_integer($item->size) && !is_string($item->size)) || + (is_string($item->size) && $item->size !== '') + ) { + throw new \BadMethodCallException("Property 'size' of argument 'item' of event {$this->name} has a wrong item. Valid: empty string or integer"); + } - // A string - if (!isset($item->mime_type) || !is_string($item->mime_type)) - { - throw new \BadMethodCallException("Property 'mime_type' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); - } + // A string + if (!isset($item->mime_type) || !is_string($item->mime_type)) { + throw new \BadMethodCallException("Property 'mime_type' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); + } - // An integer - if (!isset($item->width) || !is_integer($item->width)) - { - throw new \BadMethodCallException("Property 'width' of argument 'item' of event {$this->name} has a wrong item. Valid: integer"); - } + // An integer + if (!isset($item->width) || !is_integer($item->width)) { + throw new \BadMethodCallException("Property 'width' of argument 'item' of event {$this->name} has a wrong item. Valid: integer"); + } - // An integer - if (!isset($item->height) || !is_integer($item->height)) - { - throw new \BadMethodCallException("Property 'height' of argument 'item' of event {$this->name} has a wrong item. Valid: integer"); - } + // An integer + if (!isset($item->height) || !is_integer($item->height)) { + throw new \BadMethodCallException("Property 'height' of argument 'item' of event {$this->name} has a wrong item. Valid: integer"); + } - // A string - if (!isset($item->create_date) || !is_string($item->create_date)) - { - throw new \BadMethodCallException("Property 'create_date' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); - } + // A string + if (!isset($item->create_date) || !is_string($item->create_date)) { + throw new \BadMethodCallException("Property 'create_date' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); + } - // A string - if (!isset($item->create_date_formatted) || !is_string($item->create_date_formatted)) - { - throw new \BadMethodCallException("Property 'create_date_formatted' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); - } + // A string + if (!isset($item->create_date_formatted) || !is_string($item->create_date_formatted)) { + throw new \BadMethodCallException("Property 'create_date_formatted' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); + } - // A string - if (!isset($item->modified_date) || !is_string($item->modified_date)) - { - throw new \BadMethodCallException("Property 'modified_date' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); - } + // A string + if (!isset($item->modified_date) || !is_string($item->modified_date)) { + throw new \BadMethodCallException("Property 'modified_date' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); + } - // A string - if (!isset($item->modified_date_formatted) || !is_string($item->modified_date_formatted)) - { - throw new \BadMethodCallException("Property 'modified_date_formatted' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); - } - } + // A string + if (!isset($item->modified_date_formatted) || !is_string($item->modified_date_formatted)) { + throw new \BadMethodCallException("Property 'modified_date_formatted' of argument 'item' of event {$this->name} has a wrong item. Valid: string"); + } + } } diff --git a/administrator/components/com_media/src/Event/FetchMediaItemEvent.php b/administrator/components/com_media/src/Event/FetchMediaItemEvent.php index 82479be97bc98..2f47d5820924b 100644 --- a/administrator/components/com_media/src/Event/FetchMediaItemEvent.php +++ b/administrator/components/com_media/src/Event/FetchMediaItemEvent.php @@ -1,4 +1,5 @@ validate($item); + $this->validate($item); - return $item; - } + return $item; + } } diff --git a/administrator/components/com_media/src/Event/FetchMediaItemUrlEvent.php b/administrator/components/com_media/src/Event/FetchMediaItemUrlEvent.php index c137e62507d39..e9ccb9c2a3a64 100644 --- a/administrator/components/com_media/src/Event/FetchMediaItemUrlEvent.php +++ b/administrator/components/com_media/src/Event/FetchMediaItemUrlEvent.php @@ -1,4 +1,5 @@ arguments[$arguments['adapter']] = $arguments['adapter']; - unset($arguments['adapter']); + $this->arguments[$arguments['adapter']] = $arguments['adapter']; + unset($arguments['adapter']); - // Check for required arguments - if (!\array_key_exists('path', $arguments) || !is_string($arguments['path'])) - { - throw new \BadMethodCallException("Argument 'path' of event $name is not of the expected type"); - } + // Check for required arguments + if (!\array_key_exists('path', $arguments) || !is_string($arguments['path'])) { + throw new \BadMethodCallException("Argument 'path' of event $name is not of the expected type"); + } - $this->arguments[$arguments['path']] = $arguments['path']; - unset($arguments['path']); + $this->arguments[$arguments['path']] = $arguments['path']; + unset($arguments['path']); - // Check for required arguments - if (!\array_key_exists('url', $arguments) || !is_string($arguments['url'])) - { - throw new \BadMethodCallException("Argument 'url' of event $name is not of the expected type"); - } + // Check for required arguments + if (!\array_key_exists('url', $arguments) || !is_string($arguments['url'])) { + throw new \BadMethodCallException("Argument 'url' of event $name is not of the expected type"); + } - parent::__construct($name, $arguments); - } + parent::__construct($name, $arguments); + } - /** - * Validate $value to be a string - * - * @param string $value The value to set - * - * @return string - * - * @since 4.1.0 - */ - protected function setUrl(string $value): string - { - return $value; - } + /** + * Validate $value to be a string + * + * @param string $value The value to set + * + * @return string + * + * @since 4.1.0 + */ + protected function setUrl(string $value): string + { + return $value; + } - /** - * Forbid setting $path - * - * @param string $value The value to set - * - * @since 4.1.0 - * - * @throws \BadMethodCallException - */ - protected function setPath(string $value): string - { - throw new \BadMethodCallException('Cannot set the argument "path" of the immutable event ' . $this->name . '.'); - } + /** + * Forbid setting $path + * + * @param string $value The value to set + * + * @since 4.1.0 + * + * @throws \BadMethodCallException + */ + protected function setPath(string $value): string + { + throw new \BadMethodCallException('Cannot set the argument "path" of the immutable event ' . $this->name . '.'); + } - /** - * Forbid setting $path - * - * @param string $value The value to set - * - * @since 4.1.0 - * - * @throws \BadMethodCallException - */ - protected function setAdapter(string $value): string - { - throw new \BadMethodCallException('Cannot set the argument "adapter" of the immutable event ' . $this->name . '.'); - } + /** + * Forbid setting $path + * + * @param string $value The value to set + * + * @since 4.1.0 + * + * @throws \BadMethodCallException + */ + protected function setAdapter(string $value): string + { + throw new \BadMethodCallException('Cannot set the argument "adapter" of the immutable event ' . $this->name . '.'); + } } diff --git a/administrator/components/com_media/src/Event/FetchMediaItemsEvent.php b/administrator/components/com_media/src/Event/FetchMediaItemsEvent.php index e541e8ac617eb..cf054d5b77f6f 100644 --- a/administrator/components/com_media/src/Event/FetchMediaItemsEvent.php +++ b/administrator/components/com_media/src/Event/FetchMediaItemsEvent.php @@ -1,4 +1,5 @@ validate($clone); + $this->validate($clone); - $result[] = $clone; - } + $result[] = $clone; + } - return $result; - } + return $result; + } - /** - * Returns the items. - * - * @param array $items The value to set - * - * @return array - * - * @since 4.1.0 - */ - protected function getItems(array $items): array - { - $result = []; + /** + * Returns the items. + * + * @param array $items The value to set + * + * @return array + * + * @since 4.1.0 + */ + protected function getItems(array $items): array + { + $result = []; - foreach($items as $item) - { - $result[] = clone $item; - } + foreach ($items as $item) { + $result[] = clone $item; + } - return $result; - } + return $result; + } } diff --git a/administrator/components/com_media/src/Event/MediaProviderEvent.php b/administrator/components/com_media/src/Event/MediaProviderEvent.php index 8aeb7d60d71cc..b4dd4ae277409 100644 --- a/administrator/components/com_media/src/Event/MediaProviderEvent.php +++ b/administrator/components/com_media/src/Event/MediaProviderEvent.php @@ -1,4 +1,5 @@ providerManager; - } + /** + * Return the ProviderManager + * + * @return ProviderManager + * + * @since 4.0.0 + */ + public function getProviderManager(): ProviderManager + { + return $this->providerManager; + } - /** - * Set the ProviderManager - * - * @param ProviderManager $providerManager The Provider Manager to be set - * - * @return void - * - * @since 4.0.0 - */ - public function setProviderManager(ProviderManager $providerManager) - { - $this->providerManager = $providerManager; - } + /** + * Set the ProviderManager + * + * @param ProviderManager $providerManager The Provider Manager to be set + * + * @return void + * + * @since 4.0.0 + */ + public function setProviderManager(ProviderManager $providerManager) + { + $this->providerManager = $providerManager; + } } diff --git a/administrator/components/com_media/src/Event/OAuthCallbackEvent.php b/administrator/components/com_media/src/Event/OAuthCallbackEvent.php index d6d612c2565eb..dadbb5e069d41 100644 --- a/administrator/components/com_media/src/Event/OAuthCallbackEvent.php +++ b/administrator/components/com_media/src/Event/OAuthCallbackEvent.php @@ -1,4 +1,5 @@ context; - } + /** + * Get the event context. + * + * @return string + * + * @since 4.0.0 + */ + public function getContext() + { + return $this->context; + } - /** - * Set the event context. - * - * @param string $context Event context - * - * @return void - * - * @since 4.0.0 - */ - public function setContext($context) - { - $this->context = $context; - } + /** + * Set the event context. + * + * @param string $context Event context + * + * @return void + * + * @since 4.0.0 + */ + public function setContext($context) + { + $this->context = $context; + } - /** - * Get the event input. - * - * @return Input - * - * @since 4.0.0 - */ - public function getInput() - { - return $this->input; - } + /** + * Get the event input. + * + * @return Input + * + * @since 4.0.0 + */ + public function getInput() + { + return $this->input; + } - /** - * Set the event input. - * - * @param Input $input Event input - * - * @return void - * - * @since 4.0.0 - */ - public function setInput($input) - { - $this->input = $input; - } + /** + * Set the event input. + * + * @param Input $input Event input + * + * @return void + * + * @since 4.0.0 + */ + public function setInput($input) + { + $this->input = $input; + } } diff --git a/administrator/components/com_media/src/Exception/FileExistsException.php b/administrator/components/com_media/src/Exception/FileExistsException.php index 1e629684b1a0a..38da3e1b0bc90 100644 --- a/administrator/components/com_media/src/Exception/FileExistsException.php +++ b/administrator/components/com_media/src/Exception/FileExistsException.php @@ -1,4 +1,5 @@ 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') - { - $file->url = $this->getUrl($adapter, $file->path); - } - - if (isset($options['content']) && $options['content'] && $file->type == 'file') - { - $resource = $this->getAdapter($adapter)->getResource($file->path); - - if ($resource) - { - $file->content = base64_encode(stream_get_contents($resource)); - } - } - - $file->path = $adapter . ":" . $file->path; - $file->adapter = $adapter; - - $event = new FetchMediaItemEvent('onFetchMediaItem', ['item' => $file]); - Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); - - return $event->getArgument('item'); - } - - /** - * 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 4.0.0 - * @throws \Exception - * @see AdapterInterface::getFile() - */ - public function getFiles($adapter, $path = '/', $options = []) - { - // 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') - { - $file->url = $this->getUrl($adapter, $file->path); - } - - if (isset($options['content']) && $options['content'] && $file->type == 'file') - { - $resource = $this->getAdapter($adapter)->getResource($file->path); - - if ($resource) - { - $file->content = base64_encode(stream_get_contents($resource)); - } - } - - $file->path = $adapter . ":" . $file->path; - $file->adapter = $adapter; - } - - // Make proper indexes - $files = array_values($files); - - $event = new FetchMediaItemsEvent('onFetchMediaItems', ['items' => $files]); - Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); - - return $event->getArgument('items'); - } - - /** - * 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 overridden when it exists - * - * @return string - * - * @since 4.0.0 - * @throws \Exception - * @see AdapterInterface::createFolder() - */ - public function createFolder($adapter, $name, $path, $override) - { - try - { - $file = $this->getFile($adapter, $path . '/' . $name); - } - catch (FileNotFoundException $e) - { - // Do nothing - } - - // Check if the file exists - if (isset($file) && !$override) - { - throw new FileExistsException; - } - - $app = Factory::getApplication(); - $object = new CMSObject; - $object->adapter = $adapter; - $object->name = $name; - $object->path = $path; - - PluginHelper::importPlugin('content'); - - $result = $app->triggerEvent('onContentBeforeSave', ['com_media.folder', $object, true, $object]); - - if (in_array(false, $result, true)) - { - throw new \Exception($object->getError()); - } - - $object->name = $this->getAdapter($object->adapter)->createFolder($object->name, $object->path); - - $app->triggerEvent('onContentAfterSave', ['com_media.folder', $object, true, $object]); - - return $object->name; - } - - /** - * 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 string $data The data - * @param boolean $override Should the file being overridden when it exists - * - * @return string - * - * @since 4.0.0 - * @throws \Exception - * @see AdapterInterface::createFile() - */ - public function createFile($adapter, $name, $path, $data, $override) - { - try - { - $file = $this->getFile($adapter, $path . '/' . $name); - } - catch (FileNotFoundException $e) - { - // Do nothing - } - - // 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; - } - - $app = Factory::getApplication(); - $object = new CMSObject; - $object->adapter = $adapter; - $object->name = $name; - $object->path = $path; - $object->data = $data; - $object->extension = strtolower(File::getExt($name)); - - PluginHelper::importPlugin('content'); - - // Also include the filesystem plugins, perhaps they support batch processing too - PluginHelper::importPlugin('media-action'); - - $result = $app->triggerEvent('onContentBeforeSave', ['com_media.file', $object, true, $object]); - - if (in_array(false, $result, true)) - { - throw new \Exception($object->getError()); - } - - $object->name = $this->getAdapter($object->adapter)->createFile($object->name, $object->path, $object->data); - - $app->triggerEvent('onContentAfterSave', ['com_media.file', $object, true, $object]); - - return $object->name; - } - - /** - * 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 string $data The data - * - * @return void - * - * @since 4.0.0 - * @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; - } - - $app = Factory::getApplication(); - $object = new CMSObject; - $object->adapter = $adapter; - $object->name = $name; - $object->path = $path; - $object->data = $data; - $object->extension = strtolower(File::getExt($name)); - - PluginHelper::importPlugin('content'); - - // Also include the filesystem plugins, perhaps they support batch processing too - PluginHelper::importPlugin('media-action'); - - $result = $app->triggerEvent('onContentBeforeSave', ['com_media.file', $object, false, $object]); - - if (in_array(false, $result, true)) - { - throw new \Exception($object->getError()); - } - - $this->getAdapter($object->adapter)->updateFile($object->name, $object->path, $object->data); - - $app->triggerEvent('onContentAfterSave', ['com_media.file', $object, false, $object]); - } - - /** - * 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 4.0.0 - * @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; - } - - $type = $file->type === 'file' ? 'file' : 'folder'; - $app = Factory::getApplication(); - $object = new CMSObject; - $object->adapter = $adapter; - $object->path = $path; - - PluginHelper::importPlugin('content'); - - // Also include the filesystem plugins, perhaps they support batch processing too - PluginHelper::importPlugin('media-action'); - - $result = $app->triggerEvent('onContentBeforeDelete', ['com_media.' . $type, $object]); - - if (in_array(false, $result, true)) - { - throw new \Exception($object->getError()); - } - - $this->getAdapter($object->adapter)->delete($object->path); - - $app->triggerEvent('onContentAfterDelete', ['com_media.' . $type, $object]); - } - - /** - * 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 string - * - * @since 4.0.0 - * @throws \Exception - */ - public function copy($adapter, $sourcePath, $destinationPath, $force = false) - { - return $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 string - * - * @since 4.0.0 - * @throws \Exception - */ - public function move($adapter, $sourcePath, $destinationPath, $force = false) - { - return $this->getAdapter($adapter)->move($sourcePath, $destinationPath, $force); - } - - /** - * Returns a 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 4.0.0 - * @throws FileNotFoundException - */ - public function getUrl($adapter, $path) - { - // Check if it is a media file - if (!$this->isMediaFile($path)) - { - throw new InvalidPathException; - } - - $url = $this->getAdapter($adapter)->getUrl($path); - - $event = new FetchMediaItemUrlEvent('onFetchMediaFileUrl', ['adapter' => $adapter, 'path' => $path, 'url' => $url]); - Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); - - return $event->getArgument('url'); - } - - /** - * 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 4.0.0 - * @throws \Exception - */ - public function search($adapter, $needle, $path = '/', $recursive = true) - { - return $this->getAdapter($adapter)->search($path, $needle, $recursive); - } - - /** - * Checks if the given path is an allowed media file. - * - * @param string $path The path to file - * - * @return boolean - * - * @since 4.0.0 - */ - 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 options from the input or fallback to images only - $mediaTypes = explode(',', Factory::getApplication()->input->getString('mediatypes', '0')); - $types = []; - $extensions = []; - - // Default to showing all supported formats - if (count($mediaTypes) === 0) { - $mediaTypes = ['0', '1', '2', '3']; - } - - array_map( - function ($mediaType) use (&$types) { - switch ($mediaType) { - case '0': - $types[] = 'images'; - break; - case '1': - $types[] = 'audios'; - break; - case '2': - $types[] = 'videos'; - break; - case '3': - $types[] = 'documents'; - break; - default: - break; - } - }, - $mediaTypes - ); - - $images = array_map( - 'trim', - explode( - ',', - ComponentHelper::getParams('com_media')->get( - 'image_extensions', - 'bmp,gif,jpg,jpeg,png,webp' - ) - ) - ); - $audios = array_map( - 'trim', - explode( - ',', - ComponentHelper::getParams('com_media')->get( - 'audio_extensions', - 'mp3,m4a,mp4a,ogg' - ) - ) - ); - $videos = array_map( - 'trim', - explode( - ',', - ComponentHelper::getParams('com_media')->get( - 'video_extensions', - 'mp4,mp4v,mpeg,mov,webm' - ) - ) - ); - $documents = array_map( - 'trim', - explode( - ',', - ComponentHelper::getParams('com_media')->get( - 'doc_extensions', - 'doc,odg,odp,ods,odt,pdf,ppt,txt,xcf,xls,csv' - ) - ) - ); - - foreach ($types as $type) { - if (in_array($type, ['images', 'audios', 'videos', 'documents'])) { - $extensions = array_merge($extensions, ${$type}); - } - } - - // Make them an array - $this->allowedExtensions = $extensions; - } - - // Extract the extension - $extension = strtolower(substr($path, strrpos($path, '.') + 1)); - - // Check if the extension exists in the allowed extensions - return in_array($extension, $this->allowedExtensions); - } + use ProviderManagerHelperTrait; + + /** + * The available extensions. + * + * @var string[] + * @since 4.0.0 + */ + private $allowedExtensions = null; + + /** + * 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 4.0.0 + * @throws \Exception + * @see AdapterInterface::getFile() + */ + public function getFile($adapter, $path = '/', $options = []) + { + // 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') { + $file->url = $this->getUrl($adapter, $file->path); + } + + if (isset($options['content']) && $options['content'] && $file->type == 'file') { + $resource = $this->getAdapter($adapter)->getResource($file->path); + + if ($resource) { + $file->content = base64_encode(stream_get_contents($resource)); + } + } + + $file->path = $adapter . ":" . $file->path; + $file->adapter = $adapter; + + $event = new FetchMediaItemEvent('onFetchMediaItem', ['item' => $file]); + Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); + + return $event->getArgument('item'); + } + + /** + * 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 4.0.0 + * @throws \Exception + * @see AdapterInterface::getFile() + */ + public function getFiles($adapter, $path = '/', $options = []) + { + // 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') { + $file->url = $this->getUrl($adapter, $file->path); + } + + if (isset($options['content']) && $options['content'] && $file->type == 'file') { + $resource = $this->getAdapter($adapter)->getResource($file->path); + + if ($resource) { + $file->content = base64_encode(stream_get_contents($resource)); + } + } + + $file->path = $adapter . ":" . $file->path; + $file->adapter = $adapter; + } + + // Make proper indexes + $files = array_values($files); + + $event = new FetchMediaItemsEvent('onFetchMediaItems', ['items' => $files]); + Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); + + return $event->getArgument('items'); + } + + /** + * 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 overridden when it exists + * + * @return string + * + * @since 4.0.0 + * @throws \Exception + * @see AdapterInterface::createFolder() + */ + public function createFolder($adapter, $name, $path, $override) + { + try { + $file = $this->getFile($adapter, $path . '/' . $name); + } catch (FileNotFoundException $e) { + // Do nothing + } + + // Check if the file exists + if (isset($file) && !$override) { + throw new FileExistsException(); + } + + $app = Factory::getApplication(); + $object = new CMSObject(); + $object->adapter = $adapter; + $object->name = $name; + $object->path = $path; + + PluginHelper::importPlugin('content'); + + $result = $app->triggerEvent('onContentBeforeSave', ['com_media.folder', $object, true, $object]); + + if (in_array(false, $result, true)) { + throw new \Exception($object->getError()); + } + + $object->name = $this->getAdapter($object->adapter)->createFolder($object->name, $object->path); + + $app->triggerEvent('onContentAfterSave', ['com_media.folder', $object, true, $object]); + + return $object->name; + } + + /** + * 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 string $data The data + * @param boolean $override Should the file being overridden when it exists + * + * @return string + * + * @since 4.0.0 + * @throws \Exception + * @see AdapterInterface::createFile() + */ + public function createFile($adapter, $name, $path, $data, $override) + { + try { + $file = $this->getFile($adapter, $path . '/' . $name); + } catch (FileNotFoundException $e) { + // Do nothing + } + + // 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(); + } + + $app = Factory::getApplication(); + $object = new CMSObject(); + $object->adapter = $adapter; + $object->name = $name; + $object->path = $path; + $object->data = $data; + $object->extension = strtolower(File::getExt($name)); + + PluginHelper::importPlugin('content'); + + // Also include the filesystem plugins, perhaps they support batch processing too + PluginHelper::importPlugin('media-action'); + + $result = $app->triggerEvent('onContentBeforeSave', ['com_media.file', $object, true, $object]); + + if (in_array(false, $result, true)) { + throw new \Exception($object->getError()); + } + + $object->name = $this->getAdapter($object->adapter)->createFile($object->name, $object->path, $object->data); + + $app->triggerEvent('onContentAfterSave', ['com_media.file', $object, true, $object]); + + return $object->name; + } + + /** + * 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 string $data The data + * + * @return void + * + * @since 4.0.0 + * @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(); + } + + $app = Factory::getApplication(); + $object = new CMSObject(); + $object->adapter = $adapter; + $object->name = $name; + $object->path = $path; + $object->data = $data; + $object->extension = strtolower(File::getExt($name)); + + PluginHelper::importPlugin('content'); + + // Also include the filesystem plugins, perhaps they support batch processing too + PluginHelper::importPlugin('media-action'); + + $result = $app->triggerEvent('onContentBeforeSave', ['com_media.file', $object, false, $object]); + + if (in_array(false, $result, true)) { + throw new \Exception($object->getError()); + } + + $this->getAdapter($object->adapter)->updateFile($object->name, $object->path, $object->data); + + $app->triggerEvent('onContentAfterSave', ['com_media.file', $object, false, $object]); + } + + /** + * 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 4.0.0 + * @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(); + } + + $type = $file->type === 'file' ? 'file' : 'folder'; + $app = Factory::getApplication(); + $object = new CMSObject(); + $object->adapter = $adapter; + $object->path = $path; + + PluginHelper::importPlugin('content'); + + // Also include the filesystem plugins, perhaps they support batch processing too + PluginHelper::importPlugin('media-action'); + + $result = $app->triggerEvent('onContentBeforeDelete', ['com_media.' . $type, $object]); + + if (in_array(false, $result, true)) { + throw new \Exception($object->getError()); + } + + $this->getAdapter($object->adapter)->delete($object->path); + + $app->triggerEvent('onContentAfterDelete', ['com_media.' . $type, $object]); + } + + /** + * 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 string + * + * @since 4.0.0 + * @throws \Exception + */ + public function copy($adapter, $sourcePath, $destinationPath, $force = false) + { + return $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 string + * + * @since 4.0.0 + * @throws \Exception + */ + public function move($adapter, $sourcePath, $destinationPath, $force = false) + { + return $this->getAdapter($adapter)->move($sourcePath, $destinationPath, $force); + } + + /** + * Returns a 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 4.0.0 + * @throws FileNotFoundException + */ + public function getUrl($adapter, $path) + { + // Check if it is a media file + if (!$this->isMediaFile($path)) { + throw new InvalidPathException(); + } + + $url = $this->getAdapter($adapter)->getUrl($path); + + $event = new FetchMediaItemUrlEvent('onFetchMediaFileUrl', ['adapter' => $adapter, 'path' => $path, 'url' => $url]); + Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); + + return $event->getArgument('url'); + } + + /** + * 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 4.0.0 + * @throws \Exception + */ + public function search($adapter, $needle, $path = '/', $recursive = true) + { + return $this->getAdapter($adapter)->search($path, $needle, $recursive); + } + + /** + * Checks if the given path is an allowed media file. + * + * @param string $path The path to file + * + * @return boolean + * + * @since 4.0.0 + */ + 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 options from the input or fallback to images only + $mediaTypes = explode(',', Factory::getApplication()->input->getString('mediatypes', '0')); + $types = []; + $extensions = []; + + // Default to showing all supported formats + if (count($mediaTypes) === 0) { + $mediaTypes = ['0', '1', '2', '3']; + } + + array_map( + function ($mediaType) use (&$types) { + switch ($mediaType) { + case '0': + $types[] = 'images'; + break; + case '1': + $types[] = 'audios'; + break; + case '2': + $types[] = 'videos'; + break; + case '3': + $types[] = 'documents'; + break; + default: + break; + } + }, + $mediaTypes + ); + + $images = array_map( + 'trim', + explode( + ',', + ComponentHelper::getParams('com_media')->get( + 'image_extensions', + 'bmp,gif,jpg,jpeg,png,webp' + ) + ) + ); + $audios = array_map( + 'trim', + explode( + ',', + ComponentHelper::getParams('com_media')->get( + 'audio_extensions', + 'mp3,m4a,mp4a,ogg' + ) + ) + ); + $videos = array_map( + 'trim', + explode( + ',', + ComponentHelper::getParams('com_media')->get( + 'video_extensions', + 'mp4,mp4v,mpeg,mov,webm' + ) + ) + ); + $documents = array_map( + 'trim', + explode( + ',', + ComponentHelper::getParams('com_media')->get( + 'doc_extensions', + 'doc,odg,odp,ods,odt,pdf,ppt,txt,xcf,xls,csv' + ) + ) + ); + + foreach ($types as $type) { + if (in_array($type, ['images', 'audios', 'videos', 'documents'])) { + $extensions = array_merge($extensions, ${$type}); + } + } + + // Make them an array + $this->allowedExtensions = $extensions; + } + + // Extract the extension + $extension = strtolower(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/src/Model/FileModel.php b/administrator/components/com_media/src/Model/FileModel.php index eb2e5c26fcece..dc74be6544b40 100644 --- a/administrator/components/com_media/src/Model/FileModel.php +++ b/administrator/components/com_media/src/Model/FileModel.php @@ -1,4 +1,5 @@ loadForm('com_media.file', 'file', ['control' => 'jform', 'load_data' => $loadData]); + // Get the form. + $form = $this->loadForm('com_media.file', 'file', ['control' => 'jform', 'load_data' => $loadData]); - if (empty($form)) - { - return false; - } + if (empty($form)) { + return false; + } - return $form; - } + 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 An object with file information - * - * @since 4.0.0 - * @see ApiModel::getFile() - */ - public function getFileInformation($path) - { - list($adapter, $path) = explode(':', $path, 2); + /** + * 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 An object with file information + * + * @since 4.0.0 + * @see ApiModel::getFile() + */ + public function getFileInformation($path) + { + list($adapter, $path) = explode(':', $path, 2); - return $this->bootComponent('com_media')->getMVCFactory()->createModel('Api', 'Administrator') - ->getFile($adapter, $path, ['url' => true, 'content' => true]); - } + return $this->bootComponent('com_media')->getMVCFactory()->createModel('Api', 'Administrator') + ->getFile($adapter, $path, ['url' => true, 'content' => true]); + } } diff --git a/administrator/components/com_media/src/Model/MediaModel.php b/administrator/components/com_media/src/Model/MediaModel.php index 257113bc153fe..b111ad1846a1d 100644 --- a/administrator/components/com_media/src/Model/MediaModel.php +++ b/administrator/components/com_media/src/Model/MediaModel.php @@ -1,4 +1,5 @@ getProviderManager()->getProviders() as $provider) - { - $result = new \stdClass; - $result->name = $provider->getID(); - $result->displayName = $provider->getDisplayName(); - $result->adapterNames = []; - - foreach ($provider->getAdapters() as $adapter) - { - $result->adapterNames[] = $adapter->getAdapterName(); - } - - $results[] = $result; - } - - return $results; - } + use ProviderManagerHelperTrait; + + /** + * Obtain list of supported providers + * + * @return array + * + * @since 4.0.0 + */ + public function getProviders() + { + $results = []; + + foreach ($this->getProviderManager()->getProviders() as $provider) { + $result = new \stdClass(); + $result->name = $provider->getID(); + $result->displayName = $provider->getDisplayName(); + $result->adapterNames = []; + + foreach ($provider->getAdapters() as $adapter) { + $result->adapterNames[] = $adapter->getAdapterName(); + } + + $results[] = $result; + } + + return $results; + } } diff --git a/administrator/components/com_media/src/Plugin/MediaActionPlugin.php b/administrator/components/com_media/src/Plugin/MediaActionPlugin.php index 830aeec699147..34ecb8b464836 100644 --- a/administrator/components/com_media/src/Plugin/MediaActionPlugin.php +++ b/administrator/components/com_media/src/Plugin/MediaActionPlugin.php @@ -1,4 +1,5 @@ getName() != 'com_media.file') - { - return; - } + /** + * The form event. Load additional parameters when available into the field form. + * Only when the type of the form is of interest. + * + * @param Form $form The form + * @param \stdClass $data The data + * + * @return void + * + * @since 4.0.0 + */ + public function onContentPrepareForm(Form $form, $data) + { + // Check if it is the right form + if ($form->getName() != 'com_media.file') { + return; + } - $this->loadCss(); - $this->loadJs(); + $this->loadCss(); + $this->loadJs(); - // The file with the params for the edit view - $paramsFile = JPATH_PLUGINS . '/media-action/' . $this->_name . '/form/' . $this->_name . '.xml'; + // The file with the params for the edit view + $paramsFile = JPATH_PLUGINS . '/media-action/' . $this->_name . '/form/' . $this->_name . '.xml'; - // When the file exists, load it into the form - if (file_exists($paramsFile)) - { - $form->loadFile($paramsFile); - } - } + // When the file exists, load it into the form + if (file_exists($paramsFile)) { + $form->loadFile($paramsFile); + } + } - /** - * Load the javascript files of the plugin. - * - * @return void - * - * @since 4.0.0 - */ - protected function loadJs() - { - HTMLHelper::_( - 'script', - 'plg_media-action_' . $this->_name . '/' . $this->_name . '.js', - ['version' => 'auto', 'relative' => true], - ['type' => 'module'] - ); - } + /** + * Load the javascript files of the plugin. + * + * @return void + * + * @since 4.0.0 + */ + protected function loadJs() + { + HTMLHelper::_( + 'script', + 'plg_media-action_' . $this->_name . '/' . $this->_name . '.js', + ['version' => 'auto', 'relative' => true], + ['type' => 'module'] + ); + } - /** - * Load the CSS files of the plugin. - * - * @return void - * - * @since 4.0.0 - */ - protected function loadCss() - { - HTMLHelper::_( - 'stylesheet', - 'plg_media-action_' . $this->_name . '/' . $this->_name . '.css', - ['version' => 'auto', 'relative' => true] - ); - } + /** + * Load the CSS files of the plugin. + * + * @return void + * + * @since 4.0.0 + */ + protected function loadCss() + { + HTMLHelper::_( + 'stylesheet', + 'plg_media-action_' . $this->_name . '/' . $this->_name . '.css', + ['version' => 'auto', 'relative' => true] + ); + } } diff --git a/administrator/components/com_media/src/Provider/ProviderInterface.php b/administrator/components/com_media/src/Provider/ProviderInterface.php index 13a270c14b783..ecfc89773c186 100644 --- a/administrator/components/com_media/src/Provider/ProviderInterface.php +++ b/administrator/components/com_media/src/Provider/ProviderInterface.php @@ -1,4 +1,5 @@ providers; - } - - /** - * Register a provider into the ProviderManager - * - * @param ProviderInterface $provider The provider to be registered - * - * @return void - * - * @since 4.0.0 - */ - public function registerProvider(ProviderInterface $provider) - { - $this->providers[$provider->getID()] = $provider; - } - - /** - * Unregister a provider from the ProviderManager. - * When no provider, or null is passed in, then all providers are cleared. - * - * @param ProviderInterface|null $provider The provider to be unregistered - * - * @return void - * - * @since 4.0.6 - */ - public function unregisterProvider(ProviderInterface $provider = null): void - { - if ($provider === null) - { - $this->providers = []; - return; - } - - if (!array_key_exists($provider->getID(), $this->providers)) - { - return; - } - - unset($this->providers[$provider->getID()]); - } - - /** - * Returns the provider for a particular ID - * - * @param string $id The ID for the provider - * - * @return ProviderInterface - * - * @throws \Exception - * - * @since 4.0.0 - */ - public function getProvider($id) - { - if (!isset($this->providers[$id])) - { - throw new \Exception(Text::_('COM_MEDIA_ERROR_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 4.0.0 - */ - public function getAdapter($name) - { - list($provider, $account) = array_pad(explode('-', $name, 2), 2, null); - - if ($account == null) - { - throw new \Exception(Text::_('COM_MEDIA_ERROR_ACCOUNT_NOT_SET')); - } - - $adapters = $this->getProvider($provider)->getAdapters(); - - if (!isset($adapters[$account])) - { - throw new \Exception(Text::_('COM_MEDIA_ERROR_ACCOUNT_NOT_FOUND')); - } - - return $adapters[$account]; - } + /** + * The array of providers + * + * @var ProviderInterface[] + * + * @since 4.0.0 + */ + private $providers = []; + + /** + * Returns an associative array of adapters with provider name as the key + * + * @return ProviderInterface[] + * + * @since 4.0.0 + */ + public function getProviders() + { + return $this->providers; + } + + /** + * Register a provider into the ProviderManager + * + * @param ProviderInterface $provider The provider to be registered + * + * @return void + * + * @since 4.0.0 + */ + public function registerProvider(ProviderInterface $provider) + { + $this->providers[$provider->getID()] = $provider; + } + + /** + * Unregister a provider from the ProviderManager. + * When no provider, or null is passed in, then all providers are cleared. + * + * @param ProviderInterface|null $provider The provider to be unregistered + * + * @return void + * + * @since 4.0.6 + */ + public function unregisterProvider(ProviderInterface $provider = null): void + { + if ($provider === null) { + $this->providers = []; + return; + } + + if (!array_key_exists($provider->getID(), $this->providers)) { + return; + } + + unset($this->providers[$provider->getID()]); + } + + /** + * Returns the provider for a particular ID + * + * @param string $id The ID for the provider + * + * @return ProviderInterface + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function getProvider($id) + { + if (!isset($this->providers[$id])) { + throw new \Exception(Text::_('COM_MEDIA_ERROR_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 4.0.0 + */ + public function getAdapter($name) + { + list($provider, $account) = array_pad(explode('-', $name, 2), 2, null); + + if ($account == null) { + throw new \Exception(Text::_('COM_MEDIA_ERROR_ACCOUNT_NOT_SET')); + } + + $adapters = $this->getProvider($provider)->getAdapters(); + + if (!isset($adapters[$account])) { + throw new \Exception(Text::_('COM_MEDIA_ERROR_ACCOUNT_NOT_FOUND')); + } + + return $adapters[$account]; + } } diff --git a/administrator/components/com_media/src/Provider/ProviderManagerHelperTrait.php b/administrator/components/com_media/src/Provider/ProviderManagerHelperTrait.php index 7c95165602386..6af5d690f2db6 100644 --- a/administrator/components/com_media/src/Provider/ProviderManagerHelperTrait.php +++ b/administrator/components/com_media/src/Provider/ProviderManagerHelperTrait.php @@ -1,4 +1,5 @@ providerManager) - { - // Fire the event to get the results - $eventParameters = ['context' => 'AdapterManager', 'providerManager' => new ProviderManager]; - $event = new MediaProviderEvent('onSetupProviders', $eventParameters); - PluginHelper::importPlugin('filesystem'); - Factory::getApplication()->triggerEvent('onSetupProviders', $event); - $this->providerManager = $event->getProviderManager(); - } - - return $this->providerManager; - } - - /** - * Returns a provider for the given id. - * - * @return ProviderInterface - * - * @throws \Exception - * - * @since 4.1.0 - */ - public function getProvider(String $id): ProviderInterface - { - return $this->getProviderManager()->getProvider($id); - } - - /** - * Return an adapter for the given name. - * - * @return AdapterInterface - * - * @throws \Exception - * - * @since 4.1.0 - */ - public function getAdapter(String $name): AdapterInterface - { - return $this->getProviderManager()->getAdapter($name); - } - - /** - * Returns an array with the adapter name as key and the path of the file. - * - * @return array - * - * @throws \InvalidArgumentException - * - * @since 4.1.0 - */ - protected function resolveAdapterAndPath(String $path): array - { - $result = []; - $parts = explode(':', $path, 2); - - // If we have 2 parts, we have both an adapter name and a file path - if (\count($parts) === 2) - { - $result['adapter'] = $parts[0]; - $result['path'] = $parts[1]; - - return $result; - } - - if (!$this->getDefaultAdapterName()) - { - throw new \InvalidArgumentException(Text::_('COM_MEDIA_ERROR_NO_ADAPTER_FOUND')); - } - - // If we have less than 2 parts, we return a default adapter name - $result['adapter'] = $this->getDefaultAdapterName(); - - // If we have 1 part, we return it as the path. Otherwise we return a default path - $result['path'] = \count($parts) ? $parts[0] : '/'; - - return $result; - } - - /** - * Returns the default adapter name. - * - * @return string|null - * - * @throws \Exception - * - * @since 4.1.0 - */ - protected function getDefaultAdapterName(): ?string - { - if ($this->defaultAdapterName) - { - return $this->defaultAdapterName; - } - - $defaultAdapter = $this->getAdapter('local-' . ComponentHelper::getParams('com_media')->get('file_path', 'images')); - - if (!$defaultAdapter - && $this->getProviderManager()->getProvider('local') - && $this->getProviderManager()->getProvider('local')->getAdapters()) - { - $defaultAdapter = $this->getProviderManager()->getProvider('local')->getAdapters()[0]; - } - - if (!$defaultAdapter) - { - return null; - } - - $this->defaultAdapterName = 'local-' . $defaultAdapter->getAdapterName(); - - return $this->defaultAdapterName; - } + /** + * Holds the available media file adapters. + * + * @var ProviderManager + * + * @since 4.1.0 + */ + private $providerManager = null; + + /** + * The default adapter name. + * + * @var string + * + * @since 4.1.0 + */ + private $defaultAdapterName = null; + + /** + * Return a provider manager. + * + * @return ProviderManager + * + * @since 4.1.0 + */ + public function getProviderManager(): ProviderManager + { + if (!$this->providerManager) { + // Fire the event to get the results + $eventParameters = ['context' => 'AdapterManager', 'providerManager' => new ProviderManager()]; + $event = new MediaProviderEvent('onSetupProviders', $eventParameters); + PluginHelper::importPlugin('filesystem'); + Factory::getApplication()->triggerEvent('onSetupProviders', $event); + $this->providerManager = $event->getProviderManager(); + } + + return $this->providerManager; + } + + /** + * Returns a provider for the given id. + * + * @return ProviderInterface + * + * @throws \Exception + * + * @since 4.1.0 + */ + public function getProvider(string $id): ProviderInterface + { + return $this->getProviderManager()->getProvider($id); + } + + /** + * Return an adapter for the given name. + * + * @return AdapterInterface + * + * @throws \Exception + * + * @since 4.1.0 + */ + public function getAdapter(string $name): AdapterInterface + { + return $this->getProviderManager()->getAdapter($name); + } + + /** + * Returns an array with the adapter name as key and the path of the file. + * + * @return array + * + * @throws \InvalidArgumentException + * + * @since 4.1.0 + */ + protected function resolveAdapterAndPath(string $path): array + { + $result = []; + $parts = explode(':', $path, 2); + + // If we have 2 parts, we have both an adapter name and a file path + if (\count($parts) === 2) { + $result['adapter'] = $parts[0]; + $result['path'] = $parts[1]; + + return $result; + } + + if (!$this->getDefaultAdapterName()) { + throw new \InvalidArgumentException(Text::_('COM_MEDIA_ERROR_NO_ADAPTER_FOUND')); + } + + // If we have less than 2 parts, we return a default adapter name + $result['adapter'] = $this->getDefaultAdapterName(); + + // If we have 1 part, we return it as the path. Otherwise we return a default path + $result['path'] = \count($parts) ? $parts[0] : '/'; + + return $result; + } + + /** + * Returns the default adapter name. + * + * @return string|null + * + * @throws \Exception + * + * @since 4.1.0 + */ + protected function getDefaultAdapterName(): ?string + { + if ($this->defaultAdapterName) { + return $this->defaultAdapterName; + } + + $defaultAdapter = $this->getAdapter('local-' . ComponentHelper::getParams('com_media')->get('file_path', 'images')); + + if ( + !$defaultAdapter + && $this->getProviderManager()->getProvider('local') + && $this->getProviderManager()->getProvider('local')->getAdapters() + ) { + $defaultAdapter = $this->getProviderManager()->getProvider('local')->getAdapters()[0]; + } + + if (!$defaultAdapter) { + return null; + } + + $this->defaultAdapterName = 'local-' . $defaultAdapter->getAdapterName(); + + return $this->defaultAdapterName; + } } diff --git a/administrator/components/com_media/src/View/File/HtmlView.php b/administrator/components/com_media/src/View/File/HtmlView.php index 1681758459c89..a8dfde9509294 100644 --- a/administrator/components/com_media/src/View/File/HtmlView.php +++ b/administrator/components/com_media/src/View/File/HtmlView.php @@ -1,4 +1,5 @@ input; - /** - * Execute and display a template script. - * - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.0.0 - */ - public function display($tpl = null) - { - $input = Factory::getApplication()->input; - - $this->form = $this->get('Form'); + $this->form = $this->get('Form'); - // The component params - $this->params = ComponentHelper::getParams('com_media'); + // The component params + $this->params = ComponentHelper::getParams('com_media'); - // The requested file - $this->file = $this->getModel()->getFileInformation($input->getString('path', null)); + // The requested file + $this->file = $this->getModel()->getFileInformation($input->getString('path', null)); - if (empty($this->file->content)) - { - // @todo error handling controller redirect files - throw new \Exception(Text::_('COM_MEDIA_ERROR_NO_CONTENT_AVAILABLE')); - } + if (empty($this->file->content)) { + // @todo error handling controller redirect files + throw new \Exception(Text::_('COM_MEDIA_ERROR_NO_CONTENT_AVAILABLE')); + } - $this->addToolbar(); + $this->addToolbar(); - parent::display($tpl); - } + parent::display($tpl); + } - /** - * Add the toolbar buttons - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - ToolbarHelper::title(Text::_('COM_MEDIA_EDIT'), 'images mediamanager'); + /** + * Add the toolbar buttons + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + ToolbarHelper::title(Text::_('COM_MEDIA_EDIT'), 'images mediamanager'); - ToolbarHelper::apply('apply'); - ToolbarHelper::save('save'); - ToolbarHelper::custom('reset', 'refresh', '', 'COM_MEDIA_RESET', false); + ToolbarHelper::apply('apply'); + ToolbarHelper::save('save'); + ToolbarHelper::custom('reset', 'refresh', '', 'COM_MEDIA_RESET', false); - ToolbarHelper::cancel('cancel', 'JTOOLBAR_CLOSE'); - } + ToolbarHelper::cancel('cancel', 'JTOOLBAR_CLOSE'); + } } diff --git a/administrator/components/com_media/tmpl/file/default.php b/administrator/components/com_media/tmpl/file/default.php index 83a9fd379435a..6ae34c01068d4 100644 --- a/administrator/components/com_media/tmpl/file/default.php +++ b/administrator/components/com_media/tmpl/file/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useStyle('com_media.mediamanager'); + ->useScript('form.validate') + ->useStyle('com_media.mediamanager'); $script = $wa->getAsset('script', 'com_media.edit-images')->getUri(true); @@ -37,25 +38,25 @@ // Load the toolbar when we are in an iframe if ($tmpl == 'component') { - echo '
'; - echo Toolbar::getInstance('toolbar')->render(); - echo '
'; + echo '
'; + echo Toolbar::getInstance('toolbar')->render(); + echo '
'; } $mediaTypes = $input->getString('mediatypes', '0'); // Populate the media config $config = [ - 'apiBaseUrl' => Uri::base() . 'index.php?option=com_media&format=json' . '&mediatypes=' . $mediaTypes, - 'csrfToken' => Session::getFormToken(), - 'uploadPath' => $this->file->path, - 'editViewUrl' => Uri::base() . 'index.php?option=com_media&view=file' . ($tmpl ? '&tmpl=' . $tmpl : '') . '&mediatypes=' . $mediaTypes, - 'imagesExtensions' => explode(',', $params->get('image_extensions', 'bmp,gif,jpg,jpeg,png,webp')), - 'audioExtensions' => explode(',', $params->get('audio_extensions', 'mp3,m4a,mp4a,ogg')), - 'videoExtensions' => explode(',', $params->get('video_extensions', 'mp4,mp4v,mpeg,mov,webm')), - 'documentExtensions' => explode(',', $params->get('doc_extensions', 'doc,odg,odp,ods,odt,pdf,ppt,txt,xcf,xls,csv')), - 'maxUploadSizeMb' => $params->get('upload_maxsize', 10), - 'contents' => $this->file->content, + 'apiBaseUrl' => Uri::base() . 'index.php?option=com_media&format=json' . '&mediatypes=' . $mediaTypes, + 'csrfToken' => Session::getFormToken(), + 'uploadPath' => $this->file->path, + 'editViewUrl' => Uri::base() . 'index.php?option=com_media&view=file' . ($tmpl ? '&tmpl=' . $tmpl : '') . '&mediatypes=' . $mediaTypes, + 'imagesExtensions' => explode(',', $params->get('image_extensions', 'bmp,gif,jpg,jpeg,png,webp')), + 'audioExtensions' => explode(',', $params->get('audio_extensions', 'mp3,m4a,mp4a,ogg')), + 'videoExtensions' => explode(',', $params->get('video_extensions', 'mp4,mp4v,mpeg,mov,webm')), + 'documentExtensions' => explode(',', $params->get('doc_extensions', 'doc,odg,odp,ods,odt,pdf,ppt,txt,xcf,xls,csv')), + 'maxUploadSizeMb' => $params->get('upload_maxsize', 10), + 'contents' => $this->file->content, ]; $this->document->addScriptOptions('com_media', $config); @@ -63,13 +64,13 @@ $this->useCoreUI = true; ?>
- getFieldsets(); ?> - - 'attrib-' . reset($fieldSets)->name, 'breakpoint' => 768]); ?> - - '; ?> - - - + getFieldsets(); ?> + + 'attrib-' . reset($fieldSets)->name, 'breakpoint' => 768]); ?> + + '; ?> + + +
diff --git a/administrator/components/com_menus/helpers/menus.php b/administrator/components/com_menus/helpers/menus.php index 570b89ca30513..41cbaa13a4231 100644 --- a/administrator/components/com_menus/helpers/menus.php +++ b/administrator/components/com_menus/helpers/menus.php @@ -1,13 +1,16 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; + /** * Menus component helper. diff --git a/administrator/components/com_menus/layouts/joomla/menu/edit_modules.php b/administrator/components/com_menus/layouts/joomla/menu/edit_modules.php index 2c4ca108534ee..a8d3e336658c1 100644 --- a/administrator/components/com_menus/layouts/joomla/menu/edit_modules.php +++ b/administrator/components/com_menus/layouts/joomla/menu/edit_modules.php @@ -1,4 +1,5 @@ input; $component = $input->getCmd('option', 'com_content'); -if ($component == 'com_categories') -{ - $extension = $input->getCmd('extension', 'com_content'); - $parts = explode('.', $extension); - $component = $parts[0]; +if ($component == 'com_categories') { + $extension = $input->getCmd('extension', 'com_content'); + $parts = explode('.', $extension); + $component = $parts[0]; } $saveHistory = ComponentHelper::getParams($component)->get('save_history', 0); $fields = $displayData->get('fields') ?: array( - array('parent', 'parent_id'), - array('published', 'state', 'enabled'), - array('category', 'catid'), - 'featured', - 'sticky', - 'access', - 'language', - 'tags', - 'note', - 'version_note', + array('parent', 'parent_id'), + array('published', 'state', 'enabled'), + array('category', 'catid'), + 'featured', + 'sticky', + 'access', + 'language', + 'tags', + 'note', + 'version_note', ); $hiddenFields = $displayData->get('hidden_fields') ?: array(); -if (!$saveHistory) -{ - $hiddenFields[] = 'version_note'; +if (!$saveHistory) { + $hiddenFields[] = 'version_note'; } $html = array(); $html[] = '
    '; -foreach ($fields as $field) -{ - $field = is_array($field) ? $field : array($field); +foreach ($fields as $field) { + $field = is_array($field) ? $field : array($field); - foreach ($field as $f) - { - if ($form->getField($f)) - { - if (in_array($f, $hiddenFields)) - { - $form->setFieldAttribute($f, 'type', 'hidden'); - } + foreach ($field as $f) { + if ($form->getField($f)) { + if (in_array($f, $hiddenFields)) { + $form->setFieldAttribute($f, 'type', 'hidden'); + } - $html[] = '
  • ' . $form->renderField($f) . '
  • '; - break; - } - } + $html[] = '
  • ' . $form->renderField($f) . '
  • '; + break; + } + } } $html[] = '
'; diff --git a/administrator/components/com_menus/layouts/joomla/searchtools/default.php b/administrator/components/com_menus/layouts/joomla/searchtools/default.php index 9f0758024b86b..0901324668cab 100644 --- a/administrator/components/com_menus/layouts/joomla/searchtools/default.php +++ b/administrator/components/com_menus/layouts/joomla/searchtools/default.php @@ -1,4 +1,5 @@ filterForm) && !empty($data['view']->filterForm)) -{ - // Checks if a selector (e.g. client_id) exists. - if ($selectorField = $data['view']->filterForm->getField($selectorFieldName)) - { - $showSelector = $selectorField->getAttribute('filtermode', '') === 'selector' ? true : $showSelector; - - // Checks if a selector should be shown in the current layout. - if (isset($data['view']->layout)) - { - $showSelector = $selectorField->getAttribute('layout', 'default') != $data['view']->layout ? false : $showSelector; - } - - // Unset the selector field from active filters group. - unset($data['view']->activeFilters[$selectorFieldName]); - } - - if ($data['view'] instanceof \Joomla\Component\Menus\Administrator\View\Items\HtmlView) : - unset($data['view']->activeFilters['client_id']); - endif; - - // Checks if the filters button should exist. - $filters = $data['view']->filterForm->getGroup('filter'); - $showFilterButton = isset($filters['filter_search']) && count($filters) === 1 ? false : true; - - // Checks if it should show the be hidden. - $hideActiveFilters = empty($data['view']->activeFilters); - - // Check if the no results message should appear. - if (isset($data['view']->total) && (int) $data['view']->total === 0) - { - $noResults = $data['view']->filterForm->getFieldAttribute('search', 'noresults', '', 'filter'); - if (!empty($noResults)) - { - $noResultsText = Text::_($noResults); - } - } +if (isset($data['view']->filterForm) && !empty($data['view']->filterForm)) { + // Checks if a selector (e.g. client_id) exists. + if ($selectorField = $data['view']->filterForm->getField($selectorFieldName)) { + $showSelector = $selectorField->getAttribute('filtermode', '') === 'selector' ? true : $showSelector; + + // Checks if a selector should be shown in the current layout. + if (isset($data['view']->layout)) { + $showSelector = $selectorField->getAttribute('layout', 'default') != $data['view']->layout ? false : $showSelector; + } + + // Unset the selector field from active filters group. + unset($data['view']->activeFilters[$selectorFieldName]); + } + + if ($data['view'] instanceof \Joomla\Component\Menus\Administrator\View\Items\HtmlView) : + unset($data['view']->activeFilters['client_id']); + endif; + + // Checks if the filters button should exist. + $filters = $data['view']->filterForm->getGroup('filter'); + $showFilterButton = isset($filters['filter_search']) && count($filters) === 1 ? false : true; + + // Checks if it should show the be hidden. + $hideActiveFilters = empty($data['view']->activeFilters); + + // Check if the no results message should appear. + if (isset($data['view']->total) && (int) $data['view']->total === 0) { + $noResults = $data['view']->filterForm->getFieldAttribute('search', 'noresults', '', 'filter'); + if (!empty($noResults)) { + $noResultsText = Text::_($noResults); + } + } } // Set some basic options. $customOptions = array( - 'filtersHidden' => isset($data['options']['filtersHidden']) && $data['options']['filtersHidden'] ? $data['options']['filtersHidden'] : $hideActiveFilters, - 'filterButton' => isset($data['options']['filterButton']) && $data['options']['filterButton'] ? $data['options']['filterButton'] : $showFilterButton, - 'defaultLimit' => $data['options']['defaultLimit'] ?? Factory::getApplication()->get('list_limit', 20), - 'searchFieldSelector' => '#filter_search', - 'selectorFieldName' => $selectorFieldName, - 'showSelector' => $showSelector, - 'orderFieldSelector' => '#list_fullordering', - 'showNoResults' => !empty($noResultsText), - 'noResultsText' => !empty($noResultsText) ? $noResultsText : '', - 'formSelector' => !empty($data['options']['formSelector']) ? $data['options']['formSelector'] : '#adminForm', + 'filtersHidden' => isset($data['options']['filtersHidden']) && $data['options']['filtersHidden'] ? $data['options']['filtersHidden'] : $hideActiveFilters, + 'filterButton' => isset($data['options']['filterButton']) && $data['options']['filterButton'] ? $data['options']['filterButton'] : $showFilterButton, + 'defaultLimit' => $data['options']['defaultLimit'] ?? Factory::getApplication()->get('list_limit', 20), + 'searchFieldSelector' => '#filter_search', + 'selectorFieldName' => $selectorFieldName, + 'showSelector' => $showSelector, + 'orderFieldSelector' => '#list_fullordering', + 'showNoResults' => !empty($noResultsText), + 'noResultsText' => !empty($noResultsText) ? $noResultsText : '', + 'formSelector' => !empty($data['options']['formSelector']) ? $data['options']['formSelector'] : '#adminForm', ); // Merge custom options in the options array. @@ -89,39 +85,39 @@ HTMLHelper::_('searchtools.form', $data['options']['formSelector'], $data['options']); ?> - sublayout('noitems', $data); ?> + sublayout('noitems', $data); ?> diff --git a/administrator/components/com_menus/services/provider.php b/administrator/components/com_menus/services/provider.php index 3f6056d7734c4..2251234727ec5 100644 --- a/administrator/components/com_menus/services/provider.php +++ b/administrator/components/com_menus/services/provider.php @@ -1,4 +1,5 @@ set(AssociationExtensionInterface::class, new AssociationsHelper); - - $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Menus')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Menus')); - - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MenusComponent($container->get(ComponentDispatcherFactoryInterface::class)); - - $component->setRegistry($container->get(Registry::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); - - return $component; - } - ); - } + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->set(AssociationExtensionInterface::class, new AssociationsHelper()); + + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Menus')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Menus')); + + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MenusComponent($container->get(ComponentDispatcherFactoryInterface::class)); + + $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); + + return $component; + } + ); + } }; diff --git a/administrator/components/com_menus/src/Controller/AjaxController.php b/administrator/components/com_menus/src/Controller/AjaxController.php index 32787bc54a12c..127fcca779e09 100644 --- a/administrator/components/com_menus/src/Controller/AjaxController.php +++ b/administrator/components/com_menus/src/Controller/AjaxController.php @@ -1,4 +1,5 @@ input->getInt('assocId', 0); + /** + * Method to fetch associations of a menu item + * + * The method assumes that the following http parameters are passed in an Ajax Get request: + * token: the form token + * assocId: the id of the menu item whose associations are to be returned + * excludeLang: the association for this language is to be excluded + * + * @return null + * + * @since 3.9.0 + */ + public function fetchAssociations() + { + if (!Session::checkToken('get')) { + echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true); + } else { + $assocId = $this->input->getInt('assocId', 0); - if ($assocId == 0) - { - echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); + if ($assocId == 0) { + echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); - return; - } + return; + } - $excludeLang = $this->input->get('excludeLang', '', 'STRING'); + $excludeLang = $this->input->get('excludeLang', '', 'STRING'); - $associations = Associations::getAssociations('com_menus', '#__menu', 'com_menus.item', (int) $assocId, 'id', '', ''); + $associations = Associations::getAssociations('com_menus', '#__menu', 'com_menus.item', (int) $assocId, 'id', '', ''); - unset($associations[$excludeLang]); + unset($associations[$excludeLang]); - // Add the title to each of the associated records - Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_menus/tables'); - $menuTable = Table::getInstance('Menu', 'JTable', array()); + // Add the title to each of the associated records + Table::addIncludePath(JPATH_ADMINISTRATOR . '/components/com_menus/tables'); + $menuTable = Table::getInstance('Menu', 'JTable', array()); - foreach ($associations as $lang => $association) - { - $menuTable->load($association->id); - $associations[$lang]->title = $menuTable->title; - } + foreach ($associations as $lang => $association) { + $menuTable->load($association->id); + $associations[$lang]->title = $menuTable->title; + } - $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); + $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); - if (count($associations) == 0) - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); - } - elseif ($countContentLanguages > count($associations) + 2) - { - $tags = implode(', ', array_keys($associations)); - $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); - } - else - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); - } + if (count($associations) == 0) { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); + } elseif ($countContentLanguages > count($associations) + 2) { + $tags = implode(', ', array_keys($associations)); + $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); + } else { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); + } - echo new JsonResponse($associations, $message); - } - } + echo new JsonResponse($associations, $message); + } + } } diff --git a/administrator/components/com_menus/src/Controller/DisplayController.php b/administrator/components/com_menus/src/Controller/DisplayController.php index 4f6047b026eb7..7723dc6bccc05 100644 --- a/administrator/components/com_menus/src/Controller/DisplayController.php +++ b/administrator/components/com_menus/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->post->getCmd('menutype', ''); + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array|boolean $urlparams An array of safe URL parameters and their variable types, for valid values see {@link JFilterInput::clean()}. + * + * @return static This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + // Verify menu + $menuType = $this->input->post->getCmd('menutype', ''); - if ($menuType !== '') - { - $uri = Uri::getInstance(); + if ($menuType !== '') { + $uri = Uri::getInstance(); - if ($uri->getVar('menutype') !== $menuType) - { - $uri->setVar('menutype', $menuType); + if ($uri->getVar('menutype') !== $menuType) { + $uri->setVar('menutype', $menuType); - if ($forcedLanguage = $this->input->post->get('forcedLanguage')) - { - $uri->setVar('forcedLanguage', $forcedLanguage); - } + if ($forcedLanguage = $this->input->post->get('forcedLanguage')) { + $uri->setVar('forcedLanguage', $forcedLanguage); + } - $this->setRedirect(Route::_('index.php' . $uri->toString(['query']), false)); + $this->setRedirect(Route::_('index.php' . $uri->toString(['query']), false)); - return parent::display(); - } - } + return parent::display(); + } + } - // Check if we have a mod_menu module set to All languages or a mod_menu module for each admin language. - if ($langMissing = $this->getModel('Menus', 'Administrator')->getMissingModuleLanguages()) - { - $this->app->enqueueMessage(Text::sprintf('JMENU_MULTILANG_WARNING_MISSING_MODULES', implode(', ', $langMissing)), 'warning'); - } + // Check if we have a mod_menu module set to All languages or a mod_menu module for each admin language. + if ($langMissing = $this->getModel('Menus', 'Administrator')->getMissingModuleLanguages()) { + $this->app->enqueueMessage(Text::sprintf('JMENU_MULTILANG_WARNING_MISSING_MODULES', implode(', ', $langMissing)), 'warning'); + } - return parent::display(); - } + return parent::display(); + } } diff --git a/administrator/components/com_menus/src/Controller/ItemController.php b/administrator/components/com_menus/src/Controller/ItemController.php index cc1b0a5682d9f..f88c2d1abe35b 100644 --- a/administrator/components/com_menus/src/Controller/ItemController.php +++ b/administrator/components/com_menus/src/Controller/ItemController.php @@ -1,4 +1,5 @@ app->getIdentity(); - - $menuType = $this->input->getCmd('menutype', $data['menutype'] ?? ''); - - $menutypeID = 0; - - // Load menutype ID - if ($menuType) - { - $menutypeID = (int) $this->getMenuTypeId($menuType); - } - - return $user->authorise('core.create', 'com_menus.menu.' . $menutypeID); - } - - /** - * Method to check if you edit a record. - * - * Extended classes can override this if necessary. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key; default is id. - * - * @return boolean - * - * @since 3.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $user = $this->app->getIdentity(); - - $menutypeID = 0; - - if (isset($data[$key])) - { - $model = $this->getModel(); - $item = $model->getItem($data[$key]); - - if (!empty($item->menutype)) - { - // Protected menutype, do not allow edit - if ($item->menutype == 'main') - { - return false; - } - - $menutypeID = (int) $this->getMenuTypeId($item->menutype); - } - } - - return $user->authorise('core.edit', 'com_menus.menu.' . (int) $menutypeID); - } - - /** - * Loads the menutype ID by a given menutype string - * - * @param string $menutype The given menutype - * - * @return integer - * - * @since 3.6 - */ - protected function getMenuTypeId($menutype) - { - $model = $this->getModel(); - $table = $model->getTable('MenuType'); - - $table->load(array('menutype' => $menutype)); - - return (int) $table->id; - } - - /** - * Method to add a new menu item. - * - * @return mixed True if the record can be added, otherwise false. - * - * @since 1.6 - */ - public function add() - { - $result = parent::add(); - - if ($result) - { - $context = 'com_menus.edit.item'; - - $this->app->setUserState($context . '.type', null); - $this->app->setUserState($context . '.link', null); - } - - return $result; - } - - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 1.6 - */ - public function batch($model = null) - { - $this->checkToken(); - - /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ - $model = $this->getModel('Item', 'Administrator', array()); - - // Preset the redirect - $this->setRedirect(Route::_('index.php?option=com_menus&view=items' . $this->getRedirectToListAppend(), false)); - - return parent::batch($model); - } - - /** - * Method to cancel an edit. - * - * @param string $key The name of the primary key of the URL variable. - * - * @return boolean True if access level checks pass, false otherwise. - * - * @since 1.6 - */ - public function cancel($key = null) - { - $this->checkToken(); - - $result = parent::cancel(); - - if ($result) - { - // Clear the ancillary data from the session. - $context = 'com_menus.edit.item'; - $this->app->setUserState($context . '.type', null); - $this->app->setUserState($context . '.link', null); - - // Redirect to the list screen. - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend() - . '&menutype=' . $this->app->getUserState('com_menus.items.menutype'), false - ) - ); - } - - return $result; - } - - /** - * Method to edit an existing record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key - * (sometimes required to avoid router collisions). - * - * @return boolean True if access level check and checkout passes, false otherwise. - * - * @since 1.6 - */ - public function edit($key = null, $urlVar = null) - { - $result = parent::edit(); - - if ($result) - { - // Push the new ancillary data into the session. - $this->app->setUserState('com_menus.edit.item.type', null); - $this->app->setUserState('com_menus.edit.item.link', null); - } - - return $result; - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 3.0.1 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $append = parent::getRedirectToItemAppend($recordId, $urlVar); - - if ($recordId) - { - /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ - $model = $this->getModel(); - $item = $model->getItem($recordId); - $clientId = $item->client_id; - $append = '&client_id=' . $clientId . $append; - } - else - { - $clientId = $this->input->get('client_id', '0', 'int'); - $menuType = $this->input->get('menutype', 'mainmenu', 'cmd'); - $append = '&client_id=' . $clientId . ($menuType ? '&menutype=' . $menuType : '') . $append; - } - - return $append; - } - - /** - * Method to save a record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean True if successful, false otherwise. - * - * @since 1.6 - */ - public function save($key = null, $urlVar = null) - { - // Check for request forgeries. - $this->checkToken(); - - /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ - $model = $this->getModel('Item', 'Administrator', array()); - $table = $model->getTable(); - $data = $this->input->post->get('jform', array(), 'array'); - $task = $this->getTask(); - $context = 'com_menus.edit.item'; - $app = $this->app; - - // Set the menutype should we need it. - if ($data['menutype'] !== '') - { - $this->input->set('menutype', $data['menutype']); - } - - // Determine the name of the primary key for the data. - if (empty($key)) - { - $key = $table->getKeyName(); - } - - // To avoid data collisions the urlVar may be different from the primary key. - if (empty($urlVar)) - { - $urlVar = $key; - } - - $recordId = $this->input->getInt($urlVar); - - // Populate the row id from the session. - $data[$key] = $recordId; - - // The save2copy task needs to be handled slightly differently. - if ($task == 'save2copy') - { - // Check-in the original row. - if ($model->checkin($data['id']) === false) - { - // Check-in failed, go back to the item and display a notice. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'warning'); - - return false; - } - - // Reset the ID and then treat the request as for Apply. - $data['id'] = 0; - $data['associations'] = array(); - $task = 'apply'; - } - - // Access check. - if (!$this->allowSave($data, $key)) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(), false - ) - ); - - return false; - } - - // Validate the posted data. - // This post is made up of two forms, one for the item and one for params. - $form = $model->getForm($data); - - if (!$form) - { - throw new \Exception($model->getError(), 500); - } - - if ($data['type'] == 'url') - { - $data['link'] = str_replace(array('"', '>', '<'), '', $data['link']); - - if (strstr($data['link'], ':')) - { - $segments = explode(':', $data['link']); - $protocol = strtolower($segments[0]); - $scheme = array( - 'http', 'https', 'ftp', 'ftps', 'gopher', 'mailto', - 'news', 'prospero', 'telnet', 'rlogin', 'tn3270', 'wais', - 'mid', 'cid', 'nntp', 'tel', 'urn', 'ldap', 'file', 'fax', - 'modem', 'git', 'sms', - ); - - if (!in_array($protocol, $scheme)) - { - $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'warning'); - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId), false) - ); - - return false; - } - } - } - - $data = $model->validate($form, $data); - - // Preprocess request fields to ensure that we remove not set or empty request params - $request = $form->getGroup('request', true); - - // Check for the special 'request' entry. - if ($data['type'] == 'component' && !empty($request)) - { - $removeArgs = array(); - - if (!isset($data['request']) || !is_array($data['request'])) - { - $data['request'] = array(); - } - - foreach ($request as $field) - { - $fieldName = $field->getAttribute('name'); - - if (!isset($data['request'][$fieldName]) || $data['request'][$fieldName] == '') - { - $removeArgs[$fieldName] = ''; - } - } - - // Parse the submitted link arguments. - $args = array(); - parse_str(parse_url($data['link'], PHP_URL_QUERY), $args); - - // Merge in the user supplied request arguments. - $args = array_merge($args, $data['request']); - - // Remove the unused request params - if (!empty($args) && !empty($removeArgs)) - { - $args = array_diff_key($args, $removeArgs); - } - - $data['link'] = 'index.php?' . urldecode(http_build_query($args, '', '&')); - } - - // Check for validation errors. - if ($data === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Save the data in the session. - $app->setUserState('com_menus.edit.item.data', $data); - - // Redirect back to the edit screen. - $editUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); - $this->setRedirect(Route::_($editUrl, false)); - - return false; - } - - // Attempt to save the data. - if (!$model->save($data)) - { - // Save the data in the session. - $app->setUserState('com_menus.edit.item.data', $data); - - // Redirect back to the edit screen. - $editUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - $this->setRedirect(Route::_($editUrl, false)); - - return false; - } - - // Save succeeded, check-in the row. - if ($model->checkin($data['id']) === false) - { - // Check-in failed, go back to the row and display a notice. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'warning'); - $redirectUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); - $this->setRedirect(Route::_($redirectUrl, false)); - - return false; - } - - $this->setMessage(Text::_('COM_MENUS_SAVE_SUCCESS')); - - // Redirect the user and adjust session state based on the chosen task. - switch ($task) - { - case 'apply': - // Set the row data in the session. - $recordId = $model->getState($this->context . '.id'); - $this->holdEditId($context, $recordId); - $app->setUserState('com_menus.edit.item.data', null); - $app->setUserState('com_menus.edit.item.type', null); - $app->setUserState('com_menus.edit.item.link', null); - - // Redirect back to the edit screen. - $editUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); - $this->setRedirect(Route::_($editUrl, false)); - break; - - case 'save2new': - // Clear the row id and data in the session. - $this->releaseEditId($context, $recordId); - $app->setUserState('com_menus.edit.item.data', null); - $app->setUserState('com_menus.edit.item.type', null); - $app->setUserState('com_menus.edit.item.link', null); - - // Redirect back to the edit screen. - $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend(), false)); - break; - - default: - // Clear the row id and data in the session. - $this->releaseEditId($context, $recordId); - $app->setUserState('com_menus.edit.item.data', null); - $app->setUserState('com_menus.edit.item.type', null); - $app->setUserState('com_menus.edit.item.link', null); - - // Redirect to the list screen. - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend() - . '&menutype=' . $app->getUserState('com_menus.items.menutype'), false - ) - ); - break; - } - - return true; - } - - /** - * Sets the type of the menu item currently being edited. - * - * @return void - * - * @since 1.6 - */ - public function setType() - { - $this->checkToken(); - - $app = $this->app; - - // Get the posted values from the request. - $data = $this->input->post->get('jform', array(), 'array'); - - // Get the type. - $type = $data['type']; - - $type = json_decode(base64_decode($type)); - $title = $type->title ?? null; - $recordId = $type->id ?? 0; - - $specialTypes = array('alias', 'separator', 'url', 'heading', 'container'); - - if (!in_array($title, $specialTypes)) - { - $title = 'component'; - } - else - { - // Set correct component id to ensure proper 404 messages with system links - $data['component_id'] = 0; - } - - $app->setUserState('com_menus.edit.item.type', $title); - - if ($title == 'component') - { - if (isset($type->request)) - { - // Clean component name - $type->request->option = InputFilter::getInstance()->clean($type->request->option, 'CMD'); - - $component = ComponentHelper::getComponent($type->request->option); - $data['component_id'] = $component->id; - - $app->setUserState('com_menus.edit.item.link', 'index.php?' . Uri::buildQuery((array) $type->request)); - } - } - // If the type is alias you just need the item id from the menu item referenced. - elseif ($title == 'alias') - { - $app->setUserState('com_menus.edit.item.link', 'index.php?Itemid='); - } - - unset($data['request']); - - $data['type'] = $title; - - if ($this->input->get('fieldtype') == 'type') - { - $data['link'] = $app->getUserState('com_menus.edit.item.link'); - } - - // Save the data in the session. - $app->setUserState('com_menus.edit.item.data', $data); - - $this->type = $type; - $this->setRedirect( - Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId), false) - ); - } - - /** - * Gets the parent items of the menu location currently. - * - * @return void - * - * @since 3.2 - */ - public function getParentItem() - { - $app = $this->app; - - $results = array(); - $menutype = $this->input->get->get('menutype'); - - if ($menutype) - { - /** @var \Joomla\Component\Menus\Administrator\Model\ItemsModel $model */ - $model = $this->getModel('Items', 'Administrator', array()); - $model->getState(); - $model->setState('filter.menutype', $menutype); - $model->setState('list.select', 'a.id, a.title, a.level'); - $model->setState('list.start', '0'); - $model->setState('list.limit', '0'); - - $results = $model->getItems(); - - // Pad the option text with spaces using depth level as a multiplier. - for ($i = 0, $n = count($results); $i < $n; $i++) - { - $results[$i]->title = str_repeat(' - ', $results[$i]->level) . $results[$i]->title; - } - } - - // Output a \JSON object - echo json_encode($results); - - $app->close(); - } + /** + * Method to check if you can add a new record. + * + * Extended classes can override this if necessary. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 3.6 + */ + protected function allowAdd($data = array()) + { + $user = $this->app->getIdentity(); + + $menuType = $this->input->getCmd('menutype', $data['menutype'] ?? ''); + + $menutypeID = 0; + + // Load menutype ID + if ($menuType) { + $menutypeID = (int) $this->getMenuTypeId($menuType); + } + + return $user->authorise('core.create', 'com_menus.menu.' . $menutypeID); + } + + /** + * Method to check if you edit a record. + * + * Extended classes can override this if necessary. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key; default is id. + * + * @return boolean + * + * @since 3.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $user = $this->app->getIdentity(); + + $menutypeID = 0; + + if (isset($data[$key])) { + $model = $this->getModel(); + $item = $model->getItem($data[$key]); + + if (!empty($item->menutype)) { + // Protected menutype, do not allow edit + if ($item->menutype == 'main') { + return false; + } + + $menutypeID = (int) $this->getMenuTypeId($item->menutype); + } + } + + return $user->authorise('core.edit', 'com_menus.menu.' . (int) $menutypeID); + } + + /** + * Loads the menutype ID by a given menutype string + * + * @param string $menutype The given menutype + * + * @return integer + * + * @since 3.6 + */ + protected function getMenuTypeId($menutype) + { + $model = $this->getModel(); + $table = $model->getTable('MenuType'); + + $table->load(array('menutype' => $menutype)); + + return (int) $table->id; + } + + /** + * Method to add a new menu item. + * + * @return mixed True if the record can be added, otherwise false. + * + * @since 1.6 + */ + public function add() + { + $result = parent::add(); + + if ($result) { + $context = 'com_menus.edit.item'; + + $this->app->setUserState($context . '.type', null); + $this->app->setUserState($context . '.link', null); + } + + return $result; + } + + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 1.6 + */ + public function batch($model = null) + { + $this->checkToken(); + + /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ + $model = $this->getModel('Item', 'Administrator', array()); + + // Preset the redirect + $this->setRedirect(Route::_('index.php?option=com_menus&view=items' . $this->getRedirectToListAppend(), false)); + + return parent::batch($model); + } + + /** + * Method to cancel an edit. + * + * @param string $key The name of the primary key of the URL variable. + * + * @return boolean True if access level checks pass, false otherwise. + * + * @since 1.6 + */ + public function cancel($key = null) + { + $this->checkToken(); + + $result = parent::cancel(); + + if ($result) { + // Clear the ancillary data from the session. + $context = 'com_menus.edit.item'; + $this->app->setUserState($context . '.type', null); + $this->app->setUserState($context . '.link', null); + + // Redirect to the list screen. + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend() + . '&menutype=' . $this->app->getUserState('com_menus.items.menutype'), + false + ) + ); + } + + return $result; + } + + /** + * Method to edit an existing record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key + * (sometimes required to avoid router collisions). + * + * @return boolean True if access level check and checkout passes, false otherwise. + * + * @since 1.6 + */ + public function edit($key = null, $urlVar = null) + { + $result = parent::edit(); + + if ($result) { + // Push the new ancillary data into the session. + $this->app->setUserState('com_menus.edit.item.type', null); + $this->app->setUserState('com_menus.edit.item.link', null); + } + + return $result; + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 3.0.1 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $append = parent::getRedirectToItemAppend($recordId, $urlVar); + + if ($recordId) { + /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ + $model = $this->getModel(); + $item = $model->getItem($recordId); + $clientId = $item->client_id; + $append = '&client_id=' . $clientId . $append; + } else { + $clientId = $this->input->get('client_id', '0', 'int'); + $menuType = $this->input->get('menutype', 'mainmenu', 'cmd'); + $append = '&client_id=' . $clientId . ($menuType ? '&menutype=' . $menuType : '') . $append; + } + + return $append; + } + + /** + * Method to save a record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 1.6 + */ + public function save($key = null, $urlVar = null) + { + // Check for request forgeries. + $this->checkToken(); + + /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ + $model = $this->getModel('Item', 'Administrator', array()); + $table = $model->getTable(); + $data = $this->input->post->get('jform', array(), 'array'); + $task = $this->getTask(); + $context = 'com_menus.edit.item'; + $app = $this->app; + + // Set the menutype should we need it. + if ($data['menutype'] !== '') { + $this->input->set('menutype', $data['menutype']); + } + + // Determine the name of the primary key for the data. + if (empty($key)) { + $key = $table->getKeyName(); + } + + // To avoid data collisions the urlVar may be different from the primary key. + if (empty($urlVar)) { + $urlVar = $key; + } + + $recordId = $this->input->getInt($urlVar); + + // Populate the row id from the session. + $data[$key] = $recordId; + + // The save2copy task needs to be handled slightly differently. + if ($task == 'save2copy') { + // Check-in the original row. + if ($model->checkin($data['id']) === false) { + // Check-in failed, go back to the item and display a notice. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'warning'); + + return false; + } + + // Reset the ID and then treat the request as for Apply. + $data['id'] = 0; + $data['associations'] = array(); + $task = 'apply'; + } + + // Access check. + if (!$this->allowSave($data, $key)) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . $this->getRedirectToListAppend(), + false + ) + ); + + return false; + } + + // Validate the posted data. + // This post is made up of two forms, one for the item and one for params. + $form = $model->getForm($data); + + if (!$form) { + throw new \Exception($model->getError(), 500); + } + + if ($data['type'] == 'url') { + $data['link'] = str_replace(array('"', '>', '<'), '', $data['link']); + + if (strstr($data['link'], ':')) { + $segments = explode(':', $data['link']); + $protocol = strtolower($segments[0]); + $scheme = array( + 'http', 'https', 'ftp', 'ftps', 'gopher', 'mailto', + 'news', 'prospero', 'telnet', 'rlogin', 'tn3270', 'wais', + 'mid', 'cid', 'nntp', 'tel', 'urn', 'ldap', 'file', 'fax', + 'modem', 'git', 'sms', + ); + + if (!in_array($protocol, $scheme)) { + $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'warning'); + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId), false) + ); + + return false; + } + } + } + + $data = $model->validate($form, $data); + + // Preprocess request fields to ensure that we remove not set or empty request params + $request = $form->getGroup('request', true); + + // Check for the special 'request' entry. + if ($data['type'] == 'component' && !empty($request)) { + $removeArgs = array(); + + if (!isset($data['request']) || !is_array($data['request'])) { + $data['request'] = array(); + } + + foreach ($request as $field) { + $fieldName = $field->getAttribute('name'); + + if (!isset($data['request'][$fieldName]) || $data['request'][$fieldName] == '') { + $removeArgs[$fieldName] = ''; + } + } + + // Parse the submitted link arguments. + $args = array(); + parse_str(parse_url($data['link'], PHP_URL_QUERY), $args); + + // Merge in the user supplied request arguments. + $args = array_merge($args, $data['request']); + + // Remove the unused request params + if (!empty($args) && !empty($removeArgs)) { + $args = array_diff_key($args, $removeArgs); + } + + $data['link'] = 'index.php?' . urldecode(http_build_query($args, '', '&')); + } + + // Check for validation errors. + if ($data === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Save the data in the session. + $app->setUserState('com_menus.edit.item.data', $data); + + // Redirect back to the edit screen. + $editUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); + $this->setRedirect(Route::_($editUrl, false)); + + return false; + } + + // Attempt to save the data. + if (!$model->save($data)) { + // Save the data in the session. + $app->setUserState('com_menus.edit.item.data', $data); + + // Redirect back to the edit screen. + $editUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); + $this->setRedirect(Route::_($editUrl, false)); + + return false; + } + + // Save succeeded, check-in the row. + if ($model->checkin($data['id']) === false) { + // Check-in failed, go back to the row and display a notice. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'warning'); + $redirectUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); + $this->setRedirect(Route::_($redirectUrl, false)); + + return false; + } + + $this->setMessage(Text::_('COM_MENUS_SAVE_SUCCESS')); + + // Redirect the user and adjust session state based on the chosen task. + switch ($task) { + case 'apply': + // Set the row data in the session. + $recordId = $model->getState($this->context . '.id'); + $this->holdEditId($context, $recordId); + $app->setUserState('com_menus.edit.item.data', null); + $app->setUserState('com_menus.edit.item.type', null); + $app->setUserState('com_menus.edit.item.link', null); + + // Redirect back to the edit screen. + $editUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId); + $this->setRedirect(Route::_($editUrl, false)); + break; + + case 'save2new': + // Clear the row id and data in the session. + $this->releaseEditId($context, $recordId); + $app->setUserState('com_menus.edit.item.data', null); + $app->setUserState('com_menus.edit.item.type', null); + $app->setUserState('com_menus.edit.item.link', null); + + // Redirect back to the edit screen. + $this->setRedirect(Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend(), false)); + break; + + default: + // Clear the row id and data in the session. + $this->releaseEditId($context, $recordId); + $app->setUserState('com_menus.edit.item.data', null); + $app->setUserState('com_menus.edit.item.type', null); + $app->setUserState('com_menus.edit.item.link', null); + + // Redirect to the list screen. + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend() + . '&menutype=' . $app->getUserState('com_menus.items.menutype'), + false + ) + ); + break; + } + + return true; + } + + /** + * Sets the type of the menu item currently being edited. + * + * @return void + * + * @since 1.6 + */ + public function setType() + { + $this->checkToken(); + + $app = $this->app; + + // Get the posted values from the request. + $data = $this->input->post->get('jform', array(), 'array'); + + // Get the type. + $type = $data['type']; + + $type = json_decode(base64_decode($type)); + $title = $type->title ?? null; + $recordId = $type->id ?? 0; + + $specialTypes = array('alias', 'separator', 'url', 'heading', 'container'); + + if (!in_array($title, $specialTypes)) { + $title = 'component'; + } else { + // Set correct component id to ensure proper 404 messages with system links + $data['component_id'] = 0; + } + + $app->setUserState('com_menus.edit.item.type', $title); + + if ($title == 'component') { + if (isset($type->request)) { + // Clean component name + $type->request->option = InputFilter::getInstance()->clean($type->request->option, 'CMD'); + + $component = ComponentHelper::getComponent($type->request->option); + $data['component_id'] = $component->id; + + $app->setUserState('com_menus.edit.item.link', 'index.php?' . Uri::buildQuery((array) $type->request)); + } + } elseif ($title == 'alias') { + // If the type is alias you just need the item id from the menu item referenced. + $app->setUserState('com_menus.edit.item.link', 'index.php?Itemid='); + } + + unset($data['request']); + + $data['type'] = $title; + + if ($this->input->get('fieldtype') == 'type') { + $data['link'] = $app->getUserState('com_menus.edit.item.link'); + } + + // Save the data in the session. + $app->setUserState('com_menus.edit.item.data', $data); + + $this->type = $type; + $this->setRedirect( + Route::_('index.php?option=' . $this->option . '&view=' . $this->view_item . $this->getRedirectToItemAppend($recordId), false) + ); + } + + /** + * Gets the parent items of the menu location currently. + * + * @return void + * + * @since 3.2 + */ + public function getParentItem() + { + $app = $this->app; + + $results = array(); + $menutype = $this->input->get->get('menutype'); + + if ($menutype) { + /** @var \Joomla\Component\Menus\Administrator\Model\ItemsModel $model */ + $model = $this->getModel('Items', 'Administrator', array()); + $model->getState(); + $model->setState('filter.menutype', $menutype); + $model->setState('list.select', 'a.id, a.title, a.level'); + $model->setState('list.start', '0'); + $model->setState('list.limit', '0'); + + $results = $model->getItems(); + + // Pad the option text with spaces using depth level as a multiplier. + for ($i = 0, $n = count($results); $i < $n; $i++) { + $results[$i]->title = str_repeat(' - ', $results[$i]->level) . $results[$i]->title; + } + } + + // Output a \JSON object + echo json_encode($results); + + $app->close(); + } } diff --git a/administrator/components/com_menus/src/Controller/ItemsController.php b/administrator/components/com_menus/src/Controller/ItemsController.php index 484ee17a0ba5c..7d9556809bf3d 100644 --- a/administrator/components/com_menus/src/Controller/ItemsController.php +++ b/administrator/components/com_menus/src/Controller/ItemsController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Menus\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Menus\Administrator\Controller; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Language\Text; @@ -27,247 +27,219 @@ */ class ItemsController extends AdminController { - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * @param CMSApplication $app The Application for the dispatcher - * @param Input $input Input - * - * @since 1.6 - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - parent::__construct($config, $factory, $app, $input); - - $this->registerTask('unsetDefault', 'setDefault'); - } - - /** - * Proxy for getModel. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return object The model. - * - * @since 1.6 - */ - public function getModel($name = 'Item', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to get the number of published frontend menu items for quickicons - * - * @return void - * - * @since 4.0.0 - */ - public function getQuickiconContent() - { - $model = $this->getModel('Items'); - - $model->setState('filter.published', 1); - $model->setState('filter.client_id', 0); - - $amount = (int) $model->getTotal(); - - $result = []; - - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_MENUS_ITEMS_N_QUICKICON_SRONLY', $amount); - $result['name'] = Text::plural('COM_MENUS_ITEMS_N_QUICKICON', $amount); - - echo new JsonResponse($result); - } - - /** - * Rebuild the nested set tree. - * - * @return boolean False on failure or error, true on success. - * - * @since 1.6 - */ - public function rebuild() - { - $this->checkToken(); - - $this->setRedirect('index.php?option=com_menus&view=items&menutype=' . $this->input->getCmd('menutype')); - - /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ - $model = $this->getModel(); - - if ($model->rebuild()) - { - // Reorder succeeded. - $this->setMessage(Text::_('COM_MENUS_ITEMS_REBUILD_SUCCESS')); - - return true; - } - else - { - // Rebuild failed. - $this->setMessage(Text::sprintf('COM_MENUS_ITEMS_REBUILD_FAILED'), 'error'); - - return false; - } - } - - /** - * Method to set the home property for a list of items - * - * @return void - * - * @since 1.6 - */ - public function setDefault() - { - // Check for request forgeries - $this->checkToken('request'); - - $app = $this->app; - - // Get items to publish from the request. - $cid = (array) $this->input->get('cid', array(), 'int'); - $data = array('setDefault' => 1, 'unsetDefault' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($data, $task, 0, 'int'); - - // Remove zero values resulting from input filter - $cid = array_filter($cid); - - if (empty($cid)) - { - $this->setMessage(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), 'warning'); - } - else - { - // Get the model. - $model = $this->getModel(); - - // Publish the items. - if (!$model->setHome($cid, $value)) - { - $this->setMessage($model->getError(), 'warning'); - } - else - { - if ($value == 1) - { - $ntext = 'COM_MENUS_ITEMS_SET_HOME'; - } - else - { - $ntext = 'COM_MENUS_ITEMS_UNSET_HOME'; - } - - $this->setMessage(Text::plural($ntext, count($cid))); - } - } - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . '&menutype=' . $app->getUserState('com_menus.items.menutype'), false - ) - ); - } - - /** - * Method to publish a list of items - * - * @return void - * - * @since 3.6.0 - */ - public function publish() - { - // Check for request forgeries - $this->checkToken(); - - // Get items to publish from the request. - $cid = (array) $this->input->get('cid', array(), 'int'); - $data = array('publish' => 1, 'unpublish' => 0, 'trash' => -2, 'report' => -3); - $task = $this->getTask(); - $value = ArrayHelper::getValue($data, $task, 0, 'int'); - - // Remove zero values resulting from input filter - $cid = array_filter($cid); - - if (empty($cid)) - { - try - { - Log::add(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - $this->setMessage(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), 'warning'); - } - } - else - { - // Get the model. - $model = $this->getModel(); - - // Publish the items. - try - { - $model->publish($cid, $value); - $errors = $model->getErrors(); - $messageType = 'message'; - - if ($value == 1) - { - if ($errors) - { - $messageType = 'error'; - $ntext = $this->text_prefix . '_N_ITEMS_FAILED_PUBLISHING'; - } - else - { - $ntext = $this->text_prefix . '_N_ITEMS_PUBLISHED'; - } - } - elseif ($value == 0) - { - $ntext = $this->text_prefix . '_N_ITEMS_UNPUBLISHED'; - } - else - { - $ntext = $this->text_prefix . '_N_ITEMS_TRASHED'; - } - - $this->setMessage(Text::plural($ntext, count($cid)), $messageType); - } - catch (\Exception $e) - { - $this->setMessage($e->getMessage(), 'error'); - } - } - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list . '&menutype=' . - $this->app->getUserState('com_menus.items.menutype'), - false - ) - ); - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - return '&menutype=' . $this->app->getUserState('com_menus.items.menutype'); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('unsetDefault', 'setDefault'); + } + + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 1.6 + */ + public function getModel($name = 'Item', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to get the number of published frontend menu items for quickicons + * + * @return void + * + * @since 4.0.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('Items'); + + $model->setState('filter.published', 1); + $model->setState('filter.client_id', 0); + + $amount = (int) $model->getTotal(); + + $result = []; + + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_MENUS_ITEMS_N_QUICKICON_SRONLY', $amount); + $result['name'] = Text::plural('COM_MENUS_ITEMS_N_QUICKICON', $amount); + + echo new JsonResponse($result); + } + + /** + * Rebuild the nested set tree. + * + * @return boolean False on failure or error, true on success. + * + * @since 1.6 + */ + public function rebuild() + { + $this->checkToken(); + + $this->setRedirect('index.php?option=com_menus&view=items&menutype=' . $this->input->getCmd('menutype')); + + /** @var \Joomla\Component\Menus\Administrator\Model\ItemModel $model */ + $model = $this->getModel(); + + if ($model->rebuild()) { + // Reorder succeeded. + $this->setMessage(Text::_('COM_MENUS_ITEMS_REBUILD_SUCCESS')); + + return true; + } else { + // Rebuild failed. + $this->setMessage(Text::sprintf('COM_MENUS_ITEMS_REBUILD_FAILED'), 'error'); + + return false; + } + } + + /** + * Method to set the home property for a list of items + * + * @return void + * + * @since 1.6 + */ + public function setDefault() + { + // Check for request forgeries + $this->checkToken('request'); + + $app = $this->app; + + // Get items to publish from the request. + $cid = (array) $this->input->get('cid', array(), 'int'); + $data = array('setDefault' => 1, 'unsetDefault' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($data, $task, 0, 'int'); + + // Remove zero values resulting from input filter + $cid = array_filter($cid); + + if (empty($cid)) { + $this->setMessage(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), 'warning'); + } else { + // Get the model. + $model = $this->getModel(); + + // Publish the items. + if (!$model->setHome($cid, $value)) { + $this->setMessage($model->getError(), 'warning'); + } else { + if ($value == 1) { + $ntext = 'COM_MENUS_ITEMS_SET_HOME'; + } else { + $ntext = 'COM_MENUS_ITEMS_UNSET_HOME'; + } + + $this->setMessage(Text::plural($ntext, count($cid))); + } + } + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . '&menutype=' . $app->getUserState('com_menus.items.menutype'), + false + ) + ); + } + + /** + * Method to publish a list of items + * + * @return void + * + * @since 3.6.0 + */ + public function publish() + { + // Check for request forgeries + $this->checkToken(); + + // Get items to publish from the request. + $cid = (array) $this->input->get('cid', array(), 'int'); + $data = array('publish' => 1, 'unpublish' => 0, 'trash' => -2, 'report' => -3); + $task = $this->getTask(); + $value = ArrayHelper::getValue($data, $task, 0, 'int'); + + // Remove zero values resulting from input filter + $cid = array_filter($cid); + + if (empty($cid)) { + try { + Log::add(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + $this->setMessage(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), 'warning'); + } + } else { + // Get the model. + $model = $this->getModel(); + + // Publish the items. + try { + $model->publish($cid, $value); + $errors = $model->getErrors(); + $messageType = 'message'; + + if ($value == 1) { + if ($errors) { + $messageType = 'error'; + $ntext = $this->text_prefix . '_N_ITEMS_FAILED_PUBLISHING'; + } else { + $ntext = $this->text_prefix . '_N_ITEMS_PUBLISHED'; + } + } elseif ($value == 0) { + $ntext = $this->text_prefix . '_N_ITEMS_UNPUBLISHED'; + } else { + $ntext = $this->text_prefix . '_N_ITEMS_TRASHED'; + } + + $this->setMessage(Text::plural($ntext, count($cid)), $messageType); + } catch (\Exception $e) { + $this->setMessage($e->getMessage(), 'error'); + } + } + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list . '&menutype=' . + $this->app->getUserState('com_menus.items.menutype'), + false + ) + ); + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + return '&menutype=' . $this->app->getUserState('com_menus.items.menutype'); + } } diff --git a/administrator/components/com_menus/src/Controller/MenuController.php b/administrator/components/com_menus/src/Controller/MenuController.php index c2d2cd536c49b..9a087bbce09d6 100644 --- a/administrator/components/com_menus/src/Controller/MenuController.php +++ b/administrator/components/com_menus/src/Controller/MenuController.php @@ -1,4 +1,5 @@ setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); - } - - /** - * Method to save a menu item. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean True if successful, false otherwise. - * - * @since 1.6 - */ - public function save($key = null, $urlVar = null) - { - // Check for request forgeries. - $this->checkToken(); - - $app = $this->app; - $data = $this->input->post->get('jform', array(), 'array'); - $context = 'com_menus.edit.menu'; - $task = $this->getTask(); - $recordId = $this->input->getInt('id'); - - // Prevent using 'main' as menutype as this is reserved for backend menus - if (strtolower($data['menutype']) == 'main') - { - $this->setMessage(Text::_('COM_MENUS_ERROR_MENUTYPE'), 'error'); - - // Redirect back to the edit screen. - $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); - - return false; - } - - $data['menutype'] = InputFilter::getInstance()->clean($data['menutype'], 'TRIM'); - - // Populate the row id from the session. - $data['id'] = $recordId; - - // Get the model and attempt to validate the posted data. - /** @var \Joomla\Component\Menus\Administrator\Model\MenuModel $model */ - $model = $this->getModel('Menu', '', ['ignore_request' => false]); - $form = $model->getForm(); - - if (!$form) - { - throw new \Exception($model->getError(), 500); - } - - $validData = $model->validate($form, $data); - - // Check for validation errors. - if ($validData === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Save the data in the session. - $app->setUserState($context . '.data', $data); - - // Redirect back to the edit screen. - $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); - - return false; - } - - if (isset($validData['preset'])) - { - $preset = trim($validData['preset']) ?: null; - - unset($validData['preset']); - } - - // Attempt to save the data. - if (!$model->save($validData)) - { - // Save the data in the session. - $app->setUserState($context . '.data', $validData); - - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); - - return false; - } - - // Import the preset selected - if (isset($preset) && $data['client_id'] == 1) - { - // Menu Type has not been saved yet. Make sure items get the real menutype. - $menutype = ApplicationHelper::stringURLSafe($data['menutype']); - - try - { - MenusHelper::installPreset($preset, $menutype); - - $this->setMessage(Text::_('COM_MENUS_PRESET_IMPORT_SUCCESS')); - } - catch (\Exception $e) - { - // Save was successful but the preset could not be loaded. Let it through with just a warning - $this->setMessage(Text::sprintf('COM_MENUS_PRESET_IMPORT_FAILED', $e->getMessage())); - } - } - else - { - $this->setMessage(Text::_('COM_MENUS_MENU_SAVE_SUCCESS')); - } - - // Redirect the user and adjust session state based on the chosen task. - switch ($task) - { - case 'apply': - // Set the record data in the session. - $recordId = $model->getState($this->context . '.id'); - $this->holdEditId($context, $recordId); - $app->setUserState($context . '.data', null); - - // Redirect back to the edit screen. - $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); - break; - - case 'save2new': - // Clear the record id and data from the session. - $this->releaseEditId($context, $recordId); - $app->setUserState($context . '.data', null); - - // Redirect back to the edit screen. - $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit', false)); - break; - - default: - // Clear the record id and data from the session. - $this->releaseEditId($context, $recordId); - $app->setUserState($context . '.data', null); - - // Redirect to the list screen. - $this->setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); - break; - } - } - - /** - * Method to display a menu as preset xml. - * - * @return boolean True if successful, false otherwise. - * - * @since 3.8.0 - */ - public function exportXml() - { - // Check for request forgeries. - $this->checkToken(); - - $cid = (array) $this->input->get('cid', array(), 'int'); - - // We know the first element is the one we need because we don't allow multi selection of rows - $id = empty($cid) ? 0 : reset($cid); - - if ($id === 0) - { - $this->setMessage(Text::_('COM_MENUS_SELECT_MENU_FIRST_EXPORT'), 'warning'); - - $this->setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); - - return false; - } - - $model = $this->getModel('Menu'); - $item = $model->getItem($id); - - if (!$item->menutype) - { - $this->setMessage(Text::_('COM_MENUS_SELECT_MENU_FIRST_EXPORT'), 'warning'); - - $this->setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); - - return false; - } - - $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&menutype=' . $item->menutype . '&format=xml', false)); - - return true; - } + /** + * Dummy method to redirect back to standard controller + * + * @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()}. + * + * @return void + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $this->setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); + } + + /** + * Method to save a menu item. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 1.6 + */ + public function save($key = null, $urlVar = null) + { + // Check for request forgeries. + $this->checkToken(); + + $app = $this->app; + $data = $this->input->post->get('jform', array(), 'array'); + $context = 'com_menus.edit.menu'; + $task = $this->getTask(); + $recordId = $this->input->getInt('id'); + + // Prevent using 'main' as menutype as this is reserved for backend menus + if (strtolower($data['menutype']) == 'main') { + $this->setMessage(Text::_('COM_MENUS_ERROR_MENUTYPE'), 'error'); + + // Redirect back to the edit screen. + $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); + + return false; + } + + $data['menutype'] = InputFilter::getInstance()->clean($data['menutype'], 'TRIM'); + + // Populate the row id from the session. + $data['id'] = $recordId; + + // Get the model and attempt to validate the posted data. + /** @var \Joomla\Component\Menus\Administrator\Model\MenuModel $model */ + $model = $this->getModel('Menu', '', ['ignore_request' => false]); + $form = $model->getForm(); + + if (!$form) { + throw new \Exception($model->getError(), 500); + } + + $validData = $model->validate($form, $data); + + // Check for validation errors. + if ($validData === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Save the data in the session. + $app->setUserState($context . '.data', $data); + + // Redirect back to the edit screen. + $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); + + return false; + } + + if (isset($validData['preset'])) { + $preset = trim($validData['preset']) ?: null; + + unset($validData['preset']); + } + + // Attempt to save the data. + if (!$model->save($validData)) { + // Save the data in the session. + $app->setUserState($context . '.data', $validData); + + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); + $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); + + return false; + } + + // Import the preset selected + if (isset($preset) && $data['client_id'] == 1) { + // Menu Type has not been saved yet. Make sure items get the real menutype. + $menutype = ApplicationHelper::stringURLSafe($data['menutype']); + + try { + MenusHelper::installPreset($preset, $menutype); + + $this->setMessage(Text::_('COM_MENUS_PRESET_IMPORT_SUCCESS')); + } catch (\Exception $e) { + // Save was successful but the preset could not be loaded. Let it through with just a warning + $this->setMessage(Text::sprintf('COM_MENUS_PRESET_IMPORT_FAILED', $e->getMessage())); + } + } else { + $this->setMessage(Text::_('COM_MENUS_MENU_SAVE_SUCCESS')); + } + + // Redirect the user and adjust session state based on the chosen task. + switch ($task) { + case 'apply': + // Set the record data in the session. + $recordId = $model->getState($this->context . '.id'); + $this->holdEditId($context, $recordId); + $app->setUserState($context . '.data', null); + + // Redirect back to the edit screen. + $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit' . $this->getRedirectToItemAppend($recordId), false)); + break; + + case 'save2new': + // Clear the record id and data from the session. + $this->releaseEditId($context, $recordId); + $app->setUserState($context . '.data', null); + + // Redirect back to the edit screen. + $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&layout=edit', false)); + break; + + default: + // Clear the record id and data from the session. + $this->releaseEditId($context, $recordId); + $app->setUserState($context . '.data', null); + + // Redirect to the list screen. + $this->setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); + break; + } + } + + /** + * Method to display a menu as preset xml. + * + * @return boolean True if successful, false otherwise. + * + * @since 3.8.0 + */ + public function exportXml() + { + // Check for request forgeries. + $this->checkToken(); + + $cid = (array) $this->input->get('cid', array(), 'int'); + + // We know the first element is the one we need because we don't allow multi selection of rows + $id = empty($cid) ? 0 : reset($cid); + + if ($id === 0) { + $this->setMessage(Text::_('COM_MENUS_SELECT_MENU_FIRST_EXPORT'), 'warning'); + + $this->setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); + + return false; + } + + $model = $this->getModel('Menu'); + $item = $model->getItem($id); + + if (!$item->menutype) { + $this->setMessage(Text::_('COM_MENUS_SELECT_MENU_FIRST_EXPORT'), 'warning'); + + $this->setRedirect(Route::_('index.php?option=com_menus&view=menus', false)); + + return false; + } + + $this->setRedirect(Route::_('index.php?option=com_menus&view=menu&menutype=' . $item->menutype . '&format=xml', false)); + + return true; + } } diff --git a/administrator/components/com_menus/src/Controller/MenusController.php b/administrator/components/com_menus/src/Controller/MenusController.php index 3bde6a0490a23..44e3bdfbf477b 100644 --- a/administrator/components/com_menus/src/Controller/MenusController.php +++ b/administrator/components/com_menus/src/Controller/MenusController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Remove an item. - * - * @return void - * - * @since 1.6 - */ - public function delete() - { - // Check for request forgeries - $this->checkToken(); - - $user = $this->app->getIdentity(); - $cids = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $cids = array_filter($cids); - - if (empty($cids)) - { - $this->setMessage(Text::_('COM_MENUS_NO_MENUS_SELECTED'), 'warning'); - } - else - { - // Access checks. - foreach ($cids as $i => $id) - { - if (!$user->authorise('core.delete', 'com_menus.menu.' . (int) $id)) - { - // Prune items that you can't change. - unset($cids[$i]); - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 'error'); - } - } - - if (count($cids) > 0) - { - // Get the model. - /** @var \Joomla\Component\Menus\Administrator\Model\MenuModel $model */ - $model = $this->getModel(); - - // Remove the items. - if (!$model->delete($cids)) - { - $this->setMessage($model->getError(), 'error'); - } - else - { - $this->setMessage(Text::plural('COM_MENUS_N_MENUS_DELETED', count($cids))); - } - } - } - - $this->setRedirect('index.php?option=com_menus&view=menus'); - } - - /** - * Temporary method. This should go into the 1.5 to 1.6 upgrade routines. - * - * @return void - * - * @since 1.6 - * - * @deprecated 5.0 Will be removed without replacement as it was only used for the 1.5 to 1.6 upgrade - */ - public function resync() - { - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $parts = null; - - try - { - $query->select( - [ - $db->quoteName('element'), - $db->quoteName('extension_id'), - ] - ) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('component')); - $db->setQuery($query); - - $components = $db->loadAssocList('element', 'extension_id'); - } - catch (\RuntimeException $e) - { - $this->setMessage($e->getMessage(), 'warning'); - - return; - } - - // Load all the component menu links - $query->select( - [ - $db->quoteName('id'), - $db->quoteName('link'), - $db->quoteName('component_id'), - ] - ) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('type') . ' = ' . $db->quote('component.item')); - $db->setQuery($query); - - try - { - $items = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $this->setMessage($e->getMessage(), 'warning'); - - return; - } - - $query = $db->getQuery(true) - ->update($db->quoteName('#__menu')) - ->set($db->quoteName('component_id') . ' = :componentId') - ->where($db->quoteName('id') . ' = :itemId') - ->bind(':componentId', $componentId, ParameterType::INTEGER) - ->bind(':itemId', $itemId, ParameterType::INTEGER); - - foreach ($items as $item) - { - // Parse the link. - parse_str(parse_url($item->link, PHP_URL_QUERY), $parts); - $itemId = $item->id; - - // Tease out the option. - if (isset($parts['option'])) - { - $option = $parts['option']; - - // Lookup the component ID - if (isset($components[$option])) - { - $componentId = $components[$option]; - } - else - { - // Mismatch. Needs human intervention. - $componentId = -1; - } - - // Check for mis-matched component ids in the menu link. - if ($item->component_id != $componentId) - { - // Update the menu table. - $log = "Link $item->id refers to $item->component_id, converting to $componentId ($item->link)"; - echo "
$log"; - - try - { - $db->setQuery($query)->execute(); - } - catch (\RuntimeException $e) - { - $this->setMessage($e->getMessage(), 'warning'); - - return; - } - } - } - } - } + /** + * Display the view + * + * @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()}. + * + * @return void + * + * @since 1.6 + */ + public function display($cachable = false, $urlparams = false) + { + } + + /** + * 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 object The model. + * + * @since 1.6 + */ + public function getModel($name = 'Menu', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Remove an item. + * + * @return void + * + * @since 1.6 + */ + public function delete() + { + // Check for request forgeries + $this->checkToken(); + + $user = $this->app->getIdentity(); + $cids = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $cids = array_filter($cids); + + if (empty($cids)) { + $this->setMessage(Text::_('COM_MENUS_NO_MENUS_SELECTED'), 'warning'); + } else { + // Access checks. + foreach ($cids as $i => $id) { + if (!$user->authorise('core.delete', 'com_menus.menu.' . (int) $id)) { + // Prune items that you can't change. + unset($cids[$i]); + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 'error'); + } + } + + if (count($cids) > 0) { + // Get the model. + /** @var \Joomla\Component\Menus\Administrator\Model\MenuModel $model */ + $model = $this->getModel(); + + // Remove the items. + if (!$model->delete($cids)) { + $this->setMessage($model->getError(), 'error'); + } else { + $this->setMessage(Text::plural('COM_MENUS_N_MENUS_DELETED', count($cids))); + } + } + } + + $this->setRedirect('index.php?option=com_menus&view=menus'); + } + + /** + * Temporary method. This should go into the 1.5 to 1.6 upgrade routines. + * + * @return void + * + * @since 1.6 + * + * @deprecated 5.0 Will be removed without replacement as it was only used for the 1.5 to 1.6 upgrade + */ + public function resync() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $parts = null; + + try { + $query->select( + [ + $db->quoteName('element'), + $db->quoteName('extension_id'), + ] + ) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('component')); + $db->setQuery($query); + + $components = $db->loadAssocList('element', 'extension_id'); + } catch (\RuntimeException $e) { + $this->setMessage($e->getMessage(), 'warning'); + + return; + } + + // Load all the component menu links + $query->select( + [ + $db->quoteName('id'), + $db->quoteName('link'), + $db->quoteName('component_id'), + ] + ) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('type') . ' = ' . $db->quote('component.item')); + $db->setQuery($query); + + try { + $items = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $this->setMessage($e->getMessage(), 'warning'); + + return; + } + + $query = $db->getQuery(true) + ->update($db->quoteName('#__menu')) + ->set($db->quoteName('component_id') . ' = :componentId') + ->where($db->quoteName('id') . ' = :itemId') + ->bind(':componentId', $componentId, ParameterType::INTEGER) + ->bind(':itemId', $itemId, ParameterType::INTEGER); + + foreach ($items as $item) { + // Parse the link. + parse_str(parse_url($item->link, PHP_URL_QUERY), $parts); + $itemId = $item->id; + + // Tease out the option. + if (isset($parts['option'])) { + $option = $parts['option']; + + // Lookup the component ID + if (isset($components[$option])) { + $componentId = $components[$option]; + } else { + // Mismatch. Needs human intervention. + $componentId = -1; + } + + // Check for mis-matched component ids in the menu link. + if ($item->component_id != $componentId) { + // Update the menu table. + $log = "Link $item->id refers to $item->component_id, converting to $componentId ($item->link)"; + echo "
$log"; + + try { + $db->setQuery($query)->execute(); + } catch (\RuntimeException $e) { + $this->setMessage($e->getMessage(), 'warning'); + + return; + } + } + } + } + } } diff --git a/administrator/components/com_menus/src/Extension/MenusComponent.php b/administrator/components/com_menus/src/Extension/MenusComponent.php index cc611ed3f26e3..f634cf07a6c3a 100644 --- a/administrator/components/com_menus/src/Extension/MenusComponent.php +++ b/administrator/components/com_menus/src/Extension/MenusComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('menus', new Menus); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('menus', new Menus()); + } } diff --git a/administrator/components/com_menus/src/Field/MenuItemByTypeField.php b/administrator/components/com_menus/src/Field/MenuItemByTypeField.php index 5df789ff8640e..f9f2b3f994cf7 100644 --- a/administrator/components/com_menus/src/Field/MenuItemByTypeField.php +++ b/administrator/components/com_menus/src/Field/MenuItemByTypeField.php @@ -1,4 +1,5 @@ $name; - } - - return parent::__get($name); - } - - /** - * Method to set certain otherwise inaccessible properties of the form field object. - * - * @param string $name The property name for which to set the value. - * @param mixed $value The value of the property. - * - * @return void - * - * @since 3.8.0 - */ - public function __set($name, $value) - { - switch ($name) - { - case 'menuType': - $this->menuType = (string) $value; - break; - - case 'clientId': - $this->clientId = (int) $value; - break; - - case 'language': - case 'published': - case 'disable': - $value = (string) $value; - $this->$name = $value ? explode(',', $value) : array(); - break; - - default: - parent::__set($name, $value); - } - } - - /** - * Method to attach a JForm object to the field. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @see \Joomla\CMS\Form\FormField::setup() - * @since 3.8.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $result = parent::setup($element, $value, $group); - - if ($result == true) - { - $menuType = (string) $this->element['menu_type']; - - if (!$menuType) - { - $app = Factory::getApplication(); - $currentMenuType = $app->getUserState('com_menus.items.menutype', ''); - $menuType = $app->input->getString('menutype', $currentMenuType); - } - - $this->menuType = $menuType; - $this->clientId = (int) $this->element['client_id']; - $this->published = $this->element['published'] ? explode(',', (string) $this->element['published']) : array(); - $this->disable = $this->element['disable'] ? explode(',', (string) $this->element['disable']) : array(); - $this->language = $this->element['language'] ? explode(',', (string) $this->element['language']) : array(); - } - - return $result; - } - - /** - * Method to get the field option groups. - * - * @return array The field option objects as a nested array in groups. - * - * @since 3.8.0 - */ - protected function getGroups() - { - $groups = array(); - - $menuType = $this->menuType; - - // Get the menu items. - $items = MenusHelper::getMenuLinks($menuType, 0, 0, $this->published, $this->language, $this->clientId); - - // Build group for a specific menu type. - if ($menuType) - { - // If the menutype is empty, group the items by menutype. - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__menu_types')) - ->where($db->quoteName('menutype') . ' = :menuType') - ->bind(':menuType', $menuType); - $db->setQuery($query); - - try - { - $menuTitle = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $menuTitle = $menuType; - } - - // Initialize the group. - $groups[$menuTitle] = array(); - - // Build the options array. - foreach ($items as $key => $link) - { - // Unset if item is menu_item_root - if ($link->text === 'Menu_Item_Root') - { - unset($items[$key]); - continue; - } - - $levelPrefix = str_repeat('- ', max(0, $link->level - 1)); - - // Displays language code if not set to All - if ($link->language !== '*') - { - $lang = ' (' . $link->language . ')'; - } - else - { - $lang = ''; - } - - $groups[$menuTitle][] = HTMLHelper::_('select.option', - $link->value, $levelPrefix . $link->text . $lang, - 'value', - 'text', - in_array($link->type, $this->disable) - ); - } - } - // Build groups for all menu types. - else - { - // Build the groups arrays. - foreach ($items as $menu) - { - // Initialize the group. - $groups[$menu->title] = array(); - - // Build the options array. - foreach ($menu->links as $link) - { - $levelPrefix = str_repeat('- ', max(0, $link->level - 1)); - - // Displays language code if not set to All - if ($link->language !== '*') - { - $lang = ' (' . $link->language . ')'; - } - else - { - $lang = ''; - } - - $groups[$menu->title][] = HTMLHelper::_('select.option', - $link->value, - $levelPrefix . $link->text . $lang, - 'value', - 'text', - in_array($link->type, $this->disable) - ); - } - } - } - - // Merge any additional groups in the XML definition. - $groups = array_merge(parent::getGroups(), $groups); - - return $groups; - } + /** + * The form field type. + * + * @var string + * @since 3.8.0 + */ + public $type = 'MenuItemByType'; + + /** + * The menu type. + * + * @var string + * @since 3.8.0 + */ + protected $menuType; + + /** + * The client id. + * + * @var string + * @since 3.8.0 + */ + protected $clientId; + + /** + * The language. + * + * @var array + * @since 3.8.0 + */ + protected $language; + + /** + * The published status. + * + * @var array + * @since 3.8.0 + */ + protected $published; + + /** + * The disabled status. + * + * @var array + * @since 3.8.0 + */ + protected $disable; + + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to get the value. + * + * @return mixed The property value or null. + * + * @since 3.8.0 + */ + public function __get($name) + { + switch ($name) { + case 'menuType': + case 'clientId': + case 'language': + case 'published': + case 'disable': + return $this->$name; + } + + return parent::__get($name); + } + + /** + * Method to set certain otherwise inaccessible properties of the form field object. + * + * @param string $name The property name for which to set the value. + * @param mixed $value The value of the property. + * + * @return void + * + * @since 3.8.0 + */ + public function __set($name, $value) + { + switch ($name) { + case 'menuType': + $this->menuType = (string) $value; + break; + + case 'clientId': + $this->clientId = (int) $value; + break; + + case 'language': + case 'published': + case 'disable': + $value = (string) $value; + $this->$name = $value ? explode(',', $value) : array(); + break; + + default: + parent::__set($name, $value); + } + } + + /** + * Method to attach a JForm object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @see \Joomla\CMS\Form\FormField::setup() + * @since 3.8.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $result = parent::setup($element, $value, $group); + + if ($result == true) { + $menuType = (string) $this->element['menu_type']; + + if (!$menuType) { + $app = Factory::getApplication(); + $currentMenuType = $app->getUserState('com_menus.items.menutype', ''); + $menuType = $app->input->getString('menutype', $currentMenuType); + } + + $this->menuType = $menuType; + $this->clientId = (int) $this->element['client_id']; + $this->published = $this->element['published'] ? explode(',', (string) $this->element['published']) : array(); + $this->disable = $this->element['disable'] ? explode(',', (string) $this->element['disable']) : array(); + $this->language = $this->element['language'] ? explode(',', (string) $this->element['language']) : array(); + } + + return $result; + } + + /** + * Method to get the field option groups. + * + * @return array The field option objects as a nested array in groups. + * + * @since 3.8.0 + */ + protected function getGroups() + { + $groups = array(); + + $menuType = $this->menuType; + + // Get the menu items. + $items = MenusHelper::getMenuLinks($menuType, 0, 0, $this->published, $this->language, $this->clientId); + + // Build group for a specific menu type. + if ($menuType) { + // If the menutype is empty, group the items by menutype. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__menu_types')) + ->where($db->quoteName('menutype') . ' = :menuType') + ->bind(':menuType', $menuType); + $db->setQuery($query); + + try { + $menuTitle = $db->loadResult(); + } catch (\RuntimeException $e) { + $menuTitle = $menuType; + } + + // Initialize the group. + $groups[$menuTitle] = array(); + + // Build the options array. + foreach ($items as $key => $link) { + // Unset if item is menu_item_root + if ($link->text === 'Menu_Item_Root') { + unset($items[$key]); + continue; + } + + $levelPrefix = str_repeat('- ', max(0, $link->level - 1)); + + // Displays language code if not set to All + if ($link->language !== '*') { + $lang = ' (' . $link->language . ')'; + } else { + $lang = ''; + } + + $groups[$menuTitle][] = HTMLHelper::_( + 'select.option', + $link->value, + $levelPrefix . $link->text . $lang, + 'value', + 'text', + in_array($link->type, $this->disable) + ); + } + } else { + // Build groups for all menu types. + // Build the groups arrays. + foreach ($items as $menu) { + // Initialize the group. + $groups[$menu->title] = array(); + + // Build the options array. + foreach ($menu->links as $link) { + $levelPrefix = str_repeat('- ', max(0, $link->level - 1)); + + // Displays language code if not set to All + if ($link->language !== '*') { + $lang = ' (' . $link->language . ')'; + } else { + $lang = ''; + } + + $groups[$menu->title][] = HTMLHelper::_( + 'select.option', + $link->value, + $levelPrefix . $link->text . $lang, + 'value', + 'text', + in_array($link->type, $this->disable) + ); + } + } + } + + // Merge any additional groups in the XML definition. + $groups = array_merge(parent::getGroups(), $groups); + + return $groups; + } } diff --git a/administrator/components/com_menus/src/Field/MenuOrderingField.php b/administrator/components/com_menus/src/Field/MenuOrderingField.php index b0c89afd2bd5c..a2106cbbc45a5 100644 --- a/administrator/components/com_menus/src/Field/MenuOrderingField.php +++ b/administrator/components/com_menus/src/Field/MenuOrderingField.php @@ -1,4 +1,5 @@ form->getValue('parent_id', 0); - - if (!$parent_id) - { - return false; - } - - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('a.id', 'value'), - $db->quoteName('a.title', 'text'), - $db->quoteName('a.client_id', 'clientId'), - ] - ) - ->from($db->quoteName('#__menu', 'a')) - - ->where($db->quoteName('a.published') . ' >= 0') - ->where($db->quoteName('a.parent_id') . ' = :parentId') - ->bind(':parentId', $parent_id, ParameterType::INTEGER); - - if ($menuType = $this->form->getValue('menutype')) - { - $query->where($db->quoteName('a.menutype') . ' = :menuType') - ->bind(':menuType', $menuType); - } - else - { - $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('')); - } - - $query->order($db->quoteName('a.lft') . ' ASC'); - - // Get the options. - $db->setQuery($query); - - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - // Allow translation of custom admin menus - foreach ($options as &$option) - { - if ($option->clientId != 0) - { - $option->text = Text::_($option->text); - } - } - - $options = array_merge( - array(array('value' => '-1', 'text' => Text::_('COM_MENUS_ITEM_FIELD_ORDERING_VALUE_FIRST'))), - $options, - array(array('value' => '-2', 'text' => Text::_('COM_MENUS_ITEM_FIELD_ORDERING_VALUE_LAST'))) - ); - - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $options); - - return $options; - } - - /** - * Method to get the field input markup. - * - * @return string The field input markup. - * - * @since 1.7 - */ - protected function getInput() - { - if ($this->form->getValue('id', 0) == 0) - { - return '' . Text::_('COM_MENUS_ITEM_FIELD_ORDERING_TEXT') . ''; - } - else - { - return parent::getInput(); - } - } + /** + * The form field type. + * + * @var string + * @since 1.7 + */ + protected $type = 'MenuOrdering'; + + /** + * Method to get the list of siblings in a menu. + * The method requires that parent be set. + * + * @return array|boolean The field option objects or false if the parent field has not been set + * + * @since 1.7 + */ + protected function getOptions() + { + $options = array(); + + // Get the parent + $parent_id = (int) $this->form->getValue('parent_id', 0); + + if (!$parent_id) { + return false; + } + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('a.id', 'value'), + $db->quoteName('a.title', 'text'), + $db->quoteName('a.client_id', 'clientId'), + ] + ) + ->from($db->quoteName('#__menu', 'a')) + + ->where($db->quoteName('a.published') . ' >= 0') + ->where($db->quoteName('a.parent_id') . ' = :parentId') + ->bind(':parentId', $parent_id, ParameterType::INTEGER); + + if ($menuType = $this->form->getValue('menutype')) { + $query->where($db->quoteName('a.menutype') . ' = :menuType') + ->bind(':menuType', $menuType); + } else { + $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('')); + } + + $query->order($db->quoteName('a.lft') . ' ASC'); + + // Get the options. + $db->setQuery($query); + + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + // Allow translation of custom admin menus + foreach ($options as &$option) { + if ($option->clientId != 0) { + $option->text = Text::_($option->text); + } + } + + $options = array_merge( + array(array('value' => '-1', 'text' => Text::_('COM_MENUS_ITEM_FIELD_ORDERING_VALUE_FIRST'))), + $options, + array(array('value' => '-2', 'text' => Text::_('COM_MENUS_ITEM_FIELD_ORDERING_VALUE_LAST'))) + ); + + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $options); + + return $options; + } + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.7 + */ + protected function getInput() + { + if ($this->form->getValue('id', 0) == 0) { + return '' . Text::_('COM_MENUS_ITEM_FIELD_ORDERING_TEXT') . ''; + } else { + return parent::getInput(); + } + } } diff --git a/administrator/components/com_menus/src/Field/MenuParentField.php b/administrator/components/com_menus/src/Field/MenuParentField.php index 1eaa109792acd..24e2002c2087e 100644 --- a/administrator/components/com_menus/src/Field/MenuParentField.php +++ b/administrator/components/com_menus/src/Field/MenuParentField.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select( - [ - 'DISTINCT ' . $db->quoteName('a.id', 'value'), - $db->quoteName('a.title', 'text'), - $db->quoteName('a.level'), - $db->quoteName('a.lft'), - ] - ) - ->from($db->quoteName('#__menu', 'a')); - - // Filter by menu type. - if ($menuType = $this->form->getValue('menutype')) - { - $query->where($db->quoteName('a.menutype') . ' = :menuType') - ->bind(':menuType', $menuType); - } - else - { - // Skip special menu types - $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('')); - $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('main')); - } - - // Filter by client id. - $clientId = $this->getAttribute('clientid'); - - if (!is_null($clientId)) - { - $clientId = (int) $clientId; - $query->where($db->quoteName('a.client_id') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - } - - // Prevent parenting to children of this item. - if ($id = (int) $this->form->getValue('id')) - { - $query->join('LEFT', $db->quoteName('#__menu', 'p'), $db->quoteName('p.id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER) - ->where( - 'NOT(' . $db->quoteName('a.lft') . ' >= ' . $db->quoteName('p.lft') - . ' AND ' . $db->quoteName('a.rgt') . ' <= ' . $db->quoteName('p.rgt') . ')' - ); - } - - $query->where($db->quoteName('a.published') . ' != -2') - ->order($db->quoteName('a.lft') . ' ASC'); - - // Get the options. - $db->setQuery($query); - - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - // Pad the option text with spaces using depth level as a multiplier. - for ($i = 0, $n = count($options); $i < $n; $i++) - { - if ($clientId != 0) - { - // Allow translation of custom admin menus - $options[$i]->text = str_repeat('- ', $options[$i]->level) . Text::_($options[$i]->text); - } - else - { - $options[$i]->text = str_repeat('- ', $options[$i]->level) . $options[$i]->text; - } - } - - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $options); - - return $options; - } + /** + * The form field type. + * + * @var string + * @since 1.6 + */ + protected $type = 'MenuParent'; + + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 1.6 + */ + protected function getOptions() + { + $options = array(); + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + 'DISTINCT ' . $db->quoteName('a.id', 'value'), + $db->quoteName('a.title', 'text'), + $db->quoteName('a.level'), + $db->quoteName('a.lft'), + ] + ) + ->from($db->quoteName('#__menu', 'a')); + + // Filter by menu type. + if ($menuType = $this->form->getValue('menutype')) { + $query->where($db->quoteName('a.menutype') . ' = :menuType') + ->bind(':menuType', $menuType); + } else { + // Skip special menu types + $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('')); + $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('main')); + } + + // Filter by client id. + $clientId = $this->getAttribute('clientid'); + + if (!is_null($clientId)) { + $clientId = (int) $clientId; + $query->where($db->quoteName('a.client_id') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + } + + // Prevent parenting to children of this item. + if ($id = (int) $this->form->getValue('id')) { + $query->join('LEFT', $db->quoteName('#__menu', 'p'), $db->quoteName('p.id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER) + ->where( + 'NOT(' . $db->quoteName('a.lft') . ' >= ' . $db->quoteName('p.lft') + . ' AND ' . $db->quoteName('a.rgt') . ' <= ' . $db->quoteName('p.rgt') . ')' + ); + } + + $query->where($db->quoteName('a.published') . ' != -2') + ->order($db->quoteName('a.lft') . ' ASC'); + + // Get the options. + $db->setQuery($query); + + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + // Pad the option text with spaces using depth level as a multiplier. + for ($i = 0, $n = count($options); $i < $n; $i++) { + if ($clientId != 0) { + // Allow translation of custom admin menus + $options[$i]->text = str_repeat('- ', $options[$i]->level) . Text::_($options[$i]->text); + } else { + $options[$i]->text = str_repeat('- ', $options[$i]->level) . $options[$i]->text; + } + } + + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $options); + + return $options; + } } diff --git a/administrator/components/com_menus/src/Field/MenuPresetField.php b/administrator/components/com_menus/src/Field/MenuPresetField.php index 288da3c3185d9..049eb3278feae 100644 --- a/administrator/components/com_menus/src/Field/MenuPresetField.php +++ b/administrator/components/com_menus/src/Field/MenuPresetField.php @@ -1,4 +1,5 @@ name, Text::_($preset->title)); - } + foreach ($presets as $preset) { + $options[] = HTMLHelper::_('select.option', $preset->name, Text::_($preset->title)); + } - return array_merge(parent::getOptions(), $options); - } + return array_merge(parent::getOptions(), $options); + } } diff --git a/administrator/components/com_menus/src/Field/MenutypeField.php b/administrator/components/com_menus/src/Field/MenutypeField.php index c784466371fe4..1edebcb4032dc 100644 --- a/administrator/components/com_menus/src/Field/MenutypeField.php +++ b/administrator/components/com_menus/src/Field/MenutypeField.php @@ -1,4 +1,5 @@ form->getValue('id'); - $size = (string) ($v = $this->element['size']) ? ' size="' . $v . '"' : ''; - $class = (string) ($v = $this->element['class']) ? ' class="form-control ' . $v . '"' : ' class="form-control"'; - $required = (string) $this->element['required'] ? ' required="required"' : ''; - $clientId = (int) $this->element['clientid'] ?: 0; - - // Get a reverse lookup of the base link URL to Title - switch ($this->value) - { - case 'url': - $value = Text::_('COM_MENUS_TYPE_EXTERNAL_URL'); - break; - - case 'alias': - $value = Text::_('COM_MENUS_TYPE_ALIAS'); - break; - - case 'separator': - $value = Text::_('COM_MENUS_TYPE_SEPARATOR'); - break; - - case 'heading': - $value = Text::_('COM_MENUS_TYPE_HEADING'); - break; - - case 'container': - $value = Text::_('COM_MENUS_TYPE_CONTAINER'); - break; - - default: - $link = $this->form->getValue('link'); - $value = ''; - - if ($link !== null) - { - $model = Factory::getApplication()->bootComponent('com_menus') - ->getMVCFactory()->createModel('Menutypes', 'Administrator', array('ignore_request' => true)); - $model->setState('client_id', $clientId); - - $rlu = $model->getReverseLookup(); - - // Clean the link back to the option, view and layout - $value = Text::_(ArrayHelper::getValue($rlu, MenusHelper::getLinkKey($link))); - } - break; - } - - $link = Route::_('index.php?option=com_menus&view=menutypes&tmpl=component&client_id=' . $clientId . '&recordId=' . $recordId); - $html[] = ''; - $html[] = ''; - $html[] = HTMLHelper::_( - 'bootstrap.renderModal', - 'menuTypeModal', - array( - 'url' => $link, - 'title' => Text::_('COM_MENUS_ITEM_FIELD_TYPE_LABEL'), - 'width' => '800px', - 'height' => '300px', - 'modalWidth' => 80, - 'bodyHeight' => 70, - 'footer' => '' - ) - ); - - // This hidden field has an ID so it can be used for showon attributes - $html[] = ''; - - return implode("\n", $html); - } + /** + * The form field type. + * + * @var string + * @since 1.6 + */ + protected $type = 'menutype'; + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + $html = array(); + $recordId = (int) $this->form->getValue('id'); + $size = (string) ($v = $this->element['size']) ? ' size="' . $v . '"' : ''; + $class = (string) ($v = $this->element['class']) ? ' class="form-control ' . $v . '"' : ' class="form-control"'; + $required = (string) $this->element['required'] ? ' required="required"' : ''; + $clientId = (int) $this->element['clientid'] ?: 0; + + // Get a reverse lookup of the base link URL to Title + switch ($this->value) { + case 'url': + $value = Text::_('COM_MENUS_TYPE_EXTERNAL_URL'); + break; + + case 'alias': + $value = Text::_('COM_MENUS_TYPE_ALIAS'); + break; + + case 'separator': + $value = Text::_('COM_MENUS_TYPE_SEPARATOR'); + break; + + case 'heading': + $value = Text::_('COM_MENUS_TYPE_HEADING'); + break; + + case 'container': + $value = Text::_('COM_MENUS_TYPE_CONTAINER'); + break; + + default: + $link = $this->form->getValue('link'); + $value = ''; + + if ($link !== null) { + $model = Factory::getApplication()->bootComponent('com_menus') + ->getMVCFactory()->createModel('Menutypes', 'Administrator', array('ignore_request' => true)); + $model->setState('client_id', $clientId); + + $rlu = $model->getReverseLookup(); + + // Clean the link back to the option, view and layout + $value = Text::_(ArrayHelper::getValue($rlu, MenusHelper::getLinkKey($link))); + } + break; + } + + $link = Route::_('index.php?option=com_menus&view=menutypes&tmpl=component&client_id=' . $clientId . '&recordId=' . $recordId); + $html[] = ''; + $html[] = ''; + $html[] = HTMLHelper::_( + 'bootstrap.renderModal', + 'menuTypeModal', + array( + 'url' => $link, + 'title' => Text::_('COM_MENUS_ITEM_FIELD_TYPE_LABEL'), + 'width' => '800px', + 'height' => '300px', + 'modalWidth' => 80, + 'bodyHeight' => 70, + 'footer' => '' + ) + ); + + // This hidden field has an ID so it can be used for showon attributes + $html[] = ''; + + return implode("\n", $html); + } } diff --git a/administrator/components/com_menus/src/Field/Modal/MenuField.php b/administrator/components/com_menus/src/Field/Modal/MenuField.php index 092c1cc3b3529..48c8440101865 100644 --- a/administrator/components/com_menus/src/Field/Modal/MenuField.php +++ b/administrator/components/com_menus/src/Field/Modal/MenuField.php @@ -1,4 +1,5 @@ $name; - } - - return parent::__get($name); - } - - /** - * Method to set certain otherwise inaccessible properties of the form field object. - * - * @param string $name The property name for which to set the value. - * @param mixed $value The value of the property. - * - * @return void - * - * @since 3.7.0 - */ - public function __set($name, $value) - { - switch ($name) - { - case 'allowSelect': - case 'allowClear': - case 'allowNew': - case 'allowEdit': - case 'allowPropagate': - $value = (string) $value; - $this->$name = !($value === 'false' || $value === 'off' || $value === '0'); - break; - - default: - parent::__set($name, $value); - } - } - - /** - * Method to attach a JForm object to the field. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @see FormField::setup() - * @since 3.7.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $return = parent::setup($element, $value, $group); - - if ($return) - { - $this->allowSelect = ((string) $this->element['select']) !== 'false'; - $this->allowClear = ((string) $this->element['clear']) !== 'false'; - $this->allowPropagate = ((string) $this->element['propagate']) === 'true'; - - // Creating/editing menu items is not supported in frontend. - $isAdministrator = Factory::getApplication()->isClient('administrator'); - $this->allowNew = $isAdministrator ? ((string) $this->element['new']) === 'true' : false; - $this->allowEdit = $isAdministrator ? ((string) $this->element['edit']) === 'true' : false; - } - - return $return; - } - - /** - * Method to get the field input markup. - * - * @return string The field input markup. - * - * @since 3.7.0 - */ - protected function getInput() - { - $clientId = (int) $this->element['clientid']; - $languages = LanguageHelper::getContentLanguages(array(0, 1), false); - - // Load language - Factory::getLanguage()->load('com_menus', JPATH_ADMINISTRATOR); - - // The active article id field. - $value = (int) $this->value ?: ''; - - // Create the modal id. - $modalId = 'Item_' . $this->id; - - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); - - // Add the modal field script to the document head. - $wa->useScript('field.modal-fields'); - - // Script to proxy the select modal function to the modal-fields.js file. - if ($this->allowSelect) - { - static $scriptSelect = null; - - if (is_null($scriptSelect)) - { - $scriptSelect = array(); - } - - if (!isset($scriptSelect[$this->id])) - { - $wa->addInlineScript(" + /** + * The form field type. + * + * @var string + * @since 3.7.0 + */ + protected $type = 'Modal_Menu'; + + /** + * Determinate, if the select button is shown + * + * @var boolean + * @since 3.7.0 + */ + protected $allowSelect = true; + + /** + * Determinate, if the clear button is shown + * + * @var boolean + * @since 3.7.0 + */ + protected $allowClear = true; + + /** + * Determinate, if the create button is shown + * + * @var boolean + * @since 3.7.0 + */ + protected $allowNew = false; + + /** + * Determinate, if the edit button is shown + * + * @var boolean + * @since 3.7.0 + */ + protected $allowEdit = false; + + /** + * Determinate, if the propagate button is shown + * + * @var boolean + * @since 3.9.0 + */ + protected $allowPropagate = false; + + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to get the value. + * + * @return mixed The property value or null. + * + * @since 3.7.0 + */ + public function __get($name) + { + switch ($name) { + case 'allowSelect': + case 'allowClear': + case 'allowNew': + case 'allowEdit': + case 'allowPropagate': + return $this->$name; + } + + return parent::__get($name); + } + + /** + * Method to set certain otherwise inaccessible properties of the form field object. + * + * @param string $name The property name for which to set the value. + * @param mixed $value The value of the property. + * + * @return void + * + * @since 3.7.0 + */ + public function __set($name, $value) + { + switch ($name) { + case 'allowSelect': + case 'allowClear': + case 'allowNew': + case 'allowEdit': + case 'allowPropagate': + $value = (string) $value; + $this->$name = !($value === 'false' || $value === 'off' || $value === '0'); + break; + + default: + parent::__set($name, $value); + } + } + + /** + * Method to attach a JForm object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @see FormField::setup() + * @since 3.7.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $return = parent::setup($element, $value, $group); + + if ($return) { + $this->allowSelect = ((string) $this->element['select']) !== 'false'; + $this->allowClear = ((string) $this->element['clear']) !== 'false'; + $this->allowPropagate = ((string) $this->element['propagate']) === 'true'; + + // Creating/editing menu items is not supported in frontend. + $isAdministrator = Factory::getApplication()->isClient('administrator'); + $this->allowNew = $isAdministrator ? ((string) $this->element['new']) === 'true' : false; + $this->allowEdit = $isAdministrator ? ((string) $this->element['edit']) === 'true' : false; + } + + return $return; + } + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 3.7.0 + */ + protected function getInput() + { + $clientId = (int) $this->element['clientid']; + $languages = LanguageHelper::getContentLanguages(array(0, 1), false); + + // Load language + Factory::getLanguage()->load('com_menus', JPATH_ADMINISTRATOR); + + // The active article id field. + $value = (int) $this->value ?: ''; + + // Create the modal id. + $modalId = 'Item_' . $this->id; + + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + + // Add the modal field script to the document head. + $wa->useScript('field.modal-fields'); + + // Script to proxy the select modal function to the modal-fields.js file. + if ($this->allowSelect) { + static $scriptSelect = null; + + if (is_null($scriptSelect)) { + $scriptSelect = array(); + } + + if (!isset($scriptSelect[$this->id])) { + $wa->addInlineScript( + " window.jSelectMenu_" . $this->id . " = function (id, title, object) { window.processModalSelect('Item', '" . $this->id . "', id, title, '', object); }", - [], - ['type' => 'module'] - ); - - Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); - - $scriptSelect[$this->id] = true; - } - } - - // Setup variables for display. - $linkSuffix = '&layout=modal&client_id=' . $clientId . '&tmpl=component&' . Session::getFormToken() . '=1'; - $linkItems = 'index.php?option=com_menus&view=items' . $linkSuffix; - $linkItem = 'index.php?option=com_menus&view=item' . $linkSuffix; - $modalTitle = Text::_('COM_MENUS_SELECT_A_MENUITEM'); - - if (isset($this->element['language'])) - { - $linkItems .= '&forcedLanguage=' . $this->element['language']; - $linkItem .= '&forcedLanguage=' . $this->element['language']; - $modalTitle .= ' — ' . $this->element['label']; - } - - $urlSelect = $linkItems . '&function=jSelectMenu_' . $this->id; - $urlEdit = $linkItem . '&task=item.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; - $urlNew = $linkItem . '&task=item.add'; - - if ($value) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $value, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $title = $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - - // Placeholder if option is present or not - if (empty($title)) - { - if ($this->element->option && (string) $this->element->option['value'] == '') - { - $title_holder = Text::_($this->element->option); - } - else - { - $title_holder = Text::_('COM_MENUS_SELECT_A_MENUITEM'); - } - } - - $title = empty($title) ? $title_holder : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); - - // The current menu item display field. - $html = ''; - - if ($this->allowSelect || $this->allowNew || $this->allowEdit || $this->allowClear) - { - $html .= ''; - } - - $html .= ''; - - // Select menu item button - if ($this->allowSelect) - { - $html .= '' - . ' ' . Text::_('JSELECT') - . ''; - } - - // New menu item button - if ($this->allowNew) - { - $html .= '' - . ' ' . Text::_('JACTION_CREATE') - . ''; - } - - // Edit menu item button - if ($this->allowEdit) - { - $html .= '' - . ' ' . Text::_('JACTION_EDIT') - . ''; - } - - // Clear menu item button - if ($this->allowClear) - { - $html .= '' - . ' ' . Text::_('JCLEAR') - . ''; - } - - // Propagate menu item button - if ($this->allowPropagate && count($languages) > 2) - { - // Strip off language tag at the end - $tagLength = (int) strlen($this->element['language']); - $callbackFunctionStem = substr("jSelectMenu_" . $this->id, 0, -$tagLength); - - $html .= '' - . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') - . ''; - } - - if ($this->allowSelect || $this->allowNew || $this->allowEdit || $this->allowClear) - { - $html .= ''; - } - - // Select menu item modal - if ($this->allowSelect) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalSelect' . $modalId, - array( - 'title' => $modalTitle, - 'url' => $urlSelect, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ) - ); - } - - // New menu item modal - if ($this->allowNew) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalNew' . $modalId, - array( - 'title' => Text::_('COM_MENUS_NEW_MENUITEM'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlNew, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Edit menu item modal - if ($this->allowEdit) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalEdit' . $modalId, - array( - 'title' => Text::_('COM_MENUS_EDIT_MENUITEM'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlEdit, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Note: class='required' for client side validation. - $class = $this->required ? ' class="required modal-value"' : ''; - - // Placeholder if option is present or not when clearing field - if ($this->element->option && (string) $this->element->option['value'] == '') - { - $title_holder = Text::_($this->element->option); - } - else - { - $title_holder = Text::_('COM_MENUS_SELECT_A_MENUITEM'); - } - - $html .= ''; - - return $html; - } - - /** - * Method to get the field label markup. - * - * @return string The field label markup. - * - * @since 3.7.0 - */ - protected function getLabel() - { - return str_replace($this->id, $this->id . '_name', parent::getLabel()); - } + [], + ['type' => 'module'] + ); + + Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); + + $scriptSelect[$this->id] = true; + } + } + + // Setup variables for display. + $linkSuffix = '&layout=modal&client_id=' . $clientId . '&tmpl=component&' . Session::getFormToken() . '=1'; + $linkItems = 'index.php?option=com_menus&view=items' . $linkSuffix; + $linkItem = 'index.php?option=com_menus&view=item' . $linkSuffix; + $modalTitle = Text::_('COM_MENUS_SELECT_A_MENUITEM'); + + if (isset($this->element['language'])) { + $linkItems .= '&forcedLanguage=' . $this->element['language']; + $linkItem .= '&forcedLanguage=' . $this->element['language']; + $modalTitle .= ' — ' . $this->element['label']; + } + + $urlSelect = $linkItems . '&function=jSelectMenu_' . $this->id; + $urlEdit = $linkItem . '&task=item.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; + $urlNew = $linkItem . '&task=item.add'; + + if ($value) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $value, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $title = $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + + // Placeholder if option is present or not + if (empty($title)) { + if ($this->element->option && (string) $this->element->option['value'] == '') { + $title_holder = Text::_($this->element->option); + } else { + $title_holder = Text::_('COM_MENUS_SELECT_A_MENUITEM'); + } + } + + $title = empty($title) ? $title_holder : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); + + // The current menu item display field. + $html = ''; + + if ($this->allowSelect || $this->allowNew || $this->allowEdit || $this->allowClear) { + $html .= ''; + } + + $html .= ''; + + // Select menu item button + if ($this->allowSelect) { + $html .= '' + . ' ' . Text::_('JSELECT') + . ''; + } + + // New menu item button + if ($this->allowNew) { + $html .= '' + . ' ' . Text::_('JACTION_CREATE') + . ''; + } + + // Edit menu item button + if ($this->allowEdit) { + $html .= '' + . ' ' . Text::_('JACTION_EDIT') + . ''; + } + + // Clear menu item button + if ($this->allowClear) { + $html .= '' + . ' ' . Text::_('JCLEAR') + . ''; + } + + // Propagate menu item button + if ($this->allowPropagate && count($languages) > 2) { + // Strip off language tag at the end + $tagLength = (int) strlen($this->element['language']); + $callbackFunctionStem = substr("jSelectMenu_" . $this->id, 0, -$tagLength); + + $html .= '' + . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') + . ''; + } + + if ($this->allowSelect || $this->allowNew || $this->allowEdit || $this->allowClear) { + $html .= ''; + } + + // Select menu item modal + if ($this->allowSelect) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalSelect' . $modalId, + array( + 'title' => $modalTitle, + 'url' => $urlSelect, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ) + ); + } + + // New menu item modal + if ($this->allowNew) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalNew' . $modalId, + array( + 'title' => Text::_('COM_MENUS_NEW_MENUITEM'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlNew, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Edit menu item modal + if ($this->allowEdit) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalEdit' . $modalId, + array( + 'title' => Text::_('COM_MENUS_EDIT_MENUITEM'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlEdit, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Note: class='required' for client side validation. + $class = $this->required ? ' class="required modal-value"' : ''; + + // Placeholder if option is present or not when clearing field + if ($this->element->option && (string) $this->element->option['value'] == '') { + $title_holder = Text::_($this->element->option); + } else { + $title_holder = Text::_('COM_MENUS_SELECT_A_MENUITEM'); + } + + $html .= ''; + + return $html; + } + + /** + * Method to get the field label markup. + * + * @return string The field label markup. + * + * @since 3.7.0 + */ + protected function getLabel() + { + return str_replace($this->id, $this->id . '_name', parent::getLabel()); + } } diff --git a/administrator/components/com_menus/src/Helper/AssociationsHelper.php b/administrator/components/com_menus/src/Helper/AssociationsHelper.php index 14f0c37dddcee..d22b6cd8b8301 100644 --- a/administrator/components/com_menus/src/Helper/AssociationsHelper.php +++ b/administrator/components/com_menus/src/Helper/AssociationsHelper.php @@ -1,4 +1,5 @@ getType($typeName); - - $context = $this->extension . '.item'; - - // Get the associations. - $associations = Associations::getAssociations( - $this->extension, - $type['tables']['a'], - $context, - $id, - 'id', - 'alias', - '' - ); - - return $associations; - } - - /** - * Get item information - * - * @param string $typeName The item type - * @param int $id The id of item for which we need the associated items - * - * @return Table|null - * - * @since 3.7.0 - */ - public function getItem($typeName, $id) - { - if (empty($id)) - { - return null; - } - - $table = null; - - switch ($typeName) - { - case 'item': - $table = Table::getInstance('menu'); - break; - } - - if (is_null($table)) - { - return null; - } - - $table->load($id); - - return $table; - } - - /** - * Get information about the type - * - * @param string $typeName The item type - * - * @return array Array of item types - * - * @since 3.7.0 - */ - public function getType($typeName = '') - { - $fields = $this->getFieldsTemplate(); - $tables = array(); - $joins = array(); - $support = $this->getSupportTemplate(); - $title = ''; - - if (in_array($typeName, $this->itemTypes)) - { - switch ($typeName) - { - case 'item': - $fields['ordering'] = 'a.lft'; - $fields['level'] = 'a.level'; - $fields['catid'] = ''; - $fields['state'] = 'a.published'; - $fields['created_user_id'] = ''; - $fields['menutype'] = 'a.menutype'; - - $support['state'] = true; - $support['acl'] = true; - $support['checkout'] = true; - $support['level'] = true; - - $tables = array( - 'a' => '#__menu' - ); - - $title = 'menu'; - break; - } - } - - return array( - 'fields' => $fields, - 'support' => $support, - 'tables' => $tables, - 'joins' => $joins, - 'title' => $title - ); - } + /** + * The extension name + * + * @var array $extension + * + * @since 3.7.0 + */ + protected $extension = 'com_menus'; + + /** + * Array of item types + * + * @var array $itemTypes + * + * @since 3.7.0 + */ + protected $itemTypes = array('item'); + + /** + * Has the extension association support + * + * @var boolean $associationsSupport + * + * @since 3.7.0 + */ + protected $associationsSupport = true; + + /** + * Method to get the associations for a given item. + * + * @param integer $id Id of the item + * @param string $view Name of the view + * + * @return array Array of associations for the item + * + * @since 4.0.0 + */ + public function getAssociationsForItem($id = 0, $view = null) + { + return []; + } + + /** + * Get the associated items for an item + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return array + * + * @since 3.7.0 + */ + public function getAssociations($typeName, $id) + { + $type = $this->getType($typeName); + + $context = $this->extension . '.item'; + + // Get the associations. + $associations = Associations::getAssociations( + $this->extension, + $type['tables']['a'], + $context, + $id, + 'id', + 'alias', + '' + ); + + return $associations; + } + + /** + * Get item information + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return Table|null + * + * @since 3.7.0 + */ + public function getItem($typeName, $id) + { + if (empty($id)) { + return null; + } + + $table = null; + + switch ($typeName) { + case 'item': + $table = Table::getInstance('menu'); + break; + } + + if (is_null($table)) { + return null; + } + + $table->load($id); + + return $table; + } + + /** + * Get information about the type + * + * @param string $typeName The item type + * + * @return array Array of item types + * + * @since 3.7.0 + */ + public function getType($typeName = '') + { + $fields = $this->getFieldsTemplate(); + $tables = array(); + $joins = array(); + $support = $this->getSupportTemplate(); + $title = ''; + + if (in_array($typeName, $this->itemTypes)) { + switch ($typeName) { + case 'item': + $fields['ordering'] = 'a.lft'; + $fields['level'] = 'a.level'; + $fields['catid'] = ''; + $fields['state'] = 'a.published'; + $fields['created_user_id'] = ''; + $fields['menutype'] = 'a.menutype'; + + $support['state'] = true; + $support['acl'] = true; + $support['checkout'] = true; + $support['level'] = true; + + $tables = array( + 'a' => '#__menu' + ); + + $title = 'menu'; + break; + } + } + + return array( + 'fields' => $fields, + 'support' => $support, + 'tables' => $tables, + 'joins' => $joins, + 'title' => $title + ); + } } diff --git a/administrator/components/com_menus/src/Helper/MenusHelper.php b/administrator/components/com_menus/src/Helper/MenusHelper.php index 7d515381fd32d..bd582150b312b 100644 --- a/administrator/components/com_menus/src/Helper/MenusHelper.php +++ b/administrator/components/com_menus/src/Helper/MenusHelper.php @@ -1,4 +1,5 @@ $value) - { - if ((!in_array($name, self::$_filter)) && (!($name == 'task' && !array_key_exists('view', $request)))) - { - // Remove the variables we want to ignore. - unset($request[$name]); - } - } - - ksort($request); - - return 'index.php?' . http_build_query($request, '', '&'); - } - - /** - * Get the menu list for create a menu module - * - * @param int $clientId Optional client id - viz 0 = site, 1 = administrator, can be NULL for all - * - * @return array The menu array list - * - * @since 1.6 - */ - public static function getMenuTypes($clientId = 0) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('a.menutype')) - ->from($db->quoteName('#__menu_types', 'a')); - - if (isset($clientId)) - { - $clientId = (int) $clientId; - $query->where($db->quoteName('a.client_id') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - } - - $db->setQuery($query); - - return $db->loadColumn(); - } - - /** - * Get a list of menu links for one or all menus. - * - * @param string $menuType An option menu to filter the list on, otherwise all menu with given client id links - * are returned as a grouped array. - * @param integer $parentId An optional parent ID to pivot results around. - * @param integer $mode An optional mode. If parent ID is set and mode=2, the parent and children are excluded from the list. - * @param array $published An optional array of states - * @param array $languages Optional array of specify which languages we want to filter - * @param int $clientId Optional client id - viz 0 = site, 1 = administrator, can be NULL for all (used only if menutype not given) - * - * @return array|boolean - * - * @since 1.6 - */ - public static function getMenuLinks($menuType = null, $parentId = 0, $mode = 0, $published = array(), $languages = array(), $clientId = 0) - { - $hasClientId = $clientId !== null; - $clientId = (int) $clientId; - - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select( - [ - 'DISTINCT ' . $db->quoteName('a.id', 'value'), - $db->quoteName('a.title', 'text'), - $db->quoteName('a.alias'), - $db->quoteName('a.level'), - $db->quoteName('a.menutype'), - $db->quoteName('a.client_id'), - $db->quoteName('a.type'), - $db->quoteName('a.published'), - $db->quoteName('a.template_style_id'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.language'), - $db->quoteName('a.lft'), - $db->quoteName('e.name', 'componentname'), - $db->quoteName('e.element'), - ] - ) - ->from($db->quoteName('#__menu', 'a')) - ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('a.component_id')); - - if (Multilanguage::isEnabled()) - { - $query->select( - [ - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image', 'language_image'), - $db->quoteName('l.sef', 'language_sef'), - ] - ) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); - } - - // Filter by the type if given, this is more specific than client id - if ($menuType) - { - $query->where('(' . $db->quoteName('a.menutype') . ' = :menuType OR ' . $db->quoteName('a.parent_id') . ' = 0)') - ->bind(':menuType', $menuType); - } - elseif ($hasClientId) - { - $query->where($db->quoteName('a.client_id') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - } - - // Prevent the parent and children from showing if requested. - if ($parentId && $mode == 2) - { - $query->join('LEFT', $db->quoteName('#__menu', 'p'), $db->quoteName('p.id') . ' = :parentId') - ->where( - '(' . $db->quoteName('a.lft') . ' <= ' . $db->quoteName('p.lft') - . ' OR ' . $db->quoteName('a.rgt') . ' >= ' . $db->quoteName('p.rgt') . ')' - ) - ->bind(':parentId', $parentId, ParameterType::INTEGER); - } - - if (!empty($languages)) - { - $query->whereIn($db->quoteName('a.language'), (array) $languages, ParameterType::STRING); - } - - if (!empty($published)) - { - $query->whereIn($db->quoteName('a.published'), (array) $published); - } - - $query->where($db->quoteName('a.published') . ' != -2'); - $query->order($db->quoteName('a.lft') . ' ASC'); - - try - { - // Get the options. - $db->setQuery($query); - $links = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - if (empty($menuType)) - { - // If the menutype is empty, group the items by menutype. - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__menu_types')) - ->where($db->quoteName('menutype') . ' <> ' . $db->quote('')) - ->order( - [ - $db->quoteName('title'), - $db->quoteName('menutype'), - ] - ); - - if ($hasClientId) - { - $query->where($db->quoteName('client_id') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER); - } - - try - { - $db->setQuery($query); - $menuTypes = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - // Create a reverse lookup and aggregate the links. - $rlu = array(); - - foreach ($menuTypes as &$type) - { - $rlu[$type->menutype] = & $type; - $type->links = array(); - } - - // Loop through the list of menu links. - foreach ($links as &$link) - { - if (isset($rlu[$link->menutype])) - { - $rlu[$link->menutype]->links[] = & $link; - - // Cleanup garbage. - unset($link->menutype); - } - } - - return $menuTypes; - } - else - { - return $links; - } - } - - /** - * Get the associations - * - * @param integer $pk Menu item id - * - * @return array - * - * @since 3.0 - */ - public static function getAssociations($pk) - { - $langAssociations = Associations::getAssociations('com_menus', '#__menu', 'com_menus.item', $pk, 'id', '', ''); - $associations = array(); - - foreach ($langAssociations as $langAssociation) - { - $associations[$langAssociation->language] = $langAssociation->id; - } - - return $associations; - } - - /** - * Load the menu items from database for the given menutype - * - * @param string $menutype The selected menu type - * @param boolean $enabledOnly Whether to load only enabled/published menu items. - * @param int[] $exclude The menu items to exclude from the list - * - * @return AdministratorMenuItem A root node with the menu items as children - * - * @since 4.0.0 - */ - public static function getMenuItems($menutype, $enabledOnly = false, $exclude = array()) - { - $root = new AdministratorMenuItem; - $db = Factory::getContainer()->get(DatabaseInterface::class); - $query = $db->getQuery(true); - - // Prepare the query. - $query->select($db->quoteName('m') . '.*') - ->from($db->quoteName('#__menu', 'm')) - ->where( - [ - $db->quoteName('m.menutype') . ' = :menutype', - $db->quoteName('m.client_id') . ' = 1', - $db->quoteName('m.id') . ' > 1', - ] - ) - ->bind(':menutype', $menutype); - - if ($enabledOnly) - { - $query->where($db->quoteName('m.published') . ' = 1'); - } - - // Filter on the enabled states. - $query->select($db->quoteName('e.element')) - ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('m.component_id') . ' = ' . $db->quoteName('e.extension_id')) - ->extendWhere( - 'AND', - [ - $db->quoteName('e.enabled') . ' = 1', - $db->quoteName('e.enabled') . ' IS NULL', - ], - 'OR' - ); - - if (count($exclude)) - { - $exId = array_map('intval', array_filter($exclude, 'is_numeric')); - $exEl = array_filter($exclude, 'is_string'); - - if ($exId) - { - $query->whereNotIn($db->quoteName('m.id'), $exId) - ->whereNotIn($db->quoteName('m.parent_id'), $exId); - } - - if ($exEl) - { - $query->whereNotIn($db->quoteName('e.element'), $exEl, ParameterType::STRING); - } - } - - // Order by lft. - $query->order($db->quoteName('m.lft')); - - try - { - $menuItems = []; - $iterator = $db->setQuery($query)->getIterator(); - - foreach ($iterator as $item) - { - $menuItems[$item->id] = new AdministratorMenuItem((array) $item); - } - - unset($iterator); - - foreach ($menuItems as $menuitem) - { - // Resolve the alias item to get the original item - if ($menuitem->type == 'alias') - { - static::resolveAlias($menuitem); - } - - if ($menuitem->link = in_array($menuitem->type, array('separator', 'heading', 'container')) ? '#' : trim($menuitem->link)) - { - $menuitem->submenu = array(); - $menuitem->class = $menuitem->img ?? ''; - $menuitem->scope = $menuitem->scope ?? null; - $menuitem->target = $menuitem->browserNav ? '_blank' : ''; - } - - $menuitem->ajaxbadge = $menuitem->getParams()->get('ajax-badge'); - $menuitem->dashboard = $menuitem->getParams()->get('dashboard'); - - if ($menuitem->parent_id > 1) - { - if (isset($menuItems[$menuitem->parent_id])) - { - $menuItems[$menuitem->parent_id]->addChild($menuitem); - } - } - else - { - $root->addChild($menuitem); - } - } - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage(Text::_('JERROR_AN_ERROR_HAS_OCCURRED'), 'error'); - } - - return $root; - } - - /** - * Method to install a preset menu into database and link them to the given menutype - * - * @param string $preset The preset name - * @param string $menutype The target menutype - * - * @return void - * - * @throws \Exception - * - * @since 4.0.0 - */ - public static function installPreset($preset, $menutype) - { - $root = static::loadPreset($preset, false); - - if (count($root->getChildren()) == 0) - { - throw new \Exception(Text::_('COM_MENUS_PRESET_LOAD_FAILED')); - } - - static::installPresetItems($root, $menutype); - } - - /** - * Method to install a preset menu item into database and link it to the given menutype - * - * @param AdministratorMenuItem $node The parent node of the items to process - * @param string $menutype The target menutype - * - * @return void - * - * @throws \Exception - * - * @since 4.0.0 - */ - protected static function installPresetItems($node, $menutype) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $items = $node->getChildren(); - - static $components = array(); - - if (!$components) - { - $query->select( - [ - $db->quoteName('extension_id'), - $db->quoteName('element'), - ] - ) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('component')); - $components = $db->setQuery($query)->loadObjectList(); - $components = array_column((array) $components, 'element', 'extension_id'); - } - - Factory::getApplication()->triggerEvent('onPreprocessMenuItems', array('com_menus.administrator.import', &$items, null, true)); - - foreach ($items as $item) - { - /** @var \Joomla\CMS\Table\Menu $table */ - $table = Table::getInstance('Menu'); - - $item->alias = $menutype . '-' . $item->title; - - // Temporarily set unicodeslugs if a menu item has an unicode alias - $unicode = Factory::getApplication()->set('unicodeslugs', 1); - $item->alias = ApplicationHelper::stringURLSafe($item->alias); - Factory::getApplication()->set('unicodeslugs', $unicode); - - if ($item->type == 'separator') - { - // Do not reuse a separator - $item->title = $item->title ?: '-'; - $item->alias = microtime(true); - } - elseif ($item->type == 'heading' || $item->type == 'container') - { - // Try to match an existing record to have minimum collision for a heading - $keys = array( - 'menutype' => $menutype, - 'type' => $item->type, - 'title' => $item->title, - 'parent_id' => (int) $item->getParent()->id, - 'client_id' => 1, - ); - $table->load($keys); - } - elseif ($item->type == 'url' || $item->type == 'component') - { - if (substr($item->link, 0, 8) === 'special:') - { - $special = substr($item->link, 8); - - if ($special === 'language-forum') - { - $item->link = 'index.php?option=com_admin&view=help&layout=langforum'; - } - elseif ($special === 'custom-forum') - { - $item->link = ''; - } - } - - // Try to match an existing record to have minimum collision for a link - $keys = array( - 'menutype' => $menutype, - 'type' => $item->type, - 'link' => $item->link, - 'parent_id' => (int) $item->getParent()->id, - 'client_id' => 1, - ); - $table->load($keys); - } - - // Translate "hideitems" param value from "element" into "menu-item-id" - if ($item->type == 'container' && count($hideitems = (array) $item->getParams()->get('hideitems'))) - { - foreach ($hideitems as &$hel) - { - if (!is_numeric($hel)) - { - $hel = array_search($hel, $components); - } - } - - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__menu')) - ->whereIn($db->quoteName('component_id'), $hideitems); - $hideitems = $db->setQuery($query)->loadColumn(); - - $item->getParams()->set('hideitems', $hideitems); - } - - $record = array( - 'menutype' => $menutype, - 'title' => $item->title, - 'alias' => $item->alias, - 'type' => $item->type, - 'link' => $item->link, - 'browserNav' => $item->browserNav, - 'img' => $item->class, - 'access' => $item->access, - 'component_id' => array_search($item->element, $components) ?: 0, - 'parent_id' => (int) $item->getParent()->id, - 'client_id' => 1, - 'published' => 1, - 'language' => '*', - 'home' => 0, - 'params' => (string) $item->getParams(), - ); - - if (!$table->bind($record)) - { - throw new \Exception($table->getError()); - } - - $table->setLocation($item->getParent()->id, 'last-child'); - - if (!$table->check()) - { - throw new \Exception($table->getError()); - } - - if (!$table->store()) - { - throw new \Exception($table->getError()); - } - - $item->id = $table->get('id'); - - if ($item->hasChildren()) - { - static::installPresetItems($item, $menutype); - } - } - } - - /** - * Add a custom preset externally via plugin or any other means. - * WARNING: Presets with same name will replace previously added preset *except* Joomla's default preset (joomla) - * - * @param string $name The unique identifier for the preset. - * @param string $title The display label for the preset. - * @param string $path The path to the preset file. - * @param bool $replace Whether to replace the preset with the same name if any (except 'joomla'). - * - * @return void - * - * @since 4.0.0 - */ - public static function addPreset($name, $title, $path, $replace = true) - { - if (static::$presets === null) - { - static::getPresets(); - } - - if ($name == 'joomla') - { - $replace = false; - } - - if (($replace || !array_key_exists($name, static::$presets)) && is_file($path)) - { - $preset = new \stdClass; - - $preset->name = $name; - $preset->title = $title; - $preset->path = $path; - - static::$presets[$name] = $preset; - } - } - - /** - * Get a list of available presets. - * - * @return \stdClass[] - * - * @since 4.0.0 - */ - public static function getPresets() - { - if (static::$presets === null) - { - // Important: 'null' will cause infinite recursion. - static::$presets = array(); - - $components = ComponentHelper::getComponents(); - $lang = Factory::getApplication()->getLanguage(); - - foreach ($components as $component) - { - if (!$component->enabled) - { - continue; - } - - $folder = JPATH_ADMINISTRATOR . '/components/' . $component->option . '/presets/'; - - if (!Folder::exists($folder)) - { - continue; - } - - $lang->load($component->option . '.sys', JPATH_ADMINISTRATOR) - || $lang->load($component->option . '.sys', JPATH_ADMINISTRATOR . '/components/' . $component->option); - - $presets = Folder::files($folder, '.xml'); - - foreach ($presets as $preset) - { - $name = File::stripExt($preset); - $title = strtoupper($component->option . '_MENUS_PRESET_' . $name); - static::addPreset($name, $title, $folder . $preset); - } - } - - // Load from template folder automatically - $app = Factory::getApplication(); - $tpl = JPATH_THEMES . '/' . $app->getTemplate() . '/html/com_menus/presets'; - - if (is_dir($tpl)) - { - $files = Folder::files($tpl, '\.xml$'); - - foreach ($files as $file) - { - $name = substr($file, 0, -4); - $title = str_replace('-', ' ', $name); - - static::addPreset(strtolower($name), ucwords($title), $tpl . '/' . $file); - } - } - } - - return static::$presets; - } - - /** - * Load the menu items from a preset file into a hierarchical list of objects - * - * @param string $name The preset name - * @param bool $fallback Fallback to default (joomla) preset if the specified one could not be loaded? - * @param AdministratorMenuItem $parent Root node of the menu - * - * @return AdministratorMenuItem - * - * @since 4.0.0 - */ - public static function loadPreset($name, $fallback = true, $parent = null) - { - $presets = static::getPresets(); - - if (!$parent) - { - $parent = new AdministratorMenuItem; - } - - if (isset($presets[$name]) && ($xml = simplexml_load_file($presets[$name]->path, null, LIBXML_NOCDATA)) && $xml instanceof \SimpleXMLElement) - { - static::loadXml($xml, $parent); - } - elseif ($fallback && isset($presets['default'])) - { - if (($xml = simplexml_load_file($presets['default']->path, null, LIBXML_NOCDATA)) && $xml instanceof \SimpleXMLElement) - { - static::loadXml($xml, $parent); - } - } - - return $parent; - } - - /** - * Method to resolve the menu item alias type menu item - * - * @param AdministratorMenuItem &$item The alias object - * - * @return void - * - * @since 4.0.0 - */ - public static function resolveAlias(&$item) - { - $obj = $item; - - while ($obj->type == 'alias') - { - $aliasTo = (int) $obj->getParams()->get('aliasoptions'); - - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $query->select( - [ - $db->quoteName('a.id'), - $db->quoteName('a.link'), - $db->quoteName('a.type'), - $db->quoteName('e.element'), - ] - ) - ->from($db->quoteName('#__menu', 'a')) - ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('a.component_id')) - ->where($db->quoteName('a.id') . ' = :aliasTo') - ->bind(':aliasTo', $aliasTo, ParameterType::INTEGER); - - try - { - $obj = new AdministratorMenuItem($db->setQuery($query)->loadAssoc()); - - if (!$obj) - { - $item->link = ''; - - return; - } - } - catch (\Exception $e) - { - $item->link = ''; - - return; - } - } - - $item->id = $obj->id; - $item->link = $obj->link; - $item->type = $obj->type; - $item->element = $obj->element; - } - - /** - * Parse the flat list of menu items and prepare the hierarchy of them using parent-child relationship. - * - * @param AdministratorMenuItem $item Menu item to preprocess - * - * @return void - * - * @since 4.0.0 - */ - public static function preprocess($item) - { - // Resolve the alias item to get the original item - if ($item->type == 'alias') - { - static::resolveAlias($item); - } - - if ($item->link = in_array($item->type, array('separator', 'heading', 'container')) ? '#' : trim($item->link)) - { - $item->class = $item->img ?? ''; - $item->scope = $item->scope ?? null; - $item->target = $item->browserNav ? '_blank' : ''; - } - } - - /** - * Load a menu tree from an XML file - * - * @param \SimpleXMLElement[] $elements The xml menuitem nodes - * @param AdministratorMenuItem $parent The menu hierarchy list to be populated - * @param string[] $replace The substring replacements for iterator type items - * - * @return void - * - * @since 4.0.0 - */ - protected static function loadXml($elements, $parent, $replace = array()) - { - foreach ($elements as $element) - { - if ($element->getName() != 'menuitem') - { - continue; - } - - $select = (string) $element['sql_select']; - $from = (string) $element['sql_from']; - - /** - * Following is a repeatable group based on simple database query. This requires sql_* attributes (sql_select and sql_from are required) - * The values can be used like - "{sql:columnName}" in any attribute of repeated elements. - * The repeated elements are place inside this xml node but they will be populated in the same level in the rendered menu - */ - if ($select && $from) - { - $hidden = $element['hidden'] == 'true'; - $where = (string) $element['sql_where']; - $order = (string) $element['sql_order']; - $group = (string) $element['sql_group']; - $lJoin = (string) $element['sql_leftjoin']; - $iJoin = (string) $element['sql_innerjoin']; - - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $query->select($select)->from($from); - - if ($where) - { - $query->where($where); - } - - if ($order) - { - $query->order($order); - } - - if ($group) - { - $query->group($group); - } - - if ($lJoin) - { - $query->join('LEFT', $lJoin); - } - - if ($iJoin) - { - $query->join('INNER', $iJoin); - } - - $results = $db->setQuery($query)->loadObjectList(); - - // Skip the entire group if no items to iterate over. - if ($results) - { - // Show the repeatable group heading node only if not set as hidden. - if (!$hidden) - { - $child = static::parseXmlNode($element, $replace); - $parent->addChild($child); - } - - // Iterate over the matching records, items goes in the same level (not $item->submenu) as this node. - if ('self' == (string) $element['sql_target']) - { - foreach ($results as $result) - { - static::loadXml($element->menuitem, $child, $result); - } - } - else - { - foreach ($results as $result) - { - static::loadXml($element->menuitem, $parent, $result); - } - } - } - } - else - { - $item = static::parseXmlNode($element, $replace); - - // Process the child nodes - static::loadXml($element->menuitem, $item, $replace); - - $parent->addChild($item); - } - } - } - - /** - * Create a menu item node from an xml element - * - * @param \SimpleXMLElement $node A menuitem element from preset xml - * @param string[] $replace The values to substitute in the title, link and element texts - * - * @return \stdClass - * - * @since 4.0.0 - */ - protected static function parseXmlNode($node, $replace = array()) - { - $item = new AdministratorMenuItem; - - $item->id = null; - $item->type = (string) $node['type']; - $item->title = (string) $node['title']; - $item->alias = (string) $node['alias']; - $item->link = (string) $node['link']; - $item->target = (string) $node['target']; - $item->element = (string) $node['element']; - $item->class = (string) $node['class']; - $item->icon = (string) $node['icon']; - $item->access = (int) $node['access']; - $item->scope = (string) $node['scope'] ?: 'default'; - $item->ajaxbadge = (string) $node['ajax-badge']; - $item->dashboard = (string) $node['dashboard']; - - $params = new Registry(trim($node->params)); - $params->set('menu-permission', (string) $node['permission']); - - if ($item->type == 'separator' && trim($item->title, '- ')) - { - $params->set('text_separator', 1); - } - - if ($item->type == 'heading' || $item->type == 'container') - { - $item->link = '#'; - } - - if ((string) $node['quicktask']) - { - $params->set('menu-quicktask', (string) $node['quicktask']); - $params->set('menu-quicktask-title', (string) $node['quicktask-title']); - $params->set('menu-quicktask-icon', (string) $node['quicktask-icon']); - $params->set('menu-quicktask-permission', (string) $node['quicktask-permission']); - } - - // Translate attributes for iterator values - foreach ($replace as $var => $val) - { - $item->title = str_replace("{sql:$var}", $val, $item->title); - $item->element = str_replace("{sql:$var}", $val, $item->element); - $item->link = str_replace("{sql:$var}", $val, $item->link); - $item->class = str_replace("{sql:$var}", $val, $item->class); - $item->icon = str_replace("{sql:$var}", $val, $item->icon); - $params->set('menu-quicktask', str_replace("{sql:$var}", $val, $params->get('menu-quicktask'))); - } - - $item->setParams($params); - - return $item; - } + /** + * Defines the valid request variables for the reverse lookup. + * + * @var array + */ + protected static $_filter = array('option', 'view', 'layout'); + + /** + * List of preset include paths + * + * @var array + * + * @since 4.0.0 + */ + protected static $presets = null; + + /** + * Gets a standard form of a link for lookups. + * + * @param mixed $request A link string or array of request variables. + * + * @return mixed A link in standard option-view-layout form, or false if the supplied response is invalid. + * + * @since 1.6 + */ + public static function getLinkKey($request) + { + if (empty($request)) { + return false; + } + + // Check if the link is in the form of index.php?... + if (is_string($request)) { + $args = array(); + + if (strpos($request, 'index.php') === 0) { + parse_str(parse_url(htmlspecialchars_decode($request), PHP_URL_QUERY), $args); + } else { + parse_str($request, $args); + } + + $request = $args; + } + + // Only take the option, view and layout parts. + foreach ($request as $name => $value) { + if ((!in_array($name, self::$_filter)) && (!($name == 'task' && !array_key_exists('view', $request)))) { + // Remove the variables we want to ignore. + unset($request[$name]); + } + } + + ksort($request); + + return 'index.php?' . http_build_query($request, '', '&'); + } + + /** + * Get the menu list for create a menu module + * + * @param int $clientId Optional client id - viz 0 = site, 1 = administrator, can be NULL for all + * + * @return array The menu array list + * + * @since 1.6 + */ + public static function getMenuTypes($clientId = 0) + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('a.menutype')) + ->from($db->quoteName('#__menu_types', 'a')); + + if (isset($clientId)) { + $clientId = (int) $clientId; + $query->where($db->quoteName('a.client_id') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + } + + $db->setQuery($query); + + return $db->loadColumn(); + } + + /** + * Get a list of menu links for one or all menus. + * + * @param string $menuType An option menu to filter the list on, otherwise all menu with given client id links + * are returned as a grouped array. + * @param integer $parentId An optional parent ID to pivot results around. + * @param integer $mode An optional mode. If parent ID is set and mode=2, the parent and children are excluded from the list. + * @param array $published An optional array of states + * @param array $languages Optional array of specify which languages we want to filter + * @param int $clientId Optional client id - viz 0 = site, 1 = administrator, can be NULL for all (used only if menutype not given) + * + * @return array|boolean + * + * @since 1.6 + */ + public static function getMenuLinks($menuType = null, $parentId = 0, $mode = 0, $published = array(), $languages = array(), $clientId = 0) + { + $hasClientId = $clientId !== null; + $clientId = (int) $clientId; + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + 'DISTINCT ' . $db->quoteName('a.id', 'value'), + $db->quoteName('a.title', 'text'), + $db->quoteName('a.alias'), + $db->quoteName('a.level'), + $db->quoteName('a.menutype'), + $db->quoteName('a.client_id'), + $db->quoteName('a.type'), + $db->quoteName('a.published'), + $db->quoteName('a.template_style_id'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.language'), + $db->quoteName('a.lft'), + $db->quoteName('e.name', 'componentname'), + $db->quoteName('e.element'), + ] + ) + ->from($db->quoteName('#__menu', 'a')) + ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('a.component_id')); + + if (Multilanguage::isEnabled()) { + $query->select( + [ + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image', 'language_image'), + $db->quoteName('l.sef', 'language_sef'), + ] + ) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); + } + + // Filter by the type if given, this is more specific than client id + if ($menuType) { + $query->where('(' . $db->quoteName('a.menutype') . ' = :menuType OR ' . $db->quoteName('a.parent_id') . ' = 0)') + ->bind(':menuType', $menuType); + } elseif ($hasClientId) { + $query->where($db->quoteName('a.client_id') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + } + + // Prevent the parent and children from showing if requested. + if ($parentId && $mode == 2) { + $query->join('LEFT', $db->quoteName('#__menu', 'p'), $db->quoteName('p.id') . ' = :parentId') + ->where( + '(' . $db->quoteName('a.lft') . ' <= ' . $db->quoteName('p.lft') + . ' OR ' . $db->quoteName('a.rgt') . ' >= ' . $db->quoteName('p.rgt') . ')' + ) + ->bind(':parentId', $parentId, ParameterType::INTEGER); + } + + if (!empty($languages)) { + $query->whereIn($db->quoteName('a.language'), (array) $languages, ParameterType::STRING); + } + + if (!empty($published)) { + $query->whereIn($db->quoteName('a.published'), (array) $published); + } + + $query->where($db->quoteName('a.published') . ' != -2'); + $query->order($db->quoteName('a.lft') . ' ASC'); + + try { + // Get the options. + $db->setQuery($query); + $links = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + if (empty($menuType)) { + // If the menutype is empty, group the items by menutype. + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__menu_types')) + ->where($db->quoteName('menutype') . ' <> ' . $db->quote('')) + ->order( + [ + $db->quoteName('title'), + $db->quoteName('menutype'), + ] + ); + + if ($hasClientId) { + $query->where($db->quoteName('client_id') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER); + } + + try { + $db->setQuery($query); + $menuTypes = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + // Create a reverse lookup and aggregate the links. + $rlu = array(); + + foreach ($menuTypes as &$type) { + $rlu[$type->menutype] = & $type; + $type->links = array(); + } + + // Loop through the list of menu links. + foreach ($links as &$link) { + if (isset($rlu[$link->menutype])) { + $rlu[$link->menutype]->links[] = & $link; + + // Cleanup garbage. + unset($link->menutype); + } + } + + return $menuTypes; + } else { + return $links; + } + } + + /** + * Get the associations + * + * @param integer $pk Menu item id + * + * @return array + * + * @since 3.0 + */ + public static function getAssociations($pk) + { + $langAssociations = Associations::getAssociations('com_menus', '#__menu', 'com_menus.item', $pk, 'id', '', ''); + $associations = array(); + + foreach ($langAssociations as $langAssociation) { + $associations[$langAssociation->language] = $langAssociation->id; + } + + return $associations; + } + + /** + * Load the menu items from database for the given menutype + * + * @param string $menutype The selected menu type + * @param boolean $enabledOnly Whether to load only enabled/published menu items. + * @param int[] $exclude The menu items to exclude from the list + * + * @return AdministratorMenuItem A root node with the menu items as children + * + * @since 4.0.0 + */ + public static function getMenuItems($menutype, $enabledOnly = false, $exclude = array()) + { + $root = new AdministratorMenuItem(); + $db = Factory::getContainer()->get(DatabaseInterface::class); + $query = $db->getQuery(true); + + // Prepare the query. + $query->select($db->quoteName('m') . '.*') + ->from($db->quoteName('#__menu', 'm')) + ->where( + [ + $db->quoteName('m.menutype') . ' = :menutype', + $db->quoteName('m.client_id') . ' = 1', + $db->quoteName('m.id') . ' > 1', + ] + ) + ->bind(':menutype', $menutype); + + if ($enabledOnly) { + $query->where($db->quoteName('m.published') . ' = 1'); + } + + // Filter on the enabled states. + $query->select($db->quoteName('e.element')) + ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('m.component_id') . ' = ' . $db->quoteName('e.extension_id')) + ->extendWhere( + 'AND', + [ + $db->quoteName('e.enabled') . ' = 1', + $db->quoteName('e.enabled') . ' IS NULL', + ], + 'OR' + ); + + if (count($exclude)) { + $exId = array_map('intval', array_filter($exclude, 'is_numeric')); + $exEl = array_filter($exclude, 'is_string'); + + if ($exId) { + $query->whereNotIn($db->quoteName('m.id'), $exId) + ->whereNotIn($db->quoteName('m.parent_id'), $exId); + } + + if ($exEl) { + $query->whereNotIn($db->quoteName('e.element'), $exEl, ParameterType::STRING); + } + } + + // Order by lft. + $query->order($db->quoteName('m.lft')); + + try { + $menuItems = []; + $iterator = $db->setQuery($query)->getIterator(); + + foreach ($iterator as $item) { + $menuItems[$item->id] = new AdministratorMenuItem((array) $item); + } + + unset($iterator); + + foreach ($menuItems as $menuitem) { + // Resolve the alias item to get the original item + if ($menuitem->type == 'alias') { + static::resolveAlias($menuitem); + } + + if ($menuitem->link = in_array($menuitem->type, array('separator', 'heading', 'container')) ? '#' : trim($menuitem->link)) { + $menuitem->submenu = array(); + $menuitem->class = $menuitem->img ?? ''; + $menuitem->scope = $menuitem->scope ?? null; + $menuitem->target = $menuitem->browserNav ? '_blank' : ''; + } + + $menuitem->ajaxbadge = $menuitem->getParams()->get('ajax-badge'); + $menuitem->dashboard = $menuitem->getParams()->get('dashboard'); + + if ($menuitem->parent_id > 1) { + if (isset($menuItems[$menuitem->parent_id])) { + $menuItems[$menuitem->parent_id]->addChild($menuitem); + } + } else { + $root->addChild($menuitem); + } + } + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage(Text::_('JERROR_AN_ERROR_HAS_OCCURRED'), 'error'); + } + + return $root; + } + + /** + * Method to install a preset menu into database and link them to the given menutype + * + * @param string $preset The preset name + * @param string $menutype The target menutype + * + * @return void + * + * @throws \Exception + * + * @since 4.0.0 + */ + public static function installPreset($preset, $menutype) + { + $root = static::loadPreset($preset, false); + + if (count($root->getChildren()) == 0) { + throw new \Exception(Text::_('COM_MENUS_PRESET_LOAD_FAILED')); + } + + static::installPresetItems($root, $menutype); + } + + /** + * Method to install a preset menu item into database and link it to the given menutype + * + * @param AdministratorMenuItem $node The parent node of the items to process + * @param string $menutype The target menutype + * + * @return void + * + * @throws \Exception + * + * @since 4.0.0 + */ + protected static function installPresetItems($node, $menutype) + { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $items = $node->getChildren(); + + static $components = array(); + + if (!$components) { + $query->select( + [ + $db->quoteName('extension_id'), + $db->quoteName('element'), + ] + ) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('component')); + $components = $db->setQuery($query)->loadObjectList(); + $components = array_column((array) $components, 'element', 'extension_id'); + } + + Factory::getApplication()->triggerEvent('onPreprocessMenuItems', array('com_menus.administrator.import', &$items, null, true)); + + foreach ($items as $item) { + /** @var \Joomla\CMS\Table\Menu $table */ + $table = Table::getInstance('Menu'); + + $item->alias = $menutype . '-' . $item->title; + + // Temporarily set unicodeslugs if a menu item has an unicode alias + $unicode = Factory::getApplication()->set('unicodeslugs', 1); + $item->alias = ApplicationHelper::stringURLSafe($item->alias); + Factory::getApplication()->set('unicodeslugs', $unicode); + + if ($item->type == 'separator') { + // Do not reuse a separator + $item->title = $item->title ?: '-'; + $item->alias = microtime(true); + } elseif ($item->type == 'heading' || $item->type == 'container') { + // Try to match an existing record to have minimum collision for a heading + $keys = array( + 'menutype' => $menutype, + 'type' => $item->type, + 'title' => $item->title, + 'parent_id' => (int) $item->getParent()->id, + 'client_id' => 1, + ); + $table->load($keys); + } elseif ($item->type == 'url' || $item->type == 'component') { + if (substr($item->link, 0, 8) === 'special:') { + $special = substr($item->link, 8); + + if ($special === 'language-forum') { + $item->link = 'index.php?option=com_admin&view=help&layout=langforum'; + } elseif ($special === 'custom-forum') { + $item->link = ''; + } + } + + // Try to match an existing record to have minimum collision for a link + $keys = array( + 'menutype' => $menutype, + 'type' => $item->type, + 'link' => $item->link, + 'parent_id' => (int) $item->getParent()->id, + 'client_id' => 1, + ); + $table->load($keys); + } + + // Translate "hideitems" param value from "element" into "menu-item-id" + if ($item->type == 'container' && count($hideitems = (array) $item->getParams()->get('hideitems'))) { + foreach ($hideitems as &$hel) { + if (!is_numeric($hel)) { + $hel = array_search($hel, $components); + } + } + + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__menu')) + ->whereIn($db->quoteName('component_id'), $hideitems); + $hideitems = $db->setQuery($query)->loadColumn(); + + $item->getParams()->set('hideitems', $hideitems); + } + + $record = array( + 'menutype' => $menutype, + 'title' => $item->title, + 'alias' => $item->alias, + 'type' => $item->type, + 'link' => $item->link, + 'browserNav' => $item->browserNav, + 'img' => $item->class, + 'access' => $item->access, + 'component_id' => array_search($item->element, $components) ?: 0, + 'parent_id' => (int) $item->getParent()->id, + 'client_id' => 1, + 'published' => 1, + 'language' => '*', + 'home' => 0, + 'params' => (string) $item->getParams(), + ); + + if (!$table->bind($record)) { + throw new \Exception($table->getError()); + } + + $table->setLocation($item->getParent()->id, 'last-child'); + + if (!$table->check()) { + throw new \Exception($table->getError()); + } + + if (!$table->store()) { + throw new \Exception($table->getError()); + } + + $item->id = $table->get('id'); + + if ($item->hasChildren()) { + static::installPresetItems($item, $menutype); + } + } + } + + /** + * Add a custom preset externally via plugin or any other means. + * WARNING: Presets with same name will replace previously added preset *except* Joomla's default preset (joomla) + * + * @param string $name The unique identifier for the preset. + * @param string $title The display label for the preset. + * @param string $path The path to the preset file. + * @param bool $replace Whether to replace the preset with the same name if any (except 'joomla'). + * + * @return void + * + * @since 4.0.0 + */ + public static function addPreset($name, $title, $path, $replace = true) + { + if (static::$presets === null) { + static::getPresets(); + } + + if ($name == 'joomla') { + $replace = false; + } + + if (($replace || !array_key_exists($name, static::$presets)) && is_file($path)) { + $preset = new \stdClass(); + + $preset->name = $name; + $preset->title = $title; + $preset->path = $path; + + static::$presets[$name] = $preset; + } + } + + /** + * Get a list of available presets. + * + * @return \stdClass[] + * + * @since 4.0.0 + */ + public static function getPresets() + { + if (static::$presets === null) { + // Important: 'null' will cause infinite recursion. + static::$presets = array(); + + $components = ComponentHelper::getComponents(); + $lang = Factory::getApplication()->getLanguage(); + + foreach ($components as $component) { + if (!$component->enabled) { + continue; + } + + $folder = JPATH_ADMINISTRATOR . '/components/' . $component->option . '/presets/'; + + if (!Folder::exists($folder)) { + continue; + } + + $lang->load($component->option . '.sys', JPATH_ADMINISTRATOR) + || $lang->load($component->option . '.sys', JPATH_ADMINISTRATOR . '/components/' . $component->option); + + $presets = Folder::files($folder, '.xml'); + + foreach ($presets as $preset) { + $name = File::stripExt($preset); + $title = strtoupper($component->option . '_MENUS_PRESET_' . $name); + static::addPreset($name, $title, $folder . $preset); + } + } + + // Load from template folder automatically + $app = Factory::getApplication(); + $tpl = JPATH_THEMES . '/' . $app->getTemplate() . '/html/com_menus/presets'; + + if (is_dir($tpl)) { + $files = Folder::files($tpl, '\.xml$'); + + foreach ($files as $file) { + $name = substr($file, 0, -4); + $title = str_replace('-', ' ', $name); + + static::addPreset(strtolower($name), ucwords($title), $tpl . '/' . $file); + } + } + } + + return static::$presets; + } + + /** + * Load the menu items from a preset file into a hierarchical list of objects + * + * @param string $name The preset name + * @param bool $fallback Fallback to default (joomla) preset if the specified one could not be loaded? + * @param AdministratorMenuItem $parent Root node of the menu + * + * @return AdministratorMenuItem + * + * @since 4.0.0 + */ + public static function loadPreset($name, $fallback = true, $parent = null) + { + $presets = static::getPresets(); + + if (!$parent) { + $parent = new AdministratorMenuItem(); + } + + if (isset($presets[$name]) && ($xml = simplexml_load_file($presets[$name]->path, null, LIBXML_NOCDATA)) && $xml instanceof \SimpleXMLElement) { + static::loadXml($xml, $parent); + } elseif ($fallback && isset($presets['default'])) { + if (($xml = simplexml_load_file($presets['default']->path, null, LIBXML_NOCDATA)) && $xml instanceof \SimpleXMLElement) { + static::loadXml($xml, $parent); + } + } + + return $parent; + } + + /** + * Method to resolve the menu item alias type menu item + * + * @param AdministratorMenuItem &$item The alias object + * + * @return void + * + * @since 4.0.0 + */ + public static function resolveAlias(&$item) + { + $obj = $item; + + while ($obj->type == 'alias') { + $aliasTo = (int) $obj->getParams()->get('aliasoptions'); + + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('a.id'), + $db->quoteName('a.link'), + $db->quoteName('a.type'), + $db->quoteName('e.element'), + ] + ) + ->from($db->quoteName('#__menu', 'a')) + ->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('a.component_id')) + ->where($db->quoteName('a.id') . ' = :aliasTo') + ->bind(':aliasTo', $aliasTo, ParameterType::INTEGER); + + try { + $obj = new AdministratorMenuItem($db->setQuery($query)->loadAssoc()); + + if (!$obj) { + $item->link = ''; + + return; + } + } catch (\Exception $e) { + $item->link = ''; + + return; + } + } + + $item->id = $obj->id; + $item->link = $obj->link; + $item->type = $obj->type; + $item->element = $obj->element; + } + + /** + * Parse the flat list of menu items and prepare the hierarchy of them using parent-child relationship. + * + * @param AdministratorMenuItem $item Menu item to preprocess + * + * @return void + * + * @since 4.0.0 + */ + public static function preprocess($item) + { + // Resolve the alias item to get the original item + if ($item->type == 'alias') { + static::resolveAlias($item); + } + + if ($item->link = in_array($item->type, array('separator', 'heading', 'container')) ? '#' : trim($item->link)) { + $item->class = $item->img ?? ''; + $item->scope = $item->scope ?? null; + $item->target = $item->browserNav ? '_blank' : ''; + } + } + + /** + * Load a menu tree from an XML file + * + * @param \SimpleXMLElement[] $elements The xml menuitem nodes + * @param AdministratorMenuItem $parent The menu hierarchy list to be populated + * @param string[] $replace The substring replacements for iterator type items + * + * @return void + * + * @since 4.0.0 + */ + protected static function loadXml($elements, $parent, $replace = array()) + { + foreach ($elements as $element) { + if ($element->getName() != 'menuitem') { + continue; + } + + $select = (string) $element['sql_select']; + $from = (string) $element['sql_from']; + + /** + * Following is a repeatable group based on simple database query. This requires sql_* attributes (sql_select and sql_from are required) + * The values can be used like - "{sql:columnName}" in any attribute of repeated elements. + * The repeated elements are place inside this xml node but they will be populated in the same level in the rendered menu + */ + if ($select && $from) { + $hidden = $element['hidden'] == 'true'; + $where = (string) $element['sql_where']; + $order = (string) $element['sql_order']; + $group = (string) $element['sql_group']; + $lJoin = (string) $element['sql_leftjoin']; + $iJoin = (string) $element['sql_innerjoin']; + + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $query->select($select)->from($from); + + if ($where) { + $query->where($where); + } + + if ($order) { + $query->order($order); + } + + if ($group) { + $query->group($group); + } + + if ($lJoin) { + $query->join('LEFT', $lJoin); + } + + if ($iJoin) { + $query->join('INNER', $iJoin); + } + + $results = $db->setQuery($query)->loadObjectList(); + + // Skip the entire group if no items to iterate over. + if ($results) { + // Show the repeatable group heading node only if not set as hidden. + if (!$hidden) { + $child = static::parseXmlNode($element, $replace); + $parent->addChild($child); + } + + // Iterate over the matching records, items goes in the same level (not $item->submenu) as this node. + if ('self' == (string) $element['sql_target']) { + foreach ($results as $result) { + static::loadXml($element->menuitem, $child, $result); + } + } else { + foreach ($results as $result) { + static::loadXml($element->menuitem, $parent, $result); + } + } + } + } else { + $item = static::parseXmlNode($element, $replace); + + // Process the child nodes + static::loadXml($element->menuitem, $item, $replace); + + $parent->addChild($item); + } + } + } + + /** + * Create a menu item node from an xml element + * + * @param \SimpleXMLElement $node A menuitem element from preset xml + * @param string[] $replace The values to substitute in the title, link and element texts + * + * @return \stdClass + * + * @since 4.0.0 + */ + protected static function parseXmlNode($node, $replace = array()) + { + $item = new AdministratorMenuItem(); + + $item->id = null; + $item->type = (string) $node['type']; + $item->title = (string) $node['title']; + $item->alias = (string) $node['alias']; + $item->link = (string) $node['link']; + $item->target = (string) $node['target']; + $item->element = (string) $node['element']; + $item->class = (string) $node['class']; + $item->icon = (string) $node['icon']; + $item->access = (int) $node['access']; + $item->scope = (string) $node['scope'] ?: 'default'; + $item->ajaxbadge = (string) $node['ajax-badge']; + $item->dashboard = (string) $node['dashboard']; + + $params = new Registry(trim($node->params)); + $params->set('menu-permission', (string) $node['permission']); + + if ($item->type == 'separator' && trim($item->title, '- ')) { + $params->set('text_separator', 1); + } + + if ($item->type == 'heading' || $item->type == 'container') { + $item->link = '#'; + } + + if ((string) $node['quicktask']) { + $params->set('menu-quicktask', (string) $node['quicktask']); + $params->set('menu-quicktask-title', (string) $node['quicktask-title']); + $params->set('menu-quicktask-icon', (string) $node['quicktask-icon']); + $params->set('menu-quicktask-permission', (string) $node['quicktask-permission']); + } + + // Translate attributes for iterator values + foreach ($replace as $var => $val) { + $item->title = str_replace("{sql:$var}", $val, $item->title); + $item->element = str_replace("{sql:$var}", $val, $item->element); + $item->link = str_replace("{sql:$var}", $val, $item->link); + $item->class = str_replace("{sql:$var}", $val, $item->class); + $item->icon = str_replace("{sql:$var}", $val, $item->icon); + $params->set('menu-quicktask', str_replace("{sql:$var}", $val, $params->get('menu-quicktask'))); + } + + $item->setParams($params); + + return $item; + } } diff --git a/administrator/components/com_menus/src/Model/ItemModel.php b/administrator/components/com_menus/src/Model/ItemModel.php index 01437f6a9a13c..698d5034d341f 100644 --- a/administrator/components/com_menus/src/Model/ItemModel.php +++ b/administrator/components/com_menus/src/Model/ItemModel.php @@ -1,4 +1,5 @@ 'batchAccess', - 'language_id' => 'batchLanguage' - ); - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->published != -2) - { - return false; - } - - $menuTypeId = 0; - - if (!empty($record->menutype)) - { - $menuTypeId = $this->getMenuTypeId($record->menutype); - } - - return Factory::getUser()->authorise('core.delete', 'com_menus.menu.' . (int) $menuTypeId); - } - - /** - * Method to test whether the state of a record can be edited. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission for the component. - * - * @since 3.6 - */ - protected function canEditState($record) - { - $menuTypeId = !empty($record->menutype) ? $this->getMenuTypeId($record->menutype) : 0; - $assetKey = $menuTypeId ? 'com_menus.menu.' . (int) $menuTypeId : 'com_menus'; - - return Factory::getUser()->authorise('core.edit.state', $assetKey); - } - - /** - * Batch copy menu items to a new menu or parent. - * - * @param integer $value The new menu or sub-item. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return mixed An array of new IDs on success, boolean false on failure. - * - * @since 1.6 - */ - protected function batchCopy($value, $pks, $contexts) - { - // $value comes as {menutype}.{parent_id} - $parts = explode('.', $value); - $menuType = $parts[0]; - $parentId = ArrayHelper::getValue($parts, 1, 0, 'int'); - - $table = $this->getTable(); - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $newIds = array(); - - // Check that the parent exists - if ($parentId) - { - if (!$table->load($parentId)) - { - if ($error = $table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Non-fatal error - $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); - $parentId = 0; - } - } - } - - // If the parent is 0, set it to the ID of the root item in the tree - if (empty($parentId)) - { - if (!$parentId = $table->getRootId()) - { - $this->setError($table->getError()); - - return false; - } - } - - // Check that user has create permission for menus - $user = Factory::getUser(); - - $menuTypeId = (int) $this->getMenuTypeId($menuType); - - if (!$user->authorise('core.create', 'com_menus.menu.' . $menuTypeId)) - { - $this->setError(Text::_('COM_MENUS_BATCH_MENU_ITEM_CANNOT_CREATE')); - - return false; - } - - // We need to log the parent ID - $parents = array(); - - // Calculate the emergency stop count as a precaution against a runaway loop bug - $query->select('COUNT(' . $db->quoteName('id') . ')') - ->from($db->quoteName('#__menu')); - $db->setQuery($query); - - try - { - $count = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Parent exists so let's proceed - while (!empty($pks) && $count > 0) - { - // Pop the first id off the stack - $pk = array_shift($pks); - - $table->reset(); - - // Check that the row actually exists - if (!$table->load($pk)) - { - if ($error = $table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Not fatal error - $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); - continue; - } - } - - // Copy is a bit tricky, because we also need to copy the children - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__menu')) - ->where( - [ - $db->quoteName('lft') . ' > :lft', - $db->quoteName('rgt') . ' < :rgt', - ] - ) - ->bind(':lft', $table->lft, ParameterType::INTEGER) - ->bind(':rgt', $table->rgt, ParameterType::INTEGER); - $db->setQuery($query); - $childIds = $db->loadColumn(); - - // Add child IDs to the array only if they aren't already there. - foreach ($childIds as $childId) - { - if (!in_array($childId, $pks)) - { - $pks[] = $childId; - } - } - - // Make a copy of the old ID and Parent ID - $oldId = $table->id; - $oldParentId = $table->parent_id; - - // Reset the id because we are making a copy. - $table->id = 0; - - // If we a copying children, the Old ID will turn up in the parents list - // otherwise it's a new top level item - $table->parent_id = isset($parents[$oldParentId]) ? $parents[$oldParentId] : $parentId; - $table->menutype = $menuType; - - // Set the new location in the tree for the node. - $table->setLocation($table->parent_id, 'last-child'); - - // @todo: Deal with ordering? - // $table->ordering = 1; - $table->level = null; - $table->lft = null; - $table->rgt = null; - $table->home = 0; - - // Alter the title & alias - list($title, $alias) = $this->generateNewTitle($table->parent_id, $table->alias, $table->title); - $table->title = $title; - $table->alias = $alias; - - // Check the row. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Store the row. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Get the new item ID - $newId = $table->get('id'); - - // Add the new ID to the array - $newIds[$pk] = $newId; - - // Now we log the old 'parent' to the new 'parent' - $parents[$oldId] = $table->id; - $count--; - } - - // Rebuild the hierarchy. - if (!$table->rebuild()) - { - $this->setError($table->getError()); - - return false; - } - - // Rebuild the tree path. - if (!$table->rebuildPath($table->id)) - { - $this->setError($table->getError()); - - return false; - } - - // Clean the cache - $this->cleanCache(); - - return $newIds; - } - - /** - * Batch move menu items to a new menu or parent. - * - * @param integer $value The new menu or sub-item. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True on success. - * - * @since 1.6 - */ - protected function batchMove($value, $pks, $contexts) - { - // $value comes as {menutype}.{parent_id} - $parts = explode('.', $value); - $menuType = $parts[0]; - $parentId = ArrayHelper::getValue($parts, 1, 0, 'int'); - - $table = $this->getTable(); - $db = $this->getDatabase(); - - // Check that the parent exists. - if ($parentId) - { - if (!$table->load($parentId)) - { - if ($error = $table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Non-fatal error - $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); - $parentId = 0; - } - } - } - - // Check that user has create and edit permission for menus - $user = Factory::getUser(); - - $menuTypeId = (int) $this->getMenuTypeId($menuType); - - if (!$user->authorise('core.create', 'com_menus.menu.' . $menuTypeId)) - { - $this->setError(Text::_('COM_MENUS_BATCH_MENU_ITEM_CANNOT_CREATE')); - - return false; - } - - if (!$user->authorise('core.edit', 'com_menus.menu.' . $menuTypeId)) - { - $this->setError(Text::_('COM_MENUS_BATCH_MENU_ITEM_CANNOT_EDIT')); - - return false; - } - - // We are going to store all the children and just moved the menutype - $children = array(); - - // Parent exists so let's proceed - foreach ($pks as $pk) - { - // Check that the row actually exists - if (!$table->load($pk)) - { - if ($error = $table->getError()) - { - // Fatal error - $this->setError($error); - - return false; - } - else - { - // Not fatal error - $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); - continue; - } - } - - // Set the new location in the tree for the node. - $table->setLocation($parentId, 'last-child'); - - // Set the new Parent Id - $table->parent_id = $parentId; - - // Check if we are moving to a different menu - if ($menuType != $table->menutype) - { - // Add the child node ids to the children array. - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('lft') . ' BETWEEN :lft AND :rgt') - ->bind(':lft', $table->lft, ParameterType::INTEGER) - ->bind(':rgt', $table->rgt, ParameterType::INTEGER); - $db->setQuery($query); - $children = array_merge($children, (array) $db->loadColumn()); - } - - // Check the row. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Store the row. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Rebuild the tree path. - if (!$table->rebuildPath()) - { - $this->setError($table->getError()); - - return false; - } - } - - // Process the child rows - if (!empty($children)) - { - // Remove any duplicates and sanitize ids. - $children = array_unique($children); - $children = ArrayHelper::toInteger($children); - - // Update the menutype field in all nodes where necessary. - $query = $db->getQuery(true) - ->update($db->quoteName('#__menu')) - ->set($db->quoteName('menutype') . ' = :menuType') - ->whereIn($db->quoteName('id'), $children) - ->bind(':menuType', $menuType); - - try - { - $db->setQuery($query); - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to check if you can save a record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function canSave($data = array(), $key = 'id') - { - return Factory::getUser()->authorise('core.edit', $this->option); - } - - /** - * Method to get the row form. - * - * @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 mixed A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // The folder and element vars are passed when saving the form. - if (empty($data)) - { - $item = $this->getItem(); - - // The type should already be set. - $this->setState('item.link', $item->link); - } - else - { - $this->setState('item.link', ArrayHelper::getValue($data, 'link')); - $this->setState('item.type', ArrayHelper::getValue($data, 'type')); - } - - $clientId = $this->getState('item.client_id'); - - // Get the form. - if ($clientId == 1) - { - $form = $this->loadForm('com_menus.item.admin', 'itemadmin', array('control' => 'jform', 'load_data' => $loadData), true); - } - else - { - $form = $this->loadForm('com_menus.item', 'item', array('control' => 'jform', 'load_data' => $loadData), true); - } - - if (empty($form)) - { - return false; - } - - if ($loadData) - { - $data = $this->loadFormData(); - } - - // Modify the form based on access controls. - if (!$this->canEditState((object) $data)) - { - // Disable fields for display. - $form->setFieldAttribute('menuordering', 'disabled', 'true'); - $form->setFieldAttribute('published', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is an article you can edit. - $form->setFieldAttribute('menuordering', 'filter', 'unset'); - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - // Filter available menus - $action = $this->getState('item.id') > 0 ? 'edit' : 'create'; - - $form->setFieldAttribute('menutype', 'accesstype', $action); - $form->setFieldAttribute('type', 'clientid', $clientId); - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data, providing it has an ID and it is the same. - $itemData = (array) $this->getItem(); - - // When a new item is requested, unset the access as it will be set later from the filter - if (empty($itemData['id'])) - { - unset($itemData['access']); - } - - $sessionData = (array) Factory::getApplication()->getUserState('com_menus.edit.item.data', array()); - - // Only merge if there is a session and itemId or itemid is null. - if (isset($sessionData['id']) && isset($itemData['id']) && $sessionData['id'] === $itemData['id'] - || is_null($itemData['id'])) - { - $data = array_merge($itemData, $sessionData); - } - else - { - $data = $itemData; - } - - // For a new menu item, pre-select some filters (Status, Language, Access) in edit form if those have been selected in Menu Manager - if (empty($data['id'])) - { - // Get selected fields - $filters = Factory::getApplication()->getUserState('com_menus.items.filter'); - $data['parent_id'] = $data['parent_id'] ?? ($filters['parent_id'] ?? null); - $data['published'] = $data['published'] ?? ($filters['published'] ?? null); - $data['language'] = $data['language'] ?? ($filters['language'] ?? null); - $data['access'] = $data['access'] ?? ($filters['access'] ?? Factory::getApplication()->get('access')); - } - - if (isset($data['menutype']) && !$this->getState('item.menutypeid')) - { - $menuTypeId = (int) $this->getMenuTypeId($data['menutype']); - - $this->setState('item.menutypeid', $menuTypeId); - } - - $data = (object) $data; - - $this->preprocessData('com_menus.item', $data); - - return $data; - } - - /** - * Get the necessary data to load an item help screen. - * - * @return object An object with key, url, and local properties for loading the item help screen. - * - * @since 1.6 - */ - public function getHelp() - { - return (object) array('key' => $this->helpKey, 'url' => $this->helpURL, 'local' => $this->helpLocal); - } - - /** - * Method to get a menu item. - * - * @param integer $pk An optional id of the object to get, otherwise the id from the model state is used. - * - * @return mixed Menu item data object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('item.id'); - - // Get a level row instance. - $table = $this->getTable(); - - // Attempt to load the row. - $table->load($pk); - - // Check for a table object error. - if ($error = $table->getError()) - { - $this->setError($error); - - return false; - } - - // Prime required properties. - - if ($type = $this->getState('item.type')) - { - $table->type = $type; - } - - if (empty($table->id)) - { - $table->parent_id = $this->getState('item.parent_id'); - $table->menutype = $this->getState('item.menutype'); - $table->client_id = $this->getState('item.client_id'); - $table->params = '{}'; - } - - // If the link has been set in the state, possibly changing link type. - if ($link = $this->getState('item.link')) - { - // Check if we are changing away from the actual link type. - if (MenusHelper::getLinkKey($table->link) !== MenusHelper::getLinkKey($link) && (int) $table->id === (int) $this->getState('item.id')) - { - $table->link = $link; - } - } - - switch ($table->type) - { - case 'alias': - case 'url': - $table->component_id = 0; - $args = array(); - - if ($table->link) - { - $q = parse_url($table->link, PHP_URL_QUERY); - - if ($q) - { - parse_str($q, $args); - } - } - - break; - - case 'separator': - case 'heading': - case 'container': - $table->link = ''; - $table->component_id = 0; - break; - - case 'component': - default: - // Enforce a valid type. - $table->type = 'component'; - - // Ensure the integrity of the component_id field is maintained, particularly when changing the menu item type. - $args = []; - - if ($table->link) - { - $q = parse_url($table->link, PHP_URL_QUERY); - - if ($q) - { - parse_str($q, $args); - } - } - - if (isset($args['option'])) - { - // Load the language file for the component. - $lang = Factory::getLanguage(); - $lang->load($args['option'], JPATH_ADMINISTRATOR) - || $lang->load($args['option'], JPATH_ADMINISTRATOR . '/components/' . $args['option']); - - // Determine the component id. - $component = ComponentHelper::getComponent($args['option']); - - if (isset($component->id)) - { - $table->component_id = $component->id; - } - } - break; - } - - // We have a valid type, inject it into the state for forms to use. - $this->setState('item.type', $table->type); - - // Convert to the \Joomla\CMS\Object\CMSObject before adding the params. - $properties = $table->getProperties(1); - $result = ArrayHelper::toObject($properties); - - // Convert the params field to an array. - $registry = new Registry($table->params); - $result->params = $registry->toArray(); - - // Merge the request arguments in to the params for a component. - if ($table->type == 'component') - { - // Note that all request arguments become reserved parameter names. - $result->request = $args; - $result->params = array_merge($result->params, $args); - - // Special case for the Login menu item. - // Display the login or logout redirect URL fields if not empty - if ($table->link == 'index.php?option=com_users&view=login') - { - if (!empty($result->params['login_redirect_url'])) - { - $result->params['loginredirectchoice'] = '0'; - } - - if (!empty($result->params['logout_redirect_url'])) - { - $result->params['logoutredirectchoice'] = '0'; - } - } - } - - if ($table->type == 'alias') - { - // Note that all request arguments become reserved parameter names. - $result->params = array_merge($result->params, $args); - } - - if ($table->type == 'url') - { - // Note that all request arguments become reserved parameter names. - $result->params = array_merge($result->params, $args); - } - - // Load associated menu items, only supported for frontend for now - if ($this->getState('item.client_id') == 0 && Associations::isEnabled()) - { - if ($pk != null) - { - $result->associations = MenusHelper::getAssociations($pk); - } - else - { - $result->associations = array(); - } - } - - $result->menuordering = $pk; - - return $result; - } - - /** - * Get the list of modules not in trash. - * - * @return mixed An array of module records (id, title, position), or false on error. - * - * @since 1.6 - */ - public function getModules() - { - $clientId = (int) $this->getState('item.client_id'); - $id = (int) $this->getState('item.id'); - - // Currently any setting that affects target page for a backend menu is not supported, hence load no modules. - if ($clientId == 1) - { - return false; - } - - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - /** - * Join on the module-to-menu mapping table. - * We are only interested if the module is displayed on ALL or THIS menu item (or the inverse ID number). - * sqlsrv changes for modulelink to menu manager - */ - $query->select( - [ - $db->quoteName('a.id'), - $db->quoteName('a.title'), - $db->quoteName('a.position'), - $db->quoteName('a.published'), - $db->quoteName('map.menuid'), - ] - ) - ->from($db->quoteName('#__modules', 'a')) - ->join( - 'LEFT', - $db->quoteName('#__modules_menu', 'map'), - $db->quoteName('map.moduleid') . ' = ' . $db->quoteName('a.id') - . ' AND ' . $db->quoteName('map.menuid') . ' IN (' . implode(',', $query->bindArray([0, $id, -$id])) . ')' - ); - - $subQuery = $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName('#__modules_menu')) - ->where( - [ - $db->quoteName('moduleid') . ' = ' . $db->quoteName('a.id'), - $db->quoteName('menuid') . ' < 0', - ] - ); - - $query->select('(' . $subQuery . ') AS ' . $db->quoteName('except')); - - // Join on the asset groups table. - $query->select($db->quoteName('ag.title', 'access_title')) - ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) - ->where( - [ - $db->quoteName('a.published') . ' >= 0', - $db->quoteName('a.client_id') . ' = :clientId', - ] - ) - ->bind(':clientId', $clientId, ParameterType::INTEGER) - ->order( - [ - $db->quoteName('a.position'), - $db->quoteName('a.ordering'), - ] - ); - - $db->setQuery($query); - - try - { - $result = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return $result; - } - - /** - * Get the list of all view levels - * - * @return \stdClass[]|boolean An array of all view levels (id, title). - * - * @since 3.4 - */ - public function getViewLevels() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Get all the available view levels - $query->select($db->quoteName('id')) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__viewlevels')) - ->order($db->quoteName('id')); - - $db->setQuery($query); - - try - { - $result = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return $result; - } - - /** - * Returns a Table object, always creating it - * - * @param string $type The table type to instantiate. - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return \Joomla\Cms\Table\Table|\Joomla\Cms\Table\Nested A database object. - * - * @since 1.6 - */ - public function getTable($type = 'Menu', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } - - /** - * A protected method to get the where clause for the reorder. - * This ensures that the row will be moved relative to a row with the same menutype. - * - * @param \Joomla\CMS\Table\Menu $table - * - * @return array An array of conditions to add to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - $db = $this->getDatabase(); - - return [ - $db->quoteName('menutype') . ' = ' . $db->quote($table->menutype), - ]; - } - - /** - * Auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - // Load the User state. - $pk = $app->input->getInt('id'); - $this->setState('item.id', $pk); - - if (!$app->isClient('api')) - { - $parentId = $app->getUserState('com_menus.edit.item.parent_id'); - $menuType = $app->getUserStateFromRequest('com_menus.items.menutype', 'menutype', '', 'string'); - } - else - { - $parentId = null; - $menuType = $app->input->get('com_menus.items.menutype'); - } - - if (!$parentId) - { - $parentId = $app->input->getInt('parent_id'); - } - - $this->setState('item.parent_id', $parentId); - - // If we have a menutype we take client_id from there, unless forced otherwise - if ($menuType) - { - $menuTypeObj = $this->getMenuType($menuType); - - // An invalid menutype will be handled as clientId = 0 and menuType = '' - $menuType = (string) $menuTypeObj->menutype; - $menuTypeId = (int) $menuTypeObj->client_id; - $clientId = (int) $menuTypeObj->client_id; - } - else - { - $menuTypeId = 0; - $clientId = $app->isClient('api') ? $app->input->get('client_id') : - $app->getUserState('com_menus.items.client_id', 0); - } - - // Forced client id will override/clear menuType if conflicted - $forcedClientId = $app->input->get('client_id', null, 'string'); - - if (!$app->isClient('api')) - { - // Set the menu type and client id on the list view state, so we return to this menu after saving. - $app->setUserState('com_menus.items.menutype', $menuType); - $app->setUserState('com_menus.items.client_id', $clientId); - } - - // Current item if not new, we don't allow changing client id at all - if ($pk) - { - $table = $this->getTable(); - $table->load($pk); - $forcedClientId = $table->get('client_id', $forcedClientId); - } - - if (isset($forcedClientId) && $forcedClientId != $clientId) - { - $clientId = $forcedClientId; - $menuType = ''; - $menuTypeId = 0; - } - - $this->setState('item.menutype', $menuType); - $this->setState('item.client_id', $clientId); - $this->setState('item.menutypeid', $menuTypeId); - - if (!($type = $app->getUserState('com_menus.edit.item.type'))) - { - $type = $app->input->get('type'); - - /** - * Note: a new menu item will have no field type. - * The field is required so the user has to change it. - */ - } - - $this->setState('item.type', $type); - - $link = $app->isClient('api') ? $app->input->get('link') : - $app->getUserState('com_menus.edit.item.link'); - - if ($link) - { - $this->setState('item.link', $link); - } - - // Load the parameters. - $params = ComponentHelper::getParams('com_menus'); - $this->setState('params', $params); - } - - /** - * Loads the menutype object by a given menutype string - * - * @param string $menutype The given menutype - * - * @return \stdClass - * - * @since 3.7.0 - */ - protected function getMenuType($menutype) - { - $table = $this->getTable('MenuType'); - - $table->load(array('menutype' => $menutype)); - - return (object) $table->getProperties(); - } - - /** - * Loads the menutype ID by a given menutype string - * - * @param string $menutype The given menutype - * - * @return integer - * - * @since 3.6 - */ - protected function getMenuTypeId($menutype) - { - $menu = $this->getMenuType($menutype); - - return (int) $menu->id; - } - - /** - * Method to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import. - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $link = $this->getState('item.link'); - $type = $this->getState('item.type'); - $clientId = $this->getState('item.client_id'); - $formFile = false; - - // Load the specific type file - $typeFile = $clientId == 1 ? 'itemadmin_' . $type : 'item_' . $type; - $clientInfo = ApplicationHelper::getClientInfo($clientId); - - // Initialise form with component view params if available. - if ($type == 'component') - { - $link = $link ? htmlspecialchars_decode($link) : ''; - - // Parse the link arguments. - $args = []; - - if ($link) - { - parse_str(parse_url(htmlspecialchars_decode($link), PHP_URL_QUERY), $args); - } - - // Confirm that the option is defined. - $option = ''; - $base = ''; - - if (isset($args['option'])) - { - // The option determines the base path to work with. - $option = $args['option']; - $base = $clientInfo->path . '/components/' . $option; - } - - if (isset($args['view'])) - { - $view = $args['view']; - - // Determine the layout to search for. - if (isset($args['layout'])) - { - $layout = $args['layout']; - } - else - { - $layout = 'default'; - } - - // Check for the layout XML file. Use standard xml file if it exists. - $tplFolders = array( - $base . '/tmpl/' . $view, - $base . '/views/' . $view . '/tmpl', - $base . '/view/' . $view . '/tmpl', - ); - $path = Path::find($tplFolders, $layout . '.xml'); - - if (is_file($path)) - { - $formFile = $path; - } - - // If custom layout, get the xml file from the template folder - // template folder is first part of file name -- template:folder - if (!$formFile && (strpos($layout, ':') > 0)) - { - list($altTmpl, $altLayout) = explode(':', $layout); - - $templatePath = Path::clean($clientInfo->path . '/templates/' . $altTmpl . '/html/' . $option . '/' . $view . '/' . $altLayout . '.xml'); - - if (is_file($templatePath)) - { - $formFile = $templatePath; - } - } - } - - // Now check for a view manifest file - if (!$formFile) - { - if (isset($view)) - { - $metadataFolders = array( - $base . '/view/' . $view, - $base . '/views/' . $view - ); - $metaPath = Path::find($metadataFolders, 'metadata.xml'); - - if (is_file($path = Path::clean($metaPath))) - { - $formFile = $path; - } - } - elseif ($base) - { - // Now check for a component manifest file - $path = Path::clean($base . '/metadata.xml'); - - if (is_file($path)) - { - $formFile = $path; - } - } - } - } - - if ($formFile) - { - // If an XML file was found in the component, load it first. - // We need to qualify the full path to avoid collisions with component file names. - - if ($form->loadFile($formFile, true, '/metadata') == false) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Attempt to load the xml file. - if (!$xml = simplexml_load_file($formFile)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Get the help data from the XML file if present. - $help = $xml->xpath('/metadata/layout/help'); - } - else - { - // We don't have a component. Load the form XML to get the help path - $xmlFile = Path::find(JPATH_ADMINISTRATOR . '/components/com_menus/forms', $typeFile . '.xml'); - - if ($xmlFile) - { - if (!$xml = simplexml_load_file($xmlFile)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Get the help data from the XML file if present. - $help = $xml->xpath('/form/help'); - } - } - - if (!empty($help)) - { - $helpKey = trim((string) $help[0]['key']); - $helpURL = trim((string) $help[0]['url']); - $helpLoc = trim((string) $help[0]['local']); - - $this->helpKey = $helpKey ?: $this->helpKey; - $this->helpURL = $helpURL ?: $this->helpURL; - $this->helpLocal = (($helpLoc == 'true') || ($helpLoc == '1') || ($helpLoc == 'local')); - } - - if (!$form->loadFile($typeFile, true, false)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Association menu items, we currently do not support this for admin menu… may be later - if ($clientId == 0 && Associations::isEnabled()) - { - $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); - - if (count($languages) > 1) - { - $addform = new \SimpleXMLElement('
'); - $fields = $addform->addChild('fields'); - $fields->addAttribute('name', 'associations'); - $fieldset = $fields->addChild('fieldset'); - $fieldset->addAttribute('name', 'item_associations'); - $fieldset->addAttribute('addfieldprefix', 'Joomla\Component\Menus\Administrator\Field'); - - foreach ($languages as $language) - { - $field = $fieldset->addChild('field'); - $field->addAttribute('name', $language->lang_code); - $field->addAttribute('type', 'modal_menu'); - $field->addAttribute('language', $language->lang_code); - $field->addAttribute('label', $language->title); - $field->addAttribute('translate_label', 'false'); - $field->addAttribute('select', 'true'); - $field->addAttribute('new', 'true'); - $field->addAttribute('edit', 'true'); - $field->addAttribute('clear', 'true'); - $field->addAttribute('propagate', 'true'); - $option = $field->addChild('option', 'COM_MENUS_ITEM_FIELD_ASSOCIATION_NO_VALUE'); - $option->addAttribute('value', ''); - } - - $form->load($addform, false); - } - } - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } - - /** - * Method rebuild the entire nested set tree. - * - * @return boolean Boolean true on success, boolean false - * - * @since 1.6 - */ - public function rebuild() - { - // Initialise variables. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $table = $this->getTable(); - - try - { - $rebuildResult = $table->rebuild(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (!$rebuildResult) - { - $this->setError($table->getError()); - - return false; - } - - $query->select( - [ - $db->quoteName('id'), - $db->quoteName('params'), - ] - ) - ->from($db->quoteName('#__menu')) - ->where( - [ - $db->quoteName('params') . ' NOT LIKE ' . $db->quote('{%'), - $db->quoteName('params') . ' <> ' . $db->quote(''), - ] - ); - $db->setQuery($query); - - try - { - $items = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - $query = $db->getQuery(true) - ->update($db->quoteName('#__menu')) - ->set($db->quoteName('params') . ' = :params') - ->where($db->quoteName('id') . ' = :id') - ->bind(':params', $params) - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - - foreach ($items as &$item) - { - // Update query parameters. - $id = $item->id; - $params = new Registry($item->params); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $pk = isset($data['id']) ? $data['id'] : (int) $this->getState('item.id'); - $isNew = true; - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $table = $this->getTable(); - $context = $this->option . '.' . $this->name; - - // Include the plugins for the on save events. - PluginHelper::importPlugin($this->events_map['save']); - - // Load the row if saving an existing item. - if ($pk > 0) - { - $table->load($pk); - $isNew = false; - } - - if (!$isNew) - { - if ($table->parent_id == $data['parent_id']) - { - // If first is chosen make the item the first child of the selected parent. - if ($data['menuordering'] == -1) - { - $table->setLocation($data['parent_id'], 'first-child'); - } - // If last is chosen make it the last child of the selected parent. - elseif ($data['menuordering'] == -2) - { - $table->setLocation($data['parent_id'], 'last-child'); - } - // Don't try to put an item after itself. All other ones put after the selected item. - // $data['id'] is empty means it's a save as copy - elseif ($data['menuordering'] && $table->id != $data['menuordering'] || empty($data['id'])) - { - $table->setLocation($data['menuordering'], 'after'); - } - // \Just leave it where it is if no change is made. - elseif ($data['menuordering'] && $table->id == $data['menuordering']) - { - unset($data['menuordering']); - } - } - // Set the new parent id if parent id not matched and put in last position - else - { - $table->setLocation($data['parent_id'], 'last-child'); - } - - // Check if we are moving to a different menu - if ($data['menutype'] != $table->menutype) - { - // Add the child node ids to the children array. - $query->clear() - ->select($db->quoteName('id')) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('lft') . ' BETWEEN ' . (int) $table->lft . ' AND ' . (int) $table->rgt); - $db->setQuery($query); - $children = (array) $db->loadColumn(); - } - } - // We have a new item, so it is not a change. - else - { - $menuType = $this->getMenuType($data['menutype']); - - $data['client_id'] = $menuType->client_id; - - $table->setLocation($data['parent_id'], 'last-child'); - } - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Alter the title & alias for save2copy when required. Also, unset the home record. - if (Factory::getApplication()->input->get('task') === 'save2copy' && $data['id'] === 0) - { - $origTable = $this->getTable(); - $origTable->load($this->getState('item.id')); - - if ($table->title === $origTable->title) - { - list($title, $alias) = $this->generateNewTitle($table->parent_id, $table->alias, $table->title); - $table->title = $title; - $table->alias = $alias; - } - - if ($table->alias === $origTable->alias) - { - $table->alias = ''; - } - - $table->published = 0; - $table->home = 0; - } - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, $isNew, $data)); - - // Store the data. - if (in_array(false, $result, true)|| !$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the after save event. - Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, $isNew)); - - // Rebuild the tree path. - if (!$table->rebuildPath($table->id)) - { - $this->setError($table->getError()); - - return false; - } - - // Process the child rows - if (!empty($children)) - { - // Remove any duplicates and sanitize ids. - $children = array_unique($children); - $children = ArrayHelper::toInteger($children); - - // Update the menutype field in all nodes where necessary. - $query = $db->getQuery(true) - ->update($db->quoteName('#__menu')) - ->set($db->quoteName('menutype') . ' = :menutype') - ->whereIn($db->quoteName('id'), $children) - ->bind(':menutype', $data['menutype']); - - try - { - $db->setQuery($query); - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - $this->setState('item.id', $table->id); - $this->setState('item.menutype', $table->menutype); - - // Load associated menu items, for now not supported for admin menu… may be later - if ($table->get('client_id') == 0 && Associations::isEnabled()) - { - // Adding self to the association - $associations = isset($data['associations']) ? $data['associations'] : array(); - - // Unset any invalid associations - $associations = ArrayHelper::toInteger($associations); - - foreach ($associations as $tag => $id) - { - if (!$id) - { - unset($associations[$tag]); - } - } - - // Detecting all item menus - $all_language = $table->language == '*'; - - if ($all_language && !empty($associations)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_MENUS_ERROR_ALL_LANGUAGE_ASSOCIATED'), 'notice'); - } - - // Get associationskey for edited item - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('key')) - ->from($db->quoteName('#__associations')) - ->where( - [ - $db->quoteName('context') . ' = :context', - $db->quoteName('id') . ' = :id', - ] - ) - ->bind(':context', $this->associationsContext) - ->bind(':id', $table->id, ParameterType::INTEGER); - $db->setQuery($query); - $oldKey = $db->loadResult(); - - if ($associations || $oldKey !== null) - { - // Deleting old associations for the associated items - $where = []; - $query = $db->getQuery(true) - ->delete($db->quoteName('#__associations')) - ->where($db->quoteName('context') . ' = :context') - ->bind(':context', $this->associationsContext); - - if ($associations) - { - $where[] = $db->quoteName('id') . ' IN (' . implode(',', $query->bindArray(array_values($associations))) . ')'; - } - - if ($oldKey !== null) - { - $where[] = $db->quoteName('key') . ' = :oldKey'; - $query->bind(':oldKey', $oldKey); - } - - $query->extendWhere('AND', $where, 'OR'); - - try - { - $db->setQuery($query); - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - // Adding self to the association - if (!$all_language) - { - $associations[$table->language] = (int) $table->id; - } - - if (count($associations) > 1) - { - // Adding new association for these items - $key = md5(json_encode($associations)); - $query = $db->getQuery(true) - ->insert($db->quoteName('#__associations')) - ->columns( - [ - $db->quoteName('id'), - $db->quoteName('context'), - $db->quoteName('key'), - ] - ); - - foreach ($associations as $id) - { - $query->values( - implode( - ',', - $query->bindArray( - [$id, $this->associationsContext, $key], - [ParameterType::INTEGER, ParameterType::STRING, ParameterType::STRING] - ) - ) - ); - } - - try - { - $db->setQuery($query); - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - } - - // Clean the cache - $this->cleanCache(); - - if (isset($data['link'])) - { - $base = Uri::base(); - $juri = Uri::getInstance($base . $data['link']); - $option = $juri->getVar('option'); - - // Clean the cache - parent::cleanCache($option); - } - - if (Factory::getApplication()->input->get('task') === 'editAssociations') - { - return $this->redirectToAssociations($data); - } - - return true; - } - - /** - * Method to save the reordered nested set tree. - * First we save the new order values in the lft values of the changed ids. - * Then we invoke the table rebuild to implement the new ordering. - * - * @param array $idArray Rows identifiers to be reordered - * @param array $lftArray lft values of rows to be reordered - * - * @return boolean false on failure or error, true otherwise. - * - * @since 1.6 - */ - public function saveorder($idArray = null, $lftArray = null) - { - // Get an instance of the table object. - $table = $this->getTable(); - - if (!$table->saveorder($idArray, $lftArray)) - { - $this->setError($table->getError()); - - return false; - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to change the home state of one or more items. - * - * @param array $pks A list of the primary keys to change. - * @param integer $value The value of the home state. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function setHome(&$pks, $value = 1) - { - $table = $this->getTable(); - $pks = (array) $pks; - - $languages = array(); - $onehome = false; - - // Remember that we can set a home page for different languages, - // so we need to loop through the primary key array. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - if (!array_key_exists($table->language, $languages)) - { - $languages[$table->language] = true; - - if ($table->home == $value) - { - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('COM_MENUS_ERROR_ALREADY_HOME'), 'notice'); - } - elseif ($table->menutype == 'main') - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('COM_MENUS_ERROR_MENUTYPE_HOME'), 'error'); - } - else - { - $table->home = $value; - - if ($table->language == '*') - { - $table->published = 1; - } - - if (!$this->canSave($table)) - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - } - elseif (!$table->check()) - { - // Prune the items that failed pre-save checks. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage($table->getError(), 'error'); - } - elseif (!$table->store()) - { - // Prune the items that could not be stored. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage($table->getError(), 'error'); - } - } - } - else - { - unset($pks[$i]); - - if (!$onehome) - { - $onehome = true; - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_MENUS_ERROR_ONE_HOME'), 'notice'); - } - } - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to change the published state of one or more records. - * - * @param array $pks A list of the primary keys to change. - * @param integer $value The value of the published state. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function publish(&$pks, $value = 1) - { - $table = $this->getTable(); - $pks = (array) $pks; - - // Default menu item existence checks. - if ($value != 1) - { - foreach ($pks as $i => $pk) - { - if ($table->load($pk) && $table->home && $table->language == '*') - { - // Prune items that you can't change. - Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_MENU_UNPUBLISH_DEFAULT_HOME'), 'error'); - unset($pks[$i]); - break; - } - } - } - - // Clean the cache - $this->cleanCache(); - - // Ensure that previous checks doesn't empty the array - if (empty($pks)) - { - return true; - } - - return parent::publish($pks, $value); - } - - /** - * Method to change the title & alias. - * - * @param integer $parentId The id of the parent. - * @param string $alias The alias. - * @param string $title The title. - * - * @return array Contains the modified title and alias. - * - * @since 1.6 - */ - protected function generateNewTitle($parentId, $alias, $title) - { - // Alter the title & alias - $table = $this->getTable(); - - while ($table->load(array('alias' => $alias, 'parent_id' => $parentId))) - { - if ($title == $table->title) - { - $title = StringHelper::increment($title); - } - - $alias = StringHelper::increment($alias, 'dash'); - } - - return array($title, $alias); - } - - /** - * Custom clean the cache - * - * @param string $group Cache group name. - * @param integer $clientId @deprecated 5.0 No Longer Used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('com_menus'); - parent::cleanCache('com_modules'); - parent::cleanCache('mod_menu'); - } + /** + * The type alias for this content type. + * + * @var string + * @since 3.4 + */ + public $typeAlias = 'com_menus.item'; + + /** + * The context used for the associations table + * + * @var string + * @since 3.4.4 + */ + protected $associationsContext = 'com_menus.item'; + + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_MENUS_ITEM'; + + /** + * @var string The help screen key for the menu item. + * @since 1.6 + */ + protected $helpKey = 'Menu_Item:_New_Item'; + + /** + * @var string The help screen base URL for the menu item. + * @since 1.6 + */ + protected $helpURL; + + /** + * @var boolean True to use local lookup for the help screen. + * @since 1.6 + */ + protected $helpLocal = false; + + /** + * Batch copy/move command. If set to false, + * the batch copy/move command is not supported + * + * @var string + */ + protected $batch_copymove = 'menu_id'; + + /** + * Allowed batch commands + * + * @var array + */ + protected $batch_commands = array( + 'assetgroup_id' => 'batchAccess', + 'language_id' => 'batchLanguage' + ); + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->published != -2) { + return false; + } + + $menuTypeId = 0; + + if (!empty($record->menutype)) { + $menuTypeId = $this->getMenuTypeId($record->menutype); + } + + return Factory::getUser()->authorise('core.delete', 'com_menus.menu.' . (int) $menuTypeId); + } + + /** + * Method to test whether the state of a record can be edited. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission for the component. + * + * @since 3.6 + */ + protected function canEditState($record) + { + $menuTypeId = !empty($record->menutype) ? $this->getMenuTypeId($record->menutype) : 0; + $assetKey = $menuTypeId ? 'com_menus.menu.' . (int) $menuTypeId : 'com_menus'; + + return Factory::getUser()->authorise('core.edit.state', $assetKey); + } + + /** + * Batch copy menu items to a new menu or parent. + * + * @param integer $value The new menu or sub-item. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return mixed An array of new IDs on success, boolean false on failure. + * + * @since 1.6 + */ + protected function batchCopy($value, $pks, $contexts) + { + // $value comes as {menutype}.{parent_id} + $parts = explode('.', $value); + $menuType = $parts[0]; + $parentId = ArrayHelper::getValue($parts, 1, 0, 'int'); + + $table = $this->getTable(); + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $newIds = array(); + + // Check that the parent exists + if ($parentId) { + if (!$table->load($parentId)) { + if ($error = $table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Non-fatal error + $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); + $parentId = 0; + } + } + } + + // If the parent is 0, set it to the ID of the root item in the tree + if (empty($parentId)) { + if (!$parentId = $table->getRootId()) { + $this->setError($table->getError()); + + return false; + } + } + + // Check that user has create permission for menus + $user = Factory::getUser(); + + $menuTypeId = (int) $this->getMenuTypeId($menuType); + + if (!$user->authorise('core.create', 'com_menus.menu.' . $menuTypeId)) { + $this->setError(Text::_('COM_MENUS_BATCH_MENU_ITEM_CANNOT_CREATE')); + + return false; + } + + // We need to log the parent ID + $parents = array(); + + // Calculate the emergency stop count as a precaution against a runaway loop bug + $query->select('COUNT(' . $db->quoteName('id') . ')') + ->from($db->quoteName('#__menu')); + $db->setQuery($query); + + try { + $count = $db->loadResult(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Parent exists so let's proceed + while (!empty($pks) && $count > 0) { + // Pop the first id off the stack + $pk = array_shift($pks); + + $table->reset(); + + // Check that the row actually exists + if (!$table->load($pk)) { + if ($error = $table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Not fatal error + $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); + continue; + } + } + + // Copy is a bit tricky, because we also need to copy the children + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__menu')) + ->where( + [ + $db->quoteName('lft') . ' > :lft', + $db->quoteName('rgt') . ' < :rgt', + ] + ) + ->bind(':lft', $table->lft, ParameterType::INTEGER) + ->bind(':rgt', $table->rgt, ParameterType::INTEGER); + $db->setQuery($query); + $childIds = $db->loadColumn(); + + // Add child IDs to the array only if they aren't already there. + foreach ($childIds as $childId) { + if (!in_array($childId, $pks)) { + $pks[] = $childId; + } + } + + // Make a copy of the old ID and Parent ID + $oldId = $table->id; + $oldParentId = $table->parent_id; + + // Reset the id because we are making a copy. + $table->id = 0; + + // If we a copying children, the Old ID will turn up in the parents list + // otherwise it's a new top level item + $table->parent_id = isset($parents[$oldParentId]) ? $parents[$oldParentId] : $parentId; + $table->menutype = $menuType; + + // Set the new location in the tree for the node. + $table->setLocation($table->parent_id, 'last-child'); + + // @todo: Deal with ordering? + // $table->ordering = 1; + $table->level = null; + $table->lft = null; + $table->rgt = null; + $table->home = 0; + + // Alter the title & alias + list($title, $alias) = $this->generateNewTitle($table->parent_id, $table->alias, $table->title); + $table->title = $title; + $table->alias = $alias; + + // Check the row. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Store the row. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Get the new item ID + $newId = $table->get('id'); + + // Add the new ID to the array + $newIds[$pk] = $newId; + + // Now we log the old 'parent' to the new 'parent' + $parents[$oldId] = $table->id; + $count--; + } + + // Rebuild the hierarchy. + if (!$table->rebuild()) { + $this->setError($table->getError()); + + return false; + } + + // Rebuild the tree path. + if (!$table->rebuildPath($table->id)) { + $this->setError($table->getError()); + + return false; + } + + // Clean the cache + $this->cleanCache(); + + return $newIds; + } + + /** + * Batch move menu items to a new menu or parent. + * + * @param integer $value The new menu or sub-item. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True on success. + * + * @since 1.6 + */ + protected function batchMove($value, $pks, $contexts) + { + // $value comes as {menutype}.{parent_id} + $parts = explode('.', $value); + $menuType = $parts[0]; + $parentId = ArrayHelper::getValue($parts, 1, 0, 'int'); + + $table = $this->getTable(); + $db = $this->getDatabase(); + + // Check that the parent exists. + if ($parentId) { + if (!$table->load($parentId)) { + if ($error = $table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Non-fatal error + $this->setError(Text::_('JGLOBAL_BATCH_MOVE_PARENT_NOT_FOUND')); + $parentId = 0; + } + } + } + + // Check that user has create and edit permission for menus + $user = Factory::getUser(); + + $menuTypeId = (int) $this->getMenuTypeId($menuType); + + if (!$user->authorise('core.create', 'com_menus.menu.' . $menuTypeId)) { + $this->setError(Text::_('COM_MENUS_BATCH_MENU_ITEM_CANNOT_CREATE')); + + return false; + } + + if (!$user->authorise('core.edit', 'com_menus.menu.' . $menuTypeId)) { + $this->setError(Text::_('COM_MENUS_BATCH_MENU_ITEM_CANNOT_EDIT')); + + return false; + } + + // We are going to store all the children and just moved the menutype + $children = array(); + + // Parent exists so let's proceed + foreach ($pks as $pk) { + // Check that the row actually exists + if (!$table->load($pk)) { + if ($error = $table->getError()) { + // Fatal error + $this->setError($error); + + return false; + } else { + // Not fatal error + $this->setError(Text::sprintf('JGLOBAL_BATCH_MOVE_ROW_NOT_FOUND', $pk)); + continue; + } + } + + // Set the new location in the tree for the node. + $table->setLocation($parentId, 'last-child'); + + // Set the new Parent Id + $table->parent_id = $parentId; + + // Check if we are moving to a different menu + if ($menuType != $table->menutype) { + // Add the child node ids to the children array. + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('lft') . ' BETWEEN :lft AND :rgt') + ->bind(':lft', $table->lft, ParameterType::INTEGER) + ->bind(':rgt', $table->rgt, ParameterType::INTEGER); + $db->setQuery($query); + $children = array_merge($children, (array) $db->loadColumn()); + } + + // Check the row. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Store the row. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Rebuild the tree path. + if (!$table->rebuildPath()) { + $this->setError($table->getError()); + + return false; + } + } + + // Process the child rows + if (!empty($children)) { + // Remove any duplicates and sanitize ids. + $children = array_unique($children); + $children = ArrayHelper::toInteger($children); + + // Update the menutype field in all nodes where necessary. + $query = $db->getQuery(true) + ->update($db->quoteName('#__menu')) + ->set($db->quoteName('menutype') . ' = :menuType') + ->whereIn($db->quoteName('id'), $children) + ->bind(':menuType', $menuType); + + try { + $db->setQuery($query); + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to check if you can save a record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function canSave($data = array(), $key = 'id') + { + return Factory::getUser()->authorise('core.edit', $this->option); + } + + /** + * Method to get the row form. + * + * @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 mixed A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // The folder and element vars are passed when saving the form. + if (empty($data)) { + $item = $this->getItem(); + + // The type should already be set. + $this->setState('item.link', $item->link); + } else { + $this->setState('item.link', ArrayHelper::getValue($data, 'link')); + $this->setState('item.type', ArrayHelper::getValue($data, 'type')); + } + + $clientId = $this->getState('item.client_id'); + + // Get the form. + if ($clientId == 1) { + $form = $this->loadForm('com_menus.item.admin', 'itemadmin', array('control' => 'jform', 'load_data' => $loadData), true); + } else { + $form = $this->loadForm('com_menus.item', 'item', array('control' => 'jform', 'load_data' => $loadData), true); + } + + if (empty($form)) { + return false; + } + + if ($loadData) { + $data = $this->loadFormData(); + } + + // Modify the form based on access controls. + if (!$this->canEditState((object) $data)) { + // Disable fields for display. + $form->setFieldAttribute('menuordering', 'disabled', 'true'); + $form->setFieldAttribute('published', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is an article you can edit. + $form->setFieldAttribute('menuordering', 'filter', 'unset'); + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + // Filter available menus + $action = $this->getState('item.id') > 0 ? 'edit' : 'create'; + + $form->setFieldAttribute('menutype', 'accesstype', $action); + $form->setFieldAttribute('type', 'clientid', $clientId); + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data, providing it has an ID and it is the same. + $itemData = (array) $this->getItem(); + + // When a new item is requested, unset the access as it will be set later from the filter + if (empty($itemData['id'])) { + unset($itemData['access']); + } + + $sessionData = (array) Factory::getApplication()->getUserState('com_menus.edit.item.data', array()); + + // Only merge if there is a session and itemId or itemid is null. + if ( + isset($sessionData['id']) && isset($itemData['id']) && $sessionData['id'] === $itemData['id'] + || is_null($itemData['id']) + ) { + $data = array_merge($itemData, $sessionData); + } else { + $data = $itemData; + } + + // For a new menu item, pre-select some filters (Status, Language, Access) in edit form if those have been selected in Menu Manager + if (empty($data['id'])) { + // Get selected fields + $filters = Factory::getApplication()->getUserState('com_menus.items.filter'); + $data['parent_id'] = $data['parent_id'] ?? ($filters['parent_id'] ?? null); + $data['published'] = $data['published'] ?? ($filters['published'] ?? null); + $data['language'] = $data['language'] ?? ($filters['language'] ?? null); + $data['access'] = $data['access'] ?? ($filters['access'] ?? Factory::getApplication()->get('access')); + } + + if (isset($data['menutype']) && !$this->getState('item.menutypeid')) { + $menuTypeId = (int) $this->getMenuTypeId($data['menutype']); + + $this->setState('item.menutypeid', $menuTypeId); + } + + $data = (object) $data; + + $this->preprocessData('com_menus.item', $data); + + return $data; + } + + /** + * Get the necessary data to load an item help screen. + * + * @return object An object with key, url, and local properties for loading the item help screen. + * + * @since 1.6 + */ + public function getHelp() + { + return (object) array('key' => $this->helpKey, 'url' => $this->helpURL, 'local' => $this->helpLocal); + } + + /** + * Method to get a menu item. + * + * @param integer $pk An optional id of the object to get, otherwise the id from the model state is used. + * + * @return mixed Menu item data object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('item.id'); + + // Get a level row instance. + $table = $this->getTable(); + + // Attempt to load the row. + $table->load($pk); + + // Check for a table object error. + if ($error = $table->getError()) { + $this->setError($error); + + return false; + } + + // Prime required properties. + + if ($type = $this->getState('item.type')) { + $table->type = $type; + } + + if (empty($table->id)) { + $table->parent_id = $this->getState('item.parent_id'); + $table->menutype = $this->getState('item.menutype'); + $table->client_id = $this->getState('item.client_id'); + $table->params = '{}'; + } + + // If the link has been set in the state, possibly changing link type. + if ($link = $this->getState('item.link')) { + // Check if we are changing away from the actual link type. + if (MenusHelper::getLinkKey($table->link) !== MenusHelper::getLinkKey($link) && (int) $table->id === (int) $this->getState('item.id')) { + $table->link = $link; + } + } + + switch ($table->type) { + case 'alias': + case 'url': + $table->component_id = 0; + $args = array(); + + if ($table->link) { + $q = parse_url($table->link, PHP_URL_QUERY); + + if ($q) { + parse_str($q, $args); + } + } + + break; + + case 'separator': + case 'heading': + case 'container': + $table->link = ''; + $table->component_id = 0; + break; + + case 'component': + default: + // Enforce a valid type. + $table->type = 'component'; + + // Ensure the integrity of the component_id field is maintained, particularly when changing the menu item type. + $args = []; + + if ($table->link) { + $q = parse_url($table->link, PHP_URL_QUERY); + + if ($q) { + parse_str($q, $args); + } + } + + if (isset($args['option'])) { + // Load the language file for the component. + $lang = Factory::getLanguage(); + $lang->load($args['option'], JPATH_ADMINISTRATOR) + || $lang->load($args['option'], JPATH_ADMINISTRATOR . '/components/' . $args['option']); + + // Determine the component id. + $component = ComponentHelper::getComponent($args['option']); + + if (isset($component->id)) { + $table->component_id = $component->id; + } + } + break; + } + + // We have a valid type, inject it into the state for forms to use. + $this->setState('item.type', $table->type); + + // Convert to the \Joomla\CMS\Object\CMSObject before adding the params. + $properties = $table->getProperties(1); + $result = ArrayHelper::toObject($properties); + + // Convert the params field to an array. + $registry = new Registry($table->params); + $result->params = $registry->toArray(); + + // Merge the request arguments in to the params for a component. + if ($table->type == 'component') { + // Note that all request arguments become reserved parameter names. + $result->request = $args; + $result->params = array_merge($result->params, $args); + + // Special case for the Login menu item. + // Display the login or logout redirect URL fields if not empty + if ($table->link == 'index.php?option=com_users&view=login') { + if (!empty($result->params['login_redirect_url'])) { + $result->params['loginredirectchoice'] = '0'; + } + + if (!empty($result->params['logout_redirect_url'])) { + $result->params['logoutredirectchoice'] = '0'; + } + } + } + + if ($table->type == 'alias') { + // Note that all request arguments become reserved parameter names. + $result->params = array_merge($result->params, $args); + } + + if ($table->type == 'url') { + // Note that all request arguments become reserved parameter names. + $result->params = array_merge($result->params, $args); + } + + // Load associated menu items, only supported for frontend for now + if ($this->getState('item.client_id') == 0 && Associations::isEnabled()) { + if ($pk != null) { + $result->associations = MenusHelper::getAssociations($pk); + } else { + $result->associations = array(); + } + } + + $result->menuordering = $pk; + + return $result; + } + + /** + * Get the list of modules not in trash. + * + * @return mixed An array of module records (id, title, position), or false on error. + * + * @since 1.6 + */ + public function getModules() + { + $clientId = (int) $this->getState('item.client_id'); + $id = (int) $this->getState('item.id'); + + // Currently any setting that affects target page for a backend menu is not supported, hence load no modules. + if ($clientId == 1) { + return false; + } + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + /** + * Join on the module-to-menu mapping table. + * We are only interested if the module is displayed on ALL or THIS menu item (or the inverse ID number). + * sqlsrv changes for modulelink to menu manager + */ + $query->select( + [ + $db->quoteName('a.id'), + $db->quoteName('a.title'), + $db->quoteName('a.position'), + $db->quoteName('a.published'), + $db->quoteName('map.menuid'), + ] + ) + ->from($db->quoteName('#__modules', 'a')) + ->join( + 'LEFT', + $db->quoteName('#__modules_menu', 'map'), + $db->quoteName('map.moduleid') . ' = ' . $db->quoteName('a.id') + . ' AND ' . $db->quoteName('map.menuid') . ' IN (' . implode(',', $query->bindArray([0, $id, -$id])) . ')' + ); + + $subQuery = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__modules_menu')) + ->where( + [ + $db->quoteName('moduleid') . ' = ' . $db->quoteName('a.id'), + $db->quoteName('menuid') . ' < 0', + ] + ); + + $query->select('(' . $subQuery . ') AS ' . $db->quoteName('except')); + + // Join on the asset groups table. + $query->select($db->quoteName('ag.title', 'access_title')) + ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) + ->where( + [ + $db->quoteName('a.published') . ' >= 0', + $db->quoteName('a.client_id') . ' = :clientId', + ] + ) + ->bind(':clientId', $clientId, ParameterType::INTEGER) + ->order( + [ + $db->quoteName('a.position'), + $db->quoteName('a.ordering'), + ] + ); + + $db->setQuery($query); + + try { + $result = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return $result; + } + + /** + * Get the list of all view levels + * + * @return \stdClass[]|boolean An array of all view levels (id, title). + * + * @since 3.4 + */ + public function getViewLevels() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Get all the available view levels + $query->select($db->quoteName('id')) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__viewlevels')) + ->order($db->quoteName('id')); + + $db->setQuery($query); + + try { + $result = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return $result; + } + + /** + * Returns a Table object, always creating it + * + * @param string $type The table type to instantiate. + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\Cms\Table\Table|\Joomla\Cms\Table\Nested A database object. + * + * @since 1.6 + */ + public function getTable($type = 'Menu', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } + + /** + * A protected method to get the where clause for the reorder. + * This ensures that the row will be moved relative to a row with the same menutype. + * + * @param \Joomla\CMS\Table\Menu $table + * + * @return array An array of conditions to add to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('menutype') . ' = ' . $db->quote($table->menutype), + ]; + } + + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the User state. + $pk = $app->input->getInt('id'); + $this->setState('item.id', $pk); + + if (!$app->isClient('api')) { + $parentId = $app->getUserState('com_menus.edit.item.parent_id'); + $menuType = $app->getUserStateFromRequest('com_menus.items.menutype', 'menutype', '', 'string'); + } else { + $parentId = null; + $menuType = $app->input->get('com_menus.items.menutype'); + } + + if (!$parentId) { + $parentId = $app->input->getInt('parent_id'); + } + + $this->setState('item.parent_id', $parentId); + + // If we have a menutype we take client_id from there, unless forced otherwise + if ($menuType) { + $menuTypeObj = $this->getMenuType($menuType); + + // An invalid menutype will be handled as clientId = 0 and menuType = '' + $menuType = (string) $menuTypeObj->menutype; + $menuTypeId = (int) $menuTypeObj->client_id; + $clientId = (int) $menuTypeObj->client_id; + } else { + $menuTypeId = 0; + $clientId = $app->isClient('api') ? $app->input->get('client_id') : + $app->getUserState('com_menus.items.client_id', 0); + } + + // Forced client id will override/clear menuType if conflicted + $forcedClientId = $app->input->get('client_id', null, 'string'); + + if (!$app->isClient('api')) { + // Set the menu type and client id on the list view state, so we return to this menu after saving. + $app->setUserState('com_menus.items.menutype', $menuType); + $app->setUserState('com_menus.items.client_id', $clientId); + } + + // Current item if not new, we don't allow changing client id at all + if ($pk) { + $table = $this->getTable(); + $table->load($pk); + $forcedClientId = $table->get('client_id', $forcedClientId); + } + + if (isset($forcedClientId) && $forcedClientId != $clientId) { + $clientId = $forcedClientId; + $menuType = ''; + $menuTypeId = 0; + } + + $this->setState('item.menutype', $menuType); + $this->setState('item.client_id', $clientId); + $this->setState('item.menutypeid', $menuTypeId); + + if (!($type = $app->getUserState('com_menus.edit.item.type'))) { + $type = $app->input->get('type'); + + /** + * Note: a new menu item will have no field type. + * The field is required so the user has to change it. + */ + } + + $this->setState('item.type', $type); + + $link = $app->isClient('api') ? $app->input->get('link') : + $app->getUserState('com_menus.edit.item.link'); + + if ($link) { + $this->setState('item.link', $link); + } + + // Load the parameters. + $params = ComponentHelper::getParams('com_menus'); + $this->setState('params', $params); + } + + /** + * Loads the menutype object by a given menutype string + * + * @param string $menutype The given menutype + * + * @return \stdClass + * + * @since 3.7.0 + */ + protected function getMenuType($menutype) + { + $table = $this->getTable('MenuType'); + + $table->load(array('menutype' => $menutype)); + + return (object) $table->getProperties(); + } + + /** + * Loads the menutype ID by a given menutype string + * + * @param string $menutype The given menutype + * + * @return integer + * + * @since 3.6 + */ + protected function getMenuTypeId($menutype) + { + $menu = $this->getMenuType($menutype); + + return (int) $menu->id; + } + + /** + * Method to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import. + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $link = $this->getState('item.link'); + $type = $this->getState('item.type'); + $clientId = $this->getState('item.client_id'); + $formFile = false; + + // Load the specific type file + $typeFile = $clientId == 1 ? 'itemadmin_' . $type : 'item_' . $type; + $clientInfo = ApplicationHelper::getClientInfo($clientId); + + // Initialise form with component view params if available. + if ($type == 'component') { + $link = $link ? htmlspecialchars_decode($link) : ''; + + // Parse the link arguments. + $args = []; + + if ($link) { + parse_str(parse_url(htmlspecialchars_decode($link), PHP_URL_QUERY), $args); + } + + // Confirm that the option is defined. + $option = ''; + $base = ''; + + if (isset($args['option'])) { + // The option determines the base path to work with. + $option = $args['option']; + $base = $clientInfo->path . '/components/' . $option; + } + + if (isset($args['view'])) { + $view = $args['view']; + + // Determine the layout to search for. + if (isset($args['layout'])) { + $layout = $args['layout']; + } else { + $layout = 'default'; + } + + // Check for the layout XML file. Use standard xml file if it exists. + $tplFolders = array( + $base . '/tmpl/' . $view, + $base . '/views/' . $view . '/tmpl', + $base . '/view/' . $view . '/tmpl', + ); + $path = Path::find($tplFolders, $layout . '.xml'); + + if (is_file($path)) { + $formFile = $path; + } + + // If custom layout, get the xml file from the template folder + // template folder is first part of file name -- template:folder + if (!$formFile && (strpos($layout, ':') > 0)) { + list($altTmpl, $altLayout) = explode(':', $layout); + + $templatePath = Path::clean($clientInfo->path . '/templates/' . $altTmpl . '/html/' . $option . '/' . $view . '/' . $altLayout . '.xml'); + + if (is_file($templatePath)) { + $formFile = $templatePath; + } + } + } + + // Now check for a view manifest file + if (!$formFile) { + if (isset($view)) { + $metadataFolders = array( + $base . '/view/' . $view, + $base . '/views/' . $view + ); + $metaPath = Path::find($metadataFolders, 'metadata.xml'); + + if (is_file($path = Path::clean($metaPath))) { + $formFile = $path; + } + } elseif ($base) { + // Now check for a component manifest file + $path = Path::clean($base . '/metadata.xml'); + + if (is_file($path)) { + $formFile = $path; + } + } + } + } + + if ($formFile) { + // If an XML file was found in the component, load it first. + // We need to qualify the full path to avoid collisions with component file names. + + if ($form->loadFile($formFile, true, '/metadata') == false) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Attempt to load the xml file. + if (!$xml = simplexml_load_file($formFile)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Get the help data from the XML file if present. + $help = $xml->xpath('/metadata/layout/help'); + } else { + // We don't have a component. Load the form XML to get the help path + $xmlFile = Path::find(JPATH_ADMINISTRATOR . '/components/com_menus/forms', $typeFile . '.xml'); + + if ($xmlFile) { + if (!$xml = simplexml_load_file($xmlFile)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Get the help data from the XML file if present. + $help = $xml->xpath('/form/help'); + } + } + + if (!empty($help)) { + $helpKey = trim((string) $help[0]['key']); + $helpURL = trim((string) $help[0]['url']); + $helpLoc = trim((string) $help[0]['local']); + + $this->helpKey = $helpKey ?: $this->helpKey; + $this->helpURL = $helpURL ?: $this->helpURL; + $this->helpLocal = (($helpLoc == 'true') || ($helpLoc == '1') || ($helpLoc == 'local')); + } + + if (!$form->loadFile($typeFile, true, false)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Association menu items, we currently do not support this for admin menu… may be later + if ($clientId == 0 && Associations::isEnabled()) { + $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); + + if (count($languages) > 1) { + $addform = new \SimpleXMLElement(''); + $fields = $addform->addChild('fields'); + $fields->addAttribute('name', 'associations'); + $fieldset = $fields->addChild('fieldset'); + $fieldset->addAttribute('name', 'item_associations'); + $fieldset->addAttribute('addfieldprefix', 'Joomla\Component\Menus\Administrator\Field'); + + foreach ($languages as $language) { + $field = $fieldset->addChild('field'); + $field->addAttribute('name', $language->lang_code); + $field->addAttribute('type', 'modal_menu'); + $field->addAttribute('language', $language->lang_code); + $field->addAttribute('label', $language->title); + $field->addAttribute('translate_label', 'false'); + $field->addAttribute('select', 'true'); + $field->addAttribute('new', 'true'); + $field->addAttribute('edit', 'true'); + $field->addAttribute('clear', 'true'); + $field->addAttribute('propagate', 'true'); + $option = $field->addChild('option', 'COM_MENUS_ITEM_FIELD_ASSOCIATION_NO_VALUE'); + $option->addAttribute('value', ''); + } + + $form->load($addform, false); + } + } + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } + + /** + * Method rebuild the entire nested set tree. + * + * @return boolean Boolean true on success, boolean false + * + * @since 1.6 + */ + public function rebuild() + { + // Initialise variables. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $table = $this->getTable(); + + try { + $rebuildResult = $table->rebuild(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (!$rebuildResult) { + $this->setError($table->getError()); + + return false; + } + + $query->select( + [ + $db->quoteName('id'), + $db->quoteName('params'), + ] + ) + ->from($db->quoteName('#__menu')) + ->where( + [ + $db->quoteName('params') . ' NOT LIKE ' . $db->quote('{%'), + $db->quoteName('params') . ' <> ' . $db->quote(''), + ] + ); + $db->setQuery($query); + + try { + $items = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + $query = $db->getQuery(true) + ->update($db->quoteName('#__menu')) + ->set($db->quoteName('params') . ' = :params') + ->where($db->quoteName('id') . ' = :id') + ->bind(':params', $params) + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + + foreach ($items as &$item) { + // Update query parameters. + $id = $item->id; + $params = new Registry($item->params); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $pk = isset($data['id']) ? $data['id'] : (int) $this->getState('item.id'); + $isNew = true; + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $table = $this->getTable(); + $context = $this->option . '.' . $this->name; + + // Include the plugins for the on save events. + PluginHelper::importPlugin($this->events_map['save']); + + // Load the row if saving an existing item. + if ($pk > 0) { + $table->load($pk); + $isNew = false; + } + + if (!$isNew) { + if ($table->parent_id == $data['parent_id']) { + // If first is chosen make the item the first child of the selected parent. + if ($data['menuordering'] == -1) { + $table->setLocation($data['parent_id'], 'first-child'); + } elseif ($data['menuordering'] == -2) { + // If last is chosen make it the last child of the selected parent. + $table->setLocation($data['parent_id'], 'last-child'); + } elseif ($data['menuordering'] && $table->id != $data['menuordering'] || empty($data['id'])) { + // Don't try to put an item after itself. All other ones put after the selected item. + // $data['id'] is empty means it's a save as copy + $table->setLocation($data['menuordering'], 'after'); + } elseif ($data['menuordering'] && $table->id == $data['menuordering']) { + // \Just leave it where it is if no change is made. + unset($data['menuordering']); + } + } else { + // Set the new parent id if parent id not matched and put in last position + $table->setLocation($data['parent_id'], 'last-child'); + } + + // Check if we are moving to a different menu + if ($data['menutype'] != $table->menutype) { + // Add the child node ids to the children array. + $query->clear() + ->select($db->quoteName('id')) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('lft') . ' BETWEEN ' . (int) $table->lft . ' AND ' . (int) $table->rgt); + $db->setQuery($query); + $children = (array) $db->loadColumn(); + } + } else { + // We have a new item, so it is not a change. + $menuType = $this->getMenuType($data['menutype']); + + $data['client_id'] = $menuType->client_id; + + $table->setLocation($data['parent_id'], 'last-child'); + } + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Alter the title & alias for save2copy when required. Also, unset the home record. + if (Factory::getApplication()->input->get('task') === 'save2copy' && $data['id'] === 0) { + $origTable = $this->getTable(); + $origTable->load($this->getState('item.id')); + + if ($table->title === $origTable->title) { + list($title, $alias) = $this->generateNewTitle($table->parent_id, $table->alias, $table->title); + $table->title = $title; + $table->alias = $alias; + } + + if ($table->alias === $origTable->alias) { + $table->alias = ''; + } + + $table->published = 0; + $table->home = 0; + } + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, $isNew, $data)); + + // Store the data. + if (in_array(false, $result, true) || !$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the after save event. + Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, $isNew)); + + // Rebuild the tree path. + if (!$table->rebuildPath($table->id)) { + $this->setError($table->getError()); + + return false; + } + + // Process the child rows + if (!empty($children)) { + // Remove any duplicates and sanitize ids. + $children = array_unique($children); + $children = ArrayHelper::toInteger($children); + + // Update the menutype field in all nodes where necessary. + $query = $db->getQuery(true) + ->update($db->quoteName('#__menu')) + ->set($db->quoteName('menutype') . ' = :menutype') + ->whereIn($db->quoteName('id'), $children) + ->bind(':menutype', $data['menutype']); + + try { + $db->setQuery($query); + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + $this->setState('item.id', $table->id); + $this->setState('item.menutype', $table->menutype); + + // Load associated menu items, for now not supported for admin menu… may be later + if ($table->get('client_id') == 0 && Associations::isEnabled()) { + // Adding self to the association + $associations = isset($data['associations']) ? $data['associations'] : array(); + + // Unset any invalid associations + $associations = ArrayHelper::toInteger($associations); + + foreach ($associations as $tag => $id) { + if (!$id) { + unset($associations[$tag]); + } + } + + // Detecting all item menus + $all_language = $table->language == '*'; + + if ($all_language && !empty($associations)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_MENUS_ERROR_ALL_LANGUAGE_ASSOCIATED'), 'notice'); + } + + // Get associationskey for edited item + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('key')) + ->from($db->quoteName('#__associations')) + ->where( + [ + $db->quoteName('context') . ' = :context', + $db->quoteName('id') . ' = :id', + ] + ) + ->bind(':context', $this->associationsContext) + ->bind(':id', $table->id, ParameterType::INTEGER); + $db->setQuery($query); + $oldKey = $db->loadResult(); + + if ($associations || $oldKey !== null) { + // Deleting old associations for the associated items + $where = []; + $query = $db->getQuery(true) + ->delete($db->quoteName('#__associations')) + ->where($db->quoteName('context') . ' = :context') + ->bind(':context', $this->associationsContext); + + if ($associations) { + $where[] = $db->quoteName('id') . ' IN (' . implode(',', $query->bindArray(array_values($associations))) . ')'; + } + + if ($oldKey !== null) { + $where[] = $db->quoteName('key') . ' = :oldKey'; + $query->bind(':oldKey', $oldKey); + } + + $query->extendWhere('AND', $where, 'OR'); + + try { + $db->setQuery($query); + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + // Adding self to the association + if (!$all_language) { + $associations[$table->language] = (int) $table->id; + } + + if (count($associations) > 1) { + // Adding new association for these items + $key = md5(json_encode($associations)); + $query = $db->getQuery(true) + ->insert($db->quoteName('#__associations')) + ->columns( + [ + $db->quoteName('id'), + $db->quoteName('context'), + $db->quoteName('key'), + ] + ); + + foreach ($associations as $id) { + $query->values( + implode( + ',', + $query->bindArray( + [$id, $this->associationsContext, $key], + [ParameterType::INTEGER, ParameterType::STRING, ParameterType::STRING] + ) + ) + ); + } + + try { + $db->setQuery($query); + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + } + + // Clean the cache + $this->cleanCache(); + + if (isset($data['link'])) { + $base = Uri::base(); + $juri = Uri::getInstance($base . $data['link']); + $option = $juri->getVar('option'); + + // Clean the cache + parent::cleanCache($option); + } + + if (Factory::getApplication()->input->get('task') === 'editAssociations') { + return $this->redirectToAssociations($data); + } + + return true; + } + + /** + * Method to save the reordered nested set tree. + * First we save the new order values in the lft values of the changed ids. + * Then we invoke the table rebuild to implement the new ordering. + * + * @param array $idArray Rows identifiers to be reordered + * @param array $lftArray lft values of rows to be reordered + * + * @return boolean false on failure or error, true otherwise. + * + * @since 1.6 + */ + public function saveorder($idArray = null, $lftArray = null) + { + // Get an instance of the table object. + $table = $this->getTable(); + + if (!$table->saveorder($idArray, $lftArray)) { + $this->setError($table->getError()); + + return false; + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to change the home state of one or more items. + * + * @param array $pks A list of the primary keys to change. + * @param integer $value The value of the home state. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function setHome(&$pks, $value = 1) + { + $table = $this->getTable(); + $pks = (array) $pks; + + $languages = array(); + $onehome = false; + + // Remember that we can set a home page for different languages, + // so we need to loop through the primary key array. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + if (!array_key_exists($table->language, $languages)) { + $languages[$table->language] = true; + + if ($table->home == $value) { + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('COM_MENUS_ERROR_ALREADY_HOME'), 'notice'); + } elseif ($table->menutype == 'main') { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('COM_MENUS_ERROR_MENUTYPE_HOME'), 'error'); + } else { + $table->home = $value; + + if ($table->language == '*') { + $table->published = 1; + } + + if (!$this->canSave($table)) { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + } elseif (!$table->check()) { + // Prune the items that failed pre-save checks. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage($table->getError(), 'error'); + } elseif (!$table->store()) { + // Prune the items that could not be stored. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage($table->getError(), 'error'); + } + } + } else { + unset($pks[$i]); + + if (!$onehome) { + $onehome = true; + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_MENUS_ERROR_ONE_HOME'), 'notice'); + } + } + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to change the published state of one or more records. + * + * @param array $pks A list of the primary keys to change. + * @param integer $value The value of the published state. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function publish(&$pks, $value = 1) + { + $table = $this->getTable(); + $pks = (array) $pks; + + // Default menu item existence checks. + if ($value != 1) { + foreach ($pks as $i => $pk) { + if ($table->load($pk) && $table->home && $table->language == '*') { + // Prune items that you can't change. + Factory::getApplication()->enqueueMessage(Text::_('JLIB_DATABASE_ERROR_MENU_UNPUBLISH_DEFAULT_HOME'), 'error'); + unset($pks[$i]); + break; + } + } + } + + // Clean the cache + $this->cleanCache(); + + // Ensure that previous checks doesn't empty the array + if (empty($pks)) { + return true; + } + + return parent::publish($pks, $value); + } + + /** + * Method to change the title & alias. + * + * @param integer $parentId The id of the parent. + * @param string $alias The alias. + * @param string $title The title. + * + * @return array Contains the modified title and alias. + * + * @since 1.6 + */ + protected function generateNewTitle($parentId, $alias, $title) + { + // Alter the title & alias + $table = $this->getTable(); + + while ($table->load(array('alias' => $alias, 'parent_id' => $parentId))) { + if ($title == $table->title) { + $title = StringHelper::increment($title); + } + + $alias = StringHelper::increment($alias, 'dash'); + } + + return array($title, $alias); + } + + /** + * Custom clean the cache + * + * @param string $group Cache group name. + * @param integer $clientId @deprecated 5.0 No Longer Used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('com_menus'); + parent::cleanCache('com_modules'); + parent::cleanCache('mod_menu'); + } } diff --git a/administrator/components/com_menus/src/Model/ItemsModel.php b/administrator/components/com_menus/src/Model/ItemsModel.php index 313cdd999ee05..e8f90ac445f91 100644 --- a/administrator/components/com_menus/src/Model/ItemsModel.php +++ b/administrator/components/com_menus/src/Model/ItemsModel.php @@ -1,4 +1,5 @@ input->get('forcedLanguage', '', 'cmd'); - - // Adjust the context to support modal layouts. - if ($layout = $app->input->get('layout')) - { - $this->context .= '.' . $layout; - } - - // Adjust the context to support forced languages. - if ($forcedLanguage) - { - $this->context .= '.' . $forcedLanguage; - } - - $search = $this->getUserStateFromRequest($this->context . '.search', 'filter_search'); - $this->setState('filter.search', $search); - - $published = $this->getUserStateFromRequest($this->context . '.published', 'filter_published', ''); - $this->setState('filter.published', $published); - - $access = $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access'); - $this->setState('filter.access', $access); - - $parentId = $this->getUserStateFromRequest($this->context . '.filter.parent_id', 'filter_parent_id'); - $this->setState('filter.parent_id', $parentId); - - $level = $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level'); - $this->setState('filter.level', $level); - - // Watch changes in client_id and menutype and keep sync whenever needed. - $currentClientId = $app->getUserState($this->context . '.client_id', 0); - $clientId = $app->input->getInt('client_id', $currentClientId); - - // Load mod_menu.ini file when client is administrator - if ($clientId == 1) - { - Factory::getLanguage()->load('mod_menu', JPATH_ADMINISTRATOR); - } - - $currentMenuType = $app->getUserState($this->context . '.menutype', ''); - $menuType = $app->input->getString('menutype', $currentMenuType); - - // If client_id changed clear menutype and reset pagination - if ($clientId != $currentClientId) - { - $menuType = ''; - - $app->input->set('limitstart', 0); - $app->input->set('menutype', ''); - } - - // If menutype changed reset pagination. - if ($menuType != $currentMenuType) - { - $app->input->set('limitstart', 0); - } - - if (!$menuType) - { - $app->setUserState($this->context . '.menutype', ''); - $this->setState('menutypetitle', ''); - $this->setState('menutypeid', ''); - } - // Special menu types, if selected explicitly, will be allowed as a filter - elseif ($menuType == 'main') - { - // Adjust client_id to match the menutype. This is safe as client_id was not changed in this request. - $app->input->set('client_id', 1); - - $app->setUserState($this->context . '.menutype', $menuType); - $this->setState('menutypetitle', ucfirst($menuType)); - $this->setState('menutypeid', -1); - } - // Get the menutype object with appropriate checks. - elseif ($cMenu = $this->getMenu($menuType, true)) - { - // Adjust client_id to match the menutype. This is safe as client_id was not changed in this request. - $app->input->set('client_id', $cMenu->client_id); - - $app->setUserState($this->context . '.menutype', $menuType); - $this->setState('menutypetitle', $cMenu->title); - $this->setState('menutypeid', $cMenu->id); - } - // This menutype does not exist, leave client id unchanged but reset menutype and pagination - else - { - $menuType = ''; - - $app->input->set('limitstart', 0); - $app->input->set('menutype', $menuType); - - $app->setUserState($this->context . '.menutype', $menuType); - $this->setState('menutypetitle', ''); - $this->setState('menutypeid', ''); - } - - // Client id filter - $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); - $this->setState('filter.client_id', $clientId); - - // Use a different filter file when client is administrator - if ($clientId == 1) - { - $this->filterFormName = 'filter_itemsadmin'; - } - - $this->setState('filter.menutype', $menuType); - - $language = $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', ''); - $this->setState('filter.language', $language); - - // Component parameters. - $params = ComponentHelper::getParams('com_menus'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - - // Force a language. - if (!empty($forcedLanguage)) - { - $this->setState('filter.language', $forcedLanguage); - } - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.language'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.parent_id'); - $id .= ':' . $this->getState('filter.menutype'); - $id .= ':' . $this->getState('filter.client_id'); - - return parent::getStoreId($id); - } - - /** - * Builds an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery A query object. - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - $clientId = (int) $this->getState('filter.client_id'); - - // Select all fields from the table. - $query->select( - // We can't quote state values because they could contain expressions. - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.menutype'), - $db->quoteName('a.title'), - $db->quoteName('a.alias'), - $db->quoteName('a.note'), - $db->quoteName('a.path'), - $db->quoteName('a.link'), - $db->quoteName('a.type'), - $db->quoteName('a.parent_id'), - $db->quoteName('a.level'), - $db->quoteName('a.component_id'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.browserNav'), - $db->quoteName('a.access'), - $db->quoteName('a.img'), - $db->quoteName('a.template_style_id'), - $db->quoteName('a.params'), - $db->quoteName('a.lft'), - $db->quoteName('a.rgt'), - $db->quoteName('a.home'), - $db->quoteName('a.language'), - $db->quoteName('a.client_id'), - $db->quoteName('a.publish_up'), - $db->quoteName('a.publish_down'), - ] - ) - ) - ->select( - [ - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image', 'language_image'), - $db->quoteName('l.sef', 'language_sef'), - $db->quoteName('u.name', 'editor'), - $db->quoteName('c.element', 'componentname'), - $db->quoteName('ag.title', 'access_level'), - $db->quoteName('mt.id', 'menutype_id'), - $db->quoteName('mt.title', 'menutype_title'), - $db->quoteName('e.enabled'), - $db->quoteName('e.name'), - 'CASE WHEN ' . $db->quoteName('a.type') . ' = ' . $db->quote('component') - . ' THEN ' . $db->quoteName('a.published') . ' +2 * (' . $db->quoteName('e.enabled') . ' -1)' - . ' ELSE ' . $db->quoteName('a.published') . ' END AS ' . $db->quoteName('published'), - ] - ) - ->from($db->quoteName('#__menu', 'a')); - - // Join over the language - $query->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); - - // Join over the users. - $query->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('a.checked_out')); - - // Join over components - $query->join('LEFT', $db->quoteName('#__extensions', 'c'), $db->quoteName('c.extension_id') . ' = ' . $db->quoteName('a.component_id')); - - // Join over the asset groups. - $query->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')); - - // Join over the menu types. - $query->join('LEFT', $db->quoteName('#__menu_types', 'mt'), $db->quoteName('mt.menutype') . ' = ' . $db->quoteName('a.menutype')); - - // Join over the extensions - $query->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('a.component_id')); - - // Join over the associations. - if (Associations::isEnabled()) - { - $subQuery = $db->getQuery(true) - ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') - ->from($db->quoteName('#__associations', 'asso1')) - ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) - ->where( - [ - $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), - $db->quoteName('asso1.context') . ' = ' . $db->quote('com_menus.item'), - ] - ); - - $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); - } - - // Exclude the root category. - $query->where( - [ - $db->quoteName('a.id') . ' > 1', - $db->quoteName('a.client_id') . ' = :clientId', - ] - ) - ->bind(':clientId', $clientId, ParameterType::INTEGER); - - // Filter on the published state. - $published = $this->getState('filter.published'); - - if (is_numeric($published)) - { - $published = (int) $published; - $query->where($db->quoteName('a.published') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->where($db->quoteName('a.published') . ' IN (0, 1)'); - } - - // Filter by search in title, alias or id - if ($search = trim($this->getState('filter.search', ''))) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :search') - ->bind(':search', $search, ParameterType::INTEGER); - } - elseif (stripos($search, 'link:') === 0) - { - if ($search = str_replace(' ', '%', trim(substr($search, 5)))) - { - $query->where($db->quoteName('a.link') . ' LIKE :search') - ->bind(':search', $search); - } - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.title') . ' LIKE :search1', - $db->quoteName('a.alias') . ' LIKE :search2', - $db->quoteName('a.note') . ' LIKE :search3', - ], - 'OR' - ) - ->bind([':search1', ':search2', ':search3'], $search); - } - } - - // Filter the items over the parent id if set. - $parentId = (int) $this->getState('filter.parent_id'); - $level = (int) $this->getState('filter.level'); - - if ($parentId) - { - // Create a subquery for the sub-items list - $subQuery = $db->getQuery(true) - ->select($db->quoteName('sub.id')) - ->from($db->quoteName('#__menu', 'sub')) - ->join( - 'INNER', - $db->quoteName('#__menu', 'this'), - $db->quoteName('sub.lft') . ' > ' . $db->quoteName('this.lft') - . ' AND ' . $db->quoteName('sub.rgt') . ' < ' . $db->quoteName('this.rgt') - ) - ->where($db->quoteName('this.id') . ' = :parentId1'); - - if ($level) - { - $subQuery->where($db->quoteName('sub.level') . ' <= ' . $db->quoteName('this.level') . ' + :level - 1'); - $query->bind(':level', $level, ParameterType::INTEGER); - } - - // Add the subquery to the main query - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.parent_id') . ' = :parentId2', - $db->quoteName('a.parent_id') . ' IN (' . (string) $subQuery . ')', - ], - 'OR' - ) - ->bind([':parentId1', ':parentId2'], $parentId, ParameterType::INTEGER); - } - - // Filter on the level. - elseif ($level) - { - $query->where($db->quoteName('a.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } - - // Filter the items over the menu id if set. - $menuType = $this->getState('filter.menutype'); - - // A value "" means all - if ($menuType == '') - { - // Load all menu types we have manage access - $query2 = $db->getQuery(true) - ->select( - [ - $db->quoteName('id'), - $db->quoteName('menutype'), - ] - ) - ->from($db->quoteName('#__menu_types')) - ->where($db->quoteName('client_id') . ' = :clientId') - ->bind(':clientId', $clientId, ParameterType::INTEGER) - ->order($db->quoteName('title')); - - // Show protected items on explicit filter only - $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('main')); - - $menuTypes = $db->setQuery($query2)->loadObjectList(); - - if ($menuTypes) - { - $types = array(); - - foreach ($menuTypes as $type) - { - if ($user->authorise('core.manage', 'com_menus.menu.' . (int) $type->id)) - { - $types[] = $type->menutype; - } - } - - if ($types) - { - $query->whereIn($db->quoteName('a.menutype'), $types); - } - else - { - $query->where(0); - } - } - } - // Default behavior => load all items from a specific menu - elseif (strlen($menuType)) - { - $query->where($db->quoteName('a.menutype') . ' = :menuType') - ->bind(':menuType', $menuType); - } - // Empty menu type => error - else - { - $query->where('1 != 1'); - } - - // Filter on the access level. - if ($access = (int) $this->getState('filter.access')) - { - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - - // Implement View Level Access - if (!$user->authorise('core.admin')) - { - if ($groups = $user->getAuthorisedViewLevels()) - { - $query->whereIn($db->quoteName('a.access'), $groups); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('a.language') . ' = :language') - ->bind(':language', $language); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Method to allow derived classes to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 3.2 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $name = $form->getName(); - - if ($name == 'com_menus.items.filter') - { - $clientId = $this->getState('filter.client_id'); - $form->setFieldAttribute('menutype', 'clientid', $clientId); - } - elseif (false !== strpos($name, 'com_menus.items.modal.')) - { - $form->removeField('client_id'); - - $clientId = $this->getState('filter.client_id'); - $form->setFieldAttribute('menutype', 'clientid', $clientId); - } - } - - /** - * Get the client id for a menu - * - * @param string $menuType The menutype identifier for the menu - * @param boolean $check Flag whether to perform check against ACL as well as existence - * - * @return integer - * - * @since 3.7.0 - */ - protected function getMenu($menuType, $check = false) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->select($db->quoteName('a') . '.*') - ->from($db->quoteName('#__menu_types', 'a')) - ->where($db->quoteName('menutype') . ' = :menuType') - ->bind(':menuType', $menuType); - - $cMenu = $db->setQuery($query)->loadObject(); - - if ($check) - { - // Check if menu type exists. - if (!$cMenu) - { - Log::add(Text::_('COM_MENUS_ERROR_MENUTYPE_NOT_FOUND'), Log::ERROR, 'jerror'); - - return false; - } - // Check if menu type is valid against ACL. - elseif (!Factory::getUser()->authorise('core.manage', 'com_menus.menu.' . $cMenu->id)) - { - Log::add(Text::_('JERROR_ALERTNOAUTHOR'), Log::ERROR, 'jerror'); - - return false; - } - } - - return $cMenu; - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 3.0.1 - */ - public function getItems() - { - $store = $this->getStoreId(); - - if (!isset($this->cache[$store])) - { - $items = parent::getItems(); - $lang = Factory::getLanguage(); - $client = $this->state->get('filter.client_id'); - - if ($items) - { - foreach ($items as $item) - { - if ($extension = $item->componentname) - { - $lang->load("$extension.sys", JPATH_ADMINISTRATOR) - || $lang->load("$extension.sys", JPATH_ADMINISTRATOR . '/components/' . $extension); - } - - // Translate component name - if ($client === 1) - { - $item->title = Text::_($item->title); - } - } - } - - $this->cache[$store] = $items; - } - - return $this->cache[$store]; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'menutype', 'a.menutype', 'menutype_title', + 'title', 'a.title', + 'alias', 'a.alias', + 'published', 'a.published', + 'access', 'a.access', 'access_level', + 'language', 'a.language', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'lft', 'a.lft', + 'rgt', 'a.rgt', + 'level', 'a.level', + 'path', 'a.path', + 'client_id', 'a.client_id', + 'home', 'a.home', + 'parent_id', 'a.parent_id', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'a.ordering' + ); + + if (Associations::isEnabled()) { + $config['filter_fields'][] = 'association'; + } + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.lft', $direction = 'asc') + { + $app = Factory::getApplication(); + + $forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd'); + + // Adjust the context to support modal layouts. + if ($layout = $app->input->get('layout')) { + $this->context .= '.' . $layout; + } + + // Adjust the context to support forced languages. + if ($forcedLanguage) { + $this->context .= '.' . $forcedLanguage; + } + + $search = $this->getUserStateFromRequest($this->context . '.search', 'filter_search'); + $this->setState('filter.search', $search); + + $published = $this->getUserStateFromRequest($this->context . '.published', 'filter_published', ''); + $this->setState('filter.published', $published); + + $access = $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access'); + $this->setState('filter.access', $access); + + $parentId = $this->getUserStateFromRequest($this->context . '.filter.parent_id', 'filter_parent_id'); + $this->setState('filter.parent_id', $parentId); + + $level = $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level'); + $this->setState('filter.level', $level); + + // Watch changes in client_id and menutype and keep sync whenever needed. + $currentClientId = $app->getUserState($this->context . '.client_id', 0); + $clientId = $app->input->getInt('client_id', $currentClientId); + + // Load mod_menu.ini file when client is administrator + if ($clientId == 1) { + Factory::getLanguage()->load('mod_menu', JPATH_ADMINISTRATOR); + } + + $currentMenuType = $app->getUserState($this->context . '.menutype', ''); + $menuType = $app->input->getString('menutype', $currentMenuType); + + // If client_id changed clear menutype and reset pagination + if ($clientId != $currentClientId) { + $menuType = ''; + + $app->input->set('limitstart', 0); + $app->input->set('menutype', ''); + } + + // If menutype changed reset pagination. + if ($menuType != $currentMenuType) { + $app->input->set('limitstart', 0); + } + + if (!$menuType) { + $app->setUserState($this->context . '.menutype', ''); + $this->setState('menutypetitle', ''); + $this->setState('menutypeid', ''); + } elseif ($menuType == 'main') { + // Special menu types, if selected explicitly, will be allowed as a filter + // Adjust client_id to match the menutype. This is safe as client_id was not changed in this request. + $app->input->set('client_id', 1); + + $app->setUserState($this->context . '.menutype', $menuType); + $this->setState('menutypetitle', ucfirst($menuType)); + $this->setState('menutypeid', -1); + } elseif ($cMenu = $this->getMenu($menuType, true)) { + // Get the menutype object with appropriate checks. + // Adjust client_id to match the menutype. This is safe as client_id was not changed in this request. + $app->input->set('client_id', $cMenu->client_id); + + $app->setUserState($this->context . '.menutype', $menuType); + $this->setState('menutypetitle', $cMenu->title); + $this->setState('menutypeid', $cMenu->id); + } else { + // This menutype does not exist, leave client id unchanged but reset menutype and pagination + $menuType = ''; + + $app->input->set('limitstart', 0); + $app->input->set('menutype', $menuType); + + $app->setUserState($this->context . '.menutype', $menuType); + $this->setState('menutypetitle', ''); + $this->setState('menutypeid', ''); + } + + // Client id filter + $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); + $this->setState('filter.client_id', $clientId); + + // Use a different filter file when client is administrator + if ($clientId == 1) { + $this->filterFormName = 'filter_itemsadmin'; + } + + $this->setState('filter.menutype', $menuType); + + $language = $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', ''); + $this->setState('filter.language', $language); + + // Component parameters. + $params = ComponentHelper::getParams('com_menus'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + + // Force a language. + if (!empty($forcedLanguage)) { + $this->setState('filter.language', $forcedLanguage); + } + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.language'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.parent_id'); + $id .= ':' . $this->getState('filter.menutype'); + $id .= ':' . $this->getState('filter.client_id'); + + return parent::getStoreId($id); + } + + /** + * Builds an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery A query object. + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + $clientId = (int) $this->getState('filter.client_id'); + + // Select all fields from the table. + $query->select( + // We can't quote state values because they could contain expressions. + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.menutype'), + $db->quoteName('a.title'), + $db->quoteName('a.alias'), + $db->quoteName('a.note'), + $db->quoteName('a.path'), + $db->quoteName('a.link'), + $db->quoteName('a.type'), + $db->quoteName('a.parent_id'), + $db->quoteName('a.level'), + $db->quoteName('a.component_id'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.browserNav'), + $db->quoteName('a.access'), + $db->quoteName('a.img'), + $db->quoteName('a.template_style_id'), + $db->quoteName('a.params'), + $db->quoteName('a.lft'), + $db->quoteName('a.rgt'), + $db->quoteName('a.home'), + $db->quoteName('a.language'), + $db->quoteName('a.client_id'), + $db->quoteName('a.publish_up'), + $db->quoteName('a.publish_down'), + ] + ) + ) + ->select( + [ + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image', 'language_image'), + $db->quoteName('l.sef', 'language_sef'), + $db->quoteName('u.name', 'editor'), + $db->quoteName('c.element', 'componentname'), + $db->quoteName('ag.title', 'access_level'), + $db->quoteName('mt.id', 'menutype_id'), + $db->quoteName('mt.title', 'menutype_title'), + $db->quoteName('e.enabled'), + $db->quoteName('e.name'), + 'CASE WHEN ' . $db->quoteName('a.type') . ' = ' . $db->quote('component') + . ' THEN ' . $db->quoteName('a.published') . ' +2 * (' . $db->quoteName('e.enabled') . ' -1)' + . ' ELSE ' . $db->quoteName('a.published') . ' END AS ' . $db->quoteName('published'), + ] + ) + ->from($db->quoteName('#__menu', 'a')); + + // Join over the language + $query->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); + + // Join over the users. + $query->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('a.checked_out')); + + // Join over components + $query->join('LEFT', $db->quoteName('#__extensions', 'c'), $db->quoteName('c.extension_id') . ' = ' . $db->quoteName('a.component_id')); + + // Join over the asset groups. + $query->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')); + + // Join over the menu types. + $query->join('LEFT', $db->quoteName('#__menu_types', 'mt'), $db->quoteName('mt.menutype') . ' = ' . $db->quoteName('a.menutype')); + + // Join over the extensions + $query->join('LEFT', $db->quoteName('#__extensions', 'e'), $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('a.component_id')); + + // Join over the associations. + if (Associations::isEnabled()) { + $subQuery = $db->getQuery(true) + ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') + ->from($db->quoteName('#__associations', 'asso1')) + ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) + ->where( + [ + $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), + $db->quoteName('asso1.context') . ' = ' . $db->quote('com_menus.item'), + ] + ); + + $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); + } + + // Exclude the root category. + $query->where( + [ + $db->quoteName('a.id') . ' > 1', + $db->quoteName('a.client_id') . ' = :clientId', + ] + ) + ->bind(':clientId', $clientId, ParameterType::INTEGER); + + // Filter on the published state. + $published = $this->getState('filter.published'); + + if (is_numeric($published)) { + $published = (int) $published; + $query->where($db->quoteName('a.published') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->where($db->quoteName('a.published') . ' IN (0, 1)'); + } + + // Filter by search in title, alias or id + if ($search = trim($this->getState('filter.search', ''))) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :search') + ->bind(':search', $search, ParameterType::INTEGER); + } elseif (stripos($search, 'link:') === 0) { + if ($search = str_replace(' ', '%', trim(substr($search, 5)))) { + $query->where($db->quoteName('a.link') . ' LIKE :search') + ->bind(':search', $search); + } + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.title') . ' LIKE :search1', + $db->quoteName('a.alias') . ' LIKE :search2', + $db->quoteName('a.note') . ' LIKE :search3', + ], + 'OR' + ) + ->bind([':search1', ':search2', ':search3'], $search); + } + } + + // Filter the items over the parent id if set. + $parentId = (int) $this->getState('filter.parent_id'); + $level = (int) $this->getState('filter.level'); + + if ($parentId) { + // Create a subquery for the sub-items list + $subQuery = $db->getQuery(true) + ->select($db->quoteName('sub.id')) + ->from($db->quoteName('#__menu', 'sub')) + ->join( + 'INNER', + $db->quoteName('#__menu', 'this'), + $db->quoteName('sub.lft') . ' > ' . $db->quoteName('this.lft') + . ' AND ' . $db->quoteName('sub.rgt') . ' < ' . $db->quoteName('this.rgt') + ) + ->where($db->quoteName('this.id') . ' = :parentId1'); + + if ($level) { + $subQuery->where($db->quoteName('sub.level') . ' <= ' . $db->quoteName('this.level') . ' + :level - 1'); + $query->bind(':level', $level, ParameterType::INTEGER); + } + + // Add the subquery to the main query + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.parent_id') . ' = :parentId2', + $db->quoteName('a.parent_id') . ' IN (' . (string) $subQuery . ')', + ], + 'OR' + ) + ->bind([':parentId1', ':parentId2'], $parentId, ParameterType::INTEGER); + } elseif ($level) { + // Filter on the level. + $query->where($db->quoteName('a.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } + + // Filter the items over the menu id if set. + $menuType = $this->getState('filter.menutype'); + + // A value "" means all + if ($menuType == '') { + // Load all menu types we have manage access + $query2 = $db->getQuery(true) + ->select( + [ + $db->quoteName('id'), + $db->quoteName('menutype'), + ] + ) + ->from($db->quoteName('#__menu_types')) + ->where($db->quoteName('client_id') . ' = :clientId') + ->bind(':clientId', $clientId, ParameterType::INTEGER) + ->order($db->quoteName('title')); + + // Show protected items on explicit filter only + $query->where($db->quoteName('a.menutype') . ' != ' . $db->quote('main')); + + $menuTypes = $db->setQuery($query2)->loadObjectList(); + + if ($menuTypes) { + $types = array(); + + foreach ($menuTypes as $type) { + if ($user->authorise('core.manage', 'com_menus.menu.' . (int) $type->id)) { + $types[] = $type->menutype; + } + } + + if ($types) { + $query->whereIn($db->quoteName('a.menutype'), $types); + } else { + $query->where(0); + } + } + } elseif (strlen($menuType)) { + // Default behavior => load all items from a specific menu + $query->where($db->quoteName('a.menutype') . ' = :menuType') + ->bind(':menuType', $menuType); + } else { + // Empty menu type => error + $query->where('1 != 1'); + } + + // Filter on the access level. + if ($access = (int) $this->getState('filter.access')) { + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + + // Implement View Level Access + if (!$user->authorise('core.admin')) { + if ($groups = $user->getAuthorisedViewLevels()) { + $query->whereIn($db->quoteName('a.access'), $groups); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('a.language') . ' = :language') + ->bind(':language', $language); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Method to allow derived classes to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 3.2 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $name = $form->getName(); + + if ($name == 'com_menus.items.filter') { + $clientId = $this->getState('filter.client_id'); + $form->setFieldAttribute('menutype', 'clientid', $clientId); + } elseif (false !== strpos($name, 'com_menus.items.modal.')) { + $form->removeField('client_id'); + + $clientId = $this->getState('filter.client_id'); + $form->setFieldAttribute('menutype', 'clientid', $clientId); + } + } + + /** + * Get the client id for a menu + * + * @param string $menuType The menutype identifier for the menu + * @param boolean $check Flag whether to perform check against ACL as well as existence + * + * @return integer + * + * @since 3.7.0 + */ + protected function getMenu($menuType, $check = false) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select($db->quoteName('a') . '.*') + ->from($db->quoteName('#__menu_types', 'a')) + ->where($db->quoteName('menutype') . ' = :menuType') + ->bind(':menuType', $menuType); + + $cMenu = $db->setQuery($query)->loadObject(); + + if ($check) { + // Check if menu type exists. + if (!$cMenu) { + Log::add(Text::_('COM_MENUS_ERROR_MENUTYPE_NOT_FOUND'), Log::ERROR, 'jerror'); + + return false; + } elseif (!Factory::getUser()->authorise('core.manage', 'com_menus.menu.' . $cMenu->id)) { + // Check if menu type is valid against ACL. + Log::add(Text::_('JERROR_ALERTNOAUTHOR'), Log::ERROR, 'jerror'); + + return false; + } + } + + return $cMenu; + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 3.0.1 + */ + public function getItems() + { + $store = $this->getStoreId(); + + if (!isset($this->cache[$store])) { + $items = parent::getItems(); + $lang = Factory::getLanguage(); + $client = $this->state->get('filter.client_id'); + + if ($items) { + foreach ($items as $item) { + if ($extension = $item->componentname) { + $lang->load("$extension.sys", JPATH_ADMINISTRATOR) + || $lang->load("$extension.sys", JPATH_ADMINISTRATOR . '/components/' . $extension); + } + + // Translate component name + if ($client === 1) { + $item->title = Text::_($item->title); + } + } + } + + $this->cache[$store] = $items; + } + + return $this->cache[$store]; + } } diff --git a/administrator/components/com_menus/src/Model/MenuModel.php b/administrator/components/com_menus/src/Model/MenuModel.php index 3a26565ace426..b6370f673347f 100644 --- a/administrator/components/com_menus/src/Model/MenuModel.php +++ b/administrator/components/com_menus/src/Model/MenuModel.php @@ -1,4 +1,5 @@ authorise('core.delete', 'com_menus.menu.' . (int) $record->id); - } - - /** - * Method to test whether the state of a record can be edited. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canEditState($record) - { - return Factory::getUser()->authorise('core.edit.state', 'com_menus.menu.' . (int) $record->id); - } - - /** - * Returns a Table object, always creating it - * - * @param string $type The table type to instantiate - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A database object - * - * @since 1.6 - */ - public function getTable($type = 'MenuType', $prefix = '\JTable', $config = array()) - { - return Table::getInstance($type, $prefix, $config); - } - - /** - * Auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - // Load the User state. - $id = $app->input->getInt('id'); - $this->setState('menu.id', $id); - - // Load the parameters. - $params = ComponentHelper::getParams('com_menus'); - $this->setState('params', $params); - - // Load the clientId. - $clientId = $app->getUserStateFromRequest('com_menus.menus.client_id', 'client_id', 0, 'int'); - $this->setState('client_id', $clientId); - } - - /** - * Method to get a menu item. - * - * @param integer $itemId The id of the menu item to get. - * - * @return mixed Menu item data object on success, false on failure. - * - * @since 1.6 - */ - public function &getItem($itemId = null) - { - $itemId = (!empty($itemId)) ? $itemId : (int) $this->getState('menu.id'); - - // Get a menu item row instance. - $table = $this->getTable(); - - // Attempt to load the row. - $return = $table->load($itemId); - - // Check for a table object error. - if ($return === false && $table->getError()) - { - $this->setError($table->getError()); - - return false; - } - - $properties = $table->getProperties(1); - $value = ArrayHelper::toObject($properties, CMSObject::class); - - return $value; - } - - /** - * Method to get the menu item form. - * - * @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 Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_menus.menu', 'menu', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - if (!$this->getState('client_id', 0)) - { - $form->removeField('preset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_menus.edit.menu.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - if (empty($data->id)) - { - $data->client_id = $this->state->get('client_id', 0); - } - } - else - { - unset($data['preset']); - } - - $this->preprocessData('com_menus.menu', $data); - - return $data; - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @see JFormRule - * @see JFilterInput - * @since 3.9.23 - */ - public function validate($form, $data, $group = null) - { - if (!Factory::getUser()->authorise('core.admin', 'com_menus')) - { - if (isset($data['rules'])) - { - unset($data['rules']); - } - } - - return parent::validate($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $id = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('menu.id'); - $isNew = true; - - // Get a row instance. - $table = $this->getTable(); - - // Include the plugins for the save events. - PluginHelper::importPlugin('content'); - - // Load the row if saving an existing item. - if ($id > 0) - { - $isNew = false; - $table->load($id); - } - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before event. - $result = Factory::getApplication()->triggerEvent('onContentBeforeSave', array($this->_context, &$table, $isNew, $data)); - - // Store the data. - if (in_array(false, $result, true) || !$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the after save event. - Factory::getApplication()->triggerEvent('onContentAfterSave', array($this->_context, &$table, $isNew)); - - $this->setState('menu.id', $table->id); - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to delete groups. - * - * @param array $itemIds An array of item ids. - * - * @return boolean Returns true on success, false on failure. - * - * @since 1.6 - */ - public function delete($itemIds) - { - // Sanitize the ids. - $itemIds = ArrayHelper::toInteger((array) $itemIds); - - // Get a group row instance. - $table = $this->getTable(); - - // Include the plugins for the delete events. - PluginHelper::importPlugin('content'); - - // Iterate the items to delete each one. - foreach ($itemIds as $itemId) - { - if ($table->load($itemId)) - { - // Trigger the before delete event. - $result = Factory::getApplication()->triggerEvent('onContentBeforeDelete', array($this->_context, $table)); - - if (in_array(false, $result, true) || !$table->delete($itemId)) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the after delete event. - Factory::getApplication()->triggerEvent('onContentAfterDelete', array($this->_context, $table)); - - // @todo: Delete the menu associations - Menu items and Modules - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Gets a list of all mod_mainmenu modules and collates them by menutype - * - * @return array - * - * @since 1.6 - */ - public function &getModules() - { - $db = $this->getDatabase(); - - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('a.id'), - $db->quoteName('a.title'), - $db->quoteName('a.params'), - $db->quoteName('a.position'), - $db->quoteName('ag.title', 'access_title'), - ] - ) - ->from($db->quoteName('#__modules', 'a')) - ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) - ->where($db->quoteName('a.module') . ' = ' . $db->quote('mod_menu')); - $db->setQuery($query); - - $modules = $db->loadObjectList(); - - $result = array(); - - foreach ($modules as &$module) - { - $params = new Registry($module->params); - - $menuType = $params->get('menutype'); - - if (!isset($result[$menuType])) - { - $result[$menuType] = array(); - } - - $result[$menuType][] = & $module; - } - - return $result; - } - - /** - * Returns the extension elements for the given items - * - * @param array $itemIds The item ids - * - * @return array - * - * @since 4.2.0 - */ - public function getExtensionElementsForMenuItems(array $itemIds): array - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query - ->select($db->quoteName('e.element')) - ->from($db->quoteName('#__extensions', 'e')) - ->join('INNER', $db->quoteName('#__menu', 'm'), $db->quoteName('m.component_id') . ' = ' . $db->quoteName('e.extension_id')) - ->whereIn($db->quoteName('m.id'), ArrayHelper::toInteger($itemIds)); - - return $db->setQuery($query)->loadColumn(); - } - - /** - * Custom clean the cache - * - * @param string $group Cache group name. - * @param integer $clientId @deprecated 5.0 No Longer used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('com_menus'); - parent::cleanCache('com_modules'); - parent::cleanCache('mod_menu'); - } + /** + * The prefix to use with controller messages. + * + * @var string + * @since 1.6 + */ + protected $text_prefix = 'COM_MENUS_MENU'; + + /** + * Model context string. + * + * @var string + */ + protected $_context = 'com_menus.menu'; + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + return Factory::getUser()->authorise('core.delete', 'com_menus.menu.' . (int) $record->id); + } + + /** + * Method to test whether the state of a record can be edited. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canEditState($record) + { + return Factory::getUser()->authorise('core.edit.state', 'com_menus.menu.' . (int) $record->id); + } + + /** + * Returns a Table object, always creating it + * + * @param string $type The table type to instantiate + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A database object + * + * @since 1.6 + */ + public function getTable($type = 'MenuType', $prefix = '\JTable', $config = array()) + { + return Table::getInstance($type, $prefix, $config); + } + + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the User state. + $id = $app->input->getInt('id'); + $this->setState('menu.id', $id); + + // Load the parameters. + $params = ComponentHelper::getParams('com_menus'); + $this->setState('params', $params); + + // Load the clientId. + $clientId = $app->getUserStateFromRequest('com_menus.menus.client_id', 'client_id', 0, 'int'); + $this->setState('client_id', $clientId); + } + + /** + * Method to get a menu item. + * + * @param integer $itemId The id of the menu item to get. + * + * @return mixed Menu item data object on success, false on failure. + * + * @since 1.6 + */ + public function &getItem($itemId = null) + { + $itemId = (!empty($itemId)) ? $itemId : (int) $this->getState('menu.id'); + + // Get a menu item row instance. + $table = $this->getTable(); + + // Attempt to load the row. + $return = $table->load($itemId); + + // Check for a table object error. + if ($return === false && $table->getError()) { + $this->setError($table->getError()); + + return false; + } + + $properties = $table->getProperties(1); + $value = ArrayHelper::toObject($properties, CMSObject::class); + + return $value; + } + + /** + * Method to get the menu item form. + * + * @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 Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_menus.menu', 'menu', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + if (!$this->getState('client_id', 0)) { + $form->removeField('preset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_menus.edit.menu.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + if (empty($data->id)) { + $data->client_id = $this->state->get('client_id', 0); + } + } else { + unset($data['preset']); + } + + $this->preprocessData('com_menus.menu', $data); + + return $data; + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @see JFormRule + * @see JFilterInput + * @since 3.9.23 + */ + public function validate($form, $data, $group = null) + { + if (!Factory::getUser()->authorise('core.admin', 'com_menus')) { + if (isset($data['rules'])) { + unset($data['rules']); + } + } + + return parent::validate($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $id = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('menu.id'); + $isNew = true; + + // Get a row instance. + $table = $this->getTable(); + + // Include the plugins for the save events. + PluginHelper::importPlugin('content'); + + // Load the row if saving an existing item. + if ($id > 0) { + $isNew = false; + $table->load($id); + } + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before event. + $result = Factory::getApplication()->triggerEvent('onContentBeforeSave', array($this->_context, &$table, $isNew, $data)); + + // Store the data. + if (in_array(false, $result, true) || !$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the after save event. + Factory::getApplication()->triggerEvent('onContentAfterSave', array($this->_context, &$table, $isNew)); + + $this->setState('menu.id', $table->id); + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to delete groups. + * + * @param array $itemIds An array of item ids. + * + * @return boolean Returns true on success, false on failure. + * + * @since 1.6 + */ + public function delete($itemIds) + { + // Sanitize the ids. + $itemIds = ArrayHelper::toInteger((array) $itemIds); + + // Get a group row instance. + $table = $this->getTable(); + + // Include the plugins for the delete events. + PluginHelper::importPlugin('content'); + + // Iterate the items to delete each one. + foreach ($itemIds as $itemId) { + if ($table->load($itemId)) { + // Trigger the before delete event. + $result = Factory::getApplication()->triggerEvent('onContentBeforeDelete', array($this->_context, $table)); + + if (in_array(false, $result, true) || !$table->delete($itemId)) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the after delete event. + Factory::getApplication()->triggerEvent('onContentAfterDelete', array($this->_context, $table)); + + // @todo: Delete the menu associations - Menu items and Modules + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Gets a list of all mod_mainmenu modules and collates them by menutype + * + * @return array + * + * @since 1.6 + */ + public function &getModules() + { + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('a.id'), + $db->quoteName('a.title'), + $db->quoteName('a.params'), + $db->quoteName('a.position'), + $db->quoteName('ag.title', 'access_title'), + ] + ) + ->from($db->quoteName('#__modules', 'a')) + ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) + ->where($db->quoteName('a.module') . ' = ' . $db->quote('mod_menu')); + $db->setQuery($query); + + $modules = $db->loadObjectList(); + + $result = array(); + + foreach ($modules as &$module) { + $params = new Registry($module->params); + + $menuType = $params->get('menutype'); + + if (!isset($result[$menuType])) { + $result[$menuType] = array(); + } + + $result[$menuType][] = & $module; + } + + return $result; + } + + /** + * Returns the extension elements for the given items + * + * @param array $itemIds The item ids + * + * @return array + * + * @since 4.2.0 + */ + public function getExtensionElementsForMenuItems(array $itemIds): array + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query + ->select($db->quoteName('e.element')) + ->from($db->quoteName('#__extensions', 'e')) + ->join('INNER', $db->quoteName('#__menu', 'm'), $db->quoteName('m.component_id') . ' = ' . $db->quoteName('e.extension_id')) + ->whereIn($db->quoteName('m.id'), ArrayHelper::toInteger($itemIds)); + + return $db->setQuery($query)->loadColumn(); + } + + /** + * Custom clean the cache + * + * @param string $group Cache group name. + * @param integer $clientId @deprecated 5.0 No Longer used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('com_menus'); + parent::cleanCache('com_modules'); + parent::cleanCache('mod_menu'); + } } diff --git a/administrator/components/com_menus/src/Model/MenusModel.php b/administrator/components/com_menus/src/Model/MenusModel.php index 8ae09b42d569b..dd9152b6a2efe 100644 --- a/administrator/components/com_menus/src/Model/MenusModel.php +++ b/administrator/components/com_menus/src/Model/MenusModel.php @@ -1,4 +1,5 @@ getStoreId('getItems'); - - // Try to load the data from internal storage. - if (!empty($this->cache[$store])) - { - return $this->cache[$store]; - } - - // Load the list items. - $items = parent::getItems(); - - // If empty or an error, just return. - if (empty($items)) - { - return array(); - } - - // Getting the following metric by joins is WAY TOO SLOW. - // Faster to do three queries for very large menu trees. - - // Get the menu types of menus in the list. - $db = $this->getDatabase(); - $menuTypes = array_column((array) $items, 'menutype'); - - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('m.menutype'), - 'COUNT(DISTINCT ' . $db->quoteName('m.id') . ') AS ' . $db->quoteName('count_published'), - ] - ) - ->from($db->quoteName('#__menu', 'm')) - ->where($db->quoteName('m.published') . ' = :published') - ->whereIn($db->quoteName('m.menutype'), $menuTypes, ParameterType::STRING) - ->group($db->quoteName('m.menutype')) - ->bind(':published', $published, ParameterType::INTEGER); - - $db->setQuery($query); - - // Get the published menu counts. - try - { - $published = 1; - $countPublished = $db->loadAssocList('menutype', 'count_published'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Get the unpublished menu counts. - try - { - $published = 0; - $countUnpublished = $db->loadAssocList('menutype', 'count_published'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Get the trashed menu counts. - try - { - $published = -2; - $countTrashed = $db->loadAssocList('menutype', 'count_published'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Inject the values back into the array. - foreach ($items as $item) - { - $item->count_published = $countPublished[$item->menutype] ?? 0; - $item->count_unpublished = $countUnpublished[$item->menutype] ?? 0; - $item->count_trashed = $countTrashed[$item->menutype] ?? 0; - } - - // Add the items to the internal cache. - $this->cache[$store] = $items; - - return $this->cache[$store]; - } - - /** - * Method to build an SQL query to load the list data. - * - * @return string An SQL query - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $clientId = (int) $this->getState('client_id'); - - // Select all fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.menutype'), - $db->quoteName('a.title'), - $db->quoteName('a.description'), - $db->quoteName('a.client_id'), - ] - ) - ) - ->from($db->quoteName('#__menu_types', 'a')) - ->where( - [ - $db->quoteName('a.id') . ' > 0', - $db->quoteName('a.client_id') . ' = :clientId', - ] - ) - ->bind(':clientId', $clientId, ParameterType::INTEGER); - - // Filter by search in title or menutype - if ($search = trim($this->getState('filter.search', ''))) - { - $search = '%' . str_replace(' ', '%', $search) . '%'; - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.title') . ' LIKE :search1' , - $db->quoteName('a.menutype') . ' LIKE :search2', - ], - 'OR' - ) - ->bind([':search1', ':search2'], $search); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.id')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'a.title', $direction = 'asc') - { - $search = $this->getUserStateFromRequest($this->context . '.search', 'filter_search'); - $this->setState('filter.search', $search); - - $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); - $this->setState('client_id', $clientId); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Gets the extension id of the core mod_menu module. - * - * @return integer - * - * @since 2.5 - */ - public function getModMenuId() - { - $clientId = (int) $this->getState('client_id'); - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('e.extension_id')) - ->from($db->quoteName('#__extensions', 'e')) - ->where( - [ - $db->quoteName('e.type') . ' = ' . $db->quote('module'), - $db->quoteName('e.element') . ' = ' . $db->quote('mod_menu'), - $db->quoteName('e.client_id') . ' = :clientId', - ] - ) - ->bind(':clientId', $clientId, ParameterType::INTEGER); - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Gets a list of all mod_mainmenu modules and collates them by menutype - * - * @return array - * - * @since 1.6 - */ - public function &getModules() - { - $model = $this->bootComponent('com_menus') - ->getMVCFactory()->createModel('Menu', 'Administrator', ['ignore_request' => true]); - $result = $model->getModules(); - - return $result; - } - - /** - * Returns the missing module languages. - * - * @return array - * - * @since _DEPLOY_VERSION__ - */ - public function getMissingModuleLanguages(): array - { - // Check custom administrator menu modules - if (!ModuleHelper::isAdminMultilang()) - { - return []; - } - - $languages = LanguageHelper::getInstalledLanguages(1, true); - $langCodes = []; - - foreach ($languages as $language) - { - if (isset($language->metadata['nativeName'])) - { - $languageName = $language->metadata['nativeName']; - } - else - { - $languageName = $language->metadata['name']; - } - - $langCodes[$language->metadata['tag']] = $languageName; - } - - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->select($db->quoteName('m.language')) - ->from($db->quoteName('#__modules', 'm')) - ->where( - [ - $db->quoteName('m.module') . ' = ' . $db->quote('mod_menu'), - $db->quoteName('m.published') . ' = 1', - $db->quoteName('m.client_id') . ' = 1', - ] - ) - ->group($db->quoteName('m.language')); - - $mLanguages = $db->setQuery($query)->loadColumn(); - - // Check if we have a mod_menu module set to All languages or a mod_menu module for each admin language. - if (!in_array('*', $mLanguages) && count($langMissing = array_diff(array_keys($langCodes), $mLanguages))) - { - return array_intersect_key($langCodes, array_flip($langMissing)); - } - - return []; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'menutype', 'a.menutype', + 'client_id', 'a.client_id', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Overrides the getItems method to attach additional metrics to the list. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 1.6.1 + */ + public function getItems() + { + // Get a storage key. + $store = $this->getStoreId('getItems'); + + // Try to load the data from internal storage. + if (!empty($this->cache[$store])) { + return $this->cache[$store]; + } + + // Load the list items. + $items = parent::getItems(); + + // If empty or an error, just return. + if (empty($items)) { + return array(); + } + + // Getting the following metric by joins is WAY TOO SLOW. + // Faster to do three queries for very large menu trees. + + // Get the menu types of menus in the list. + $db = $this->getDatabase(); + $menuTypes = array_column((array) $items, 'menutype'); + + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('m.menutype'), + 'COUNT(DISTINCT ' . $db->quoteName('m.id') . ') AS ' . $db->quoteName('count_published'), + ] + ) + ->from($db->quoteName('#__menu', 'm')) + ->where($db->quoteName('m.published') . ' = :published') + ->whereIn($db->quoteName('m.menutype'), $menuTypes, ParameterType::STRING) + ->group($db->quoteName('m.menutype')) + ->bind(':published', $published, ParameterType::INTEGER); + + $db->setQuery($query); + + // Get the published menu counts. + try { + $published = 1; + $countPublished = $db->loadAssocList('menutype', 'count_published'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Get the unpublished menu counts. + try { + $published = 0; + $countUnpublished = $db->loadAssocList('menutype', 'count_published'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Get the trashed menu counts. + try { + $published = -2; + $countTrashed = $db->loadAssocList('menutype', 'count_published'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Inject the values back into the array. + foreach ($items as $item) { + $item->count_published = $countPublished[$item->menutype] ?? 0; + $item->count_unpublished = $countUnpublished[$item->menutype] ?? 0; + $item->count_trashed = $countTrashed[$item->menutype] ?? 0; + } + + // Add the items to the internal cache. + $this->cache[$store] = $items; + + return $this->cache[$store]; + } + + /** + * Method to build an SQL query to load the list data. + * + * @return string An SQL query + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $clientId = (int) $this->getState('client_id'); + + // Select all fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.menutype'), + $db->quoteName('a.title'), + $db->quoteName('a.description'), + $db->quoteName('a.client_id'), + ] + ) + ) + ->from($db->quoteName('#__menu_types', 'a')) + ->where( + [ + $db->quoteName('a.id') . ' > 0', + $db->quoteName('a.client_id') . ' = :clientId', + ] + ) + ->bind(':clientId', $clientId, ParameterType::INTEGER); + + // Filter by search in title or menutype + if ($search = trim($this->getState('filter.search', ''))) { + $search = '%' . str_replace(' ', '%', $search) . '%'; + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.title') . ' LIKE :search1' , + $db->quoteName('a.menutype') . ' LIKE :search2', + ], + 'OR' + ) + ->bind([':search1', ':search2'], $search); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.id')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.title', $direction = 'asc') + { + $search = $this->getUserStateFromRequest($this->context . '.search', 'filter_search'); + $this->setState('filter.search', $search); + + $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); + $this->setState('client_id', $clientId); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Gets the extension id of the core mod_menu module. + * + * @return integer + * + * @since 2.5 + */ + public function getModMenuId() + { + $clientId = (int) $this->getState('client_id'); + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('e.extension_id')) + ->from($db->quoteName('#__extensions', 'e')) + ->where( + [ + $db->quoteName('e.type') . ' = ' . $db->quote('module'), + $db->quoteName('e.element') . ' = ' . $db->quote('mod_menu'), + $db->quoteName('e.client_id') . ' = :clientId', + ] + ) + ->bind(':clientId', $clientId, ParameterType::INTEGER); + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Gets a list of all mod_mainmenu modules and collates them by menutype + * + * @return array + * + * @since 1.6 + */ + public function &getModules() + { + $model = $this->bootComponent('com_menus') + ->getMVCFactory()->createModel('Menu', 'Administrator', ['ignore_request' => true]); + $result = $model->getModules(); + + return $result; + } + + /** + * Returns the missing module languages. + * + * @return array + * + * @since _DEPLOY_VERSION__ + */ + public function getMissingModuleLanguages(): array + { + // Check custom administrator menu modules + if (!ModuleHelper::isAdminMultilang()) { + return []; + } + + $languages = LanguageHelper::getInstalledLanguages(1, true); + $langCodes = []; + + foreach ($languages as $language) { + if (isset($language->metadata['nativeName'])) { + $languageName = $language->metadata['nativeName']; + } else { + $languageName = $language->metadata['name']; + } + + $langCodes[$language->metadata['tag']] = $languageName; + } + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select($db->quoteName('m.language')) + ->from($db->quoteName('#__modules', 'm')) + ->where( + [ + $db->quoteName('m.module') . ' = ' . $db->quote('mod_menu'), + $db->quoteName('m.published') . ' = 1', + $db->quoteName('m.client_id') . ' = 1', + ] + ) + ->group($db->quoteName('m.language')); + + $mLanguages = $db->setQuery($query)->loadColumn(); + + // Check if we have a mod_menu module set to All languages or a mod_menu module for each admin language. + if (!in_array('*', $mLanguages) && count($langMissing = array_diff(array_keys($langCodes), $mLanguages))) { + return array_intersect_key($langCodes, array_flip($langMissing)); + } + + return []; + } } diff --git a/administrator/components/com_menus/src/Model/MenutypesModel.php b/administrator/components/com_menus/src/Model/MenutypesModel.php index 85ed3f75a8e26..a05676f9bfbbd 100644 --- a/administrator/components/com_menus/src/Model/MenutypesModel.php +++ b/administrator/components/com_menus/src/Model/MenutypesModel.php @@ -1,4 +1,5 @@ input->get('client_id', 0); - - $this->state->set('client_id', $clientId); - } - - /** - * Method to get the reverse lookup of the base link URL to Title - * - * @return array Array of reverse lookup of the base link URL to Title - * - * @since 1.6 - */ - public function getReverseLookup() - { - if (empty($this->rlu)) - { - $this->getTypeOptions(); - } - - return $this->rlu; - } - - /** - * Method to get the available menu item type options. - * - * @return array Array of groups with menu item types. - * - * @since 1.6 - */ - public function getTypeOptions() - { - $lang = Factory::getLanguage(); - $list = array(); - - // Get the list of components. - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('name'), - $db->quoteName('element', 'option'), - ] - ) - ->from($db->quoteName('#__extensions')) - ->where( - [ - $db->quoteName('type') . ' = ' . $db->quote('component'), - $db->quoteName('enabled') . ' = 1', - ] - ) - ->order($db->quoteName('name') . ' ASC'); - $db->setQuery($query); - $components = $db->loadObjectList(); - - foreach ($components as $component) - { - $options = $this->getTypeOptionsByComponent($component->option); - - if ($options) - { - $list[$component->name] = $options; - - // Create the reverse lookup for link-to-name. - foreach ($options as $option) - { - if (isset($option->request)) - { - $this->addReverseLookupUrl($option); - - if (isset($option->request['option'])) - { - $componentLanguageFolder = JPATH_ADMINISTRATOR . '/components/' . $option->request['option']; - $lang->load($option->request['option'] . '.sys', JPATH_ADMINISTRATOR) - || $lang->load($option->request['option'] . '.sys', $componentLanguageFolder); - } - } - } - } - } - - // Allow a system plugin to insert dynamic menu types to the list shown in menus: - Factory::getApplication()->triggerEvent('onAfterGetMenuTypeOptions', array(&$list, $this)); - - return $list; - } - - /** - * Method to create the reverse lookup for link-to-name. - * (can be used from onAfterGetMenuTypeOptions handlers) - * - * @param CMSObject $option Object with request array or string and title public variables - * - * @return void - * - * @since 3.1 - */ - public function addReverseLookupUrl($option) - { - $this->rlu[MenusHelper::getLinkKey($option->request)] = $option->get('title'); - } - - /** - * Get menu types by component. - * - * @param string $component Component URL option. - * - * @return array - * - * @since 1.6 - */ - protected function getTypeOptionsByComponent($component) - { - $options = array(); - $client = ApplicationHelper::getClientInfo($this->getState('client_id')); - $mainXML = $client->path . '/components/' . $component . '/metadata.xml'; - - if (is_file($mainXML)) - { - $options = $this->getTypeOptionsFromXml($mainXML, $component); - } - - if (empty($options)) - { - $options = $this->getTypeOptionsFromMvc($component); - } - - if ($client->id == 1 && empty($options)) - { - $options = $this->getTypeOptionsFromManifest($component); - } - - return $options; - } - - /** - * Get the menu types from an XML file - * - * @param string $file File path - * @param string $component Component option as in URL - * - * @return array|boolean - * - * @since 1.6 - */ - protected function getTypeOptionsFromXml($file, $component) - { - $options = array(); - - // Attempt to load the xml file. - if (!$xml = simplexml_load_file($file)) - { - return false; - } - - // Look for the first menu node off of the root node. - if (!$menu = $xml->xpath('menu[1]')) - { - return false; - } - else - { - $menu = $menu[0]; - } - - // If we have no options to parse, just add the base component to the list of options. - if (!empty($menu['options']) && $menu['options'] == 'none') - { - // Create the menu option for the component. - $o = new CMSObject; - $o->title = (string) $menu['name']; - $o->description = (string) $menu['msg']; - $o->request = array('option' => $component); - - $options[] = $o; - - return $options; - } - - // Look for the first options node off of the menu node. - if (!$optionsNode = $menu->xpath('options[1]')) - { - return false; - } - else - { - $optionsNode = $optionsNode[0]; - } - - // Make sure the options node has children. - if (!$children = $optionsNode->children()) - { - return false; - } - - // Process each child as an option. - foreach ($children as $child) - { - if ($child->getName() == 'option') - { - // Create the menu option for the component. - $o = new CMSObject; - $o->title = (string) $child['name']; - $o->description = (string) $child['msg']; - $o->request = array('option' => $component, (string) $optionsNode['var'] => (string) $child['value']); - - $options[] = $o; - } - elseif ($child->getName() == 'default') - { - // Create the menu option for the component. - $o = new CMSObject; - $o->title = (string) $child['name']; - $o->description = (string) $child['msg']; - $o->request = array('option' => $component); - - $options[] = $o; - } - } - - return $options; - } - - /** - * Get menu types from MVC - * - * @param string $component Component option like in URLs - * - * @return array|boolean - * - * @since 1.6 - */ - protected function getTypeOptionsFromMvc($component) - { - $options = array(); - $views = array(); - - foreach ($this->getFolders($component) as $path) - { - if (!is_dir($path)) - { - continue; - } - - $views = array_merge($views, Folder::folders($path, '.', false, true)); - } - - foreach ($views as $viewPath) - { - $view = basename($viewPath); - - // Ignore private views. - if (strpos($view, '_') !== 0) - { - // Determine if a metadata file exists for the view. - $file = $viewPath . '/metadata.xml'; - - if (is_file($file)) - { - // Attempt to load the xml file. - if ($xml = simplexml_load_file($file)) - { - // Look for the first view node off of the root node. - if ($menu = $xml->xpath('view[1]')) - { - $menu = $menu[0]; - - // If the view is hidden from the menu, discard it and move on to the next view. - if (!empty($menu['hidden']) && $menu['hidden'] == 'true') - { - unset($xml); - continue; - } - - // Do we have an options node or should we process layouts? - // Look for the first options node off of the menu node. - if ($optionsNode = $menu->xpath('options[1]')) - { - $optionsNode = $optionsNode[0]; - - // Make sure the options node has children. - if ($children = $optionsNode->children()) - { - // Process each child as an option. - foreach ($children as $child) - { - if ($child->getName() == 'option') - { - // Create the menu option for the component. - $o = new CMSObject; - $o->title = (string) $child['name']; - $o->description = (string) $child['msg']; - $o->request = array('option' => $component, 'view' => $view, (string) $optionsNode['var'] => (string) $child['value']); - - $options[] = $o; - } - elseif ($child->getName() == 'default') - { - // Create the menu option for the component. - $o = new CMSObject; - $o->title = (string) $child['name']; - $o->description = (string) $child['msg']; - $o->request = array('option' => $component, 'view' => $view); - - $options[] = $o; - } - } - } - } - else - { - $options = array_merge($options, (array) $this->getTypeOptionsFromLayouts($component, $view)); - } - } - - unset($xml); - } - } - else - { - $options = array_merge($options, (array) $this->getTypeOptionsFromLayouts($component, $view)); - } - } - } - - return $options; - } - - /** - * Get menu types from Component manifest - * - * @param string $component Component option like in URLs - * - * @return array|boolean - * - * @since 3.7.0 - */ - protected function getTypeOptionsFromManifest($component) - { - // Load the component manifest - $fileName = JPATH_ADMINISTRATOR . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml'; - - if (!is_file($fileName)) - { - return false; - } - - if (!($manifest = simplexml_load_file($fileName))) - { - return false; - } - - // Check for a valid XML root tag. - if ($manifest->getName() != 'extension') - { - return false; - } - - $options = array(); - - // Start with the component root menu. - $rootMenu = $manifest->administration->menu; - - // If the menu item doesn't exist or is hidden do nothing. - if (!$rootMenu || in_array((string) $rootMenu['hidden'], array('true', 'hidden'))) - { - return $options; - } - - // Create the root menu option. - $ro = new \stdClass; - $ro->title = (string) trim($rootMenu); - $ro->description = ''; - $ro->request = array('option' => $component); - - // Process submenu options. - $submenu = $manifest->administration->submenu; - - if (!$submenu) - { - return $options; - } - - foreach ($submenu->menu as $child) - { - $attributes = $child->attributes(); - - $o = new \stdClass; - $o->title = (string) trim($child); - $o->description = ''; - - if ((string) $attributes->link) - { - parse_str((string) $attributes->link, $request); - } - else - { - $request = array(); - - $request['option'] = $component; - $request['act'] = (string) $attributes->act; - $request['task'] = (string) $attributes->task; - $request['controller'] = (string) $attributes->controller; - $request['view'] = (string) $attributes->view; - $request['layout'] = (string) $attributes->layout; - $request['sub'] = (string) $attributes->sub; - } - - $o->request = array_filter($request, 'strlen'); - $options[] = new CMSObject($o); - - // Do not repeat the default view link (index.php?option=com_abc). - if (count($o->request) == 1) - { - $ro = null; - } - } - - if ($ro) - { - $options[] = new CMSObject($ro); - } - - return $options; - } - - /** - * Get the menu types from component layouts - * - * @param string $component Component option as in URLs - * @param string $view Name of the view - * - * @return array - * - * @since 1.6 - */ - protected function getTypeOptionsFromLayouts($component, $view) - { - $options = array(); - $layouts = array(); - $layoutNames = array(); - $lang = Factory::getLanguage(); - $client = ApplicationHelper::getClientInfo($this->getState('client_id')); - - // Get the views for this component. - foreach ($this->getFolders($component) as $folder) - { - $path = $folder . '/' . $view . '/tmpl'; - - if (!is_dir($path)) - { - $path = $folder . '/' . $view; - } - - if (!is_dir($path)) - { - continue; - } - - $layouts = array_merge($layouts, Folder::files($path, '.xml$', false, true)); - } - - // Build list of standard layout names - foreach ($layouts as $layout) - { - // Ignore private layouts. - if (strpos(basename($layout), '_') === false) - { - // Get the layout name. - $layoutNames[] = basename($layout, '.xml'); - } - } - - // Get the template layouts - // @todo: This should only search one template -- the current template for this item (default of specified) - $folders = Folder::folders($client->path . '/templates', '', false, true); - - // Array to hold association between template file names and templates - $templateName = array(); - - foreach ($folders as $folder) - { - if (is_dir($folder . '/html/' . $component . '/' . $view)) - { - $template = basename($folder); - $lang->load('tpl_' . $template . '.sys', $client->path) - || $lang->load('tpl_' . $template . '.sys', $client->path . '/templates/' . $template); - - $templateLayouts = Folder::files($folder . '/html/' . $component . '/' . $view, '.xml$', false, true); - - foreach ($templateLayouts as $layout) - { - // Get the layout name. - $templateLayoutName = basename($layout, '.xml'); - - // Add to the list only if it is not a standard layout - if (array_search($templateLayoutName, $layoutNames) === false) - { - $layouts[] = $layout; - - // Set template name array so we can get the right template for the layout - $templateName[$layout] = basename($folder); - } - } - } - } - - // Process the found layouts. - foreach ($layouts as $layout) - { - // Ignore private layouts. - if (strpos(basename($layout), '_') === false) - { - $file = $layout; - - // Get the layout name. - $layout = basename($layout, '.xml'); - - // Create the menu option for the layout. - $o = new CMSObject; - $o->title = ucfirst($layout); - $o->description = ''; - $o->request = array('option' => $component, 'view' => $view); - - // Only add the layout request argument if not the default layout. - if ($layout != 'default') - { - // If the template is set, add in format template:layout so we save the template name - $o->request['layout'] = isset($templateName[$file]) ? $templateName[$file] . ':' . $layout : $layout; - } - - // Load layout metadata if it exists. - if (is_file($file)) - { - // Attempt to load the xml file. - if ($xml = simplexml_load_file($file)) - { - // Look for the first view node off of the root node. - if ($menu = $xml->xpath('layout[1]')) - { - $menu = $menu[0]; - - // If the view is hidden from the menu, discard it and move on to the next view. - if (!empty($menu['hidden']) && $menu['hidden'] == 'true') - { - unset($xml); - unset($o); - continue; - } - - // Populate the title and description if they exist. - if (!empty($menu['title'])) - { - $o->title = trim((string) $menu['title']); - } - - if (!empty($menu->message[0])) - { - $o->description = trim((string) $menu->message[0]); - } - } - } - } - - // Add the layout to the options array. - $options[] = $o; - } - } - - return $options; - } - - /** - * Get the folders with template files for the given component. - * - * @param string $component Component option as in URLs - * - * @return array - * - * @since 4.0.0 - */ - private function getFolders($component) - { - $client = ApplicationHelper::getClientInfo($this->getState('client_id')); - - if (!is_dir($client->path . '/components/' . $component)) - { - return array(); - } - - $folders = Folder::folders($client->path . '/components/' . $component, '^view[s]?$', false, true); - $folders = array_merge($folders, Folder::folders($client->path . '/components/' . $component, '^tmpl?$', false, true)); - - if (!$folders) - { - return array(); - } - - return $folders; - } + /** + * A reverse lookup of the base link URL to Title + * + * @var array + */ + protected $rlu = array(); + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * @return void + * + * @note Calling getState in this method will result in recursion. + * @since 3.0.1 + */ + protected function populateState() + { + parent::populateState(); + + $clientId = Factory::getApplication()->input->get('client_id', 0); + + $this->state->set('client_id', $clientId); + } + + /** + * Method to get the reverse lookup of the base link URL to Title + * + * @return array Array of reverse lookup of the base link URL to Title + * + * @since 1.6 + */ + public function getReverseLookup() + { + if (empty($this->rlu)) { + $this->getTypeOptions(); + } + + return $this->rlu; + } + + /** + * Method to get the available menu item type options. + * + * @return array Array of groups with menu item types. + * + * @since 1.6 + */ + public function getTypeOptions() + { + $lang = Factory::getLanguage(); + $list = array(); + + // Get the list of components. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('name'), + $db->quoteName('element', 'option'), + ] + ) + ->from($db->quoteName('#__extensions')) + ->where( + [ + $db->quoteName('type') . ' = ' . $db->quote('component'), + $db->quoteName('enabled') . ' = 1', + ] + ) + ->order($db->quoteName('name') . ' ASC'); + $db->setQuery($query); + $components = $db->loadObjectList(); + + foreach ($components as $component) { + $options = $this->getTypeOptionsByComponent($component->option); + + if ($options) { + $list[$component->name] = $options; + + // Create the reverse lookup for link-to-name. + foreach ($options as $option) { + if (isset($option->request)) { + $this->addReverseLookupUrl($option); + + if (isset($option->request['option'])) { + $componentLanguageFolder = JPATH_ADMINISTRATOR . '/components/' . $option->request['option']; + $lang->load($option->request['option'] . '.sys', JPATH_ADMINISTRATOR) + || $lang->load($option->request['option'] . '.sys', $componentLanguageFolder); + } + } + } + } + } + + // Allow a system plugin to insert dynamic menu types to the list shown in menus: + Factory::getApplication()->triggerEvent('onAfterGetMenuTypeOptions', array(&$list, $this)); + + return $list; + } + + /** + * Method to create the reverse lookup for link-to-name. + * (can be used from onAfterGetMenuTypeOptions handlers) + * + * @param CMSObject $option Object with request array or string and title public variables + * + * @return void + * + * @since 3.1 + */ + public function addReverseLookupUrl($option) + { + $this->rlu[MenusHelper::getLinkKey($option->request)] = $option->get('title'); + } + + /** + * Get menu types by component. + * + * @param string $component Component URL option. + * + * @return array + * + * @since 1.6 + */ + protected function getTypeOptionsByComponent($component) + { + $options = array(); + $client = ApplicationHelper::getClientInfo($this->getState('client_id')); + $mainXML = $client->path . '/components/' . $component . '/metadata.xml'; + + if (is_file($mainXML)) { + $options = $this->getTypeOptionsFromXml($mainXML, $component); + } + + if (empty($options)) { + $options = $this->getTypeOptionsFromMvc($component); + } + + if ($client->id == 1 && empty($options)) { + $options = $this->getTypeOptionsFromManifest($component); + } + + return $options; + } + + /** + * Get the menu types from an XML file + * + * @param string $file File path + * @param string $component Component option as in URL + * + * @return array|boolean + * + * @since 1.6 + */ + protected function getTypeOptionsFromXml($file, $component) + { + $options = array(); + + // Attempt to load the xml file. + if (!$xml = simplexml_load_file($file)) { + return false; + } + + // Look for the first menu node off of the root node. + if (!$menu = $xml->xpath('menu[1]')) { + return false; + } else { + $menu = $menu[0]; + } + + // If we have no options to parse, just add the base component to the list of options. + if (!empty($menu['options']) && $menu['options'] == 'none') { + // Create the menu option for the component. + $o = new CMSObject(); + $o->title = (string) $menu['name']; + $o->description = (string) $menu['msg']; + $o->request = array('option' => $component); + + $options[] = $o; + + return $options; + } + + // Look for the first options node off of the menu node. + if (!$optionsNode = $menu->xpath('options[1]')) { + return false; + } else { + $optionsNode = $optionsNode[0]; + } + + // Make sure the options node has children. + if (!$children = $optionsNode->children()) { + return false; + } + + // Process each child as an option. + foreach ($children as $child) { + if ($child->getName() == 'option') { + // Create the menu option for the component. + $o = new CMSObject(); + $o->title = (string) $child['name']; + $o->description = (string) $child['msg']; + $o->request = array('option' => $component, (string) $optionsNode['var'] => (string) $child['value']); + + $options[] = $o; + } elseif ($child->getName() == 'default') { + // Create the menu option for the component. + $o = new CMSObject(); + $o->title = (string) $child['name']; + $o->description = (string) $child['msg']; + $o->request = array('option' => $component); + + $options[] = $o; + } + } + + return $options; + } + + /** + * Get menu types from MVC + * + * @param string $component Component option like in URLs + * + * @return array|boolean + * + * @since 1.6 + */ + protected function getTypeOptionsFromMvc($component) + { + $options = array(); + $views = array(); + + foreach ($this->getFolders($component) as $path) { + if (!is_dir($path)) { + continue; + } + + $views = array_merge($views, Folder::folders($path, '.', false, true)); + } + + foreach ($views as $viewPath) { + $view = basename($viewPath); + + // Ignore private views. + if (strpos($view, '_') !== 0) { + // Determine if a metadata file exists for the view. + $file = $viewPath . '/metadata.xml'; + + if (is_file($file)) { + // Attempt to load the xml file. + if ($xml = simplexml_load_file($file)) { + // Look for the first view node off of the root node. + if ($menu = $xml->xpath('view[1]')) { + $menu = $menu[0]; + + // If the view is hidden from the menu, discard it and move on to the next view. + if (!empty($menu['hidden']) && $menu['hidden'] == 'true') { + unset($xml); + continue; + } + + // Do we have an options node or should we process layouts? + // Look for the first options node off of the menu node. + if ($optionsNode = $menu->xpath('options[1]')) { + $optionsNode = $optionsNode[0]; + + // Make sure the options node has children. + if ($children = $optionsNode->children()) { + // Process each child as an option. + foreach ($children as $child) { + if ($child->getName() == 'option') { + // Create the menu option for the component. + $o = new CMSObject(); + $o->title = (string) $child['name']; + $o->description = (string) $child['msg']; + $o->request = array('option' => $component, 'view' => $view, (string) $optionsNode['var'] => (string) $child['value']); + + $options[] = $o; + } elseif ($child->getName() == 'default') { + // Create the menu option for the component. + $o = new CMSObject(); + $o->title = (string) $child['name']; + $o->description = (string) $child['msg']; + $o->request = array('option' => $component, 'view' => $view); + + $options[] = $o; + } + } + } + } else { + $options = array_merge($options, (array) $this->getTypeOptionsFromLayouts($component, $view)); + } + } + + unset($xml); + } + } else { + $options = array_merge($options, (array) $this->getTypeOptionsFromLayouts($component, $view)); + } + } + } + + return $options; + } + + /** + * Get menu types from Component manifest + * + * @param string $component Component option like in URLs + * + * @return array|boolean + * + * @since 3.7.0 + */ + protected function getTypeOptionsFromManifest($component) + { + // Load the component manifest + $fileName = JPATH_ADMINISTRATOR . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml'; + + if (!is_file($fileName)) { + return false; + } + + if (!($manifest = simplexml_load_file($fileName))) { + return false; + } + + // Check for a valid XML root tag. + if ($manifest->getName() != 'extension') { + return false; + } + + $options = array(); + + // Start with the component root menu. + $rootMenu = $manifest->administration->menu; + + // If the menu item doesn't exist or is hidden do nothing. + if (!$rootMenu || in_array((string) $rootMenu['hidden'], array('true', 'hidden'))) { + return $options; + } + + // Create the root menu option. + $ro = new \stdClass(); + $ro->title = (string) trim($rootMenu); + $ro->description = ''; + $ro->request = array('option' => $component); + + // Process submenu options. + $submenu = $manifest->administration->submenu; + + if (!$submenu) { + return $options; + } + + foreach ($submenu->menu as $child) { + $attributes = $child->attributes(); + + $o = new \stdClass(); + $o->title = (string) trim($child); + $o->description = ''; + + if ((string) $attributes->link) { + parse_str((string) $attributes->link, $request); + } else { + $request = array(); + + $request['option'] = $component; + $request['act'] = (string) $attributes->act; + $request['task'] = (string) $attributes->task; + $request['controller'] = (string) $attributes->controller; + $request['view'] = (string) $attributes->view; + $request['layout'] = (string) $attributes->layout; + $request['sub'] = (string) $attributes->sub; + } + + $o->request = array_filter($request, 'strlen'); + $options[] = new CMSObject($o); + + // Do not repeat the default view link (index.php?option=com_abc). + if (count($o->request) == 1) { + $ro = null; + } + } + + if ($ro) { + $options[] = new CMSObject($ro); + } + + return $options; + } + + /** + * Get the menu types from component layouts + * + * @param string $component Component option as in URLs + * @param string $view Name of the view + * + * @return array + * + * @since 1.6 + */ + protected function getTypeOptionsFromLayouts($component, $view) + { + $options = array(); + $layouts = array(); + $layoutNames = array(); + $lang = Factory::getLanguage(); + $client = ApplicationHelper::getClientInfo($this->getState('client_id')); + + // Get the views for this component. + foreach ($this->getFolders($component) as $folder) { + $path = $folder . '/' . $view . '/tmpl'; + + if (!is_dir($path)) { + $path = $folder . '/' . $view; + } + + if (!is_dir($path)) { + continue; + } + + $layouts = array_merge($layouts, Folder::files($path, '.xml$', false, true)); + } + + // Build list of standard layout names + foreach ($layouts as $layout) { + // Ignore private layouts. + if (strpos(basename($layout), '_') === false) { + // Get the layout name. + $layoutNames[] = basename($layout, '.xml'); + } + } + + // Get the template layouts + // @todo: This should only search one template -- the current template for this item (default of specified) + $folders = Folder::folders($client->path . '/templates', '', false, true); + + // Array to hold association between template file names and templates + $templateName = array(); + + foreach ($folders as $folder) { + if (is_dir($folder . '/html/' . $component . '/' . $view)) { + $template = basename($folder); + $lang->load('tpl_' . $template . '.sys', $client->path) + || $lang->load('tpl_' . $template . '.sys', $client->path . '/templates/' . $template); + + $templateLayouts = Folder::files($folder . '/html/' . $component . '/' . $view, '.xml$', false, true); + + foreach ($templateLayouts as $layout) { + // Get the layout name. + $templateLayoutName = basename($layout, '.xml'); + + // Add to the list only if it is not a standard layout + if (array_search($templateLayoutName, $layoutNames) === false) { + $layouts[] = $layout; + + // Set template name array so we can get the right template for the layout + $templateName[$layout] = basename($folder); + } + } + } + } + + // Process the found layouts. + foreach ($layouts as $layout) { + // Ignore private layouts. + if (strpos(basename($layout), '_') === false) { + $file = $layout; + + // Get the layout name. + $layout = basename($layout, '.xml'); + + // Create the menu option for the layout. + $o = new CMSObject(); + $o->title = ucfirst($layout); + $o->description = ''; + $o->request = array('option' => $component, 'view' => $view); + + // Only add the layout request argument if not the default layout. + if ($layout != 'default') { + // If the template is set, add in format template:layout so we save the template name + $o->request['layout'] = isset($templateName[$file]) ? $templateName[$file] . ':' . $layout : $layout; + } + + // Load layout metadata if it exists. + if (is_file($file)) { + // Attempt to load the xml file. + if ($xml = simplexml_load_file($file)) { + // Look for the first view node off of the root node. + if ($menu = $xml->xpath('layout[1]')) { + $menu = $menu[0]; + + // If the view is hidden from the menu, discard it and move on to the next view. + if (!empty($menu['hidden']) && $menu['hidden'] == 'true') { + unset($xml); + unset($o); + continue; + } + + // Populate the title and description if they exist. + if (!empty($menu['title'])) { + $o->title = trim((string) $menu['title']); + } + + if (!empty($menu->message[0])) { + $o->description = trim((string) $menu->message[0]); + } + } + } + } + + // Add the layout to the options array. + $options[] = $o; + } + } + + return $options; + } + + /** + * Get the folders with template files for the given component. + * + * @param string $component Component option as in URLs + * + * @return array + * + * @since 4.0.0 + */ + private function getFolders($component) + { + $client = ApplicationHelper::getClientInfo($this->getState('client_id')); + + if (!is_dir($client->path . '/components/' . $component)) { + return array(); + } + + $folders = Folder::folders($client->path . '/components/' . $component, '^view[s]?$', false, true); + $folders = array_merge($folders, Folder::folders($client->path . '/components/' . $component, '^tmpl?$', false, true)); + + if (!$folders) { + return array(); + } + + return $folders; + } } diff --git a/administrator/components/com_menus/src/Service/HTML/Menus.php b/administrator/components/com_menus/src/Service/HTML/Menus.php index 4fac4b103af72..883193041fe81 100644 --- a/administrator/components/com_menus/src/Service/HTML/Menus.php +++ b/administrator/components/com_menus/src/Service/HTML/Menus.php @@ -1,4 +1,5 @@ getQuery(true) - ->select( - [ - $db->quoteName('m.id'), - $db->quoteName('m.title'), - $db->quoteName('l.sef', 'lang_sef'), - $db->quoteName('l.lang_code'), - $db->quoteName('mt.title', 'menu_title'), - $db->quoteName('l.image'), - $db->quoteName('l.title', 'language_title'), - ] - ) - ->from($db->quoteName('#__menu', 'm')) - ->join('LEFT', $db->quoteName('#__menu_types', 'mt'), $db->quoteName('mt.menutype') . ' = ' . $db->quoteName('m.menutype')) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('m.language') . ' = ' . $db->quoteName('l.lang_code')) - ->whereIn($db->quoteName('m.id'), array_values($associations)) - ->where($db->quoteName('m.id') . ' != :itemid') - ->bind(':itemid', $itemid, ParameterType::INTEGER); - $db->setQuery($query); + // Get the associations + if ($associations = MenusHelper::getAssociations($itemid)) { + // Get the associated menu items + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('m.id'), + $db->quoteName('m.title'), + $db->quoteName('l.sef', 'lang_sef'), + $db->quoteName('l.lang_code'), + $db->quoteName('mt.title', 'menu_title'), + $db->quoteName('l.image'), + $db->quoteName('l.title', 'language_title'), + ] + ) + ->from($db->quoteName('#__menu', 'm')) + ->join('LEFT', $db->quoteName('#__menu_types', 'mt'), $db->quoteName('mt.menutype') . ' = ' . $db->quoteName('m.menutype')) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('m.language') . ' = ' . $db->quoteName('l.lang_code')) + ->whereIn($db->quoteName('m.id'), array_values($associations)) + ->where($db->quoteName('m.id') . ' != :itemid') + ->bind(':itemid', $itemid, ParameterType::INTEGER); + $db->setQuery($query); - try - { - $items = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500); - } + try { + $items = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500); + } - // Construct html - if ($items) - { - $languages = LanguageHelper::getContentLanguages(array(0, 1)); - $content_languages = array_column($languages, 'lang_code'); + // Construct html + if ($items) { + $languages = LanguageHelper::getContentLanguages(array(0, 1)); + $content_languages = array_column($languages, 'lang_code'); - foreach ($items as &$item) - { - if (in_array($item->lang_code, $content_languages)) - { - $text = $item->lang_code; - $url = Route::_('index.php?option=com_menus&task=item.edit&id=' . (int) $item->id); - $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
' - . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
' . Text::sprintf('COM_MENUS_MENU_SPRINTF', $item->menu_title); - $classes = 'badge bg-secondary'; + foreach ($items as &$item) { + if (in_array($item->lang_code, $content_languages)) { + $text = $item->lang_code; + $url = Route::_('index.php?option=com_menus&task=item.edit&id=' . (int) $item->id); + $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
' + . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
' . Text::sprintf('COM_MENUS_MENU_SPRINTF', $item->menu_title); + $classes = 'badge bg-secondary'; - $item->link = '' . $text . '' - . ''; - } - else - { - // Display warning if Content Language is trashed or deleted - Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); - } - } - } + $item->link = '' . $text . '' + . ''; + } else { + // Display warning if Content Language is trashed or deleted + Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); + } + } + } - $html = LayoutHelper::render('joomla.content.associations', $items); - } + $html = LayoutHelper::render('joomla.content.associations', $items); + } - return $html; - } + return $html; + } - /** - * Returns a visibility state on a grid - * - * @param integer $params Params of item. - * - * @return string The Html code - * - * @since 3.7.0 - */ - public function visibility($params) - { - $registry = new Registry; + /** + * Returns a visibility state on a grid + * + * @param integer $params Params of item. + * + * @return string The Html code + * + * @since 3.7.0 + */ + public function visibility($params) + { + $registry = new Registry(); - try - { - $registry->loadString($params); - } - catch (\Exception $e) - { - // Invalid JSON - } + try { + $registry->loadString($params); + } catch (\Exception $e) { + // Invalid JSON + } - $show_menu = $registry->get('menu_show'); + $show_menu = $registry->get('menu_show'); - return ($show_menu === 0) ? '' . Text::_('COM_MENUS_LABEL_HIDDEN') . '' : ''; - } + return ($show_menu === 0) ? '' . Text::_('COM_MENUS_LABEL_HIDDEN') . '' : ''; + } } diff --git a/administrator/components/com_menus/src/Table/MenuTable.php b/administrator/components/com_menus/src/Table/MenuTable.php index 33a60da57b6ff..e70e094cded10 100644 --- a/administrator/components/com_menus/src/Table/MenuTable.php +++ b/administrator/components/com_menus/src/Table/MenuTable.php @@ -1,4 +1,5 @@ getDbo(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__modules_menu')) - ->where($db->quoteName('menuid') . ' = :pk') - ->bind(':pk', $pk, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - } + if ($return) { + // Delete key from the #__modules_menu table + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__modules_menu')) + ->where($db->quoteName('menuid') . ' = :pk') + ->bind(':pk', $pk, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + } - return $return; - } + return $return; + } - /** - * Overloaded check function - * - * @return boolean True on success, false on failure - * - * @see JTable::check - * @since 4.0.0 - */ - public function check() - { - $return = parent::check(); + /** + * Overloaded check function + * + * @return boolean True on success, false on failure + * + * @see JTable::check + * @since 4.0.0 + */ + public function check() + { + $return = parent::check(); - if ($return) - { - // Set publish_up to null date if not set - if (!$this->publish_up) - { - $this->publish_up = null; - } + if ($return) { + // Set publish_up to null date if not set + if (!$this->publish_up) { + $this->publish_up = null; + } - // Set publish_down to null date if not set - if (!$this->publish_down) - { - $this->publish_down = null; - } + // Set publish_down to null date if not set + if (!$this->publish_down) { + $this->publish_down = null; + } - // Check the publish down date is not earlier than publish up. - if (!is_null($this->publish_down) && !is_null($this->publish_up) && $this->publish_down < $this->publish_up) - { - $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); + // Check the publish down date is not earlier than publish up. + if (!is_null($this->publish_down) && !is_null($this->publish_up) && $this->publish_down < $this->publish_up) { + $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); - return false; - } + return false; + } - if ((int) $this->home) - { - // Set the publish down/up always for home. - $this->publish_up = null; - $this->publish_down = null; - } - } + if ((int) $this->home) { + // Set the publish down/up always for home. + $this->publish_up = null; + $this->publish_down = null; + } + } - return $return; - } + return $return; + } } diff --git a/administrator/components/com_menus/src/Table/MenuTypeTable.php b/administrator/components/com_menus/src/Table/MenuTypeTable.php index 108656290abae..b1a2ebe9eb0bf 100644 --- a/administrator/components/com_menus/src/Table/MenuTypeTable.php +++ b/administrator/components/com_menus/src/Table/MenuTypeTable.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->modules = $this->get('Modules'); - $this->levels = $this->get('ViewLevels'); - $this->canDo = ContentHelper::getActions('com_menus', 'menu', (int) $this->state->get('item.menutypeid')); - - // Check if we're allowed to edit this item - // No need to check for create, because then the moduletype select is empty - if (!empty($this->item->id) && !$this->canDo->get('core.edit')) - { - throw new \Exception(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // If we are forcing a language in modal (used for associations). - if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) - { - // Set the language field to the forcedLanguage and disable changing it. - $this->form->setValue('language', null, $forcedLanguage); - $this->form->setFieldAttribute('language', 'readonly', 'true'); - - // Only allow to select categories with All language or with the forced language. - $this->form->setFieldAttribute('parent_id', 'language', '*,' . $forcedLanguage); - } - - parent::display($tpl); - $this->addToolbar(); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $input = Factory::getApplication()->input; - $input->set('hidemainmenu', true); - - $user = $this->getCurrentUser(); - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); - $canDo = $this->canDo; - $clientId = $this->state->get('item.client_id', 0); - - ToolbarHelper::title(Text::_($isNew ? 'COM_MENUS_VIEW_NEW_ITEM_TITLE' : 'COM_MENUS_VIEW_EDIT_ITEM_TITLE'), 'list menu-add'); - - $toolbarButtons = []; - - // If a new item, can save the item. Allow users with edit permissions to apply changes to prevent returning to grid. - if ($isNew && $canDo->get('core.create')) - { - if ($canDo->get('core.edit')) - { - ToolbarHelper::apply('item.apply'); - } - - $toolbarButtons[] = ['save', 'item.save']; - } - - // If not checked out, can save the item. - if (!$isNew && !$checkedOut && $canDo->get('core.edit')) - { - ToolbarHelper::apply('item.apply'); - - $toolbarButtons[] = ['save', 'item.save']; - } - - // If the user can create new items, allow them to see Save & New - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'item.save2new']; - } - - // If an existing item, can save to a copy only if we have create rights. - if (!$isNew && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'item.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (!$isNew && Associations::isEnabled() && ComponentHelper::isEnabled('com_associations') && $clientId != 1) - { - ToolbarHelper::custom('item.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); - } - - if ($isNew) - { - ToolbarHelper::cancel('item.cancel'); - } - else - { - ToolbarHelper::cancel('item.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - - // Get the help information for the menu item. - $lang = Factory::getLanguage(); - - $help = $this->get('Help'); - - if ($lang->hasKey($help->url)) - { - $debug = $lang->setDebug(false); - $url = Text::_($help->url); - $lang->setDebug($debug); - } - else - { - $url = $help->url; - } - - ToolbarHelper::help($help->key, $help->local, $url); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var CMSObject + */ + protected $item; + + /** + * @var mixed + */ + protected $modules; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The actions the user is authorised to perform + * + * @var CMSObject + * @since 3.7.0 + */ + protected $canDo; + + /** + * A list of view levels containing the id and title of the view level + * + * @var \stdClass[] + * @since 4.0.0 + */ + protected $levels; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->modules = $this->get('Modules'); + $this->levels = $this->get('ViewLevels'); + $this->canDo = ContentHelper::getActions('com_menus', 'menu', (int) $this->state->get('item.menutypeid')); + + // Check if we're allowed to edit this item + // No need to check for create, because then the moduletype select is empty + if (!empty($this->item->id) && !$this->canDo->get('core.edit')) { + throw new \Exception(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // If we are forcing a language in modal (used for associations). + if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) { + // Set the language field to the forcedLanguage and disable changing it. + $this->form->setValue('language', null, $forcedLanguage); + $this->form->setFieldAttribute('language', 'readonly', 'true'); + + // Only allow to select categories with All language or with the forced language. + $this->form->setFieldAttribute('parent_id', 'language', '*,' . $forcedLanguage); + } + + parent::display($tpl); + $this->addToolbar(); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $input = Factory::getApplication()->input; + $input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); + $canDo = $this->canDo; + $clientId = $this->state->get('item.client_id', 0); + + ToolbarHelper::title(Text::_($isNew ? 'COM_MENUS_VIEW_NEW_ITEM_TITLE' : 'COM_MENUS_VIEW_EDIT_ITEM_TITLE'), 'list menu-add'); + + $toolbarButtons = []; + + // If a new item, can save the item. Allow users with edit permissions to apply changes to prevent returning to grid. + if ($isNew && $canDo->get('core.create')) { + if ($canDo->get('core.edit')) { + ToolbarHelper::apply('item.apply'); + } + + $toolbarButtons[] = ['save', 'item.save']; + } + + // If not checked out, can save the item. + if (!$isNew && !$checkedOut && $canDo->get('core.edit')) { + ToolbarHelper::apply('item.apply'); + + $toolbarButtons[] = ['save', 'item.save']; + } + + // If the user can create new items, allow them to see Save & New + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'item.save2new']; + } + + // If an existing item, can save to a copy only if we have create rights. + if (!$isNew && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'item.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (!$isNew && Associations::isEnabled() && ComponentHelper::isEnabled('com_associations') && $clientId != 1) { + ToolbarHelper::custom('item.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); + } + + if ($isNew) { + ToolbarHelper::cancel('item.cancel'); + } else { + ToolbarHelper::cancel('item.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + + // Get the help information for the menu item. + $lang = Factory::getLanguage(); + + $help = $this->get('Help'); + + if ($lang->hasKey($help->url)) { + $debug = $lang->setDebug(false); + $url = Text::_($help->url); + $lang->setDebug($debug); + } else { + $url = $help->url; + } + + ToolbarHelper::help($help->key, $help->local, $url); + } } diff --git a/administrator/components/com_menus/src/View/Items/HtmlView.php b/administrator/components/com_menus/src/View/Items/HtmlView.php index fe1c86bdd9521..3182c810e376b 100644 --- a/administrator/components/com_menus/src/View/Items/HtmlView.php +++ b/administrator/components/com_menus/src/View/Items/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->total = $this->get('Total'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->ordering = array(); - - // Preprocess the list of items to find ordering divisions. - foreach ($this->items as $item) - { - $this->ordering[$item->parent_id][] = $item->id; - - // Item type text - switch ($item->type) - { - case 'url': - $value = Text::_('COM_MENUS_TYPE_EXTERNAL_URL'); - break; - - case 'alias': - $value = Text::_('COM_MENUS_TYPE_ALIAS'); - break; - - case 'separator': - $value = Text::_('COM_MENUS_TYPE_SEPARATOR'); - break; - - case 'heading': - $value = Text::_('COM_MENUS_TYPE_HEADING'); - break; - - case 'container': - $value = Text::_('COM_MENUS_TYPE_CONTAINER'); - break; - - case 'component': - default: - // Load language - $lang->load($item->componentname . '.sys', JPATH_ADMINISTRATOR) - || $lang->load($item->componentname . '.sys', JPATH_ADMINISTRATOR . '/components/' . $item->componentname); - - if (!empty($item->componentname)) - { - $titleParts = array(); - $titleParts[] = Text::_($item->componentname); - $vars = null; - - parse_str($item->link, $vars); - - if (isset($vars['view'])) - { - // Attempt to load the view xml file. - $file = JPATH_SITE . '/components/' . $item->componentname . '/views/' . $vars['view'] . '/metadata.xml'; - - if (!is_file($file)) - { - $file = JPATH_SITE . '/components/' . $item->componentname . '/view/' . $vars['view'] . '/metadata.xml'; - } - - if (is_file($file) && $xml = simplexml_load_file($file)) - { - // Look for the first view node off of the root node. - if ($view = $xml->xpath('view[1]')) - { - // Add view title if present. - if (!empty($view[0]['title'])) - { - $viewTitle = trim((string) $view[0]['title']); - - // Check if the key is valid. Needed due to B/C so we don't show untranslated keys. This check should be removed with Joomla 4. - if ($lang->hasKey($viewTitle)) - { - $titleParts[] = Text::_($viewTitle); - } - } - } - } - - $vars['layout'] = $vars['layout'] ?? 'default'; - - // Attempt to load the layout xml file. - // If Alternative Menu Item, get template folder for layout file - if (strpos($vars['layout'], ':') > 0) - { - // Use template folder for layout file - $temp = explode(':', $vars['layout']); - $file = JPATH_SITE . '/templates/' . $temp[0] . '/html/' . $item->componentname . '/' . $vars['view'] . '/' . $temp[1] . '.xml'; - - // Load template language file - $lang->load('tpl_' . $temp[0] . '.sys', JPATH_SITE) - || $lang->load('tpl_' . $temp[0] . '.sys', JPATH_SITE . '/templates/' . $temp[0]); - } - else - { - $base = $this->state->get('filter.client_id') == 0 ? JPATH_SITE : JPATH_ADMINISTRATOR; - - // Get XML file from component folder for standard layouts - $file = $base . '/components/' . $item->componentname . '/tmpl/' . $vars['view'] - . '/' . $vars['layout'] . '.xml'; - - if (!file_exists($file)) - { - $file = $base . '/components/' . $item->componentname . '/views/' - . $vars['view'] . '/tmpl/' . $vars['layout'] . '.xml'; - - if (!file_exists($file)) - { - $file = $base . '/components/' . $item->componentname . '/view/' - . $vars['view'] . '/tmpl/' . $vars['layout'] . '.xml'; - } - } - } - - if (is_file($file) && $xml = simplexml_load_file($file)) - { - // Look for the first view node off of the root node. - if ($layout = $xml->xpath('layout[1]')) - { - if (!empty($layout[0]['title'])) - { - $titleParts[] = Text::_(trim((string) $layout[0]['title'])); - } - } - - if (!empty($layout[0]->message[0])) - { - $item->item_type_desc = Text::_(trim((string) $layout[0]->message[0])); - } - } - - unset($xml); - - // Special case if neither a view nor layout title is found - if (count($titleParts) == 1) - { - $titleParts[] = $vars['view']; - } - } - - $value = implode(' » ', $titleParts); - } - else - { - if (preg_match("/^index.php\?option=([a-zA-Z\-0-9_]*)/", $item->link, $result)) - { - $value = Text::sprintf('COM_MENUS_TYPE_UNEXISTING', $result[1]); - } - else - { - $value = Text::_('COM_MENUS_TYPE_UNKNOWN'); - } - } - break; - } - - $item->item_type = $value; - $item->protected = $item->menutype == 'main'; - } - - // Levels filter. - $options = array(); - $options[] = HTMLHelper::_('select.option', '1', Text::_('J1')); - $options[] = HTMLHelper::_('select.option', '2', Text::_('J2')); - $options[] = HTMLHelper::_('select.option', '3', Text::_('J3')); - $options[] = HTMLHelper::_('select.option', '4', Text::_('J4')); - $options[] = HTMLHelper::_('select.option', '5', Text::_('J5')); - $options[] = HTMLHelper::_('select.option', '6', Text::_('J6')); - $options[] = HTMLHelper::_('select.option', '7', Text::_('J7')); - $options[] = HTMLHelper::_('select.option', '8', Text::_('J8')); - $options[] = HTMLHelper::_('select.option', '9', Text::_('J9')); - $options[] = HTMLHelper::_('select.option', '10', Text::_('J10')); - - $this->f_levels = $options; - - // We don't need toolbar in the modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - else - { - // In menu associations modal we need to remove language filter if forcing a language. - if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) - { - // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. - $languageXml = new \SimpleXMLElement(''); - $this->filterForm->setField($languageXml, 'filter', true); - - // Also, unset the active language filter so the search tools is not open by default with this filter. - unset($this->activeFilters['language']); - } - } - - // Allow a system plugin to insert dynamic menu types to the list shown in menus: - Factory::getApplication()->triggerEvent('onBeforeRenderMenuItems', array($this)); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $menutypeId = (int) $this->state->get('menutypeid'); - - $canDo = ContentHelper::getActions('com_menus', 'menu', (int) $menutypeId); - $user = $this->getCurrentUser(); - - // Get the menu title - $menuTypeTitle = $this->get('State')->get('menutypetitle'); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - if ($menuTypeTitle) - { - ToolbarHelper::title(Text::sprintf('COM_MENUS_VIEW_ITEMS_MENU_TITLE', $menuTypeTitle), 'list menumgr'); - } - else - { - ToolbarHelper::title(Text::_('COM_MENUS_VIEW_ITEMS_ALL_TITLE'), 'list menumgr'); - } - - if ($canDo->get('core.create')) - { - $toolbar->addNew('item.add'); - } - - $protected = $this->state->get('filter.menutype') == 'main'; - - if (($canDo->get('core.edit.state') || $this->getCurrentUser()->authorise('core.admin')) && !$protected - || $canDo->get('core.edit.state') && $this->state->get('filter.client_id') == 0) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state') && !$protected) - { - $childBar->publish('items.publish')->listCheck(true); - - $childBar->unpublish('items.unpublish')->listCheck(true); - } - - if ($this->getCurrentUser()->authorise('core.admin') && !$protected) - { - $childBar->checkin('items.checkin')->listCheck(true); - } - - if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) - { - if ($this->state->get('filter.client_id') == 0) - { - $childBar->makeDefault('items.setDefault')->listCheck(true); - } - - if (!$protected) - { - $childBar->trash('items.trash')->listCheck(true); - } - } - - // Add a batch button - if (!$protected && $user->authorise('core.create', 'com_menus') - && $user->authorise('core.edit', 'com_menus') - && $user->authorise('core.edit.state', 'com_menus')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if ($this->getCurrentUser()->authorise('core.admin')) - { - $toolbar->standardButton('refresh') - ->text('JTOOLBAR_REBUILD') - ->task('items.rebuild'); - } - - if (!$protected && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('items.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences('com_menus'); - } - - $toolbar->help('Menus:_Items'); - } + /** + * Array used for displaying the levels filter + * + * @var \stdClass[] + * @since 4.0.0 + */ + protected $f_levels; + + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Ordering of the items + * + * @var array + * @since 4.0.0 + */ + protected $ordering; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $lang = Factory::getLanguage(); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->total = $this->get('Total'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->ordering = array(); + + // Preprocess the list of items to find ordering divisions. + foreach ($this->items as $item) { + $this->ordering[$item->parent_id][] = $item->id; + + // Item type text + switch ($item->type) { + case 'url': + $value = Text::_('COM_MENUS_TYPE_EXTERNAL_URL'); + break; + + case 'alias': + $value = Text::_('COM_MENUS_TYPE_ALIAS'); + break; + + case 'separator': + $value = Text::_('COM_MENUS_TYPE_SEPARATOR'); + break; + + case 'heading': + $value = Text::_('COM_MENUS_TYPE_HEADING'); + break; + + case 'container': + $value = Text::_('COM_MENUS_TYPE_CONTAINER'); + break; + + case 'component': + default: + // Load language + $lang->load($item->componentname . '.sys', JPATH_ADMINISTRATOR) + || $lang->load($item->componentname . '.sys', JPATH_ADMINISTRATOR . '/components/' . $item->componentname); + + if (!empty($item->componentname)) { + $titleParts = array(); + $titleParts[] = Text::_($item->componentname); + $vars = null; + + parse_str($item->link, $vars); + + if (isset($vars['view'])) { + // Attempt to load the view xml file. + $file = JPATH_SITE . '/components/' . $item->componentname . '/views/' . $vars['view'] . '/metadata.xml'; + + if (!is_file($file)) { + $file = JPATH_SITE . '/components/' . $item->componentname . '/view/' . $vars['view'] . '/metadata.xml'; + } + + if (is_file($file) && $xml = simplexml_load_file($file)) { + // Look for the first view node off of the root node. + if ($view = $xml->xpath('view[1]')) { + // Add view title if present. + if (!empty($view[0]['title'])) { + $viewTitle = trim((string) $view[0]['title']); + + // Check if the key is valid. Needed due to B/C so we don't show untranslated keys. This check should be removed with Joomla 4. + if ($lang->hasKey($viewTitle)) { + $titleParts[] = Text::_($viewTitle); + } + } + } + } + + $vars['layout'] = $vars['layout'] ?? 'default'; + + // Attempt to load the layout xml file. + // If Alternative Menu Item, get template folder for layout file + if (strpos($vars['layout'], ':') > 0) { + // Use template folder for layout file + $temp = explode(':', $vars['layout']); + $file = JPATH_SITE . '/templates/' . $temp[0] . '/html/' . $item->componentname . '/' . $vars['view'] . '/' . $temp[1] . '.xml'; + + // Load template language file + $lang->load('tpl_' . $temp[0] . '.sys', JPATH_SITE) + || $lang->load('tpl_' . $temp[0] . '.sys', JPATH_SITE . '/templates/' . $temp[0]); + } else { + $base = $this->state->get('filter.client_id') == 0 ? JPATH_SITE : JPATH_ADMINISTRATOR; + + // Get XML file from component folder for standard layouts + $file = $base . '/components/' . $item->componentname . '/tmpl/' . $vars['view'] + . '/' . $vars['layout'] . '.xml'; + + if (!file_exists($file)) { + $file = $base . '/components/' . $item->componentname . '/views/' + . $vars['view'] . '/tmpl/' . $vars['layout'] . '.xml'; + + if (!file_exists($file)) { + $file = $base . '/components/' . $item->componentname . '/view/' + . $vars['view'] . '/tmpl/' . $vars['layout'] . '.xml'; + } + } + } + + if (is_file($file) && $xml = simplexml_load_file($file)) { + // Look for the first view node off of the root node. + if ($layout = $xml->xpath('layout[1]')) { + if (!empty($layout[0]['title'])) { + $titleParts[] = Text::_(trim((string) $layout[0]['title'])); + } + } + + if (!empty($layout[0]->message[0])) { + $item->item_type_desc = Text::_(trim((string) $layout[0]->message[0])); + } + } + + unset($xml); + + // Special case if neither a view nor layout title is found + if (count($titleParts) == 1) { + $titleParts[] = $vars['view']; + } + } + + $value = implode(' » ', $titleParts); + } else { + if (preg_match("/^index.php\?option=([a-zA-Z\-0-9_]*)/", $item->link, $result)) { + $value = Text::sprintf('COM_MENUS_TYPE_UNEXISTING', $result[1]); + } else { + $value = Text::_('COM_MENUS_TYPE_UNKNOWN'); + } + } + break; + } + + $item->item_type = $value; + $item->protected = $item->menutype == 'main'; + } + + // Levels filter. + $options = array(); + $options[] = HTMLHelper::_('select.option', '1', Text::_('J1')); + $options[] = HTMLHelper::_('select.option', '2', Text::_('J2')); + $options[] = HTMLHelper::_('select.option', '3', Text::_('J3')); + $options[] = HTMLHelper::_('select.option', '4', Text::_('J4')); + $options[] = HTMLHelper::_('select.option', '5', Text::_('J5')); + $options[] = HTMLHelper::_('select.option', '6', Text::_('J6')); + $options[] = HTMLHelper::_('select.option', '7', Text::_('J7')); + $options[] = HTMLHelper::_('select.option', '8', Text::_('J8')); + $options[] = HTMLHelper::_('select.option', '9', Text::_('J9')); + $options[] = HTMLHelper::_('select.option', '10', Text::_('J10')); + + $this->f_levels = $options; + + // We don't need toolbar in the modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } else { + // In menu associations modal we need to remove language filter if forcing a language. + if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) { + // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. + $languageXml = new \SimpleXMLElement(''); + $this->filterForm->setField($languageXml, 'filter', true); + + // Also, unset the active language filter so the search tools is not open by default with this filter. + unset($this->activeFilters['language']); + } + } + + // Allow a system plugin to insert dynamic menu types to the list shown in menus: + Factory::getApplication()->triggerEvent('onBeforeRenderMenuItems', array($this)); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $menutypeId = (int) $this->state->get('menutypeid'); + + $canDo = ContentHelper::getActions('com_menus', 'menu', (int) $menutypeId); + $user = $this->getCurrentUser(); + + // Get the menu title + $menuTypeTitle = $this->get('State')->get('menutypetitle'); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + if ($menuTypeTitle) { + ToolbarHelper::title(Text::sprintf('COM_MENUS_VIEW_ITEMS_MENU_TITLE', $menuTypeTitle), 'list menumgr'); + } else { + ToolbarHelper::title(Text::_('COM_MENUS_VIEW_ITEMS_ALL_TITLE'), 'list menumgr'); + } + + if ($canDo->get('core.create')) { + $toolbar->addNew('item.add'); + } + + $protected = $this->state->get('filter.menutype') == 'main'; + + if ( + ($canDo->get('core.edit.state') || $this->getCurrentUser()->authorise('core.admin')) && !$protected + || $canDo->get('core.edit.state') && $this->state->get('filter.client_id') == 0 + ) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state') && !$protected) { + $childBar->publish('items.publish')->listCheck(true); + + $childBar->unpublish('items.unpublish')->listCheck(true); + } + + if ($this->getCurrentUser()->authorise('core.admin') && !$protected) { + $childBar->checkin('items.checkin')->listCheck(true); + } + + if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) { + if ($this->state->get('filter.client_id') == 0) { + $childBar->makeDefault('items.setDefault')->listCheck(true); + } + + if (!$protected) { + $childBar->trash('items.trash')->listCheck(true); + } + } + + // Add a batch button + if ( + !$protected && $user->authorise('core.create', 'com_menus') + && $user->authorise('core.edit', 'com_menus') + && $user->authorise('core.edit.state', 'com_menus') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if ($this->getCurrentUser()->authorise('core.admin')) { + $toolbar->standardButton('refresh') + ->text('JTOOLBAR_REBUILD') + ->task('items.rebuild'); + } + + if (!$protected && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('items.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences('com_menus'); + } + + $toolbar->help('Menus:_Items'); + } } diff --git a/administrator/components/com_menus/src/View/Menu/HtmlView.php b/administrator/components/com_menus/src/View/Menu/HtmlView.php index 4ceeff9dd6030..13c422e68c50a 100644 --- a/administrator/components/com_menus/src/View/Menu/HtmlView.php +++ b/administrator/components/com_menus/src/View/Menu/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - $this->canDo = ContentHelper::getActions('com_menus', 'menu', $this->item->id); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - parent::display($tpl); - $this->addToolbar(); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $input = Factory::getApplication()->input; - $input->set('hidemainmenu', true); - - $isNew = ($this->item->id == 0); - - ToolbarHelper::title(Text::_($isNew ? 'COM_MENUS_VIEW_NEW_MENU_TITLE' : 'COM_MENUS_VIEW_EDIT_MENU_TITLE'), 'list menu'); - - $toolbarButtons = []; - - // If a new item, can save the item. Allow users with edit permissions to apply changes to prevent returning to grid. - if ($isNew && $this->canDo->get('core.create')) - { - if ($this->canDo->get('core.edit')) - { - ToolbarHelper::apply('menu.apply'); - } - - $toolbarButtons[] = ['save', 'menu.save']; - } - - // If user can edit, can save the item. - if (!$isNew && $this->canDo->get('core.edit')) - { - ToolbarHelper::apply('menu.apply'); - - $toolbarButtons[] = ['save', 'menu.save']; - } - - // If the user can create new items, allow them to see Save & New - if ($this->canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'menu.save2new']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if ($isNew) - { - ToolbarHelper::cancel('menu.cancel'); - } - else - { - ToolbarHelper::cancel('menu.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Menus:_Edit'); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The actions the user is authorised to perform + * + * @var CMSObject + */ + protected $canDo; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + $this->canDo = ContentHelper::getActions('com_menus', 'menu', $this->item->id); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + parent::display($tpl); + $this->addToolbar(); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $input = Factory::getApplication()->input; + $input->set('hidemainmenu', true); + + $isNew = ($this->item->id == 0); + + ToolbarHelper::title(Text::_($isNew ? 'COM_MENUS_VIEW_NEW_MENU_TITLE' : 'COM_MENUS_VIEW_EDIT_MENU_TITLE'), 'list menu'); + + $toolbarButtons = []; + + // If a new item, can save the item. Allow users with edit permissions to apply changes to prevent returning to grid. + if ($isNew && $this->canDo->get('core.create')) { + if ($this->canDo->get('core.edit')) { + ToolbarHelper::apply('menu.apply'); + } + + $toolbarButtons[] = ['save', 'menu.save']; + } + + // If user can edit, can save the item. + if (!$isNew && $this->canDo->get('core.edit')) { + ToolbarHelper::apply('menu.apply'); + + $toolbarButtons[] = ['save', 'menu.save']; + } + + // If the user can create new items, allow them to see Save & New + if ($this->canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'menu.save2new']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if ($isNew) { + ToolbarHelper::cancel('menu.cancel'); + } else { + ToolbarHelper::cancel('menu.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Menus:_Edit'); + } } diff --git a/administrator/components/com_menus/src/View/Menu/XmlView.php b/administrator/components/com_menus/src/View/Menu/XmlView.php index c10e4a5a56909..1f45e4b4d6f6c 100644 --- a/administrator/components/com_menus/src/View/Menu/XmlView.php +++ b/administrator/components/com_menus/src/View/Menu/XmlView.php @@ -1,4 +1,5 @@ input->getCmd('menutype'); - - if ($menutype) - { - $root = MenusHelper::getMenuItems($menutype, true); - } - - if (!$root->hasChildren()) - { - Log::add(Text::_('COM_MENUS_SELECT_MENU_FIRST_EXPORT'), Log::WARNING, 'jerror'); - - $app->redirect(Route::_('index.php?option=com_menus&view=menus', false)); - - return; - } - - $this->items = $root->getChildren(true); - - $xml = new \SimpleXMLElement('' - ); - - foreach ($this->items as $item) - { - $this->addXmlChild($xml, $item); - } - - if (headers_sent($file, $line)) - { - Log::add("Headers already sent at $file:$line.", Log::ERROR, 'jerror'); - - return; - } - - header('content-type: application/xml'); - header('content-disposition: attachment; filename="' . $menutype . '.xml"'); - header("Cache-Control: no-cache, must-revalidate"); - header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); - - $dom = new \DOMDocument; - $dom->preserveWhiteSpace = true; - $dom->formatOutput = true; - $dom->loadXML($xml->asXML()); - - echo $dom->saveXML(); - - $app->close(); - } - - /** - * Add a child node to the xml - * - * @param \SimpleXMLElement $xml The current XML node which would become the parent to the new node - * @param \stdClass $item The menuitem object to create the child XML node from - * - * @return void - * - * @since 3.8.0 - */ - protected function addXmlChild($xml, $item) - { - $node = $xml->addChild('menuitem'); - - $node['type'] = $item->type; - - if ($item->title) - { - $node['title'] = htmlentities($item->title, ENT_XML1); - } - - if ($item->link) - { - $node['link'] = $item->link; - } - - if ($item->element) - { - $node['element'] = $item->element; - } - - if (isset($item->class) && $item->class) - { - $node['class'] = htmlentities($item->class, ENT_XML1); - } - - if ($item->access) - { - $node['access'] = $item->access; - } - - if ($item->browserNav) - { - $node['target'] = '_blank'; - } - - if ($item->getParams() && $hideitems = $item->getParams()->get('hideitems')) - { - $item->getParams()->set('hideitems', $this->getModel('Menu')->getExtensionElementsForMenuItems($hideitems)); - - $node->addChild('params', htmlentities((string) $item->getParams(), ENT_XML1)); - } - - if (isset($item->submenu)) - { - foreach ($item->submenu as $sub) - { - $this->addXmlChild($node, $sub); - } - } - } + /** + * @var \stdClass[] + * + * @since 3.8.0 + */ + protected $items; + + /** + * @var \Joomla\CMS\Object\CMSObject + * + * @since 3.8.0 + */ + protected $state; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 3.8.0 + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $menutype = $app->input->getCmd('menutype'); + + if ($menutype) { + $root = MenusHelper::getMenuItems($menutype, true); + } + + if (!$root->hasChildren()) { + Log::add(Text::_('COM_MENUS_SELECT_MENU_FIRST_EXPORT'), Log::WARNING, 'jerror'); + + $app->redirect(Route::_('index.php?option=com_menus&view=menus', false)); + + return; + } + + $this->items = $root->getChildren(true); + + $xml = new \SimpleXMLElement(''); + + foreach ($this->items as $item) { + $this->addXmlChild($xml, $item); + } + + if (headers_sent($file, $line)) { + Log::add("Headers already sent at $file:$line.", Log::ERROR, 'jerror'); + + return; + } + + header('content-type: application/xml'); + header('content-disposition: attachment; filename="' . $menutype . '.xml"'); + header("Cache-Control: no-cache, must-revalidate"); + header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); + + $dom = new \DOMDocument(); + $dom->preserveWhiteSpace = true; + $dom->formatOutput = true; + $dom->loadXML($xml->asXML()); + + echo $dom->saveXML(); + + $app->close(); + } + + /** + * Add a child node to the xml + * + * @param \SimpleXMLElement $xml The current XML node which would become the parent to the new node + * @param \stdClass $item The menuitem object to create the child XML node from + * + * @return void + * + * @since 3.8.0 + */ + protected function addXmlChild($xml, $item) + { + $node = $xml->addChild('menuitem'); + + $node['type'] = $item->type; + + if ($item->title) { + $node['title'] = htmlentities($item->title, ENT_XML1); + } + + if ($item->link) { + $node['link'] = $item->link; + } + + if ($item->element) { + $node['element'] = $item->element; + } + + if (isset($item->class) && $item->class) { + $node['class'] = htmlentities($item->class, ENT_XML1); + } + + if ($item->access) { + $node['access'] = $item->access; + } + + if ($item->browserNav) { + $node['target'] = '_blank'; + } + + if ($item->getParams() && $hideitems = $item->getParams()->get('hideitems')) { + $item->getParams()->set('hideitems', $this->getModel('Menu')->getExtensionElementsForMenuItems($hideitems)); + + $node->addChild('params', htmlentities((string) $item->getParams(), ENT_XML1)); + } + + if (isset($item->submenu)) { + foreach ($item->submenu as $sub) { + $this->addXmlChild($node, $sub); + } + } + } } diff --git a/administrator/components/com_menus/src/View/Menus/HtmlView.php b/administrator/components/com_menus/src/View/Menus/HtmlView.php index e3ca25e2e3a13..9a9a81d36257e 100644 --- a/administrator/components/com_menus/src/View/Menus/HtmlView.php +++ b/administrator/components/com_menus/src/View/Menus/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->modules = $this->get('Modules'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - - if ($this->getLayout() == 'default') - { - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_menus'); - - ToolbarHelper::title(Text::_('COM_MENUS_VIEW_MENUS_TITLE'), 'list menumgr'); - - if ($canDo->get('core.create')) - { - ToolbarHelper::addNew('menu.add'); - } - - if ($canDo->get('core.delete')) - { - ToolbarHelper::divider(); - ToolbarHelper::deleteList('COM_MENUS_MENU_CONFIRM_DELETE', 'menus.delete', 'JTOOLBAR_DELETE'); - } - - if ($canDo->get('core.admin') && $this->state->get('client_id') == 1) - { - ToolbarHelper::custom('menu.exportXml', 'download', '', 'COM_MENUS_MENU_EXPORT_BUTTON', true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::divider(); - ToolbarHelper::preferences('com_menus'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Menus'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * List of all mod_mainmenu modules collated by menutype + * + * @var array + */ + protected $modules; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->modules = $this->get('Modules'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + + if ($this->getLayout() == 'default') { + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_menus'); + + ToolbarHelper::title(Text::_('COM_MENUS_VIEW_MENUS_TITLE'), 'list menumgr'); + + if ($canDo->get('core.create')) { + ToolbarHelper::addNew('menu.add'); + } + + if ($canDo->get('core.delete')) { + ToolbarHelper::divider(); + ToolbarHelper::deleteList('COM_MENUS_MENU_CONFIRM_DELETE', 'menus.delete', 'JTOOLBAR_DELETE'); + } + + if ($canDo->get('core.admin') && $this->state->get('client_id') == 1) { + ToolbarHelper::custom('menu.exportXml', 'download', '', 'COM_MENUS_MENU_EXPORT_BUTTON', true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::divider(); + ToolbarHelper::preferences('com_menus'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Menus'); + } } diff --git a/administrator/components/com_menus/src/View/Menutypes/HtmlView.php b/administrator/components/com_menus/src/View/Menutypes/HtmlView.php index 5844c40e6fb82..28ff53640b912 100644 --- a/administrator/components/com_menus/src/View/Menutypes/HtmlView.php +++ b/administrator/components/com_menus/src/View/Menutypes/HtmlView.php @@ -1,4 +1,5 @@ recordId = $app->input->getInt('recordId'); - - $types = $this->get('TypeOptions'); - - $this->addCustomTypes($types); - - $sortedTypes = array(); - - foreach ($types as $name => $list) - { - $tmp = array(); - - foreach ($list as $item) - { - $tmp[Text::_($item->title)] = $item; - } - - uksort($tmp, 'strcasecmp'); - $sortedTypes[Text::_($name)] = $tmp; - } - - uksort($sortedTypes, 'strcasecmp'); - - $this->types = $sortedTypes; - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.0 - */ - protected function addToolbar() - { - // Add page title - ToolbarHelper::title(Text::_('COM_MENUS'), 'list menumgr'); - - // Get the toolbar object instance - $bar = Toolbar::getInstance('toolbar'); - - // Cancel - $title = Text::_('JTOOLBAR_CANCEL'); - $dhtml = ""; - $bar->appendButton('Custom', $dhtml, 'new'); - } - - /** - * Method to add system link types to the link types array - * - * @param array $types The list of link types - * - * @return void - * - * @since 3.7.0 - */ - protected function addCustomTypes(&$types) - { - if (empty($types)) - { - $types = array(); - } - - // Adding System Links - $list = array(); - $o = new CMSObject; - $o->title = 'COM_MENUS_TYPE_EXTERNAL_URL'; - $o->type = 'url'; - $o->description = 'COM_MENUS_TYPE_EXTERNAL_URL_DESC'; - $o->request = null; - $list[] = $o; - - $o = new CMSObject; - $o->title = 'COM_MENUS_TYPE_ALIAS'; - $o->type = 'alias'; - $o->description = 'COM_MENUS_TYPE_ALIAS_DESC'; - $o->request = null; - $list[] = $o; - - $o = new CMSObject; - $o->title = 'COM_MENUS_TYPE_SEPARATOR'; - $o->type = 'separator'; - $o->description = 'COM_MENUS_TYPE_SEPARATOR_DESC'; - $o->request = null; - $list[] = $o; - - $o = new CMSObject; - $o->title = 'COM_MENUS_TYPE_HEADING'; - $o->type = 'heading'; - $o->description = 'COM_MENUS_TYPE_HEADING_DESC'; - $o->request = null; - $list[] = $o; - - if ($this->get('state')->get('client_id') == 1) - { - $o = new CMSObject; - $o->title = 'COM_MENUS_TYPE_CONTAINER'; - $o->type = 'container'; - $o->description = 'COM_MENUS_TYPE_CONTAINER_DESC'; - $o->request = null; - $list[] = $o; - } - - $types['COM_MENUS_TYPE_SYSTEM'] = $list; - } + $bar->appendButton('Custom', $dhtml, 'new'); + } + + /** + * Method to add system link types to the link types array + * + * @param array $types The list of link types + * + * @return void + * + * @since 3.7.0 + */ + protected function addCustomTypes(&$types) + { + if (empty($types)) { + $types = array(); + } + + // Adding System Links + $list = array(); + $o = new CMSObject(); + $o->title = 'COM_MENUS_TYPE_EXTERNAL_URL'; + $o->type = 'url'; + $o->description = 'COM_MENUS_TYPE_EXTERNAL_URL_DESC'; + $o->request = null; + $list[] = $o; + + $o = new CMSObject(); + $o->title = 'COM_MENUS_TYPE_ALIAS'; + $o->type = 'alias'; + $o->description = 'COM_MENUS_TYPE_ALIAS_DESC'; + $o->request = null; + $list[] = $o; + + $o = new CMSObject(); + $o->title = 'COM_MENUS_TYPE_SEPARATOR'; + $o->type = 'separator'; + $o->description = 'COM_MENUS_TYPE_SEPARATOR_DESC'; + $o->request = null; + $list[] = $o; + + $o = new CMSObject(); + $o->title = 'COM_MENUS_TYPE_HEADING'; + $o->type = 'heading'; + $o->description = 'COM_MENUS_TYPE_HEADING_DESC'; + $o->request = null; + $list[] = $o; + + if ($this->get('state')->get('client_id') == 1) { + $o = new CMSObject(); + $o->title = 'COM_MENUS_TYPE_CONTAINER'; + $o->type = 'container'; + $o->description = 'COM_MENUS_TYPE_CONTAINER_DESC'; + $o->request = null; + $list[] = $o; + } + + $types['COM_MENUS_TYPE_SYSTEM'] = $list; + } } diff --git a/administrator/components/com_menus/tmpl/item/edit.php b/administrator/components/com_menus/tmpl/item/edit.php index dc9f5991e1774..bd89bd47306ef 100644 --- a/administrator/components/com_menus/tmpl/item/edit.php +++ b/administrator/components/com_menus/tmpl/item/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_menus.admin-item-edit'); + ->useScript('form.validate') + ->useScript('com_menus.admin-item-edit'); $assoc = Associations::isEnabled(); $input = Factory::getApplication()->input; @@ -40,166 +41,158 @@ $lang = Factory::getLanguage()->getTag(); // Load mod_menu.ini file when client is administrator -if ($clientId === 1) -{ - Factory::getLanguage()->load('mod_menu', JPATH_ADMINISTRATOR); +if ($clientId === 1) { + Factory::getLanguage()->load('mod_menu', JPATH_ADMINISTRATOR); } ?> - - - - item->id != 0) : ?> -
-
-
-
- -
-
- -
-
-
-
- - -
- - 'details', 'recall' => true, 'breakpoint' => 768]); ?> - - -
-
- form->renderField('type'); - - if ($this->item->type == 'alias') - { - echo $this->form->renderField('aliasoptions', 'params'); - } - - if ($this->item->type == 'separator') - { - echo $this->form->renderField('text_separator', 'params'); - } - - echo $this->form->renderFieldset('request'); - - if ($this->item->type == 'url') - { - $this->form->setFieldAttribute('link', 'readonly', 'false'); - $this->form->setFieldAttribute('link', 'required', 'true'); - } - - echo $this->form->renderField('link'); - - if ($this->item->type == 'alias') - { - echo $this->form->renderField('alias_redirect', 'params'); - } - - echo $this->form->renderField('browserNav'); - echo $this->form->renderField('template_style_id'); - - if (!$isModal && $this->item->type == 'container') - { - echo $this->loadTemplate('container'); - } - ?> -
-
- fields = array( - 'id', - 'client_id', - 'menutype', - 'parent_id', - 'menuordering', - 'published', - 'publish_up', - 'publish_down', - 'home', - 'access', - 'language', - 'note', - ); - - if ($this->item->type != 'component') - { - $this->fields = array_diff($this->fields, array('home')); - $this->form->setFieldAttribute('publish_up', 'showon', ''); - $this->form->setFieldAttribute('publish_down', 'showon', ''); - } - ?> - fields = array( - 'id', - 'client_id', - 'menutype', - 'parent_id', - 'menuordering', - 'published', - 'home', - 'publish_up', - 'publish_down', - 'access', - 'language', - 'note', - ); - - if ($this->item->type != 'component') - { - $this->fields = array_diff($this->fields, array('home')); - $this->form->setFieldAttribute('publish_up', 'showon', ''); - $this->form->setFieldAttribute('publish_down', 'showon', ''); - } - - echo LayoutHelper::render('joomla.edit.global', $this); ?> -
-
- - - fieldsets = array(); - $this->ignore_fieldsets = array('aliasoptions', 'request', 'item_associations'); - echo LayoutHelper::render('joomla.edit.params', $this); - ?> - - state->get('item.client_id') != 1) : ?> - -
- -
- -
-
- - - - - - modules)) : ?> - -
- -
- loadTemplate('modules'); ?> -
-
- - - - -
- - - - - form->getInput('component_id'); ?> - - + + + + item->id != 0) : ?> +
+
+
+
+ +
+
+ +
+
+
+
+ + +
+ + 'details', 'recall' => true, 'breakpoint' => 768]); ?> + + +
+
+ form->renderField('type'); + + if ($this->item->type == 'alias') { + echo $this->form->renderField('aliasoptions', 'params'); + } + + if ($this->item->type == 'separator') { + echo $this->form->renderField('text_separator', 'params'); + } + + echo $this->form->renderFieldset('request'); + + if ($this->item->type == 'url') { + $this->form->setFieldAttribute('link', 'readonly', 'false'); + $this->form->setFieldAttribute('link', 'required', 'true'); + } + + echo $this->form->renderField('link'); + + if ($this->item->type == 'alias') { + echo $this->form->renderField('alias_redirect', 'params'); + } + + echo $this->form->renderField('browserNav'); + echo $this->form->renderField('template_style_id'); + + if (!$isModal && $this->item->type == 'container') { + echo $this->loadTemplate('container'); + } + ?> +
+
+ fields = array( + 'id', + 'client_id', + 'menutype', + 'parent_id', + 'menuordering', + 'published', + 'publish_up', + 'publish_down', + 'home', + 'access', + 'language', + 'note', + ); + + if ($this->item->type != 'component') { + $this->fields = array_diff($this->fields, array('home')); + $this->form->setFieldAttribute('publish_up', 'showon', ''); + $this->form->setFieldAttribute('publish_down', 'showon', ''); + } + ?> + fields = array( + 'id', + 'client_id', + 'menutype', + 'parent_id', + 'menuordering', + 'published', + 'home', + 'publish_up', + 'publish_down', + 'access', + 'language', + 'note', + ); + + if ($this->item->type != 'component') { + $this->fields = array_diff($this->fields, array('home')); + $this->form->setFieldAttribute('publish_up', 'showon', ''); + $this->form->setFieldAttribute('publish_down', 'showon', ''); + } + + echo LayoutHelper::render('joomla.edit.global', $this); ?> +
+
+ + + fieldsets = array(); + $this->ignore_fieldsets = array('aliasoptions', 'request', 'item_associations'); + echo LayoutHelper::render('joomla.edit.params', $this); + ?> + + state->get('item.client_id') != 1) : ?> + +
+ +
+ +
+
+ + + + + + modules)) : ?> + +
+ +
+ loadTemplate('modules'); ?> +
+
+ + + + +
+ + + + + form->getInput('component_id'); ?> + + diff --git a/administrator/components/com_menus/tmpl/item/edit_container.php b/administrator/components/com_menus/tmpl/item/edit_container.php index 09d6ef94bec7f..53f7d07060971 100644 --- a/administrator/components/com_menus/tmpl/item/edit_container.php +++ b/administrator/components/com_menus/tmpl/item/edit_container.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; @@ -18,92 +20,86 @@ /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('joomla.treeselectmenu') - ->useStyle('com_menus.admin-item-edit-container') - ->useScript('com_menus.admin-item-edit-container'); + ->useStyle('com_menus.admin-item-edit-container') + ->useScript('com_menus.admin-item-edit-container'); ?> diff --git a/administrator/components/com_menus/tmpl/item/edit_modules.php b/administrator/components/com_menus/tmpl/item/edit_modules.php index 2088aa583a49b..d8f7ae83481a8 100644 --- a/administrator/components/com_menus/tmpl/item/edit_modules.php +++ b/administrator/components/com_menus/tmpl/item/edit_modules.php @@ -1,4 +1,5 @@ levels as $key => $value) -{ - $allLevels[$value->id] = $value->title; +foreach ($this->levels as $key => $value) { + $allLevels[$value->id] = $value->title; } $this->document->addScriptOptions('menus-edit-modules', ['viewLevels' => $allLevels, 'itemId' => $this->item->id]); @@ -23,26 +23,26 @@ /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useStyle('com_menus.admin-item-edit-modules') - ->useScript('com_menus.admin-item-edit-modules'); + ->useScript('com_menus.admin-item-edit-modules'); // Set up the bootstrap modal that will be used for all module editors echo HTMLHelper::_( - 'bootstrap.renderModal', - 'moduleEditModal', - array( - 'title' => Text::_('COM_MENUS_EDIT_MODULE_SETTINGS'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'bodyHeight' => '70', - 'modalWidth' => '80', - 'footer' => '' - . '' - . '', - ) + 'bootstrap.renderModal', + 'moduleEditModal', + array( + 'title' => Text::_('COM_MENUS_EDIT_MODULE_SETTINGS'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'bodyHeight' => '70', + 'modalWidth' => '80', + 'footer' => '' + . '' + . '', + ) ); ?> @@ -53,97 +53,97 @@ echo LayoutHelper::render('joomla.menu.edit_modules', $this); ?> - - - - - - - - - - - - modules as $i => &$module) : ?> - menuid)) : ?> - except || $module->menuid < 0) : ?> - - - - - - - - published) : ?> - - - - - - - - - - - - - + + + + + + + + + + + + modules as $i => &$module) : ?> + menuid)) : ?> + except || $module->menuid < 0) : ?> + + + + + + + + published) : ?> + + + + + + + + + + + + +
- -
- - - - - - - - - -
- - - escape($module->access_title); ?> - - escape($module->position); ?> - - published) : ?> - - - - - - - - -
+ +
+ + + + + + + + + +
+ + + escape($module->access_title); ?> + + escape($module->position); ?> + + published) : ?> + + + + + + + + +
diff --git a/administrator/components/com_menus/tmpl/item/modal.php b/administrator/components/com_menus/tmpl/item/modal.php index 2a62e7aa4c3a1..f5c2253fc4208 100644 --- a/administrator/components/com_menus/tmpl/item/modal.php +++ b/administrator/components/com_menus/tmpl/item/modal.php @@ -1,4 +1,5 @@
- setLayout('edit'); ?> - loadTemplate(); ?> + setLayout('edit'); ?> + loadTemplate(); ?>
diff --git a/administrator/components/com_menus/tmpl/items/default.php b/administrator/components/com_menus/tmpl/items/default.php index 7b8a79ba495f3..33900dd7c4346 100644 --- a/administrator/components/com_menus/tmpl/items/default.php +++ b/administrator/components/com_menus/tmpl/items/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $app = Factory::getApplication(); @@ -32,10 +33,9 @@ $saveOrder = ($listOrder == 'a.lft' && strtolower($listDirn) == 'asc'); $menuType = (string) $app->getUserState('com_menus.items.menutype', ''); -if ($saveOrder && $menuType && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_menus&task=items.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && $menuType && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_menus&task=items.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } $assoc = Associations::isEnabled() && $this->state->get('filter.client_id') == 0; @@ -43,248 +43,241 @@ ?>
-
-
-
- $this, 'options' => array('selectorFieldName' => 'menutype'))); ?> - items)) : ?> - - - - - - - - - - - - state->get('filter.client_id') == 0) : ?> - - - state->get('filter.client_id') == 0) : ?> - - - - - - state->get('filter.client_id') == 0) && (Multilanguage::isEnabled())) : ?> - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="false"> - items as $i => $item) : - $orderkey = array_search($item->id, $this->ordering[$item->parent_id]); - $canCreate = $user->authorise('core.create', 'com_menus.menu.' . $item->menutype_id); - $canEdit = $user->authorise('core.edit', 'com_menus.menu.' . $item->menutype_id); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_menus.menu.' . $item->menutype_id) && $canCheckin; + id="adminForm"> +
+
+
+ $this, 'options' => array('selectorFieldName' => 'menutype'))); ?> + items)) : ?> +
+ + + + + + + + + + + state->get('filter.client_id') == 0) : ?> + + + state->get('filter.client_id') == 0) : ?> + + + + + + state->get('filter.client_id') == 0) && (Multilanguage::isEnabled())) : ?> + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="false"> + items as $i => $item) : + $orderkey = array_search($item->id, $this->ordering[$item->parent_id]); + $canCreate = $user->authorise('core.create', 'com_menus.menu.' . $item->menutype_id); + $canEdit = $user->authorise('core.edit', 'com_menus.menu.' . $item->menutype_id); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_menus.menu.' . $item->menutype_id) && $canCheckin; - // Get the parents of item for sorting - if ($item->level > 1) - { - $parentsStr = ''; - $_currentParentId = $item->parent_id; - $parentsStr = ' ' . $_currentParentId; + // Get the parents of item for sorting + if ($item->level > 1) { + $parentsStr = ''; + $_currentParentId = $item->parent_id; + $parentsStr = ' ' . $_currentParentId; - for ($j = 0; $j < $item->level; $j++) - { - foreach ($this->ordering as $k => $v) - { - $v = implode('-', $v); - $v = '-' . $v . '-'; + for ($j = 0; $j < $item->level; $j++) { + foreach ($this->ordering as $k => $v) { + $v = implode('-', $v); + $v = '-' . $v . '-'; - if (strpos($v, '-' . $_currentParentId . '-') !== false) - { - $parentsStr .= ' ' . $k; - $_currentParentId = $k; - break; - } - } - } - } - else - { - $parentsStr = ''; - } - ?> - - - - + + + - - - - - state->get('filter.client_id') == 0) : ?> - - - state->get('filter.client_id') == 0) : ?> - - - - - - state->get('filter.client_id') == 0 && Multilanguage::isEnabled()) : ?> - - - - - - - + if (!$canChange) { + $iconClass = ' inactive'; + } elseif (!$saveOrder) { + $iconClass = ' inactive" title="' . Text::_('JORDERINGDISABLED'); + } + ?> + + + + + + + + + + published, $i, 'items.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> + + + $item->level)); ?> + + checked_out) : ?> + editor, $item->checked_out_time, 'items.', $canCheckin); ?> + + protected) : ?> + + escape($item->title); ?> + + escape($item->title); ?> + + params); ?> +
+ + + type != 'url') : ?> + note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + + type == 'url' && $item->note) : ?> + escape($item->note)); ?> + + +
+
+ + + escape($item->item_type); ?> + +
+ type === 'component' && !$item->enabled) : ?> +
+ + enabled === null ? 'JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND' : 'COM_MENUS_LABEL_DISABLED'); ?> + +
+ + + + escape($item->menutype_title ?: ucwords($item->menutype)); ?> + + state->get('filter.client_id') == 0) : ?> + + type == 'component') : ?> + language == '*' || $item->home == '0') : ?> + home, $i, 'items.', ($item->language != '*' || !$item->home) && $canChange && !$item->protected, 'cb', null, 'icon-home', 'icon-circle'); ?> + + + language_image) : ?> + language_image . '.gif', $item->language_title, array('title' => Text::sprintf('COM_MENUS_GRID_UNSET_LANGUAGE', $item->language_title)), true); ?> + + language; ?> + + + + language_image) : ?> + language_image . '.gif', $item->language_title, array('title' => $item->language_title), true); ?> + + language; ?> + + + + + + state->get('filter.client_id') == 0) : ?> + + escape($item->access_level); ?> + + + + + association) : ?> + id); ?> + + + + state->get('filter.client_id') == 0 && Multilanguage::isEnabled()) : ?> + + + + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', 'com_menus') || $user->authorise('core.edit', 'com_menus')) : ?> - Text::_('COM_MENUS_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer') - ), - $this->loadTemplate('batch_body') - ); ?> - - + + authorise('core.create', 'com_menus') || $user->authorise('core.edit', 'com_menus')) : ?> + Text::_('COM_MENUS_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer') + ), + $this->loadTemplate('batch_body') + ); ?> + + - - - -
-
-
+ + + + + +
diff --git a/administrator/components/com_menus/tmpl/items/default_batch_body.php b/administrator/components/com_menus/tmpl/items/default_batch_body.php index acb8b8e97b71f..8a0c950eab285 100644 --- a/administrator/components/com_menus/tmpl/items/default_batch_body.php +++ b/administrator/components/com_menus/tmpl/items/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Factory; @@ -15,73 +17,72 @@ use Joomla\CMS\Layout\LayoutHelper; $options = [ - HTMLHelper::_('select.option', 'c', Text::_('JLIB_HTML_BATCH_COPY')), - HTMLHelper::_('select.option', 'm', Text::_('JLIB_HTML_BATCH_MOVE')) + HTMLHelper::_('select.option', 'c', Text::_('JLIB_HTML_BATCH_COPY')), + HTMLHelper::_('select.option', 'm', Text::_('JLIB_HTML_BATCH_MOVE')) ]; $published = (int) $this->state->get('filter.published'); $clientId = (int) $this->state->get('filter.client_id'); $menuType = Factory::getApplication()->getUserState('com_menus.items.menutype', ''); -if ($clientId == 1) -{ - /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = $this->document->getWebAssetManager(); - $wa->useScript('com_menus.batch-body'); - $wa->useScript('joomla.batch-copymove'); +if ($clientId == 1) { + /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = $this->document->getWebAssetManager(); + $wa->useScript('com_menus.batch-body'); + $wa->useScript('joomla.batch-copymove'); } ?>
- - -
- -
-
- -
-
- -
-
- -
-
-
- -
- = 0) : ?> -
-
- - -
+ + +
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+ = 0) : ?> +
+
+ + +
-
- - -
-
- +
+ + +
+
+ - -

- -
- -
-

-
- + +

+ +
+ +
+

+
+
diff --git a/administrator/components/com_menus/tmpl/items/default_batch_footer.php b/administrator/components/com_menus/tmpl/items/default_batch_footer.php index 6ce0e9df6c672..4f2df096275cf 100644 --- a/administrator/components/com_menus/tmpl/items/default_batch_footer.php +++ b/administrator/components/com_menus/tmpl/items/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Factory; @@ -16,10 +18,10 @@ $menuType = Factory::getApplication()->getUserState('com_menus.items.menutype', ''); ?> -= 0 && $clientId == 1)): ?> - += 0 && $clientId == 1)) : ?> + diff --git a/administrator/components/com_menus/tmpl/items/modal.php b/administrator/components/com_menus/tmpl/items/modal.php index f4c6afef18278..e592075877504 100644 --- a/administrator/components/com_menus/tmpl/items/modal.php +++ b/administrator/components/com_menus/tmpl/items/modal.php @@ -1,4 +1,5 @@ isClient('site')) -{ - Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); +if ($app->isClient('site')) { + Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); } /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ @@ -35,162 +35,155 @@ $link = 'index.php?option=com_menus&view=items&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; $multilang = Multilanguage::isEnabled(); -if (!empty($editor)) -{ - // This view is used also in com_menus. Load the xtd script only if the editor is set! - $this->document->addScriptOptions('xtd-menus', array('editor' => $editor)); - $onclick = "jSelectMenuItem"; - $link = 'index.php?option=com_menus&view=items&layout=modal&tmpl=component&editor=' . $editor . '&' . Session::getFormToken() . '=1'; +if (!empty($editor)) { + // This view is used also in com_menus. Load the xtd script only if the editor is set! + $this->document->addScriptOptions('xtd-menus', array('editor' => $editor)); + $onclick = "jSelectMenuItem"; + $link = 'index.php?option=com_menus&view=items&layout=modal&tmpl=component&editor=' . $editor . '&' . Session::getFormToken() . '=1'; } ?>
-
- $this, 'options' => array('selectorFieldName' => 'menutype'))); ?> + + $this, 'options' => array('selectorFieldName' => 'menutype'))); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - items as $i => $item) : ?> - type, array('separator', 'heading', 'alias', 'url', 'container')); ?> - language && $multilang) - { - if ($item->language !== '*') - { - $language = $item->language; - } - else - { - $language = ''; - } - } - elseif (!$multilang) - { - $language = ''; - } - ?> - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - -
- published, $i, 'items.', false, 'cb', $item->publish_up, $item->publish_down); ?> - - $item->level)); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - - params); ?> -
- - - note)) : ?> - escape($item->alias)); ?> - - escape($item->alias), $this->escape($item->note)); ?> - - -
-
- - - escape($item->item_type); ?> - -
- type === 'component' && !$item->enabled) : ?> -
- - enabled === null ? 'JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND' : 'COM_MENUS_LABEL_DISABLED'); ?> - -
- -
- escape($item->menutype_title); ?> - - type == 'component') : ?> - language == '*' || $item->home == '0') : ?> - home, $i, 'items.', ($item->language != '*' || !$item->home) && false && !$item->protected, 'cb', null, 'home', 'circle'); ?> - - language_image) : ?> - language_image . '.gif', $item->language_title, array('title' => $item->language_title), true); ?> - - language; ?> - - - - - escape($item->access_level); ?> - - language == '') : ?> - - language == '*') : ?> - - - - - - id; ?> -
+ items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + items as $i => $item) : ?> + type, array('separator', 'heading', 'alias', 'url', 'container')); ?> + language && $multilang) { + if ($item->language !== '*') { + $language = $item->language; + } else { + $language = ''; + } + } elseif (!$multilang) { + $language = ''; + } + ?> + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + +
+ published, $i, 'items.', false, 'cb', $item->publish_up, $item->publish_down); ?> + + $item->level)); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + + params); ?> +
+ + + note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + + +
+
+ + + escape($item->item_type); ?> + +
+ type === 'component' && !$item->enabled) : ?> +
+ + enabled === null ? 'JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND' : 'COM_MENUS_LABEL_DISABLED'); ?> + +
+ +
+ escape($item->menutype_title); ?> + + type == 'component') : ?> + language == '*' || $item->home == '0') : ?> + home, $i, 'items.', ($item->language != '*' || !$item->home) && false && !$item->protected, 'cb', null, 'home', 'circle'); ?> + + language_image) : ?> + language_image . '.gif', $item->language_title, array('title' => $item->language_title), true); ?> + + language; ?> + + + + + escape($item->access_level); ?> + + language == '') : ?> + + language == '*') : ?> + + + + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - - - + + + + + -
+
diff --git a/administrator/components/com_menus/tmpl/menu/edit.php b/administrator/components/com_menus/tmpl/menu/edit.php index 7959fda2742d5..186b659dce12b 100644 --- a/administrator/components/com_menus/tmpl/menu/edit.php +++ b/administrator/components/com_menus/tmpl/menu/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('core') - ->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('keepalive') + ->useScript('form.validate'); Text::script('ERROR'); ?>
- + -
- 'details', 'recall' => true, 'breakpoint' => 768]); ?> +
+ 'details', 'recall' => true, 'breakpoint' => 768]); ?> - + -
- +
+ -
-
- form->renderField('menutype'); +
+
+ form->renderField('menutype'); - echo $this->form->renderField('description'); + echo $this->form->renderField('description'); - echo $this->form->renderField('client_id'); + echo $this->form->renderField('client_id'); - echo $this->form->renderField('preset'); - ?> -
-
-
+ echo $this->form->renderField('preset'); + ?> +
+
+ - + - canDo->get('core.admin')) : ?> - -
- -
- form->getInput('rules'); ?> -
-
- - + canDo->get('core.admin')) : ?> + +
+ +
+ form->getInput('rules'); ?> +
+
+ + - - - - + + + +
diff --git a/administrator/components/com_menus/tmpl/menus/default.php b/administrator/components/com_menus/tmpl/menus/default.php index 29d4da506d5da..09cc238824ea0 100644 --- a/administrator/components/com_menus/tmpl/menus/default.php +++ b/administrator/components/com_menus/tmpl/menus/default.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Factory; @@ -18,8 +20,8 @@ /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ $wa = $this->document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect') - ->useScript('com_menus.admin-menus'); + ->useScript('multiselect') + ->useScript('com_menus.admin-menus'); $uri = Uri::getInstance(); $return = base64_encode($uri); @@ -29,242 +31,240 @@ $modMenuId = (int) $this->get('ModMenuId'); $itemIds = []; -foreach ($this->items as $item) -{ - if ($user->authorise('core.edit', 'com_menus')) - { - $itemIds[] = $item->id; - } +foreach ($this->items as $item) { + if ($user->authorise('core.edit', 'com_menus')) { + $itemIds[] = $item->id; + } } $this->document->addScriptOptions('menus-default', ['items' => $itemIds]); ?>
-
-
-
- $this, 'options' => array('filterButton' => false))); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - items as $i => $item) : - $canEdit = $user->authorise('core.edit', 'com_menus.menu.' . (int) $item->id); - $canManageItems = $user->authorise('core.manage', 'com_menus.menu.' . (int) $item->id); - ?> - - - - - - - - - - - - - +
+
+
+ $this, 'options' => array('filterButton' => false))); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + items as $i => $item) : + $canEdit = $user->authorise('core.edit', 'com_menus.menu.' . (int) $item->id); + $canManageItems = $user->authorise('core.manage', 'com_menus.menu.' . (int) $item->id); + ?> + + + + + + + + + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
-
-
+ + + +
+
+
diff --git a/administrator/components/com_menus/tmpl/menutypes/default.php b/administrator/components/com_menus/tmpl/menutypes/default.php index 9a5c7efeae1b1..a0a32a9d6f658 100644 --- a/administrator/components/com_menus/tmpl/menutypes/default.php +++ b/administrator/components/com_menus/tmpl/menutypes/default.php @@ -1,4 +1,5 @@ 'slide1')); ?> - - types as $name => $list) : ?> - -
- $item) : ?> - $this->recordId, 'title' => $item->type ?? $item->title, 'request' => $item->request); ?> - - -
- -
- - description); ?> - -
- -
- - + + types as $name => $list) : ?> + +
+ $item) : ?> + $this->recordId, 'title' => $item->type ?? $item->title, 'request' => $item->request); ?> + + +
+ +
+ + description); ?> + +
+ +
+ + registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Messages')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Messages')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Messages')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Messages')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MessagesComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MessagesComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_messages/src/Controller/ConfigController.php b/administrator/components/com_messages/src/Controller/ConfigController.php index cf76dd1ddb86d..37d5b9097b388 100644 --- a/administrator/components/com_messages/src/Controller/ConfigController.php +++ b/administrator/components/com_messages/src/Controller/ConfigController.php @@ -1,4 +1,5 @@ checkToken(); - - $model = $this->getModel('Config'); - $data = $this->input->post->get('jform', array(), 'array'); - - // Validate the posted data. - $form = $model->getForm(); - - if (!$form) - { - throw new \Exception($model->getError(), 500); - } - - $data = $model->validate($form, $data); - - // Check for validation errors. - if ($data === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $this->app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Redirect back to the main list. - $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); - - return false; - } - - // Attempt to save the data. - if (!$model->save($data)) - { - // Redirect back to the main list. - $this->setMessage(Text::sprintf('JERROR_SAVE_FAILED', $model->getError()), 'warning'); - $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); - - return false; - } - - // Redirect to the list screen. - $this->setMessage(Text::_('COM_MESSAGES_CONFIG_SAVED')); - $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); - - return true; - } - - /** - * Cancel operation. - * - * @return void - * - * @since 4.0.0 - */ - public function cancel() - { - $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); - } + /** + * Method to save a record. + * + * @return boolean + * + * @since 1.6 + */ + public function save() + { + // Check for request forgeries. + $this->checkToken(); + + $model = $this->getModel('Config'); + $data = $this->input->post->get('jform', array(), 'array'); + + // Validate the posted data. + $form = $model->getForm(); + + if (!$form) { + throw new \Exception($model->getError(), 500); + } + + $data = $model->validate($form, $data); + + // Check for validation errors. + if ($data === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $this->app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Redirect back to the main list. + $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); + + return false; + } + + // Attempt to save the data. + if (!$model->save($data)) { + // Redirect back to the main list. + $this->setMessage(Text::sprintf('JERROR_SAVE_FAILED', $model->getError()), 'warning'); + $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); + + return false; + } + + // Redirect to the list screen. + $this->setMessage(Text::_('COM_MESSAGES_CONFIG_SAVED')); + $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); + + return true; + } + + /** + * Cancel operation. + * + * @return void + * + * @since 4.0.0 + */ + public function cancel() + { + $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); + } } diff --git a/administrator/components/com_messages/src/Controller/DisplayController.php b/administrator/components/com_messages/src/Controller/DisplayController.php index f7115d296aa79..55c8662bf06a8 100644 --- a/administrator/components/com_messages/src/Controller/DisplayController.php +++ b/administrator/components/com_messages/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'messages'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); - - // Check for edit form. - if ($view == 'message' && $layout == 'edit' && !$this->checkEditId('com_messages.edit.message', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); - - return false; - } - - return parent::display(); - } + /** + * The default view. + * + * @var string + * @since 1.6 + */ + protected $default_view = 'messages'; + + /** + * Method to display a view. + * + * @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()}. + * + * @return static|boolean This object to support chaining or false on failure. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $view = $this->input->get('view', 'messages'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); + + // Check for edit form. + if ($view == 'message' && $layout == 'edit' && !$this->checkEditId('com_messages.edit.message', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_messages&view=messages', false)); + + return false; + } + + return parent::display(); + } } diff --git a/administrator/components/com_messages/src/Controller/MessageController.php b/administrator/components/com_messages/src/Controller/MessageController.php index 44b8361751cf1..f8b0349cb4fbe 100644 --- a/administrator/components/com_messages/src/Controller/MessageController.php +++ b/administrator/components/com_messages/src/Controller/MessageController.php @@ -1,4 +1,5 @@ input->getInt('reply_id')) - { - $this->setRedirect('index.php?option=com_messages&view=message&layout=edit&reply_id=' . $replyId); - } - else - { - $this->setMessage(Text::_('COM_MESSAGES_INVALID_REPLY_ID')); - $this->setRedirect('index.php?option=com_messages&view=messages'); - } - } + /** + * Reply to an existing message. + * + * This is a simple redirect to the compose form. + * + * @return void + * + * @since 1.6 + */ + public function reply() + { + if ($replyId = $this->input->getInt('reply_id')) { + $this->setRedirect('index.php?option=com_messages&view=message&layout=edit&reply_id=' . $replyId); + } else { + $this->setMessage(Text::_('COM_MESSAGES_INVALID_REPLY_ID')); + $this->setRedirect('index.php?option=com_messages&view=messages'); + } + } } diff --git a/administrator/components/com_messages/src/Controller/MessagesController.php b/administrator/components/com_messages/src/Controller/MessagesController.php index 0d160b86d827f..38a2bd3d52c58 100644 --- a/administrator/components/com_messages/src/Controller/MessagesController.php +++ b/administrator/components/com_messages/src/Controller/MessagesController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * 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 object The model. + * + * @since 1.6 + */ + public function getModel($name = 'Message', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_messages/src/Extension/MessagesComponent.php b/administrator/components/com_messages/src/Extension/MessagesComponent.php index ab09ea87e936d..959f8607c2dd8 100644 --- a/administrator/components/com_messages/src/Extension/MessagesComponent.php +++ b/administrator/components/com_messages/src/Extension/MessagesComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('messages', new Messages); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('messages', new Messages()); + } } diff --git a/administrator/components/com_messages/src/Field/MessageStatesField.php b/administrator/components/com_messages/src/Field/MessageStatesField.php index 5e5dbfdc15fc6..8dac33b2bfa12 100644 --- a/administrator/components/com_messages/src/Field/MessageStatesField.php +++ b/administrator/components/com_messages/src/Field/MessageStatesField.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select('id') - ->from('#__usergroups'); - $db->setQuery($query); + /** + * Method to get the filtering groups (null means no filtering) + * + * @return array|null array of filtering groups or null. + * + * @since 1.6 + */ + protected function getGroups() + { + // Compute usergroups + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('id') + ->from('#__usergroups'); + $db->setQuery($query); - try - { - $groups = $db->loadColumn(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'notice'); + try { + $groups = $db->loadColumn(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'notice'); - return null; - } + return null; + } - foreach ($groups as $i => $group) - { - if (Access::checkGroup($group, 'core.admin')) - { - continue; - } + foreach ($groups as $i => $group) { + if (Access::checkGroup($group, 'core.admin')) { + continue; + } - if (!Access::checkGroup($group, 'core.manage', 'com_messages')) - { - unset($groups[$i]); - continue; - } + if (!Access::checkGroup($group, 'core.manage', 'com_messages')) { + unset($groups[$i]); + continue; + } - if (!Access::checkGroup($group, 'core.login.admin')) - { - unset($groups[$i]); - } - } + if (!Access::checkGroup($group, 'core.login.admin')) { + unset($groups[$i]); + } + } - return array_values($groups); - } + return array_values($groups); + } - /** - * Method to get the users to exclude from the list of users - * - * @return array|null array of users to exclude or null to to not exclude them - * - * @since 1.6 - */ - protected function getExcluded() - { - return array(Factory::getUser()->id); - } + /** + * Method to get the users to exclude from the list of users + * + * @return array|null array of users to exclude or null to to not exclude them + * + * @since 1.6 + */ + protected function getExcluded() + { + return array(Factory::getUser()->id); + } } diff --git a/administrator/components/com_messages/src/Helper/MessagesHelper.php b/administrator/components/com_messages/src/Helper/MessagesHelper.php index 28598ba737f4b..c9750020d417f 100644 --- a/administrator/components/com_messages/src/Helper/MessagesHelper.php +++ b/administrator/components/com_messages/src/Helper/MessagesHelper.php @@ -1,4 +1,5 @@ setState('user.id', $user->get('id')); - - // Load the parameters. - $params = ComponentHelper::getParams('com_messages'); - $this->setState('params', $params); - } - - /** - * Method to get a single record. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function &getItem() - { - $item = new CMSObject; - $userid = (int) $this->getState('user.id'); - - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $query->select( - [ - $db->quoteName('cfg_name'), - $db->quoteName('cfg_value'), - ] - ) - ->from($db->quoteName('#__messages_cfg')) - ->where($db->quoteName('user_id') . ' = :userid') - ->bind(':userid', $userid, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $rows = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - foreach ($rows as $row) - { - $item->set($row->cfg_name, $row->cfg_value); - } - - $this->preprocessData('com_messages.config', $item); - - return $item; - } - - /** - * Method to get the record form. - * - * @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 \Joomla\CMS\Form\Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_messages.config', 'config', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $db = $this->getDatabase(); - - if ($userId = (int) $this->getState('user.id')) - { - $query = $db->getQuery(true) - ->delete($db->quoteName('#__messages_cfg')) - ->where($db->quoteName('user_id') . ' = :userid') - ->bind(':userid', $userId, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (count($data)) - { - $query = $db->getQuery(true) - ->insert($db->quoteName('#__messages_cfg')) - ->columns( - [ - $db->quoteName('user_id'), - $db->quoteName('cfg_name'), - $db->quoteName('cfg_value'), - ] - ); - - foreach ($data as $k => $v) - { - $query->values( - implode( - ',', - $query->bindArray( - [$userId , $k, $v], - [ParameterType::INTEGER, ParameterType::STRING, ParameterType::STRING] - ) - ) - ); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - return true; - } - else - { - $this->setError('COM_MESSAGES_ERR_INVALID_USER'); - - return false; - } - } + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $user = Factory::getUser(); + + $this->setState('user.id', $user->get('id')); + + // Load the parameters. + $params = ComponentHelper::getParams('com_messages'); + $this->setState('params', $params); + } + + /** + * Method to get a single record. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function &getItem() + { + $item = new CMSObject(); + $userid = (int) $this->getState('user.id'); + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('cfg_name'), + $db->quoteName('cfg_value'), + ] + ) + ->from($db->quoteName('#__messages_cfg')) + ->where($db->quoteName('user_id') . ' = :userid') + ->bind(':userid', $userid, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $rows = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + foreach ($rows as $row) { + $item->set($row->cfg_name, $row->cfg_value); + } + + $this->preprocessData('com_messages.config', $item); + + return $item; + } + + /** + * Method to get the record form. + * + * @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 \Joomla\CMS\Form\Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_messages.config', 'config', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $db = $this->getDatabase(); + + if ($userId = (int) $this->getState('user.id')) { + $query = $db->getQuery(true) + ->delete($db->quoteName('#__messages_cfg')) + ->where($db->quoteName('user_id') . ' = :userid') + ->bind(':userid', $userId, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (count($data)) { + $query = $db->getQuery(true) + ->insert($db->quoteName('#__messages_cfg')) + ->columns( + [ + $db->quoteName('user_id'), + $db->quoteName('cfg_name'), + $db->quoteName('cfg_value'), + ] + ); + + foreach ($data as $k => $v) { + $query->values( + implode( + ',', + $query->bindArray( + [$userId , $k, $v], + [ParameterType::INTEGER, ParameterType::STRING, ParameterType::STRING] + ) + ) + ); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + return true; + } else { + $this->setError('COM_MESSAGES_ERR_INVALID_USER'); + + return false; + } + } } diff --git a/administrator/components/com_messages/src/Model/MessageModel.php b/administrator/components/com_messages/src/Model/MessageModel.php index 3864871baeff5..bc480fda65e7c 100644 --- a/administrator/components/com_messages/src/Model/MessageModel.php +++ b/administrator/components/com_messages/src/Model/MessageModel.php @@ -1,4 +1,5 @@ input; - - $user = Factory::getUser(); - $this->setState('user.id', $user->get('id')); - - $messageId = (int) $input->getInt('message_id'); - $this->setState('message.id', $messageId); - - $replyId = (int) $input->getInt('reply_id'); - $this->setState('reply.id', $replyId); - } - - /** - * Check that recipient user is the one trying to delete and then call parent delete method - * - * @param array &$pks An array of record primary keys. - * - * @return boolean True if successful, false if an error occurs. - * - * @since 3.1 - */ - public function delete(&$pks) - { - $pks = (array) $pks; - $table = $this->getTable(); - $user = Factory::getUser(); - - // Iterate the items to delete each one. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - if ($table->user_id_to != $user->id) - { - // Prune items that you can't change. - unset($pks[$i]); - - try - { - Log::add(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 'warning'); - } - - return false; - } - } - else - { - $this->setError($table->getError()); - - return false; - } - } - - return parent::delete($pks); - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - if (!isset($this->item)) - { - if ($this->item = parent::getItem($pk)) - { - // Invalid message_id returns 0 - if ($this->item->user_id_to === '0') - { - $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); - - return false; - } - - // Prime required properties. - if (empty($this->item->message_id)) - { - // Prepare data for a new record. - if ($replyId = (int) $this->getState('reply.id')) - { - // If replying to a message, preload some data. - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName(['subject', 'user_id_from', 'user_id_to'])) - ->from($db->quoteName('#__messages')) - ->where($db->quoteName('message_id') . ' = :messageid') - ->bind(':messageid', $replyId, ParameterType::INTEGER); - - try - { - $message = $db->setQuery($query)->loadObject(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (!$message || $message->user_id_to != Factory::getUser()->id) - { - $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); - - return false; - } - - $this->item->set('user_id_to', $message->user_id_from); - $re = Text::_('COM_MESSAGES_RE'); - - if (stripos($message->subject, $re) !== 0) - { - $this->item->set('subject', $re . ' ' . $message->subject); - } - } - } - elseif ($this->item->user_id_to != Factory::getUser()->id) - { - $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); - - return false; - } - else - { - // Mark message read - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->update($db->quoteName('#__messages')) - ->set($db->quoteName('state') . ' = 1') - ->where($db->quoteName('message_id') . ' = :messageid') - ->bind(':messageid', $this->item->message_id, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - } - } - - // Get the user name for an existing message. - if ($this->item->user_id_from && $fromUser = new User($this->item->user_id_from)) - { - $this->item->set('from_user_name', $fromUser->name); - } - } - - return $this->item; - } - - /** - * Method to get the record form. - * - * @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 \Joomla\CMS\Form\Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_messages.message', 'message', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_messages.edit.message.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_messages.message', $data); - - return $data; - } - - /** - * Checks that the current user matches the message recipient and calls the parent publish method - * - * @param array &$pks A list of the primary keys to change. - * @param integer $value The value of the published state. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function publish(&$pks, $value = 1) - { - $user = Factory::getUser(); - $table = $this->getTable(); - $pks = (array) $pks; - - // Check that the recipient matches the current user - foreach ($pks as $i => $pk) - { - $table->reset(); - - if ($table->load($pk)) - { - if ($table->user_id_to != $user->id) - { - // Prune items that you can't change. - unset($pks[$i]); - - try - { - Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'warning'); - } - - return false; - } - } - } - - return parent::publish($pks, $value); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $table = $this->getTable(); - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Assign empty values. - if (empty($table->user_id_from)) - { - $table->user_id_from = Factory::getUser()->get('id'); - } - - if ((int) $table->date_time == 0) - { - $table->date_time = Factory::getDate()->toSql(); - } - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Load the user details (already valid from table check). - $toUser = User::getInstance($table->user_id_to); - - // Check if recipient can access com_messages. - if (!$toUser->authorise('core.login.admin') || !$toUser->authorise('core.manage', 'com_messages')) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_RECIPIENT_NOT_AUTHORISED')); - - return false; - } - - // Load the recipient user configuration. - $model = $this->bootComponent('com_messages') - ->getMVCFactory()->createModel('Config', 'Administrator', ['ignore_request' => true]); - $model->setState('user.id', $table->user_id_to); - $config = $model->getItem(); - - if (empty($config)) - { - $this->setError($model->getError()); - - return false; - } - - if ($config->get('lock', false)) - { - $this->setError(Text::_('COM_MESSAGES_ERR_SEND_FAILED')); - - return false; - } - - // Store the data. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - $key = $table->getKeyName(); - - if (isset($table->$key)) - { - $this->setState($this->getName() . '.id', $table->$key); - } - - if ($config->get('mail_on_new', true)) - { - $fromUser = User::getInstance($table->user_id_from); - $debug = Factory::getApplication()->get('debug_lang'); - $default_language = ComponentHelper::getParams('com_languages')->get('administrator'); - $lang = Language::getInstance($toUser->getParam('admin_language', $default_language), $debug); - $lang->load('com_messages', JPATH_ADMINISTRATOR); - - // Build the email subject and message - $app = Factory::getApplication(); - $linkMode = $app->get('force_ssl', 0) >= 1 ? Route::TLS_FORCE : Route::TLS_IGNORE; - $sitename = $app->get('sitename'); - $fromName = $fromUser->get('name'); - $siteURL = Route::link( - 'administrator', - 'index.php?option=com_messages&view=message&message_id=' . $table->message_id, - false, - $linkMode, - true - ); - $subject = html_entity_decode($table->subject, ENT_COMPAT, 'UTF-8'); - $message = strip_tags(html_entity_decode($table->message, ENT_COMPAT, 'UTF-8')); - - // Send the email - $mailer = new MailTemplate('com_messages.new_message', $lang->getTag()); - $data = [ - 'subject' => $subject, - 'message' => $message, - 'fromname' => $fromName, - 'sitename' => $sitename, - 'siteurl' => $siteURL, - 'fromemail' => $fromUser->email, - 'toname' => $toUser->name, - 'toemail' => $toUser->email - ]; - $mailer->addTemplateData($data); - $mailer->setReplyTo($fromUser->email, $fromUser->name); - $mailer->addRecipient($toUser->email, $toUser->name); - - try - { - $mailer->send(); - } - catch (MailDisabledException | phpMailerException $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $this->setError(Text::_('COM_MESSAGES_ERROR_MAIL_FAILED')); - - return false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $this->setError(Text::_('COM_MESSAGES_ERROR_MAIL_FAILED')); - - return false; - } - } - } - - return true; - } - - /** - * Sends a message to the site's super users - * - * @param string $subject The message subject - * @param string $message The message - * - * @return boolean - * - * @since 3.9.0 - */ - public function notifySuperUsers($subject, $message, $fromUser = null) - { - $db = $this->getDatabase(); - - try - { - /** @var Asset $table */ - $table = Table::getInstance('Asset'); - $rootId = $table->getRootId(); - - /** @var Rule[] $rules */ - $rules = Access::getAssetRules($rootId)->getData(); - $rawGroups = $rules['core.admin']->getData(); - - if (empty($rawGroups)) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_MISSING_ROOT_ASSET_GROUPS')); - - return false; - } - - $groups = array(); - - foreach ($rawGroups as $g => $enabled) - { - if ($enabled) - { - $groups[] = $g; - } - } - - if (empty($groups)) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_NO_GROUPS_SET_AS_SUPER_USER')); - - return false; - } - - $query = $db->getQuery(true) - ->select($db->quoteName('map.user_id')) - ->from($db->quoteName('#__user_usergroup_map', 'map')) - ->join('LEFT', - $db->quoteName('#__users', 'u'), - $db->quoteName('u.id') . ' = ' . $db->quoteName('map.user_id') - ) - ->whereIn($db->quoteName('map.group_id'), $groups) - ->where($db->quoteName('u.block') . ' = 0') - ->where($db->quoteName('u.sendEmail') . ' = 1'); - - $userIDs = $db->setQuery($query)->loadColumn(0); - - if (empty($userIDs)) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_NO_USERS_SET_AS_SUPER_USER')); - - return false; - } - - foreach ($userIDs as $id) - { - /* - * All messages must have a valid from user, we have use cases where an unauthenticated user may trigger this - * so we will set the from user as the to user - */ - $data = [ - 'user_id_from' => $id, - 'user_id_to' => $id, - 'subject' => $subject, - 'message' => $message, - ]; - - if (!$this->save($data)) - { - return false; - } - } - - return true; - } - catch (\Exception $exception) - { - $this->setError($exception->getMessage()); - - return false; - } - } + /** + * Message + * + * @var \stdClass + */ + protected $item; + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + parent::populateState(); + + $input = Factory::getApplication()->input; + + $user = Factory::getUser(); + $this->setState('user.id', $user->get('id')); + + $messageId = (int) $input->getInt('message_id'); + $this->setState('message.id', $messageId); + + $replyId = (int) $input->getInt('reply_id'); + $this->setState('reply.id', $replyId); + } + + /** + * Check that recipient user is the one trying to delete and then call parent delete method + * + * @param array &$pks An array of record primary keys. + * + * @return boolean True if successful, false if an error occurs. + * + * @since 3.1 + */ + public function delete(&$pks) + { + $pks = (array) $pks; + $table = $this->getTable(); + $user = Factory::getUser(); + + // Iterate the items to delete each one. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + if ($table->user_id_to != $user->id) { + // Prune items that you can't change. + unset($pks[$i]); + + try { + Log::add(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED'), 'warning'); + } + + return false; + } + } else { + $this->setError($table->getError()); + + return false; + } + } + + return parent::delete($pks); + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + if (!isset($this->item)) { + if ($this->item = parent::getItem($pk)) { + // Invalid message_id returns 0 + if ($this->item->user_id_to === '0') { + $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); + + return false; + } + + // Prime required properties. + if (empty($this->item->message_id)) { + // Prepare data for a new record. + if ($replyId = (int) $this->getState('reply.id')) { + // If replying to a message, preload some data. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['subject', 'user_id_from', 'user_id_to'])) + ->from($db->quoteName('#__messages')) + ->where($db->quoteName('message_id') . ' = :messageid') + ->bind(':messageid', $replyId, ParameterType::INTEGER); + + try { + $message = $db->setQuery($query)->loadObject(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (!$message || $message->user_id_to != Factory::getUser()->id) { + $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); + + return false; + } + + $this->item->set('user_id_to', $message->user_id_from); + $re = Text::_('COM_MESSAGES_RE'); + + if (stripos($message->subject, $re) !== 0) { + $this->item->set('subject', $re . ' ' . $message->subject); + } + } + } elseif ($this->item->user_id_to != Factory::getUser()->id) { + $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); + + return false; + } else { + // Mark message read + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->update($db->quoteName('#__messages')) + ->set($db->quoteName('state') . ' = 1') + ->where($db->quoteName('message_id') . ' = :messageid') + ->bind(':messageid', $this->item->message_id, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + } + } + + // Get the user name for an existing message. + if ($this->item->user_id_from && $fromUser = new User($this->item->user_id_from)) { + $this->item->set('from_user_name', $fromUser->name); + } + } + + return $this->item; + } + + /** + * Method to get the record form. + * + * @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 \Joomla\CMS\Form\Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_messages.message', 'message', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_messages.edit.message.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_messages.message', $data); + + return $data; + } + + /** + * Checks that the current user matches the message recipient and calls the parent publish method + * + * @param array &$pks A list of the primary keys to change. + * @param integer $value The value of the published state. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function publish(&$pks, $value = 1) + { + $user = Factory::getUser(); + $table = $this->getTable(); + $pks = (array) $pks; + + // Check that the recipient matches the current user + foreach ($pks as $i => $pk) { + $table->reset(); + + if ($table->load($pk)) { + if ($table->user_id_to != $user->id) { + // Prune items that you can't change. + unset($pks[$i]); + + try { + Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'warning'); + } + + return false; + } + } + } + + return parent::publish($pks, $value); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $table = $this->getTable(); + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Assign empty values. + if (empty($table->user_id_from)) { + $table->user_id_from = Factory::getUser()->get('id'); + } + + if ((int) $table->date_time == 0) { + $table->date_time = Factory::getDate()->toSql(); + } + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Load the user details (already valid from table check). + $toUser = User::getInstance($table->user_id_to); + + // Check if recipient can access com_messages. + if (!$toUser->authorise('core.login.admin') || !$toUser->authorise('core.manage', 'com_messages')) { + $this->setError(Text::_('COM_MESSAGES_ERROR_RECIPIENT_NOT_AUTHORISED')); + + return false; + } + + // Load the recipient user configuration. + $model = $this->bootComponent('com_messages') + ->getMVCFactory()->createModel('Config', 'Administrator', ['ignore_request' => true]); + $model->setState('user.id', $table->user_id_to); + $config = $model->getItem(); + + if (empty($config)) { + $this->setError($model->getError()); + + return false; + } + + if ($config->get('lock', false)) { + $this->setError(Text::_('COM_MESSAGES_ERR_SEND_FAILED')); + + return false; + } + + // Store the data. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + $key = $table->getKeyName(); + + if (isset($table->$key)) { + $this->setState($this->getName() . '.id', $table->$key); + } + + if ($config->get('mail_on_new', true)) { + $fromUser = User::getInstance($table->user_id_from); + $debug = Factory::getApplication()->get('debug_lang'); + $default_language = ComponentHelper::getParams('com_languages')->get('administrator'); + $lang = Language::getInstance($toUser->getParam('admin_language', $default_language), $debug); + $lang->load('com_messages', JPATH_ADMINISTRATOR); + + // Build the email subject and message + $app = Factory::getApplication(); + $linkMode = $app->get('force_ssl', 0) >= 1 ? Route::TLS_FORCE : Route::TLS_IGNORE; + $sitename = $app->get('sitename'); + $fromName = $fromUser->get('name'); + $siteURL = Route::link( + 'administrator', + 'index.php?option=com_messages&view=message&message_id=' . $table->message_id, + false, + $linkMode, + true + ); + $subject = html_entity_decode($table->subject, ENT_COMPAT, 'UTF-8'); + $message = strip_tags(html_entity_decode($table->message, ENT_COMPAT, 'UTF-8')); + + // Send the email + $mailer = new MailTemplate('com_messages.new_message', $lang->getTag()); + $data = [ + 'subject' => $subject, + 'message' => $message, + 'fromname' => $fromName, + 'sitename' => $sitename, + 'siteurl' => $siteURL, + 'fromemail' => $fromUser->email, + 'toname' => $toUser->name, + 'toemail' => $toUser->email + ]; + $mailer->addTemplateData($data); + $mailer->setReplyTo($fromUser->email, $fromUser->name); + $mailer->addRecipient($toUser->email, $toUser->name); + + try { + $mailer->send(); + } catch (MailDisabledException | phpMailerException $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $this->setError(Text::_('COM_MESSAGES_ERROR_MAIL_FAILED')); + + return false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $this->setError(Text::_('COM_MESSAGES_ERROR_MAIL_FAILED')); + + return false; + } + } + } + + return true; + } + + /** + * Sends a message to the site's super users + * + * @param string $subject The message subject + * @param string $message The message + * + * @return boolean + * + * @since 3.9.0 + */ + public function notifySuperUsers($subject, $message, $fromUser = null) + { + $db = $this->getDatabase(); + + try { + /** @var Asset $table */ + $table = Table::getInstance('Asset'); + $rootId = $table->getRootId(); + + /** @var Rule[] $rules */ + $rules = Access::getAssetRules($rootId)->getData(); + $rawGroups = $rules['core.admin']->getData(); + + if (empty($rawGroups)) { + $this->setError(Text::_('COM_MESSAGES_ERROR_MISSING_ROOT_ASSET_GROUPS')); + + return false; + } + + $groups = array(); + + foreach ($rawGroups as $g => $enabled) { + if ($enabled) { + $groups[] = $g; + } + } + + if (empty($groups)) { + $this->setError(Text::_('COM_MESSAGES_ERROR_NO_GROUPS_SET_AS_SUPER_USER')); + + return false; + } + + $query = $db->getQuery(true) + ->select($db->quoteName('map.user_id')) + ->from($db->quoteName('#__user_usergroup_map', 'map')) + ->join( + 'LEFT', + $db->quoteName('#__users', 'u'), + $db->quoteName('u.id') . ' = ' . $db->quoteName('map.user_id') + ) + ->whereIn($db->quoteName('map.group_id'), $groups) + ->where($db->quoteName('u.block') . ' = 0') + ->where($db->quoteName('u.sendEmail') . ' = 1'); + + $userIDs = $db->setQuery($query)->loadColumn(0); + + if (empty($userIDs)) { + $this->setError(Text::_('COM_MESSAGES_ERROR_NO_USERS_SET_AS_SUPER_USER')); + + return false; + } + + foreach ($userIDs as $id) { + /* + * All messages must have a valid from user, we have use cases where an unauthenticated user may trigger this + * so we will set the from user as the to user + */ + $data = [ + 'user_id_from' => $id, + 'user_id_to' => $id, + 'subject' => $subject, + 'message' => $message, + ]; + + if (!$this->save($data)) { + return false; + } + } + + return true; + } catch (\Exception $exception) { + $this->setError($exception->getMessage()); + + return false; + } + } } diff --git a/administrator/components/com_messages/src/Model/MessagesModel.php b/administrator/components/com_messages/src/Model/MessagesModel.php index 0077db55d982d..181e2e711291a 100644 --- a/administrator/components/com_messages/src/Model/MessagesModel.php +++ b/administrator/components/com_messages/src/Model/MessagesModel.php @@ -1,4 +1,5 @@ getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - $id = (int) $user->get('id'); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a') . '.*', - $db->quoteName('u.name', 'user_from'), - ] - ) - ); - $query->from($db->quoteName('#__messages', 'a')); - - // Join over the users for message owner. - $query->join('INNER', - $db->quoteName('#__users', 'u'), - $db->quoteName('u.id') . ' = ' . $db->quoteName('a.user_id_from') - ) - ->where($db->quoteName('a.user_id_to') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - - // Filter by published state. - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $state = (int) $state; - $query->where($db->quoteName('a.state') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - elseif ($state !== '*') - { - $query->whereIn($db->quoteName('a.state'), [0, 1]); - } - - // Filter by search in subject or message. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.subject') . ' LIKE :subject', - $db->quoteName('a.message') . ' LIKE :message', - ], - 'OR' - ) - ->bind(':subject', $search) - ->bind(':message', $search); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.date_time')) . ' ' . $db->escape($this->getState('list.direction', 'DESC'))); - - return $query; - } - - /** - * Purge the messages table of old messages for the given user ID. - * - * @param int $userId The user id - * - * @return void - * - * @since 4.2.0 - */ - public function purge(int $userId): void - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName(['cfg_name', 'cfg_value'])) - ->from($db->quoteName('#__messages_cfg')) - ->where( - [ - $db->quoteName('user_id') . ' = :userId', - $db->quoteName('cfg_name') . ' = ' . $db->quote('auto_purge'), - ] - ) - ->bind(':userId', $userId, ParameterType::INTEGER); - - $db->setQuery($query); - $config = $db->loadObject(); - - // Default is 7 days - $purge = 7; - - // Check if auto_purge value set - if (\is_object($config) && $config->cfg_name === 'auto_purge') - { - $purge = $config->cfg_value; - } - - // If purge value is not 0, then allow purging of old messages - if ($purge > 0) - { - // Purge old messages at day set in message configuration - $past = Factory::getDate(time() - $purge * 86400)->toSql(); - - $query = $db->getQuery(true) - ->delete($db->quoteName('#__messages')) - ->where( - [ - $db->quoteName('date_time') . ' < :past', - $db->quoteName('user_id_to') . ' = :userId', - ] - ) - ->bind(':past', $past) - ->bind(':userId', $userId, ParameterType::INTEGER); - - $db->setQuery($query); - $db->execute(); - } - } + /** + * Override parent constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'message_id', 'a.id', + 'subject', 'a.subject', + 'state', 'a.state', + 'user_id_from', 'a.user_id_from', + 'user_id_to', 'a.user_id_to', + 'date_time', 'a.date_time', + 'priority', 'a.priority', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.date_time', $direction = 'desc') + { + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + $id = (int) $user->get('id'); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a') . '.*', + $db->quoteName('u.name', 'user_from'), + ] + ) + ); + $query->from($db->quoteName('#__messages', 'a')); + + // Join over the users for message owner. + $query->join( + 'INNER', + $db->quoteName('#__users', 'u'), + $db->quoteName('u.id') . ' = ' . $db->quoteName('a.user_id_from') + ) + ->where($db->quoteName('a.user_id_to') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + // Filter by published state. + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $state = (int) $state; + $query->where($db->quoteName('a.state') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } elseif ($state !== '*') { + $query->whereIn($db->quoteName('a.state'), [0, 1]); + } + + // Filter by search in subject or message. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.subject') . ' LIKE :subject', + $db->quoteName('a.message') . ' LIKE :message', + ], + 'OR' + ) + ->bind(':subject', $search) + ->bind(':message', $search); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.date_time')) . ' ' . $db->escape($this->getState('list.direction', 'DESC'))); + + return $query; + } + + /** + * Purge the messages table of old messages for the given user ID. + * + * @param int $userId The user id + * + * @return void + * + * @since 4.2.0 + */ + public function purge(int $userId): void + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['cfg_name', 'cfg_value'])) + ->from($db->quoteName('#__messages_cfg')) + ->where( + [ + $db->quoteName('user_id') . ' = :userId', + $db->quoteName('cfg_name') . ' = ' . $db->quote('auto_purge'), + ] + ) + ->bind(':userId', $userId, ParameterType::INTEGER); + + $db->setQuery($query); + $config = $db->loadObject(); + + // Default is 7 days + $purge = 7; + + // Check if auto_purge value set + if (\is_object($config) && $config->cfg_name === 'auto_purge') { + $purge = $config->cfg_value; + } + + // If purge value is not 0, then allow purging of old messages + if ($purge > 0) { + // Purge old messages at day set in message configuration + $past = Factory::getDate(time() - $purge * 86400)->toSql(); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__messages')) + ->where( + [ + $db->quoteName('date_time') . ' < :past', + $db->quoteName('user_id_to') . ' = :userId', + ] + ) + ->bind(':past', $past) + ->bind(':userId', $userId, ParameterType::INTEGER); + + $db->setQuery($query); + $db->execute(); + } + } } diff --git a/administrator/components/com_messages/src/Service/HTML/Messages.php b/administrator/components/com_messages/src/Service/HTML/Messages.php index 17be8a3280f74..623157328ea36 100644 --- a/administrator/components/com_messages/src/Service/HTML/Messages.php +++ b/administrator/components/com_messages/src/Service/HTML/Messages.php @@ -1,4 +1,5 @@ array('trash', 'messages.unpublish', 'JTRASHED', 'COM_MESSAGES_MARK_AS_UNREAD'), - 1 => array('publish', 'messages.unpublish', 'COM_MESSAGES_OPTION_READ', 'COM_MESSAGES_MARK_AS_UNREAD'), - 0 => array('unpublish', 'messages.publish', 'COM_MESSAGES_OPTION_UNREAD', 'COM_MESSAGES_MARK_AS_READ'), - ); + /** + * Get the HTML code of the state switcher + * + * @param int $i Row number + * @param int $value The state value + * @param boolean $canChange Can the user change the state? + * + * @return string + * + * @since 3.4 + */ + public function status($i, $value = 0, $canChange = false) + { + // Array of image, task, title, action. + $states = array( + -2 => array('trash', 'messages.unpublish', 'JTRASHED', 'COM_MESSAGES_MARK_AS_UNREAD'), + 1 => array('publish', 'messages.unpublish', 'COM_MESSAGES_OPTION_READ', 'COM_MESSAGES_MARK_AS_UNREAD'), + 0 => array('unpublish', 'messages.publish', 'COM_MESSAGES_OPTION_UNREAD', 'COM_MESSAGES_MARK_AS_READ'), + ); - $state = ArrayHelper::getValue($states, (int) $value, $states[0]); - $icon = $state[0]; + $state = ArrayHelper::getValue($states, (int) $value, $states[0]); + $icon = $state[0]; - if ($canChange) - { - $html = ''; - } + if ($canChange) { + $html = ''; + } - return $html; - } + return $html; + } } diff --git a/administrator/components/com_messages/src/Table/MessageTable.php b/administrator/components/com_messages/src/Table/MessageTable.php index a750d7d7c17a3..045194f063f41 100644 --- a/administrator/components/com_messages/src/Table/MessageTable.php +++ b/administrator/components/com_messages/src/Table/MessageTable.php @@ -1,4 +1,5 @@ setColumnAlias('published', 'state'); - } - - /** - * Validation and filtering. - * - * @return boolean - * - * @since 1.5 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Check the to and from users. - $user = new User($this->user_id_from); - - if (empty($user->id)) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_FROM_USER')); - - return false; - } - - $user = new User($this->user_id_to); - - if (empty($user->id)) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_TO_USER')); - - return false; - } - - if (empty($this->subject)) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_SUBJECT')); - - return false; - } - - if (empty($this->message)) - { - $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_MESSAGE')); - - return false; - } - - return true; - } + /** + * Constructor + * + * @param DatabaseDriver $db Database connector object + * + * @since 1.5 + */ + public function __construct(DatabaseDriver $db) + { + parent::__construct('#__messages', 'message_id', $db); + + $this->setColumnAlias('published', 'state'); + } + + /** + * Validation and filtering. + * + * @return boolean + * + * @since 1.5 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Check the to and from users. + $user = new User($this->user_id_from); + + if (empty($user->id)) { + $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_FROM_USER')); + + return false; + } + + $user = new User($this->user_id_to); + + if (empty($user->id)) { + $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_TO_USER')); + + return false; + } + + if (empty($this->subject)) { + $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_SUBJECT')); + + return false; + } + + if (empty($this->message)) { + $this->setError(Text::_('COM_MESSAGES_ERROR_INVALID_MESSAGE')); + + return false; + } + + return true; + } } diff --git a/administrator/components/com_messages/src/View/Config/HtmlView.php b/administrator/components/com_messages/src/View/Config/HtmlView.php index c4b9e79201721..508dbf7a5f2ca 100644 --- a/administrator/components/com_messages/src/View/Config/HtmlView.php +++ b/administrator/components/com_messages/src/View/Config/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } - // Bind the record to the form. - $this->form->bind($this->item); + // Bind the record to the form. + $this->form->bind($this->item); - $this->addToolbar(); + $this->addToolbar(); - parent::display($tpl); - } + parent::display($tpl); + } - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); - ToolbarHelper::title(Text::_('COM_MESSAGES_TOOLBAR_MY_SETTINGS'), 'envelope'); + ToolbarHelper::title(Text::_('COM_MESSAGES_TOOLBAR_MY_SETTINGS'), 'envelope'); - ToolbarHelper::apply('config.save', 'JSAVE'); + ToolbarHelper::apply('config.save', 'JSAVE'); - ToolbarHelper::cancel('config.cancel', 'JCANCEL'); - } + ToolbarHelper::cancel('config.cancel', 'JCANCEL'); + } } diff --git a/administrator/components/com_messages/src/View/Message/HtmlView.php b/administrator/components/com_messages/src/View/Message/HtmlView.php index 8d6a13cadfe03..44fadfb3c5831 100644 --- a/administrator/components/com_messages/src/View/Message/HtmlView.php +++ b/administrator/components/com_messages/src/View/Message/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - elseif ($this->getLayout() !== 'edit' && empty($this->item->message_id)) - { - throw new GenericDataException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } elseif ($this->getLayout() !== 'edit' && empty($this->item->message_id)) { + throw new GenericDataException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } - parent::display($tpl); - $this->addToolbar(); - } + parent::display($tpl); + $this->addToolbar(); + } - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $app = Factory::getApplication(); + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $app = Factory::getApplication(); - if ($this->getLayout() == 'edit') - { - $app->input->set('hidemainmenu', true); - ToolbarHelper::title(Text::_('COM_MESSAGES_WRITE_PRIVATE_MESSAGE'), 'envelope-open-text new-privatemessage'); - ToolbarHelper::custom('message.save', 'envelope', '', 'COM_MESSAGES_TOOLBAR_SEND', false); - ToolbarHelper::cancel('message.cancel'); - ToolbarHelper::help('Private_Messages:_Write'); - } - else - { - ToolbarHelper::title(Text::_('COM_MESSAGES_VIEW_PRIVATE_MESSAGE'), 'envelope inbox'); - $sender = User::getInstance($this->item->user_id_from); + if ($this->getLayout() == 'edit') { + $app->input->set('hidemainmenu', true); + ToolbarHelper::title(Text::_('COM_MESSAGES_WRITE_PRIVATE_MESSAGE'), 'envelope-open-text new-privatemessage'); + ToolbarHelper::custom('message.save', 'envelope', '', 'COM_MESSAGES_TOOLBAR_SEND', false); + ToolbarHelper::cancel('message.cancel'); + ToolbarHelper::help('Private_Messages:_Write'); + } else { + ToolbarHelper::title(Text::_('COM_MESSAGES_VIEW_PRIVATE_MESSAGE'), 'envelope inbox'); + $sender = User::getInstance($this->item->user_id_from); - if ($sender->id !== $app->getIdentity()->get('id') && ($sender->authorise('core.admin') - || $sender->authorise('core.manage', 'com_messages') && $sender->authorise('core.login.admin')) - && $app->getIdentity()->authorise('core.manage', 'com_users') - ) - { - ToolbarHelper::custom('message.reply', 'redo', '', 'COM_MESSAGES_TOOLBAR_REPLY', false); - } + if ( + $sender->id !== $app->getIdentity()->get('id') && ($sender->authorise('core.admin') + || $sender->authorise('core.manage', 'com_messages') && $sender->authorise('core.login.admin')) + && $app->getIdentity()->authorise('core.manage', 'com_users') + ) { + ToolbarHelper::custom('message.reply', 'redo', '', 'COM_MESSAGES_TOOLBAR_REPLY', false); + } - ToolbarHelper::cancel('message.cancel'); - ToolbarHelper::help('Private_Messages:_Read'); - } - } + ToolbarHelper::cancel('message.cancel'); + ToolbarHelper::help('Private_Messages:_Read'); + } + } } diff --git a/administrator/components/com_messages/src/View/Messages/HtmlView.php b/administrator/components/com_messages/src/View/Messages/HtmlView.php index ec7d978dbd503..1b101fd64e988 100644 --- a/administrator/components/com_messages/src/View/Messages/HtmlView.php +++ b/administrator/components/com_messages/src/View/Messages/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $state = $this->get('State'); - $canDo = ContentHelper::getActions('com_messages'); - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_MESSAGES_MANAGER_MESSAGES'), 'envelope inbox'); - - // Only display the New button if the user has the access level to create a message and if they have access to the list of users - if ($canDo->get('core.create') && $user->authorise('core.manage', 'com_users')) - { - $toolbar->addNew('message.add'); - } - - if (!$this->isEmptyState && $canDo->get('core.edit.state')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('messages.publish') - ->text('COM_MESSAGES_TOOLBAR_MARK_AS_READ') - ->listCheck(true); - - $childBar->unpublish('messages.unpublish') - ->text('COM_MESSAGES_TOOLBAR_MARK_AS_UNREAD') - ->listCheck(true); - - if ($this->state->get('filter.state') != -2) - { - $childBar->trash('messages.trash')->listCheck(true); - } - } - - $toolbar->appendButton('Link', 'cog', 'COM_MESSAGES_TOOLBAR_MY_SETTINGS', 'index.php?option=com_messages&view=config'); - ToolbarHelper::divider(); - - if (!$this->isEmptyState && $this->state->get('filter.state') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('messages.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin')) - { - $toolbar->preferences('com_messages'); - } - - $toolbar->help('Private_Messages'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $state = $this->get('State'); + $canDo = ContentHelper::getActions('com_messages'); + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_MESSAGES_MANAGER_MESSAGES'), 'envelope inbox'); + + // Only display the New button if the user has the access level to create a message and if they have access to the list of users + if ($canDo->get('core.create') && $user->authorise('core.manage', 'com_users')) { + $toolbar->addNew('message.add'); + } + + if (!$this->isEmptyState && $canDo->get('core.edit.state')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('messages.publish') + ->text('COM_MESSAGES_TOOLBAR_MARK_AS_READ') + ->listCheck(true); + + $childBar->unpublish('messages.unpublish') + ->text('COM_MESSAGES_TOOLBAR_MARK_AS_UNREAD') + ->listCheck(true); + + if ($this->state->get('filter.state') != -2) { + $childBar->trash('messages.trash')->listCheck(true); + } + } + + $toolbar->appendButton('Link', 'cog', 'COM_MESSAGES_TOOLBAR_MY_SETTINGS', 'index.php?option=com_messages&view=config'); + ToolbarHelper::divider(); + + if (!$this->isEmptyState && $this->state->get('filter.state') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('messages.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin')) { + $toolbar->preferences('com_messages'); + } + + $toolbar->help('Private_Messages'); + } } diff --git a/administrator/components/com_messages/tmpl/config/default.php b/administrator/components/com_messages/tmpl/config/default.php index 1c3d3dba57fa6..862c2a06f37d8 100644 --- a/administrator/components/com_messages/tmpl/config/default.php +++ b/administrator/components/com_messages/tmpl/config/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?>
-
-
-
-
- - form->renderField('lock'); ?> - form->renderField('mail_on_new'); ?> - form->renderField('auto_purge'); ?> -
-
-
-
+
+
+
+
+ + form->renderField('lock'); ?> + form->renderField('mail_on_new'); ?> + form->renderField('auto_purge'); ?> +
+
+
+
- - + +
diff --git a/administrator/components/com_messages/tmpl/message/default.php b/administrator/components/com_messages/tmpl/message/default.php index ce8277f94cf91..7cb2ee34822d2 100644 --- a/administrator/components/com_messages/tmpl/message/default.php +++ b/administrator/components/com_messages/tmpl/message/default.php @@ -1,4 +1,5 @@
-
-
-
-
-
- -
-
- item->get('from_user_name'); ?> -
-
-
-
- -
-
- item->date_time, Text::_('DATE_FORMAT_LC2')); ?> -
-
-
-
- -
-
- item->subject; ?> -
-
-
-
- -
-
- item->message; ?> -
-
- - - -
-
-
+
+
+
+
+
+ +
+
+ item->get('from_user_name'); ?> +
+
+
+
+ +
+
+ item->date_time, Text::_('DATE_FORMAT_LC2')); ?> +
+
+
+
+ +
+
+ item->subject; ?> +
+
+
+
+ +
+
+ item->message; ?> +
+
+ + + +
+
+
diff --git a/administrator/components/com_messages/tmpl/message/edit.php b/administrator/components/com_messages/tmpl/message/edit.php index 2ddcf004df2b0..00b33c96a3702 100644 --- a/administrator/components/com_messages/tmpl/message/edit.php +++ b/administrator/components/com_messages/tmpl/message/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?>
-
-
-
-
- form->getLabel('user_id_to'); ?> - form->getInput('user_id_to'); ?> -
-
- form->getLabel('subject'); ?> - form->getInput('subject'); ?> -
-
- form->getLabel('message'); ?> - form->getInput('message'); ?> -
-
-
-
- - +
+
+
+
+ form->getLabel('user_id_to'); ?> + form->getInput('user_id_to'); ?> +
+
+ form->getLabel('subject'); ?> + form->getInput('subject'); ?> +
+
+ form->getLabel('message'); ?> + form->getInput('message'); ?> +
+
+
+
+ +
diff --git a/administrator/components/com_messages/tmpl/messages/default.php b/administrator/components/com_messages/tmpl/messages/default.php index 57ab907ee6a5f..0572dfdc0cf29 100644 --- a/administrator/components/com_messages/tmpl/messages/default.php +++ b/administrator/components/com_messages/tmpl/messages/default.php @@ -1,4 +1,5 @@ escape($this->state->get('list.direction')); ?>
-
- $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - items as $i => $item) : - $canChange = $user->authorise('core.edit.state', 'com_messages'); - ?> - - - - - - - - - -
- , - , - -
- - - - - - - - - -
- message_id, false, 'cid', 'cb', $item->subject); ?> - - - escape($item->subject); ?> - - state, $canChange); ?> - - user_from; ?> - - date_time, Text::_('DATE_FORMAT_LC2')); ?> -
+
+ $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + items as $i => $item) : + $canChange = $user->authorise('core.edit.state', 'com_messages'); + ?> + + + + + + + + + +
+ , + , + +
+ + + + + + + + + +
+ message_id, false, 'cid', 'cb', $item->subject); ?> + + + escape($item->subject); ?> + + state, $canChange); ?> + + user_from; ?> + + date_time, Text::_('DATE_FORMAT_LC2')); ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - -
- - - -
-
+ +
+ + + +
+
diff --git a/administrator/components/com_messages/tmpl/messages/emptystate.php b/administrator/components/com_messages/tmpl/messages/emptystate.php index bd872cff2d773..43a4b1a7a54c4 100644 --- a/administrator/components/com_messages/tmpl/messages/emptystate.php +++ b/administrator/components/com_messages/tmpl/messages/emptystate.php @@ -1,4 +1,5 @@ 'COM_MESSAGES', - 'formURL' => 'index.php?option=com_messages&view=messages', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Private_Messages', - 'icon' => 'icon-envelope inbox', + 'textPrefix' => 'COM_MESSAGES', + 'formURL' => 'index.php?option=com_messages&view=messages', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Private_Messages', + 'icon' => 'icon-envelope inbox', ]; -if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_messages') - && Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_users')) -{ - $displayData['createURL'] = 'index.php?option=com_messages&task=message.add'; +if ( + Factory::getApplication()->getIdentity()->authorise('core.create', 'com_messages') + && Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_users') +) { + $displayData['createURL'] = 'index.php?option=com_messages&task=message.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_modules/helpers/modules.php b/administrator/components/com_modules/helpers/modules.php index a2419e69b6609..7a2058e8a32b2 100644 --- a/administrator/components/com_modules/helpers/modules.php +++ b/administrator/components/com_modules/helpers/modules.php @@ -1,13 +1,16 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; + /** * Modules component helper. @@ -18,5 +21,4 @@ */ abstract class ModulesHelper extends \Joomla\Component\Modules\Administrator\Helper\ModulesHelper { - } diff --git a/administrator/components/com_modules/layouts/joomla/form/field/modulespositionedit.php b/administrator/components/com_modules/layouts/joomla/form/field/modulespositionedit.php index 2af5c7d562a7b..671d77c70a44b 100644 --- a/administrator/components/com_modules/layouts/joomla/form/field/modulespositionedit.php +++ b/administrator/components/com_modules/layouts/joomla/form/field/modulespositionedit.php @@ -1,4 +1,5 @@ escape(Text::_('JGLOBAL_TYPE_OR_SELECT_SOME_OPTIONS')) . '" ', + 'class="' . $class . '"', + ' allow-custom', + ' search-placeholder="' . $this->escape(Text::_('JGLOBAL_TYPE_OR_SELECT_SOME_OPTIONS')) . '" ', ); $selectAttr = array( - $disabled ? 'disabled' : '', - $readonly ? 'readonly' : '', - strlen($hint) ? 'placeholder="' . $this->escape($hint) . '"' : '', - $onchange ? ' onchange="' . $onchange . '"' : '', - $autofocus ? ' autofocus' : '', + $disabled ? 'disabled' : '', + $readonly ? 'readonly' : '', + strlen($hint) ? 'placeholder="' . $this->escape($hint) . '"' : '', + $onchange ? ' onchange="' . $onchange . '"' : '', + $autofocus ? ' autofocus' : '', ); -if ($required) -{ - $selectAttr[] = ' required class="required"'; - $attributes[] = ' required'; +if ($required) { + $selectAttr[] = ' required class="required"'; + $attributes[] = ' required'; } Text::script('JGLOBAL_SELECT_NO_RESULTS_MATCH'); Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); Factory::getDocument()->getWebAssetManager() - ->usePreset('choicesjs') - ->useScript('webcomponent.field-fancy-select'); + ->usePreset('choicesjs') + ->useScript('webcomponent.field-fancy-select'); ?> > $id, - 'list.select' => $value, - 'list.attr' => implode(' ', $selectAttr), - ) - ); -?> + echo HTMLHelper::_('select.groupedlist', $positions, $name, array( + 'id' => $id, + 'list.select' => $value, + 'list.attr' => implode(' ', $selectAttr), + )); + ?> diff --git a/administrator/components/com_modules/layouts/toolbar/cancelselect.php b/administrator/components/com_modules/layouts/toolbar/cancelselect.php index b94d855fd051f..7637060854cd8 100644 --- a/administrator/components/com_modules/layouts/toolbar/cancelselect.php +++ b/administrator/components/com_modules/layouts/toolbar/cancelselect.php @@ -1,4 +1,5 @@ - + diff --git a/administrator/components/com_modules/services/provider.php b/administrator/components/com_modules/services/provider.php index 4998d4d42da02..e624191955b75 100644 --- a/administrator/components/com_modules/services/provider.php +++ b/administrator/components/com_modules/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Modules')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Modules')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Modules')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Modules')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new ModulesComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new ModulesComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_modules/src/Controller/DisplayController.php b/administrator/components/com_modules/src/Controller/DisplayController.php index 6751f988c7a0d..e4a8e7b303f90 100644 --- a/administrator/components/com_modules/src/Controller/DisplayController.php +++ b/administrator/components/com_modules/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('layout', 'edit'); - $id = $this->input->getInt('id'); + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array|boolean $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()} + * + * @return static|boolean This object to support chaining or false on failure. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $layout = $this->input->get('layout', 'edit'); + $id = $this->input->getInt('id'); - // Verify client - $clientId = $this->input->post->getInt('client_id'); + // Verify client + $clientId = $this->input->post->getInt('client_id'); - if (!is_null($clientId)) - { - $uri = Uri::getInstance(); + if (!is_null($clientId)) { + $uri = Uri::getInstance(); - if ((int) $uri->getVar('client_id') !== (int) $clientId) - { - $this->setRedirect(Route::_('index.php?option=com_modules&view=modules&client_id=' . $clientId, false)); + if ((int) $uri->getVar('client_id') !== (int) $clientId) { + $this->setRedirect(Route::_('index.php?option=com_modules&view=modules&client_id=' . $clientId, false)); - return false; - } - } + return false; + } + } - // Check for edit form. - if ($layout == 'edit' && !$this->checkEditId('com_modules.edit.module', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } + // Check for edit form. + if ($layout == 'edit' && !$this->checkEditId('com_modules.edit.module', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_modules&view=modules&client_id=' . $this->input->getInt('client_id'), false)); + $this->setRedirect(Route::_('index.php?option=com_modules&view=modules&client_id=' . $this->input->getInt('client_id'), false)); - return false; - } + return false; + } - // Check if we have a mod_menu module set to All languages or a mod_menu module for each admin language. - $factory = $this->app->bootComponent('menus')->getMVCFactory(); + // Check if we have a mod_menu module set to All languages or a mod_menu module for each admin language. + $factory = $this->app->bootComponent('menus')->getMVCFactory(); - if ($langMissing = $factory->createModel('Menus', 'Administrator')->getMissingModuleLanguages()) - { - $this->app->enqueueMessage(Text::sprintf('JMENU_MULTILANG_WARNING_MISSING_MODULES', implode(', ', $langMissing)), 'warning'); - } + if ($langMissing = $factory->createModel('Menus', 'Administrator')->getMissingModuleLanguages()) { + $this->app->enqueueMessage(Text::sprintf('JMENU_MULTILANG_WARNING_MISSING_MODULES', implode(', ', $langMissing)), 'warning'); + } - return parent::display(); - } + return parent::display(); + } } diff --git a/administrator/components/com_modules/src/Controller/ModuleController.php b/administrator/components/com_modules/src/Controller/ModuleController.php index ef53b80adf3fe..d695c394a63a0 100644 --- a/administrator/components/com_modules/src/Controller/ModuleController.php +++ b/administrator/components/com_modules/src/Controller/ModuleController.php @@ -1,4 +1,5 @@ app; - - // Get the result of the parent method. If an error, just return it. - $result = parent::add(); - - if ($result instanceof \Exception) - { - return $result; - } - - // Look for the Extension ID. - $extensionId = $this->input->get('eid', 0, 'int'); - - if (empty($extensionId)) - { - $redirectUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . '&layout=edit'; - - $this->setRedirect(Route::_($redirectUrl, false)); - - $app->enqueueMessage(Text::_('COM_MODULES_ERROR_INVALID_EXTENSION'), 'warning'); - } - - $app->setUserState('com_modules.add.module.extension_id', $extensionId); - $app->setUserState('com_modules.add.module.params', null); - - // Parameters could be coming in for a new item, so let's set them. - $params = $this->input->get('params', array(), 'array'); - $app->setUserState('com_modules.add.module.params', $params); - } - - /** - * Override parent cancel method to reset the add module state. - * - * @param string $key The name of the primary key of the URL variable. - * - * @return boolean True if access level checks pass, false otherwise. - * - * @since 1.6 - */ - public function cancel($key = null) - { - $result = parent::cancel(); - - $this->app->setUserState('com_modules.add.module.extension_id', null); - $this->app->setUserState('com_modules.add.module.params', null); - - if ($return = $this->input->get('return', '', 'BASE64')) - { - $return = base64_decode($return); - - // Don't redirect to an external URL. - if (!Uri::isInternal($return)) - { - $return = Uri::base(); - } - - $this->app->redirect($return); - } - - return $result; - } - - /** - * Override parent allowSave method. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowSave($data, $key = 'id') - { - // Use custom position if selected - if (isset($data['custom_position'])) - { - if (empty($data['position'])) - { - $data['position'] = $data['custom_position']; - } - - unset($data['custom_position']); - } - - return parent::allowSave($data, $key); - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 3.2 - */ - protected function allowEdit($data = array(), $key = 'id') - { - // Initialise variables. - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - - // Zero record (id:0), return component edit permission by calling parent controller method - if (!$recordId) - { - return parent::allowEdit($data, $key); - } - - // Check edit on the record asset (explicit or inherited) - if ($this->app->getIdentity()->authorise('core.edit', 'com_modules.module.' . $recordId)) - { - return true; - } - - return false; - } - - /** - * Method to run batch operations. - * - * @param string $model The model - * - * @return boolean True on success. - * - * @since 1.7 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - $model = $this->getModel('Module', 'Administrator', array()); - - // Preset the redirect - $redirectUrl = 'index.php?option=com_modules&view=modules' . $this->getRedirectToListAppend(); - - $this->setRedirect(Route::_($redirectUrl, false)); - - return parent::batch($model); - } - - /** - * Function that allows child controller access to model data after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 1.6 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) - { - $task = $this->getTask(); - - switch ($task) - { - case 'save2new': - $this->app->setUserState('com_modules.add.module.extension_id', $model->getState('module.extension_id')); - break; - - default: - $this->app->setUserState('com_modules.add.module.extension_id', null); - break; - } - - $this->app->setUserState('com_modules.add.module.params', null); - } - - /** - * Method to save a record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key - * - * @return boolean True if successful, false otherwise. - */ - public function save($key = null, $urlVar = null) - { - $this->checkToken(); - - if ($this->app->getDocument()->getType() == 'json') - { - $model = $this->getModel(); - $data = $this->input->post->get('jform', array(), 'array'); - $item = $model->getItem($this->input->get('id')); - $properties = $item->getProperties(); - - if (isset($data['params'])) - { - unset($properties['params']); - } - - // Replace changed properties - $data = array_replace_recursive($properties, $data); - - if (!empty($data['assigned'])) - { - $data['assigned'] = array_map('abs', $data['assigned']); - } - - // Add new data to input before process by parent save() - $this->input->post->set('jform', $data); - - // Add path of forms directory - Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_modules/models/forms'); - } - - return parent::save($key, $urlVar); - - } - - /** - * Method to get the other modules in the same position - * - * @return string The data for the Ajax request. - * - * @since 3.6.3 - */ - public function orderPosition() - { - $app = $this->app; - - // Send json mime type. - $app->mimeType = 'application/json'; - $app->setHeader('Content-Type', $app->mimeType . '; charset=' . $app->charSet); - $app->sendHeaders(); - - // Check if user token is valid. - if (!Session::checkToken('get')) - { - $app->enqueueMessage(Text::_('JINVALID_TOKEN_NOTICE'), 'error'); - echo new JsonResponse; - $app->close(); - } - - $clientId = $this->input->getValue('client_id'); - $position = $this->input->getValue('position'); - $moduleId = $this->input->getValue('module_id'); - - // Access check. - if (!$this->app->getIdentity()->authorise('core.create', 'com_modules') - && !$this->app->getIdentity()->authorise('core.edit.state', 'com_modules') - && ($moduleId && !$this->app->getIdentity()->authorise('core.edit.state', 'com_modules.module.' . $moduleId))) - { - $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error'); - echo new JsonResponse; - $app->close(); - } - - $db = Factory::getDbo(); - $clientId = (int) $clientId; - $query = $db->getQuery(true) - ->select($db->quoteName(['position', 'ordering', 'title'])) - ->from($db->quoteName('#__modules')) - ->where($db->quoteName('client_id') . ' = :clientid') - ->where($db->quoteName('position') . ' = :position') - ->order($db->quoteName('ordering')) - ->bind(':clientid', $clientId, ParameterType::INTEGER) - ->bind(':position', $position); - - $db->setQuery($query); - - try - { - $orders = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - - return ''; - } - - $orders2 = array(); - $n = count($orders); - - if ($n > 0) - { - for ($i = 0; $i < $n; $i++) - { - if (!isset($orders2[$orders[$i]->position])) - { - $orders2[$orders[$i]->position] = 0; - } - - $orders2[$orders[$i]->position]++; - $ord = $orders2[$orders[$i]->position]; - $title = Text::sprintf('COM_MODULES_OPTION_ORDER_POSITION', $ord, htmlspecialchars($orders[$i]->title, ENT_QUOTES, 'UTF-8')); - - $html[] = $orders[$i]->position . ',' . $ord . ',' . $title; - } - } - else - { - $html[] = $position . ',' . 1 . ',' . Text::_('JNONE'); - } - - echo new JsonResponse($html); - $app->close(); - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $append = parent::getRedirectToItemAppend($recordId); - $append .= '&client_id=' . $this->input->getInt('client_id'); - - return $append; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - $append .= '&client_id=' . $this->input->getInt('client_id'); - - return $append; - } + /** + * Override parent add method. + * + * @return \Exception|void True if the record can be added, a \Exception object if not. + * + * @since 1.6 + */ + public function add() + { + $app = $this->app; + + // Get the result of the parent method. If an error, just return it. + $result = parent::add(); + + if ($result instanceof \Exception) { + return $result; + } + + // Look for the Extension ID. + $extensionId = $this->input->get('eid', 0, 'int'); + + if (empty($extensionId)) { + $redirectUrl = 'index.php?option=' . $this->option . '&view=' . $this->view_item . '&layout=edit'; + + $this->setRedirect(Route::_($redirectUrl, false)); + + $app->enqueueMessage(Text::_('COM_MODULES_ERROR_INVALID_EXTENSION'), 'warning'); + } + + $app->setUserState('com_modules.add.module.extension_id', $extensionId); + $app->setUserState('com_modules.add.module.params', null); + + // Parameters could be coming in for a new item, so let's set them. + $params = $this->input->get('params', array(), 'array'); + $app->setUserState('com_modules.add.module.params', $params); + } + + /** + * Override parent cancel method to reset the add module state. + * + * @param string $key The name of the primary key of the URL variable. + * + * @return boolean True if access level checks pass, false otherwise. + * + * @since 1.6 + */ + public function cancel($key = null) + { + $result = parent::cancel(); + + $this->app->setUserState('com_modules.add.module.extension_id', null); + $this->app->setUserState('com_modules.add.module.params', null); + + if ($return = $this->input->get('return', '', 'BASE64')) { + $return = base64_decode($return); + + // Don't redirect to an external URL. + if (!Uri::isInternal($return)) { + $return = Uri::base(); + } + + $this->app->redirect($return); + } + + return $result; + } + + /** + * Override parent allowSave method. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowSave($data, $key = 'id') + { + // Use custom position if selected + if (isset($data['custom_position'])) { + if (empty($data['position'])) { + $data['position'] = $data['custom_position']; + } + + unset($data['custom_position']); + } + + return parent::allowSave($data, $key); + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 3.2 + */ + protected function allowEdit($data = array(), $key = 'id') + { + // Initialise variables. + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + + // Zero record (id:0), return component edit permission by calling parent controller method + if (!$recordId) { + return parent::allowEdit($data, $key); + } + + // Check edit on the record asset (explicit or inherited) + if ($this->app->getIdentity()->authorise('core.edit', 'com_modules.module.' . $recordId)) { + return true; + } + + return false; + } + + /** + * Method to run batch operations. + * + * @param string $model The model + * + * @return boolean True on success. + * + * @since 1.7 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + $model = $this->getModel('Module', 'Administrator', array()); + + // Preset the redirect + $redirectUrl = 'index.php?option=com_modules&view=modules' . $this->getRedirectToListAppend(); + + $this->setRedirect(Route::_($redirectUrl, false)); + + return parent::batch($model); + } + + /** + * Function that allows child controller access to model data after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 1.6 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) + { + $task = $this->getTask(); + + switch ($task) { + case 'save2new': + $this->app->setUserState('com_modules.add.module.extension_id', $model->getState('module.extension_id')); + break; + + default: + $this->app->setUserState('com_modules.add.module.extension_id', null); + break; + } + + $this->app->setUserState('com_modules.add.module.params', null); + } + + /** + * Method to save a record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key + * + * @return boolean True if successful, false otherwise. + */ + public function save($key = null, $urlVar = null) + { + $this->checkToken(); + + if ($this->app->getDocument()->getType() == 'json') { + $model = $this->getModel(); + $data = $this->input->post->get('jform', array(), 'array'); + $item = $model->getItem($this->input->get('id')); + $properties = $item->getProperties(); + + if (isset($data['params'])) { + unset($properties['params']); + } + + // Replace changed properties + $data = array_replace_recursive($properties, $data); + + if (!empty($data['assigned'])) { + $data['assigned'] = array_map('abs', $data['assigned']); + } + + // Add new data to input before process by parent save() + $this->input->post->set('jform', $data); + + // Add path of forms directory + Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_modules/models/forms'); + } + + return parent::save($key, $urlVar); + } + + /** + * Method to get the other modules in the same position + * + * @return string The data for the Ajax request. + * + * @since 3.6.3 + */ + public function orderPosition() + { + $app = $this->app; + + // Send json mime type. + $app->mimeType = 'application/json'; + $app->setHeader('Content-Type', $app->mimeType . '; charset=' . $app->charSet); + $app->sendHeaders(); + + // Check if user token is valid. + if (!Session::checkToken('get')) { + $app->enqueueMessage(Text::_('JINVALID_TOKEN_NOTICE'), 'error'); + echo new JsonResponse(); + $app->close(); + } + + $clientId = $this->input->getValue('client_id'); + $position = $this->input->getValue('position'); + $moduleId = $this->input->getValue('module_id'); + + // Access check. + if ( + !$this->app->getIdentity()->authorise('core.create', 'com_modules') + && !$this->app->getIdentity()->authorise('core.edit.state', 'com_modules') + && ($moduleId && !$this->app->getIdentity()->authorise('core.edit.state', 'com_modules.module.' . $moduleId)) + ) { + $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 'error'); + echo new JsonResponse(); + $app->close(); + } + + $db = Factory::getDbo(); + $clientId = (int) $clientId; + $query = $db->getQuery(true) + ->select($db->quoteName(['position', 'ordering', 'title'])) + ->from($db->quoteName('#__modules')) + ->where($db->quoteName('client_id') . ' = :clientid') + ->where($db->quoteName('position') . ' = :position') + ->order($db->quoteName('ordering')) + ->bind(':clientid', $clientId, ParameterType::INTEGER) + ->bind(':position', $position); + + $db->setQuery($query); + + try { + $orders = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + + return ''; + } + + $orders2 = array(); + $n = count($orders); + + if ($n > 0) { + for ($i = 0; $i < $n; $i++) { + if (!isset($orders2[$orders[$i]->position])) { + $orders2[$orders[$i]->position] = 0; + } + + $orders2[$orders[$i]->position]++; + $ord = $orders2[$orders[$i]->position]; + $title = Text::sprintf('COM_MODULES_OPTION_ORDER_POSITION', $ord, htmlspecialchars($orders[$i]->title, ENT_QUOTES, 'UTF-8')); + + $html[] = $orders[$i]->position . ',' . $ord . ',' . $title; + } + } else { + $html[] = $position . ',' . 1 . ',' . Text::_('JNONE'); + } + + echo new JsonResponse($html); + $app->close(); + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $append = parent::getRedirectToItemAppend($recordId); + $append .= '&client_id=' . $this->input->getInt('client_id'); + + return $append; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + $append .= '&client_id=' . $this->input->getInt('client_id'); + + return $append; + } } diff --git a/administrator/components/com_modules/src/Controller/ModulesController.php b/administrator/components/com_modules/src/Controller/ModulesController.php index 005e5e8ffa0f3..bf4dd445f89cb 100644 --- a/administrator/components/com_modules/src/Controller/ModulesController.php +++ b/administrator/components/com_modules/src/Controller/ModulesController.php @@ -1,4 +1,5 @@ checkToken(); - - $pks = (array) $this->input->post->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $pks = array_filter($pks); - - try - { - if (empty($pks)) - { - throw new \Exception(Text::_('COM_MODULES_ERROR_NO_MODULES_SELECTED')); - } - - $model = $this->getModel(); - $model->duplicate($pks); - $this->setMessage(Text::plural('COM_MODULES_N_MODULES_DUPLICATED', count($pks))); - } - catch (\Exception $e) - { - $this->app->enqueueMessage($e->getMessage(), 'warning'); - } - - $this->setRedirect('index.php?option=com_modules&view=modules' . $this->getRedirectToListAppend()); - } - - /** - * 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 object The model. - * - * @since 1.6 - */ - public function getModel($name = 'Module', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to get the number of frontend modules - * - * @return void - * - * @since 4.0.0 - */ - public function getQuickiconContent() - { - $model = $this->getModel('Modules'); - - $model->setState('filter.state', 1); - $model->setState('filter.client_id', 0); - - $amount = (int) $model->getTotal(); - - $result = []; - - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_MODULES_N_QUICKICON_SRONLY', $amount); - $result['name'] = Text::plural('COM_MODULES_N_QUICKICON', $amount); - - echo new JsonResponse($result); - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - $append .= '&client_id=' . $this->input->getInt('client_id'); - - return $append; - } + /** + * Method to clone an existing module. + * + * @return void + * + * @since 1.6 + */ + public function duplicate() + { + // Check for request forgeries + $this->checkToken(); + + $pks = (array) $this->input->post->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $pks = array_filter($pks); + + try { + if (empty($pks)) { + throw new \Exception(Text::_('COM_MODULES_ERROR_NO_MODULES_SELECTED')); + } + + $model = $this->getModel(); + $model->duplicate($pks); + $this->setMessage(Text::plural('COM_MODULES_N_MODULES_DUPLICATED', count($pks))); + } catch (\Exception $e) { + $this->app->enqueueMessage($e->getMessage(), 'warning'); + } + + $this->setRedirect('index.php?option=com_modules&view=modules' . $this->getRedirectToListAppend()); + } + + /** + * 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 object The model. + * + * @since 1.6 + */ + public function getModel($name = 'Module', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to get the number of frontend modules + * + * @return void + * + * @since 4.0.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('Modules'); + + $model->setState('filter.state', 1); + $model->setState('filter.client_id', 0); + + $amount = (int) $model->getTotal(); + + $result = []; + + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_MODULES_N_QUICKICON_SRONLY', $amount); + $result['name'] = Text::plural('COM_MODULES_N_QUICKICON', $amount); + + echo new JsonResponse($result); + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + $append .= '&client_id=' . $this->input->getInt('client_id'); + + return $append; + } } diff --git a/administrator/components/com_modules/src/Extension/ModulesComponent.php b/administrator/components/com_modules/src/Extension/ModulesComponent.php index 167c8b8ff60fd..1d90e6d75b772 100644 --- a/administrator/components/com_modules/src/Extension/ModulesComponent.php +++ b/administrator/components/com_modules/src/Extension/ModulesComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('modules', new Modules); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('modules', new Modules()); + } } diff --git a/administrator/components/com_modules/src/Field/ModulesModuleField.php b/administrator/components/com_modules/src/Field/ModulesModuleField.php index f748ff6f55af0..ae307df61c29b 100644 --- a/administrator/components/com_modules/src/Field/ModulesModuleField.php +++ b/administrator/components/com_modules/src/Field/ModulesModuleField.php @@ -1,4 +1,5 @@ $name; - } + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to get the value. + * + * @return mixed The property value or null. + * + * @since 4.0.0 + */ + public function __get($name) + { + switch ($name) { + case 'client': + return $this->$name; + } - return parent::__get($name); - } + return parent::__get($name); + } - /** - * Method to set certain otherwise inaccessible properties of the form field object. - * - * @param string $name The property name for which to set the value. - * @param mixed $value The value of the property. - * - * @return void - * - * @since 4.0.0 - */ - public function __set($name, $value) - { - switch ($name) - { - case 'client': - $this->$name = (string) $value; - break; + /** + * Method to set certain otherwise inaccessible properties of the form field object. + * + * @param string $name The property name for which to set the value. + * @param mixed $value The value of the property. + * + * @return void + * + * @since 4.0.0 + */ + public function __set($name, $value) + { + switch ($name) { + case 'client': + $this->$name = (string) $value; + break; - default: - parent::__set($name, $value); - } - } + default: + parent::__set($name, $value); + } + } - /** - * Method to attach a Form object to the field. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @see FormField::setup() - * @since 4.0.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $result = parent::setup($element, $value, $group); + /** + * Method to attach a Form object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @see FormField::setup() + * @since 4.0.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $result = parent::setup($element, $value, $group); - if ($result === true) - { - $this->client = $this->element['client'] ? (string) $this->element['client'] : 'site'; - } + if ($result === true) { + $this->client = $this->element['client'] ? (string) $this->element['client'] : 'site'; + } - return $result; - } + return $result; + } - /** - * Method to get the field options. - * - * @return array The field option objects. - * - * @since 3.4.2 - */ - public function getOptions() - { - $clientId = $this->client === 'administrator' ? 1 : 0; - $options = ModulesHelper::getModules($clientId); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.4.2 + */ + public function getOptions() + { + $clientId = $this->client === 'administrator' ? 1 : 0; + $options = ModulesHelper::getModules($clientId); - return array_merge(parent::getOptions(), $options); - } + return array_merge(parent::getOptions(), $options); + } } diff --git a/administrator/components/com_modules/src/Field/ModulesPositionField.php b/administrator/components/com_modules/src/Field/ModulesPositionField.php index e7e73d25b377d..b6a82d5d4116a 100644 --- a/administrator/components/com_modules/src/Field/ModulesPositionField.php +++ b/administrator/components/com_modules/src/Field/ModulesPositionField.php @@ -1,4 +1,5 @@ $name; - } + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to get the value. + * + * @return mixed The property value or null. + * + * @since 4.0.0 + */ + public function __get($name) + { + switch ($name) { + case 'client': + return $this->$name; + } - return parent::__get($name); - } + return parent::__get($name); + } - /** - * Method to set certain otherwise inaccessible properties of the form field object. - * - * @param string $name The property name for which to set the value. - * @param mixed $value The value of the property. - * - * @return void - * - * @since 4.0.0 - */ - public function __set($name, $value) - { - switch ($name) - { - case 'client': - $this->$name = (string) $value; - break; + /** + * Method to set certain otherwise inaccessible properties of the form field object. + * + * @param string $name The property name for which to set the value. + * @param mixed $value The value of the property. + * + * @return void + * + * @since 4.0.0 + */ + public function __set($name, $value) + { + switch ($name) { + case 'client': + $this->$name = (string) $value; + break; - default: - parent::__set($name, $value); - } - } + default: + parent::__set($name, $value); + } + } - /** - * Method to attach a Form object to the field. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @see FormField::setup() - * @since 4.0.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $result = parent::setup($element, $value, $group); + /** + * Method to attach a Form object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @see FormField::setup() + * @since 4.0.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $result = parent::setup($element, $value, $group); - if ($result === true) - { - $this->client = $this->element['client'] ? (string) $this->element['client'] : 'site'; - } + if ($result === true) { + $this->client = $this->element['client'] ? (string) $this->element['client'] : 'site'; + } - return $result; - } + return $result; + } - /** - * Method to get the field options. - * - * @return array The field option objects. - * - * @since 3.4.2 - */ - public function getOptions() - { - $clientId = $this->client === 'administrator' ? 1 : 0; - $options = ModulesHelper::getPositions($clientId); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 3.4.2 + */ + public function getOptions() + { + $clientId = $this->client === 'administrator' ? 1 : 0; + $options = ModulesHelper::getPositions($clientId); - return array_merge(parent::getOptions(), $options); - } + return array_merge(parent::getOptions(), $options); + } } diff --git a/administrator/components/com_modules/src/Field/ModulesPositioneditField.php b/administrator/components/com_modules/src/Field/ModulesPositioneditField.php index 040f5f3d98eff..71163417131d3 100644 --- a/administrator/components/com_modules/src/Field/ModulesPositioneditField.php +++ b/administrator/components/com_modules/src/Field/ModulesPositioneditField.php @@ -1,4 +1,5 @@ $name; - } - - return parent::__get($name); - } - - /** - * Method to set certain otherwise inaccessible properties of the form field object. - * - * @param string $name The property name for which to set the value. - * @param mixed $value The value of the property. - * - * @return void - * - * @since 4.0.0 - */ - public function __set($name, $value) - { - switch ($name) - { - case 'client': - $this->$name = (string) $value; - break; - - default: - parent::__set($name, $value); - } - } - - /** - * Method to attach a Form object to the field. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @see FormField::setup() - * @since 4.0.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $result = parent::setup($element, $value, $group); - - if ($result === true) - { - $this->client = $this->element['client'] ? (string) $this->element['client'] : 'site'; - } - - return $result; - } - - /** - * Method to get the field input markup. - * - * @return string The field input markup. - * - * @since 4.0.0 - */ - protected function getInput() - { - $data = $this->getLayoutData(); - - $clientId = $this->client === 'administrator' ? 1 : 0; - $positions = HTMLHelper::_('modules.positions', $clientId, 1, $this->value); - - $data['client'] = $clientId; - $data['positions'] = $positions; - - $renderer = $this->getRenderer($this->layout); - $renderer->setComponent('com_modules'); - $renderer->setClient(1); - - return $renderer->render($data); - } + /** + * The form field type. + * + * @var string + * @since 4.0.0 + */ + protected $type = 'ModulesPositionedit'; + + /** + * Name of the layout being used to render the field + * + * @var string + * @since 4.0.0 + */ + protected $layout = 'joomla.form.field.modulespositionedit'; + + /** + * Client name. + * + * @var string + * @since 4.0.0 + */ + protected $client; + + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to get the value. + * + * @return mixed The property value or null. + * + * @since 4.0.0 + */ + public function __get($name) + { + switch ($name) { + case 'client': + return $this->$name; + } + + return parent::__get($name); + } + + /** + * Method to set certain otherwise inaccessible properties of the form field object. + * + * @param string $name The property name for which to set the value. + * @param mixed $value The value of the property. + * + * @return void + * + * @since 4.0.0 + */ + public function __set($name, $value) + { + switch ($name) { + case 'client': + $this->$name = (string) $value; + break; + + default: + parent::__set($name, $value); + } + } + + /** + * Method to attach a Form object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @see FormField::setup() + * @since 4.0.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $result = parent::setup($element, $value, $group); + + if ($result === true) { + $this->client = $this->element['client'] ? (string) $this->element['client'] : 'site'; + } + + return $result; + } + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 4.0.0 + */ + protected function getInput() + { + $data = $this->getLayoutData(); + + $clientId = $this->client === 'administrator' ? 1 : 0; + $positions = HTMLHelper::_('modules.positions', $clientId, 1, $this->value); + + $data['client'] = $clientId; + $data['positions'] = $positions; + + $renderer = $this->getRenderer($this->layout); + $renderer->setComponent('com_modules'); + $renderer->setClient(1); + + return $renderer->render($data); + } } diff --git a/administrator/components/com_modules/src/Helper/ModulesHelper.php b/administrator/components/com_modules/src/Helper/ModulesHelper.php index 6657d2f1b988a..e2aefdc58a359 100644 --- a/administrator/components/com_modules/src/Helper/ModulesHelper.php +++ b/administrator/components/com_modules/src/Helper/ModulesHelper.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('DISTINCT ' . $db->quoteName('position')) - ->from($db->quoteName('#__modules')) - ->where($db->quoteName('client_id') . ' = :clientid') - ->order($db->quoteName('position')) - ->bind(':clientid', $clientId, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $positions = $db->loadColumn(); - $positions = is_array($positions) ? $positions : array(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return; - } - - // Build the list - $options = array(); - - foreach ($positions as $position) - { - if (!$position && !$editPositions) - { - $options[] = HTMLHelper::_('select.option', 'none', Text::_('COM_MODULES_NONE')); - } - elseif (!$position) - { - $options[] = HTMLHelper::_('select.option', '', Text::_('COM_MODULES_NONE')); - } - else - { - $options[] = HTMLHelper::_('select.option', $position, $position); - } - } - - return $options; - } - - /** - * Return a list of templates - * - * @param integer $clientId Client ID - * @param string $state State - * @param string $template Template name - * - * @return array List of templates - */ - public static function getTemplates($clientId = 0, $state = '', $template = '') - { - $db = Factory::getDbo(); - $clientId = (int) $clientId; - - // Get the database object and a new query object. - $query = $db->getQuery(true); - - // Build the query. - $query->select($db->quoteName(['element', 'name', 'enabled'])) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('client_id') . ' = :clientid') - ->where($db->quoteName('type') . ' = ' . $db->quote('template')); - - if ($state != '') - { - $query->where($db->quoteName('enabled') . ' = :state') - ->bind(':state', $state); - } - - if ($template != '') - { - $query->where($db->quoteName('element') . ' = :element') - ->bind(':element', $template); - } - - $query->bind(':clientid', $clientId, ParameterType::INTEGER); - - // Set the query and load the templates. - $db->setQuery($query); - $templates = $db->loadObjectList('element'); - - return $templates; - } - - /** - * Get a list of the unique modules installed in the client application. - * - * @param int $clientId The client id. - * - * @return array Array of unique modules - */ - public static function getModules($clientId) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('element AS value, name AS text') - ->from('#__extensions as e') - ->where('e.client_id = ' . (int) $clientId) - ->where('type = ' . $db->quote('module')) - ->join('LEFT', '#__modules as m ON m.module=e.element AND m.client_id=e.client_id') - ->where('m.module IS NOT NULL') - ->group('element,name'); - - $db->setQuery($query); - $modules = $db->loadObjectList(); - $lang = Factory::getLanguage(); - - foreach ($modules as $i => $module) - { - $extension = $module->value; - $path = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; - $source = $path . "/modules/$extension"; - $lang->load("$extension.sys", $path) - || $lang->load("$extension.sys", $source); - $modules[$i]->text = Text::_($module->text); - } - - $modules = ArrayHelper::sortObjects($modules, 'text', 1, true, true); - - return $modules; - } - - /** - * Get a list of the assignment options for modules to menus. - * - * @param int $clientId The client id. - * - * @return array - */ - public static function getAssignmentOptions($clientId) - { - $options = array(); - $options[] = HTMLHelper::_('select.option', '0', 'COM_MODULES_OPTION_MENU_ALL'); - $options[] = HTMLHelper::_('select.option', '-', 'COM_MODULES_OPTION_MENU_NONE'); - - if ($clientId == 0) - { - $options[] = HTMLHelper::_('select.option', '1', 'COM_MODULES_OPTION_MENU_INCLUDE'); - $options[] = HTMLHelper::_('select.option', '-1', 'COM_MODULES_OPTION_MENU_EXCLUDE'); - } - - return $options; - } - - /** - * Return a translated module position name - * - * @param integer $clientId Application client id 0: site | 1: admin - * @param string $template Template name - * @param string $position Position name - * - * @return string Return a translated position name - * - * @since 3.0 - */ - public static function getTranslatedModulePosition($clientId, $template, $position) - { - // Template translation - $lang = Factory::getLanguage(); - $path = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; - - $loaded = $lang->getPaths('tpl_' . $template . '.sys'); - - // Only load the template's language file if it hasn't been already - if (!$loaded) - { - $lang->load('tpl_' . $template . '.sys', $path, null, false, false) - || $lang->load('tpl_' . $template . '.sys', $path . '/templates/' . $template, null, false, false) - || $lang->load('tpl_' . $template . '.sys', $path, $lang->getDefault(), false, false) - || $lang->load('tpl_' . $template . '.sys', $path . '/templates/' . $template, $lang->getDefault(), false, false); - } - - $langKey = strtoupper('TPL_' . $template . '_POSITION_' . $position); - $text = Text::_($langKey); - - // Avoid untranslated strings - if (!self::isTranslatedText($langKey, $text)) - { - // Modules component translation - $langKey = strtoupper('COM_MODULES_POSITION_' . $position); - $text = Text::_($langKey); - - // Avoid untranslated strings - if (!self::isTranslatedText($langKey, $text)) - { - // Try to humanize the position name - $text = ucfirst(preg_replace('/^' . $template . '\-/', '', $position)); - $text = ucwords(str_replace(array('-', '_'), ' ', $text)); - } - } - - return $text; - } - - /** - * Check if the string was translated - * - * @param string $langKey Language file text key - * @param string $text The "translated" text to be checked - * - * @return boolean Return true for translated text - * - * @since 3.0 - */ - public static function isTranslatedText($langKey, $text) - { - return $text !== $langKey; - } - - /** - * Create and return a new Option - * - * @param string $value The option value [optional] - * @param string $text The option text [optional] - * - * @return object The option as an object (\stdClass instance) - * - * @since 3.0 - */ - public static function createOption($value = '', $text = '') - { - if (empty($text)) - { - $text = $value; - } - - $option = new \stdClass; - $option->value = $value; - $option->text = $text; - - return $option; - } - - /** - * Create and return a new Option Group - * - * @param string $label Value and label for group [optional] - * @param array $options Array of options to insert into group [optional] - * - * @return array Return the new group as an array - * - * @since 3.0 - */ - public static function createOptionGroup($label = '', $options = array()) - { - $group = array(); - $group['value'] = $label; - $group['text'] = $label; - $group['items'] = $options; - - return $group; - } + /** + * Get a list of filter options for the state of a module. + * + * @return array An array of \JHtmlOption elements. + */ + public static function getStateOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '1', Text::_('JPUBLISHED')); + $options[] = HTMLHelper::_('select.option', '0', Text::_('JUNPUBLISHED')); + $options[] = HTMLHelper::_('select.option', '-2', Text::_('JTRASHED')); + $options[] = HTMLHelper::_('select.option', '*', Text::_('JALL')); + + return $options; + } + + /** + * Get a list of filter options for the application clients. + * + * @return array An array of \JHtmlOption elements. + */ + public static function getClientOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '0', Text::_('JSITE')); + $options[] = HTMLHelper::_('select.option', '1', Text::_('JADMINISTRATOR')); + + return $options; + } + + /** + * Get a list of modules positions + * + * @param integer $clientId Client ID + * @param boolean $editPositions Allow to edit the positions + * + * @return array A list of positions + */ + public static function getPositions($clientId, $editPositions = false) + { + $db = Factory::getDbo(); + $clientId = (int) $clientId; + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('position')) + ->from($db->quoteName('#__modules')) + ->where($db->quoteName('client_id') . ' = :clientid') + ->order($db->quoteName('position')) + ->bind(':clientid', $clientId, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $positions = $db->loadColumn(); + $positions = is_array($positions) ? $positions : array(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return; + } + + // Build the list + $options = array(); + + foreach ($positions as $position) { + if (!$position && !$editPositions) { + $options[] = HTMLHelper::_('select.option', 'none', Text::_('COM_MODULES_NONE')); + } elseif (!$position) { + $options[] = HTMLHelper::_('select.option', '', Text::_('COM_MODULES_NONE')); + } else { + $options[] = HTMLHelper::_('select.option', $position, $position); + } + } + + return $options; + } + + /** + * Return a list of templates + * + * @param integer $clientId Client ID + * @param string $state State + * @param string $template Template name + * + * @return array List of templates + */ + public static function getTemplates($clientId = 0, $state = '', $template = '') + { + $db = Factory::getDbo(); + $clientId = (int) $clientId; + + // Get the database object and a new query object. + $query = $db->getQuery(true); + + // Build the query. + $query->select($db->quoteName(['element', 'name', 'enabled'])) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('client_id') . ' = :clientid') + ->where($db->quoteName('type') . ' = ' . $db->quote('template')); + + if ($state != '') { + $query->where($db->quoteName('enabled') . ' = :state') + ->bind(':state', $state); + } + + if ($template != '') { + $query->where($db->quoteName('element') . ' = :element') + ->bind(':element', $template); + } + + $query->bind(':clientid', $clientId, ParameterType::INTEGER); + + // Set the query and load the templates. + $db->setQuery($query); + $templates = $db->loadObjectList('element'); + + return $templates; + } + + /** + * Get a list of the unique modules installed in the client application. + * + * @param int $clientId The client id. + * + * @return array Array of unique modules + */ + public static function getModules($clientId) + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('element AS value, name AS text') + ->from('#__extensions as e') + ->where('e.client_id = ' . (int) $clientId) + ->where('type = ' . $db->quote('module')) + ->join('LEFT', '#__modules as m ON m.module=e.element AND m.client_id=e.client_id') + ->where('m.module IS NOT NULL') + ->group('element,name'); + + $db->setQuery($query); + $modules = $db->loadObjectList(); + $lang = Factory::getLanguage(); + + foreach ($modules as $i => $module) { + $extension = $module->value; + $path = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; + $source = $path . "/modules/$extension"; + $lang->load("$extension.sys", $path) + || $lang->load("$extension.sys", $source); + $modules[$i]->text = Text::_($module->text); + } + + $modules = ArrayHelper::sortObjects($modules, 'text', 1, true, true); + + return $modules; + } + + /** + * Get a list of the assignment options for modules to menus. + * + * @param int $clientId The client id. + * + * @return array + */ + public static function getAssignmentOptions($clientId) + { + $options = array(); + $options[] = HTMLHelper::_('select.option', '0', 'COM_MODULES_OPTION_MENU_ALL'); + $options[] = HTMLHelper::_('select.option', '-', 'COM_MODULES_OPTION_MENU_NONE'); + + if ($clientId == 0) { + $options[] = HTMLHelper::_('select.option', '1', 'COM_MODULES_OPTION_MENU_INCLUDE'); + $options[] = HTMLHelper::_('select.option', '-1', 'COM_MODULES_OPTION_MENU_EXCLUDE'); + } + + return $options; + } + + /** + * Return a translated module position name + * + * @param integer $clientId Application client id 0: site | 1: admin + * @param string $template Template name + * @param string $position Position name + * + * @return string Return a translated position name + * + * @since 3.0 + */ + public static function getTranslatedModulePosition($clientId, $template, $position) + { + // Template translation + $lang = Factory::getLanguage(); + $path = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; + + $loaded = $lang->getPaths('tpl_' . $template . '.sys'); + + // Only load the template's language file if it hasn't been already + if (!$loaded) { + $lang->load('tpl_' . $template . '.sys', $path, null, false, false) + || $lang->load('tpl_' . $template . '.sys', $path . '/templates/' . $template, null, false, false) + || $lang->load('tpl_' . $template . '.sys', $path, $lang->getDefault(), false, false) + || $lang->load('tpl_' . $template . '.sys', $path . '/templates/' . $template, $lang->getDefault(), false, false); + } + + $langKey = strtoupper('TPL_' . $template . '_POSITION_' . $position); + $text = Text::_($langKey); + + // Avoid untranslated strings + if (!self::isTranslatedText($langKey, $text)) { + // Modules component translation + $langKey = strtoupper('COM_MODULES_POSITION_' . $position); + $text = Text::_($langKey); + + // Avoid untranslated strings + if (!self::isTranslatedText($langKey, $text)) { + // Try to humanize the position name + $text = ucfirst(preg_replace('/^' . $template . '\-/', '', $position)); + $text = ucwords(str_replace(array('-', '_'), ' ', $text)); + } + } + + return $text; + } + + /** + * Check if the string was translated + * + * @param string $langKey Language file text key + * @param string $text The "translated" text to be checked + * + * @return boolean Return true for translated text + * + * @since 3.0 + */ + public static function isTranslatedText($langKey, $text) + { + return $text !== $langKey; + } + + /** + * Create and return a new Option + * + * @param string $value The option value [optional] + * @param string $text The option text [optional] + * + * @return object The option as an object (\stdClass instance) + * + * @since 3.0 + */ + public static function createOption($value = '', $text = '') + { + if (empty($text)) { + $text = $value; + } + + $option = new \stdClass(); + $option->value = $value; + $option->text = $text; + + return $option; + } + + /** + * Create and return a new Option Group + * + * @param string $label Value and label for group [optional] + * @param array $options Array of options to insert into group [optional] + * + * @return array Return the new group as an array + * + * @since 3.0 + */ + public static function createOptionGroup($label = '', $options = array()) + { + $group = array(); + $group['value'] = $label; + $group['text'] = $label; + $group['items'] = $options; + + return $group; + } } diff --git a/administrator/components/com_modules/src/Model/ModuleModel.php b/administrator/components/com_modules/src/Model/ModuleModel.php index fc5c49137da52..ab36763da9ab9 100644 --- a/administrator/components/com_modules/src/Model/ModuleModel.php +++ b/administrator/components/com_modules/src/Model/ModuleModel.php @@ -1,4 +1,5 @@ 'batchAccess', - 'language_id' => 'batchLanguage', - ); - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - */ - public function __construct($config = array()) - { - $config = array_merge( - array( - 'event_after_delete' => 'onExtensionAfterDelete', - 'event_after_save' => 'onExtensionAfterSave', - 'event_before_delete' => 'onExtensionBeforeDelete', - 'event_before_save' => 'onExtensionBeforeSave', - 'events_map' => array( - 'save' => 'extension', - 'delete' => 'extension' - ) - ), $config - ); - - parent::__construct($config); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - // Load the User state. - $pk = $app->input->getInt('id'); - - if (!$pk) - { - if ($extensionId = (int) $app->getUserState('com_modules.add.module.extension_id')) - { - $this->setState('extension.id', $extensionId); - } - } - - $this->setState('module.id', $pk); - - // Load the parameters. - $params = ComponentHelper::getParams('com_modules'); - $this->setState('params', $params); - } - - /** - * Batch copy modules to a new position or current. - * - * @param integer $value The new value matching a module position. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 2.5 - */ - protected function batchCopy($value, $pks, $contexts) - { - // Set the variables - $user = Factory::getUser(); - $table = $this->getTable(); - $newIds = array(); - - foreach ($pks as $pk) - { - if ($user->authorise('core.create', 'com_modules')) - { - $table->reset(); - $table->load($pk); - - // Set the new position - if ($value == 'noposition') - { - $position = ''; - } - elseif ($value == 'nochange') - { - $position = $table->position; - } - else - { - $position = $value; - } - - $table->position = $position; - - // Copy of the Asset ID - $oldAssetId = $table->asset_id; - - // Alter the title if necessary - $data = $this->generateNewTitle(0, $table->title, $table->position); - $table->title = $data['0']; - - // Reset the ID because we are making a copy - $table->id = 0; - - // Unpublish the new module - $table->published = 0; - - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Get the new item ID - $newId = $table->get('id'); - - // Add the new ID to the array - $newIds[$pk] = $newId; - - // Now we need to handle the module assignments - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('menuid')) - ->from($db->quoteName('#__modules_menu')) - ->where($db->quoteName('moduleid') . ' = :moduleid') - ->bind(':moduleid', $pk, ParameterType::INTEGER); - $db->setQuery($query); - $menus = $db->loadColumn(); - - // Insert the new records into the table - foreach ($menus as $i => $menu) - { - $query->clear() - ->insert($db->quoteName('#__modules_menu')) - ->columns($db->quoteName(['moduleid', 'menuid'])) - ->values(implode(', ', [':newid' . $i, ':menu' . $i])) - ->bind(':newid' . $i, $newId, ParameterType::INTEGER) - ->bind(':menu' . $i, $menu, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - } - - // Copy rules - $query->clear() - ->update($db->quoteName('#__assets', 't')) - ->join('INNER', $db->quoteName('#__assets', 's') . - ' ON ' . $db->quoteName('s.id') . ' = ' . $oldAssetId - ) - ->set($db->quoteName('t.rules') . ' = ' . $db->quoteName('s.rules')) - ->where($db->quoteName('t.id') . ' = ' . $table->asset_id); - - $db->setQuery($query)->execute(); - } - else - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_CREATE')); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return $newIds; - } - - /** - * Batch move modules to a new position or current. - * - * @param integer $value The new value matching a module position. - * @param array $pks An array of row IDs. - * @param array $contexts An array of item contexts. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 2.5 - */ - protected function batchMove($value, $pks, $contexts) - { - // Set the variables - $user = Factory::getUser(); - $table = $this->getTable(); - - foreach ($pks as $pk) - { - if ($user->authorise('core.edit', 'com_modules')) - { - $table->reset(); - $table->load($pk); - - // Set the new position - if ($value == 'noposition') - { - $position = ''; - } - elseif ($value == 'nochange') - { - $position = $table->position; - } - else - { - $position = $value; - } - - $table->position = $position; - - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - } - else - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); - - return false; - } - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to test whether a record can have its state edited. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 3.2 - */ - protected function canEditState($record) - { - // Check for existing module. - if (!empty($record->id)) - { - return Factory::getUser()->authorise('core.edit.state', 'com_modules.module.' . (int) $record->id); - } - - // Default to component settings if module not known. - return parent::canEditState($record); - } - - /** - * Method to delete rows. - * - * @param array &$pks An array of item ids. - * - * @return boolean Returns true on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function delete(&$pks) - { - $app = Factory::getApplication(); - $pks = (array) $pks; - $user = Factory::getUser(); - $table = $this->getTable(); - $context = $this->option . '.' . $this->name; - - // Include the plugins for the on delete events. - PluginHelper::importPlugin($this->events_map['delete']); - - // Iterate the items to delete each one. - foreach ($pks as $pk) - { - if ($table->load($pk)) - { - // Access checks. - if (!$user->authorise('core.delete', 'com_modules.module.' . (int) $pk) || $table->published != -2) - { - Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); - - return; - } - - // Trigger the before delete event. - $result = $app->triggerEvent($this->event_before_delete, array($context, $table)); - - if (in_array(false, $result, true) || !$table->delete($pk)) - { - throw new \Exception($table->getError()); - } - else - { - // Delete the menu assignments - $pk = (int) $pk; - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__modules_menu')) - ->where($db->quoteName('moduleid') . ' = :moduleid') - ->bind(':moduleid', $pk, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - // Trigger the after delete event. - $app->triggerEvent($this->event_after_delete, array($context, $table)); - } - - // Clear module cache - parent::cleanCache($table->module); - } - else - { - throw new \Exception($table->getError()); - } - } - - // Clear modules cache - $this->cleanCache(); - - return true; - } - - /** - * Method to duplicate modules. - * - * @param array &$pks An array of primary key IDs. - * - * @return boolean Boolean true on success - * - * @since 1.6 - * @throws \Exception - */ - public function duplicate(&$pks) - { - $user = Factory::getUser(); - $db = $this->getDatabase(); - - // Access checks. - if (!$user->authorise('core.create', 'com_modules')) - { - throw new \Exception(Text::_('JERROR_CORE_CREATE_NOT_PERMITTED')); - } - - $table = $this->getTable(); - - foreach ($pks as $pk) - { - if ($table->load($pk, true)) - { - // Reset the id to create a new record. - $table->id = 0; - - // Alter the title. - $m = null; - - if (preg_match('#\((\d+)\)$#', $table->title, $m)) - { - $table->title = preg_replace('#\(\d+\)$#', '(' . ($m[1] + 1) . ')', $table->title); - } - - $data = $this->generateNewTitle(0, $table->title, $table->position); - $table->title = $data[0]; - - // Unpublish duplicate module - $table->published = 0; - - if (!$table->check() || !$table->store()) - { - throw new \Exception($table->getError()); - } - - $pk = (int) $pk; - $query = $db->getQuery(true) - ->select($db->quoteName('menuid')) - ->from($db->quoteName('#__modules_menu')) - ->where($db->quoteName('moduleid') . ' = :moduleid') - ->bind(':moduleid', $pk, ParameterType::INTEGER); - - $db->setQuery($query); - $rows = $db->loadColumn(); - - foreach ($rows as $menuid) - { - $tuples[] = (int) $table->id . ',' . (int) $menuid; - } - } - else - { - throw new \Exception($table->getError()); - } - } - - if (!empty($tuples)) - { - // Module-Menu Mapping: Do it in one query - $query = $db->getQuery(true) - ->insert($db->quoteName('#__modules_menu')) - ->columns($db->quoteName(array('moduleid', 'menuid'))) - ->values($tuples); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - } - - // Clear modules cache - $this->cleanCache(); - - return true; - } - - /** - * Method to change the title. - * - * @param integer $categoryId The id of the category. Not used here. - * @param string $title The title. - * @param string $position The position. - * - * @return array Contains the modified title. - * - * @since 2.5 - */ - protected function generateNewTitle($categoryId, $title, $position) - { - // Alter the title & alias - $table = $this->getTable(); - - while ($table->load(array('position' => $position, 'title' => $title))) - { - $title = StringHelper::increment($title); - } - - return array($title); - } - - /** - * Method to get the client object - * - * @return void - * - * @since 1.6 - */ - public function &getClient() - { - return $this->_client; - } - - /** - * Method to get the record form. - * - * @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 Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // The folder and element vars are passed when saving the form. - if (empty($data)) - { - $item = $this->getItem(); - $clientId = $item->client_id; - $module = $item->module; - $id = $item->id; - } - else - { - $clientId = ArrayHelper::getValue($data, 'client_id'); - $module = ArrayHelper::getValue($data, 'module'); - $id = ArrayHelper::getValue($data, 'id'); - } - - // Add the default fields directory - $baseFolder = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; - Form::addFieldPath($baseFolder . '/modules/' . $module . '/field'); - - // These variables are used to add data from the plugin XML files. - $this->setState('item.client_id', $clientId); - $this->setState('item.module', $module); - - // Get the form. - if ($clientId == 1) - { - $form = $this->loadForm('com_modules.module.admin', 'moduleadmin', array('control' => 'jform', 'load_data' => $loadData), true); - - // Display language field to filter admin custom menus per language - if (!ModuleHelper::isAdminMultilang()) - { - $form->setFieldAttribute('language', 'type', 'hidden'); - } - } - else - { - $form = $this->loadForm('com_modules.module', 'module', array('control' => 'jform', 'load_data' => $loadData), true); - } - - if (empty($form)) - { - return false; - } - - $user = Factory::getUser(); - - /** - * Check for existing module - * Modify the form based on Edit State access controls. - */ - if ($id != 0 && (!$user->authorise('core.edit.state', 'com_modules.module.' . (int) $id)) - || ($id == 0 && !$user->authorise('core.edit.state', 'com_modules')) ) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('published', 'disabled', 'true'); - $form->setFieldAttribute('publish_up', 'disabled', 'true'); - $form->setFieldAttribute('publish_down', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('published', 'filter', 'unset'); - $form->setFieldAttribute('publish_up', 'filter', 'unset'); - $form->setFieldAttribute('publish_down', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - $app = Factory::getApplication(); - - // Check the session for previously entered form data. - $data = $app->getUserState('com_modules.edit.module.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Pre-select some filters (Status, Module Position, Language, Access Level) in edit form if those have been selected in Module Manager - if (!$data->id) - { - $clientId = $app->input->getInt('client_id', 0); - $filters = (array) $app->getUserState('com_modules.modules.' . $clientId . '.filter'); - $data->set('published', $app->input->getInt('published', ((isset($filters['state']) && $filters['state'] !== '') ? $filters['state'] : null))); - $data->set('position', $app->input->getInt('position', (!empty($filters['position']) ? $filters['position'] : null))); - $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); - $data->set('access', $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access')))); - } - - // Avoid to delete params of a second module opened in a new browser tab while new one is not saved yet. - if (empty($data->params)) - { - // This allows us to inject parameter settings into a new module. - $params = $app->getUserState('com_modules.add.module.params'); - - if (is_array($params)) - { - $data->set('params', $params); - } - } - } - - $this->preprocessData('com_modules.module', $data); - - return $data; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - $pk = (!empty($pk)) ? (int) $pk : (int) $this->getState('module.id'); - $db = $this->getDatabase(); - - if (!isset($this->_cache[$pk])) - { - // Get a row instance. - $table = $this->getTable(); - - // Attempt to load the row. - $return = $table->load($pk); - - // Check for a table object error. - if ($return === false && $error = $table->getError()) - { - $this->setError($error); - - return false; - } - - // Check if we are creating a new extension. - if (empty($pk)) - { - if ($extensionId = (int) $this->getState('extension.id')) - { - $query = $db->getQuery(true) - ->select($db->quoteName(['element', 'client_id'])) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('extension_id') . ' = :extensionid') - ->where($db->quoteName('type') . ' = ' . $db->quote('module')) - ->bind(':extensionid', $extensionId, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $extension = $db->loadObject(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (empty($extension)) - { - $this->setError('COM_MODULES_ERROR_CANNOT_FIND_MODULE'); - - return false; - } - - // Extension found, prime some module values. - $table->module = $extension->element; - $table->client_id = $extension->client_id; - } - else - { - Factory::getApplication()->redirect(Route::_('index.php?option=com_modules&view=modules', false)); - - return false; - } - } - - // Convert to the \Joomla\CMS\Object\CMSObject before adding other data. - $properties = $table->getProperties(1); - $this->_cache[$pk] = ArrayHelper::toObject($properties, CMSObject::class); - - // Convert the params field to an array. - $registry = new Registry($table->params); - $this->_cache[$pk]->params = $registry->toArray(); - - // Determine the page assignment mode. - $query = $db->getQuery(true) - ->select($db->quoteName('menuid')) - ->from($db->quoteName('#__modules_menu')) - ->where($db->quoteName('moduleid') . ' = :moduleid') - ->bind(':moduleid', $pk, ParameterType::INTEGER); - $db->setQuery($query); - $assigned = $db->loadColumn(); - - if (empty($pk)) - { - // If this is a new module, assign to all pages. - $assignment = 0; - } - elseif (empty($assigned)) - { - // For an existing module it is assigned to none. - $assignment = '-'; - } - else - { - if ($assigned[0] > 0) - { - $assignment = 1; - } - elseif ($assigned[0] < 0) - { - $assignment = -1; - } - else - { - $assignment = 0; - } - } - - $this->_cache[$pk]->assigned = $assigned; - $this->_cache[$pk]->assignment = $assignment; - - // Get the module XML. - $client = ApplicationHelper::getClientInfo($table->client_id); - $path = Path::clean($client->path . '/modules/' . $table->module . '/' . $table->module . '.xml'); - - if (file_exists($path)) - { - $this->_cache[$pk]->xml = simplexml_load_file($path); - } - else - { - $this->_cache[$pk]->xml = null; - } - } - - return $this->_cache[$pk]; - } - - /** - * Get the necessary data to load an item help screen. - * - * @return object An object with key, url, and local properties for loading the item help screen. - * - * @since 1.6 - */ - public function getHelp() - { - return (object) array('key' => $this->helpKey, 'url' => $this->helpURL); - } - - /** - * Returns a reference to the a Table object, always creating it. - * - * @param string $type The table type to instantiate - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A database object - * - * @since 1.6 - */ - public function getTable($type = 'Module', $prefix = 'JTable', $config = array()) - { - return Table::getInstance($type, $prefix, $config); - } - - /** - * Prepare and sanitise the table prior to saving. - * - * @param Table $table The database object - * - * @return void - * - * @since 1.6 - */ - protected function prepareTable($table) - { - $table->title = htmlspecialchars_decode($table->title, ENT_QUOTES); - $table->position = trim($table->position); - } - - /** - * Method to preprocess the form - * - * @param Form $form A form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error loading the form. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $lang = Factory::getLanguage(); - $clientId = $this->getState('item.client_id'); - $module = $this->getState('item.module'); - - $client = ApplicationHelper::getClientInfo($clientId); - $formFile = Path::clean($client->path . '/modules/' . $module . '/' . $module . '.xml'); - - // Load the core and/or local language file(s). - $lang->load($module, $client->path) - || $lang->load($module, $client->path . '/modules/' . $module); - - if (file_exists($formFile)) - { - // Get the module form. - if (!$form->loadFile($formFile, false, '//config')) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Attempt to load the xml file. - if (!$xml = simplexml_load_file($formFile)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Get the help data from the XML file if present. - $help = $xml->xpath('/extension/help'); - - if (!empty($help)) - { - $helpKey = trim((string) $help[0]['key']); - $helpURL = trim((string) $help[0]['url']); - - $this->helpKey = $helpKey ?: $this->helpKey; - $this->helpURL = $helpURL ?: $this->helpURL; - } - } - - // Load the default advanced params - Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_modules/models/forms'); - $form->loadFile('advanced', false); - - // Load chrome specific params for global files - $chromePath = JPATH_SITE . '/layouts/chromes'; - $chromeFormFiles = Folder::files($chromePath, '.*\.xml'); - - if ($chromeFormFiles) - { - Form::addFormPath($chromePath); - - foreach ($chromeFormFiles as $formFile) - { - $form->loadFile(basename($formFile, '.xml'), false); - } - } - - // Load chrome specific params for template files - $templates = ModulesHelper::getTemplates($clientId); - - foreach ($templates as $template) - { - $chromePath = $client->path . '/templates/' . $template->element . '/html/layouts/chromes'; - - // Skip if there is no chrome folder in that template. - if (!is_dir($chromePath)) - { - continue; - } - - $chromeFormFiles = Folder::files($chromePath, '.*\.xml'); - - if ($chromeFormFiles) - { - Form::addFormPath($chromePath); - - foreach ($chromeFormFiles as $formFile) - { - $form->loadFile(basename($formFile, '.xml'), false); - } - } - } - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } - - /** - * Loads ContentHelper for filters before validating data. - * - * @param object $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the group(defaults to null). - * - * @return mixed Array of filtered data if valid, false otherwise. - * - * @since 1.1 - */ - public function validate($form, $data, $group = null) - { - if (!Factory::getUser()->authorise('core.admin', 'com_modules')) - { - if (isset($data['rules'])) - { - unset($data['rules']); - } - } - - return parent::validate($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - $input = Factory::getApplication()->input; - $table = $this->getTable(); - $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('module.id'); - $isNew = true; - $context = $this->option . '.' . $this->name; - - // Include the plugins for the save event. - PluginHelper::importPlugin($this->events_map['save']); - - // Load the row if saving an existing record. - if ($pk > 0) - { - $table->load($pk); - $isNew = false; - } - - // Alter the title and published state for Save as Copy - if ($input->get('task') == 'save2copy') - { - $orig_table = clone $this->getTable(); - $orig_table->load((int) $input->getInt('id')); - $data['published'] = 0; - - if ($data['title'] == $orig_table->title) - { - $data['title'] = StringHelper::increment($data['title']); - } - } - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Prepare the row for saving - $this->prepareTable($table); - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, $isNew)); - - if (in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - // Store the data. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Process the menu link mappings. - $assignment = $data['assignment'] ?? 0; - - $table->id = (int) $table->id; - - // Delete old module to menu item associations - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__modules_menu')) - ->where($db->quoteName('moduleid') . ' = :moduleid') - ->bind(':moduleid', $table->id, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // If the assignment is numeric, then something is selected (otherwise it's none). - if (is_numeric($assignment)) - { - // Variable is numeric, but could be a string. - $assignment = (int) $assignment; - - // Logic check: if no module excluded then convert to display on all. - if ($assignment == -1 && empty($data['assigned'])) - { - $assignment = 0; - } - - // Check needed to stop a module being assigned to `All` - // and other menu items resulting in a module being displayed twice. - if ($assignment === 0) - { - // Assign new module to `all` menu item associations. - $query->clear() - ->insert($db->quoteName('#__modules_menu')) - ->columns($db->quoteName(['moduleid', 'menuid'])) - ->values(implode(', ', [':moduleid', 0])) - ->bind(':moduleid', $table->id, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - elseif (!empty($data['assigned'])) - { - // Get the sign of the number. - $sign = $assignment < 0 ? -1 : 1; - - $query->clear() - ->insert($db->quoteName('#__modules_menu')) - ->columns($db->quoteName(array('moduleid', 'menuid'))); - - foreach ($data['assigned'] as &$pk) - { - $query->values((int) $table->id . ',' . (int) $pk * $sign); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - } - - // Trigger the after save event. - Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, $isNew)); - - // Compute the extension id of this module in case the controller wants it. - $query->clear() - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions', 'e')) - ->join( - 'LEFT', - $db->quoteName('#__modules', 'm') . ' ON ' . $db->quoteName('e.client_id') . ' = ' . (int) $table->client_id . - ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quoteName('m.module') - ) - ->where($db->quoteName('m.id') . ' = :id') - ->bind(':id', $table->id, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $extensionId = $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - $this->setState('module.extension_id', $extensionId); - $this->setState('module.id', $table->id); - - // Clear modules cache - $this->cleanCache(); - - // Clean module cache - parent::cleanCache($table->module); - - return true; - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param object $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - $db = $this->getDatabase(); - - return [ - $db->quoteName('client_id') . ' = ' . (int) $table->client_id, - $db->quoteName('position') . ' = ' . $db->quote($table->position), - ]; - } - - /** - * Custom clean cache method for different clients - * - * @param string $group The name of the plugin group to import (defaults to null). - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('com_modules'); - } + /** + * The type alias for this content type. + * + * @var string + * @since 3.4 + */ + public $typeAlias = 'com_modules.module'; + + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_MODULES'; + + /** + * @var string The help screen key for the module. + * @since 1.6 + */ + protected $helpKey = ''; + + /** + * @var string The help screen base URL for the module. + * @since 1.6 + */ + protected $helpURL; + + /** + * Batch copy/move command. If set to false, + * the batch copy/move command is not supported + * + * @var string + */ + protected $batch_copymove = 'position_id'; + + /** + * Allowed batch commands + * + * @var array + */ + protected $batch_commands = array( + 'assetgroup_id' => 'batchAccess', + 'language_id' => 'batchLanguage', + ); + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + */ + public function __construct($config = array()) + { + $config = array_merge( + array( + 'event_after_delete' => 'onExtensionAfterDelete', + 'event_after_save' => 'onExtensionAfterSave', + 'event_before_delete' => 'onExtensionBeforeDelete', + 'event_before_save' => 'onExtensionBeforeSave', + 'events_map' => array( + 'save' => 'extension', + 'delete' => 'extension' + ) + ), + $config + ); + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the User state. + $pk = $app->input->getInt('id'); + + if (!$pk) { + if ($extensionId = (int) $app->getUserState('com_modules.add.module.extension_id')) { + $this->setState('extension.id', $extensionId); + } + } + + $this->setState('module.id', $pk); + + // Load the parameters. + $params = ComponentHelper::getParams('com_modules'); + $this->setState('params', $params); + } + + /** + * Batch copy modules to a new position or current. + * + * @param integer $value The new value matching a module position. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 2.5 + */ + protected function batchCopy($value, $pks, $contexts) + { + // Set the variables + $user = Factory::getUser(); + $table = $this->getTable(); + $newIds = array(); + + foreach ($pks as $pk) { + if ($user->authorise('core.create', 'com_modules')) { + $table->reset(); + $table->load($pk); + + // Set the new position + if ($value == 'noposition') { + $position = ''; + } elseif ($value == 'nochange') { + $position = $table->position; + } else { + $position = $value; + } + + $table->position = $position; + + // Copy of the Asset ID + $oldAssetId = $table->asset_id; + + // Alter the title if necessary + $data = $this->generateNewTitle(0, $table->title, $table->position); + $table->title = $data['0']; + + // Reset the ID because we are making a copy + $table->id = 0; + + // Unpublish the new module + $table->published = 0; + + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Get the new item ID + $newId = $table->get('id'); + + // Add the new ID to the array + $newIds[$pk] = $newId; + + // Now we need to handle the module assignments + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('menuid')) + ->from($db->quoteName('#__modules_menu')) + ->where($db->quoteName('moduleid') . ' = :moduleid') + ->bind(':moduleid', $pk, ParameterType::INTEGER); + $db->setQuery($query); + $menus = $db->loadColumn(); + + // Insert the new records into the table + foreach ($menus as $i => $menu) { + $query->clear() + ->insert($db->quoteName('#__modules_menu')) + ->columns($db->quoteName(['moduleid', 'menuid'])) + ->values(implode(', ', [':newid' . $i, ':menu' . $i])) + ->bind(':newid' . $i, $newId, ParameterType::INTEGER) + ->bind(':menu' . $i, $menu, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + } + + // Copy rules + $query->clear() + ->update($db->quoteName('#__assets', 't')) + ->join('INNER', $db->quoteName('#__assets', 's') . + ' ON ' . $db->quoteName('s.id') . ' = ' . $oldAssetId) + ->set($db->quoteName('t.rules') . ' = ' . $db->quoteName('s.rules')) + ->where($db->quoteName('t.id') . ' = ' . $table->asset_id); + + $db->setQuery($query)->execute(); + } else { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_CREATE')); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return $newIds; + } + + /** + * Batch move modules to a new position or current. + * + * @param integer $value The new value matching a module position. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 2.5 + */ + protected function batchMove($value, $pks, $contexts) + { + // Set the variables + $user = Factory::getUser(); + $table = $this->getTable(); + + foreach ($pks as $pk) { + if ($user->authorise('core.edit', 'com_modules')) { + $table->reset(); + $table->load($pk); + + // Set the new position + if ($value == 'noposition') { + $position = ''; + } elseif ($value == 'nochange') { + $position = $table->position; + } else { + $position = $value; + } + + $table->position = $position; + + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + } else { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_BATCH_CANNOT_EDIT')); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to test whether a record can have its state edited. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 3.2 + */ + protected function canEditState($record) + { + // Check for existing module. + if (!empty($record->id)) { + return Factory::getUser()->authorise('core.edit.state', 'com_modules.module.' . (int) $record->id); + } + + // Default to component settings if module not known. + return parent::canEditState($record); + } + + /** + * Method to delete rows. + * + * @param array &$pks An array of item ids. + * + * @return boolean Returns true on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function delete(&$pks) + { + $app = Factory::getApplication(); + $pks = (array) $pks; + $user = Factory::getUser(); + $table = $this->getTable(); + $context = $this->option . '.' . $this->name; + + // Include the plugins for the on delete events. + PluginHelper::importPlugin($this->events_map['delete']); + + // Iterate the items to delete each one. + foreach ($pks as $pk) { + if ($table->load($pk)) { + // Access checks. + if (!$user->authorise('core.delete', 'com_modules.module.' . (int) $pk) || $table->published != -2) { + Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); + + return; + } + + // Trigger the before delete event. + $result = $app->triggerEvent($this->event_before_delete, array($context, $table)); + + if (in_array(false, $result, true) || !$table->delete($pk)) { + throw new \Exception($table->getError()); + } else { + // Delete the menu assignments + $pk = (int) $pk; + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__modules_menu')) + ->where($db->quoteName('moduleid') . ' = :moduleid') + ->bind(':moduleid', $pk, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + // Trigger the after delete event. + $app->triggerEvent($this->event_after_delete, array($context, $table)); + } + + // Clear module cache + parent::cleanCache($table->module); + } else { + throw new \Exception($table->getError()); + } + } + + // Clear modules cache + $this->cleanCache(); + + return true; + } + + /** + * Method to duplicate modules. + * + * @param array &$pks An array of primary key IDs. + * + * @return boolean Boolean true on success + * + * @since 1.6 + * @throws \Exception + */ + public function duplicate(&$pks) + { + $user = Factory::getUser(); + $db = $this->getDatabase(); + + // Access checks. + if (!$user->authorise('core.create', 'com_modules')) { + throw new \Exception(Text::_('JERROR_CORE_CREATE_NOT_PERMITTED')); + } + + $table = $this->getTable(); + + foreach ($pks as $pk) { + if ($table->load($pk, true)) { + // Reset the id to create a new record. + $table->id = 0; + + // Alter the title. + $m = null; + + if (preg_match('#\((\d+)\)$#', $table->title, $m)) { + $table->title = preg_replace('#\(\d+\)$#', '(' . ($m[1] + 1) . ')', $table->title); + } + + $data = $this->generateNewTitle(0, $table->title, $table->position); + $table->title = $data[0]; + + // Unpublish duplicate module + $table->published = 0; + + if (!$table->check() || !$table->store()) { + throw new \Exception($table->getError()); + } + + $pk = (int) $pk; + $query = $db->getQuery(true) + ->select($db->quoteName('menuid')) + ->from($db->quoteName('#__modules_menu')) + ->where($db->quoteName('moduleid') . ' = :moduleid') + ->bind(':moduleid', $pk, ParameterType::INTEGER); + + $db->setQuery($query); + $rows = $db->loadColumn(); + + foreach ($rows as $menuid) { + $tuples[] = (int) $table->id . ',' . (int) $menuid; + } + } else { + throw new \Exception($table->getError()); + } + } + + if (!empty($tuples)) { + // Module-Menu Mapping: Do it in one query + $query = $db->getQuery(true) + ->insert($db->quoteName('#__modules_menu')) + ->columns($db->quoteName(array('moduleid', 'menuid'))) + ->values($tuples); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + } + + // Clear modules cache + $this->cleanCache(); + + return true; + } + + /** + * Method to change the title. + * + * @param integer $categoryId The id of the category. Not used here. + * @param string $title The title. + * @param string $position The position. + * + * @return array Contains the modified title. + * + * @since 2.5 + */ + protected function generateNewTitle($categoryId, $title, $position) + { + // Alter the title & alias + $table = $this->getTable(); + + while ($table->load(array('position' => $position, 'title' => $title))) { + $title = StringHelper::increment($title); + } + + return array($title); + } + + /** + * Method to get the client object + * + * @return void + * + * @since 1.6 + */ + public function &getClient() + { + return $this->_client; + } + + /** + * Method to get the record form. + * + * @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 Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // The folder and element vars are passed when saving the form. + if (empty($data)) { + $item = $this->getItem(); + $clientId = $item->client_id; + $module = $item->module; + $id = $item->id; + } else { + $clientId = ArrayHelper::getValue($data, 'client_id'); + $module = ArrayHelper::getValue($data, 'module'); + $id = ArrayHelper::getValue($data, 'id'); + } + + // Add the default fields directory + $baseFolder = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; + Form::addFieldPath($baseFolder . '/modules/' . $module . '/field'); + + // These variables are used to add data from the plugin XML files. + $this->setState('item.client_id', $clientId); + $this->setState('item.module', $module); + + // Get the form. + if ($clientId == 1) { + $form = $this->loadForm('com_modules.module.admin', 'moduleadmin', array('control' => 'jform', 'load_data' => $loadData), true); + + // Display language field to filter admin custom menus per language + if (!ModuleHelper::isAdminMultilang()) { + $form->setFieldAttribute('language', 'type', 'hidden'); + } + } else { + $form = $this->loadForm('com_modules.module', 'module', array('control' => 'jform', 'load_data' => $loadData), true); + } + + if (empty($form)) { + return false; + } + + $user = Factory::getUser(); + + /** + * Check for existing module + * Modify the form based on Edit State access controls. + */ + if ( + $id != 0 && (!$user->authorise('core.edit.state', 'com_modules.module.' . (int) $id)) + || ($id == 0 && !$user->authorise('core.edit.state', 'com_modules')) + ) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('published', 'disabled', 'true'); + $form->setFieldAttribute('publish_up', 'disabled', 'true'); + $form->setFieldAttribute('publish_down', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('published', 'filter', 'unset'); + $form->setFieldAttribute('publish_up', 'filter', 'unset'); + $form->setFieldAttribute('publish_down', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + $app = Factory::getApplication(); + + // Check the session for previously entered form data. + $data = $app->getUserState('com_modules.edit.module.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Pre-select some filters (Status, Module Position, Language, Access Level) in edit form if those have been selected in Module Manager + if (!$data->id) { + $clientId = $app->input->getInt('client_id', 0); + $filters = (array) $app->getUserState('com_modules.modules.' . $clientId . '.filter'); + $data->set('published', $app->input->getInt('published', ((isset($filters['state']) && $filters['state'] !== '') ? $filters['state'] : null))); + $data->set('position', $app->input->getInt('position', (!empty($filters['position']) ? $filters['position'] : null))); + $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); + $data->set('access', $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access')))); + } + + // Avoid to delete params of a second module opened in a new browser tab while new one is not saved yet. + if (empty($data->params)) { + // This allows us to inject parameter settings into a new module. + $params = $app->getUserState('com_modules.add.module.params'); + + if (is_array($params)) { + $data->set('params', $params); + } + } + } + + $this->preprocessData('com_modules.module', $data); + + return $data; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + $pk = (!empty($pk)) ? (int) $pk : (int) $this->getState('module.id'); + $db = $this->getDatabase(); + + if (!isset($this->_cache[$pk])) { + // Get a row instance. + $table = $this->getTable(); + + // Attempt to load the row. + $return = $table->load($pk); + + // Check for a table object error. + if ($return === false && $error = $table->getError()) { + $this->setError($error); + + return false; + } + + // Check if we are creating a new extension. + if (empty($pk)) { + if ($extensionId = (int) $this->getState('extension.id')) { + $query = $db->getQuery(true) + ->select($db->quoteName(['element', 'client_id'])) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('extension_id') . ' = :extensionid') + ->where($db->quoteName('type') . ' = ' . $db->quote('module')) + ->bind(':extensionid', $extensionId, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $extension = $db->loadObject(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (empty($extension)) { + $this->setError('COM_MODULES_ERROR_CANNOT_FIND_MODULE'); + + return false; + } + + // Extension found, prime some module values. + $table->module = $extension->element; + $table->client_id = $extension->client_id; + } else { + Factory::getApplication()->redirect(Route::_('index.php?option=com_modules&view=modules', false)); + + return false; + } + } + + // Convert to the \Joomla\CMS\Object\CMSObject before adding other data. + $properties = $table->getProperties(1); + $this->_cache[$pk] = ArrayHelper::toObject($properties, CMSObject::class); + + // Convert the params field to an array. + $registry = new Registry($table->params); + $this->_cache[$pk]->params = $registry->toArray(); + + // Determine the page assignment mode. + $query = $db->getQuery(true) + ->select($db->quoteName('menuid')) + ->from($db->quoteName('#__modules_menu')) + ->where($db->quoteName('moduleid') . ' = :moduleid') + ->bind(':moduleid', $pk, ParameterType::INTEGER); + $db->setQuery($query); + $assigned = $db->loadColumn(); + + if (empty($pk)) { + // If this is a new module, assign to all pages. + $assignment = 0; + } elseif (empty($assigned)) { + // For an existing module it is assigned to none. + $assignment = '-'; + } else { + if ($assigned[0] > 0) { + $assignment = 1; + } elseif ($assigned[0] < 0) { + $assignment = -1; + } else { + $assignment = 0; + } + } + + $this->_cache[$pk]->assigned = $assigned; + $this->_cache[$pk]->assignment = $assignment; + + // Get the module XML. + $client = ApplicationHelper::getClientInfo($table->client_id); + $path = Path::clean($client->path . '/modules/' . $table->module . '/' . $table->module . '.xml'); + + if (file_exists($path)) { + $this->_cache[$pk]->xml = simplexml_load_file($path); + } else { + $this->_cache[$pk]->xml = null; + } + } + + return $this->_cache[$pk]; + } + + /** + * Get the necessary data to load an item help screen. + * + * @return object An object with key, url, and local properties for loading the item help screen. + * + * @since 1.6 + */ + public function getHelp() + { + return (object) array('key' => $this->helpKey, 'url' => $this->helpURL); + } + + /** + * Returns a reference to the a Table object, always creating it. + * + * @param string $type The table type to instantiate + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A database object + * + * @since 1.6 + */ + public function getTable($type = 'Module', $prefix = 'JTable', $config = array()) + { + return Table::getInstance($type, $prefix, $config); + } + + /** + * Prepare and sanitise the table prior to saving. + * + * @param Table $table The database object + * + * @return void + * + * @since 1.6 + */ + protected function prepareTable($table) + { + $table->title = htmlspecialchars_decode($table->title, ENT_QUOTES); + $table->position = trim($table->position); + } + + /** + * Method to preprocess the form + * + * @param Form $form A form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error loading the form. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $lang = Factory::getLanguage(); + $clientId = $this->getState('item.client_id'); + $module = $this->getState('item.module'); + + $client = ApplicationHelper::getClientInfo($clientId); + $formFile = Path::clean($client->path . '/modules/' . $module . '/' . $module . '.xml'); + + // Load the core and/or local language file(s). + $lang->load($module, $client->path) + || $lang->load($module, $client->path . '/modules/' . $module); + + if (file_exists($formFile)) { + // Get the module form. + if (!$form->loadFile($formFile, false, '//config')) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Attempt to load the xml file. + if (!$xml = simplexml_load_file($formFile)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Get the help data from the XML file if present. + $help = $xml->xpath('/extension/help'); + + if (!empty($help)) { + $helpKey = trim((string) $help[0]['key']); + $helpURL = trim((string) $help[0]['url']); + + $this->helpKey = $helpKey ?: $this->helpKey; + $this->helpURL = $helpURL ?: $this->helpURL; + } + } + + // Load the default advanced params + Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_modules/models/forms'); + $form->loadFile('advanced', false); + + // Load chrome specific params for global files + $chromePath = JPATH_SITE . '/layouts/chromes'; + $chromeFormFiles = Folder::files($chromePath, '.*\.xml'); + + if ($chromeFormFiles) { + Form::addFormPath($chromePath); + + foreach ($chromeFormFiles as $formFile) { + $form->loadFile(basename($formFile, '.xml'), false); + } + } + + // Load chrome specific params for template files + $templates = ModulesHelper::getTemplates($clientId); + + foreach ($templates as $template) { + $chromePath = $client->path . '/templates/' . $template->element . '/html/layouts/chromes'; + + // Skip if there is no chrome folder in that template. + if (!is_dir($chromePath)) { + continue; + } + + $chromeFormFiles = Folder::files($chromePath, '.*\.xml'); + + if ($chromeFormFiles) { + Form::addFormPath($chromePath); + + foreach ($chromeFormFiles as $formFile) { + $form->loadFile(basename($formFile, '.xml'), false); + } + } + } + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } + + /** + * Loads ContentHelper for filters before validating data. + * + * @param object $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the group(defaults to null). + * + * @return mixed Array of filtered data if valid, false otherwise. + * + * @since 1.1 + */ + public function validate($form, $data, $group = null) + { + if (!Factory::getUser()->authorise('core.admin', 'com_modules')) { + if (isset($data['rules'])) { + unset($data['rules']); + } + } + + return parent::validate($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + $input = Factory::getApplication()->input; + $table = $this->getTable(); + $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('module.id'); + $isNew = true; + $context = $this->option . '.' . $this->name; + + // Include the plugins for the save event. + PluginHelper::importPlugin($this->events_map['save']); + + // Load the row if saving an existing record. + if ($pk > 0) { + $table->load($pk); + $isNew = false; + } + + // Alter the title and published state for Save as Copy + if ($input->get('task') == 'save2copy') { + $orig_table = clone $this->getTable(); + $orig_table->load((int) $input->getInt('id')); + $data['published'] = 0; + + if ($data['title'] == $orig_table->title) { + $data['title'] = StringHelper::increment($data['title']); + } + } + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Prepare the row for saving + $this->prepareTable($table); + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, $isNew)); + + if (in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + // Store the data. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Process the menu link mappings. + $assignment = $data['assignment'] ?? 0; + + $table->id = (int) $table->id; + + // Delete old module to menu item associations + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__modules_menu')) + ->where($db->quoteName('moduleid') . ' = :moduleid') + ->bind(':moduleid', $table->id, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // If the assignment is numeric, then something is selected (otherwise it's none). + if (is_numeric($assignment)) { + // Variable is numeric, but could be a string. + $assignment = (int) $assignment; + + // Logic check: if no module excluded then convert to display on all. + if ($assignment == -1 && empty($data['assigned'])) { + $assignment = 0; + } + + // Check needed to stop a module being assigned to `All` + // and other menu items resulting in a module being displayed twice. + if ($assignment === 0) { + // Assign new module to `all` menu item associations. + $query->clear() + ->insert($db->quoteName('#__modules_menu')) + ->columns($db->quoteName(['moduleid', 'menuid'])) + ->values(implode(', ', [':moduleid', 0])) + ->bind(':moduleid', $table->id, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } elseif (!empty($data['assigned'])) { + // Get the sign of the number. + $sign = $assignment < 0 ? -1 : 1; + + $query->clear() + ->insert($db->quoteName('#__modules_menu')) + ->columns($db->quoteName(array('moduleid', 'menuid'))); + + foreach ($data['assigned'] as &$pk) { + $query->values((int) $table->id . ',' . (int) $pk * $sign); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + } + + // Trigger the after save event. + Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, $isNew)); + + // Compute the extension id of this module in case the controller wants it. + $query->clear() + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions', 'e')) + ->join( + 'LEFT', + $db->quoteName('#__modules', 'm') . ' ON ' . $db->quoteName('e.client_id') . ' = ' . (int) $table->client_id . + ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quoteName('m.module') + ) + ->where($db->quoteName('m.id') . ' = :id') + ->bind(':id', $table->id, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $extensionId = $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + $this->setState('module.extension_id', $extensionId); + $this->setState('module.id', $table->id); + + // Clear modules cache + $this->cleanCache(); + + // Clean module cache + parent::cleanCache($table->module); + + return true; + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param object $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('client_id') . ' = ' . (int) $table->client_id, + $db->quoteName('position') . ' = ' . $db->quote($table->position), + ]; + } + + /** + * Custom clean cache method for different clients + * + * @param string $group The name of the plugin group to import (defaults to null). + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('com_modules'); + } } diff --git a/administrator/components/com_modules/src/Model/ModulesModel.php b/administrator/components/com_modules/src/Model/ModulesModel.php index 5abc720bda5c5..48579f3f02359 100644 --- a/administrator/components/com_modules/src/Model/ModulesModel.php +++ b/administrator/components/com_modules/src/Model/ModulesModel.php @@ -1,4 +1,5 @@ input->get('layout', '', 'cmd'); - - // Adjust the context to support modal layouts. - if ($layout) - { - $this->context .= '.' . $layout; - } - - // Make context client aware - $this->context .= '.' . $app->input->get->getInt('client_id', 0); - - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.position', $this->getUserStateFromRequest($this->context . '.filter.position', 'filter_position', '', 'string')); - $this->setState('filter.module', $this->getUserStateFromRequest($this->context . '.filter.module', 'filter_module', '', 'string')); - $this->setState('filter.menuitem', $this->getUserStateFromRequest($this->context . '.filter.menuitem', 'filter_menuitem', '', 'cmd')); - $this->setState('filter.access', $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access', '', 'cmd')); - - // If in modal layout on the frontend, state and language are always forced. - if ($app->isClient('site') && $layout === 'modal') - { - $this->setState('filter.language', 'current'); - $this->setState('filter.state', 1); - } - // If in backend (modal or not) we get the same fields from the user request. - else - { - $this->setState('filter.language', $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', '', 'string')); - $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'string')); - } - - // Special case for the client id. - if ($app->isClient('site') || $layout === 'modal') - { - $this->setState('client_id', 0); - $clientId = 0; - } - else - { - $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); - $clientId = (!in_array($clientId, array(0, 1))) ? 0 : $clientId; - $this->setState('client_id', $clientId); - } - - // Use a different filter file when client is administrator - if ($clientId == 1) - { - $this->filterFormName = 'filter_modulesadmin'; - } - - // Load the parameters. - $params = ComponentHelper::getParams('com_modules'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('client_id'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.position'); - $id .= ':' . $this->getState('filter.module'); - $id .= ':' . $this->getState('filter.menuitem'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.language'); - - return parent::getStoreId($id); - } - - /** - * Returns an object list - * - * @param DatabaseQuery $query The query - * @param int $limitstart Offset - * @param int $limit The number of records - * - * @return array - */ - protected function _getList($query, $limitstart = 0, $limit = 0) - { - $listOrder = $this->getState('list.ordering', 'a.position'); - $listDirn = $this->getState('list.direction', 'asc'); - - $db = $this->getDatabase(); - - // If ordering by fields that need translate we need to sort the array of objects after translating them. - if (in_array($listOrder, array('pages', 'name'))) - { - // Fetch the results. - $db->setQuery($query); - $result = $db->loadObjectList(); - - // Translate the results. - $this->translate($result); - - // Sort the array of translated objects. - $result = ArrayHelper::sortObjects($result, $listOrder, strtolower($listDirn) == 'desc' ? -1 : 1, true, true); - - // Process pagination. - $total = count($result); - $this->cache[$this->getStoreId('getTotal')] = $total; - - if ($total < $limitstart) - { - $limitstart = 0; - $this->setState('list.start', 0); - } - - return array_slice($result, $limitstart, $limit ?: null); - } - - // If ordering by fields that doesn't need translate just order the query. - if ($listOrder === 'a.ordering') - { - $query->order($db->quoteName('a.position') . ' ASC') - ->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); - } - elseif ($listOrder === 'a.position') - { - $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)) - ->order($db->quoteName('a.ordering') . ' ASC'); - } - else - { - $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); - } - - // Process pagination. - $result = parent::_getList($query, $limitstart, $limit); - - // Translate the results. - $this->translate($result); - - return $result; - } - - /** - * Translate a list of objects - * - * @param array &$items The array of objects - * - * @return array The array of translated objects - */ - protected function translate(&$items) - { - $lang = Factory::getLanguage(); - $clientPath = $this->getState('client_id') ? JPATH_ADMINISTRATOR : JPATH_SITE; - - foreach ($items as $item) - { - $extension = $item->module; - $source = $clientPath . "/modules/$extension"; - $lang->load("$extension.sys", $clientPath) - || $lang->load("$extension.sys", $source); - $item->name = Text::_($item->name); - - if (is_null($item->pages)) - { - $item->pages = Text::_('JNONE'); - } - elseif ($item->pages < 0) - { - $item->pages = Text::_('COM_MODULES_ASSIGNED_VARIES_EXCEPT'); - } - elseif ($item->pages > 0) - { - $item->pages = Text::_('COM_MODULES_ASSIGNED_VARIES_ONLY'); - } - else - { - $item->pages = Text::_('JALL'); - } - } - } - - /** - * Build an SQL query to load the list data. - * - * @return DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields. - $query->select( - $this->getState( - 'list.select', - 'a.id, a.title, a.note, a.position, a.module, a.language,' . - 'a.checked_out, a.checked_out_time, a.published AS published, e.enabled AS enabled, a.access, a.ordering, a.publish_up, a.publish_down' - ) - ); - - // From modules table. - $query->from($db->quoteName('#__modules', 'a')); - - // Join over the language - $query->select($db->quoteName('l.title', 'language_title')) - ->select($db->quoteName('l.image', 'language_image')) - ->join('LEFT', $db->quoteName('#__languages', 'l') . ' ON ' . $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); - - // Join over the users for the checked out user. - $query->select($db->quoteName('uc.name', 'editor')) - ->join('LEFT', $db->quoteName('#__users', 'uc') . ' ON ' . $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); - - // Join over the asset groups. - $query->select($db->quoteName('ag.title', 'access_level')) - ->join('LEFT', $db->quoteName('#__viewlevels', 'ag') . ' ON ' . $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')); - - // Join over the module menus - $query->select('MIN(mm.menuid) AS pages') - ->join('LEFT', $db->quoteName('#__modules_menu', 'mm') . ' ON ' . $db->quoteName('mm.moduleid') . ' = ' . $db->quoteName('a.id')); - - // Join over the extensions - $query->select($db->quoteName('e.name', 'name')) - ->join('LEFT', $db->quoteName('#__extensions', 'e') . ' ON ' . $db->quoteName('e.element') . ' = ' . $db->quoteName('a.module')); - - // Group (careful with PostgreSQL) - $query->group( - 'a.id, a.title, a.note, a.position, a.module, a.language, a.checked_out, ' - . 'a.checked_out_time, a.published, a.access, a.ordering, l.title, l.image, uc.name, ag.title, e.name, ' - . 'l.lang_code, uc.id, ag.id, mm.moduleid, e.element, a.publish_up, a.publish_down, e.enabled' - ); - - // Filter by client. - $clientId = (int) $this->getState('client_id'); - $query->where($db->quoteName('a.client_id') . ' = :aclientid') - ->where($db->quoteName('e.client_id') . ' = :eclientid') - ->bind(':aclientid', $clientId, ParameterType::INTEGER) - ->bind(':eclientid', $clientId, ParameterType::INTEGER); - - // Filter by current user access level. - $user = Factory::getUser(); - - // Get the current user for authorisation checks - if ($user->authorise('core.admin') !== true) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('a.access'), $groups); - } - - // Filter by access level. - if ($access = $this->getState('filter.access')) - { - $access = (int) $access; - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - - // Filter by published state. - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $state = (int) $state; - $query->where($db->quoteName('a.published') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - elseif ($state === '') - { - $query->whereIn($db->quoteName('a.published'), [0, 1]); - } - - // Filter by position. - if ($position = $this->getState('filter.position')) - { - $position = ($position === 'none') ? '' : $position; - $query->where($db->quoteName('a.position') . ' = :position') - ->bind(':position', $position); - } - - // Filter by module. - if ($module = $this->getState('filter.module')) - { - $query->where($db->quoteName('a.module') . ' = :module') - ->bind(':module', $module); - } - - // Filter by menuitem id (only for site client). - if ((int) $clientId === 0 && $menuItemId = $this->getState('filter.menuitem')) - { - // If user selected the modules not assigned to any page (menu item). - if ((int) $menuItemId === -1) - { - $query->having('MIN(' . $db->quoteName('mm.menuid') . ') IS NULL'); - } - // If user selected the modules assigned to some particular page (menu item). - else - { - // Modules in "All" pages. - $subQuery1 = $db->getQuery(true); - $subQuery1->select('MIN(' . $db->quoteName('menuid') . ')') - ->from($db->quoteName('#__modules_menu')) - ->where($db->quoteName('moduleid') . ' = ' . $db->quoteName('a.id')); - - // Modules in "Selected" pages that have the chosen menu item id. - $menuItemId = (int) $menuItemId; - $minusMenuItemId = $menuItemId * -1; - $subQuery2 = $db->getQuery(true); - $subQuery2->select($db->quoteName('moduleid')) - ->from($db->quoteName('#__modules_menu')) - ->where($db->quoteName('menuid') . ' = :menuitemid2'); - - // Modules in "All except selected" pages that doesn't have the chosen menu item id. - $subQuery3 = $db->getQuery(true); - $subQuery3->select($db->quoteName('moduleid')) - ->from($db->quoteName('#__modules_menu')) - ->where($db->quoteName('menuid') . ' = :menuitemid3'); - - // Filter by modules assigned to the selected menu item. - $query->where('( + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @see \JController + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'published', 'a.published', 'state', + 'access', 'a.access', + 'ag.title', 'access_level', + 'ordering', 'a.ordering', + 'module', 'a.module', + 'language', 'a.language', + 'l.title', 'language_title', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'client_id', 'a.client_id', + 'position', 'a.position', + 'pages', + 'name', 'e.name', + 'menuitem', + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.position', $direction = 'asc') + { + $app = Factory::getApplication(); + + $layout = $app->input->get('layout', '', 'cmd'); + + // Adjust the context to support modal layouts. + if ($layout) { + $this->context .= '.' . $layout; + } + + // Make context client aware + $this->context .= '.' . $app->input->get->getInt('client_id', 0); + + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.position', $this->getUserStateFromRequest($this->context . '.filter.position', 'filter_position', '', 'string')); + $this->setState('filter.module', $this->getUserStateFromRequest($this->context . '.filter.module', 'filter_module', '', 'string')); + $this->setState('filter.menuitem', $this->getUserStateFromRequest($this->context . '.filter.menuitem', 'filter_menuitem', '', 'cmd')); + $this->setState('filter.access', $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access', '', 'cmd')); + + // If in modal layout on the frontend, state and language are always forced. + if ($app->isClient('site') && $layout === 'modal') { + $this->setState('filter.language', 'current'); + $this->setState('filter.state', 1); + } else { + // If in backend (modal or not) we get the same fields from the user request. + $this->setState('filter.language', $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', '', 'string')); + $this->setState('filter.state', $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'string')); + } + + // Special case for the client id. + if ($app->isClient('site') || $layout === 'modal') { + $this->setState('client_id', 0); + $clientId = 0; + } else { + $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); + $clientId = (!in_array($clientId, array(0, 1))) ? 0 : $clientId; + $this->setState('client_id', $clientId); + } + + // Use a different filter file when client is administrator + if ($clientId == 1) { + $this->filterFormName = 'filter_modulesadmin'; + } + + // Load the parameters. + $params = ComponentHelper::getParams('com_modules'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('client_id'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.position'); + $id .= ':' . $this->getState('filter.module'); + $id .= ':' . $this->getState('filter.menuitem'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.language'); + + return parent::getStoreId($id); + } + + /** + * Returns an object list + * + * @param DatabaseQuery $query The query + * @param int $limitstart Offset + * @param int $limit The number of records + * + * @return array + */ + protected function _getList($query, $limitstart = 0, $limit = 0) + { + $listOrder = $this->getState('list.ordering', 'a.position'); + $listDirn = $this->getState('list.direction', 'asc'); + + $db = $this->getDatabase(); + + // If ordering by fields that need translate we need to sort the array of objects after translating them. + if (in_array($listOrder, array('pages', 'name'))) { + // Fetch the results. + $db->setQuery($query); + $result = $db->loadObjectList(); + + // Translate the results. + $this->translate($result); + + // Sort the array of translated objects. + $result = ArrayHelper::sortObjects($result, $listOrder, strtolower($listDirn) == 'desc' ? -1 : 1, true, true); + + // Process pagination. + $total = count($result); + $this->cache[$this->getStoreId('getTotal')] = $total; + + if ($total < $limitstart) { + $limitstart = 0; + $this->setState('list.start', 0); + } + + return array_slice($result, $limitstart, $limit ?: null); + } + + // If ordering by fields that doesn't need translate just order the query. + if ($listOrder === 'a.ordering') { + $query->order($db->quoteName('a.position') . ' ASC') + ->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); + } elseif ($listOrder === 'a.position') { + $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)) + ->order($db->quoteName('a.ordering') . ' ASC'); + } else { + $query->order($db->quoteName($listOrder) . ' ' . $db->escape($listDirn)); + } + + // Process pagination. + $result = parent::_getList($query, $limitstart, $limit); + + // Translate the results. + $this->translate($result); + + return $result; + } + + /** + * Translate a list of objects + * + * @param array &$items The array of objects + * + * @return array The array of translated objects + */ + protected function translate(&$items) + { + $lang = Factory::getLanguage(); + $clientPath = $this->getState('client_id') ? JPATH_ADMINISTRATOR : JPATH_SITE; + + foreach ($items as $item) { + $extension = $item->module; + $source = $clientPath . "/modules/$extension"; + $lang->load("$extension.sys", $clientPath) + || $lang->load("$extension.sys", $source); + $item->name = Text::_($item->name); + + if (is_null($item->pages)) { + $item->pages = Text::_('JNONE'); + } elseif ($item->pages < 0) { + $item->pages = Text::_('COM_MODULES_ASSIGNED_VARIES_EXCEPT'); + } elseif ($item->pages > 0) { + $item->pages = Text::_('COM_MODULES_ASSIGNED_VARIES_ONLY'); + } else { + $item->pages = Text::_('JALL'); + } + } + } + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields. + $query->select( + $this->getState( + 'list.select', + 'a.id, a.title, a.note, a.position, a.module, a.language,' . + 'a.checked_out, a.checked_out_time, a.published AS published, e.enabled AS enabled, a.access, a.ordering, a.publish_up, a.publish_down' + ) + ); + + // From modules table. + $query->from($db->quoteName('#__modules', 'a')); + + // Join over the language + $query->select($db->quoteName('l.title', 'language_title')) + ->select($db->quoteName('l.image', 'language_image')) + ->join('LEFT', $db->quoteName('#__languages', 'l') . ' ON ' . $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); + + // Join over the users for the checked out user. + $query->select($db->quoteName('uc.name', 'editor')) + ->join('LEFT', $db->quoteName('#__users', 'uc') . ' ON ' . $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); + + // Join over the asset groups. + $query->select($db->quoteName('ag.title', 'access_level')) + ->join('LEFT', $db->quoteName('#__viewlevels', 'ag') . ' ON ' . $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')); + + // Join over the module menus + $query->select('MIN(mm.menuid) AS pages') + ->join('LEFT', $db->quoteName('#__modules_menu', 'mm') . ' ON ' . $db->quoteName('mm.moduleid') . ' = ' . $db->quoteName('a.id')); + + // Join over the extensions + $query->select($db->quoteName('e.name', 'name')) + ->join('LEFT', $db->quoteName('#__extensions', 'e') . ' ON ' . $db->quoteName('e.element') . ' = ' . $db->quoteName('a.module')); + + // Group (careful with PostgreSQL) + $query->group( + 'a.id, a.title, a.note, a.position, a.module, a.language, a.checked_out, ' + . 'a.checked_out_time, a.published, a.access, a.ordering, l.title, l.image, uc.name, ag.title, e.name, ' + . 'l.lang_code, uc.id, ag.id, mm.moduleid, e.element, a.publish_up, a.publish_down, e.enabled' + ); + + // Filter by client. + $clientId = (int) $this->getState('client_id'); + $query->where($db->quoteName('a.client_id') . ' = :aclientid') + ->where($db->quoteName('e.client_id') . ' = :eclientid') + ->bind(':aclientid', $clientId, ParameterType::INTEGER) + ->bind(':eclientid', $clientId, ParameterType::INTEGER); + + // Filter by current user access level. + $user = Factory::getUser(); + + // Get the current user for authorisation checks + if ($user->authorise('core.admin') !== true) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('a.access'), $groups); + } + + // Filter by access level. + if ($access = $this->getState('filter.access')) { + $access = (int) $access; + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + + // Filter by published state. + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $state = (int) $state; + $query->where($db->quoteName('a.published') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } elseif ($state === '') { + $query->whereIn($db->quoteName('a.published'), [0, 1]); + } + + // Filter by position. + if ($position = $this->getState('filter.position')) { + $position = ($position === 'none') ? '' : $position; + $query->where($db->quoteName('a.position') . ' = :position') + ->bind(':position', $position); + } + + // Filter by module. + if ($module = $this->getState('filter.module')) { + $query->where($db->quoteName('a.module') . ' = :module') + ->bind(':module', $module); + } + + // Filter by menuitem id (only for site client). + if ((int) $clientId === 0 && $menuItemId = $this->getState('filter.menuitem')) { + // If user selected the modules not assigned to any page (menu item). + if ((int) $menuItemId === -1) { + $query->having('MIN(' . $db->quoteName('mm.menuid') . ') IS NULL'); + } else { + // If user selected the modules assigned to some particular page (menu item). + // Modules in "All" pages. + $subQuery1 = $db->getQuery(true); + $subQuery1->select('MIN(' . $db->quoteName('menuid') . ')') + ->from($db->quoteName('#__modules_menu')) + ->where($db->quoteName('moduleid') . ' = ' . $db->quoteName('a.id')); + + // Modules in "Selected" pages that have the chosen menu item id. + $menuItemId = (int) $menuItemId; + $minusMenuItemId = $menuItemId * -1; + $subQuery2 = $db->getQuery(true); + $subQuery2->select($db->quoteName('moduleid')) + ->from($db->quoteName('#__modules_menu')) + ->where($db->quoteName('menuid') . ' = :menuitemid2'); + + // Modules in "All except selected" pages that doesn't have the chosen menu item id. + $subQuery3 = $db->getQuery(true); + $subQuery3->select($db->quoteName('moduleid')) + ->from($db->quoteName('#__modules_menu')) + ->where($db->quoteName('menuid') . ' = :menuitemid3'); + + // Filter by modules assigned to the selected menu item. + $query->where('( (' . $subQuery1 . ') = 0 OR ((' . $subQuery1 . ') > 0 AND ' . $db->quoteName('a.id') . ' IN (' . $subQuery2 . ')) OR ((' . $subQuery1 . ') < 0 AND ' . $db->quoteName('a.id') . ' NOT IN (' . $subQuery3 . ')) - )' - ); - $query->bind(':menuitemid2', $menuItemId, ParameterType::INTEGER); - $query->bind(':menuitemid3', $minusMenuItemId, ParameterType::INTEGER); - } - } - - // Filter by search in title or note or id:. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . StringHelper::strtolower($search) . '%'; - $query->extendWhere( - 'AND', - [ - 'LOWER(' . $db->quoteName('a.title') . ') LIKE :title', - 'LOWER(' . $db->quoteName('a.note') . ') LIKE :note', - ], - 'OR' - ) - ->bind(':title', $search) - ->bind(':note', $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - if ($language === 'current') - { - $language = [Factory::getLanguage()->getTag(), '*']; - $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); - } - else - { - $query->where($db->quoteName('a.language') . ' = :language') - ->bind(':language', $language); - } - } - - return $query; - } - - /** - * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. - * - * @return DatabaseQuery - * - * @since 4.0.0 - */ - protected function getEmptyStateQuery() - { - $query = parent::getEmptyStateQuery(); - - $clientId = (int) $this->getState('client_id'); - - $query->where($this->getDatabase()->quoteName('a.client_id') . ' = :client_id') - ->bind(':client_id', $clientId, ParameterType::INTEGER); - - return $query; - } + )'); + $query->bind(':menuitemid2', $menuItemId, ParameterType::INTEGER); + $query->bind(':menuitemid3', $minusMenuItemId, ParameterType::INTEGER); + } + } + + // Filter by search in title or note or id:. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . StringHelper::strtolower($search) . '%'; + $query->extendWhere( + 'AND', + [ + 'LOWER(' . $db->quoteName('a.title') . ') LIKE :title', + 'LOWER(' . $db->quoteName('a.note') . ') LIKE :note', + ], + 'OR' + ) + ->bind(':title', $search) + ->bind(':note', $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + if ($language === 'current') { + $language = [Factory::getLanguage()->getTag(), '*']; + $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); + } else { + $query->where($db->quoteName('a.language') . ' = :language') + ->bind(':language', $language); + } + } + + return $query; + } + + /** + * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. + * + * @return DatabaseQuery + * + * @since 4.0.0 + */ + protected function getEmptyStateQuery() + { + $query = parent::getEmptyStateQuery(); + + $clientId = (int) $this->getState('client_id'); + + $query->where($this->getDatabase()->quoteName('a.client_id') . ' = :client_id') + ->bind(':client_id', $clientId, ParameterType::INTEGER); + + return $query; + } } diff --git a/administrator/components/com_modules/src/Model/PositionsModel.php b/administrator/components/com_modules/src/Model/PositionsModel.php index 0213400e5f5d2..4d4c1b2a7ed88 100644 --- a/administrator/components/com_modules/src/Model/PositionsModel.php +++ b/administrator/components/com_modules/src/Model/PositionsModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest($this->context . '.filter.search', 'filter_search'); - $this->setState('filter.search', $search); - - $state = $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'string'); - $this->setState('filter.state', $state); - - $template = $this->getUserStateFromRequest($this->context . '.filter.template', 'filter_template', '', 'string'); - $this->setState('filter.template', $template); - - $type = $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string'); - $this->setState('filter.type', $type); - - // Special case for the client id. - $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); - $clientId = (!in_array((int) $clientId, array (0, 1))) ? 0 : (int) $clientId; - $this->setState('client_id', $clientId); - - // Load the parameters. - $params = ComponentHelper::getParams('com_modules'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 1.6 - */ - public function getItems() - { - if (!isset($this->items)) - { - $lang = Factory::getLanguage(); - $search = $this->getState('filter.search'); - $state = $this->getState('filter.state'); - $clientId = $this->getState('client_id'); - $filter_template = $this->getState('filter.template'); - $type = $this->getState('filter.type'); - $ordering = $this->getState('list.ordering'); - $direction = $this->getState('list.direction'); - $limitstart = $this->getState('list.start'); - $limit = $this->getState('list.limit'); - $client = ApplicationHelper::getClientInfo($clientId); - - if ($type != 'template') - { - $clientId = (int) $clientId; - - // Get the database object and a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('position', 'value')) - ->from($db->quoteName('#__modules')) - ->where($db->quoteName('client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); - - if ($search) - { - $search = '%' . str_replace(' ', '%', trim($search), true) . '%'; - $query->where($db->quoteName('position') . ' LIKE :position') - ->bind(':position', $search); - } - - $db->setQuery($query); - - try - { - $positions = $db->loadObjectList('value'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - foreach ($positions as $value => $position) - { - $positions[$value] = array(); - } - } - else - { - $positions = array(); - } - - // Load the positions from the installed templates. - foreach (ModulesHelper::getTemplates($clientId) as $template) - { - $path = Path::clean($client->path . '/templates/' . $template->element . '/templateDetails.xml'); - - if (file_exists($path)) - { - $xml = simplexml_load_file($path); - - if (isset($xml->positions[0])) - { - $lang->load('tpl_' . $template->element . '.sys', $client->path) - || $lang->load('tpl_' . $template->element . '.sys', $client->path . '/templates/' . $template->element); - - foreach ($xml->positions[0] as $position) - { - $value = (string) $position['value']; - $label = (string) $position; - - if (!$value) - { - $value = $label; - $label = preg_replace('/[^a-zA-Z0-9_\-]/', '_', 'TPL_' . $template->element . '_POSITION_' . $value); - $altlabel = preg_replace('/[^a-zA-Z0-9_\-]/', '_', 'COM_MODULES_POSITION_' . $value); - - if (!$lang->hasKey($label) && $lang->hasKey($altlabel)) - { - $label = $altlabel; - } - } - - if ($type == 'user' || ($state != '' && $state != $template->enabled)) - { - unset($positions[$value]); - } - elseif (preg_match(chr(1) . $search . chr(1) . 'i', $value) && ($filter_template == '' || $filter_template == $template->element)) - { - if (!isset($positions[$value])) - { - $positions[$value] = array(); - } - - $positions[$value][$template->name] = $label; - } - } - } - } - } - - $this->total = count($positions); - - if ($limitstart >= $this->total) - { - $limitstart = $limitstart < $limit ? 0 : $limitstart - $limit; - $this->setState('list.start', $limitstart); - } - - if ($ordering == 'value') - { - if ($direction == 'asc') - { - ksort($positions); - } - else - { - krsort($positions); - } - } - else - { - if ($direction == 'asc') - { - asort($positions); - } - else - { - arsort($positions); - } - } - - $this->items = array_slice($positions, $limitstart, $limit ?: null); - } - - return $this->items; - } - - /** - * Method to get the total number of items. - * - * @return integer The total number of items. - * - * @since 1.6 - */ - public function getTotal() - { - if (!isset($this->total)) - { - $this->getItems(); - } - - return $this->total; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @see \JController + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'value', + 'templates', + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'ordering', $direction = 'asc') + { + // Load the filter state. + $search = $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search'); + $this->setState('filter.search', $search); + + $state = $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state', '', 'string'); + $this->setState('filter.state', $state); + + $template = $this->getUserStateFromRequest($this->context . '.filter.template', 'filter_template', '', 'string'); + $this->setState('filter.template', $template); + + $type = $this->getUserStateFromRequest($this->context . '.filter.type', 'filter_type', '', 'string'); + $this->setState('filter.type', $type); + + // Special case for the client id. + $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); + $clientId = (!in_array((int) $clientId, array (0, 1))) ? 0 : (int) $clientId; + $this->setState('client_id', $clientId); + + // Load the parameters. + $params = ComponentHelper::getParams('com_modules'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 1.6 + */ + public function getItems() + { + if (!isset($this->items)) { + $lang = Factory::getLanguage(); + $search = $this->getState('filter.search'); + $state = $this->getState('filter.state'); + $clientId = $this->getState('client_id'); + $filter_template = $this->getState('filter.template'); + $type = $this->getState('filter.type'); + $ordering = $this->getState('list.ordering'); + $direction = $this->getState('list.direction'); + $limitstart = $this->getState('list.start'); + $limit = $this->getState('list.limit'); + $client = ApplicationHelper::getClientInfo($clientId); + + if ($type != 'template') { + $clientId = (int) $clientId; + + // Get the database object and a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('position', 'value')) + ->from($db->quoteName('#__modules')) + ->where($db->quoteName('client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); + + if ($search) { + $search = '%' . str_replace(' ', '%', trim($search), true) . '%'; + $query->where($db->quoteName('position') . ' LIKE :position') + ->bind(':position', $search); + } + + $db->setQuery($query); + + try { + $positions = $db->loadObjectList('value'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + foreach ($positions as $value => $position) { + $positions[$value] = array(); + } + } else { + $positions = array(); + } + + // Load the positions from the installed templates. + foreach (ModulesHelper::getTemplates($clientId) as $template) { + $path = Path::clean($client->path . '/templates/' . $template->element . '/templateDetails.xml'); + + if (file_exists($path)) { + $xml = simplexml_load_file($path); + + if (isset($xml->positions[0])) { + $lang->load('tpl_' . $template->element . '.sys', $client->path) + || $lang->load('tpl_' . $template->element . '.sys', $client->path . '/templates/' . $template->element); + + foreach ($xml->positions[0] as $position) { + $value = (string) $position['value']; + $label = (string) $position; + + if (!$value) { + $value = $label; + $label = preg_replace('/[^a-zA-Z0-9_\-]/', '_', 'TPL_' . $template->element . '_POSITION_' . $value); + $altlabel = preg_replace('/[^a-zA-Z0-9_\-]/', '_', 'COM_MODULES_POSITION_' . $value); + + if (!$lang->hasKey($label) && $lang->hasKey($altlabel)) { + $label = $altlabel; + } + } + + if ($type == 'user' || ($state != '' && $state != $template->enabled)) { + unset($positions[$value]); + } elseif (preg_match(chr(1) . $search . chr(1) . 'i', $value) && ($filter_template == '' || $filter_template == $template->element)) { + if (!isset($positions[$value])) { + $positions[$value] = array(); + } + + $positions[$value][$template->name] = $label; + } + } + } + } + } + + $this->total = count($positions); + + if ($limitstart >= $this->total) { + $limitstart = $limitstart < $limit ? 0 : $limitstart - $limit; + $this->setState('list.start', $limitstart); + } + + if ($ordering == 'value') { + if ($direction == 'asc') { + ksort($positions); + } else { + krsort($positions); + } + } else { + if ($direction == 'asc') { + asort($positions); + } else { + arsort($positions); + } + } + + $this->items = array_slice($positions, $limitstart, $limit ?: null); + } + + return $this->items; + } + + /** + * Method to get the total number of items. + * + * @return integer The total number of items. + * + * @since 1.6 + */ + public function getTotal() + { + if (!isset($this->total)) { + $this->getItems(); + } + + return $this->total; + } } diff --git a/administrator/components/com_modules/src/Model/SelectModel.php b/administrator/components/com_modules/src/Model/SelectModel.php index eb901af523edc..36fbc6b3df615 100644 --- a/administrator/components/com_modules/src/Model/SelectModel.php +++ b/administrator/components/com_modules/src/Model/SelectModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest('com_modules.modules.client_id', 'client_id', 0); - $this->setState('client_id', (int) $clientId); - - // Load the parameters. - $params = ComponentHelper::getParams('com_modules'); - $this->setState('params', $params); - - // Manually set limits to get all modules. - $this->setState('list.limit', 0); - $this->setState('list.start', 0); - $this->setState('list.ordering', 'a.name'); - $this->setState('list.direction', 'ASC'); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('client_id'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.extension_id, a.name, a.element AS module' - ) - ); - $query->from($db->quoteName('#__extensions', 'a')); - - // Filter by module - $query->where($db->quoteName('a.type') . ' = ' . $db->quote('module')); - - // Filter by client. - $clientId = (int) $this->getState('client_id'); - $query->where($db->quoteName('a.client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); - - // Filter by enabled - $query->where($db->quoteName('a.enabled') . ' = 1'); - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Method to get a list of items. - * - * @return mixed An array of objects on success, false on failure. - */ - public function getItems() - { - // Get the list of items from the database. - $items = parent::getItems(); - - $client = ApplicationHelper::getClientInfo($this->getState('client_id', 0)); - $lang = Factory::getLanguage(); - - // Loop through the results to add the XML metadata, - // and load language support. - foreach ($items as &$item) - { - $path = Path::clean($client->path . '/modules/' . $item->module . '/' . $item->module . '.xml'); - - if (file_exists($path)) - { - $item->xml = simplexml_load_file($path); - } - else - { - $item->xml = null; - } - - // 1.5 Format; Core files or language packs then - // 1.6 3PD Extension Support - $lang->load($item->module . '.sys', $client->path) - || $lang->load($item->module . '.sys', $client->path . '/modules/' . $item->module); - $item->name = Text::_($item->name); - - if (isset($item->xml) && $text = trim($item->xml->description)) - { - $item->desc = Text::_($text); - } - else - { - $item->desc = Text::_('COM_MODULES_NODESCRIPTION'); - } - } - - $items = ArrayHelper::sortObjects($items, 'name', 1, true, true); - - // @todo: Use the cached XML from the extensions table? - - return $items; - } + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + + // Load the filter state. + $clientId = $app->getUserStateFromRequest('com_modules.modules.client_id', 'client_id', 0); + $this->setState('client_id', (int) $clientId); + + // Load the parameters. + $params = ComponentHelper::getParams('com_modules'); + $this->setState('params', $params); + + // Manually set limits to get all modules. + $this->setState('list.limit', 0); + $this->setState('list.start', 0); + $this->setState('list.ordering', 'a.name'); + $this->setState('list.direction', 'ASC'); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('client_id'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.extension_id, a.name, a.element AS module' + ) + ); + $query->from($db->quoteName('#__extensions', 'a')); + + // Filter by module + $query->where($db->quoteName('a.type') . ' = ' . $db->quote('module')); + + // Filter by client. + $clientId = (int) $this->getState('client_id'); + $query->where($db->quoteName('a.client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); + + // Filter by enabled + $query->where($db->quoteName('a.enabled') . ' = 1'); + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Method to get a list of items. + * + * @return mixed An array of objects on success, false on failure. + */ + public function getItems() + { + // Get the list of items from the database. + $items = parent::getItems(); + + $client = ApplicationHelper::getClientInfo($this->getState('client_id', 0)); + $lang = Factory::getLanguage(); + + // Loop through the results to add the XML metadata, + // and load language support. + foreach ($items as &$item) { + $path = Path::clean($client->path . '/modules/' . $item->module . '/' . $item->module . '.xml'); + + if (file_exists($path)) { + $item->xml = simplexml_load_file($path); + } else { + $item->xml = null; + } + + // 1.5 Format; Core files or language packs then + // 1.6 3PD Extension Support + $lang->load($item->module . '.sys', $client->path) + || $lang->load($item->module . '.sys', $client->path . '/modules/' . $item->module); + $item->name = Text::_($item->name); + + if (isset($item->xml) && $text = trim($item->xml->description)) { + $item->desc = Text::_($text); + } else { + $item->desc = Text::_('COM_MODULES_NODESCRIPTION'); + } + } + + $items = ArrayHelper::sortObjects($items, 'name', 1, true, true); + + // @todo: Use the cached XML from the extensions table? + + return $items; + } } diff --git a/administrator/components/com_modules/src/Service/HTML/Modules.php b/administrator/components/com_modules/src/Service/HTML/Modules.php index 2e850cda51920..f934fd3d9d2a8 100644 --- a/administrator/components/com_modules/src/Service/HTML/Modules.php +++ b/administrator/components/com_modules/src/Service/HTML/Modules.php @@ -1,4 +1,5 @@ element, $template->name); - } - - return $options; - } - - /** - * Builds an array of template type options - * - * @return array - */ - public function types() - { - $options = array(); - $options[] = HTMLHelper::_('select.option', 'user', 'COM_MODULES_OPTION_POSITION_USER_DEFINED'); - $options[] = HTMLHelper::_('select.option', 'template', 'COM_MODULES_OPTION_POSITION_TEMPLATE_DEFINED'); - - return $options; - } - - /** - * Builds an array of template state options - * - * @return array - */ - public function templateStates() - { - $options = array(); - $options[] = HTMLHelper::_('select.option', '1', 'JENABLED'); - $options[] = HTMLHelper::_('select.option', '0', 'JDISABLED'); - - return $options; - } - - /** - * Returns a published state on a grid - * - * @param integer $value The state value. - * @param integer $i The row index - * @param boolean $enabled An optional setting for access control on the action. - * @param string $checkbox An optional prefix for checkboxes. - * - * @return string The Html code - * - * @see HTMLHelperJGrid::state - * @since 1.7.1 - */ - public function state($value, $i, $enabled = true, $checkbox = 'cb') - { - $states = array( - 1 => array( - 'unpublish', - 'COM_MODULES_EXTENSION_PUBLISHED_ENABLED', - 'COM_MODULES_HTML_UNPUBLISH_ENABLED', - 'COM_MODULES_EXTENSION_PUBLISHED_ENABLED', - true, - 'publish', - 'publish', - ), - 0 => array( - 'publish', - 'COM_MODULES_EXTENSION_UNPUBLISHED_ENABLED', - 'COM_MODULES_HTML_PUBLISH_ENABLED', - 'COM_MODULES_EXTENSION_UNPUBLISHED_ENABLED', - true, - 'unpublish', - 'unpublish', - ), - -1 => array( - 'unpublish', - 'COM_MODULES_EXTENSION_PUBLISHED_DISABLED', - 'COM_MODULES_HTML_UNPUBLISH_DISABLED', - 'COM_MODULES_EXTENSION_PUBLISHED_DISABLED', - true, - 'warning', - 'warning', - ), - -2 => array( - 'publish', - 'COM_MODULES_EXTENSION_UNPUBLISHED_DISABLED', - 'COM_MODULES_HTML_PUBLISH_DISABLED', - 'COM_MODULES_EXTENSION_UNPUBLISHED_DISABLED', - true, - 'unpublish', - 'unpublish', - ), - ); - - return HTMLHelper::_('jgrid.state', $states, $value, $i, 'modules.', $enabled, true, $checkbox); - } - - /** - * Display a batch widget for the module position selector. - * - * @param integer $clientId The client ID. - * @param integer $state The state of the module (enabled, unenabled, trashed). - * @param string $selectedPosition The currently selected position for the module. - * - * @return string The necessary positions for the widget. - * - * @since 2.5 - */ - public function positions($clientId, $state = 1, $selectedPosition = '') - { - $templates = array_keys(ModulesHelper::getTemplates($clientId, $state)); - $templateGroups = array(); - - // Add an empty value to be able to deselect a module position - $option = ModulesHelper::createOption('', Text::_('COM_MODULES_NONE')); - $templateGroups[''] = ModulesHelper::createOptionGroup('', array($option)); - - // Add positions from templates - $isTemplatePosition = false; - - foreach ($templates as $template) - { - $options = array(); - - $positions = TemplatesHelper::getPositions($clientId, $template); - - if (is_array($positions)) - { - foreach ($positions as $position) - { - $text = ModulesHelper::getTranslatedModulePosition($clientId, $template, $position) . ' [' . $position . ']'; - $options[] = ModulesHelper::createOption($position, $text); - - if (!$isTemplatePosition && $selectedPosition === $position) - { - $isTemplatePosition = true; - } - } - - $options = ArrayHelper::sortObjects($options, 'text'); - } - - $templateGroups[$template] = ModulesHelper::createOptionGroup(ucfirst($template), $options); - } - - // Add custom position to options - $customGroupText = Text::_('COM_MODULES_CUSTOM_POSITION'); - $editPositions = true; - $customPositions = ModulesHelper::getPositions($clientId, $editPositions); - - $app = Factory::getApplication(); - - $position = $app->getUserState('com_modules.modules.' . $clientId . '.filter.position'); - - if ($position) - { - $customPositions[] = HTMLHelper::_('select.option', $position); - - $customPositions = array_unique($customPositions, SORT_REGULAR); - } - - $templateGroups[$customGroupText] = ModulesHelper::createOptionGroup($customGroupText, $customPositions); - - return $templateGroups; - } - - /** - * Get a select with the batch action options - * - * @return void - */ - public function batchOptions() - { - // Create the copy/move options. - $options = array( - HTMLHelper::_('select.option', 'c', Text::_('JLIB_HTML_BATCH_COPY')), - HTMLHelper::_('select.option', 'm', Text::_('JLIB_HTML_BATCH_MOVE')) - ); - - echo HTMLHelper::_('select.radiolist', $options, 'batch[move_copy]', '', 'value', 'text', 'm'); - } - - /** - * Method to get the field options. - * - * @param integer $clientId The client ID - * - * @return array The field option objects. - * - * @since 2.5 - * - * @deprecated 5.0 Will be removed with no replacement - */ - public function positionList($clientId = 0) - { - $clientId = (int) $clientId; - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('position', 'value')) - ->select($db->quoteName('position', 'text')) - ->from($db->quoteName('#__modules')) - ->where($db->quoteName('client_id') . ' = :clientid') - ->order($db->quoteName('position')) - ->bind(':clientid', $clientId, ParameterType::INTEGER); - - // Get the options. - $db->setQuery($query); - - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - // Pop the first item off the array if it's blank - if (count($options)) - { - if (strlen($options[0]->text) < 1) - { - array_shift($options); - } - } - - return $options; - } + /** + * Builds an array of template options + * + * @param integer $clientId The client id. + * @param string $state The state of the template. + * + * @return array + */ + public function templates($clientId = 0, $state = '') + { + $options = array(); + $templates = ModulesHelper::getTemplates($clientId, $state); + + foreach ($templates as $template) { + $options[] = HTMLHelper::_('select.option', $template->element, $template->name); + } + + return $options; + } + + /** + * Builds an array of template type options + * + * @return array + */ + public function types() + { + $options = array(); + $options[] = HTMLHelper::_('select.option', 'user', 'COM_MODULES_OPTION_POSITION_USER_DEFINED'); + $options[] = HTMLHelper::_('select.option', 'template', 'COM_MODULES_OPTION_POSITION_TEMPLATE_DEFINED'); + + return $options; + } + + /** + * Builds an array of template state options + * + * @return array + */ + public function templateStates() + { + $options = array(); + $options[] = HTMLHelper::_('select.option', '1', 'JENABLED'); + $options[] = HTMLHelper::_('select.option', '0', 'JDISABLED'); + + return $options; + } + + /** + * Returns a published state on a grid + * + * @param integer $value The state value. + * @param integer $i The row index + * @param boolean $enabled An optional setting for access control on the action. + * @param string $checkbox An optional prefix for checkboxes. + * + * @return string The Html code + * + * @see HTMLHelperJGrid::state + * @since 1.7.1 + */ + public function state($value, $i, $enabled = true, $checkbox = 'cb') + { + $states = array( + 1 => array( + 'unpublish', + 'COM_MODULES_EXTENSION_PUBLISHED_ENABLED', + 'COM_MODULES_HTML_UNPUBLISH_ENABLED', + 'COM_MODULES_EXTENSION_PUBLISHED_ENABLED', + true, + 'publish', + 'publish', + ), + 0 => array( + 'publish', + 'COM_MODULES_EXTENSION_UNPUBLISHED_ENABLED', + 'COM_MODULES_HTML_PUBLISH_ENABLED', + 'COM_MODULES_EXTENSION_UNPUBLISHED_ENABLED', + true, + 'unpublish', + 'unpublish', + ), + -1 => array( + 'unpublish', + 'COM_MODULES_EXTENSION_PUBLISHED_DISABLED', + 'COM_MODULES_HTML_UNPUBLISH_DISABLED', + 'COM_MODULES_EXTENSION_PUBLISHED_DISABLED', + true, + 'warning', + 'warning', + ), + -2 => array( + 'publish', + 'COM_MODULES_EXTENSION_UNPUBLISHED_DISABLED', + 'COM_MODULES_HTML_PUBLISH_DISABLED', + 'COM_MODULES_EXTENSION_UNPUBLISHED_DISABLED', + true, + 'unpublish', + 'unpublish', + ), + ); + + return HTMLHelper::_('jgrid.state', $states, $value, $i, 'modules.', $enabled, true, $checkbox); + } + + /** + * Display a batch widget for the module position selector. + * + * @param integer $clientId The client ID. + * @param integer $state The state of the module (enabled, unenabled, trashed). + * @param string $selectedPosition The currently selected position for the module. + * + * @return string The necessary positions for the widget. + * + * @since 2.5 + */ + public function positions($clientId, $state = 1, $selectedPosition = '') + { + $templates = array_keys(ModulesHelper::getTemplates($clientId, $state)); + $templateGroups = array(); + + // Add an empty value to be able to deselect a module position + $option = ModulesHelper::createOption('', Text::_('COM_MODULES_NONE')); + $templateGroups[''] = ModulesHelper::createOptionGroup('', array($option)); + + // Add positions from templates + $isTemplatePosition = false; + + foreach ($templates as $template) { + $options = array(); + + $positions = TemplatesHelper::getPositions($clientId, $template); + + if (is_array($positions)) { + foreach ($positions as $position) { + $text = ModulesHelper::getTranslatedModulePosition($clientId, $template, $position) . ' [' . $position . ']'; + $options[] = ModulesHelper::createOption($position, $text); + + if (!$isTemplatePosition && $selectedPosition === $position) { + $isTemplatePosition = true; + } + } + + $options = ArrayHelper::sortObjects($options, 'text'); + } + + $templateGroups[$template] = ModulesHelper::createOptionGroup(ucfirst($template), $options); + } + + // Add custom position to options + $customGroupText = Text::_('COM_MODULES_CUSTOM_POSITION'); + $editPositions = true; + $customPositions = ModulesHelper::getPositions($clientId, $editPositions); + + $app = Factory::getApplication(); + + $position = $app->getUserState('com_modules.modules.' . $clientId . '.filter.position'); + + if ($position) { + $customPositions[] = HTMLHelper::_('select.option', $position); + + $customPositions = array_unique($customPositions, SORT_REGULAR); + } + + $templateGroups[$customGroupText] = ModulesHelper::createOptionGroup($customGroupText, $customPositions); + + return $templateGroups; + } + + /** + * Get a select with the batch action options + * + * @return void + */ + public function batchOptions() + { + // Create the copy/move options. + $options = array( + HTMLHelper::_('select.option', 'c', Text::_('JLIB_HTML_BATCH_COPY')), + HTMLHelper::_('select.option', 'm', Text::_('JLIB_HTML_BATCH_MOVE')) + ); + + echo HTMLHelper::_('select.radiolist', $options, 'batch[move_copy]', '', 'value', 'text', 'm'); + } + + /** + * Method to get the field options. + * + * @param integer $clientId The client ID + * + * @return array The field option objects. + * + * @since 2.5 + * + * @deprecated 5.0 Will be removed with no replacement + */ + public function positionList($clientId = 0) + { + $clientId = (int) $clientId; + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('position', 'value')) + ->select($db->quoteName('position', 'text')) + ->from($db->quoteName('#__modules')) + ->where($db->quoteName('client_id') . ' = :clientid') + ->order($db->quoteName('position')) + ->bind(':clientid', $clientId, ParameterType::INTEGER); + + // Get the options. + $db->setQuery($query); + + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + // Pop the first item off the array if it's blank + if (count($options)) { + if (strlen($options[0]->text) < 1) { + array_shift($options); + } + } + + return $options; + } } diff --git a/administrator/components/com_modules/src/View/Module/HtmlView.php b/administrator/components/com_modules/src/View/Module/HtmlView.php index 5a21ecb1ee68e..a2dfd77ea4e96 100644 --- a/administrator/components/com_modules/src/View/Module/HtmlView.php +++ b/administrator/components/com_modules/src/View/Module/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - $this->canDo = ContentHelper::getActions('com_modules', 'module', $this->item->id); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = $this->getCurrentUser(); - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); - $canDo = $this->canDo; - - ToolbarHelper::title(Text::sprintf('COM_MODULES_MANAGER_MODULE', Text::_($this->item->module)), 'cube module'); - - // For new records, check the create permission. - if ($isNew && $canDo->get('core.create')) - { - ToolbarHelper::apply('module.apply'); - - ToolbarHelper::saveGroup( - [ - ['save', 'module.save'], - ['save2new', 'module.save2new'] - ], - 'btn-success' - ); - - ToolbarHelper::cancel('module.cancel'); - } - else - { - $toolbarButtons = []; - - // Can't save the record if it's checked out. - if (!$checkedOut) - { - // Since it's an existing record, check the edit permission. - if ($canDo->get('core.edit')) - { - ToolbarHelper::apply('module.apply'); - - $toolbarButtons[] = ['save', 'module.save']; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'module.save2new']; - } - } - } - - // If checked out, we can still save - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'module.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel('module.cancel', 'JTOOLBAR_CLOSE'); - } - - // Get the help information for the menu item. - $lang = Factory::getLanguage(); - - $help = $this->get('Help'); - - if ($lang->hasKey($help->url)) - { - $debug = $lang->setDebug(false); - $url = Text::_($help->url); - $lang->setDebug($debug); - } - else - { - $url = null; - } - - ToolbarHelper::inlinehelp(); - ToolbarHelper::help($help->key, false, $url); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * The actions the user is authorised to perform + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 4.0.0 + */ + protected $canDo; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + $this->canDo = ContentHelper::getActions('com_modules', 'module', $this->item->id); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); + $canDo = $this->canDo; + + ToolbarHelper::title(Text::sprintf('COM_MODULES_MANAGER_MODULE', Text::_($this->item->module)), 'cube module'); + + // For new records, check the create permission. + if ($isNew && $canDo->get('core.create')) { + ToolbarHelper::apply('module.apply'); + + ToolbarHelper::saveGroup( + [ + ['save', 'module.save'], + ['save2new', 'module.save2new'] + ], + 'btn-success' + ); + + ToolbarHelper::cancel('module.cancel'); + } else { + $toolbarButtons = []; + + // Can't save the record if it's checked out. + if (!$checkedOut) { + // Since it's an existing record, check the edit permission. + if ($canDo->get('core.edit')) { + ToolbarHelper::apply('module.apply'); + + $toolbarButtons[] = ['save', 'module.save']; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'module.save2new']; + } + } + } + + // If checked out, we can still save + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'module.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel('module.cancel', 'JTOOLBAR_CLOSE'); + } + + // Get the help information for the menu item. + $lang = Factory::getLanguage(); + + $help = $this->get('Help'); + + if ($lang->hasKey($help->url)) { + $debug = $lang->setDebug(false); + $url = Text::_($help->url); + $lang->setDebug($debug); + } else { + $url = null; + } + + ToolbarHelper::inlinehelp(); + ToolbarHelper::help($help->key, false, $url); + } } diff --git a/administrator/components/com_modules/src/View/Modules/HtmlView.php b/administrator/components/com_modules/src/View/Modules/HtmlView.php index 853ae9b074b13..8f505a6f58ede 100644 --- a/administrator/components/com_modules/src/View/Modules/HtmlView.php +++ b/administrator/components/com_modules/src/View/Modules/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->total = $this->get('Total'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->clientId = $this->state->get('client_id'); - - if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - /** - * The code below make sure the remembered position will be available from filter dropdown even if there are no - * modules available for this position. This will make the UI less confusing for users in case there is only one - * module in the selected position and user: - * 1. Edit the module, change it to new position, save it and come back to Modules Management Screen - * 2. Or move that module to new position using Batch action - */ - if (count($this->items) === 0 && $this->state->get('filter.position')) - { - $selectedPosition = $this->state->get('filter.position'); - $positionField = $this->filterForm->getField('position', 'filter'); - - $positionExists = false; - - foreach ($positionField->getOptions() as $option) - { - if ($option->value === $selectedPosition) - { - $positionExists = true; - break; - } - } - - if ($positionExists === false) - { - $positionField->addOption($selectedPosition, ['value' => $selectedPosition]); - } - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // We do not need the Language filter when modules are not filtered - if ($this->clientId == 1 && !ModuleHelper::isAdminMultilang()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - - // We don't need the toolbar in the modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - // If in modal layout. - else - { - // Client id selector should not exist. - $this->filterForm->removeField('client_id', ''); - - // If in the frontend state and language should not activate the search tools. - if (Factory::getApplication()->isClient('site')) - { - unset($this->activeFilters['state']); - unset($this->activeFilters['language']); - } - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $state = $this->get('State'); - $canDo = ContentHelper::getActions('com_modules'); - $user = $this->getCurrentUser(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - if ($state->get('client_id') == 1) - { - ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_ADMIN'), 'cube module'); - } - else - { - ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_SITE'), 'cube module'); - } - - if ($canDo->get('core.create')) - { - $toolbar->standardButton('new', 'JTOOLBAR_NEW') - ->onclick("location.href='index.php?option=com_modules&view=select&client_id=" . $this->state->get('client_id', 0) . "'"); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $this->getCurrentUser()->authorise('core.admin'))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('modules.publish')->listCheck(true); - - $childBar->unpublish('modules.unpublish')->listCheck(true); - } - - if ($this->getCurrentUser()->authorise('core.admin')) - { - $childBar->checkin('modules.checkin')->listCheck(true); - } - - if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) - { - $childBar->trash('modules.trash')->listCheck(true); - } - - // Add a batch button - if ($user->authorise('core.create', 'com_modules') && $user->authorise('core.edit', 'com_modules') - && $user->authorise('core.edit.state', 'com_modules')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - - if ($canDo->get('core.create')) - { - $childBar->standardButton('copy') - ->text('JTOOLBAR_DUPLICATE') - ->task('modules.duplicate') - ->listCheck(true); - } - } - - if (!$this->isEmptyState && ($state->get('filter.state') == -2 && $canDo->get('core.delete'))) - { - $toolbar->delete('modules.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin')) - { - $toolbar->preferences('com_modules'); - } - - $toolbar->help('Modules'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->total = $this->get('Total'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->clientId = $this->state->get('client_id'); + + if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + /** + * The code below make sure the remembered position will be available from filter dropdown even if there are no + * modules available for this position. This will make the UI less confusing for users in case there is only one + * module in the selected position and user: + * 1. Edit the module, change it to new position, save it and come back to Modules Management Screen + * 2. Or move that module to new position using Batch action + */ + if (count($this->items) === 0 && $this->state->get('filter.position')) { + $selectedPosition = $this->state->get('filter.position'); + $positionField = $this->filterForm->getField('position', 'filter'); + + $positionExists = false; + + foreach ($positionField->getOptions() as $option) { + if ($option->value === $selectedPosition) { + $positionExists = true; + break; + } + } + + if ($positionExists === false) { + $positionField->addOption($selectedPosition, ['value' => $selectedPosition]); + } + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // We do not need the Language filter when modules are not filtered + if ($this->clientId == 1 && !ModuleHelper::isAdminMultilang()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + + // We don't need the toolbar in the modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } else { + // If in modal layout. + // Client id selector should not exist. + $this->filterForm->removeField('client_id', ''); + + // If in the frontend state and language should not activate the search tools. + if (Factory::getApplication()->isClient('site')) { + unset($this->activeFilters['state']); + unset($this->activeFilters['language']); + } + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $state = $this->get('State'); + $canDo = ContentHelper::getActions('com_modules'); + $user = $this->getCurrentUser(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + if ($state->get('client_id') == 1) { + ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_ADMIN'), 'cube module'); + } else { + ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_SITE'), 'cube module'); + } + + if ($canDo->get('core.create')) { + $toolbar->standardButton('new', 'JTOOLBAR_NEW') + ->onclick("location.href='index.php?option=com_modules&view=select&client_id=" . $this->state->get('client_id', 0) . "'"); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $this->getCurrentUser()->authorise('core.admin'))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state')) { + $childBar->publish('modules.publish')->listCheck(true); + + $childBar->unpublish('modules.unpublish')->listCheck(true); + } + + if ($this->getCurrentUser()->authorise('core.admin')) { + $childBar->checkin('modules.checkin')->listCheck(true); + } + + if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) { + $childBar->trash('modules.trash')->listCheck(true); + } + + // Add a batch button + if ( + $user->authorise('core.create', 'com_modules') && $user->authorise('core.edit', 'com_modules') + && $user->authorise('core.edit.state', 'com_modules') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + + if ($canDo->get('core.create')) { + $childBar->standardButton('copy') + ->text('JTOOLBAR_DUPLICATE') + ->task('modules.duplicate') + ->listCheck(true); + } + } + + if (!$this->isEmptyState && ($state->get('filter.state') == -2 && $canDo->get('core.delete'))) { + $toolbar->delete('modules.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin')) { + $toolbar->preferences('com_modules'); + } + + $toolbar->help('Modules'); + } } diff --git a/administrator/components/com_modules/src/View/Select/HtmlView.php b/administrator/components/com_modules/src/View/Select/HtmlView.php index 3084005611456..9684b10b5222e 100644 --- a/administrator/components/com_modules/src/View/Select/HtmlView.php +++ b/administrator/components/com_modules/src/View/Select/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->items = $this->get('Items'); - $this->modalLink = ''; + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->modalLink = ''; - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } - $this->addToolbar(); - parent::display($tpl); - } + $this->addToolbar(); + parent::display($tpl); + } - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $state = $this->get('State'); - $clientId = (int) $state->get('client_id', 0); + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $state = $this->get('State'); + $clientId = (int) $state->get('client_id', 0); - // Add page title - ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_SITE'), 'cube module'); + // Add page title + ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_SITE'), 'cube module'); - if ($clientId === 1) - { - ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_ADMIN'), 'cube module'); - } + if ($clientId === 1) { + ToolbarHelper::title(Text::_('COM_MODULES_MANAGER_MODULES_ADMIN'), 'cube module'); + } - // Get the toolbar object instance - $bar = Toolbar::getInstance('toolbar'); + // Get the toolbar object instance + $bar = Toolbar::getInstance('toolbar'); - // Instantiate a new FileLayout instance and render the layout - $layout = new FileLayout('toolbar.cancelselect'); + // Instantiate a new FileLayout instance and render the layout + $layout = new FileLayout('toolbar.cancelselect'); - $bar->appendButton('Custom', $layout->render(array('client_id' => $clientId)), 'new'); - } + $bar->appendButton('Custom', $layout->render(array('client_id' => $clientId)), 'new'); + } } diff --git a/administrator/components/com_modules/tmpl/module/edit.php b/administrator/components/com_modules/tmpl/module/edit.php index 982bbf002d4fc..0dbdb4680d460 100644 --- a/administrator/components/com_modules/tmpl/module/edit.php +++ b/administrator/components/com_modules/tmpl/module/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_modules.admin-module-edit'); + ->useScript('form.validate') + ->useScript('com_modules.admin-module-edit'); $input = Factory::getApplication()->input; @@ -54,151 +54,144 @@
- - -
- 'general', 'recall' => true, 'breakpoint' => 768]); ?> - - - -
-
- item->xml) : ?> - item->xml->description) : ?> -

- item->xml) - { - echo ($text = (string) $this->item->xml->name) ? Text::_($text) : $this->item->module; - } - else - { - echo Text::_('COM_MODULES_ERR_XML'); - } - ?> -

-
- - item->client_id == 0 ? Text::_('JSITE') : Text::_('JADMINISTRATOR'); ?> - -
-
- fieldset = 'description'; - $short_description = Text::_($this->item->xml->description); - $long_description = LayoutHelper::render('joomla.edit.fieldset', $this); - - if (!$long_description) - { - $truncated = HTMLHelper::_('string.truncate', $short_description, 550, true, false); - - if (strlen($truncated) > 500) - { - $long_description = $short_description; - $short_description = HTMLHelper::_('string.truncate', $truncated, 250); - - if ($short_description == $long_description) - { - $long_description = ''; - } - } - } - ?> -

- -

- - - -

- -
- - -
- - -
- - form->getInput($hasContentFieldName); - } - $this->fieldset = 'basic'; - $html = LayoutHelper::render('joomla.edit.fieldset', $this); - echo $html ? '
' . $html : ''; - ?> -
-
- fields = array( - 'showtitle', - 'position', - 'published', - 'publish_up', - 'publish_down', - 'access', - 'ordering', - 'language', - 'note' - ); - - ?> - item->client_id == 0) : ?> - - - - -
-
- - - - -
-
- -
-
- - - - item->client_id == 0) : ?> - -
- -
- loadTemplate('assignment'); ?> -
-
- - - - fieldsets = array(); - $this->ignore_fieldsets = array('basic', 'description'); - echo LayoutHelper::render('joomla.edit.params', $this); - ?> - - canDo->get('core.admin')) : ?> - -
- -
- form->getInput('rules'); ?> -
-
- - - - - - - - - form->getInput('module'); ?> - form->getInput('client_id'); ?> -
+ + +
+ 'general', 'recall' => true, 'breakpoint' => 768]); ?> + + + +
+
+ item->xml) : ?> + item->xml->description) : ?> +

+ item->xml) { + echo ($text = (string) $this->item->xml->name) ? Text::_($text) : $this->item->module; + } else { + echo Text::_('COM_MODULES_ERR_XML'); + } + ?> +

+
+ + item->client_id == 0 ? Text::_('JSITE') : Text::_('JADMINISTRATOR'); ?> + +
+
+ fieldset = 'description'; + $short_description = Text::_($this->item->xml->description); + $long_description = LayoutHelper::render('joomla.edit.fieldset', $this); + + if (!$long_description) { + $truncated = HTMLHelper::_('string.truncate', $short_description, 550, true, false); + + if (strlen($truncated) > 500) { + $long_description = $short_description; + $short_description = HTMLHelper::_('string.truncate', $truncated, 250); + + if ($short_description == $long_description) { + $long_description = ''; + } + } + } + ?> +

+ +

+ + + +

+ +
+ + +
+ + +
+ + form->getInput($hasContentFieldName); + } + $this->fieldset = 'basic'; + $html = LayoutHelper::render('joomla.edit.fieldset', $this); + echo $html ? '
' . $html : ''; + ?> +
+
+ fields = array( + 'showtitle', + 'position', + 'published', + 'publish_up', + 'publish_down', + 'access', + 'ordering', + 'language', + 'note' + ); + + ?> + item->client_id == 0) : ?> + + + + +
+
+ + + + +
+
+ +
+
+ + + + item->client_id == 0) : ?> + +
+ +
+ loadTemplate('assignment'); ?> +
+
+ + + + fieldsets = array(); + $this->ignore_fieldsets = array('basic', 'description'); + echo LayoutHelper::render('joomla.edit.params', $this); + ?> + + canDo->get('core.admin')) : ?> + +
+ +
+ form->getInput('rules'); ?> +
+
+ + + + + + + + + form->getInput('module'); ?> + form->getInput('client_id'); ?> +
diff --git a/administrator/components/com_modules/tmpl/module/edit_assignment.php b/administrator/components/com_modules/tmpl/module/edit_assignment.php index bcd98ef2b1056..311e19998e796 100644 --- a/administrator/components/com_modules/tmpl/module/edit_assignment.php +++ b/administrator/components/com_modules/tmpl/module/edit_assignment.php @@ -1,4 +1,5 @@ document->getWebAssetManager() - ->useScript('joomla.treeselectmenu') - ->useScript('com_modules.admin-module-edit-assignment'); + ->useScript('joomla.treeselectmenu') + ->useScript('com_modules.admin-module-edit-assignment'); ?>
- -
- -
+ +
+ +
+ + diff --git a/administrator/components/com_modules/tmpl/module/modal.php b/administrator/components/com_modules/tmpl/module/modal.php index bb8088a027279..b90680e779098 100644 --- a/administrator/components/com_modules/tmpl/module/modal.php +++ b/administrator/components/com_modules/tmpl/module/modal.php @@ -1,4 +1,5 @@
- setLayout('edit'); ?> - loadTemplate(); ?> + setLayout('edit'); ?> + loadTemplate(); ?>
diff --git a/administrator/components/com_modules/tmpl/modules/default.php b/administrator/components/com_modules/tmpl/modules/default.php index c3bccaa60f501..d4ba1df7fdf59 100644 --- a/administrator/components/com_modules/tmpl/modules/default.php +++ b/administrator/components/com_modules/tmpl/modules/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $clientId = (int) $this->state->get('client_id', 0); $user = Factory::getUser(); @@ -29,191 +30,191 @@ $listDirn = $this->escape($this->state->get('list.direction')); $saveOrder = ($listOrder == 'a.ordering'); -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_modules&task=modules.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_modules&task=modules.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
-
- $this)); ?> - total > 0) : ?> - - - - - - - - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="false"> - items as $i => $item) : - $ordering = ($listOrder == 'a.ordering'); - $canCreate = $user->authorise('core.create', 'com_modules'); - $canEdit = $user->authorise('core.edit', 'com_modules.module.' . $item->id); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id')|| is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_modules.module.' . $item->id) && $canCheckin; - ?> - - - - - + + + + + + + + + + + + + + + +
- , - , - -
- - - - - - - - - - - - - - - - - - - - - -
- id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - - enabled > 0) : ?> - published, $i, 'modules.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> - - - - - - - -
- checked_out) : ?> - editor, $item->checked_out_time, 'modules.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - +
+ $this)); ?> + total > 0) : ?> + + + + + + + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="false"> + items as $i => $item) : + $ordering = ($listOrder == 'a.ordering'); + $canCreate = $user->authorise('core.create', 'com_modules'); + $canEdit = $user->authorise('core.edit', 'com_modules.module.' . $item->id); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_modules.module.' . $item->id) && $canCheckin; + ?> + + + + + - - - - - - - - - - - - - - - -
+ , + , + +
+ + + + + + + + + + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + + enabled > 0) : ?> + published, $i, 'modules.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> + + + + + + + +
+ checked_out) : ?> + editor, $item->checked_out_time, 'modules.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + - note)) : ?> -
- escape($item->note)); ?> -
- -
-
- position) : ?> - - position; ?> - - - - - - - - name; ?> - - pages; ?> - - escape($item->access_level); ?> - - - - language == ''):?> - - language == '*'):?> - - - escape($item->language); ?> - - - id; ?> -
+ note)) : ?> +
+ escape($item->note)); ?> +
+ +
+
+ position) : ?> + + position; ?> + + + + + + + + name; ?> + + pages; ?> + + escape($item->access_level); ?> + + + + language == '') :?> + + language == '*') :?> + + + escape($item->language); ?> + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - authorise('core.create', 'com_modules') - && $user->authorise('core.edit', 'com_modules') - && $user->authorise('core.edit.state', 'com_modules')) : ?> - Text::_('COM_MODULES_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ), - $this->loadTemplate('batch_body') - ); ?> - - - - -
+ + authorise('core.create', 'com_modules') + && $user->authorise('core.edit', 'com_modules') + && $user->authorise('core.edit.state', 'com_modules') +) : ?> + Text::_('COM_MODULES_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ), + $this->loadTemplate('batch_body') + ); ?> + + + + +
diff --git a/administrator/components/com_modules/tmpl/modules/default_batch_body.php b/administrator/components/com_modules/tmpl/modules/default_batch_body.php index 2a52e9c45f60c..4683baede1166 100644 --- a/administrator/components/com_modules/tmpl/modules/default_batch_body.php +++ b/administrator/components/com_modules/tmpl/modules/default_batch_body.php @@ -1,4 +1,5 @@ 'batch-position-id', + 'id' => 'batch-position-id', ); Text::script('JGLOBAL_SELECT_NO_RESULTS_MATCH'); Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); $this->document->getWebAssetManager() - ->usePreset('choicesjs') - ->useScript('webcomponent.field-fancy-select') - ->useScript('joomla.batch-copymove'); + ->usePreset('choicesjs') + ->useScript('webcomponent.field-fancy-select') + ->useScript('joomla.batch-copymove'); ?>
-

-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
-
- = 0) : ?> -
-
- -
- - - -
- -
-
-
- -
-
+

+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ = 0) : ?> +
+
+ +
+ + + +
+ +
+
+
+ +
+
diff --git a/administrator/components/com_modules/tmpl/modules/default_batch_footer.php b/administrator/components/com_modules/tmpl/modules/default_batch_footer.php index 44f8345ac2384..0a684547cb746 100644 --- a/administrator/components/com_modules/tmpl/modules/default_batch_footer.php +++ b/administrator/components/com_modules/tmpl/modules/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/administrator/components/com_modules/tmpl/modules/emptystate.php b/administrator/components/com_modules/tmpl/modules/emptystate.php index 323715f6070cd..f0b2b02ed1d2a 100644 --- a/administrator/components/com_modules/tmpl/modules/emptystate.php +++ b/administrator/components/com_modules/tmpl/modules/emptystate.php @@ -1,4 +1,5 @@ 'COM_MODULES', - 'formURL' => 'index.php?option=com_modules&view=select&client_id=' . $this->clientId, - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Module', - 'icon' => 'icon-cube module', - // Although it is (almost) impossible to get to this page with no created Administrator Modules, we add this for completeness. - 'title' => Text::_('COM_MODULES_EMPTYSTATE_TITLE_' . ($this->clientId ? 'ADMINISTRATOR' : 'SITE')), + 'textPrefix' => 'COM_MODULES', + 'formURL' => 'index.php?option=com_modules&view=select&client_id=' . $this->clientId, + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Module', + 'icon' => 'icon-cube module', + // Although it is (almost) impossible to get to this page with no created Administrator Modules, we add this for completeness. + 'title' => Text::_('COM_MODULES_EMPTYSTATE_TITLE_' . ($this->clientId ? 'ADMINISTRATOR' : 'SITE')), ]; -if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_modules')) -{ - $displayData['createURL'] = 'index.php?option=com_modules&view=select&client_id=' . $this->clientId; +if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_modules')) { + $displayData['createURL'] = 'index.php?option=com_modules&view=select&client_id=' . $this->clientId; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_modules/tmpl/modules/modal.php b/administrator/components/com_modules/tmpl/modules/modal.php index 972919a7dd79f..3c24c77f552c0 100644 --- a/administrator/components/com_modules/tmpl/modules/modal.php +++ b/administrator/components/com_modules/tmpl/modules/modal.php @@ -1,4 +1,5 @@ isClient('site')) -{ - Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); +if (Factory::getApplication()->isClient('site')) { + Session::checkToken('get') or die(Text::_('JINVALID_TOKEN')); } /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ @@ -30,109 +30,108 @@ $editor = Factory::getApplication()->input->get('editor', '', 'cmd'); $link = 'index.php?option=com_modules&view=modules&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; -if (!empty($editor)) -{ - $link .= '&editor=' . $editor; +if (!empty($editor)) { + $link .= '&editor=' . $editor; } ?>
-
+ - $this)); ?> + $this)); ?> - total > 0) : ?> - - - - - - - - - - - - - - - - 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - 2 => 'icon-folder', - ); - foreach ($this->items as $i => $item) : - ?> - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - -
- - - - - - escape($item->title); ?> - - - position) : ?> - escape($item->position); ?> - - - - - name; ?> - - pages; ?> - - escape($item->access_level); ?> - - - - id; ?> -
+ total > 0) : ?> + + + + + + + + + + + + + + + + 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + 2 => 'icon-folder', + ); + foreach ($this->items as $i => $item) : + ?> + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + +
+ + + + + + escape($item->title); ?> + + + position) : ?> + escape($item->position); ?> + + + + + name; ?> + + pages; ?> + + escape($item->access_level); ?> + + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - - + + + + -
+
diff --git a/administrator/components/com_modules/tmpl/select/default.php b/administrator/components/com_modules/tmpl/select/default.php index 344db412c06b9..e335ed5b81026 100644 --- a/administrator/components/com_modules/tmpl/select/default.php +++ b/administrator/components/com_modules/tmpl/select/default.php @@ -1,4 +1,5 @@ useScript('com_modules.admin-module-search'); if ($function) : - $wa->useScript('com_modules.admin-select-modal'); + $wa->useScript('com_modules.admin-select-modal'); endif; ?>
-
-
- -
- -
- -
-
-
-
+
+
+ +
+ +
+ +
+
+
+
-
-
- - -
-

- -

-
- items as &$item) : ?> - - state->get('client_id', 0) . $this->modalLink . '&eid=' . $item->extension_id; ?> - escape($item->name); ?> - escape(strip_tags($item->desc)), 200); ?> - -
-

-

- -

-
- - - -
- -
-
+
+
+ + +
+

+ +

+
+ items as &$item) : ?> + + state->get('client_id', 0) . $this->modalLink . '&eid=' . $item->extension_id; ?> + escape($item->name); ?> + escape(strip_tags($item->desc)), 200); ?> + +
+

+

+ +

+
+ + + +
+ +
+
diff --git a/administrator/components/com_modules/tmpl/select/modal.php b/administrator/components/com_modules/tmpl/select/modal.php index 2e6998d652bb3..25ac8d0afdee1 100644 --- a/administrator/components/com_modules/tmpl/select/modal.php +++ b/administrator/components/com_modules/tmpl/select/modal.php @@ -1,4 +1,5 @@ modalLink = '&tmpl=component&view=module&layout=modal'; ?>
- setLayout('default'); ?> - loadTemplate(); ?> + setLayout('default'); ?> + loadTemplate(); ?>
diff --git a/administrator/components/com_newsfeeds/helpers/newsfeeds.php b/administrator/components/com_newsfeeds/helpers/newsfeeds.php index 608aeabc716dc..3347beaead9af 100644 --- a/administrator/components/com_newsfeeds/helpers/newsfeeds.php +++ b/administrator/components/com_newsfeeds/helpers/newsfeeds.php @@ -1,13 +1,16 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; + /** * Newsfeeds component helper. diff --git a/administrator/components/com_newsfeeds/services/provider.php b/administrator/components/com_newsfeeds/services/provider.php index 3374c7259492a..0382efe8a6595 100644 --- a/administrator/components/com_newsfeeds/services/provider.php +++ b/administrator/components/com_newsfeeds/services/provider.php @@ -1,4 +1,5 @@ set(AssociationExtensionInterface::class, new AssociationsHelper); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->set(AssociationExtensionInterface::class, new AssociationsHelper()); - $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Newsfeeds')); - $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Newsfeeds')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Newsfeeds')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Newsfeeds')); + $container->registerServiceProvider(new CategoryFactory('\\Joomla\\Component\\Newsfeeds')); + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Newsfeeds')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Newsfeeds')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Newsfeeds')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new NewsfeedsComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new NewsfeedsComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); - $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setCategoryFactory($container->get(CategoryFactoryInterface::class)); + $component->setAssociationExtension($container->get(AssociationExtensionInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_newsfeeds/src/Controller/AjaxController.php b/administrator/components/com_newsfeeds/src/Controller/AjaxController.php index 34487acbf8275..ce5d7b74a405b 100644 --- a/administrator/components/com_newsfeeds/src/Controller/AjaxController.php +++ b/administrator/components/com_newsfeeds/src/Controller/AjaxController.php @@ -1,4 +1,5 @@ input->getInt('assocId', 0); + /** + * Method to fetch associations of a newsfeed + * + * The method assumes that the following http parameters are passed in an Ajax Get request: + * token: the form token + * assocId: the id of the newsfeed whose associations are to be returned + * excludeLang: the association for this language is to be excluded + * + * @return null + * + * @since 3.9.0 + */ + public function fetchAssociations() + { + if (!Session::checkToken('get')) { + echo new JsonResponse(null, Text::_('JINVALID_TOKEN'), true); + } else { + $assocId = $this->input->getInt('assocId', 0); - if ($assocId == 0) - { - echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); + if ($assocId == 0) { + echo new JsonResponse(null, Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'assocId'), true); - return; - } + return; + } - $excludeLang = $this->input->get('excludeLang', '', 'STRING'); + $excludeLang = $this->input->get('excludeLang', '', 'STRING'); - $associations = Associations::getAssociations('com_newsfeeds', '#__newsfeeds', 'com_newsfeeds.item', (int) $assocId); + $associations = Associations::getAssociations('com_newsfeeds', '#__newsfeeds', 'com_newsfeeds.item', (int) $assocId); - unset($associations[$excludeLang]); + unset($associations[$excludeLang]); - // Add the title to each of the associated records - $newsfeedsTable = $this->factory->createTable('Newsfeed', 'Administrator'); + // Add the title to each of the associated records + $newsfeedsTable = $this->factory->createTable('Newsfeed', 'Administrator'); - foreach ($associations as $lang => $association) - { - $newsfeedsTable->load($association->id); - $associations[$lang]->title = $newsfeedsTable->name; - } + foreach ($associations as $lang => $association) { + $newsfeedsTable->load($association->id); + $associations[$lang]->title = $newsfeedsTable->name; + } - $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); + $countContentLanguages = count(LanguageHelper::getContentLanguages(array(0, 1), false)); - if (count($associations) == 0) - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); - } - elseif ($countContentLanguages > count($associations) + 2) - { - $tags = implode(', ', array_keys($associations)); - $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); - } - else - { - $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); - } + if (count($associations) == 0) { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_NONE'); + } elseif ($countContentLanguages > count($associations) + 2) { + $tags = implode(', ', array_keys($associations)); + $message = Text::sprintf('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_SOME', $tags); + } else { + $message = Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_MESSAGE_ALL'); + } - echo new JsonResponse($associations, $message); - } - } + echo new JsonResponse($associations, $message); + } + } } diff --git a/administrator/components/com_newsfeeds/src/Controller/DisplayController.php b/administrator/components/com_newsfeeds/src/Controller/DisplayController.php index 7eeabbb78ade9..96908aa30c306 100644 --- a/administrator/components/com_newsfeeds/src/Controller/DisplayController.php +++ b/administrator/components/com_newsfeeds/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'newsfeeds'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); - - // Check for edit form. - if ($view == 'newsfeed' && $layout == 'edit' && !$this->checkEditId('com_newsfeeds.edit.newsfeed', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_newsfeeds&view=newsfeeds', false)); - - return false; - } - - return parent::display(); - } + /** + * The default view. + * + * @var string + * @since 1.6 + */ + protected $default_view = 'newsfeeds'; + + /** + * Method to display a view. + * + * @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()}. + * + * @return static|boolean This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = array()) + { + $view = $this->input->get('view', 'newsfeeds'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); + + // Check for edit form. + if ($view == 'newsfeed' && $layout == 'edit' && !$this->checkEditId('com_newsfeeds.edit.newsfeed', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_newsfeeds&view=newsfeeds', false)); + + return false; + } + + return parent::display(); + } } diff --git a/administrator/components/com_newsfeeds/src/Controller/NewsfeedController.php b/administrator/components/com_newsfeeds/src/Controller/NewsfeedController.php index 0ef1f40524d42..8c7db02614af9 100644 --- a/administrator/components/com_newsfeeds/src/Controller/NewsfeedController.php +++ b/administrator/components/com_newsfeeds/src/Controller/NewsfeedController.php @@ -1,4 +1,5 @@ input->getInt('filter_category_id'), 'int'); - $allow = null; - - if ($categoryId) - { - // If the category has been passed in the URL check it. - $allow = $this->app->getIdentity()->authorise('core.create', $this->option . '.category.' . $categoryId); - } - - if ($allow === null) - { - // In the absence of better information, revert to the component permissions. - return parent::allowAdd($data); - } - else - { - return $allow; - } - } - - /** - * Method to check if you can edit a record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - - // Since there is no asset tracking, fallback to the component permissions. - if (!$recordId) - { - return parent::allowEdit($data, $key); - } - - // Get the item. - $item = $this->getModel()->getItem($recordId); - - // Since there is no item, return false. - if (empty($item)) - { - return false; - } - - $user = $this->app->getIdentity(); - - // Check if can edit own core.edit.own. - $canEditOwn = $user->authorise('core.edit.own', $this->option . '.category.' . (int) $item->catid) && $item->created_by == $user->id; - - // Check the category core.edit permissions. - return $canEditOwn || $user->authorise('core.edit', $this->option . '.category.' . (int) $item->catid); - } - - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 2.5 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - $model = $this->getModel('Newsfeed', '', array()); - - // Preset the redirect - $this->setRedirect(Route::_('index.php?option=com_newsfeeds&view=newsfeeds' . $this->getRedirectToListAppend(), false)); - - return parent::batch($model); - } + use VersionableControllerTrait; + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowAdd($data = array()) + { + $categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('filter_category_id'), 'int'); + $allow = null; + + if ($categoryId) { + // If the category has been passed in the URL check it. + $allow = $this->app->getIdentity()->authorise('core.create', $this->option . '.category.' . $categoryId); + } + + if ($allow === null) { + // In the absence of better information, revert to the component permissions. + return parent::allowAdd($data); + } else { + return $allow; + } + } + + /** + * Method to check if you can edit a record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + + // Since there is no asset tracking, fallback to the component permissions. + if (!$recordId) { + return parent::allowEdit($data, $key); + } + + // Get the item. + $item = $this->getModel()->getItem($recordId); + + // Since there is no item, return false. + if (empty($item)) { + return false; + } + + $user = $this->app->getIdentity(); + + // Check if can edit own core.edit.own. + $canEditOwn = $user->authorise('core.edit.own', $this->option . '.category.' . (int) $item->catid) && $item->created_by == $user->id; + + // Check the category core.edit permissions. + return $canEditOwn || $user->authorise('core.edit', $this->option . '.category.' . (int) $item->catid); + } + + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 2.5 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + $model = $this->getModel('Newsfeed', '', array()); + + // Preset the redirect + $this->setRedirect(Route::_('index.php?option=com_newsfeeds&view=newsfeeds' . $this->getRedirectToListAppend(), false)); + + return parent::batch($model); + } } diff --git a/administrator/components/com_newsfeeds/src/Controller/NewsfeedsController.php b/administrator/components/com_newsfeeds/src/Controller/NewsfeedsController.php index 72bbd6303e4dc..ad56ff2ab61e5 100644 --- a/administrator/components/com_newsfeeds/src/Controller/NewsfeedsController.php +++ b/administrator/components/com_newsfeeds/src/Controller/NewsfeedsController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * 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 object The model. + * + * @since 1.6 + */ + public function getModel($name = 'Newsfeed', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_newsfeeds/src/Extension/NewsfeedsComponent.php b/administrator/components/com_newsfeeds/src/Extension/NewsfeedsComponent.php index a9226c389ae80..5eb61733fb079 100644 --- a/administrator/components/com_newsfeeds/src/Extension/NewsfeedsComponent.php +++ b/administrator/components/com_newsfeeds/src/Extension/NewsfeedsComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('newsfeedsadministrator', new AdministratorService); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('newsfeedsadministrator', new AdministratorService()); + } - /** - * Returns the table for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getTableNameForSection(string $section = null) - { - return $section === 'category' ? 'categories' : 'newsfeeds'; - } + /** + * Returns the table for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getTableNameForSection(string $section = null) + { + return $section === 'category' ? 'categories' : 'newsfeeds'; + } - /** - * Returns the state column for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getStateColumnForSection(string $section = null) - { - return 'published'; - } + /** + * Returns the state column for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getStateColumnForSection(string $section = null) + { + return 'published'; + } } diff --git a/administrator/components/com_newsfeeds/src/Field/Modal/NewsfeedField.php b/administrator/components/com_newsfeeds/src/Field/Modal/NewsfeedField.php index 3dfdaa5a5b186..fc2aef842ab41 100644 --- a/administrator/components/com_newsfeeds/src/Field/Modal/NewsfeedField.php +++ b/administrator/components/com_newsfeeds/src/Field/Modal/NewsfeedField.php @@ -1,4 +1,5 @@ element['new'] == 'true'); - $allowEdit = ((string) $this->element['edit'] == 'true'); - $allowClear = ((string) $this->element['clear'] != 'false'); - $allowSelect = ((string) $this->element['select'] != 'false'); - $allowPropagate = ((string) $this->element['propagate'] == 'true'); - - $languages = LanguageHelper::getContentLanguages(array(0, 1), false); - - // Load language - Factory::getLanguage()->load('com_newsfeeds', JPATH_ADMINISTRATOR); - - // The active newsfeed id field. - $value = (int) $this->value ?: ''; - - // Create the modal id. - $modalId = 'Newsfeed_' . $this->id; - - /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ - $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); - - // Add the modal field script to the document head. - $wa->useScript('field.modal-fields'); - - // Script to proxy the select modal function to the modal-fields.js file. - if ($allowSelect) - { - static $scriptSelect = null; - - if (is_null($scriptSelect)) - { - $scriptSelect = array(); - } - - if (!isset($scriptSelect[$this->id])) - { - $wa->addInlineScript(" + /** + * The form field type. + * + * @var string + * @since 1.6 + */ + protected $type = 'Modal_Newsfeed'; + + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + $allowNew = ((string) $this->element['new'] == 'true'); + $allowEdit = ((string) $this->element['edit'] == 'true'); + $allowClear = ((string) $this->element['clear'] != 'false'); + $allowSelect = ((string) $this->element['select'] != 'false'); + $allowPropagate = ((string) $this->element['propagate'] == 'true'); + + $languages = LanguageHelper::getContentLanguages(array(0, 1), false); + + // Load language + Factory::getLanguage()->load('com_newsfeeds', JPATH_ADMINISTRATOR); + + // The active newsfeed id field. + $value = (int) $this->value ?: ''; + + // Create the modal id. + $modalId = 'Newsfeed_' . $this->id; + + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + + // Add the modal field script to the document head. + $wa->useScript('field.modal-fields'); + + // Script to proxy the select modal function to the modal-fields.js file. + if ($allowSelect) { + static $scriptSelect = null; + + if (is_null($scriptSelect)) { + $scriptSelect = array(); + } + + if (!isset($scriptSelect[$this->id])) { + $wa->addInlineScript( + " window.jSelectNewsfeed_" . $this->id . " = function (id, title, object) { window.processModalSelect('Newsfeed', '" . $this->id . "', id, title, '', object); }", - [], - ['type' => 'module'] - ); - - Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); - - $scriptSelect[$this->id] = true; - } - } - - // Setup variables for display. - $linkNewsfeeds = 'index.php?option=com_newsfeeds&view=newsfeeds&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; - $linkNewsfeed = 'index.php?option=com_newsfeeds&view=newsfeed&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; - $modalTitle = Text::_('COM_NEWSFEEDS_SELECT_A_FEED'); - - if (isset($this->element['language'])) - { - $linkNewsfeeds .= '&forcedLanguage=' . $this->element['language']; - $linkNewsfeed .= '&forcedLanguage=' . $this->element['language']; - $modalTitle .= ' — ' . $this->element['label']; - } - - $urlSelect = $linkNewsfeeds . '&function=jSelectNewsfeed_' . $this->id; - $urlEdit = $linkNewsfeed . '&task=newsfeed.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; - $urlNew = $linkNewsfeed . '&task=newsfeed.add'; - - if ($value) - { - $id = (int) $value; - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('name')) - ->from($db->quoteName('#__newsfeeds')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $title = $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - - $title = empty($title) ? Text::_('COM_NEWSFEEDS_SELECT_A_FEED') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); - - // The current newsfeed display field. - $html = ''; - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - $html .= ''; - - // Select newsfeed button - if ($allowSelect) - { - $html .= '' - . ' ' . Text::_('JSELECT') - . ''; - } - - // New newsfeed button - if ($allowNew) - { - $html .= '' - . ' ' . Text::_('JACTION_CREATE') - . ''; - } - - // Edit newsfeed button - if ($allowEdit) - { - $html .= '' - . ' ' . Text::_('JACTION_EDIT') - . ''; - } - - // Clear newsfeed button - if ($allowClear) - { - $html .= '' - . ' ' . Text::_('JCLEAR') - . ''; - } - - // Propagate newsfeed button - if ($allowPropagate && count($languages) > 2) - { - // Strip off language tag at the end - $tagLength = (int) strlen($this->element['language']); - $callbackFunctionStem = substr("jSelectNewsfeed_" . $this->id, 0, -$tagLength); - - $html .= '' - . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') - . ''; - } - - if ($allowSelect || $allowNew || $allowEdit || $allowClear) - { - $html .= ''; - } - - // Select newsfeed modal - if ($allowSelect) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalSelect' . $modalId, - array( - 'title' => $modalTitle, - 'url' => $urlSelect, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ) - ); - } - - // New newsfeed modal - if ($allowNew) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalNew' . $modalId, - array( - 'title' => Text::_('COM_NEWSFEEDS_NEW_NEWSFEED'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlNew, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Edit newsfeed modal. - if ($allowEdit) - { - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - 'ModalEdit' . $modalId, - array( - 'title' => Text::_('COM_NEWSFEEDS_EDIT_NEWSFEED'), - 'backdrop' => 'static', - 'keyboard' => false, - 'closeButton' => false, - 'url' => $urlEdit, - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '' - . '' - . '', - ) - ); - } - - // Add class='required' for client side validation - $class = $this->required ? ' class="required modal-value"' : ''; - - $html .= ''; - - return $html; - } - - /** - * Method to get the field label markup. - * - * @return string The field label markup. - * - * @since 3.4 - */ - protected function getLabel() - { - return str_replace($this->id, $this->id . '_name', parent::getLabel()); - } + [], + ['type' => 'module'] + ); + + Text::script('JGLOBAL_ASSOCIATIONS_PROPAGATE_FAILED'); + + $scriptSelect[$this->id] = true; + } + } + + // Setup variables for display. + $linkNewsfeeds = 'index.php?option=com_newsfeeds&view=newsfeeds&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; + $linkNewsfeed = 'index.php?option=com_newsfeeds&view=newsfeed&layout=modal&tmpl=component&' . Session::getFormToken() . '=1'; + $modalTitle = Text::_('COM_NEWSFEEDS_SELECT_A_FEED'); + + if (isset($this->element['language'])) { + $linkNewsfeeds .= '&forcedLanguage=' . $this->element['language']; + $linkNewsfeed .= '&forcedLanguage=' . $this->element['language']; + $modalTitle .= ' — ' . $this->element['label']; + } + + $urlSelect = $linkNewsfeeds . '&function=jSelectNewsfeed_' . $this->id; + $urlEdit = $linkNewsfeed . '&task=newsfeed.edit&id=\' + document.getElementById("' . $this->id . '_id").value + \''; + $urlNew = $linkNewsfeed . '&task=newsfeed.add'; + + if ($value) { + $id = (int) $value; + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('name')) + ->from($db->quoteName('#__newsfeeds')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $title = $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + + $title = empty($title) ? Text::_('COM_NEWSFEEDS_SELECT_A_FEED') : htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); + + // The current newsfeed display field. + $html = ''; + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + $html .= ''; + + // Select newsfeed button + if ($allowSelect) { + $html .= '' + . ' ' . Text::_('JSELECT') + . ''; + } + + // New newsfeed button + if ($allowNew) { + $html .= '' + . ' ' . Text::_('JACTION_CREATE') + . ''; + } + + // Edit newsfeed button + if ($allowEdit) { + $html .= '' + . ' ' . Text::_('JACTION_EDIT') + . ''; + } + + // Clear newsfeed button + if ($allowClear) { + $html .= '' + . ' ' . Text::_('JCLEAR') + . ''; + } + + // Propagate newsfeed button + if ($allowPropagate && count($languages) > 2) { + // Strip off language tag at the end + $tagLength = (int) strlen($this->element['language']); + $callbackFunctionStem = substr("jSelectNewsfeed_" . $this->id, 0, -$tagLength); + + $html .= '' + . ' ' . Text::_('JGLOBAL_ASSOCIATIONS_PROPAGATE_BUTTON') + . ''; + } + + if ($allowSelect || $allowNew || $allowEdit || $allowClear) { + $html .= ''; + } + + // Select newsfeed modal + if ($allowSelect) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalSelect' . $modalId, + array( + 'title' => $modalTitle, + 'url' => $urlSelect, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ) + ); + } + + // New newsfeed modal + if ($allowNew) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalNew' . $modalId, + array( + 'title' => Text::_('COM_NEWSFEEDS_NEW_NEWSFEED'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlNew, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Edit newsfeed modal. + if ($allowEdit) { + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + 'ModalEdit' . $modalId, + array( + 'title' => Text::_('COM_NEWSFEEDS_EDIT_NEWSFEED'), + 'backdrop' => 'static', + 'keyboard' => false, + 'closeButton' => false, + 'url' => $urlEdit, + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '' + . '' + . '', + ) + ); + } + + // Add class='required' for client side validation + $class = $this->required ? ' class="required modal-value"' : ''; + + $html .= ''; + + return $html; + } + + /** + * Method to get the field label markup. + * + * @return string The field label markup. + * + * @since 3.4 + */ + protected function getLabel() + { + return str_replace($this->id, $this->id . '_name', parent::getLabel()); + } } diff --git a/administrator/components/com_newsfeeds/src/Field/NewsfeedsField.php b/administrator/components/com_newsfeeds/src/Field/NewsfeedsField.php index e85fa5578b1fb..f0744fc212621 100644 --- a/administrator/components/com_newsfeeds/src/Field/NewsfeedsField.php +++ b/administrator/components/com_newsfeeds/src/Field/NewsfeedsField.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('id', 'value'), - $db->quoteName('name', 'text'), - ] - ) - ->from($db->quoteName('#__newsfeeds', 'a')) - ->order($db->quoteName('a.name')); + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('id', 'value'), + $db->quoteName('name', 'text'), + ] + ) + ->from($db->quoteName('#__newsfeeds', 'a')) + ->order($db->quoteName('a.name')); - // Get the options. - $db->setQuery($query); + // Get the options. + $db->setQuery($query); - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $options); + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $options); - return $options; - } + return $options; + } } diff --git a/administrator/components/com_newsfeeds/src/Helper/AssociationsHelper.php b/administrator/components/com_newsfeeds/src/Helper/AssociationsHelper.php index 85c90141fbb19..33b59a0200b95 100644 --- a/administrator/components/com_newsfeeds/src/Helper/AssociationsHelper.php +++ b/administrator/components/com_newsfeeds/src/Helper/AssociationsHelper.php @@ -1,4 +1,5 @@ getType($typeName); - - $context = $this->extension . '.item'; - $catidField = 'catid'; - - if ($typeName === 'category') - { - $context = 'com_categories.item'; - $catidField = ''; - } - - // Get the associations. - $associations = Associations::getAssociations( - $this->extension, - $type['tables']['a'], - $context, - $id, - 'id', - 'alias', - $catidField - ); - - return $associations; - } - - /** - * Get item information - * - * @param string $typeName The item type - * @param int $id The id of item for which we need the associated items - * - * @return Table|null - * - * @since 3.7.0 - */ - public function getItem($typeName, $id) - { - if (empty($id)) - { - return null; - } - - $table = null; - - switch ($typeName) - { - case 'newsfeed': - $table = Table::getInstance('NewsfeedTable', 'Joomla\\Component\\Newsfeeds\\Administrator\\Table\\'); - break; - - case 'category': - $table = Table::getInstance('Category'); - break; - } - - if (empty($table)) - { - return null; - } - - $table->load($id); - - return $table; - } - - /** - * Get information about the type - * - * @param string $typeName The item type - * - * @return array Array of item types - * - * @since 3.7.0 - */ - public function getType($typeName = '') - { - $fields = $this->getFieldsTemplate(); - $tables = array(); - $joins = array(); - $support = $this->getSupportTemplate(); - $title = ''; - - if (in_array($typeName, $this->itemTypes)) - { - switch ($typeName) - { - case 'newsfeed': - $fields['title'] = 'a.name'; - $fields['state'] = 'a.published'; - - $support['state'] = true; - $support['acl'] = true; - $support['checkout'] = true; - $support['category'] = true; - $support['save2copy'] = true; - - $tables = array( - 'a' => '#__newsfeeds' - ); - $title = 'newsfeed'; - break; - - case 'category': - $fields['created_user_id'] = 'a.created_user_id'; - $fields['ordering'] = 'a.lft'; - $fields['level'] = 'a.level'; - $fields['catid'] = ''; - $fields['state'] = 'a.published'; - - $support['state'] = true; - $support['acl'] = true; - $support['checkout'] = true; - $support['level'] = true; - - $tables = array( - 'a' => '#__categories' - ); - - $title = 'category'; - break; - } - } - - return array( - 'fields' => $fields, - 'support' => $support, - 'tables' => $tables, - 'joins' => $joins, - 'title' => $title - ); - } + /** + * The extension name + * + * @var array $extension + * + * @since 3.7.0 + */ + protected $extension = 'com_newsfeeds'; + + /** + * Array of item types + * + * @var array $itemTypes + * + * @since 3.7.0 + */ + protected $itemTypes = array('newsfeed', 'category'); + + /** + * Has the extension association support + * + * @var boolean $associationsSupport + * + * @since 3.7.0 + */ + protected $associationsSupport = true; + + /** + * Method to get the associations for a given item. + * + * @param integer $id Id of the item + * @param string $view Name of the view + * + * @return array Array of associations for the item + * + * @since 4.0.0 + */ + public function getAssociationsForItem($id = 0, $view = null) + { + return AssociationHelper::getAssociations($id, $view); + } + + /** + * Get the associated items for an item + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return array + * + * @since 3.7.0 + */ + public function getAssociations($typeName, $id) + { + $type = $this->getType($typeName); + + $context = $this->extension . '.item'; + $catidField = 'catid'; + + if ($typeName === 'category') { + $context = 'com_categories.item'; + $catidField = ''; + } + + // Get the associations. + $associations = Associations::getAssociations( + $this->extension, + $type['tables']['a'], + $context, + $id, + 'id', + 'alias', + $catidField + ); + + return $associations; + } + + /** + * Get item information + * + * @param string $typeName The item type + * @param int $id The id of item for which we need the associated items + * + * @return Table|null + * + * @since 3.7.0 + */ + public function getItem($typeName, $id) + { + if (empty($id)) { + return null; + } + + $table = null; + + switch ($typeName) { + case 'newsfeed': + $table = Table::getInstance('NewsfeedTable', 'Joomla\\Component\\Newsfeeds\\Administrator\\Table\\'); + break; + + case 'category': + $table = Table::getInstance('Category'); + break; + } + + if (empty($table)) { + return null; + } + + $table->load($id); + + return $table; + } + + /** + * Get information about the type + * + * @param string $typeName The item type + * + * @return array Array of item types + * + * @since 3.7.0 + */ + public function getType($typeName = '') + { + $fields = $this->getFieldsTemplate(); + $tables = array(); + $joins = array(); + $support = $this->getSupportTemplate(); + $title = ''; + + if (in_array($typeName, $this->itemTypes)) { + switch ($typeName) { + case 'newsfeed': + $fields['title'] = 'a.name'; + $fields['state'] = 'a.published'; + + $support['state'] = true; + $support['acl'] = true; + $support['checkout'] = true; + $support['category'] = true; + $support['save2copy'] = true; + + $tables = array( + 'a' => '#__newsfeeds' + ); + $title = 'newsfeed'; + break; + + case 'category': + $fields['created_user_id'] = 'a.created_user_id'; + $fields['ordering'] = 'a.lft'; + $fields['level'] = 'a.level'; + $fields['catid'] = ''; + $fields['state'] = 'a.published'; + + $support['state'] = true; + $support['acl'] = true; + $support['checkout'] = true; + $support['level'] = true; + + $tables = array( + 'a' => '#__categories' + ); + + $title = 'category'; + break; + } + } + + return array( + 'fields' => $fields, + 'support' => $support, + 'tables' => $tables, + 'joins' => $joins, + 'title' => $title + ); + } } diff --git a/administrator/components/com_newsfeeds/src/Helper/NewsfeedsHelper.php b/administrator/components/com_newsfeeds/src/Helper/NewsfeedsHelper.php index 55dc24d5ff30d..bd9dd739aa96c 100644 --- a/administrator/components/com_newsfeeds/src/Helper/NewsfeedsHelper.php +++ b/administrator/components/com_newsfeeds/src/Helper/NewsfeedsHelper.php @@ -1,4 +1,5 @@ getQuery(true); - $query->select( - [ - $db->quoteName('published', 'state'), - 'COUNT(*) AS ' . $db->quoteName('count'), - ] - ) - ->from($db->quoteName('#__newsfeeds')) - ->where($db->quoteName('catid') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER) - ->group($db->quoteName('state')); - $db->setQuery($query); - - foreach ($items as $item) - { - $item->count_trashed = 0; - $item->count_archived = 0; - $item->count_unpublished = 0; - $item->count_published = 0; - - $id = (int) $item->id; - $newfeeds = $db->loadObjectList(); - - foreach ($newfeeds as $newsfeed) - { - if ($newsfeed->state == 1) - { - $item->count_published = $newsfeed->count; - } - - if ($newsfeed->state == 0) - { - $item->count_unpublished = $newsfeed->count; - } - - if ($newsfeed->state == 2) - { - $item->count_archived = $newsfeed->count; - } - - if ($newsfeed->state == -2) - { - $item->count_trashed = $newsfeed->count; - } - } - } - - return $items; - } - - /** - * Adds Count Items for Tag Manager. - * - * @param \stdClass[] &$items The newsfeed tag objects - * @param string $extension The name of the active view. - * - * @return \stdClass[] - * - * @since 3.6 - */ - public static function countTagItems(&$items, $extension) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $parts = explode('.', $extension); - $section = null; - - if (count($parts) > 1) - { - $section = $parts[1]; - } - - $query->select( - [ - $db->quoteName('published', 'state'), - 'COUNT(*) AS ' . $db->quoteName('count'), - ] - ) - ->from($db->quoteName('#__contentitem_tag_map', 'ct')); - - if ($section === 'category') - { - $query->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('ct.content_item_id') . ' = ' . $db->quoteName('c.id')); - } - else - { - $query->join('LEFT', $db->quoteName('#__newsfeeds', 'c'), $db->quoteName('ct.content_item_id') . ' = ' . $db->quoteName('c.id')); - } - - $query->where( - [ - $db->quoteName('ct.tag_id') . ' = :id', - $db->quoteName('ct.type_alias') . ' = :extension', - ] - ) - ->bind(':id', $id, ParameterType::INTEGER) - ->bind(':extension', $extension) - ->group($db->quoteName('state')); - - $db->setQuery($query); - - foreach ($items as $item) - { - $item->count_trashed = 0; - $item->count_archived = 0; - $item->count_unpublished = 0; - $item->count_published = 0; - - // Update ID used in database query. - $id = (int) $item->id; - $newsfeeds = $db->loadObjectList(); - - foreach ($newsfeeds as $newsfeed) - { - if ($newsfeed->state == 1) - { - $item->count_published = $newsfeed->count; - } - - if ($newsfeed->state == 0) - { - $item->count_unpublished = $newsfeed->count; - } - - if ($newsfeed->state == 2) - { - $item->count_archived = $newsfeed->count; - } - - if ($newsfeed->state == -2) - { - $item->count_trashed = $newsfeed->count; - } - } - } - - return $items; - } + /** + * Name of the extension + * + * @var string + */ + public static $extension = 'com_newsfeeds'; + + /** + * Adds Count Items for Category Manager. + * + * @param \stdClass[] &$items The banner category objects + * + * @return \stdClass[] + * + * @since 3.5 + */ + public static function countItems(&$items) + { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('published', 'state'), + 'COUNT(*) AS ' . $db->quoteName('count'), + ] + ) + ->from($db->quoteName('#__newsfeeds')) + ->where($db->quoteName('catid') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER) + ->group($db->quoteName('state')); + $db->setQuery($query); + + foreach ($items as $item) { + $item->count_trashed = 0; + $item->count_archived = 0; + $item->count_unpublished = 0; + $item->count_published = 0; + + $id = (int) $item->id; + $newfeeds = $db->loadObjectList(); + + foreach ($newfeeds as $newsfeed) { + if ($newsfeed->state == 1) { + $item->count_published = $newsfeed->count; + } + + if ($newsfeed->state == 0) { + $item->count_unpublished = $newsfeed->count; + } + + if ($newsfeed->state == 2) { + $item->count_archived = $newsfeed->count; + } + + if ($newsfeed->state == -2) { + $item->count_trashed = $newsfeed->count; + } + } + } + + return $items; + } + + /** + * Adds Count Items for Tag Manager. + * + * @param \stdClass[] &$items The newsfeed tag objects + * @param string $extension The name of the active view. + * + * @return \stdClass[] + * + * @since 3.6 + */ + public static function countTagItems(&$items, $extension) + { + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $parts = explode('.', $extension); + $section = null; + + if (count($parts) > 1) { + $section = $parts[1]; + } + + $query->select( + [ + $db->quoteName('published', 'state'), + 'COUNT(*) AS ' . $db->quoteName('count'), + ] + ) + ->from($db->quoteName('#__contentitem_tag_map', 'ct')); + + if ($section === 'category') { + $query->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('ct.content_item_id') . ' = ' . $db->quoteName('c.id')); + } else { + $query->join('LEFT', $db->quoteName('#__newsfeeds', 'c'), $db->quoteName('ct.content_item_id') . ' = ' . $db->quoteName('c.id')); + } + + $query->where( + [ + $db->quoteName('ct.tag_id') . ' = :id', + $db->quoteName('ct.type_alias') . ' = :extension', + ] + ) + ->bind(':id', $id, ParameterType::INTEGER) + ->bind(':extension', $extension) + ->group($db->quoteName('state')); + + $db->setQuery($query); + + foreach ($items as $item) { + $item->count_trashed = 0; + $item->count_archived = 0; + $item->count_unpublished = 0; + $item->count_published = 0; + + // Update ID used in database query. + $id = (int) $item->id; + $newsfeeds = $db->loadObjectList(); + + foreach ($newsfeeds as $newsfeed) { + if ($newsfeed->state == 1) { + $item->count_published = $newsfeed->count; + } + + if ($newsfeed->state == 0) { + $item->count_unpublished = $newsfeed->count; + } + + if ($newsfeed->state == 2) { + $item->count_archived = $newsfeed->count; + } + + if ($newsfeed->state == -2) { + $item->count_trashed = $newsfeed->count; + } + } + } + + return $items; + } } diff --git a/administrator/components/com_newsfeeds/src/Model/NewsfeedModel.php b/administrator/components/com_newsfeeds/src/Model/NewsfeedModel.php index 46640cb02322b..405f86a920f2e 100644 --- a/administrator/components/com_newsfeeds/src/Model/NewsfeedModel.php +++ b/administrator/components/com_newsfeeds/src/Model/NewsfeedModel.php @@ -1,4 +1,5 @@ id) || $record->published != -2) - { - return false; - } - - if (!empty($record->catid)) - { - return Factory::getUser()->authorise('core.delete', 'com_newsfeed.category.' . (int) $record->catid); - } - - return parent::canDelete($record); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 1.6 - */ - protected function canEditState($record) - { - if (!empty($record->catid)) - { - return Factory::getUser()->authorise('core.edit.state', 'com_newsfeeds.category.' . (int) $record->catid); - } - - return parent::canEditState($record); - } - - /** - * Method to get the record form. - * - * @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 Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_newsfeeds.newsfeed', 'newsfeed', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Modify the form based on access controls. - if (!$this->canEditState((object) $data)) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('published', 'disabled', 'true'); - $form->setFieldAttribute('publish_up', 'disabled', 'true'); - $form->setFieldAttribute('publish_down', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('published', 'filter', 'unset'); - $form->setFieldAttribute('publish_up', 'filter', 'unset'); - $form->setFieldAttribute('publish_down', 'filter', 'unset'); - } - - // Don't allow to change the created_by user if not allowed to access com_users. - if (!Factory::getUser()->authorise('core.manage', 'com_users')) - { - $form->setFieldAttribute('created_by', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_newsfeeds.edit.newsfeed.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Prime some default values. - if ($this->getState('newsfeed.id') == 0) - { - $app = Factory::getApplication(); - $data->set('catid', $app->input->get('catid', $app->getUserState('com_newsfeeds.newsfeeds.filter.category_id'), 'int')); - } - } - - $this->preprocessData('com_newsfeeds.newsfeed', $data); - - return $data; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 3.0 - */ - public function save($data) - { - $input = Factory::getApplication()->input; - - // Create new category, if needed. - $createCategory = true; - - // If category ID is provided, check if it's valid. - if (is_numeric($data['catid']) && $data['catid']) - { - $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_newsfeeds'); - } - - // Save New Category - if ($createCategory && $this->canCreateCategory()) - { - $category = [ - // Remove #new# prefix, if exists. - 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], - 'parent_id' => 1, - 'extension' => 'com_newsfeeds', - 'language' => $data['language'], - 'published' => 1, - ]; - - /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ - $categoryModel = Factory::getApplication()->bootComponent('com_categories') - ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); - - // Create new category. - if (!$categoryModel->save($category)) - { - $this->setError($categoryModel->getError()); - - return false; - } - - // Get the Category ID. - $data['catid'] = $categoryModel->getState('category.id'); - } - - // Alter the name for save as copy - if ($input->get('task') == 'save2copy') - { - $origTable = clone $this->getTable(); - $origTable->load($input->getInt('id')); - - if ($data['name'] == $origTable->name) - { - list($name, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['name']); - $data['name'] = $name; - $data['alias'] = $alias; - } - else - { - if ($data['alias'] == $origTable->alias) - { - $data['alias'] = ''; - } - } - - $data['published'] = 0; - } - - return parent::save($data); - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - if ($item = parent::getItem($pk)) - { - // Convert the params field to an array. - $registry = new Registry($item->metadata); - $item->metadata = $registry->toArray(); - - // Convert the images field to an array. - $registry = new Registry($item->images); - $item->images = $registry->toArray(); - } - - // Load associated newsfeeds items - $assoc = Associations::isEnabled(); - - if ($assoc) - { - $item->associations = array(); - - if ($item->id != null) - { - $associations = Associations::getAssociations('com_newsfeeds', '#__newsfeeds', 'com_newsfeeds.item', $item->id); - - foreach ($associations as $tag => $association) - { - $item->associations[$tag] = $association->id; - } - } - } - - if (!empty($item->id)) - { - $item->tags = new TagsHelper; - $item->tags->getTagIds($item->id, 'com_newsfeeds.newsfeed'); - - // @todo: We probably don't need this in any client - but needs careful validation - if (!Factory::getApplication()->isClient('api')) - { - $item->metadata['tags'] = $item->tags; - } - } - - return $item; - } - - /** - * Prepare and sanitise the table prior to saving. - * - * @param \Joomla\CMS\Table\Table $table The table object - * - * @return void - */ - protected function prepareTable($table) - { - $date = Factory::getDate(); - $user = Factory::getUser(); - - $table->name = htmlspecialchars_decode($table->name, ENT_QUOTES); - $table->alias = ApplicationHelper::stringURLSafe($table->alias, $table->language); - - if (empty($table->alias)) - { - $table->alias = ApplicationHelper::stringURLSafe($table->name, $table->language); - } - - if (empty($table->id)) - { - // Set the values - $table->created = $date->toSql(); - - // Set ordering to the last item if not set - if (empty($table->ordering)) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('MAX(' . $db->quoteName('ordering') . ')') - ->from($db->quoteName('#__newsfeeds')); - $db->setQuery($query); - $max = $db->loadResult(); - - $table->ordering = $max + 1; - } - } - else - { - // Set the values - $table->modified = $date->toSql(); - $table->modified_by = $user->get('id'); - } - - // Increment the content version number. - $table->version++; - } - - /** - * Method to change the published state of one or more records. - * - * @param array &$pks A list of the primary keys to change. - * @param integer $value The value of the published state. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function publish(&$pks, $value = 1) - { - $result = parent::publish($pks, $value); - - // Clean extra cache for newsfeeds - $this->cleanCache('feed_parser'); - - return $result; - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param object $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - return [ - $this->getDatabase()->quoteName('catid') . ' = ' . (int) $table->catid, - ]; - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param Form $form The form object. - * @param array $data The data to be injected into the form - * @param string $group The plugin group to process - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - if ($this->canCreateCategory()) - { - $form->setFieldAttribute('catid', 'allowAdd', 'true'); - - // Add a prefix for categories created on the fly. - $form->setFieldAttribute('catid', 'customPrefix', '#new#'); - } - - // Association newsfeeds items - if (Associations::isEnabled()) - { - $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); - - if (count($languages) > 1) - { - $addform = new \SimpleXMLElement('
'); - $fields = $addform->addChild('fields'); - $fields->addAttribute('name', 'associations'); - $fieldset = $fields->addChild('fieldset'); - $fieldset->addAttribute('name', 'item_associations'); - - foreach ($languages as $language) - { - $field = $fieldset->addChild('field'); - $field->addAttribute('name', $language->lang_code); - $field->addAttribute('type', 'modal_newsfeed'); - $field->addAttribute('language', $language->lang_code); - $field->addAttribute('label', $language->title); - $field->addAttribute('translate_label', 'false'); - $field->addAttribute('select', 'true'); - $field->addAttribute('new', 'true'); - $field->addAttribute('edit', 'true'); - $field->addAttribute('clear', 'true'); - $field->addAttribute('propagate', 'true'); - } - - $form->load($addform, false); - } - } - - parent::preprocessForm($form, $data, $group); - } - - /** - * Is the user allowed to create an on the fly category? - * - * @return boolean - * - * @since 3.6.1 - */ - private function canCreateCategory() - { - return Factory::getUser()->authorise('core.create', 'com_newsfeeds'); - } + use VersionableModelTrait; + + /** + * The type alias for this content type. + * + * @var string + * @since 3.2 + */ + public $typeAlias = 'com_newsfeeds.newsfeed'; + + /** + * The context used for the associations table + * + * @var string + * @since 3.4.4 + */ + protected $associationsContext = 'com_newsfeeds.item'; + + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_NEWSFEEDS'; + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->published != -2) { + return false; + } + + if (!empty($record->catid)) { + return Factory::getUser()->authorise('core.delete', 'com_newsfeed.category.' . (int) $record->catid); + } + + return parent::canDelete($record); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canEditState($record) + { + if (!empty($record->catid)) { + return Factory::getUser()->authorise('core.edit.state', 'com_newsfeeds.category.' . (int) $record->catid); + } + + return parent::canEditState($record); + } + + /** + * Method to get the record form. + * + * @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 Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_newsfeeds.newsfeed', 'newsfeed', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Modify the form based on access controls. + if (!$this->canEditState((object) $data)) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('published', 'disabled', 'true'); + $form->setFieldAttribute('publish_up', 'disabled', 'true'); + $form->setFieldAttribute('publish_down', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('published', 'filter', 'unset'); + $form->setFieldAttribute('publish_up', 'filter', 'unset'); + $form->setFieldAttribute('publish_down', 'filter', 'unset'); + } + + // Don't allow to change the created_by user if not allowed to access com_users. + if (!Factory::getUser()->authorise('core.manage', 'com_users')) { + $form->setFieldAttribute('created_by', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_newsfeeds.edit.newsfeed.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Prime some default values. + if ($this->getState('newsfeed.id') == 0) { + $app = Factory::getApplication(); + $data->set('catid', $app->input->get('catid', $app->getUserState('com_newsfeeds.newsfeeds.filter.category_id'), 'int')); + } + } + + $this->preprocessData('com_newsfeeds.newsfeed', $data); + + return $data; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 3.0 + */ + public function save($data) + { + $input = Factory::getApplication()->input; + + // Create new category, if needed. + $createCategory = true; + + // If category ID is provided, check if it's valid. + if (is_numeric($data['catid']) && $data['catid']) { + $createCategory = !CategoriesHelper::validateCategoryId($data['catid'], 'com_newsfeeds'); + } + + // Save New Category + if ($createCategory && $this->canCreateCategory()) { + $category = [ + // Remove #new# prefix, if exists. + 'title' => strpos($data['catid'], '#new#') === 0 ? substr($data['catid'], 5) : $data['catid'], + 'parent_id' => 1, + 'extension' => 'com_newsfeeds', + 'language' => $data['language'], + 'published' => 1, + ]; + + /** @var \Joomla\Component\Categories\Administrator\Model\CategoryModel $categoryModel */ + $categoryModel = Factory::getApplication()->bootComponent('com_categories') + ->getMVCFactory()->createModel('Category', 'Administrator', ['ignore_request' => true]); + + // Create new category. + if (!$categoryModel->save($category)) { + $this->setError($categoryModel->getError()); + + return false; + } + + // Get the Category ID. + $data['catid'] = $categoryModel->getState('category.id'); + } + + // Alter the name for save as copy + if ($input->get('task') == 'save2copy') { + $origTable = clone $this->getTable(); + $origTable->load($input->getInt('id')); + + if ($data['name'] == $origTable->name) { + list($name, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['name']); + $data['name'] = $name; + $data['alias'] = $alias; + } else { + if ($data['alias'] == $origTable->alias) { + $data['alias'] = ''; + } + } + + $data['published'] = 0; + } + + return parent::save($data); + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + if ($item = parent::getItem($pk)) { + // Convert the params field to an array. + $registry = new Registry($item->metadata); + $item->metadata = $registry->toArray(); + + // Convert the images field to an array. + $registry = new Registry($item->images); + $item->images = $registry->toArray(); + } + + // Load associated newsfeeds items + $assoc = Associations::isEnabled(); + + if ($assoc) { + $item->associations = array(); + + if ($item->id != null) { + $associations = Associations::getAssociations('com_newsfeeds', '#__newsfeeds', 'com_newsfeeds.item', $item->id); + + foreach ($associations as $tag => $association) { + $item->associations[$tag] = $association->id; + } + } + } + + if (!empty($item->id)) { + $item->tags = new TagsHelper(); + $item->tags->getTagIds($item->id, 'com_newsfeeds.newsfeed'); + + // @todo: We probably don't need this in any client - but needs careful validation + if (!Factory::getApplication()->isClient('api')) { + $item->metadata['tags'] = $item->tags; + } + } + + return $item; + } + + /** + * Prepare and sanitise the table prior to saving. + * + * @param \Joomla\CMS\Table\Table $table The table object + * + * @return void + */ + protected function prepareTable($table) + { + $date = Factory::getDate(); + $user = Factory::getUser(); + + $table->name = htmlspecialchars_decode($table->name, ENT_QUOTES); + $table->alias = ApplicationHelper::stringURLSafe($table->alias, $table->language); + + if (empty($table->alias)) { + $table->alias = ApplicationHelper::stringURLSafe($table->name, $table->language); + } + + if (empty($table->id)) { + // Set the values + $table->created = $date->toSql(); + + // Set ordering to the last item if not set + if (empty($table->ordering)) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('MAX(' . $db->quoteName('ordering') . ')') + ->from($db->quoteName('#__newsfeeds')); + $db->setQuery($query); + $max = $db->loadResult(); + + $table->ordering = $max + 1; + } + } else { + // Set the values + $table->modified = $date->toSql(); + $table->modified_by = $user->get('id'); + } + + // Increment the content version number. + $table->version++; + } + + /** + * Method to change the published state of one or more records. + * + * @param array &$pks A list of the primary keys to change. + * @param integer $value The value of the published state. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function publish(&$pks, $value = 1) + { + $result = parent::publish($pks, $value); + + // Clean extra cache for newsfeeds + $this->cleanCache('feed_parser'); + + return $result; + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param object $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + return [ + $this->getDatabase()->quoteName('catid') . ' = ' . (int) $table->catid, + ]; + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param Form $form The form object. + * @param array $data The data to be injected into the form + * @param string $group The plugin group to process + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + if ($this->canCreateCategory()) { + $form->setFieldAttribute('catid', 'allowAdd', 'true'); + + // Add a prefix for categories created on the fly. + $form->setFieldAttribute('catid', 'customPrefix', '#new#'); + } + + // Association newsfeeds items + if (Associations::isEnabled()) { + $languages = LanguageHelper::getContentLanguages(false, false, null, 'ordering', 'asc'); + + if (count($languages) > 1) { + $addform = new \SimpleXMLElement(''); + $fields = $addform->addChild('fields'); + $fields->addAttribute('name', 'associations'); + $fieldset = $fields->addChild('fieldset'); + $fieldset->addAttribute('name', 'item_associations'); + + foreach ($languages as $language) { + $field = $fieldset->addChild('field'); + $field->addAttribute('name', $language->lang_code); + $field->addAttribute('type', 'modal_newsfeed'); + $field->addAttribute('language', $language->lang_code); + $field->addAttribute('label', $language->title); + $field->addAttribute('translate_label', 'false'); + $field->addAttribute('select', 'true'); + $field->addAttribute('new', 'true'); + $field->addAttribute('edit', 'true'); + $field->addAttribute('clear', 'true'); + $field->addAttribute('propagate', 'true'); + } + + $form->load($addform, false); + } + } + + parent::preprocessForm($form, $data, $group); + } + + /** + * Is the user allowed to create an on the fly category? + * + * @return boolean + * + * @since 3.6.1 + */ + private function canCreateCategory() + { + return Factory::getUser()->authorise('core.create', 'com_newsfeeds'); + } } diff --git a/administrator/components/com_newsfeeds/src/Model/NewsfeedsModel.php b/administrator/components/com_newsfeeds/src/Model/NewsfeedsModel.php index c861401ac04f5..8bbc97324ea25 100644 --- a/administrator/components/com_newsfeeds/src/Model/NewsfeedsModel.php +++ b/administrator/components/com_newsfeeds/src/Model/NewsfeedsModel.php @@ -1,4 +1,5 @@ input->get('forcedLanguage', '', 'cmd'); - - // Adjust the context to support modal layouts. - if ($layout = $app->input->get('layout')) - { - $this->context .= '.' . $layout; - } - - // Adjust the context to support forced languages. - if ($forcedLanguage) - { - $this->context .= '.' . $forcedLanguage; - } - - // Load the parameters. - $params = ComponentHelper::getParams('com_newsfeeds'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - - // Force a language. - if (!empty($forcedLanguage)) - { - $this->setState('filter.language', $forcedLanguage); - } - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.category_id'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.language'); - $id .= ':' . $this->getState('filter.level'); - $id .= ':' . serialize($this->getState('filter.tag')); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.name'), - $db->quoteName('a.alias'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.catid'), - $db->quoteName('a.numarticles'), - $db->quoteName('a.cache_time'), - $db->quoteName('a.created_by'), - $db->quoteName('a.published'), - $db->quoteName('a.access'), - $db->quoteName('a.ordering'), - $db->quoteName('a.language'), - $db->quoteName('a.publish_up'), - $db->quoteName('a.publish_down'), - ] - ) - ) - ->select( - [ - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image', 'language_image'), - $db->quoteName('uc.name', 'editor'), - $db->quoteName('ag.title', 'access_level'), - $db->quoteName('c.title', 'category_title'), - ] - ) - ->from($db->quoteName('#__newsfeeds', 'a')) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')) - ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) - ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')); - - // Join over the associations. - if (Associations::isEnabled()) - { - $subQuery = $db->getQuery(true) - ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') - ->from($db->quoteName('#__associations', 'asso1')) - ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) - ->where( - [ - $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), - $db->quoteName('asso1.context') . ' = ' . $db->quote('com_newsfeeds.item'), - ] - ); - - $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); - } - - // Filter by access level. - if ($access = (int) $this->getState('filter.access')) - { - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - - // Implement View Level Access - if (!$user->authorise('core.admin')) - { - $query->whereIn($db->quoteName('a.access'), $user->getAuthorisedViewLevels()); - } - - // Filter by published state. - $published = (string) $this->getState('filter.published'); - - if (is_numeric($published)) - { - $published = (int) $published; - $query->where($db->quoteName('a.published') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->where($db->quoteName('a.published') . ' IN (0, 1)'); - } - - // Filter by category. - $categoryId = $this->getState('filter.category_id'); - - if (is_numeric($categoryId)) - { - $categoryId = (int) $categoryId; - $query->where($db->quoteName('a.catid') . ' = :categoryId') - ->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } - - // Filter on the level. - if ($level = (int) $this->getState('filter.level')) - { - $query->where($db->quoteName('c.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } - - // Filter by search in title - if ($search = $this->getState('filter.search')) - { - if (stripos($search, 'id:') === 0) - { - $search = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :search') - ->bind(':search', $search, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where('(' . $db->quoteName('a.name') . ' LIKE :search1 OR ' . $db->quoteName('a.alias') . ' LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('a.language') . ' = :language') - ->bind(':language', $language); - } - - // Filter by a single or group of tags. - $tag = $this->getState('filter.tag'); - - // Run simplified query when filtering by one tag. - if (\is_array($tag) && \count($tag) === 1) - { - $tag = $tag[0]; - } - - if ($tag && \is_array($tag)) - { - $tag = ArrayHelper::toInteger($tag); - - $subQuery = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('content_item_id')) - ->from($db->quoteName('#__contentitem_tag_map')) - ->where( - [ - $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', - $db->quoteName('type_alias') . ' = ' . $db->quote('com_newsfeeds.newsfeed'), - ] - ); - - $query->join( - 'INNER', - '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ); - } - elseif ($tag = (int) $tag) - { - $query->join( - 'INNER', - $db->quoteName('#__contentitem_tag_map', 'tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ) - ->where( - [ - $db->quoteName('tagmap.tag_id') . ' = :tag', - $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_newsfeeds.newsfeed'), - ] - ) - ->bind(':tag', $tag, ParameterType::INTEGER); - } - - // Add the list ordering clause. - $orderCol = $this->state->get('list.ordering', 'a.name'); - $orderDirn = $this->state->get('list.direction', 'ASC'); - - if ($orderCol == 'a.ordering' || $orderCol == 'category_title') - { - $ordering = [ - $db->quoteName('c.title') . ' ' . $db->escape($orderDirn), - $db->quoteName('a.ordering') . ' ' . $db->escape($orderDirn), - ]; - } - else - { - $ordering = $db->escape($orderCol) . ' ' . $db->escape($orderDirn); - } - - $query->order($ordering); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'alias', 'a.alias', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'catid', 'a.catid', 'category_id', 'category_title', + 'published', 'a.published', + 'access', 'a.access', 'access_level', + 'created', 'a.created', + 'created_by', 'a.created_by', + 'ordering', 'a.ordering', + 'language', 'a.language', 'language_title', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'cache_time', 'a.cache_time', + 'numarticles', + 'tag', + 'level', 'c.level', + 'tag', + ); + + if (Associations::isEnabled()) { + $config['filter_fields'][] = 'association'; + } + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.name', $direction = 'asc') + { + $app = Factory::getApplication(); + + $forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd'); + + // Adjust the context to support modal layouts. + if ($layout = $app->input->get('layout')) { + $this->context .= '.' . $layout; + } + + // Adjust the context to support forced languages. + if ($forcedLanguage) { + $this->context .= '.' . $forcedLanguage; + } + + // Load the parameters. + $params = ComponentHelper::getParams('com_newsfeeds'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + + // Force a language. + if (!empty($forcedLanguage)) { + $this->setState('filter.language', $forcedLanguage); + } + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.category_id'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.language'); + $id .= ':' . $this->getState('filter.level'); + $id .= ':' . serialize($this->getState('filter.tag')); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.name'), + $db->quoteName('a.alias'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.catid'), + $db->quoteName('a.numarticles'), + $db->quoteName('a.cache_time'), + $db->quoteName('a.created_by'), + $db->quoteName('a.published'), + $db->quoteName('a.access'), + $db->quoteName('a.ordering'), + $db->quoteName('a.language'), + $db->quoteName('a.publish_up'), + $db->quoteName('a.publish_down'), + ] + ) + ) + ->select( + [ + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image', 'language_image'), + $db->quoteName('uc.name', 'editor'), + $db->quoteName('ag.title', 'access_level'), + $db->quoteName('c.title', 'category_title'), + ] + ) + ->from($db->quoteName('#__newsfeeds', 'a')) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')) + ->join('LEFT', $db->quoteName('#__viewlevels', 'ag'), $db->quoteName('ag.id') . ' = ' . $db->quoteName('a.access')) + ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')); + + // Join over the associations. + if (Associations::isEnabled()) { + $subQuery = $db->getQuery(true) + ->select('COUNT(' . $db->quoteName('asso1.id') . ') > 1') + ->from($db->quoteName('#__associations', 'asso1')) + ->join('INNER', $db->quoteName('#__associations', 'asso2'), $db->quoteName('asso1.key') . ' = ' . $db->quoteName('asso2.key')) + ->where( + [ + $db->quoteName('asso1.id') . ' = ' . $db->quoteName('a.id'), + $db->quoteName('asso1.context') . ' = ' . $db->quote('com_newsfeeds.item'), + ] + ); + + $query->select('(' . $subQuery . ') AS ' . $db->quoteName('association')); + } + + // Filter by access level. + if ($access = (int) $this->getState('filter.access')) { + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + + // Implement View Level Access + if (!$user->authorise('core.admin')) { + $query->whereIn($db->quoteName('a.access'), $user->getAuthorisedViewLevels()); + } + + // Filter by published state. + $published = (string) $this->getState('filter.published'); + + if (is_numeric($published)) { + $published = (int) $published; + $query->where($db->quoteName('a.published') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->where($db->quoteName('a.published') . ' IN (0, 1)'); + } + + // Filter by category. + $categoryId = $this->getState('filter.category_id'); + + if (is_numeric($categoryId)) { + $categoryId = (int) $categoryId; + $query->where($db->quoteName('a.catid') . ' = :categoryId') + ->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } + + // Filter on the level. + if ($level = (int) $this->getState('filter.level')) { + $query->where($db->quoteName('c.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } + + // Filter by search in title + if ($search = $this->getState('filter.search')) { + if (stripos($search, 'id:') === 0) { + $search = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :search') + ->bind(':search', $search, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where('(' . $db->quoteName('a.name') . ' LIKE :search1 OR ' . $db->quoteName('a.alias') . ' LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('a.language') . ' = :language') + ->bind(':language', $language); + } + + // Filter by a single or group of tags. + $tag = $this->getState('filter.tag'); + + // Run simplified query when filtering by one tag. + if (\is_array($tag) && \count($tag) === 1) { + $tag = $tag[0]; + } + + if ($tag && \is_array($tag)) { + $tag = ArrayHelper::toInteger($tag); + + $subQuery = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('content_item_id')) + ->from($db->quoteName('#__contentitem_tag_map')) + ->where( + [ + $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tag)) . ')', + $db->quoteName('type_alias') . ' = ' . $db->quote('com_newsfeeds.newsfeed'), + ] + ); + + $query->join( + 'INNER', + '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ); + } elseif ($tag = (int) $tag) { + $query->join( + 'INNER', + $db->quoteName('#__contentitem_tag_map', 'tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ) + ->where( + [ + $db->quoteName('tagmap.tag_id') . ' = :tag', + $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_newsfeeds.newsfeed'), + ] + ) + ->bind(':tag', $tag, ParameterType::INTEGER); + } + + // Add the list ordering clause. + $orderCol = $this->state->get('list.ordering', 'a.name'); + $orderDirn = $this->state->get('list.direction', 'ASC'); + + if ($orderCol == 'a.ordering' || $orderCol == 'category_title') { + $ordering = [ + $db->quoteName('c.title') . ' ' . $db->escape($orderDirn), + $db->quoteName('a.ordering') . ' ' . $db->escape($orderDirn), + ]; + } else { + $ordering = $db->escape($orderCol) . ' ' . $db->escape($orderDirn); + } + + $query->order($ordering); + + return $query; + } } diff --git a/administrator/components/com_newsfeeds/src/Service/HTML/AdministratorService.php b/administrator/components/com_newsfeeds/src/Service/HTML/AdministratorService.php index 0b620ba53c22a..aaa5999f674d1 100644 --- a/administrator/components/com_newsfeeds/src/Service/HTML/AdministratorService.php +++ b/administrator/components/com_newsfeeds/src/Service/HTML/AdministratorService.php @@ -1,4 +1,5 @@ $associated) - { - $associations[$tag] = (int) $associated->id; - } + // Get the associations + if ($associations = Associations::getAssociations('com_newsfeeds', '#__newsfeeds', 'com_newsfeeds.item', $newsfeedid)) { + foreach ($associations as $tag => $associated) { + $associations[$tag] = (int) $associated->id; + } - // Get the associated newsfeed items - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $query - ->select( - [ - $db->quoteName('c.id'), - $db->quoteName('c.name', 'title'), - $db->quoteName('cat.title', 'category_title'), - $db->quoteName('l.sef', 'lang_sef'), - $db->quoteName('l.lang_code'), - $db->quoteName('l.image'), - $db->quoteName('l.title', 'language_title'), - ] - ) - ->from($db->quoteName('#__newsfeeds', 'c')) - ->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('cat.id') . ' = ' . $db->quoteName('c.catid')) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code')) - ->where( - [ - $db->quoteName('c.id') . ' IN (' . implode(',', $query->bindArray(array_values($associations))) . ')', - $db->quoteName('c.id') . ' != :id', - ] - ) - ->bind(':id', $newsfeedid, ParameterType::INTEGER); - $db->setQuery($query); + // Get the associated newsfeed items + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $query + ->select( + [ + $db->quoteName('c.id'), + $db->quoteName('c.name', 'title'), + $db->quoteName('cat.title', 'category_title'), + $db->quoteName('l.sef', 'lang_sef'), + $db->quoteName('l.lang_code'), + $db->quoteName('l.image'), + $db->quoteName('l.title', 'language_title'), + ] + ) + ->from($db->quoteName('#__newsfeeds', 'c')) + ->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('cat.id') . ' = ' . $db->quoteName('c.catid')) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('c.language') . ' = ' . $db->quoteName('l.lang_code')) + ->where( + [ + $db->quoteName('c.id') . ' IN (' . implode(',', $query->bindArray(array_values($associations))) . ')', + $db->quoteName('c.id') . ' != :id', + ] + ) + ->bind(':id', $newsfeedid, ParameterType::INTEGER); + $db->setQuery($query); - try - { - $items = $db->loadObjectList('id'); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500); - } + try { + $items = $db->loadObjectList('id'); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500); + } - if ($items) - { - $languages = LanguageHelper::getContentLanguages(array(0, 1)); - $content_languages = array_column($languages, 'lang_code'); + if ($items) { + $languages = LanguageHelper::getContentLanguages(array(0, 1)); + $content_languages = array_column($languages, 'lang_code'); - foreach ($items as &$item) - { - if (in_array($item->lang_code, $content_languages)) - { - $text = $item->lang_code; - $url = Route::_('index.php?option=com_newsfeeds&task=newsfeed.edit&id=' . (int) $item->id); - $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
' - . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
' . Text::sprintf('JCATEGORY_SPRINTF', $item->category_title); - $classes = 'badge bg-secondary'; + foreach ($items as &$item) { + if (in_array($item->lang_code, $content_languages)) { + $text = $item->lang_code; + $url = Route::_('index.php?option=com_newsfeeds&task=newsfeed.edit&id=' . (int) $item->id); + $tooltip = '' . htmlspecialchars($item->language_title, ENT_QUOTES, 'UTF-8') . '
' + . htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') . '
' . Text::sprintf('JCATEGORY_SPRINTF', $item->category_title); + $classes = 'badge bg-secondary'; - $item->link = '' . $text . '' - . ''; - } - else - { - // Display warning if Content Language is trashed or deleted - Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); - } - } - } + $item->link = '' . $text . '' + . ''; + } else { + // Display warning if Content Language is trashed or deleted + Factory::getApplication()->enqueueMessage(Text::sprintf('JGLOBAL_ASSOCIATIONS_CONTENTLANGUAGE_WARNING', $item->lang_code), 'warning'); + } + } + } - $html = LayoutHelper::render('joomla.content.associations', $items); - } + $html = LayoutHelper::render('joomla.content.associations', $items); + } - return $html; - } + return $html; + } } diff --git a/administrator/components/com_newsfeeds/src/Table/NewsfeedTable.php b/administrator/components/com_newsfeeds/src/Table/NewsfeedTable.php index 6d54fe1acf1a5..915229d3ee494 100644 --- a/administrator/components/com_newsfeeds/src/Table/NewsfeedTable.php +++ b/administrator/components/com_newsfeeds/src/Table/NewsfeedTable.php @@ -1,4 +1,5 @@ typeAlias = 'com_newsfeeds.newsfeed'; - parent::__construct('#__newsfeeds', 'id', $db); - $this->setColumnAlias('title', 'name'); - } - - /** - * Overloaded check method to ensure data integrity. - * - * @return boolean True on success. - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Check for valid name. - if (trim($this->name) == '') - { - $this->setError(Text::_('COM_NEWSFEEDS_WARNING_PROVIDE_VALID_NAME')); - - return false; - } - - if (empty($this->alias)) - { - $this->alias = $this->name; - } - - $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); - - if (trim(str_replace('-', '', $this->alias)) == '') - { - $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); - } - - // Check for a valid category. - if (!$this->catid = (int) $this->catid) - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_CATEGORY_REQUIRED')); - - return false; - } - - // Check the publish down date is not earlier than publish up. - if ((int) $this->publish_down > 0 && $this->publish_down < $this->publish_up) - { - $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); - - return false; - } - - // Clean up description -- eliminate quotes and <> brackets - if (!empty($this->metadesc)) - { - // Only process if not empty - $bad_characters = array("\"", '<', '>'); - $this->metadesc = StringHelper::str_ireplace($bad_characters, '', $this->metadesc); - } - - if (is_null($this->hits)) - { - $this->hits = 0; - } - - return true; - } - - /** - * Overridden \JTable::store to set modified data. - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function store($updateNulls = true) - { - $date = Factory::getDate(); - $user = Factory::getUser(); - - // Set created date if not set. - if (!(int) $this->created) - { - $this->created = $date->toSql(); - } - - if ($this->id) - { - // Existing item - $this->modified_by = $user->get('id'); - $this->modified = $date->toSql(); - } - else - { - // Field created_by can be set by the user, so we don't touch it if it's set. - if (empty($this->created_by)) - { - $this->created_by = $user->get('id'); - } - - if (!(int) $this->modified) - { - $this->modified = $this->created; - } - - if (empty($this->modified_by)) - { - $this->modified_by = $this->created_by; - } - } - - // Set publish_up, publish_down to null if not set - if (!$this->publish_up) - { - $this->publish_up = null; - } - - if (!$this->publish_down) - { - $this->publish_down = null; - } - - // Verify that the alias is unique - $table = Table::getInstance('NewsfeedTable', __NAMESPACE__ . '\\', array('dbo' => $this->_db)); - - if ($table->load(array('alias' => $this->alias, 'catid' => $this->catid)) && ($table->id != $this->id || $this->id == 0)) - { - $this->setError(Text::_('COM_NEWSFEEDS_ERROR_UNIQUE_ALIAS')); - - return false; - } - - // Save links as punycode. - $this->link = PunycodeHelper::urlToPunycode($this->link); - - return parent::store($updateNulls); - } - - /** - * Get the type alias for the history table - * - * @return string The alias as described above - * - * @since 4.0.0 - */ - public function getTypeAlias() - { - return $this->typeAlias; - } + use TaggableTableTrait; + + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Ensure the params, metadata and images are json encoded in the bind method + * + * @var array + * @since 3.3 + */ + protected $_jsonEncode = array('params', 'metadata', 'images'); + + /** + * Constructor + * + * @param DatabaseDriver $db A database connector object + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = 'com_newsfeeds.newsfeed'; + parent::__construct('#__newsfeeds', 'id', $db); + $this->setColumnAlias('title', 'name'); + } + + /** + * Overloaded check method to ensure data integrity. + * + * @return boolean True on success. + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Check for valid name. + if (trim($this->name) == '') { + $this->setError(Text::_('COM_NEWSFEEDS_WARNING_PROVIDE_VALID_NAME')); + + return false; + } + + if (empty($this->alias)) { + $this->alias = $this->name; + } + + $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); + + if (trim(str_replace('-', '', $this->alias)) == '') { + $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); + } + + // Check for a valid category. + if (!$this->catid = (int) $this->catid) { + $this->setError(Text::_('JLIB_DATABASE_ERROR_CATEGORY_REQUIRED')); + + return false; + } + + // Check the publish down date is not earlier than publish up. + if ((int) $this->publish_down > 0 && $this->publish_down < $this->publish_up) { + $this->setError(Text::_('JGLOBAL_START_PUBLISH_AFTER_FINISH')); + + return false; + } + + // Clean up description -- eliminate quotes and <> brackets + if (!empty($this->metadesc)) { + // Only process if not empty + $bad_characters = array("\"", '<', '>'); + $this->metadesc = StringHelper::str_ireplace($bad_characters, '', $this->metadesc); + } + + if (is_null($this->hits)) { + $this->hits = 0; + } + + return true; + } + + /** + * Overridden \JTable::store to set modified data. + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function store($updateNulls = true) + { + $date = Factory::getDate(); + $user = Factory::getUser(); + + // Set created date if not set. + if (!(int) $this->created) { + $this->created = $date->toSql(); + } + + if ($this->id) { + // Existing item + $this->modified_by = $user->get('id'); + $this->modified = $date->toSql(); + } else { + // Field created_by can be set by the user, so we don't touch it if it's set. + if (empty($this->created_by)) { + $this->created_by = $user->get('id'); + } + + if (!(int) $this->modified) { + $this->modified = $this->created; + } + + if (empty($this->modified_by)) { + $this->modified_by = $this->created_by; + } + } + + // Set publish_up, publish_down to null if not set + if (!$this->publish_up) { + $this->publish_up = null; + } + + if (!$this->publish_down) { + $this->publish_down = null; + } + + // Verify that the alias is unique + $table = Table::getInstance('NewsfeedTable', __NAMESPACE__ . '\\', array('dbo' => $this->_db)); + + if ($table->load(array('alias' => $this->alias, 'catid' => $this->catid)) && ($table->id != $this->id || $this->id == 0)) { + $this->setError(Text::_('COM_NEWSFEEDS_ERROR_UNIQUE_ALIAS')); + + return false; + } + + // Save links as punycode. + $this->link = PunycodeHelper::urlToPunycode($this->link); + + return parent::store($updateNulls); + } + + /** + * Get the type alias for the history table + * + * @return string The alias as described above + * + * @since 4.0.0 + */ + public function getTypeAlias() + { + return $this->typeAlias; + } } diff --git a/administrator/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php b/administrator/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php index 612996ed2182a..1b0385e5c8b7b 100644 --- a/administrator/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php +++ b/administrator/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->item = $this->get('Item'); - $this->form = $this->get('Form'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // If we are forcing a language in modal (used for associations). - if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) - { - // Set the language field to the forcedLanguage and disable changing it. - $this->form->setValue('language', null, $forcedLanguage); - $this->form->setFieldAttribute('language', 'readonly', 'true'); - - // Only allow to select categories with All language or with the forced language. - $this->form->setFieldAttribute('catid', 'language', '*,' . $forcedLanguage); - - // Only allow to select tags with All language or with the forced language. - $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = $this->getCurrentUser(); - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); - - // Since we don't track these assets at the item level, use the category id. - $canDo = ContentHelper::getActions('com_newsfeeds', 'category', $this->item->catid); - - $title = $isNew ? Text::_('COM_NEWSFEEDS_MANAGER_NEWSFEED_NEW') : Text::_('COM_NEWSFEEDS_MANAGER_NEWSFEED_EDIT'); - ToolbarHelper::title($title, 'rss newsfeeds'); - - $toolbarButtons = []; - - // If not checked out, can save the item. - if (!$checkedOut && ($canDo->get('core.edit') || count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0)) - { - ToolbarHelper::apply('newsfeed.apply'); - - $toolbarButtons[] = ['save', 'newsfeed.save']; - } - - if (!$checkedOut && count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0) - { - $toolbarButtons[] = ['save2new', 'newsfeed.save2new']; - } - - // If an existing item, can save to a copy. - if (!$isNew && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'newsfeed.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('newsfeed.cancel'); - } - else - { - ToolbarHelper::cancel('newsfeed.cancel', 'JTOOLBAR_CLOSE'); - - if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) - { - ToolbarHelper::versions('com_newsfeeds.newsfeed', $this->item->id); - } - } - - if (!$isNew && Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) - { - ToolbarHelper::custom('newsfeed.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('News_Feeds:_New_or_Edit'); - } + /** + * The item object for the newsfeed + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 1.6 + */ + protected $item; + + /** + * The form object for the newsfeed + * + * @var \Joomla\CMS\Form\Form + * + * @since 1.6 + */ + protected $form; + + /** + * The model state of the newsfeed + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 1.6 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->item = $this->get('Item'); + $this->form = $this->get('Form'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // If we are forcing a language in modal (used for associations). + if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) { + // Set the language field to the forcedLanguage and disable changing it. + $this->form->setValue('language', null, $forcedLanguage); + $this->form->setFieldAttribute('language', 'readonly', 'true'); + + // Only allow to select categories with All language or with the forced language. + $this->form->setFieldAttribute('catid', 'language', '*,' . $forcedLanguage); + + // Only allow to select tags with All language or with the forced language. + $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); + + // Since we don't track these assets at the item level, use the category id. + $canDo = ContentHelper::getActions('com_newsfeeds', 'category', $this->item->catid); + + $title = $isNew ? Text::_('COM_NEWSFEEDS_MANAGER_NEWSFEED_NEW') : Text::_('COM_NEWSFEEDS_MANAGER_NEWSFEED_EDIT'); + ToolbarHelper::title($title, 'rss newsfeeds'); + + $toolbarButtons = []; + + // If not checked out, can save the item. + if (!$checkedOut && ($canDo->get('core.edit') || count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0)) { + ToolbarHelper::apply('newsfeed.apply'); + + $toolbarButtons[] = ['save', 'newsfeed.save']; + } + + if (!$checkedOut && count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0) { + $toolbarButtons[] = ['save2new', 'newsfeed.save2new']; + } + + // If an existing item, can save to a copy. + if (!$isNew && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'newsfeed.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('newsfeed.cancel'); + } else { + ToolbarHelper::cancel('newsfeed.cancel', 'JTOOLBAR_CLOSE'); + + if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) { + ToolbarHelper::versions('com_newsfeeds.newsfeed', $this->item->id); + } + } + + if (!$isNew && Associations::isEnabled() && ComponentHelper::isEnabled('com_associations')) { + ToolbarHelper::custom('newsfeed.editAssociations', 'contract', '', 'JTOOLBAR_ASSOCIATIONS', false, false); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('News_Feeds:_New_or_Edit'); + } } diff --git a/administrator/components/com_newsfeeds/src/View/Newsfeeds/HtmlView.php b/administrator/components/com_newsfeeds/src/View/Newsfeeds/HtmlView.php index 0fbe696d4f476..4219a2cff9122 100644 --- a/administrator/components/com_newsfeeds/src/View/Newsfeeds/HtmlView.php +++ b/administrator/components/com_newsfeeds/src/View/Newsfeeds/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // We don't need toolbar in the modal layout. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - else - { - // In article associations modal we need to remove language filter if forcing a language. - // We also need to change the category filter to show show categories with All or the forced language. - if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) - { - // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. - $languageXml = new \SimpleXMLElement(''); - $this->filterForm->setField($languageXml, 'filter', true); - - // Also, unset the active language filter so the search tools is not open by default with this filter. - unset($this->activeFilters['language']); - - // One last changes needed is to change the category filter to just show categories with All language or with the forced language. - $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); - } - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $state = $this->get('State'); - $canDo = ContentHelper::getActions('com_newsfeeds', 'category', $state->get('filter.category_id')); - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_NEWSFEEDS_MANAGER_NEWSFEEDS'), 'rss newsfeeds'); - - if ($canDo->get('core.create') || count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0) - { - $toolbar->addNew('newsfeed.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('newsfeeds.publish')->listCheck(true); - $childBar->unpublish('newsfeeds.unpublish')->listCheck(true); - $childBar->archive('newsfeeds.archive')->listCheck(true); - - if ($user->authorise('core.admin')) - { - $childBar->checkin('newsfeeds.checkin')->listCheck(true); - } - - if ($this->state->get('filter.published') != -2) - { - $childBar->trash('newsfeeds.trash')->listCheck(true); - } - - // Add a batch button - if ($user->authorise('core.create', 'com_newsfeeds') - && $user->authorise('core.edit', 'com_newsfeeds') - && $user->authorise('core.edit.state', 'com_newsfeeds')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if (!$this->isEmptyState && $state->get('filter.published') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('newsfeeds.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($user->authorise('core.admin', 'com_newsfeeds') || $user->authorise('core.options', 'com_newsfeeds')) - { - $toolbar->preferences('com_newsfeeds'); - } - - $toolbar->help('News_Feeds'); - } + /** + * The list of newsfeeds + * + * @var CMSObject + * + * @since 1.6 + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 1.6 + */ + protected $pagination; + + /** + * The model state + * + * @var CMSObject + * + * @since 1.6 + */ + protected $state; + + /** + * Is this view an Empty State + * + * @var boolean + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // We don't need toolbar in the modal layout. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } else { + // In article associations modal we need to remove language filter if forcing a language. + // We also need to change the category filter to show show categories with All or the forced language. + if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) { + // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. + $languageXml = new \SimpleXMLElement(''); + $this->filterForm->setField($languageXml, 'filter', true); + + // Also, unset the active language filter so the search tools is not open by default with this filter. + unset($this->activeFilters['language']); + + // One last changes needed is to change the category filter to just show categories with All language or with the forced language. + $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); + } + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $state = $this->get('State'); + $canDo = ContentHelper::getActions('com_newsfeeds', 'category', $state->get('filter.category_id')); + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_NEWSFEEDS_MANAGER_NEWSFEEDS'), 'rss newsfeeds'); + + if ($canDo->get('core.create') || count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0) { + $toolbar->addNew('newsfeed.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('newsfeeds.publish')->listCheck(true); + $childBar->unpublish('newsfeeds.unpublish')->listCheck(true); + $childBar->archive('newsfeeds.archive')->listCheck(true); + + if ($user->authorise('core.admin')) { + $childBar->checkin('newsfeeds.checkin')->listCheck(true); + } + + if ($this->state->get('filter.published') != -2) { + $childBar->trash('newsfeeds.trash')->listCheck(true); + } + + // Add a batch button + if ( + $user->authorise('core.create', 'com_newsfeeds') + && $user->authorise('core.edit', 'com_newsfeeds') + && $user->authorise('core.edit.state', 'com_newsfeeds') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if (!$this->isEmptyState && $state->get('filter.published') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('newsfeeds.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($user->authorise('core.admin', 'com_newsfeeds') || $user->authorise('core.options', 'com_newsfeeds')) { + $toolbar->preferences('com_newsfeeds'); + } + + $toolbar->help('News_Feeds'); + } } diff --git a/administrator/components/com_newsfeeds/tmpl/newsfeed/edit.php b/administrator/components/com_newsfeeds/tmpl/newsfeed/edit.php index 34dcd86d2be5b..dd09e694e8368 100644 --- a/administrator/components/com_newsfeeds/tmpl/newsfeed/edit.php +++ b/administrator/components/com_newsfeeds/tmpl/newsfeed/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $app = Factory::getApplication(); $input = $app->input; @@ -38,82 +39,82 @@ - - -
- 'details', 'recall' => true, 'breakpoint' => 768]); ?> - - item->id) ? Text::_('COM_NEWSFEEDS_NEW_NEWSFEED') : Text::_('COM_NEWSFEEDS_EDIT_NEWSFEED')); ?> -
-
-
- form->renderField('link'); ?> - form->renderField('description'); ?> -
-
-
- -
-
- - - -
-
-
- -
- form->getGroup('images') as $field) : ?> - renderField(); ?> - -
-
-
-
- loadTemplate('display'); ?> -
-
- - - - - -
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
- - - - -
- -
- -
-
- - - - - - -
- - - + + +
+ 'details', 'recall' => true, 'breakpoint' => 768]); ?> + + item->id) ? Text::_('COM_NEWSFEEDS_NEW_NEWSFEED') : Text::_('COM_NEWSFEEDS_EDIT_NEWSFEED')); ?> +
+
+
+ form->renderField('link'); ?> + form->renderField('description'); ?> +
+
+
+ +
+
+ + + +
+
+
+ +
+ form->getGroup('images') as $field) : ?> + renderField(); ?> + +
+
+
+
+ loadTemplate('display'); ?> +
+
+ + + + + +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+ + + + +
+ +
+ +
+
+ + + + + + +
+ + + diff --git a/administrator/components/com_newsfeeds/tmpl/newsfeed/edit_display.php b/administrator/components/com_newsfeeds/tmpl/newsfeed/edit_display.php index 9ba2e1a89b48f..889b0677275ba 100644 --- a/administrator/components/com_newsfeeds/tmpl/newsfeed/edit_display.php +++ b/administrator/components/com_newsfeeds/tmpl/newsfeed/edit_display.php @@ -1,4 +1,5 @@
- -
- -
+ +
+ +
diff --git a/administrator/components/com_newsfeeds/tmpl/newsfeed/modal.php b/administrator/components/com_newsfeeds/tmpl/newsfeed/modal.php index fcf153534e31c..91f2a029c98ee 100644 --- a/administrator/components/com_newsfeeds/tmpl/newsfeed/modal.php +++ b/administrator/components/com_newsfeeds/tmpl/newsfeed/modal.php @@ -1,4 +1,5 @@
- setLayout('edit'); ?> - loadTemplate(); ?> + setLayout('edit'); ?> + loadTemplate(); ?>
diff --git a/administrator/components/com_newsfeeds/tmpl/newsfeeds/default.php b/administrator/components/com_newsfeeds/tmpl/newsfeeds/default.php index 7611fe76a8789..8fa3553429ed1 100644 --- a/administrator/components/com_newsfeeds/tmpl/newsfeeds/default.php +++ b/administrator/components/com_newsfeeds/tmpl/newsfeeds/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); @@ -29,172 +30,172 @@ $saveOrder = $listOrder == 'a.ordering'; $assoc = Associations::isEnabled(); -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_newsfeeds&task=newsfeeds.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_newsfeeds&task=newsfeeds.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
-
-
-
- $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items as $i => $item) : - $ordering = ($listOrder == 'a.ordering'); - $canCreate = $user->authorise('core.create', 'com_newsfeeds.category.' . $item->catid); - $canEdit = $user->authorise('core.edit', 'com_newsfeeds.category.' . $item->catid); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); - $canEditOwn = $user->authorise('core.edit.own', 'com_newsfeeds.category.' . $item->catid) && $item->created_by == $user->id; - $canChange = $user->authorise('core.edit.state', 'com_newsfeeds.category.' . $item->catid) && $canCheckin; - ?> - - - - - - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - - - - - -
- id, false, 'cid', 'cb', $item->name); ?> - - - - - - - - - - published, $i, 'newsfeeds.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> - -
- checked_out) : ?> - editor, $item->checked_out_time, 'newsfeeds.', $canCheckin); ?> - - - - escape($item->name); ?> - - escape($item->name); ?> - -
- escape($item->alias)); ?> -
-
- escape($item->category_title); ?> -
-
-
- escape($item->access_level); ?> - - numarticles; ?> - - cache_time; ?> - - association) : ?> - id); ?> - - - - - id; ?> -
+
+
+
+ $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items as $i => $item) : + $ordering = ($listOrder == 'a.ordering'); + $canCreate = $user->authorise('core.create', 'com_newsfeeds.category.' . $item->catid); + $canEdit = $user->authorise('core.edit', 'com_newsfeeds.category.' . $item->catid); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); + $canEditOwn = $user->authorise('core.edit.own', 'com_newsfeeds.category.' . $item->catid) && $item->created_by == $user->id; + $canChange = $user->authorise('core.edit.state', 'com_newsfeeds.category.' . $item->catid) && $canCheckin; + ?> + + + + + + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->name); ?> + + + + + + + + + + published, $i, 'newsfeeds.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> + +
+ checked_out) : ?> + editor, $item->checked_out_time, 'newsfeeds.', $canCheckin); ?> + + + + escape($item->name); ?> + + escape($item->name); ?> + +
+ escape($item->alias)); ?> +
+
+ escape($item->category_title); ?> +
+
+
+ escape($item->access_level); ?> + + numarticles; ?> + + cache_time; ?> + + association) : ?> + id); ?> + + + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', 'com_newsfeeds') - && $user->authorise('core.edit', 'com_newsfeeds') - && $user->authorise('core.edit.state', 'com_newsfeeds')) : ?> - Text::_('COM_NEWSFEEDS_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ), - $this->loadTemplate('batch_body') - ); ?> - - - - - -
-
-
+ + authorise('core.create', 'com_newsfeeds') + && $user->authorise('core.edit', 'com_newsfeeds') + && $user->authorise('core.edit.state', 'com_newsfeeds') +) : ?> + Text::_('COM_NEWSFEEDS_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ), + $this->loadTemplate('batch_body') + ); ?> + + + + + +
+
+
diff --git a/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_body.php b/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_body.php index 71e3729811e86..0578eea2932b0 100644 --- a/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_body.php +++ b/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Multilanguage; @@ -15,32 +17,32 @@ ?>
-
- -
-
- -
-
- -
-
- -
-
-
-
- = 0) : ?> -
-
- 'com_newsfeeds']); ?> -
-
- -
-
- -
-
-
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ = 0) : ?> +
+
+ 'com_newsfeeds']); ?> +
+
+ +
+
+ +
+
+
diff --git a/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_footer.php b/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_footer.php index b8394dfdf6527..f43ed8b7b5b8b 100644 --- a/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_footer.php +++ b/administrator/components/com_newsfeeds/tmpl/newsfeeds/default_batch_footer.php @@ -1,4 +1,5 @@ diff --git a/administrator/components/com_newsfeeds/tmpl/newsfeeds/emptystate.php b/administrator/components/com_newsfeeds/tmpl/newsfeeds/emptystate.php index 64c1fd1b22b52..234282c353b0e 100644 --- a/administrator/components/com_newsfeeds/tmpl/newsfeeds/emptystate.php +++ b/administrator/components/com_newsfeeds/tmpl/newsfeeds/emptystate.php @@ -1,4 +1,5 @@ 'COM_NEWSFEEDS', - 'formURL' => 'index.php?option=com_newsfeeds&view=newsfeeds', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:News_Feeds', - 'icon' => 'icon-rss newsfeeds', + 'textPrefix' => 'COM_NEWSFEEDS', + 'formURL' => 'index.php?option=com_newsfeeds&view=newsfeeds', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:News_Feeds', + 'icon' => 'icon-rss newsfeeds', ]; $user = Factory::getApplication()->getIdentity(); -if ($user->authorise('core.create', 'com_newsfeeds') || count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0) -{ - $displayData['createURL'] = 'index.php?option=com_newsfeeds&task=newsfeed.add'; +if ($user->authorise('core.create', 'com_newsfeeds') || count($user->getAuthorisedCategories('com_newsfeeds', 'core.create')) > 0) { + $displayData['createURL'] = 'index.php?option=com_newsfeeds&task=newsfeed.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_newsfeeds/tmpl/newsfeeds/modal.php b/administrator/components/com_newsfeeds/tmpl/newsfeeds/modal.php index 6ab5819394f2a..c99d6372b0724 100644 --- a/administrator/components/com_newsfeeds/tmpl/newsfeeds/modal.php +++ b/administrator/components/com_newsfeeds/tmpl/newsfeeds/modal.php @@ -1,4 +1,5 @@
-
+ - $this)); ?> + $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - 'icon-trash', - 0 => 'icon-times', - 1 => 'icon-check', - 2 => 'icon-folder', - ); - ?> - items as $i => $item) : ?> - language && $multilang) - { - $tag = strlen($item->language); - if ($tag == 5) - { - $lang = substr($item->language, 0, 2); - } - elseif ($tag == 6) - { - $lang = substr($item->language, 0, 3); - } - else { - $lang = ''; - } - } - elseif (!$multilang) - { - $lang = ''; - } - ?> - - - - - - - - - - - -
- , - , - -
- - - - - - - - - -
- - - - - - escape($item->name); ?> -
- escape($item->category_title); ?> -
-
- escape($item->access_level); ?> - - - - id; ?> -
+ items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + 'icon-trash', + 0 => 'icon-times', + 1 => 'icon-check', + 2 => 'icon-folder', + ); + ?> + items as $i => $item) : ?> + language && $multilang) { + $tag = strlen($item->language); + if ($tag == 5) { + $lang = substr($item->language, 0, 2); + } elseif ($tag == 6) { + $lang = substr($item->language, 0, 3); + } else { + $lang = ''; + } + } elseif (!$multilang) { + $lang = ''; + } + ?> + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + +
+ + + + + + escape($item->name); ?> +
+ escape($item->category_title); ?> +
+
+ escape($item->access_level); ?> + + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - - + + + + -
+
diff --git a/administrator/components/com_plugins/helpers/plugins.php b/administrator/components/com_plugins/helpers/plugins.php index a2da98baa77f8..81ef529218600 100644 --- a/administrator/components/com_plugins/helpers/plugins.php +++ b/administrator/components/com_plugins/helpers/plugins.php @@ -1,13 +1,16 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; + /** * Plugins component helper. @@ -18,5 +21,4 @@ */ class PluginsHelper extends \Joomla\Component\Plugins\Administrator\Helper\PluginsHelper { - } diff --git a/administrator/components/com_plugins/services/provider.php b/administrator/components/com_plugins/services/provider.php index a36ebdc941949..44576bab87204 100644 --- a/administrator/components/com_plugins/services/provider.php +++ b/administrator/components/com_plugins/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Plugins')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Plugins')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Plugins')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Plugins')); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_plugins/src/Controller/DisplayController.php b/administrator/components/com_plugins/src/Controller/DisplayController.php index bf6cbc9179085..5f3e173eb1e3d 100644 --- a/administrator/components/com_plugins/src/Controller/DisplayController.php +++ b/administrator/components/com_plugins/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'plugins'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('extension_id'); - - // Check for edit form. - if ($view == 'plugin' && $layout == 'edit' && !$this->checkEditId('com_plugins.edit.plugin', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_plugins&view=plugins', false)); - - return false; - } - - parent::display(); - } + /** + * The default view. + * + * @var string + * @since 1.6 + */ + protected $default_view = 'plugins'; + + /** + * Method to display a view. + * + * @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()}. + * + * @return static|boolean This object to support chaining or false on failure. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $view = $this->input->get('view', 'plugins'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('extension_id'); + + // Check for edit form. + if ($view == 'plugin' && $layout == 'edit' && !$this->checkEditId('com_plugins.edit.plugin', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_plugins&view=plugins', false)); + + return false; + } + + parent::display(); + } } diff --git a/administrator/components/com_plugins/src/Controller/PluginController.php b/administrator/components/com_plugins/src/Controller/PluginController.php index 8585648f24198..04f04cb0e49a8 100644 --- a/administrator/components/com_plugins/src/Controller/PluginController.php +++ b/administrator/components/com_plugins/src/Controller/PluginController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Plugins\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Plugins\Administrator\Controller; use Joomla\CMS\MVC\Controller\FormController; diff --git a/administrator/components/com_plugins/src/Controller/PluginsController.php b/administrator/components/com_plugins/src/Controller/PluginsController.php index aa0883e13569d..9c647466f15dc 100644 --- a/administrator/components/com_plugins/src/Controller/PluginsController.php +++ b/administrator/components/com_plugins/src/Controller/PluginsController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to get the number of activated plugins - * - * @return void - * - * @since 4.0.0 - */ - public function getQuickiconContent() - { - $model = $this->getModel('Plugins'); - - $model->setState('filter.enabled', 1); - - $amount = (int) $model->getTotal(); - - $result = []; - - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_PLUGINS_N_QUICKICON_SRONLY', $amount); - $result['name'] = Text::plural('COM_PLUGINS_N_QUICKICON', $amount); - - echo new JsonResponse($result); - } + /** + * 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 object The model. + * + * @since 1.6 + */ + public function getModel($name = 'Plugin', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to get the number of activated plugins + * + * @return void + * + * @since 4.0.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('Plugins'); + + $model->setState('filter.enabled', 1); + + $amount = (int) $model->getTotal(); + + $result = []; + + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_PLUGINS_N_QUICKICON_SRONLY', $amount); + $result['name'] = Text::plural('COM_PLUGINS_N_QUICKICON', $amount); + + echo new JsonResponse($result); + } } diff --git a/administrator/components/com_plugins/src/Field/PluginElementField.php b/administrator/components/com_plugins/src/Field/PluginElementField.php index 4f50bc7a0a48f..2b4f3d268bd9f 100644 --- a/administrator/components/com_plugins/src/Field/PluginElementField.php +++ b/administrator/components/com_plugins/src/Field/PluginElementField.php @@ -1,4 +1,5 @@ getDatabase(); - $folder = $this->form->getValue('folder'); + /** + * Builds the query for the ordering list. + * + * @return \Joomla\Database\DatabaseQuery The query for the ordering form field. + */ + protected function getQuery() + { + $db = $this->getDatabase(); + $folder = $this->form->getValue('folder'); - // Build the query for the ordering list. - $query = $db->getQuery(true) - ->select( - array( - $db->quoteName('ordering', 'value'), - $db->quoteName('name', 'text'), - $db->quoteName('type'), - $db->quote('folder'), - $db->quote('extension_id') - ) - ) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = :folder') - ->order($db->quoteName('ordering')) - ->bind(':folder', $folder); + // Build the query for the ordering list. + $query = $db->getQuery(true) + ->select( + array( + $db->quoteName('ordering', 'value'), + $db->quoteName('name', 'text'), + $db->quoteName('type'), + $db->quote('folder'), + $db->quote('extension_id') + ) + ) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = :folder') + ->order($db->quoteName('ordering')) + ->bind(':folder', $folder); - return $query; - } + return $query; + } - /** - * Retrieves the current Item's Id. - * - * @return integer The current item ID. - */ - protected function getItemId() - { - return (int) $this->form->getValue('extension_id'); - } + /** + * Retrieves the current Item's Id. + * + * @return integer The current item ID. + */ + protected function getItemId() + { + return (int) $this->form->getValue('extension_id'); + } } diff --git a/administrator/components/com_plugins/src/Helper/PluginsHelper.php b/administrator/components/com_plugins/src/Helper/PluginsHelper.php index 39bcf135b691c..56acb59239390 100644 --- a/administrator/components/com_plugins/src/Helper/PluginsHelper.php +++ b/administrator/components/com_plugins/src/Helper/PluginsHelper.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('DISTINCT(folder) AS value, folder AS text') - ->from('#__extensions') - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->order('folder'); - - $db->setQuery($query); - - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - return $options; - } - - /** - * Returns a list of elements filter options. - * - * @return string The HTML code for the select tag - */ - public static function elementOptions() - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('DISTINCT(element) AS value, element AS text') - ->from('#__extensions') - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->order('element'); - $db->setQuery($query); - - try - { - $options = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - - return $options; - } - - /** - * Parse the template file. - * - * @param string $templateBaseDir Base path to the template directory. - * @param string $templateDir Template directory. - * - * @return CMSObject|bool - */ - public function parseXMLTemplateFile($templateBaseDir, $templateDir) - { - $data = new CMSObject; - - // Check of the xml file exists. - $filePath = Path::clean($templateBaseDir . '/templates/' . $templateDir . '/templateDetails.xml'); - - if (is_file($filePath)) - { - $xml = Installer::parseXMLInstallFile($filePath); - - if ($xml['type'] != 'template') - { - return false; - } - - foreach ($xml as $key => $value) - { - $data->set($key, $value); - } - } - - return $data; - } + public static $extension = 'com_plugins'; + + /** + * Returns an array of standard published state filter options. + * + * @return array The HTML code for the select tag + */ + public static function publishedOptions() + { + // Build the active state filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '1', 'JENABLED'); + $options[] = HTMLHelper::_('select.option', '0', 'JDISABLED'); + + return $options; + } + + /** + * Returns a list of folders filter options. + * + * @return string The HTML code for the select tag + */ + public static function folderOptions() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('DISTINCT(folder) AS value, folder AS text') + ->from('#__extensions') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->order('folder'); + + $db->setQuery($query); + + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + return $options; + } + + /** + * Returns a list of elements filter options. + * + * @return string The HTML code for the select tag + */ + public static function elementOptions() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('DISTINCT(element) AS value, element AS text') + ->from('#__extensions') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->order('element'); + $db->setQuery($query); + + try { + $options = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + + return $options; + } + + /** + * Parse the template file. + * + * @param string $templateBaseDir Base path to the template directory. + * @param string $templateDir Template directory. + * + * @return CMSObject|bool + */ + public function parseXMLTemplateFile($templateBaseDir, $templateDir) + { + $data = new CMSObject(); + + // Check of the xml file exists. + $filePath = Path::clean($templateBaseDir . '/templates/' . $templateDir . '/templateDetails.xml'); + + if (is_file($filePath)) { + $xml = Installer::parseXMLInstallFile($filePath); + + if ($xml['type'] != 'template') { + return false; + } + + foreach ($xml as $key => $value) { + $data->set($key, $value); + } + } + + return $data; + } } diff --git a/administrator/components/com_plugins/src/Model/PluginModel.php b/administrator/components/com_plugins/src/Model/PluginModel.php index 834c2b077e4ca..73dce3b58a259 100644 --- a/administrator/components/com_plugins/src/Model/PluginModel.php +++ b/administrator/components/com_plugins/src/Model/PluginModel.php @@ -1,4 +1,5 @@ 'onExtensionAfterSave', - 'event_before_save' => 'onExtensionBeforeSave', - 'events_map' => array( - 'save' => 'extension' - ) - ), $config - ); - - parent::__construct($config, $factory); - } - - /** - * Method to get the record form. - * - * @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 Form|bool A Form object on success, false on failure. - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // The folder and element vars are passed when saving the form. - if (empty($data)) - { - $item = $this->getItem(); - $folder = $item->folder; - $element = $item->element; - } - else - { - $folder = ArrayHelper::getValue($data, 'folder', '', 'cmd'); - $element = ArrayHelper::getValue($data, 'element', '', 'cmd'); - } - - // Add the default fields directory - Form::addFieldPath(JPATH_PLUGINS . '/' . $folder . '/' . $element . '/field'); - - // These variables are used to add data from the plugin XML files. - $this->setState('item.folder', $folder); - $this->setState('item.element', $element); - - // Get the form. - $form = $this->loadForm('com_plugins.plugin', 'plugin', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Modify the form based on access controls. - if (!$this->canEditState((object) $data)) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('enabled', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('enabled', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_plugins.edit.plugin.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_plugins.plugin', $data); - - return $data; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - */ - public function getItem($pk = null) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('plugin.id'); - - $cacheId = $pk; - - if (\is_array($cacheId)) - { - $cacheId = serialize($cacheId); - } - - if (!isset($this->_cache[$cacheId])) - { - // Get a row instance. - $table = $this->getTable(); - - // Attempt to load the row. - $return = $table->load(\is_array($pk) ? $pk : ['extension_id' => $pk, 'type' => 'plugin']); - - // Check for a table object error. - if ($return === false) - { - return false; - } - - // Convert to the \Joomla\CMS\Object\CMSObject before adding other data. - $properties = $table->getProperties(1); - $this->_cache[$cacheId] = ArrayHelper::toObject($properties, CMSObject::class); - - // Convert the params field to an array. - $registry = new Registry($table->params); - $this->_cache[$cacheId]->params = $registry->toArray(); - - // Get the plugin XML. - $path = Path::clean(JPATH_PLUGINS . '/' . $table->folder . '/' . $table->element . '/' . $table->element . '.xml'); - - if (file_exists($path)) - { - $this->_cache[$cacheId]->xml = simplexml_load_file($path); - } - else - { - $this->_cache[$cacheId]->xml = null; - } - } - - return $this->_cache[$cacheId]; - } - - /** - * Returns a reference to the Table object, always creating it. - * - * @param string $type The table type to instantiate. - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A database object - */ - public function getTable($type = 'Extension', $prefix = 'JTable', $config = array()) - { - return Table::getInstance($type, $prefix, $config); - } - - /** - * Auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - */ - protected function populateState() - { - // Execute the parent method. - parent::populateState(); - - $app = Factory::getApplication(); - - // Load the User state. - $pk = $app->input->getInt('extension_id'); - $this->setState('plugin.id', $pk); - } - - /** - * Preprocess the form. - * - * @param Form $form A form object. - * @param mixed $data The data expected for the form. - * @param string $group Cache group name. - * - * @return mixed True if successful. - * - * @since 1.6 - * - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $folder = $this->getState('item.folder'); - $element = $this->getState('item.element'); - $lang = Factory::getLanguage(); - - // Load the core and/or local language sys file(s) for the ordering field. - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('element')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = :folder') - ->bind(':folder', $folder); - $db->setQuery($query); - $elements = $db->loadColumn(); - - foreach ($elements as $elementa) - { - $lang->load('plg_' . $folder . '_' . $elementa . '.sys', JPATH_ADMINISTRATOR) - || $lang->load('plg_' . $folder . '_' . $elementa . '.sys', JPATH_PLUGINS . '/' . $folder . '/' . $elementa); - } - - if (empty($folder) || empty($element)) - { - $app = Factory::getApplication(); - $app->redirect(Route::_('index.php?option=com_plugins&view=plugins', false)); - } - - $formFile = Path::clean(JPATH_PLUGINS . '/' . $folder . '/' . $element . '/' . $element . '.xml'); - - if (!file_exists($formFile)) - { - throw new \Exception(Text::sprintf('COM_PLUGINS_ERROR_FILE_NOT_FOUND', $element . '.xml')); - } - - // Load the core and/or local language file(s). - $lang->load('plg_' . $folder . '_' . $element, JPATH_ADMINISTRATOR) - || $lang->load('plg_' . $folder . '_' . $element, JPATH_PLUGINS . '/' . $folder . '/' . $element); - - if (file_exists($formFile)) - { - // Get the plugin form. - if (!$form->loadFile($formFile, false, '//config')) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - } - - // Attempt to load the xml file. - if (!$xml = simplexml_load_file($formFile)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Get the help data from the XML file if present. - $help = $xml->xpath('/extension/help'); - - if (!empty($help)) - { - $helpKey = trim((string) $help[0]['key']); - $helpURL = trim((string) $help[0]['url']); - - $this->helpKey = $helpKey ?: $this->helpKey; - $this->helpURL = $helpURL ?: $this->helpURL; - } - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param object $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 1.6 - */ - protected function getReorderConditions($table) - { - $db = $this->getDatabase(); - - return [ - $db->quoteName('type') . ' = ' . $db->quote($table->type), - $db->quoteName('folder') . ' = ' . $db->quote($table->folder), - ]; - } - - /** - * Override method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - // Setup type. - $data['type'] = 'plugin'; - - return parent::save($data); - } - - /** - * Get the necessary data to load an item help screen. - * - * @return object An object with key, url, and local properties for loading the item help screen. - * - * @since 1.6 - */ - public function getHelp() - { - return (object) array('key' => $this->helpKey, 'url' => $this->helpURL); - } - - /** - * Custom clean cache method, plugins are cached in 2 places for different clients. - * - * @param string $group Cache group name. - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('com_plugins'); - } + /** + * @var string The help screen key for the module. + * @since 1.6 + */ + protected $helpKey = 'Plugins:_Name_of_Plugin'; + + /** + * @var string The help screen base URL for the module. + * @since 1.6 + */ + protected $helpURL; + + /** + * @var array An array of cached plugin items. + * @since 1.6 + */ + protected $_cache; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + $config = array_merge( + array( + 'event_after_save' => 'onExtensionAfterSave', + 'event_before_save' => 'onExtensionBeforeSave', + 'events_map' => array( + 'save' => 'extension' + ) + ), + $config + ); + + parent::__construct($config, $factory); + } + + /** + * Method to get the record form. + * + * @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 Form|bool A Form object on success, false on failure. + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // The folder and element vars are passed when saving the form. + if (empty($data)) { + $item = $this->getItem(); + $folder = $item->folder; + $element = $item->element; + } else { + $folder = ArrayHelper::getValue($data, 'folder', '', 'cmd'); + $element = ArrayHelper::getValue($data, 'element', '', 'cmd'); + } + + // Add the default fields directory + Form::addFieldPath(JPATH_PLUGINS . '/' . $folder . '/' . $element . '/field'); + + // These variables are used to add data from the plugin XML files. + $this->setState('item.folder', $folder); + $this->setState('item.element', $element); + + // Get the form. + $form = $this->loadForm('com_plugins.plugin', 'plugin', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Modify the form based on access controls. + if (!$this->canEditState((object) $data)) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('enabled', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('enabled', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_plugins.edit.plugin.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_plugins.plugin', $data); + + return $data; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + */ + public function getItem($pk = null) + { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('plugin.id'); + + $cacheId = $pk; + + if (\is_array($cacheId)) { + $cacheId = serialize($cacheId); + } + + if (!isset($this->_cache[$cacheId])) { + // Get a row instance. + $table = $this->getTable(); + + // Attempt to load the row. + $return = $table->load(\is_array($pk) ? $pk : ['extension_id' => $pk, 'type' => 'plugin']); + + // Check for a table object error. + if ($return === false) { + return false; + } + + // Convert to the \Joomla\CMS\Object\CMSObject before adding other data. + $properties = $table->getProperties(1); + $this->_cache[$cacheId] = ArrayHelper::toObject($properties, CMSObject::class); + + // Convert the params field to an array. + $registry = new Registry($table->params); + $this->_cache[$cacheId]->params = $registry->toArray(); + + // Get the plugin XML. + $path = Path::clean(JPATH_PLUGINS . '/' . $table->folder . '/' . $table->element . '/' . $table->element . '.xml'); + + if (file_exists($path)) { + $this->_cache[$cacheId]->xml = simplexml_load_file($path); + } else { + $this->_cache[$cacheId]->xml = null; + } + } + + return $this->_cache[$cacheId]; + } + + /** + * Returns a reference to the Table object, always creating it. + * + * @param string $type The table type to instantiate. + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A database object + */ + public function getTable($type = 'Extension', $prefix = 'JTable', $config = array()) + { + return Table::getInstance($type, $prefix, $config); + } + + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + // Execute the parent method. + parent::populateState(); + + $app = Factory::getApplication(); + + // Load the User state. + $pk = $app->input->getInt('extension_id'); + $this->setState('plugin.id', $pk); + } + + /** + * Preprocess the form. + * + * @param Form $form A form object. + * @param mixed $data The data expected for the form. + * @param string $group Cache group name. + * + * @return mixed True if successful. + * + * @since 1.6 + * + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $folder = $this->getState('item.folder'); + $element = $this->getState('item.element'); + $lang = Factory::getLanguage(); + + // Load the core and/or local language sys file(s) for the ordering field. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('element')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = :folder') + ->bind(':folder', $folder); + $db->setQuery($query); + $elements = $db->loadColumn(); + + foreach ($elements as $elementa) { + $lang->load('plg_' . $folder . '_' . $elementa . '.sys', JPATH_ADMINISTRATOR) + || $lang->load('plg_' . $folder . '_' . $elementa . '.sys', JPATH_PLUGINS . '/' . $folder . '/' . $elementa); + } + + if (empty($folder) || empty($element)) { + $app = Factory::getApplication(); + $app->redirect(Route::_('index.php?option=com_plugins&view=plugins', false)); + } + + $formFile = Path::clean(JPATH_PLUGINS . '/' . $folder . '/' . $element . '/' . $element . '.xml'); + + if (!file_exists($formFile)) { + throw new \Exception(Text::sprintf('COM_PLUGINS_ERROR_FILE_NOT_FOUND', $element . '.xml')); + } + + // Load the core and/or local language file(s). + $lang->load('plg_' . $folder . '_' . $element, JPATH_ADMINISTRATOR) + || $lang->load('plg_' . $folder . '_' . $element, JPATH_PLUGINS . '/' . $folder . '/' . $element); + + if (file_exists($formFile)) { + // Get the plugin form. + if (!$form->loadFile($formFile, false, '//config')) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + } + + // Attempt to load the xml file. + if (!$xml = simplexml_load_file($formFile)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Get the help data from the XML file if present. + $help = $xml->xpath('/extension/help'); + + if (!empty($help)) { + $helpKey = trim((string) $help[0]['key']); + $helpURL = trim((string) $help[0]['url']); + + $this->helpKey = $helpKey ?: $this->helpKey; + $this->helpURL = $helpURL ?: $this->helpURL; + } + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param object $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 1.6 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('type') . ' = ' . $db->quote($table->type), + $db->quoteName('folder') . ' = ' . $db->quote($table->folder), + ]; + } + + /** + * Override method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + // Setup type. + $data['type'] = 'plugin'; + + return parent::save($data); + } + + /** + * Get the necessary data to load an item help screen. + * + * @return object An object with key, url, and local properties for loading the item help screen. + * + * @since 1.6 + */ + public function getHelp() + { + return (object) array('key' => $this->helpKey, 'url' => $this->helpURL); + } + + /** + * Custom clean cache method, plugins are cached in 2 places for different clients. + * + * @param string $group Cache group name. + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('com_plugins'); + } } diff --git a/administrator/components/com_plugins/src/Model/PluginsModel.php b/administrator/components/com_plugins/src/Model/PluginsModel.php index 6672c88654f9e..c2edcd2fd8e07 100644 --- a/administrator/components/com_plugins/src/Model/PluginsModel.php +++ b/administrator/components/com_plugins/src/Model/PluginsModel.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Plugins\Administrator\Model; -\defined('_JEXEC') or die; +namespace Joomla\Component\Plugins\Administrator\Model; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; @@ -25,282 +25,262 @@ */ class PluginsModel extends ListModel { - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * - * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel - * @since 3.2 - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null) - { - if (empty($config['filter_fields'])) - { - $config['filter_fields'] = array( - 'extension_id', 'a.extension_id', - 'name', 'a.name', - 'folder', 'a.folder', - 'element', 'a.element', - 'checked_out', 'a.checked_out', - 'checked_out_time', 'a.checked_out_time', - 'state', 'a.state', - 'enabled', 'a.enabled', - 'access', 'a.access', 'access_level', - 'ordering', 'a.ordering', - 'client_id', 'a.client_id', - ); - } - - parent::__construct($config, $factory); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'folder', $direction = 'asc') - { - // Load the parameters. - $params = ComponentHelper::getParams('com_plugins'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.enabled'); - $id .= ':' . $this->getState('filter.folder'); - $id .= ':' . $this->getState('filter.element'); - - return parent::getStoreId($id); - } - - /** - * Returns an object list. - * - * @param \Joomla\Database\DatabaseQuery $query A database query object. - * @param integer $limitstart Offset. - * @param integer $limit The number of records. - * - * @return array - */ - protected function _getList($query, $limitstart = 0, $limit = 0) - { - $search = $this->getState('filter.search'); - $ordering = $this->getState('list.ordering', 'ordering'); - - // If "Sort Table By:" is not set, set ordering to name - if ($ordering == '') - { - $ordering = 'name'; - } - - $db = $this->getDatabase(); - - if ($ordering == 'name' || (!empty($search) && stripos($search, 'id:') !== 0)) - { - $db->setQuery($query); - $result = $db->loadObjectList(); - $this->translate($result); - - if (!empty($search)) - { - $escapedSearchString = $this->refineSearchStringToRegex($search, '/'); - - foreach ($result as $i => $item) - { - if (!preg_match("/$escapedSearchString/i", $item->name)) - { - unset($result[$i]); - } - } - } - - $orderingDirection = strtolower($this->getState('list.direction')); - $direction = ($orderingDirection == 'desc') ? -1 : 1; - $result = ArrayHelper::sortObjects($result, $ordering, $direction, true, true); - - $total = count($result); - $this->cache[$this->getStoreId('getTotal')] = $total; - - if ($total < $limitstart) - { - $limitstart = 0; - } - - $this->cache[$this->getStoreId('getStart')] = $limitstart; - - return array_slice($result, $limitstart, $limit ?: null); - } - else - { - if ($ordering == 'ordering') - { - $query->order('a.folder ASC'); - $ordering = 'a.ordering'; - } - - $query->order($db->quoteName($ordering) . ' ' . $this->getState('list.direction')); - - if ($ordering == 'folder') - { - $query->order('a.ordering ASC'); - } - - $result = parent::_getList($query, $limitstart, $limit); - $this->translate($result); - - return $result; - } - } - - /** - * Translate a list of objects. - * - * @param array &$items The array of objects. - * - * @return array The array of translated objects. - */ - protected function translate(&$items) - { - $lang = Factory::getLanguage(); - - foreach ($items as &$item) - { - $source = JPATH_PLUGINS . '/' . $item->folder . '/' . $item->element; - $extension = 'plg_' . $item->folder . '_' . $item->element; - $lang->load($extension . '.sys', JPATH_ADMINISTRATOR) - || $lang->load($extension . '.sys', $source); - $item->name = Text::_($item->name); - } - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.extension_id , a.name, a.element, a.folder, a.checked_out, a.checked_out_time,' . - ' a.enabled, a.access, a.ordering, a.note' - ) - ) - ->from($db->quoteName('#__extensions') . ' AS a') - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')); - - // Join over the users for the checked out user. - $query->select('uc.name AS editor') - ->join('LEFT', '#__users AS uc ON uc.id=a.checked_out'); - - // Join over the asset groups. - $query->select('ag.title AS access_level') - ->join('LEFT', '#__viewlevels AS ag ON ag.id = a.access'); - - // Filter by access level. - if ($access = $this->getState('filter.access')) - { - $access = (int) $access; - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - - // Filter by published state. - $published = (string) $this->getState('filter.enabled'); - - if (is_numeric($published)) - { - $published = (int) $published; - $query->where($db->quoteName('a.enabled') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->whereIn($db->quoteName('a.enabled'), [0, 1]); - } - - // Filter by state. - $query->where('a.state >= 0'); - - // Filter by folder. - if ($folder = $this->getState('filter.folder')) - { - $query->where($db->quoteName('a.folder') . ' = :folder') - ->bind(':folder', $folder); - } - - // Filter by element. - if ($element = $this->getState('filter.element')) - { - $query->where($db->quoteName('a.element') . ' = :element') - ->bind(':element', $element); - } - - // Filter by search in name or id. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.extension_id') . ' = :id'); - $query->bind(':id', $ids, ParameterType::INTEGER); - } - } - - return $query; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 3.5 - */ - protected function loadFormData() - { - $data = parent::loadFormData(); - - // Set the selected filter values for pages that use the Layouts for filtering - $data->list['sortTable'] = $this->state->get('list.ordering'); - $data->list['directionTable'] = $this->state->get('list.direction'); - - return $data; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'extension_id', 'a.extension_id', + 'name', 'a.name', + 'folder', 'a.folder', + 'element', 'a.element', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'state', 'a.state', + 'enabled', 'a.enabled', + 'access', 'a.access', 'access_level', + 'ordering', 'a.ordering', + 'client_id', 'a.client_id', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'folder', $direction = 'asc') + { + // Load the parameters. + $params = ComponentHelper::getParams('com_plugins'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.enabled'); + $id .= ':' . $this->getState('filter.folder'); + $id .= ':' . $this->getState('filter.element'); + + return parent::getStoreId($id); + } + + /** + * Returns an object list. + * + * @param \Joomla\Database\DatabaseQuery $query A database query object. + * @param integer $limitstart Offset. + * @param integer $limit The number of records. + * + * @return array + */ + protected function _getList($query, $limitstart = 0, $limit = 0) + { + $search = $this->getState('filter.search'); + $ordering = $this->getState('list.ordering', 'ordering'); + + // If "Sort Table By:" is not set, set ordering to name + if ($ordering == '') { + $ordering = 'name'; + } + + $db = $this->getDatabase(); + + if ($ordering == 'name' || (!empty($search) && stripos($search, 'id:') !== 0)) { + $db->setQuery($query); + $result = $db->loadObjectList(); + $this->translate($result); + + if (!empty($search)) { + $escapedSearchString = $this->refineSearchStringToRegex($search, '/'); + + foreach ($result as $i => $item) { + if (!preg_match("/$escapedSearchString/i", $item->name)) { + unset($result[$i]); + } + } + } + + $orderingDirection = strtolower($this->getState('list.direction')); + $direction = ($orderingDirection == 'desc') ? -1 : 1; + $result = ArrayHelper::sortObjects($result, $ordering, $direction, true, true); + + $total = count($result); + $this->cache[$this->getStoreId('getTotal')] = $total; + + if ($total < $limitstart) { + $limitstart = 0; + } + + $this->cache[$this->getStoreId('getStart')] = $limitstart; + + return array_slice($result, $limitstart, $limit ?: null); + } else { + if ($ordering == 'ordering') { + $query->order('a.folder ASC'); + $ordering = 'a.ordering'; + } + + $query->order($db->quoteName($ordering) . ' ' . $this->getState('list.direction')); + + if ($ordering == 'folder') { + $query->order('a.ordering ASC'); + } + + $result = parent::_getList($query, $limitstart, $limit); + $this->translate($result); + + return $result; + } + } + + /** + * Translate a list of objects. + * + * @param array &$items The array of objects. + * + * @return array The array of translated objects. + */ + protected function translate(&$items) + { + $lang = Factory::getLanguage(); + + foreach ($items as &$item) { + $source = JPATH_PLUGINS . '/' . $item->folder . '/' . $item->element; + $extension = 'plg_' . $item->folder . '_' . $item->element; + $lang->load($extension . '.sys', JPATH_ADMINISTRATOR) + || $lang->load($extension . '.sys', $source); + $item->name = Text::_($item->name); + } + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.extension_id , a.name, a.element, a.folder, a.checked_out, a.checked_out_time,' . + ' a.enabled, a.access, a.ordering, a.note' + ) + ) + ->from($db->quoteName('#__extensions') . ' AS a') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')); + + // Join over the users for the checked out user. + $query->select('uc.name AS editor') + ->join('LEFT', '#__users AS uc ON uc.id=a.checked_out'); + + // Join over the asset groups. + $query->select('ag.title AS access_level') + ->join('LEFT', '#__viewlevels AS ag ON ag.id = a.access'); + + // Filter by access level. + if ($access = $this->getState('filter.access')) { + $access = (int) $access; + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + + // Filter by published state. + $published = (string) $this->getState('filter.enabled'); + + if (is_numeric($published)) { + $published = (int) $published; + $query->where($db->quoteName('a.enabled') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->whereIn($db->quoteName('a.enabled'), [0, 1]); + } + + // Filter by state. + $query->where('a.state >= 0'); + + // Filter by folder. + if ($folder = $this->getState('filter.folder')) { + $query->where($db->quoteName('a.folder') . ' = :folder') + ->bind(':folder', $folder); + } + + // Filter by element. + if ($element = $this->getState('filter.element')) { + $query->where($db->quoteName('a.element') . ' = :element') + ->bind(':element', $element); + } + + // Filter by search in name or id. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.extension_id') . ' = :id'); + $query->bind(':id', $ids, ParameterType::INTEGER); + } + } + + return $query; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 3.5 + */ + protected function loadFormData() + { + $data = parent::loadFormData(); + + // Set the selected filter values for pages that use the Layouts for filtering + $data->list['sortTable'] = $this->state->get('list.ordering'); + $data->list['directionTable'] = $this->state->get('list.direction'); + + return $data; + } } diff --git a/administrator/components/com_plugins/src/View/Plugin/HtmlView.php b/administrator/components/com_plugins/src/View/Plugin/HtmlView.php index 888a2303002ab..313b3056e49b5 100644 --- a/administrator/components/com_plugins/src/View/Plugin/HtmlView.php +++ b/administrator/components/com_plugins/src/View/Plugin/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->item = $this->get('Item'); - $this->form = $this->get('Form'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $canDo = ContentHelper::getActions('com_plugins'); - - ToolbarHelper::title(Text::sprintf('COM_PLUGINS_MANAGER_PLUGIN', Text::_($this->item->name)), 'plug plugin'); - - // If not checked out, can save the item. - if ($canDo->get('core.edit')) - { - ToolbarHelper::apply('plugin.apply'); - - ToolbarHelper::save('plugin.save'); - } - - ToolbarHelper::cancel('plugin.cancel', 'JTOOLBAR_CLOSE'); - ToolbarHelper::divider(); - - // Get the help information for the plugin item. - $lang = Factory::getLanguage(); - - $help = $this->get('Help'); - - if ($help->url && $lang->hasKey($help->url)) - { - $debug = $lang->setDebug(false); - $url = Text::_($help->url); - $lang->setDebug($debug); - } - else - { - $url = null; - } - - ToolbarHelper::inlinehelp(); - ToolbarHelper::help($help->key, false, $url); - } + /** + * The item object for the newsfeed + * + * @var CMSObject + */ + protected $item; + + /** + * The form object for the newsfeed + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The model state of the newsfeed + * + * @var CMSObject + */ + protected $state; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->item = $this->get('Item'); + $this->form = $this->get('Form'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $canDo = ContentHelper::getActions('com_plugins'); + + ToolbarHelper::title(Text::sprintf('COM_PLUGINS_MANAGER_PLUGIN', Text::_($this->item->name)), 'plug plugin'); + + // If not checked out, can save the item. + if ($canDo->get('core.edit')) { + ToolbarHelper::apply('plugin.apply'); + + ToolbarHelper::save('plugin.save'); + } + + ToolbarHelper::cancel('plugin.cancel', 'JTOOLBAR_CLOSE'); + ToolbarHelper::divider(); + + // Get the help information for the plugin item. + $lang = Factory::getLanguage(); + + $help = $this->get('Help'); + + if ($help->url && $lang->hasKey($help->url)) { + $debug = $lang->setDebug(false); + $url = Text::_($help->url); + $lang->setDebug($debug); + } else { + $url = null; + } + + ToolbarHelper::inlinehelp(); + ToolbarHelper::help($help->key, false, $url); + } } diff --git a/administrator/components/com_plugins/src/View/Plugins/HtmlView.php b/administrator/components/com_plugins/src/View/Plugins/HtmlView.php index dae68430e1421..b283d03bfea56 100644 --- a/administrator/components/com_plugins/src/View/Plugins/HtmlView.php +++ b/administrator/components/com_plugins/src/View/Plugins/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_plugins'); - - ToolbarHelper::title(Text::_('COM_PLUGINS_MANAGER_PLUGINS'), 'plug plugin'); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - if ($canDo->get('core.edit.state')) - { - $toolbar->publish('plugins.publish', 'JTOOLBAR_ENABLE')->listCheck(true); - $toolbar->unpublish('plugins.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); - $toolbar->checkin('plugins.checkin')->listCheck(true); - } - - if ($canDo->get('core.admin')) - { - $toolbar->preferences('com_plugins'); - } - - $toolbar->help('Plugins'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_plugins'); + + ToolbarHelper::title(Text::_('COM_PLUGINS_MANAGER_PLUGINS'), 'plug plugin'); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + if ($canDo->get('core.edit.state')) { + $toolbar->publish('plugins.publish', 'JTOOLBAR_ENABLE')->listCheck(true); + $toolbar->unpublish('plugins.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); + $toolbar->checkin('plugins.checkin')->listCheck(true); + } + + if ($canDo->get('core.admin')) { + $toolbar->preferences('com_plugins'); + } + + $toolbar->help('Plugins'); + } } diff --git a/administrator/components/com_plugins/tmpl/plugin/edit.php b/administrator/components/com_plugins/tmpl/plugin/edit.php index 06dae096c9dd4..c97f8d19a7afd 100644 --- a/administrator/components/com_plugins/tmpl/plugin/edit.php +++ b/administrator/components/com_plugins/tmpl/plugin/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $this->fieldsets = $this->form->getFieldsets('params'); $this->useCoreUI = true; @@ -32,111 +33,105 @@ ?>
-
- - 'general', 'recall' => true, 'breakpoint' => 768]); ?> - - - -
-
- item->xml) : ?> - item->xml->description) : ?> -

- item->xml) - { - echo ($text = (string) $this->item->xml->name) ? Text::_($text) : $this->item->name; - } - else - { - echo Text::_('COM_PLUGINS_XML_ERR'); - } - ?> -

-
- - form->getValue('folder'); ?> - / - - form->getValue('element'); ?> - -
-
- fieldset = 'description'; - $short_description = Text::_($this->item->xml->description); - $long_description = LayoutHelper::render('joomla.edit.fieldset', $this); - - if (!$long_description) - { - $truncated = HTMLHelper::_('string.truncate', $short_description, 550, true, false); - - if (strlen($truncated) > 500) - { - $long_description = $short_description; - $short_description = HTMLHelper::_('string.truncate', $truncated, 250); - - if ($short_description == $long_description) - { - $long_description = ''; - } - } - } - ?> -

- -

- - - -

- -
- - -
- - -
- - fieldset = 'basic'; - $html = LayoutHelper::render('joomla.edit.fieldset', $this); - echo $html ? '
' . $html : ''; - ?> -
-
- fields = array( - 'enabled', - 'access', - 'ordering', - 'folder', - 'element', - 'note', - ); ?> - -
-
- - - - - - - - - fieldsets = array(); - $this->ignore_fieldsets = array('basic', 'description'); - echo LayoutHelper::render('joomla.edit.params', $this); - ?> - - -
- - - +
+ + 'general', 'recall' => true, 'breakpoint' => 768]); ?> + + + +
+
+ item->xml) : ?> + item->xml->description) : ?> +

+ item->xml) { + echo ($text = (string) $this->item->xml->name) ? Text::_($text) : $this->item->name; + } else { + echo Text::_('COM_PLUGINS_XML_ERR'); + } + ?> +

+
+ + form->getValue('folder'); ?> + / + + form->getValue('element'); ?> + +
+
+ fieldset = 'description'; + $short_description = Text::_($this->item->xml->description); + $long_description = LayoutHelper::render('joomla.edit.fieldset', $this); + + if (!$long_description) { + $truncated = HTMLHelper::_('string.truncate', $short_description, 550, true, false); + + if (strlen($truncated) > 500) { + $long_description = $short_description; + $short_description = HTMLHelper::_('string.truncate', $truncated, 250); + + if ($short_description == $long_description) { + $long_description = ''; + } + } + } + ?> +

+ +

+ + + +

+ +
+ + +
+ + +
+ + fieldset = 'basic'; + $html = LayoutHelper::render('joomla.edit.fieldset', $this); + echo $html ? '
' . $html : ''; + ?> +
+
+ fields = array( + 'enabled', + 'access', + 'ordering', + 'folder', + 'element', + 'note', + ); ?> + +
+
+ + + + + + + + + fieldsets = array(); + $this->ignore_fieldsets = array('basic', 'description'); + echo LayoutHelper::render('joomla.edit.params', $this); + ?> + + +
+ + +
diff --git a/administrator/components/com_plugins/tmpl/plugin/modal.php b/administrator/components/com_plugins/tmpl/plugin/modal.php index 4c6534c5c401d..aeff088dc4fb9 100644 --- a/administrator/components/com_plugins/tmpl/plugin/modal.php +++ b/administrator/components/com_plugins/tmpl/plugin/modal.php @@ -1,4 +1,5 @@
- setLayout('edit'); ?> - loadTemplate(); ?> + setLayout('edit'); ?> + loadTemplate(); ?>
diff --git a/administrator/components/com_plugins/tmpl/plugins/default.php b/administrator/components/com_plugins/tmpl/plugins/default.php index 541fd8e0f351f..296766df2ccb3 100644 --- a/administrator/components/com_plugins/tmpl/plugins/default.php +++ b/administrator/components/com_plugins/tmpl/plugins/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); $saveOrder = $listOrder == 'ordering'; -if ($saveOrder) -{ - $saveOrderingUrl = 'index.php?option=com_plugins&task=plugins.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder) { + $saveOrderingUrl = 'index.php?option=com_plugins&task=plugins.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
-
- $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items as $i => $item) : - $ordering = ($listOrder == 'ordering'); - $canEdit = $user->authorise('core.edit', 'com_plugins'); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_plugins') && $canCheckin; - ?> - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - -
- extension_id, false, 'cid', 'cb', $item->name); ?> - - - - - - - - - - enabled, $i, 'plugins.', $canChange); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'plugins.', $canCheckin); ?> - - - - name; ?> - note)) : ?> -
- escape($item->note)); ?> -
- - - name; ?> - -
- escape($item->folder); ?> - - escape($item->element); ?> - - escape($item->access_level); ?> - - extension_id; ?> -
+
+ $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items as $i => $item) : + $ordering = ($listOrder == 'ordering'); + $canEdit = $user->authorise('core.edit', 'com_plugins'); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_plugins') && $canCheckin; + ?> + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + +
+ extension_id, false, 'cid', 'cb', $item->name); ?> + + + + + + + + + + enabled, $i, 'plugins.', $canChange); ?> + + checked_out) : ?> + editor, $item->checked_out_time, 'plugins.', $canCheckin); ?> + + + + name; ?> + note)) : ?> +
+ escape($item->note)); ?> +
+ + + name; ?> + +
+ escape($item->folder); ?> + + escape($item->element); ?> + + escape($item->access_level); ?> + + extension_id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
+ + + +
diff --git a/administrator/components/com_postinstall/services/provider.php b/administrator/components/com_postinstall/services/provider.php index eeb3f3a013f91..d0995305ee02e 100644 --- a/administrator/components/com_postinstall/services/provider.php +++ b/administrator/components/com_postinstall/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Postinstall')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Postinstall')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Postinstall')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Postinstall')); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_postinstall/src/Controller/DisplayController.php b/administrator/components/com_postinstall/src/Controller/DisplayController.php index e9a6be0e6d11e..c3ab27fd1b675 100644 --- a/administrator/components/com_postinstall/src/Controller/DisplayController.php +++ b/administrator/components/com_postinstall/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ app->getIdentity()->authorise('core.manage', 'com_postinstall')) - { - throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); - } - - $model = $this->getModel('Messages'); - - echo new JsonResponse($model->getItemsCount()); - } + /** + * @var string The default view. + * @since 1.6 + */ + protected $default_view = 'messages'; + + /** + * Provide the data for a badge in a menu item via JSON + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function getMenuBadgeData() + { + if (!$this->app->getIdentity()->authorise('core.manage', 'com_postinstall')) { + throw new \Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); + } + + $model = $this->getModel('Messages'); + + echo new JsonResponse($model->getItemsCount()); + } } diff --git a/administrator/components/com_postinstall/src/Controller/MessageController.php b/administrator/components/com_postinstall/src/Controller/MessageController.php index 1342187f6713c..d837fec65cce2 100644 --- a/administrator/components/com_postinstall/src/Controller/MessageController.php +++ b/administrator/components/com_postinstall/src/Controller/MessageController.php @@ -1,4 +1,5 @@ checkToken('get'); - - /** @var MessagesModel $model */ - $model = $this->getModel('Messages', '', ['ignore_request' => true]); - $eid = $this->input->getInt('eid'); - - if (empty($eid)) - { - $eid = $model->getJoomlaFilesExtensionId(); - } - - $model->resetMessages($eid); - - $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); - } - - /** - * Unpublishes post-installation message of the specified extension. - * - * @return void - * - * @since 3.2 - */ - public function unpublish() - { - $model = $this->getModel('Messages', '', ['ignore_request' => true]); - - $id = $this->input->get('id'); - - $eid = (int) $model->getState('eid', $model->getJoomlaFilesExtensionId()); - - if (empty($eid)) - { - $eid = $model->getJoomlaFilesExtensionId(); - } - - $model->setState('published', 0); - $model->unpublishMessage($id); - - $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); - } - - /** - * Re-Publishes an archived post-installation message of the specified extension. - * - * @return void - * - * @since 4.2.0 - */ - public function republish() - { - $model = $this->getModel('Messages', '', ['ignore_request' => true]); - - $id = $this->input->get('id'); - - $eid = (int) $model->getState('eid', $model->getJoomlaFilesExtensionId()); - - if (empty($eid)) - { - $eid = $model->getJoomlaFilesExtensionId(); - } - - $model->setState('published', 1); - $model->republishMessage($id); - - $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); - } - - /** - * Archives a published post-installation message of the specified extension. - * - * @return void - * - * @since 4.2.0 - */ - public function archive() - { - $model = $this->getModel('Messages', '', ['ignore_request' => true]); - - $id = $this->input->get('id'); - - $eid = (int) $model->getState('eid', $model->getJoomlaFilesExtensionId()); - - if (empty($eid)) - { - $eid = $model->getJoomlaFilesExtensionId(); - } - - $model->setState('published', 2); - $model->archiveMessage($id); - - $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); - } - - /** - * Executes the action associated with an item. - * - * @return void - * - * @since 3.2 - */ - public function action() - { - $this->checkToken('get'); - - $model = $this->getModel('Messages', '', ['ignore_request' => true]); - - $id = $this->input->get('id'); - - $item = $model->getItem($id); - - switch ($item->type) - { - case 'link': - $this->setRedirect($item->action); - - return; - - case 'action': - $helper = new PostinstallHelper; - $file = $helper->parsePath($item->action_file); - - if (File::exists($file)) - { - require_once $file; - - call_user_func($item->action); - } - break; - } - - $this->setRedirect('index.php?option=com_postinstall'); - } - - /** - * Hides all post-installation messages of the specified extension. - * - * @return void - * - * @since 3.8.7 - */ - public function hideAll() - { - $this->checkToken(); - - /** @var MessagesModel $model */ - $model = $this->getModel('Messages', '', ['ignore_request' => true]); - $eid = $this->input->getInt('eid'); - - if (empty($eid)) - { - $eid = $model->getJoomlaFilesExtensionId(); - } - - $model->hideMessages($eid); - $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); - } + /** + * Resets all post-installation messages of the specified extension. + * + * @return void + * + * @since 3.2 + */ + public function reset() + { + $this->checkToken('get'); + + /** @var MessagesModel $model */ + $model = $this->getModel('Messages', '', ['ignore_request' => true]); + $eid = $this->input->getInt('eid'); + + if (empty($eid)) { + $eid = $model->getJoomlaFilesExtensionId(); + } + + $model->resetMessages($eid); + + $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); + } + + /** + * Unpublishes post-installation message of the specified extension. + * + * @return void + * + * @since 3.2 + */ + public function unpublish() + { + $model = $this->getModel('Messages', '', ['ignore_request' => true]); + + $id = $this->input->get('id'); + + $eid = (int) $model->getState('eid', $model->getJoomlaFilesExtensionId()); + + if (empty($eid)) { + $eid = $model->getJoomlaFilesExtensionId(); + } + + $model->setState('published', 0); + $model->unpublishMessage($id); + + $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); + } + + /** + * Re-Publishes an archived post-installation message of the specified extension. + * + * @return void + * + * @since 4.2.0 + */ + public function republish() + { + $model = $this->getModel('Messages', '', ['ignore_request' => true]); + + $id = $this->input->get('id'); + + $eid = (int) $model->getState('eid', $model->getJoomlaFilesExtensionId()); + + if (empty($eid)) { + $eid = $model->getJoomlaFilesExtensionId(); + } + + $model->setState('published', 1); + $model->republishMessage($id); + + $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); + } + + /** + * Archives a published post-installation message of the specified extension. + * + * @return void + * + * @since 4.2.0 + */ + public function archive() + { + $model = $this->getModel('Messages', '', ['ignore_request' => true]); + + $id = $this->input->get('id'); + + $eid = (int) $model->getState('eid', $model->getJoomlaFilesExtensionId()); + + if (empty($eid)) { + $eid = $model->getJoomlaFilesExtensionId(); + } + + $model->setState('published', 2); + $model->archiveMessage($id); + + $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); + } + + /** + * Executes the action associated with an item. + * + * @return void + * + * @since 3.2 + */ + public function action() + { + $this->checkToken('get'); + + $model = $this->getModel('Messages', '', ['ignore_request' => true]); + + $id = $this->input->get('id'); + + $item = $model->getItem($id); + + switch ($item->type) { + case 'link': + $this->setRedirect($item->action); + + return; + + case 'action': + $helper = new PostinstallHelper(); + $file = $helper->parsePath($item->action_file); + + if (File::exists($file)) { + require_once $file; + + call_user_func($item->action); + } + break; + } + + $this->setRedirect('index.php?option=com_postinstall'); + } + + /** + * Hides all post-installation messages of the specified extension. + * + * @return void + * + * @since 3.8.7 + */ + public function hideAll() + { + $this->checkToken(); + + /** @var MessagesModel $model */ + $model = $this->getModel('Messages', '', ['ignore_request' => true]); + $eid = $this->input->getInt('eid'); + + if (empty($eid)) { + $eid = $model->getJoomlaFilesExtensionId(); + } + + $model->hideMessages($eid); + $this->setRedirect('index.php?option=com_postinstall&eid=' . $eid); + } } diff --git a/administrator/components/com_postinstall/src/Helper/PostinstallHelper.php b/administrator/components/com_postinstall/src/Helper/PostinstallHelper.php index 8b10b66df68be..2ee2362abfece 100644 --- a/administrator/components/com_postinstall/src/Helper/PostinstallHelper.php +++ b/administrator/components/com_postinstall/src/Helper/PostinstallHelper.php @@ -1,4 +1,5 @@ input->getInt('eid'); - - if ($eid) - { - $this->setState('eid', $eid); - } - } - - /** - * Gets an item with the given id from the database - * - * @param integer $id The item id - * - * @return Object - * - * @since 3.2 - */ - public function getItem($id) - { - $db = $this->getDatabase(); - $id = (int) $id; - - $query = $db->getQuery(true); - $query->select( - [ - $db->quoteName('postinstall_message_id'), - $db->quoteName('extension_id'), - $db->quoteName('title_key'), - $db->quoteName('description_key'), - $db->quoteName('action_key'), - $db->quoteName('language_extension'), - $db->quoteName('language_client_id'), - $db->quoteName('type'), - $db->quoteName('action_file'), - $db->quoteName('action'), - $db->quoteName('condition_file'), - $db->quoteName('condition_method'), - $db->quoteName('version_introduced'), - $db->quoteName('enabled'), - ] - ) - ->from($db->quoteName('#__postinstall_messages')) - ->where($db->quoteName('postinstall_message_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - - $db->setQuery($query); - - $result = $db->loadObject(); - - return $result; - } - - /** - * Unpublishes specified post-install message - * - * @param integer $id The message id - * - * @return void - */ - public function unpublishMessage($id) - { - $db = $this->getDatabase(); - $id = (int) $id; - - $query = $db->getQuery(true); - $query - ->update($db->quoteName('#__postinstall_messages')) - ->set($db->quoteName('enabled') . ' = 0') - ->where($db->quoteName('postinstall_message_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - Factory::getCache()->clean('com_postinstall'); - } - - /** - * Archives specified post-install message - * - * @param integer $id The message id - * - * @return void - * - * @since 4.2.0 - */ - public function archiveMessage($id) - { - $db = $this->getDatabase(); - $id = (int) $id; - - $query = $db->getQuery(true); - $query - ->update($db->quoteName('#__postinstall_messages')) - ->set($db->quoteName('enabled') . ' = 2') - ->where($db->quoteName('postinstall_message_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - Factory::getCache()->clean('com_postinstall'); - } - - /** - * Republishes specified post-install message - * - * @param integer $id The message id - * - * @return void - * - * @since 4.2.0 - */ - public function republishMessage($id) - { - $db = $this->getDatabase(); - $id = (int) $id; - - $query = $db->getQuery(true); - $query - ->update($db->quoteName('#__postinstall_messages')) - ->set($db->quoteName('enabled') . ' = 1') - ->where($db->quoteName('postinstall_message_id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - Factory::getCache()->clean('com_postinstall'); - } - - /** - * Returns a list of messages from the #__postinstall_messages table - * - * @return array - * - * @since 3.2 - */ - public function getItems() - { - // Add a forced extension filtering to the list - $eid = (int) $this->getState('eid', $this->getJoomlaFilesExtensionId()); - - // Build a cache ID for the resulting data object - $cacheId = 'postinstall_messages.' . $eid; - - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $query->select( - [ - $db->quoteName('postinstall_message_id'), - $db->quoteName('extension_id'), - $db->quoteName('title_key'), - $db->quoteName('description_key'), - $db->quoteName('action_key'), - $db->quoteName('language_extension'), - $db->quoteName('language_client_id'), - $db->quoteName('type'), - $db->quoteName('action_file'), - $db->quoteName('action'), - $db->quoteName('condition_file'), - $db->quoteName('condition_method'), - $db->quoteName('version_introduced'), - $db->quoteName('enabled'), - ] - ) - ->from($db->quoteName('#__postinstall_messages')); - $query->where($db->quoteName('extension_id') . ' = :eid') - ->bind(':eid', $eid, ParameterType::INTEGER); - - // Force filter only enabled messages - $query->whereIn($db->quoteName('enabled'), [1, 2]); - $db->setQuery($query); - - try - { - /** @var CallbackController $cache */ - $cache = $this->getCacheControllerFactory()->createCacheController('callback', ['defaultgroup' => 'com_postinstall']); - - $result = $cache->get(array($db, 'loadObjectList'), array(), md5($cacheId), false); - } - catch (\RuntimeException $e) - { - $app = Factory::getApplication(); - $app->getLogger()->warning( - Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $e->getMessage()), - array('category' => 'jerror') - ); - - return array(); - } - - $this->onProcessList($result); - - return $result; - } - - /** - * Returns a count of all enabled messages from the #__postinstall_messages table - * - * @return integer - * - * @since 4.0.0 - */ - public function getItemsCount() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $query->select( - [ - $db->quoteName('language_extension'), - $db->quoteName('language_client_id'), - $db->quoteName('condition_file'), - $db->quoteName('condition_method'), - ] - ) - ->from($db->quoteName('#__postinstall_messages')); - - // Force filter only enabled messages - $query->where($db->quoteName('enabled') . ' = 1'); - $db->setQuery($query); - - try - { - /** @var CallbackController $cache */ - $cache = Factory::getContainer()->get(CacheControllerFactoryInterface::class) - ->createCacheController('callback', ['defaultgroup' => 'com_postinstall']); - - // Get the resulting data object for cache ID 'all.1' from com_postinstall group. - $result = $cache->get(array($db, 'loadObjectList'), array(), md5('all.1'), false); - } - catch (\RuntimeException $e) - { - $app = Factory::getApplication(); - $app->getLogger()->warning( - Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $e->getMessage()), - array('category' => 'jerror') - ); - - return 0; - } - - $this->onProcessList($result); - - return \count($result); - } - - /** - * Returns the name of an extension, as registered in the #__extensions table - * - * @param integer $eid The extension ID - * - * @return string The extension name - * - * @since 3.2 - */ - public function getExtensionName($eid) - { - // Load the extension's information from the database - $db = $this->getDatabase(); - $eid = (int) $eid; - - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('name'), - $db->quoteName('element'), - $db->quoteName('client_id'), - ] - ) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('extension_id') . ' = :eid') - ->bind(':eid', $eid, ParameterType::INTEGER) - ->setLimit(1); - - $db->setQuery($query); - - $extension = $db->loadObject(); - - if (!is_object($extension)) - { - return ''; - } - - // Load language files - $basePath = JPATH_ADMINISTRATOR; - - if ($extension->client_id == 0) - { - $basePath = JPATH_SITE; - } - - $lang = Factory::getApplication()->getLanguage(); - $lang->load($extension->element, $basePath); - - // Return the localised name - return Text::_(strtoupper($extension->name)); - } - - /** - * Resets all messages for an extension - * - * @param integer $eid The extension ID whose messages we'll reset - * - * @return mixed False if we fail, a db cursor otherwise - * - * @since 3.2 - */ - public function resetMessages($eid) - { - $db = $this->getDatabase(); - $eid = (int) $eid; - - $query = $db->getQuery(true) - ->update($db->quoteName('#__postinstall_messages')) - ->set($db->quoteName('enabled') . ' = 1') - ->where($db->quoteName('extension_id') . ' = :eid') - ->bind(':eid', $eid, ParameterType::INTEGER); - $db->setQuery($query); - - $result = $db->execute(); - Factory::getCache()->clean('com_postinstall'); - - return $result; - } - - /** - * Hides all messages for an extension - * - * @param integer $eid The extension ID whose messages we'll hide - * - * @return mixed False if we fail, a db cursor otherwise - * - * @since 3.8.7 - */ - public function hideMessages($eid) - { - $db = $this->getDatabase(); - $eid = (int) $eid; - - $query = $db->getQuery(true) - ->update($db->quoteName('#__postinstall_messages')) - ->set($db->quoteName('enabled') . ' = 0') - ->where($db->quoteName('extension_id') . ' = :eid') - ->bind(':eid', $eid, ParameterType::INTEGER); - $db->setQuery($query); - - $result = $db->execute(); - Factory::getCache()->clean('com_postinstall'); - - return $result; - } - - /** - * List post-processing. This is used to run the programmatic display - * conditions against each list item and decide if we have to show it or - * not. - * - * Do note that this a core method of the RAD Layer which operates directly - * on the list it's being fed. A little touch of modern magic. - * - * @param array &$resultArray A list of items to process - * - * @return void - * - * @since 3.2 - */ - protected function onProcessList(&$resultArray) - { - $unset_keys = array(); - $language_extensions = array(); - - // Order the results DESC so the newest is on the top. - $resultArray = array_reverse($resultArray); - - foreach ($resultArray as $key => $item) - { - // Filter out messages based on dynamically loaded programmatic conditions. - if (!empty($item->condition_file) && !empty($item->condition_method)) - { - $helper = new PostinstallHelper; - $file = $helper->parsePath($item->condition_file); - - if (File::exists($file)) - { - require_once $file; - - $result = call_user_func($item->condition_method); - - if ($result === false) - { - $unset_keys[] = $key; - } - } - } - - // Load the necessary language files. - if (!empty($item->language_extension)) - { - $hash = $item->language_client_id . '-' . $item->language_extension; - - if (!in_array($hash, $language_extensions)) - { - $language_extensions[] = $hash; - Factory::getApplication()->getLanguage()->load($item->language_extension, $item->language_client_id == 0 ? JPATH_SITE : JPATH_ADMINISTRATOR); - } - } - } - - if (!empty($unset_keys)) - { - foreach ($unset_keys as $key) - { - unset($resultArray[$key]); - } - } - } - - /** - * Get the dropdown options for the list of component with post-installation messages - * - * @since 3.4 - * - * @return array Compatible with JHtmlSelect::genericList - */ - public function getComponentOptions() - { - $db = $this->getDatabase(); - - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__postinstall_messages')) - ->group($db->quoteName('extension_id')); - $db->setQuery($query); - $extension_ids = $db->loadColumn(); - - $options = array(); - - Factory::getApplication()->getLanguage()->load('files_joomla.sys', JPATH_SITE, null, false, false); - - foreach ($extension_ids as $eid) - { - $options[] = HTMLHelper::_('select.option', $eid, $this->getExtensionName($eid)); - } - - return $options; - } - - /** - * Adds or updates a post-installation message (PIM) definition. You can use this in your post-installation script using this code: - * - * require_once JPATH_LIBRARIES . '/fof/include.php'; - * FOFModel::getTmpInstance('Messages', 'PostinstallModel')->addPostInstallationMessage($options); - * - * The $options array contains the following mandatory keys: - * - * extension_id The numeric ID of the extension this message is for (see the #__extensions table) - * - * type One of message, link or action. Their meaning is: - * message Informative message. The user can dismiss it. - * link The action button links to a URL. The URL is defined in the action parameter. - * action A PHP action takes place when the action button is clicked. You need to specify the action_file - * (RAD path to the PHP file) and action (PHP function name) keys. See below for more information. - * - * title_key The Text language key for the title of this PIM. - * Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_TITLE - * - * description_key The Text language key for the main body (description) of this PIM - * Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_DESCRIPTION - * - * action_key The Text language key for the action button. Ignored and not required when type=message - * Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_ACTION - * - * language_extension The extension name which holds the language keys used above. - * For example, com_foobar, mod_something, plg_system_whatever, tpl_mytemplate - * - * language_client_id Should we load the frontend (0) or backend (1) language keys? - * - * version_introduced Which was the version of your extension where this message appeared for the first time? - * Example: 3.2.1 - * - * enabled Must be 1 for this message to be enabled. If you omit it, it defaults to 1. - * - * condition_file The RAD path to a PHP file containing a PHP function which determines whether this message should be shown to - * the user. @see FOFTemplateUtils::parsePath() for RAD path format. Joomla! will include this file before calling - * the condition_method. - * Example: admin://components/com_foobar/helpers/postinstall.php - * - * condition_method The name of a PHP function which will be used to determine whether to show this message to the user. This must be - * a simple PHP user function (not a class method, static method etc) which returns true to show the message and false - * to hide it. This function is defined in the condition_file. - * Example: com_foobar_postinstall_messageone_condition - * - * When type=message no additional keys are required. - * - * When type=link the following additional keys are required: - * - * action The URL which will open when the user clicks on the PIM's action button - * Example: index.php?option=com_foobar&view=tools&task=installSampleData - * - * When type=action the following additional keys are required: - * - * action_file The RAD path to a PHP file containing a PHP function which performs the action of this PIM. @see FOFTemplateUtils::parsePath() - * for RAD path format. Joomla! will include this file before calling the function defined in the action key below. - * Example: admin://components/com_foobar/helpers/postinstall.php - * - * action The name of a PHP function which will be used to run the action of this PIM. This must be a simple PHP user function - * (not a class method, static method etc) which returns no result. - * Example: com_foobar_postinstall_messageone_action - * - * @param array $options See description - * - * @return $this - * - * @throws \Exception - */ - public function addPostInstallationMessage(array $options) - { - // Make sure there are options set - if (!is_array($options)) - { - throw new \Exception('Post-installation message definitions must be of type array', 500); - } - - // Initialise array keys - $defaultOptions = array( - 'extension_id' => '', - 'type' => '', - 'title_key' => '', - 'description_key' => '', - 'action_key' => '', - 'language_extension' => '', - 'language_client_id' => '', - 'action_file' => '', - 'action' => '', - 'condition_file' => '', - 'condition_method' => '', - 'version_introduced' => '', - 'enabled' => '1', - ); - - $options = array_merge($defaultOptions, $options); - - // Array normalisation. Removes array keys not belonging to a definition. - $defaultKeys = array_keys($defaultOptions); - $allKeys = array_keys($options); - $extraKeys = array_diff($allKeys, $defaultKeys); - - if (!empty($extraKeys)) - { - foreach ($extraKeys as $key) - { - unset($options[$key]); - } - } - - // Normalisation of integer values - $options['extension_id'] = (int) $options['extension_id']; - $options['language_client_id'] = (int) $options['language_client_id']; - $options['enabled'] = (int) $options['enabled']; - - // Normalisation of 0/1 values - foreach (array('language_client_id', 'enabled') as $key) - { - $options[$key] = $options[$key] ? 1 : 0; - } - - // Make sure there's an extension_id - if (!(int) $options['extension_id']) - { - throw new \Exception('Post-installation message definitions need an extension_id', 500); - } - - // Make sure there's a valid type - if (!in_array($options['type'], array('message', 'link', 'action'))) - { - throw new \Exception('Post-installation message definitions need to declare a type of message, link or action', 500); - } - - // Make sure there's a title key - if (empty($options['title_key'])) - { - throw new \Exception('Post-installation message definitions need a title key', 500); - } - - // Make sure there's a description key - if (empty($options['description_key'])) - { - throw new \Exception('Post-installation message definitions need a description key', 500); - } - - // If the type is anything other than message you need an action key - if (($options['type'] != 'message') && empty($options['action_key'])) - { - throw new \Exception('Post-installation message definitions need an action key when they are of type "' . $options['type'] . '"', 500); - } - - // You must specify the language extension - if (empty($options['language_extension'])) - { - throw new \Exception('Post-installation message definitions need to specify which extension contains their language keys', 500); - } - - // The action file and method are only required for the "action" type - if ($options['type'] == 'action') - { - if (empty($options['action_file'])) - { - throw new \Exception('Post-installation message definitions need an action file when they are of type "action"', 500); - } - - $helper = new PostinstallHelper; - $file_path = $helper->parsePath($options['action_file']); - - if (!@is_file($file_path)) - { - throw new \Exception('The action file ' . $options['action_file'] . ' of your post-installation message definition does not exist', 500); - } - - if (empty($options['action'])) - { - throw new \Exception('Post-installation message definitions need an action (function name) when they are of type "action"', 500); - } - } - - if ($options['type'] == 'link') - { - if (empty($options['link'])) - { - throw new \Exception('Post-installation message definitions need an action (URL) when they are of type "link"', 500); - } - } - - // The condition file and method are only required when the type is not "message" - if ($options['type'] != 'message') - { - if (empty($options['condition_file'])) - { - throw new \Exception('Post-installation message definitions need a condition file when they are of type "' . $options['type'] . '"', 500); - } - - $helper = new PostinstallHelper; - $file_path = $helper->parsePath($options['condition_file']); - - if (!@is_file($file_path)) - { - throw new \Exception('The condition file ' . $options['condition_file'] . ' of your post-installation message definition does not exist', 500); - } - - if (empty($options['condition_method'])) - { - throw new \Exception( - 'Post-installation message definitions need a condition method (function name) when they are of type "' - . $options['type'] . '"', - 500 - ); - } - } - - // Check if the definition exists - $table = $this->getTable(); - $tableName = $table->getTableName(); - $extensionId = (int) $options['extension_id']; - - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName($tableName)) - ->where( - [ - $db->quoteName('extension_id') . ' = :extensionId', - $db->quoteName('type') . ' = :type', - $db->quoteName('title_key') . ' = :titleKey', - ] - ) - ->bind(':extensionId', $extensionId, ParameterType::INTEGER) - ->bind(':type', $options['type']) - ->bind(':titleKey', $options['title_key']); - - $existingRow = $db->setQuery($query)->loadAssoc(); - - // Is the existing definition the same as the one we're trying to save? - if (!empty($existingRow)) - { - $same = true; - - foreach ($options as $k => $v) - { - if ($existingRow[$k] != $v) - { - $same = false; - break; - } - } - - // Trying to add the same row as the existing one; quit - if ($same) - { - return $this; - } - - // Otherwise it's not the same row. Remove the old row before insert a new one. - $query = $db->getQuery(true) - ->delete($db->quoteName($tableName)) - ->where( - [ - $db->quoteName('extension_id') . ' = :extensionId', - $db->quoteName('type') . ' = :type', - $db->quoteName('title_key') . ' = :titleKey', - ] - ) - ->bind(':extensionId', $extensionId, ParameterType::INTEGER) - ->bind(':type', $options['type']) - ->bind(':titleKey', $options['title_key']); - - $db->setQuery($query)->execute(); - } - - // Insert the new row - $options = (object) $options; - $db->insertObject($tableName, $options); - Factory::getCache()->clean('com_postinstall'); - - return $this; - } - - /** - * Returns the library extension ID. - * - * @return integer - * - * @since 4.0.0 - */ - public function getJoomlaFilesExtensionId() - { - return ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; - } + /** + * Method to auto-populate the state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the + * configuration flag to ignore the request is set. + * + * @return void + * + * @note Calling getState in this method will result in recursion. + * @since 4.0.0 + */ + protected function populateState() + { + parent::populateState(); + + $eid = (int) Factory::getApplication()->input->getInt('eid'); + + if ($eid) { + $this->setState('eid', $eid); + } + } + + /** + * Gets an item with the given id from the database + * + * @param integer $id The item id + * + * @return Object + * + * @since 3.2 + */ + public function getItem($id) + { + $db = $this->getDatabase(); + $id = (int) $id; + + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('postinstall_message_id'), + $db->quoteName('extension_id'), + $db->quoteName('title_key'), + $db->quoteName('description_key'), + $db->quoteName('action_key'), + $db->quoteName('language_extension'), + $db->quoteName('language_client_id'), + $db->quoteName('type'), + $db->quoteName('action_file'), + $db->quoteName('action'), + $db->quoteName('condition_file'), + $db->quoteName('condition_method'), + $db->quoteName('version_introduced'), + $db->quoteName('enabled'), + ] + ) + ->from($db->quoteName('#__postinstall_messages')) + ->where($db->quoteName('postinstall_message_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + $db->setQuery($query); + + $result = $db->loadObject(); + + return $result; + } + + /** + * Unpublishes specified post-install message + * + * @param integer $id The message id + * + * @return void + */ + public function unpublishMessage($id) + { + $db = $this->getDatabase(); + $id = (int) $id; + + $query = $db->getQuery(true); + $query + ->update($db->quoteName('#__postinstall_messages')) + ->set($db->quoteName('enabled') . ' = 0') + ->where($db->quoteName('postinstall_message_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + Factory::getCache()->clean('com_postinstall'); + } + + /** + * Archives specified post-install message + * + * @param integer $id The message id + * + * @return void + * + * @since 4.2.0 + */ + public function archiveMessage($id) + { + $db = $this->getDatabase(); + $id = (int) $id; + + $query = $db->getQuery(true); + $query + ->update($db->quoteName('#__postinstall_messages')) + ->set($db->quoteName('enabled') . ' = 2') + ->where($db->quoteName('postinstall_message_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + Factory::getCache()->clean('com_postinstall'); + } + + /** + * Republishes specified post-install message + * + * @param integer $id The message id + * + * @return void + * + * @since 4.2.0 + */ + public function republishMessage($id) + { + $db = $this->getDatabase(); + $id = (int) $id; + + $query = $db->getQuery(true); + $query + ->update($db->quoteName('#__postinstall_messages')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('postinstall_message_id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + Factory::getCache()->clean('com_postinstall'); + } + + /** + * Returns a list of messages from the #__postinstall_messages table + * + * @return array + * + * @since 3.2 + */ + public function getItems() + { + // Add a forced extension filtering to the list + $eid = (int) $this->getState('eid', $this->getJoomlaFilesExtensionId()); + + // Build a cache ID for the resulting data object + $cacheId = 'postinstall_messages.' . $eid; + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('postinstall_message_id'), + $db->quoteName('extension_id'), + $db->quoteName('title_key'), + $db->quoteName('description_key'), + $db->quoteName('action_key'), + $db->quoteName('language_extension'), + $db->quoteName('language_client_id'), + $db->quoteName('type'), + $db->quoteName('action_file'), + $db->quoteName('action'), + $db->quoteName('condition_file'), + $db->quoteName('condition_method'), + $db->quoteName('version_introduced'), + $db->quoteName('enabled'), + ] + ) + ->from($db->quoteName('#__postinstall_messages')); + $query->where($db->quoteName('extension_id') . ' = :eid') + ->bind(':eid', $eid, ParameterType::INTEGER); + + // Force filter only enabled messages + $query->whereIn($db->quoteName('enabled'), [1, 2]); + $db->setQuery($query); + + try { + /** @var CallbackController $cache */ + $cache = $this->getCacheControllerFactory()->createCacheController('callback', ['defaultgroup' => 'com_postinstall']); + + $result = $cache->get(array($db, 'loadObjectList'), array(), md5($cacheId), false); + } catch (\RuntimeException $e) { + $app = Factory::getApplication(); + $app->getLogger()->warning( + Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $e->getMessage()), + array('category' => 'jerror') + ); + + return array(); + } + + $this->onProcessList($result); + + return $result; + } + + /** + * Returns a count of all enabled messages from the #__postinstall_messages table + * + * @return integer + * + * @since 4.0.0 + */ + public function getItemsCount() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $query->select( + [ + $db->quoteName('language_extension'), + $db->quoteName('language_client_id'), + $db->quoteName('condition_file'), + $db->quoteName('condition_method'), + ] + ) + ->from($db->quoteName('#__postinstall_messages')); + + // Force filter only enabled messages + $query->where($db->quoteName('enabled') . ' = 1'); + $db->setQuery($query); + + try { + /** @var CallbackController $cache */ + $cache = Factory::getContainer()->get(CacheControllerFactoryInterface::class) + ->createCacheController('callback', ['defaultgroup' => 'com_postinstall']); + + // Get the resulting data object for cache ID 'all.1' from com_postinstall group. + $result = $cache->get(array($db, 'loadObjectList'), array(), md5('all.1'), false); + } catch (\RuntimeException $e) { + $app = Factory::getApplication(); + $app->getLogger()->warning( + Text::sprintf('JLIB_APPLICATION_ERROR_MODULE_LOAD', $e->getMessage()), + array('category' => 'jerror') + ); + + return 0; + } + + $this->onProcessList($result); + + return \count($result); + } + + /** + * Returns the name of an extension, as registered in the #__extensions table + * + * @param integer $eid The extension ID + * + * @return string The extension name + * + * @since 3.2 + */ + public function getExtensionName($eid) + { + // Load the extension's information from the database + $db = $this->getDatabase(); + $eid = (int) $eid; + + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('name'), + $db->quoteName('element'), + $db->quoteName('client_id'), + ] + ) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('extension_id') . ' = :eid') + ->bind(':eid', $eid, ParameterType::INTEGER) + ->setLimit(1); + + $db->setQuery($query); + + $extension = $db->loadObject(); + + if (!is_object($extension)) { + return ''; + } + + // Load language files + $basePath = JPATH_ADMINISTRATOR; + + if ($extension->client_id == 0) { + $basePath = JPATH_SITE; + } + + $lang = Factory::getApplication()->getLanguage(); + $lang->load($extension->element, $basePath); + + // Return the localised name + return Text::_(strtoupper($extension->name)); + } + + /** + * Resets all messages for an extension + * + * @param integer $eid The extension ID whose messages we'll reset + * + * @return mixed False if we fail, a db cursor otherwise + * + * @since 3.2 + */ + public function resetMessages($eid) + { + $db = $this->getDatabase(); + $eid = (int) $eid; + + $query = $db->getQuery(true) + ->update($db->quoteName('#__postinstall_messages')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('extension_id') . ' = :eid') + ->bind(':eid', $eid, ParameterType::INTEGER); + $db->setQuery($query); + + $result = $db->execute(); + Factory::getCache()->clean('com_postinstall'); + + return $result; + } + + /** + * Hides all messages for an extension + * + * @param integer $eid The extension ID whose messages we'll hide + * + * @return mixed False if we fail, a db cursor otherwise + * + * @since 3.8.7 + */ + public function hideMessages($eid) + { + $db = $this->getDatabase(); + $eid = (int) $eid; + + $query = $db->getQuery(true) + ->update($db->quoteName('#__postinstall_messages')) + ->set($db->quoteName('enabled') . ' = 0') + ->where($db->quoteName('extension_id') . ' = :eid') + ->bind(':eid', $eid, ParameterType::INTEGER); + $db->setQuery($query); + + $result = $db->execute(); + Factory::getCache()->clean('com_postinstall'); + + return $result; + } + + /** + * List post-processing. This is used to run the programmatic display + * conditions against each list item and decide if we have to show it or + * not. + * + * Do note that this a core method of the RAD Layer which operates directly + * on the list it's being fed. A little touch of modern magic. + * + * @param array &$resultArray A list of items to process + * + * @return void + * + * @since 3.2 + */ + protected function onProcessList(&$resultArray) + { + $unset_keys = array(); + $language_extensions = array(); + + // Order the results DESC so the newest is on the top. + $resultArray = array_reverse($resultArray); + + foreach ($resultArray as $key => $item) { + // Filter out messages based on dynamically loaded programmatic conditions. + if (!empty($item->condition_file) && !empty($item->condition_method)) { + $helper = new PostinstallHelper(); + $file = $helper->parsePath($item->condition_file); + + if (File::exists($file)) { + require_once $file; + + $result = call_user_func($item->condition_method); + + if ($result === false) { + $unset_keys[] = $key; + } + } + } + + // Load the necessary language files. + if (!empty($item->language_extension)) { + $hash = $item->language_client_id . '-' . $item->language_extension; + + if (!in_array($hash, $language_extensions)) { + $language_extensions[] = $hash; + Factory::getApplication()->getLanguage()->load($item->language_extension, $item->language_client_id == 0 ? JPATH_SITE : JPATH_ADMINISTRATOR); + } + } + } + + if (!empty($unset_keys)) { + foreach ($unset_keys as $key) { + unset($resultArray[$key]); + } + } + } + + /** + * Get the dropdown options for the list of component with post-installation messages + * + * @since 3.4 + * + * @return array Compatible with JHtmlSelect::genericList + */ + public function getComponentOptions() + { + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__postinstall_messages')) + ->group($db->quoteName('extension_id')); + $db->setQuery($query); + $extension_ids = $db->loadColumn(); + + $options = array(); + + Factory::getApplication()->getLanguage()->load('files_joomla.sys', JPATH_SITE, null, false, false); + + foreach ($extension_ids as $eid) { + $options[] = HTMLHelper::_('select.option', $eid, $this->getExtensionName($eid)); + } + + return $options; + } + + /** + * Adds or updates a post-installation message (PIM) definition. You can use this in your post-installation script using this code: + * + * require_once JPATH_LIBRARIES . '/fof/include.php'; + * FOFModel::getTmpInstance('Messages', 'PostinstallModel')->addPostInstallationMessage($options); + * + * The $options array contains the following mandatory keys: + * + * extension_id The numeric ID of the extension this message is for (see the #__extensions table) + * + * type One of message, link or action. Their meaning is: + * message Informative message. The user can dismiss it. + * link The action button links to a URL. The URL is defined in the action parameter. + * action A PHP action takes place when the action button is clicked. You need to specify the action_file + * (RAD path to the PHP file) and action (PHP function name) keys. See below for more information. + * + * title_key The Text language key for the title of this PIM. + * Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_TITLE + * + * description_key The Text language key for the main body (description) of this PIM + * Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_DESCRIPTION + * + * action_key The Text language key for the action button. Ignored and not required when type=message + * Example: COM_FOOBAR_POSTINSTALL_MESSAGEONE_ACTION + * + * language_extension The extension name which holds the language keys used above. + * For example, com_foobar, mod_something, plg_system_whatever, tpl_mytemplate + * + * language_client_id Should we load the frontend (0) or backend (1) language keys? + * + * version_introduced Which was the version of your extension where this message appeared for the first time? + * Example: 3.2.1 + * + * enabled Must be 1 for this message to be enabled. If you omit it, it defaults to 1. + * + * condition_file The RAD path to a PHP file containing a PHP function which determines whether this message should be shown to + * the user. @see FOFTemplateUtils::parsePath() for RAD path format. Joomla! will include this file before calling + * the condition_method. + * Example: admin://components/com_foobar/helpers/postinstall.php + * + * condition_method The name of a PHP function which will be used to determine whether to show this message to the user. This must be + * a simple PHP user function (not a class method, static method etc) which returns true to show the message and false + * to hide it. This function is defined in the condition_file. + * Example: com_foobar_postinstall_messageone_condition + * + * When type=message no additional keys are required. + * + * When type=link the following additional keys are required: + * + * action The URL which will open when the user clicks on the PIM's action button + * Example: index.php?option=com_foobar&view=tools&task=installSampleData + * + * When type=action the following additional keys are required: + * + * action_file The RAD path to a PHP file containing a PHP function which performs the action of this PIM. @see FOFTemplateUtils::parsePath() + * for RAD path format. Joomla! will include this file before calling the function defined in the action key below. + * Example: admin://components/com_foobar/helpers/postinstall.php + * + * action The name of a PHP function which will be used to run the action of this PIM. This must be a simple PHP user function + * (not a class method, static method etc) which returns no result. + * Example: com_foobar_postinstall_messageone_action + * + * @param array $options See description + * + * @return $this + * + * @throws \Exception + */ + public function addPostInstallationMessage(array $options) + { + // Make sure there are options set + if (!is_array($options)) { + throw new \Exception('Post-installation message definitions must be of type array', 500); + } + + // Initialise array keys + $defaultOptions = array( + 'extension_id' => '', + 'type' => '', + 'title_key' => '', + 'description_key' => '', + 'action_key' => '', + 'language_extension' => '', + 'language_client_id' => '', + 'action_file' => '', + 'action' => '', + 'condition_file' => '', + 'condition_method' => '', + 'version_introduced' => '', + 'enabled' => '1', + ); + + $options = array_merge($defaultOptions, $options); + + // Array normalisation. Removes array keys not belonging to a definition. + $defaultKeys = array_keys($defaultOptions); + $allKeys = array_keys($options); + $extraKeys = array_diff($allKeys, $defaultKeys); + + if (!empty($extraKeys)) { + foreach ($extraKeys as $key) { + unset($options[$key]); + } + } + + // Normalisation of integer values + $options['extension_id'] = (int) $options['extension_id']; + $options['language_client_id'] = (int) $options['language_client_id']; + $options['enabled'] = (int) $options['enabled']; + + // Normalisation of 0/1 values + foreach (array('language_client_id', 'enabled') as $key) { + $options[$key] = $options[$key] ? 1 : 0; + } + + // Make sure there's an extension_id + if (!(int) $options['extension_id']) { + throw new \Exception('Post-installation message definitions need an extension_id', 500); + } + + // Make sure there's a valid type + if (!in_array($options['type'], array('message', 'link', 'action'))) { + throw new \Exception('Post-installation message definitions need to declare a type of message, link or action', 500); + } + + // Make sure there's a title key + if (empty($options['title_key'])) { + throw new \Exception('Post-installation message definitions need a title key', 500); + } + + // Make sure there's a description key + if (empty($options['description_key'])) { + throw new \Exception('Post-installation message definitions need a description key', 500); + } + + // If the type is anything other than message you need an action key + if (($options['type'] != 'message') && empty($options['action_key'])) { + throw new \Exception('Post-installation message definitions need an action key when they are of type "' . $options['type'] . '"', 500); + } + + // You must specify the language extension + if (empty($options['language_extension'])) { + throw new \Exception('Post-installation message definitions need to specify which extension contains their language keys', 500); + } + + // The action file and method are only required for the "action" type + if ($options['type'] == 'action') { + if (empty($options['action_file'])) { + throw new \Exception('Post-installation message definitions need an action file when they are of type "action"', 500); + } + + $helper = new PostinstallHelper(); + $file_path = $helper->parsePath($options['action_file']); + + if (!@is_file($file_path)) { + throw new \Exception('The action file ' . $options['action_file'] . ' of your post-installation message definition does not exist', 500); + } + + if (empty($options['action'])) { + throw new \Exception('Post-installation message definitions need an action (function name) when they are of type "action"', 500); + } + } + + if ($options['type'] == 'link') { + if (empty($options['link'])) { + throw new \Exception('Post-installation message definitions need an action (URL) when they are of type "link"', 500); + } + } + + // The condition file and method are only required when the type is not "message" + if ($options['type'] != 'message') { + if (empty($options['condition_file'])) { + throw new \Exception('Post-installation message definitions need a condition file when they are of type "' . $options['type'] . '"', 500); + } + + $helper = new PostinstallHelper(); + $file_path = $helper->parsePath($options['condition_file']); + + if (!@is_file($file_path)) { + throw new \Exception('The condition file ' . $options['condition_file'] . ' of your post-installation message definition does not exist', 500); + } + + if (empty($options['condition_method'])) { + throw new \Exception( + 'Post-installation message definitions need a condition method (function name) when they are of type "' + . $options['type'] . '"', + 500 + ); + } + } + + // Check if the definition exists + $table = $this->getTable(); + $tableName = $table->getTableName(); + $extensionId = (int) $options['extension_id']; + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName($tableName)) + ->where( + [ + $db->quoteName('extension_id') . ' = :extensionId', + $db->quoteName('type') . ' = :type', + $db->quoteName('title_key') . ' = :titleKey', + ] + ) + ->bind(':extensionId', $extensionId, ParameterType::INTEGER) + ->bind(':type', $options['type']) + ->bind(':titleKey', $options['title_key']); + + $existingRow = $db->setQuery($query)->loadAssoc(); + + // Is the existing definition the same as the one we're trying to save? + if (!empty($existingRow)) { + $same = true; + + foreach ($options as $k => $v) { + if ($existingRow[$k] != $v) { + $same = false; + break; + } + } + + // Trying to add the same row as the existing one; quit + if ($same) { + return $this; + } + + // Otherwise it's not the same row. Remove the old row before insert a new one. + $query = $db->getQuery(true) + ->delete($db->quoteName($tableName)) + ->where( + [ + $db->quoteName('extension_id') . ' = :extensionId', + $db->quoteName('type') . ' = :type', + $db->quoteName('title_key') . ' = :titleKey', + ] + ) + ->bind(':extensionId', $extensionId, ParameterType::INTEGER) + ->bind(':type', $options['type']) + ->bind(':titleKey', $options['title_key']); + + $db->setQuery($query)->execute(); + } + + // Insert the new row + $options = (object) $options; + $db->insertObject($tableName, $options); + Factory::getCache()->clean('com_postinstall'); + + return $this; + } + + /** + * Returns the library extension ID. + * + * @return integer + * + * @since 4.0.0 + */ + public function getJoomlaFilesExtensionId() + { + return ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; + } } diff --git a/administrator/components/com_postinstall/src/View/Messages/HtmlView.php b/administrator/components/com_postinstall/src/View/Messages/HtmlView.php index fb5190ec072b1..2bdca27ff4e59 100644 --- a/administrator/components/com_postinstall/src/View/Messages/HtmlView.php +++ b/administrator/components/com_postinstall/src/View/Messages/HtmlView.php @@ -1,4 +1,5 @@ getModel(); + /** + * Executes before rendering the page for the Browse task. + * + * @param string $tpl Subtemplate to use + * + * @return void + * + * @since 3.2 + */ + public function display($tpl = null) + { + /** @var MessagesModel $model */ + $model = $this->getModel(); - $this->items = $model->getItems(); + $this->items = $model->getItems(); - if (!\count($this->items)) - { - $this->setLayout('emptystate'); - } + if (!\count($this->items)) { + $this->setLayout('emptystate'); + } - $this->joomlaFilesExtensionId = $model->getJoomlaFilesExtensionId(); - $this->eid = (int) $model->getState('eid', $this->joomlaFilesExtensionId); + $this->joomlaFilesExtensionId = $model->getJoomlaFilesExtensionId(); + $this->eid = (int) $model->getState('eid', $this->joomlaFilesExtensionId); - if (empty($this->eid)) - { - $this->eid = $this->joomlaFilesExtensionId; - } + if (empty($this->eid)) { + $this->eid = $this->joomlaFilesExtensionId; + } - $this->toolbar(); + $this->toolbar(); - $this->token = Factory::getSession()->getFormToken(); - $this->extension_options = $model->getComponentOptions(); + $this->token = Factory::getSession()->getFormToken(); + $this->extension_options = $model->getComponentOptions(); - ToolbarHelper::title(Text::sprintf('COM_POSTINSTALL_MESSAGES_TITLE', $model->getExtensionName($this->eid)), 'bell'); + ToolbarHelper::title(Text::sprintf('COM_POSTINSTALL_MESSAGES_TITLE', $model->getExtensionName($this->eid)), 'bell'); - parent::display($tpl); - } + parent::display($tpl); + } - /** - * displays the toolbar - * - * @return void - * - * @since 3.6 - */ - private function toolbar() - { - $toolbar = Toolbar::getInstance('toolbar'); + /** + * displays the toolbar + * + * @return void + * + * @since 3.6 + */ + private function toolbar() + { + $toolbar = Toolbar::getInstance('toolbar'); - if (!empty($this->items)) - { - $toolbar->unpublish('message.hideAll', 'COM_POSTINSTALL_HIDE_ALL_MESSAGES'); - } + if (!empty($this->items)) { + $toolbar->unpublish('message.hideAll', 'COM_POSTINSTALL_HIDE_ALL_MESSAGES'); + } - // Options button. - if ($this->getCurrentUser()->authorise('core.admin', 'com_postinstall')) - { - $toolbar->preferences('com_postinstall'); - $toolbar->help('Post-installation_Messages_for_Joomla_CMS'); - } - } + // Options button. + if ($this->getCurrentUser()->authorise('core.admin', 'com_postinstall')) { + $toolbar->preferences('com_postinstall'); + $toolbar->help('Post-installation_Messages_for_Joomla_CMS'); + } + } } diff --git a/administrator/components/com_postinstall/tmpl/messages/default.php b/administrator/components/com_postinstall/tmpl/messages/default.php index cb7fd12316a7c..84cf2892442b2 100644 --- a/administrator/components/com_postinstall/tmpl/messages/default.php +++ b/administrator/components/com_postinstall/tmpl/messages/default.php @@ -1,4 +1,5 @@
- - - - - extension_options, 'eid', array('onchange' => 'this.form.submit()', 'class' => 'form-select'), 'value', 'text', $this->eid, 'eid'); ?> + + + + + extension_options, 'eid', array('onchange' => 'this.form.submit()', 'class' => 'form-select'), 'value', 'text', $this->eid, 'eid'); ?>
items as $item) : ?> - enabled === 1) : ?> -
-
-

title_key); ?>

-

- version_introduced); ?> -

-
- description_key); ?> - type !== 'message') : ?> - - action_key); ?> - - - getIdentity()->authorise('core.edit.state', 'com_postinstall')) : ?> - - - - - - - -
-
-
- enabled === 2) : ?> -
-
-

title_key); ?>

-
- getIdentity()->authorise('core.edit.state', 'com_postinstall')) : ?> - - - - - - - -
-
-
- + enabled === 1) : ?> +
+
+

title_key); ?>

+

+ version_introduced); ?> +

+
+ description_key); ?> + type !== 'message') : ?> + + action_key); ?> + + + getIdentity()->authorise('core.edit.state', 'com_postinstall')) : ?> + + + + + + + +
+
+
+ enabled === 2) : ?> +
+
+

title_key); ?>

+
+ getIdentity()->authorise('core.edit.state', 'com_postinstall')) : ?> + + + + + + + +
+
+
+ diff --git a/administrator/components/com_postinstall/tmpl/messages/emptystate.php b/administrator/components/com_postinstall/tmpl/messages/emptystate.php index f58a1d4c0ed00..4c2b43aca30f3 100644 --- a/administrator/components/com_postinstall/tmpl/messages/emptystate.php +++ b/administrator/components/com_postinstall/tmpl/messages/emptystate.php @@ -1,4 +1,5 @@
- - - - - extension_options, 'eid', array('onchange' => 'this.form.submit()', 'class' => 'form-select'), 'value', 'text', $this->eid, 'eid'); ?> + + + + + extension_options, 'eid', array('onchange' => 'this.form.submit()', 'class' => 'form-select'), 'value', 'text', $this->eid, 'eid'); ?>
'COM_POSTINSTALL', - 'formURL' => 'index.php?option=com_postinstall', - 'icon' => 'icon-bell', - 'createURL' => 'index.php?option=com_postinstall&view=messages&task=message.reset&eid=' . $this->eid . '&' . $this->token . '=1', + 'textPrefix' => 'COM_POSTINSTALL', + 'formURL' => 'index.php?option=com_postinstall', + 'icon' => 'icon-bell', + 'createURL' => 'index.php?option=com_postinstall&view=messages&task=message.reset&eid=' . $this->eid . '&' . $this->token . '=1', ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_privacy/services/provider.php b/administrator/components/com_privacy/services/provider.php index fc62cfaf46de2..d4c329ecda62c 100644 --- a/administrator/components/com_privacy/services/provider.php +++ b/administrator/components/com_privacy/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Privacy')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Privacy')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Privacy')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Privacy')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Privacy')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Privacy')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new PrivacyComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new PrivacyComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_privacy/src/Controller/ConsentsController.php b/administrator/components/com_privacy/src/Controller/ConsentsController.php index 10976f3d2c2a3..fa8cda8a0a5d4 100644 --- a/administrator/components/com_privacy/src/Controller/ConsentsController.php +++ b/administrator/components/com_privacy/src/Controller/ConsentsController.php @@ -1,4 +1,5 @@ checkToken(); - - $ids = (array) $this->input->get('cid', [], 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), CMSApplication::MSG_ERROR); - } - else - { - /** @var ConsentsModel $model */ - $model = $this->getModel(); - - if (!$model->invalidate($ids)) - { - $this->setMessage($model->getError()); - } - else - { - $this->setMessage(Text::plural('COM_PRIVACY_N_CONSENTS_INVALIDATED', count($ids))); - } - } - - $this->setRedirect(Route::_('index.php?option=com_privacy&view=consents', false)); - } - - /** - * Method to invalidate all consents of a specific subject. - * - * @return void - * - * @since 3.9.0 - */ - public function invalidateAll() - { - // Check for request forgeries - $this->checkToken(); - - $filters = $this->input->get('filter', [], 'array'); - - $this->setRedirect(Route::_('index.php?option=com_privacy&view=consents', false)); - - if (isset($filters['subject']) && $filters['subject'] != '') - { - $subject = $filters['subject']; - } - else - { - $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED')); - - return; - } - - /** @var ConsentsModel $model */ - $model = $this->getModel(); - - if (!$model->invalidateAll($subject)) - { - $this->setMessage($model->getError()); - } - - $this->setMessage(Text::_('COM_PRIVACY_CONSENTS_INVALIDATED_ALL')); - } + /** + * Method to invalidate specific consents. + * + * @return void + * + * @since 3.9.0 + */ + public function invalidate() + { + // Check for request forgeries + $this->checkToken(); + + $ids = (array) $this->input->get('cid', [], 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), CMSApplication::MSG_ERROR); + } else { + /** @var ConsentsModel $model */ + $model = $this->getModel(); + + if (!$model->invalidate($ids)) { + $this->setMessage($model->getError()); + } else { + $this->setMessage(Text::plural('COM_PRIVACY_N_CONSENTS_INVALIDATED', count($ids))); + } + } + + $this->setRedirect(Route::_('index.php?option=com_privacy&view=consents', false)); + } + + /** + * Method to invalidate all consents of a specific subject. + * + * @return void + * + * @since 3.9.0 + */ + public function invalidateAll() + { + // Check for request forgeries + $this->checkToken(); + + $filters = $this->input->get('filter', [], 'array'); + + $this->setRedirect(Route::_('index.php?option=com_privacy&view=consents', false)); + + if (isset($filters['subject']) && $filters['subject'] != '') { + $subject = $filters['subject']; + } else { + $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED')); + + return; + } + + /** @var ConsentsModel $model */ + $model = $this->getModel(); + + if (!$model->invalidateAll($subject)) { + $this->setMessage($model->getError()); + } + + $this->setMessage(Text::_('COM_PRIVACY_CONSENTS_INVALIDATED_ALL')); + } } diff --git a/administrator/components/com_privacy/src/Controller/DisplayController.php b/administrator/components/com_privacy/src/Controller/DisplayController.php index 33d3fbc8dd4d3..48d5a1ab71622 100644 --- a/administrator/components/com_privacy/src/Controller/DisplayController.php +++ b/administrator/components/com_privacy/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ app->getDocument(); - - // Set the default view name and format from the Request. - $vName = $this->input->get('view', $this->default_view); - $vFormat = $document->getType(); - $lName = $this->input->get('layout', 'default', 'string'); - - // Get and render the view. - if ($view = $this->getView($vName, $vFormat)) - { - $model = $this->getModel($vName); - $view->setModel($model, true); - - if ($vName === 'request') - { - // For the default layout, we need to also push the action logs model into the view - if ($lName === 'default') - { - $logsModel = $this->app->bootComponent('com_actionlogs') - ->getMVCFactory()->createModel('Actionlogs', 'Administrator', ['ignore_request' => true]); - - // Set default ordering for the context - $logsModel->setState('list.fullordering', 'a.log_date DESC'); - - // And push the model into the view - $view->setModel($logsModel, false); - } - - // For the edit layout, if mail sending is disabled then redirect back to the list view as the form is unusable in this state - if ($lName === 'edit' && !$this->app->get('mailonline', 1)) - { - $this->setRedirect( - Route::_('index.php?option=com_privacy&view=requests', false), - Text::_('COM_PRIVACY_WARNING_CANNOT_CREATE_REQUEST_WHEN_SENDMAIL_DISABLED'), - 'warning' - ); - - return $this; - } - } - - $view->setLayout($lName); - - // Push document object into the view. - $view->document = $document; - - $view->display(); - } - - return $this; - } - - /** - * Fetch and report number urgent privacy requests in JSON format, for AJAX requests - * - * @return void - * - * @since 3.9.0 - */ - public function getNumberUrgentRequests() - { - // Check for a valid token. If invalid, send a 403 with the error message. - if (!Session::checkToken('get')) - { - $this->app->setHeader('status', 403, true); - $this->app->sendHeaders(); - echo new JsonResponse(new \Exception(Text::_('JINVALID_TOKEN'), 403)); - $this->app->close(); - } - - /** @var RequestsModel $model */ - $model = $this->getModel('requests'); - $numberUrgentRequests = $model->getNumberUrgentRequests(); - - echo new JsonResponse(['number_urgent_requests' => $numberUrgentRequests]); - } + /** + * The default view. + * + * @var string + * @since 3.9.0 + */ + protected $default_view = 'requests'; + + /** + * Method to display a view. + * + * @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()}. + * + * @return $this + * + * @since 3.9.0 + */ + public function display($cachable = false, $urlparams = []) + { + // Get the document object. + $document = $this->app->getDocument(); + + // Set the default view name and format from the Request. + $vName = $this->input->get('view', $this->default_view); + $vFormat = $document->getType(); + $lName = $this->input->get('layout', 'default', 'string'); + + // Get and render the view. + if ($view = $this->getView($vName, $vFormat)) { + $model = $this->getModel($vName); + $view->setModel($model, true); + + if ($vName === 'request') { + // For the default layout, we need to also push the action logs model into the view + if ($lName === 'default') { + $logsModel = $this->app->bootComponent('com_actionlogs') + ->getMVCFactory()->createModel('Actionlogs', 'Administrator', ['ignore_request' => true]); + + // Set default ordering for the context + $logsModel->setState('list.fullordering', 'a.log_date DESC'); + + // And push the model into the view + $view->setModel($logsModel, false); + } + + // For the edit layout, if mail sending is disabled then redirect back to the list view as the form is unusable in this state + if ($lName === 'edit' && !$this->app->get('mailonline', 1)) { + $this->setRedirect( + Route::_('index.php?option=com_privacy&view=requests', false), + Text::_('COM_PRIVACY_WARNING_CANNOT_CREATE_REQUEST_WHEN_SENDMAIL_DISABLED'), + 'warning' + ); + + return $this; + } + } + + $view->setLayout($lName); + + // Push document object into the view. + $view->document = $document; + + $view->display(); + } + + return $this; + } + + /** + * Fetch and report number urgent privacy requests in JSON format, for AJAX requests + * + * @return void + * + * @since 3.9.0 + */ + public function getNumberUrgentRequests() + { + // Check for a valid token. If invalid, send a 403 with the error message. + if (!Session::checkToken('get')) { + $this->app->setHeader('status', 403, true); + $this->app->sendHeaders(); + echo new JsonResponse(new \Exception(Text::_('JINVALID_TOKEN'), 403)); + $this->app->close(); + } + + /** @var RequestsModel $model */ + $model = $this->getModel('requests'); + $numberUrgentRequests = $model->getNumberUrgentRequests(); + + echo new JsonResponse(['number_urgent_requests' => $numberUrgentRequests]); + } } diff --git a/administrator/components/com_privacy/src/Controller/RequestController.php b/administrator/components/com_privacy/src/Controller/RequestController.php index 2f4c55c3ec1fb..84a0baaacedf0 100644 --- a/administrator/components/com_privacy/src/Controller/RequestController.php +++ b/administrator/components/com_privacy/src/Controller/RequestController.php @@ -1,4 +1,5 @@ checkToken(); - - /** @var RequestModel $model */ - $model = $this->getModel(); - - /** @var RequestTable $table */ - $table = $model->getTable(); - - // Determine the name of the primary key for the data. - if (empty($key)) - { - $key = $table->getKeyName(); - } - - // To avoid data collisions the urlVar may be different from the primary key. - if (empty($urlVar)) - { - $urlVar = $key; - } - - $recordId = $this->input->getInt($urlVar); - - $item = $model->getItem($recordId); - - // Ensure this record can transition to the requested state - if (!$this->canTransition($item, '2')) - { - $this->setMessage(Text::_('COM_PRIVACY_ERROR_COMPLETE_TRANSITION_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=com_privacy&view=request&id=' . $recordId, false - ) - ); - - return false; - } - - // Build the data array for the update - $data = [ - $key => $recordId, - 'status' => '2', - ]; - - // Access check. - if (!$this->allowSave($data, $key)) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=com_privacy&view=request&id=' . $recordId, false - ) - ); - - return false; - } - - // Attempt to save the data. - if (!$model->save($data)) - { - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=com_privacy&view=request&id=' . $recordId, false - ) - ); - - return false; - } - - // Log the request completed - $model->logRequestCompleted($recordId); - - $this->setMessage(Text::_('COM_PRIVACY_REQUEST_COMPLETED')); - - $url = 'index.php?option=com_privacy&view=requests'; - - // Check if there is a return value - $return = $this->input->get('return', null, 'base64'); - - if (!is_null($return) && Uri::isInternal(base64_decode($return))) - { - $url = base64_decode($return); - } - - // Redirect to the list screen. - $this->setRedirect(Route::_($url, false)); - - return true; - } - - /** - * Method to email the data export for a request. - * - * @return boolean - * - * @since 3.9.0 - */ - public function emailexport() - { - // Check for request forgeries. - $this->checkToken('get'); - - /** @var ExportModel $model */ - $model = $this->getModel('Export'); - - $recordId = $this->input->getUint('id'); - - if (!$model->emailDataExport($recordId)) - { - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('COM_PRIVACY_ERROR_EXPORT_EMAIL_FAILED', $model->getError()), 'error'); - } - else - { - $this->setMessage(Text::_('COM_PRIVACY_EXPORT_EMAILED')); - } - - $url = 'index.php?option=com_privacy&view=requests'; - - // Check if there is a return value - $return = $this->input->get('return', null, 'base64'); - - if (!is_null($return) && Uri::isInternal(base64_decode($return))) - { - $url = base64_decode($return); - } - - // Redirect to the list screen. - $this->setRedirect(Route::_($url, false)); - - return true; - } - - /** - * Method to export the data for a request. - * - * @return $this - * - * @since 3.9.0 - */ - public function export() - { - $this->input->set('view', 'export'); - - return $this->display(); - } - - /** - * Method to invalidate a request. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean - * - * @since 3.9.0 - */ - public function invalidate($key = null, $urlVar = null) - { - // Check for request forgeries. - $this->checkToken(); - - /** @var RequestModel $model */ - $model = $this->getModel(); - - /** @var RequestTable $table */ - $table = $model->getTable(); - - // Determine the name of the primary key for the data. - if (empty($key)) - { - $key = $table->getKeyName(); - } - - // To avoid data collisions the urlVar may be different from the primary key. - if (empty($urlVar)) - { - $urlVar = $key; - } - - $recordId = $this->input->getInt($urlVar); - - $item = $model->getItem($recordId); - - // Ensure this record can transition to the requested state - if (!$this->canTransition($item, '-1')) - { - $this->setMessage(Text::_('COM_PRIVACY_ERROR_INVALID_TRANSITION_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=com_privacy&view=request&id=' . $recordId, false - ) - ); - - return false; - } - - // Build the data array for the update - $data = [ - $key => $recordId, - 'status' => '-1', - ]; - - // Access check. - if (!$this->allowSave($data, $key)) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=com_privacy&view=request&id=' . $recordId, false - ) - ); - - return false; - } - - // Attempt to save the data. - if (!$model->save($data)) - { - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=com_privacy&view=request&id=' . $recordId, false - ) - ); - - return false; - } - - // Log the request invalidated - $model->logRequestInvalidated($recordId); - - $this->setMessage(Text::_('COM_PRIVACY_REQUEST_INVALIDATED')); - - $url = 'index.php?option=com_privacy&view=requests'; - - // Check if there is a return value - $return = $this->input->get('return', null, 'base64'); - - if (!is_null($return) && Uri::isInternal(base64_decode($return))) - { - $url = base64_decode($return); - } - - // Redirect to the list screen. - $this->setRedirect(Route::_($url, false)); - - return true; - } - - /** - * Method to remove the user data for a privacy remove request. - * - * @return boolean - * - * @since 3.9.0 - */ - public function remove() - { - // Check for request forgeries. - $this->checkToken('request'); - - /** @var RemoveModel $model */ - $model = $this->getModel('Remove'); - - $recordId = $this->input->getUint('id'); - - if (!$model->removeDataForRequest($recordId)) - { - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('COM_PRIVACY_ERROR_REMOVE_DATA_FAILED', $model->getError()), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=com_privacy&view=request&id=' . $recordId, false - ) - ); - - return false; - } - - $this->setMessage(Text::_('COM_PRIVACY_DATA_REMOVED')); - - $url = 'index.php?option=com_privacy&view=requests'; - - // Check if there is a return value - $return = $this->input->get('return', null, 'base64'); - - if (!is_null($return) && Uri::isInternal(base64_decode($return))) - { - $url = base64_decode($return); - } - - // Redirect to the list screen. - $this->setRedirect(Route::_($url, false)); - - return true; - } - - /** - * Function that allows child controller access to model data after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 3.9.0 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = []) - { - // This hook only processes new items - if (!$model->getState($model->getName() . '.new', false)) - { - return; - } - - if (!$model->logRequestCreated($model->getState($model->getName() . '.id'))) - { - if ($error = $model->getError()) - { - $this->app->enqueueMessage($error, 'warning'); - } - } - - if (!$model->notifyUserAdminCreatedRequest($model->getState($model->getName() . '.id'))) - { - if ($error = $model->getError()) - { - $this->app->enqueueMessage($error, 'warning'); - } - } - else - { - $this->app->enqueueMessage(Text::_('COM_PRIVACY_MSG_CONFIRM_EMAIL_SENT_TO_USER')); - } - } - - /** - * Method to determine if an item can transition to the specified status. - * - * @param object $item The item being updated. - * @param string $newStatus The new status of the item. - * - * @return boolean - * - * @since 3.9.0 - */ - private function canTransition($item, $newStatus) - { - switch ($item->status) - { - case '0': - // A pending item can only move to invalid through this controller due to the requirement for a user to confirm the request - return $newStatus === '-1'; - - case '1': - // A confirmed item can be marked completed or invalid - return in_array($newStatus, ['-1', '2'], true); - - // An item which is already in an invalid or complete state cannot transition, likewise if we don't know the state don't change anything - case '-1': - case '2': - default: - return false; - } - } + /** + * Method to complete a request. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean + * + * @since 3.9.0 + */ + public function complete($key = null, $urlVar = null) + { + // Check for request forgeries. + $this->checkToken(); + + /** @var RequestModel $model */ + $model = $this->getModel(); + + /** @var RequestTable $table */ + $table = $model->getTable(); + + // Determine the name of the primary key for the data. + if (empty($key)) { + $key = $table->getKeyName(); + } + + // To avoid data collisions the urlVar may be different from the primary key. + if (empty($urlVar)) { + $urlVar = $key; + } + + $recordId = $this->input->getInt($urlVar); + + $item = $model->getItem($recordId); + + // Ensure this record can transition to the requested state + if (!$this->canTransition($item, '2')) { + $this->setMessage(Text::_('COM_PRIVACY_ERROR_COMPLETE_TRANSITION_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=com_privacy&view=request&id=' . $recordId, + false + ) + ); + + return false; + } + + // Build the data array for the update + $data = [ + $key => $recordId, + 'status' => '2', + ]; + + // Access check. + if (!$this->allowSave($data, $key)) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=com_privacy&view=request&id=' . $recordId, + false + ) + ); + + return false; + } + + // Attempt to save the data. + if (!$model->save($data)) { + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=com_privacy&view=request&id=' . $recordId, + false + ) + ); + + return false; + } + + // Log the request completed + $model->logRequestCompleted($recordId); + + $this->setMessage(Text::_('COM_PRIVACY_REQUEST_COMPLETED')); + + $url = 'index.php?option=com_privacy&view=requests'; + + // Check if there is a return value + $return = $this->input->get('return', null, 'base64'); + + if (!is_null($return) && Uri::isInternal(base64_decode($return))) { + $url = base64_decode($return); + } + + // Redirect to the list screen. + $this->setRedirect(Route::_($url, false)); + + return true; + } + + /** + * Method to email the data export for a request. + * + * @return boolean + * + * @since 3.9.0 + */ + public function emailexport() + { + // Check for request forgeries. + $this->checkToken('get'); + + /** @var ExportModel $model */ + $model = $this->getModel('Export'); + + $recordId = $this->input->getUint('id'); + + if (!$model->emailDataExport($recordId)) { + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('COM_PRIVACY_ERROR_EXPORT_EMAIL_FAILED', $model->getError()), 'error'); + } else { + $this->setMessage(Text::_('COM_PRIVACY_EXPORT_EMAILED')); + } + + $url = 'index.php?option=com_privacy&view=requests'; + + // Check if there is a return value + $return = $this->input->get('return', null, 'base64'); + + if (!is_null($return) && Uri::isInternal(base64_decode($return))) { + $url = base64_decode($return); + } + + // Redirect to the list screen. + $this->setRedirect(Route::_($url, false)); + + return true; + } + + /** + * Method to export the data for a request. + * + * @return $this + * + * @since 3.9.0 + */ + public function export() + { + $this->input->set('view', 'export'); + + return $this->display(); + } + + /** + * Method to invalidate a request. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean + * + * @since 3.9.0 + */ + public function invalidate($key = null, $urlVar = null) + { + // Check for request forgeries. + $this->checkToken(); + + /** @var RequestModel $model */ + $model = $this->getModel(); + + /** @var RequestTable $table */ + $table = $model->getTable(); + + // Determine the name of the primary key for the data. + if (empty($key)) { + $key = $table->getKeyName(); + } + + // To avoid data collisions the urlVar may be different from the primary key. + if (empty($urlVar)) { + $urlVar = $key; + } + + $recordId = $this->input->getInt($urlVar); + + $item = $model->getItem($recordId); + + // Ensure this record can transition to the requested state + if (!$this->canTransition($item, '-1')) { + $this->setMessage(Text::_('COM_PRIVACY_ERROR_INVALID_TRANSITION_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=com_privacy&view=request&id=' . $recordId, + false + ) + ); + + return false; + } + + // Build the data array for the update + $data = [ + $key => $recordId, + 'status' => '-1', + ]; + + // Access check. + if (!$this->allowSave($data, $key)) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=com_privacy&view=request&id=' . $recordId, + false + ) + ); + + return false; + } + + // Attempt to save the data. + if (!$model->save($data)) { + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=com_privacy&view=request&id=' . $recordId, + false + ) + ); + + return false; + } + + // Log the request invalidated + $model->logRequestInvalidated($recordId); + + $this->setMessage(Text::_('COM_PRIVACY_REQUEST_INVALIDATED')); + + $url = 'index.php?option=com_privacy&view=requests'; + + // Check if there is a return value + $return = $this->input->get('return', null, 'base64'); + + if (!is_null($return) && Uri::isInternal(base64_decode($return))) { + $url = base64_decode($return); + } + + // Redirect to the list screen. + $this->setRedirect(Route::_($url, false)); + + return true; + } + + /** + * Method to remove the user data for a privacy remove request. + * + * @return boolean + * + * @since 3.9.0 + */ + public function remove() + { + // Check for request forgeries. + $this->checkToken('request'); + + /** @var RemoveModel $model */ + $model = $this->getModel('Remove'); + + $recordId = $this->input->getUint('id'); + + if (!$model->removeDataForRequest($recordId)) { + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('COM_PRIVACY_ERROR_REMOVE_DATA_FAILED', $model->getError()), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=com_privacy&view=request&id=' . $recordId, + false + ) + ); + + return false; + } + + $this->setMessage(Text::_('COM_PRIVACY_DATA_REMOVED')); + + $url = 'index.php?option=com_privacy&view=requests'; + + // Check if there is a return value + $return = $this->input->get('return', null, 'base64'); + + if (!is_null($return) && Uri::isInternal(base64_decode($return))) { + $url = base64_decode($return); + } + + // Redirect to the list screen. + $this->setRedirect(Route::_($url, false)); + + return true; + } + + /** + * Function that allows child controller access to model data after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 3.9.0 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = []) + { + // This hook only processes new items + if (!$model->getState($model->getName() . '.new', false)) { + return; + } + + if (!$model->logRequestCreated($model->getState($model->getName() . '.id'))) { + if ($error = $model->getError()) { + $this->app->enqueueMessage($error, 'warning'); + } + } + + if (!$model->notifyUserAdminCreatedRequest($model->getState($model->getName() . '.id'))) { + if ($error = $model->getError()) { + $this->app->enqueueMessage($error, 'warning'); + } + } else { + $this->app->enqueueMessage(Text::_('COM_PRIVACY_MSG_CONFIRM_EMAIL_SENT_TO_USER')); + } + } + + /** + * Method to determine if an item can transition to the specified status. + * + * @param object $item The item being updated. + * @param string $newStatus The new status of the item. + * + * @return boolean + * + * @since 3.9.0 + */ + private function canTransition($item, $newStatus) + { + switch ($item->status) { + case '0': + // A pending item can only move to invalid through this controller due to the requirement for a user to confirm the request + return $newStatus === '-1'; + + case '1': + // A confirmed item can be marked completed or invalid + return in_array($newStatus, ['-1', '2'], true); + + // An item which is already in an invalid or complete state cannot transition, likewise if we don't know the state don't change anything + case '-1': + case '2': + default: + return false; + } + } } diff --git a/administrator/components/com_privacy/src/Controller/RequestsController.php b/administrator/components/com_privacy/src/Controller/RequestsController.php index 4b70cfab10129..dd59161712d49 100644 --- a/administrator/components/com_privacy/src/Controller/RequestsController.php +++ b/administrator/components/com_privacy/src/Controller/RequestsController.php @@ -1,4 +1,5 @@ true]) - { - return parent::getModel($name, $prefix, $config); - } + /** + * 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 BaseDatabaseModel|boolean Model object on success; otherwise false on failure. + * + * @since 3.9.0 + */ + public function getModel($name = 'Request', $prefix = 'Administrator', $config = ['ignore_request' => true]) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_privacy/src/Dispatcher/Dispatcher.php b/administrator/components/com_privacy/src/Dispatcher/Dispatcher.php index ea1e2921de28d..b2e57a8f7f163 100644 --- a/administrator/components/com_privacy/src/Dispatcher/Dispatcher.php +++ b/administrator/components/com_privacy/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.admin', $this->option)) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + /** + * Method to check component access permission + * + * @return void + */ + 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.admin', $this->option)) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/administrator/components/com_privacy/src/Export/Domain.php b/administrator/components/com_privacy/src/Export/Domain.php index 5ed379483007e..c81ecce20d964 100644 --- a/administrator/components/com_privacy/src/Export/Domain.php +++ b/administrator/components/com_privacy/src/Export/Domain.php @@ -1,4 +1,5 @@ items[] = $item; - } + /** + * Add an item to the domain + * + * @param Item $item The item to add + * + * @return void + * + * @since 3.9.0 + */ + public function addItem(Item $item) + { + $this->items[] = $item; + } - /** - * Get the domain's items - * - * @return Item[] - * - * @since 3.9.0 - */ - public function getItems() - { - return $this->items; - } + /** + * Get the domain's items + * + * @return Item[] + * + * @since 3.9.0 + */ + public function getItems() + { + return $this->items; + } } diff --git a/administrator/components/com_privacy/src/Export/Field.php b/administrator/components/com_privacy/src/Export/Field.php index 81c94dec78783..78a9de411cd81 100644 --- a/administrator/components/com_privacy/src/Export/Field.php +++ b/administrator/components/com_privacy/src/Export/Field.php @@ -1,4 +1,5 @@ fields[] = $field; - } + /** + * Add a field to the item + * + * @param Field $field The field to add + * + * @return void + * + * @since 3.9.0 + */ + public function addField(Field $field) + { + $this->fields[] = $field; + } - /** - * Get the item's fields - * - * @return Field[] - * - * @since 3.9.0 - */ - public function getFields() - { - return $this->fields; - } + /** + * Get the item's fields + * + * @return Field[] + * + * @since 3.9.0 + */ + public function getFields() + { + return $this->fields; + } } diff --git a/administrator/components/com_privacy/src/Extension/PrivacyComponent.php b/administrator/components/com_privacy/src/Extension/PrivacyComponent.php index d270d19b49cc0..28cad9ea7e7d6 100644 --- a/administrator/components/com_privacy/src/Extension/PrivacyComponent.php +++ b/administrator/components/com_privacy/src/Extension/PrivacyComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('privacy', new Privacy); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('privacy', new Privacy()); + } } diff --git a/administrator/components/com_privacy/src/Field/RequeststatusField.php b/administrator/components/com_privacy/src/Field/RequeststatusField.php index 56bb22bdeb6f1..adfcccf2b4342 100644 --- a/administrator/components/com_privacy/src/Field/RequeststatusField.php +++ b/administrator/components/com_privacy/src/Field/RequeststatusField.php @@ -1,4 +1,5 @@ 'COM_PRIVACY_STATUS_INVALID', - '0' => 'COM_PRIVACY_STATUS_PENDING', - '1' => 'COM_PRIVACY_STATUS_CONFIRMED', - '2' => 'COM_PRIVACY_STATUS_COMPLETED', - ]; + /** + * Available statuses + * + * @var array + * @since 3.9.0 + */ + protected $predefinedOptions = [ + '-1' => 'COM_PRIVACY_STATUS_INVALID', + '0' => 'COM_PRIVACY_STATUS_PENDING', + '1' => 'COM_PRIVACY_STATUS_CONFIRMED', + '2' => 'COM_PRIVACY_STATUS_COMPLETED', + ]; } diff --git a/administrator/components/com_privacy/src/Field/RequesttypeField.php b/administrator/components/com_privacy/src/Field/RequesttypeField.php index 9d0cbacf65acc..fbf8b8c3e0b32 100644 --- a/administrator/components/com_privacy/src/Field/RequesttypeField.php +++ b/administrator/components/com_privacy/src/Field/RequesttypeField.php @@ -1,4 +1,5 @@ 'COM_PRIVACY_HEADING_REQUEST_TYPE_TYPE_EXPORT', - 'remove' => 'COM_PRIVACY_HEADING_REQUEST_TYPE_TYPE_REMOVE', - ]; + /** + * Available types + * + * @var array + * @since 3.9.0 + */ + protected $predefinedOptions = [ + 'export' => 'COM_PRIVACY_HEADING_REQUEST_TYPE_TYPE_EXPORT', + 'remove' => 'COM_PRIVACY_HEADING_REQUEST_TYPE_TYPE_REMOVE', + ]; } diff --git a/administrator/components/com_privacy/src/Helper/PrivacyHelper.php b/administrator/components/com_privacy/src/Helper/PrivacyHelper.php index a8c0f9fabaadc..57e94f73ec87c 100644 --- a/administrator/components/com_privacy/src/Helper/PrivacyHelper.php +++ b/administrator/components/com_privacy/src/Helper/PrivacyHelper.php @@ -1,4 +1,5 @@ '); + /** + * Render the data request as a XML document. + * + * @param Domain[] $exportData The data to be exported. + * + * @return string + * + * @since 3.9.0 + */ + public static function renderDataAsXml(array $exportData) + { + $export = new \SimpleXMLElement(''); - foreach ($exportData as $domain) - { - $xmlDomain = $export->addChild('domain'); - $xmlDomain->addAttribute('name', $domain->name); - $xmlDomain->addAttribute('description', $domain->description); + foreach ($exportData as $domain) { + $xmlDomain = $export->addChild('domain'); + $xmlDomain->addAttribute('name', $domain->name); + $xmlDomain->addAttribute('description', $domain->description); - foreach ($domain->getItems() as $item) - { - $xmlItem = $xmlDomain->addChild('item'); + foreach ($domain->getItems() as $item) { + $xmlItem = $xmlDomain->addChild('item'); - if ($item->id) - { - $xmlItem->addAttribute('id', $item->id); - } + if ($item->id) { + $xmlItem->addAttribute('id', $item->id); + } - foreach ($item->getFields() as $field) - { - $xmlItem->{$field->name} = $field->value; - } - } - } + foreach ($item->getFields() as $field) { + $xmlItem->{$field->name} = $field->value; + } + } + } - $dom = new \DOMDocument; - $dom->loadXML($export->asXML()); - $dom->formatOutput = true; + $dom = new \DOMDocument(); + $dom->loadXML($export->asXML()); + $dom->formatOutput = true; - return $dom->saveXML(); - } + return $dom->saveXML(); + } - /** - * Gets the privacyconsent system plugin extension id. - * - * @return integer The privacyconsent system plugin extension id. - * - * @since 3.9.2 - */ - public static function getPrivacyConsentPluginId() - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) - ->where($db->quoteName('element') . ' = ' . $db->quote('privacyconsent')); + /** + * Gets the privacyconsent system plugin extension id. + * + * @return integer The privacyconsent system plugin extension id. + * + * @since 3.9.2 + */ + public static function getPrivacyConsentPluginId() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ->where($db->quoteName('element') . ' = ' . $db->quote('privacyconsent')); - $db->setQuery($query); + $db->setQuery($query); - return (int) $db->loadResult(); - } + return (int) $db->loadResult(); + } } diff --git a/administrator/components/com_privacy/src/Model/CapabilitiesModel.php b/administrator/components/com_privacy/src/Model/CapabilitiesModel.php index 98296574a4751..de797a8059f3c 100644 --- a/administrator/components/com_privacy/src/Model/CapabilitiesModel.php +++ b/administrator/components/com_privacy/src/Model/CapabilitiesModel.php @@ -1,4 +1,5 @@ [ - Text::_('COM_PRIVACY_CORE_CAPABILITY_SESSION_IP_ADDRESS_AND_COOKIE'), - Text::sprintf('COM_PRIVACY_CORE_CAPABILITY_LOGGING_IP_ADDRESS', $app->get('log_path', JPATH_ADMINISTRATOR . '/logs')), - Text::_('COM_PRIVACY_CORE_CAPABILITY_COMMUNICATION_WITH_JOOMLA_ORG'), - ], - ]; + $coreCapabilities = [ + Text::_('COM_PRIVACY_HEADING_CORE_CAPABILITIES') => [ + Text::_('COM_PRIVACY_CORE_CAPABILITY_SESSION_IP_ADDRESS_AND_COOKIE'), + Text::sprintf('COM_PRIVACY_CORE_CAPABILITY_LOGGING_IP_ADDRESS', $app->get('log_path', JPATH_ADMINISTRATOR . '/logs')), + Text::_('COM_PRIVACY_CORE_CAPABILITY_COMMUNICATION_WITH_JOOMLA_ORG'), + ], + ]; - /* - * We will search for capabilities from the following plugin groups: - * - * - Authentication: These plugins by design process user information and may have capabilities such as creating cookies - * - Captcha: These plugins may communicate information to third party systems - * - Installer: These plugins can add additional install capabilities to the Extension Manager, such as the Install from Web service - * - Privacy: These plugins are the primary integration point into this component - * - User: These plugins are intended to extend the user management system - * - * This is in addition to plugin groups which are imported before this method is triggered, generally this is the system group. - */ + /* + * We will search for capabilities from the following plugin groups: + * + * - Authentication: These plugins by design process user information and may have capabilities such as creating cookies + * - Captcha: These plugins may communicate information to third party systems + * - Installer: These plugins can add additional install capabilities to the Extension Manager, such as the Install from Web service + * - Privacy: These plugins are the primary integration point into this component + * - User: These plugins are intended to extend the user management system + * + * This is in addition to plugin groups which are imported before this method is triggered, generally this is the system group. + */ - PluginHelper::importPlugin('authentication'); - PluginHelper::importPlugin('captcha'); - PluginHelper::importPlugin('installer'); - PluginHelper::importPlugin('privacy'); - PluginHelper::importPlugin('user'); + PluginHelper::importPlugin('authentication'); + PluginHelper::importPlugin('captcha'); + PluginHelper::importPlugin('installer'); + PluginHelper::importPlugin('privacy'); + PluginHelper::importPlugin('user'); - $pluginResults = $app->triggerEvent('onPrivacyCollectAdminCapabilities'); + $pluginResults = $app->triggerEvent('onPrivacyCollectAdminCapabilities'); - // We are going to "cheat" here and include this component's capabilities without using a plugin - $extensionCapabilities = [ - Text::_('COM_PRIVACY') => [ - Text::_('COM_PRIVACY_EXTENSION_CAPABILITY_PERSONAL_INFO'), - ], - ]; + // We are going to "cheat" here and include this component's capabilities without using a plugin + $extensionCapabilities = [ + Text::_('COM_PRIVACY') => [ + Text::_('COM_PRIVACY_EXTENSION_CAPABILITY_PERSONAL_INFO'), + ], + ]; - foreach ($pluginResults as $pluginResult) - { - $extensionCapabilities += $pluginResult; - } + foreach ($pluginResults as $pluginResult) { + $extensionCapabilities += $pluginResult; + } - // Sort the extension list alphabetically - ksort($extensionCapabilities); + // Sort the extension list alphabetically + ksort($extensionCapabilities); - // Always prepend the core capabilities to the array - return $coreCapabilities + $extensionCapabilities; - } + // Always prepend the core capabilities to the array + return $coreCapabilities + $extensionCapabilities; + } - /** - * Method to auto-populate the model state. - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState() - { - // Load the parameters. - $this->setState('params', ComponentHelper::getParams('com_privacy')); - } + /** + * Method to auto-populate the model state. + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState() + { + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_privacy')); + } } diff --git a/administrator/components/com_privacy/src/Model/ConsentsModel.php b/administrator/components/com_privacy/src/Model/ConsentsModel.php index b50b1992fea73..8e75377879459 100644 --- a/administrator/components/com_privacy/src/Model/ConsentsModel.php +++ b/administrator/components/com_privacy/src/Model/ConsentsModel.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select($this->getState('list.select', 'a.*')); - $query->from($db->quoteName('#__privacy_consents', 'a')); - - // Join over the users for the username and name. - $query->select($db->quoteName('u.username', 'username')) - ->select($db->quoteName('u.name', 'name')); - $query->join('LEFT', $db->quoteName('#__users', 'u') . ' ON u.id = a.user_id'); - - // Filter by search in email - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $ids, ParameterType::INTEGER); - } - elseif (stripos($search, 'uid:') === 0) - { - $uid = (int) substr($search, 4); - $query->where($db->quoteName('a.user_id') . ' = :uid') - ->bind(':uid', $uid, ParameterType::INTEGER); - } - elseif (stripos($search, 'name:') === 0) - { - $search = '%' . substr($search, 5) . '%'; - $query->where($db->quoteName('u.name') . ' LIKE :search') - ->bind(':search', $search); - } - else - { - $search = '%' . $search . '%'; - $query->where('(' . $db->quoteName('u.username') . ' LIKE :search)') - ->bind(':search', $search); - } - } - - $state = $this->getState('filter.state'); - - if ($state != '') - { - $state = (int) $state; - $query->where($db->quoteName('a.state') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - - // Handle the list ordering. - $ordering = $this->getState('list.ordering'); - $direction = $this->getState('list.direction'); - - if (!empty($ordering)) - { - $query->order($db->escape($ordering) . ' ' . $db->escape($direction)); - } - - return $query; - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string - * - * @since 3.9.0 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - - return parent::getStoreId($id); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState($ordering = 'a.id', $direction = 'desc') - { - // Load the filter state. - $this->setState( - 'filter.search', - $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search') - ); - - $this->setState( - 'filter.subject', - $this->getUserStateFromRequest($this->context . '.filter.subject', 'filter_subject') - ); - - $this->setState( - 'filter.state', - $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state') - ); - - // Load the parameters. - $this->setState('params', ComponentHelper::getParams('com_privacy')); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to invalidate specific consents. - * - * @param array $pks The ids of the consents to invalidate. - * - * @return boolean True on success. - */ - public function invalidate($pks) - { - // Sanitize the ids. - $pks = (array) $pks; - $pks = ArrayHelper::toInteger($pks); - - try - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->update($db->quoteName('#__privacy_consents')) - ->set($db->quoteName('state') . ' = -1') - ->whereIn($db->quoteName('id'), $pks) - ->where($db->quoteName('state') . ' = 1'); - $db->setQuery($query); - $db->execute(); - } - catch (ExecutionFailureException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return true; - } - - /** - * Method to invalidate a group of specific consents. - * - * @param array $subject The subject of the consents to invalidate. - * - * @return boolean True on success. - */ - public function invalidateAll($subject) - { - try - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->update($db->quoteName('#__privacy_consents')) - ->set($db->quoteName('state') . ' = -1') - ->where($db->quoteName('subject') . ' = :subject') - ->where($db->quoteName('state') . ' = 1') - ->bind(':subject', $subject); - $db->setQuery($query); - $db->execute(); - } - catch (ExecutionFailureException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return true; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 3.9.0 + */ + public function __construct($config = []) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = [ + 'id', 'a.id', + 'user_id', 'a.user_id', + 'subject', 'a.subject', + 'created', 'a.created', + 'username', 'u.username', + 'name', 'u.name', + 'state', 'a.state', + ]; + } + + parent::__construct($config); + } + + /** + * Method to get a DatabaseQuery object for retrieving the data set from a database. + * + * @return DatabaseQuery + * + * @since 3.9.0 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select($this->getState('list.select', 'a.*')); + $query->from($db->quoteName('#__privacy_consents', 'a')); + + // Join over the users for the username and name. + $query->select($db->quoteName('u.username', 'username')) + ->select($db->quoteName('u.name', 'name')); + $query->join('LEFT', $db->quoteName('#__users', 'u') . ' ON u.id = a.user_id'); + + // Filter by search in email + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $ids, ParameterType::INTEGER); + } elseif (stripos($search, 'uid:') === 0) { + $uid = (int) substr($search, 4); + $query->where($db->quoteName('a.user_id') . ' = :uid') + ->bind(':uid', $uid, ParameterType::INTEGER); + } elseif (stripos($search, 'name:') === 0) { + $search = '%' . substr($search, 5) . '%'; + $query->where($db->quoteName('u.name') . ' LIKE :search') + ->bind(':search', $search); + } else { + $search = '%' . $search . '%'; + $query->where('(' . $db->quoteName('u.username') . ' LIKE :search)') + ->bind(':search', $search); + } + } + + $state = $this->getState('filter.state'); + + if ($state != '') { + $state = (int) $state; + $query->where($db->quoteName('a.state') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } + + // Handle the list ordering. + $ordering = $this->getState('list.ordering'); + $direction = $this->getState('list.direction'); + + if (!empty($ordering)) { + $query->order($db->escape($ordering) . ' ' . $db->escape($direction)); + } + + return $query; + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string + * + * @since 3.9.0 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + + return parent::getStoreId($id); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState($ordering = 'a.id', $direction = 'desc') + { + // Load the filter state. + $this->setState( + 'filter.search', + $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search') + ); + + $this->setState( + 'filter.subject', + $this->getUserStateFromRequest($this->context . '.filter.subject', 'filter_subject') + ); + + $this->setState( + 'filter.state', + $this->getUserStateFromRequest($this->context . '.filter.state', 'filter_state') + ); + + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_privacy')); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to invalidate specific consents. + * + * @param array $pks The ids of the consents to invalidate. + * + * @return boolean True on success. + */ + public function invalidate($pks) + { + // Sanitize the ids. + $pks = (array) $pks; + $pks = ArrayHelper::toInteger($pks); + + try { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->update($db->quoteName('#__privacy_consents')) + ->set($db->quoteName('state') . ' = -1') + ->whereIn($db->quoteName('id'), $pks) + ->where($db->quoteName('state') . ' = 1'); + $db->setQuery($query); + $db->execute(); + } catch (ExecutionFailureException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return true; + } + + /** + * Method to invalidate a group of specific consents. + * + * @param array $subject The subject of the consents to invalidate. + * + * @return boolean True on success. + */ + public function invalidateAll($subject) + { + try { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->update($db->quoteName('#__privacy_consents')) + ->set($db->quoteName('state') . ' = -1') + ->where($db->quoteName('subject') . ' = :subject') + ->where($db->quoteName('state') . ' = 1') + ->bind(':subject', $subject); + $db->setQuery($query); + $db->execute(); + } catch (ExecutionFailureException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return true; + } } diff --git a/administrator/components/com_privacy/src/Model/ExportModel.php b/administrator/components/com_privacy/src/Model/ExportModel.php index b190bcdf3162e..748833b80f13a 100644 --- a/administrator/components/com_privacy/src/Model/ExportModel.php +++ b/administrator/components/com_privacy/src/Model/ExportModel.php @@ -1,4 +1,5 @@ getState($this->getName() . '.request_id'); - - if (!$id) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_ID_REQUIRED_FOR_EXPORT')); - - return false; - } - - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($id)) - { - $this->setError($table->getError()); + /** + * Create the export document for an information request. + * + * @param integer $id The request ID to process + * + * @return Domain[]|boolean A SimpleXMLElement object for a successful export or boolean false on an error + * + * @since 3.9.0 + */ + public function collectDataForExportRequest($id = null) + { + $id = !empty($id) ? $id : (int) $this->getState($this->getName() . '.request_id'); + + if (!$id) { + $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_ID_REQUIRED_FOR_EXPORT')); + + return false; + } + + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($id)) { + $this->setError($table->getError()); + + return false; + } + + if ($table->request_type !== 'export') { + $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_TYPE_NOT_EXPORT')); + + return false; + } + + if ($table->status != 1) { + $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_EXPORT_UNCONFIRMED_REQUEST')); + + return false; + } + + // If there is a user account associated with the email address, load it here for use in the plugins + $db = $this->getDatabase(); + + $userId = (int) $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') + ->bind(':email', $table->email) + ->setLimit(1) + )->loadResult(); + + $user = $userId ? User::getInstance($userId) : null; + + // Log the export + $this->logExport($table); + + PluginHelper::importPlugin('privacy'); + + $pluginResults = Factory::getApplication()->triggerEvent('onPrivacyExportRequest', [$table, $user]); + + $domains = []; + + foreach ($pluginResults as $pluginDomains) { + $domains = array_merge($domains, $pluginDomains); + } + + return $domains; + } + + /** + * Email the data export to the user. + * + * @param integer $id The request ID to process + * + * @return boolean + * + * @since 3.9.0 + */ + public function emailDataExport($id = null) + { + $id = !empty($id) ? $id : (int) $this->getState($this->getName() . '.request_id'); + + if (!$id) { + $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_ID_REQUIRED_FOR_EXPORT')); + + return false; + } + + $exportData = $this->collectDataForExportRequest($id); + + if ($exportData === false) { + // Error is already set, we just need to bail + return false; + } + + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($id)) { + $this->setError($table->getError()); + + return false; + } + + if ($table->request_type !== 'export') { + $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_TYPE_NOT_EXPORT')); - return false; - } + return false; + } - if ($table->request_type !== 'export') - { - $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_TYPE_NOT_EXPORT')); - - return false; - } - - if ($table->status != 1) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_EXPORT_UNCONFIRMED_REQUEST')); - - return false; - } - - // If there is a user account associated with the email address, load it here for use in the plugins - $db = $this->getDatabase(); - - $userId = (int) $db->setQuery( - $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') - ->bind(':email', $table->email) - ->setLimit(1) - )->loadResult(); - - $user = $userId ? User::getInstance($userId) : null; - - // Log the export - $this->logExport($table); - - PluginHelper::importPlugin('privacy'); - - $pluginResults = Factory::getApplication()->triggerEvent('onPrivacyExportRequest', [$table, $user]); - - $domains = []; - - foreach ($pluginResults as $pluginDomains) - { - $domains = array_merge($domains, $pluginDomains); - } - - return $domains; - } - - /** - * Email the data export to the user. - * - * @param integer $id The request ID to process - * - * @return boolean - * - * @since 3.9.0 - */ - public function emailDataExport($id = null) - { - $id = !empty($id) ? $id : (int) $this->getState($this->getName() . '.request_id'); - - if (!$id) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_ID_REQUIRED_FOR_EXPORT')); - - return false; - } - - $exportData = $this->collectDataForExportRequest($id); - - if ($exportData === false) - { - // Error is already set, we just need to bail - return false; - } - - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($id)) - { - $this->setError($table->getError()); - - return false; - } - - if ($table->request_type !== 'export') - { - $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_TYPE_NOT_EXPORT')); - - return false; - } - - if ($table->status != 1) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_EXPORT_UNCONFIRMED_REQUEST')); - - return false; - } - - // Log the email - $this->logExportEmailed($table); - - /* - * If there is an associated user account, we will attempt to send this email in the user's preferred language. - * Because of this, it is expected that Language::_() is directly called and that the Text class is NOT used - * for translating all messages. - * - * Error messages will still be displayed to the administrator, so those messages should continue to use the Text class. - */ - - $lang = Factory::getLanguage(); - - $db = $this->getDatabase(); - - $userId = (int) $db->setQuery( - $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') - ->bind(':email', $table->email), - 0, - 1 - )->loadResult(); - - if ($userId) - { - $receiver = User::getInstance($userId); - - /* - * We don't know if the user has admin access, so we will check if they have an admin language in their parameters, - * falling back to the site language, falling back to the currently active language - */ - - $langCode = $receiver->getParam('admin_language', ''); - - if (!$langCode) - { - $langCode = $receiver->getParam('language', $lang->getTag()); - } - - $lang = Language::getInstance($langCode, $lang->getDebug()); - } - - // Ensure the right language files have been loaded - $lang->load('com_privacy', JPATH_ADMINISTRATOR) - || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); - - // The mailer can be set to either throw Exceptions or return boolean false, account for both - try - { - $app = Factory::getApplication(); - $mailer = new MailTemplate('com_privacy.userdataexport', $app->getLanguage()->getTag()); - - $templateData = [ - 'sitename' => $app->get('sitename'), - 'url' => Uri::root(), - ]; - - $mailer->addRecipient($table->email); - $mailer->addTemplateData($templateData); - $mailer->addAttachment('user-data_' . Uri::getInstance()->toString(['host']) . '.xml', PrivacyHelper::renderDataAsXml($exportData)); - - if ($mailer->send() === false) - { - $this->setError($mailer->ErrorInfo); - - return false; - } - - return true; - } - catch (phpmailerException $exception) - { - $this->setError($exception->getMessage()); - - return false; - } - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @throws \Exception - * @since 3.9.0 - */ - public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Log the data export to the action log system. - * - * @param RequestTable $request The request record being processed - * - * @return void - * - * @since 3.9.0 - */ - public function logExport(RequestTable $request) - { - $user = Factory::getUser(); - - $message = [ - 'action' => 'export', - 'id' => $request->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, - 'userid' => $user->id, - 'username' => $user->username, - 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_EXPORT', 'com_privacy.request', $user->id); - } - - /** - * Log the data export email to the action log system. - * - * @param RequestTable $request The request record being processed - * - * @return void - * - * @since 3.9.0 - */ - public function logExportEmailed(RequestTable $request) - { - $user = Factory::getUser(); - - $message = [ - 'action' => 'export_emailed', - 'id' => $request->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, - 'userid' => $user->id, - 'username' => $user->username, - 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_EXPORT_EMAILED', 'com_privacy.request', $user->id); - } - - /** - * Method to auto-populate the model state. - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState() - { - // Get the pk of the record from the request. - $this->setState($this->getName() . '.request_id', Factory::getApplication()->input->getUint('id')); - - // Load the parameters. - $this->setState('params', ComponentHelper::getParams('com_privacy')); - } - - /** - * Method to fetch an instance of the action log model. - * - * @return ActionlogModel - * - * @since 4.0.0 - */ - private function getActionlogModel(): ActionlogModel - { - return Factory::getApplication()->bootComponent('com_actionlogs') - ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); - } + if ($table->status != 1) { + $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_EXPORT_UNCONFIRMED_REQUEST')); + + return false; + } + + // Log the email + $this->logExportEmailed($table); + + /* + * If there is an associated user account, we will attempt to send this email in the user's preferred language. + * Because of this, it is expected that Language::_() is directly called and that the Text class is NOT used + * for translating all messages. + * + * Error messages will still be displayed to the administrator, so those messages should continue to use the Text class. + */ + + $lang = Factory::getLanguage(); + + $db = $this->getDatabase(); + + $userId = (int) $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') + ->bind(':email', $table->email), + 0, + 1 + )->loadResult(); + + if ($userId) { + $receiver = User::getInstance($userId); + + /* + * We don't know if the user has admin access, so we will check if they have an admin language in their parameters, + * falling back to the site language, falling back to the currently active language + */ + + $langCode = $receiver->getParam('admin_language', ''); + + if (!$langCode) { + $langCode = $receiver->getParam('language', $lang->getTag()); + } + + $lang = Language::getInstance($langCode, $lang->getDebug()); + } + + // Ensure the right language files have been loaded + $lang->load('com_privacy', JPATH_ADMINISTRATOR) + || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); + + // The mailer can be set to either throw Exceptions or return boolean false, account for both + try { + $app = Factory::getApplication(); + $mailer = new MailTemplate('com_privacy.userdataexport', $app->getLanguage()->getTag()); + + $templateData = [ + 'sitename' => $app->get('sitename'), + 'url' => Uri::root(), + ]; + + $mailer->addRecipient($table->email); + $mailer->addTemplateData($templateData); + $mailer->addAttachment('user-data_' . Uri::getInstance()->toString(['host']) . '.xml', PrivacyHelper::renderDataAsXml($exportData)); + + if ($mailer->send() === false) { + $this->setError($mailer->ErrorInfo); + + return false; + } + + return true; + } catch (phpmailerException $exception) { + $this->setError($exception->getMessage()); + + return false; + } + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @throws \Exception + * @since 3.9.0 + */ + public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Log the data export to the action log system. + * + * @param RequestTable $request The request record being processed + * + * @return void + * + * @since 3.9.0 + */ + public function logExport(RequestTable $request) + { + $user = Factory::getUser(); + + $message = [ + 'action' => 'export', + 'id' => $request->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, + 'userid' => $user->id, + 'username' => $user->username, + 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_EXPORT', 'com_privacy.request', $user->id); + } + + /** + * Log the data export email to the action log system. + * + * @param RequestTable $request The request record being processed + * + * @return void + * + * @since 3.9.0 + */ + public function logExportEmailed(RequestTable $request) + { + $user = Factory::getUser(); + + $message = [ + 'action' => 'export_emailed', + 'id' => $request->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, + 'userid' => $user->id, + 'username' => $user->username, + 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_EXPORT_EMAILED', 'com_privacy.request', $user->id); + } + + /** + * Method to auto-populate the model state. + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState() + { + // Get the pk of the record from the request. + $this->setState($this->getName() . '.request_id', Factory::getApplication()->input->getUint('id')); + + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_privacy')); + } + + /** + * Method to fetch an instance of the action log model. + * + * @return ActionlogModel + * + * @since 4.0.0 + */ + private function getActionlogModel(): ActionlogModel + { + return Factory::getApplication()->bootComponent('com_actionlogs') + ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); + } } diff --git a/administrator/components/com_privacy/src/Model/RemoveModel.php b/administrator/components/com_privacy/src/Model/RemoveModel.php index ffaff9a83d628..39a102c3ed037 100644 --- a/administrator/components/com_privacy/src/Model/RemoveModel.php +++ b/administrator/components/com_privacy/src/Model/RemoveModel.php @@ -1,4 +1,5 @@ getState($this->getName() . '.request_id'); - - if (!$id) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_ID_REQUIRED_FOR_REMOVE')); - - return false; - } - - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($id)) - { - $this->setError($table->getError()); - - return false; - } - - if ($table->request_type !== 'remove') - { - $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_TYPE_NOT_REMOVE')); - - return false; - } - - if ($table->status != 1) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_REMOVE_UNCONFIRMED_REQUEST')); - - return false; - } - - // If there is a user account associated with the email address, load it here for use in the plugins - $db = $this->getDatabase(); - - $userId = (int) $db->setQuery( - $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') - ->bind(':email', $table->email) - ->setLimit(1) - )->loadResult(); - - $user = $userId ? User::getInstance($userId) : null; - - $canRemove = true; - - PluginHelper::importPlugin('privacy'); - - /** @var Status[] $pluginResults */ - $pluginResults = Factory::getApplication()->triggerEvent('onPrivacyCanRemoveData', [$table, $user]); - - foreach ($pluginResults as $status) - { - if (!$status->canRemove) - { - $this->setError($status->reason ?: Text::_('COM_PRIVACY_ERROR_CANNOT_REMOVE_DATA')); - - $canRemove = false; - } - } - - if (!$canRemove) - { - $this->logRemoveBlocked($table, $this->getErrors()); - - return false; - } - - // Log the removal - $this->logRemove($table); - - Factory::getApplication()->triggerEvent('onPrivacyRemoveData', [$table, $user]); - - return true; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @throws \Exception - * @since 3.9.0 - */ - public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Log the data removal to the action log system. - * - * @param RequestTable $request The request record being processed - * - * @return void - * - * @since 3.9.0 - */ - public function logRemove(RequestTable $request) - { - $user = Factory::getUser(); - - $message = [ - 'action' => 'remove', - 'id' => $request->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, - 'userid' => $user->id, - 'username' => $user->username, - 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_REMOVE', 'com_privacy.request', $user->id); - } - - /** - * Log the data removal being blocked to the action log system. - * - * @param RequestTable $request The request record being processed - * @param string[] $reasons The reasons given why the record could not be removed. - * - * @return void - * - * @since 3.9.0 - */ - public function logRemoveBlocked(RequestTable $request, array $reasons) - { - $user = Factory::getUser(); - - $message = [ - 'action' => 'remove-blocked', - 'id' => $request->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, - 'userid' => $user->id, - 'username' => $user->username, - 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, - 'reasons' => implode('; ', $reasons), - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_REMOVE_BLOCKED', 'com_privacy.request', $user->id); - } - - /** - * Method to auto-populate the model state. - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState() - { - // Get the pk of the record from the request. - $this->setState($this->getName() . '.request_id', Factory::getApplication()->input->getUint('id')); - - // Load the parameters. - $this->setState('params', ComponentHelper::getParams('com_privacy')); - } - - /** - * Method to fetch an instance of the action log model. - * - * @return ActionlogModel - * - * @since 4.0.0 - */ - private function getActionlogModel(): ActionlogModel - { - return Factory::getApplication()->bootComponent('com_actionlogs') - ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); - } + /** + * Remove the user data. + * + * @param integer $id The request ID to process + * + * @return boolean + * + * @since 3.9.0 + */ + public function removeDataForRequest($id = null) + { + $id = !empty($id) ? $id : (int) $this->getState($this->getName() . '.request_id'); + + if (!$id) { + $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_ID_REQUIRED_FOR_REMOVE')); + + return false; + } + + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($id)) { + $this->setError($table->getError()); + + return false; + } + + if ($table->request_type !== 'remove') { + $this->setError(Text::_('COM_PRIVACY_ERROR_REQUEST_TYPE_NOT_REMOVE')); + + return false; + } + + if ($table->status != 1) { + $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_REMOVE_UNCONFIRMED_REQUEST')); + + return false; + } + + // If there is a user account associated with the email address, load it here for use in the plugins + $db = $this->getDatabase(); + + $userId = (int) $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') + ->bind(':email', $table->email) + ->setLimit(1) + )->loadResult(); + + $user = $userId ? User::getInstance($userId) : null; + + $canRemove = true; + + PluginHelper::importPlugin('privacy'); + + /** @var Status[] $pluginResults */ + $pluginResults = Factory::getApplication()->triggerEvent('onPrivacyCanRemoveData', [$table, $user]); + + foreach ($pluginResults as $status) { + if (!$status->canRemove) { + $this->setError($status->reason ?: Text::_('COM_PRIVACY_ERROR_CANNOT_REMOVE_DATA')); + + $canRemove = false; + } + } + + if (!$canRemove) { + $this->logRemoveBlocked($table, $this->getErrors()); + + return false; + } + + // Log the removal + $this->logRemove($table); + + Factory::getApplication()->triggerEvent('onPrivacyRemoveData', [$table, $user]); + + return true; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @throws \Exception + * @since 3.9.0 + */ + public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Log the data removal to the action log system. + * + * @param RequestTable $request The request record being processed + * + * @return void + * + * @since 3.9.0 + */ + public function logRemove(RequestTable $request) + { + $user = Factory::getUser(); + + $message = [ + 'action' => 'remove', + 'id' => $request->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, + 'userid' => $user->id, + 'username' => $user->username, + 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_REMOVE', 'com_privacy.request', $user->id); + } + + /** + * Log the data removal being blocked to the action log system. + * + * @param RequestTable $request The request record being processed + * @param string[] $reasons The reasons given why the record could not be removed. + * + * @return void + * + * @since 3.9.0 + */ + public function logRemoveBlocked(RequestTable $request, array $reasons) + { + $user = Factory::getUser(); + + $message = [ + 'action' => 'remove-blocked', + 'id' => $request->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $request->id, + 'userid' => $user->id, + 'username' => $user->username, + 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, + 'reasons' => implode('; ', $reasons), + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_REMOVE_BLOCKED', 'com_privacy.request', $user->id); + } + + /** + * Method to auto-populate the model state. + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState() + { + // Get the pk of the record from the request. + $this->setState($this->getName() . '.request_id', Factory::getApplication()->input->getUint('id')); + + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_privacy')); + } + + /** + * Method to fetch an instance of the action log model. + * + * @return ActionlogModel + * + * @since 4.0.0 + */ + private function getActionlogModel(): ActionlogModel + { + return Factory::getApplication()->bootComponent('com_actionlogs') + ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); + } } diff --git a/administrator/components/com_privacy/src/Model/RequestModel.php b/administrator/components/com_privacy/src/Model/RequestModel.php index 6dde5737a1912..f034b675c3e40 100644 --- a/administrator/components/com_privacy/src/Model/RequestModel.php +++ b/administrator/components/com_privacy/src/Model/RequestModel.php @@ -1,4 +1,5 @@ loadForm('com_privacy.request', 'request', ['control' => 'jform', 'load_data' => $loadData]); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @throws \Exception - * @since 3.9.0 - */ - public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Method to get the data that should be injected in the form. - * - * @return array The default data is an empty array. - * - * @since 3.9.0 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_privacy.edit.request.data', []); - - if (empty($data)) - { - $data = $this->getItem(); - } - - return $data; - } - - /** - * Log the completion of a request to the action log system. - * - * @param integer $id The ID of the request to process. - * - * @return boolean - * - * @since 3.9.0 - */ - public function logRequestCompleted($id) - { - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($id)) - { - $this->setError($table->getError()); - - return false; - } - - $user = Factory::getUser(); - - $message = [ - 'action' => 'request-completed', - 'requesttype' => $table->request_type, - 'subjectemail' => $table->email, - 'id' => $table->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, - 'userid' => $user->id, - 'username' => $user->username, - 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_ADMIN_COMPLETED_REQUEST', 'com_privacy.request', $user->id); - - return true; - } - - /** - * Log the creation of a request to the action log system. - * - * @param integer $id The ID of the request to process. - * - * @return boolean - * - * @since 3.9.0 - */ - public function logRequestCreated($id) - { - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($id)) - { - $this->setError($table->getError()); - - return false; - } - - $user = Factory::getUser(); - - $message = [ - 'action' => 'request-created', - 'requesttype' => $table->request_type, - 'subjectemail' => $table->email, - 'id' => $table->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, - 'userid' => $user->id, - 'username' => $user->username, - 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_ADMIN_CREATED_REQUEST', 'com_privacy.request', $user->id); - - return true; - } - - /** - * Log the invalidation of a request to the action log system. - * - * @param integer $id The ID of the request to process. - * - * @return boolean - * - * @since 3.9.0 - */ - public function logRequestInvalidated($id) - { - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($id)) - { - $this->setError($table->getError()); - - return false; - } - - $user = Factory::getUser(); - - $message = [ - 'action' => 'request-invalidated', - 'requesttype' => $table->request_type, - 'subjectemail' => $table->email, - 'id' => $table->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, - 'userid' => $user->id, - 'username' => $user->username, - 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_ADMIN_INVALIDATED_REQUEST', 'com_privacy.request', $user->id); - - return true; - } - - /** - * Notifies the user that an information request has been created by a site administrator. - * - * Because confirmation tokens are stored in the database as a hashed value, this method will generate a new confirmation token - * for the request. - * - * @param integer $id The ID of the request to process. - * - * @return boolean - * - * @since 3.9.0 - */ - public function notifyUserAdminCreatedRequest($id) - { - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($id)) - { - $this->setError($table->getError()); - - return false; - } - - /* - * If there is an associated user account, we will attempt to send this email in the user's preferred language. - * Because of this, it is expected that Language::_() is directly called and that the Text class is NOT used - * for translating all messages. - * - * Error messages will still be displayed to the administrator, so those messages should continue to use the Text class. - */ - - $lang = Factory::getLanguage(); - - $db = $this->getDatabase(); - - $userId = (int) $db->setQuery( - $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') - ->bind(':email', $table->email) - ->setLimit(1) - )->loadResult(); - - if ($userId) - { - $receiver = User::getInstance($userId); - - /* - * We don't know if the user has admin access, so we will check if they have an admin language in their parameters, - * falling back to the site language, falling back to the currently active language - */ - - $langCode = $receiver->getParam('admin_language', ''); - - if (!$langCode) - { - $langCode = $receiver->getParam('language', $lang->getTag()); - } - - $lang = Language::getInstance($langCode, $lang->getDebug()); - } - - // Ensure the right language files have been loaded - $lang->load('com_privacy', JPATH_ADMINISTRATOR) - || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); - - // Regenerate the confirmation token - $token = ApplicationHelper::getHash(UserHelper::genRandomPassword()); - $hashedToken = UserHelper::hashPassword($token); - - $table->confirm_token = $hashedToken; - $table->confirm_token_created_at = Factory::getDate()->toSql(); - - try - { - $table->store(); - } - catch (ExecutionFailureException $exception) - { - $this->setError($exception->getMessage()); - - return false; - } - - // The mailer can be set to either throw Exceptions or return boolean false, account for both - try - { - $app = Factory::getApplication(); - - $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; - - $templateData = [ - 'sitename' => $app->get('sitename'), - 'url' => Uri::root(), - 'tokenurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm&confirm_token=' . $token, false, $linkMode, true), - 'formurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm', false, $linkMode, true), - 'token' => $token, - ]; - - switch ($table->request_type) - { - case 'export': - $mailer = new MailTemplate('com_privacy.notification.admin.export', $app->getLanguage()->getTag()); - - break; - - case 'remove': - $mailer = new MailTemplate('com_privacy.notification.admin.remove', $app->getLanguage()->getTag()); - - break; - - default: - $this->setError(Text::_('COM_PRIVACY_ERROR_UNKNOWN_REQUEST_TYPE')); - - return false; - } - - $mailer->addTemplateData($templateData); - $mailer->addRecipient($table->email); - - $mailer->send(); - - return true; - } - catch (MailDisabledException | phpmailerException $exception) - { - $this->setError($exception->getMessage()); - - return false; - } - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success, False on error. - * - * @since 3.9.0 - */ - public function save($data) - { - $table = $this->getTable(); - $key = $table->getKeyName(); - $pk = !empty($data[$key]) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); - - if (!$pk && !Factory::getApplication()->get('mailonline', 1)) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_CREATE_REQUEST_WHEN_SENDMAIL_DISABLED')); - - return false; - } - - return parent::save($data); - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @see \Joomla\CMS\Form\FormRule - * @see JFilterInput - * @since 3.9.0 - */ - public function validate($form, $data, $group = null) - { - $validatedData = parent::validate($form, $data, $group); - - // If parent validation failed there's no point in doing our extended validation - if ($validatedData === false) - { - return false; - } - - // Make sure the status is always 0 - $validatedData['status'] = 0; - - // The user cannot create a request for their own account - if (strtolower(Factory::getUser()->email) === strtolower($validatedData['email'])) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_CREATE_REQUEST_FOR_SELF')); - - return false; - } - - // Check for an active request for this email address - $db = $this->getDatabase(); - - $query = $db->getQuery(true) - ->select('COUNT(id)') - ->from($db->quoteName('#__privacy_requests')) - ->where($db->quoteName('email') . ' = :email') - ->where($db->quoteName('request_type') . ' = :requesttype') - ->whereIn($db->quoteName('status'), [0, 1]) - ->bind(':email', $validatedData['email']) - ->bind(':requesttype', $validatedData['request_type']); - - $activeRequestCount = (int) $db->setQuery($query)->loadResult(); - - if ($activeRequestCount > 0) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_ACTIVE_REQUEST_FOR_EMAIL')); - - return false; - } - - return $validatedData; - } - - /** - * Method to fetch an instance of the action log model. - * - * @return ActionlogModel - * - * @since 4.0.0 - */ - private function getActionlogModel(): ActionlogModel - { - return Factory::getApplication()->bootComponent('com_actionlogs') - ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); - } + /** + * Clean the cache + * + * @param string $group The cache group + * + * @return void + * + * @since 3.9.0 + */ + protected function cleanCache($group = 'com_privacy') + { + parent::cleanCache('com_privacy'); + } + + /** + * Method for getting the form from the model. + * + * @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 Form|boolean A Form object on success, false on failure + * + * @since 3.9.0 + */ + public function getForm($data = [], $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_privacy.request', 'request', ['control' => 'jform', 'load_data' => $loadData]); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @throws \Exception + * @since 3.9.0 + */ + public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Method to get the data that should be injected in the form. + * + * @return array The default data is an empty array. + * + * @since 3.9.0 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_privacy.edit.request.data', []); + + if (empty($data)) { + $data = $this->getItem(); + } + + return $data; + } + + /** + * Log the completion of a request to the action log system. + * + * @param integer $id The ID of the request to process. + * + * @return boolean + * + * @since 3.9.0 + */ + public function logRequestCompleted($id) + { + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($id)) { + $this->setError($table->getError()); + + return false; + } + + $user = Factory::getUser(); + + $message = [ + 'action' => 'request-completed', + 'requesttype' => $table->request_type, + 'subjectemail' => $table->email, + 'id' => $table->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, + 'userid' => $user->id, + 'username' => $user->username, + 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_ADMIN_COMPLETED_REQUEST', 'com_privacy.request', $user->id); + + return true; + } + + /** + * Log the creation of a request to the action log system. + * + * @param integer $id The ID of the request to process. + * + * @return boolean + * + * @since 3.9.0 + */ + public function logRequestCreated($id) + { + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($id)) { + $this->setError($table->getError()); + + return false; + } + + $user = Factory::getUser(); + + $message = [ + 'action' => 'request-created', + 'requesttype' => $table->request_type, + 'subjectemail' => $table->email, + 'id' => $table->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, + 'userid' => $user->id, + 'username' => $user->username, + 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_ADMIN_CREATED_REQUEST', 'com_privacy.request', $user->id); + + return true; + } + + /** + * Log the invalidation of a request to the action log system. + * + * @param integer $id The ID of the request to process. + * + * @return boolean + * + * @since 3.9.0 + */ + public function logRequestInvalidated($id) + { + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($id)) { + $this->setError($table->getError()); + + return false; + } + + $user = Factory::getUser(); + + $message = [ + 'action' => 'request-invalidated', + 'requesttype' => $table->request_type, + 'subjectemail' => $table->email, + 'id' => $table->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, + 'userid' => $user->id, + 'username' => $user->username, + 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $user->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_ADMIN_INVALIDATED_REQUEST', 'com_privacy.request', $user->id); + + return true; + } + + /** + * Notifies the user that an information request has been created by a site administrator. + * + * Because confirmation tokens are stored in the database as a hashed value, this method will generate a new confirmation token + * for the request. + * + * @param integer $id The ID of the request to process. + * + * @return boolean + * + * @since 3.9.0 + */ + public function notifyUserAdminCreatedRequest($id) + { + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($id)) { + $this->setError($table->getError()); + + return false; + } + + /* + * If there is an associated user account, we will attempt to send this email in the user's preferred language. + * Because of this, it is expected that Language::_() is directly called and that the Text class is NOT used + * for translating all messages. + * + * Error messages will still be displayed to the administrator, so those messages should continue to use the Text class. + */ + + $lang = Factory::getLanguage(); + + $db = $this->getDatabase(); + + $userId = (int) $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') + ->bind(':email', $table->email) + ->setLimit(1) + )->loadResult(); + + if ($userId) { + $receiver = User::getInstance($userId); + + /* + * We don't know if the user has admin access, so we will check if they have an admin language in their parameters, + * falling back to the site language, falling back to the currently active language + */ + + $langCode = $receiver->getParam('admin_language', ''); + + if (!$langCode) { + $langCode = $receiver->getParam('language', $lang->getTag()); + } + + $lang = Language::getInstance($langCode, $lang->getDebug()); + } + + // Ensure the right language files have been loaded + $lang->load('com_privacy', JPATH_ADMINISTRATOR) + || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); + + // Regenerate the confirmation token + $token = ApplicationHelper::getHash(UserHelper::genRandomPassword()); + $hashedToken = UserHelper::hashPassword($token); + + $table->confirm_token = $hashedToken; + $table->confirm_token_created_at = Factory::getDate()->toSql(); + + try { + $table->store(); + } catch (ExecutionFailureException $exception) { + $this->setError($exception->getMessage()); + + return false; + } + + // The mailer can be set to either throw Exceptions or return boolean false, account for both + try { + $app = Factory::getApplication(); + + $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; + + $templateData = [ + 'sitename' => $app->get('sitename'), + 'url' => Uri::root(), + 'tokenurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm&confirm_token=' . $token, false, $linkMode, true), + 'formurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm', false, $linkMode, true), + 'token' => $token, + ]; + + switch ($table->request_type) { + case 'export': + $mailer = new MailTemplate('com_privacy.notification.admin.export', $app->getLanguage()->getTag()); + + break; + + case 'remove': + $mailer = new MailTemplate('com_privacy.notification.admin.remove', $app->getLanguage()->getTag()); + + break; + + default: + $this->setError(Text::_('COM_PRIVACY_ERROR_UNKNOWN_REQUEST_TYPE')); + + return false; + } + + $mailer->addTemplateData($templateData); + $mailer->addRecipient($table->email); + + $mailer->send(); + + return true; + } catch (MailDisabledException | phpmailerException $exception) { + $this->setError($exception->getMessage()); + + return false; + } + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success, False on error. + * + * @since 3.9.0 + */ + public function save($data) + { + $table = $this->getTable(); + $key = $table->getKeyName(); + $pk = !empty($data[$key]) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); + + if (!$pk && !Factory::getApplication()->get('mailonline', 1)) { + $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_CREATE_REQUEST_WHEN_SENDMAIL_DISABLED')); + + return false; + } + + return parent::save($data); + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @see \Joomla\CMS\Form\FormRule + * @see JFilterInput + * @since 3.9.0 + */ + public function validate($form, $data, $group = null) + { + $validatedData = parent::validate($form, $data, $group); + + // If parent validation failed there's no point in doing our extended validation + if ($validatedData === false) { + return false; + } + + // Make sure the status is always 0 + $validatedData['status'] = 0; + + // The user cannot create a request for their own account + if (strtolower(Factory::getUser()->email) === strtolower($validatedData['email'])) { + $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_CREATE_REQUEST_FOR_SELF')); + + return false; + } + + // Check for an active request for this email address + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select('COUNT(id)') + ->from($db->quoteName('#__privacy_requests')) + ->where($db->quoteName('email') . ' = :email') + ->where($db->quoteName('request_type') . ' = :requesttype') + ->whereIn($db->quoteName('status'), [0, 1]) + ->bind(':email', $validatedData['email']) + ->bind(':requesttype', $validatedData['request_type']); + + $activeRequestCount = (int) $db->setQuery($query)->loadResult(); + + if ($activeRequestCount > 0) { + $this->setError(Text::_('COM_PRIVACY_ERROR_ACTIVE_REQUEST_FOR_EMAIL')); + + return false; + } + + return $validatedData; + } + + /** + * Method to fetch an instance of the action log model. + * + * @return ActionlogModel + * + * @since 4.0.0 + */ + private function getActionlogModel(): ActionlogModel + { + return Factory::getApplication()->bootComponent('com_actionlogs') + ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); + } } diff --git a/administrator/components/com_privacy/src/Model/RequestsModel.php b/administrator/components/com_privacy/src/Model/RequestsModel.php index c7d542d66437d..26692ab0c5108 100644 --- a/administrator/components/com_privacy/src/Model/RequestsModel.php +++ b/administrator/components/com_privacy/src/Model/RequestsModel.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select($this->getState('list.select', 'a.*')); - $query->from($db->quoteName('#__privacy_requests', 'a')); - - // Filter by status - $status = $this->getState('filter.status'); - - if (is_numeric($status)) - { - $status = (int) $status; - $query->where($db->quoteName('a.status') . ' = :status') - ->bind(':status', $status, ParameterType::INTEGER); - } - - // Filter by request type - $requestType = $this->getState('filter.request_type', ''); - - if ($requestType) - { - $query->where($db->quoteName('a.request_type') . ' = :requesttype') - ->bind(':requesttype', $requestType); - } - - // Filter by search in email - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . $search . '%'; - $query->where('(' . $db->quoteName('a.email') . ' LIKE :search)') - ->bind(':search', $search); - } - } - - // Handle the list ordering. - $ordering = $this->getState('list.ordering'); - $direction = $this->getState('list.direction'); - - if (!empty($ordering)) - { - $query->order($db->escape($ordering) . ' ' . $db->escape($direction)); - } - - return $query; - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string - * - * @since 3.9.0 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.status'); - $id .= ':' . $this->getState('filter.request_type'); - - return parent::getStoreId($id); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState($ordering = 'a.id', $direction = 'desc') - { - // Load the filter state. - $this->setState( - 'filter.search', - $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search') - ); - - $this->setState( - 'filter.status', - $this->getUserStateFromRequest($this->context . '.filter.status', 'filter_status', '', 'int') - ); - - $this->setState( - 'filter.request_type', - $this->getUserStateFromRequest($this->context . '.filter.request_type', 'filter_request_type', '', 'string') - ); - - // Load the parameters. - $this->setState('params', ComponentHelper::getParams('com_privacy')); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to return number privacy requests older than X days. - * - * @return integer - * - * @since 3.9.0 - */ - public function getNumberUrgentRequests() - { - // Load the parameters. - $params = ComponentHelper::getComponent('com_privacy')->getParams(); - $notify = (int) $params->get('notify', 14); - $now = Factory::getDate()->toSql(); - $period = '-' . $notify; - - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('COUNT(*)'); - $query->from($db->quoteName('#__privacy_requests')); - $query->where($db->quoteName('status') . ' = 1 '); - $query->where($query->dateAdd($db->quote($now), $period, 'DAY') . ' > ' . $db->quoteName('requested_at')); - $db->setQuery($query); - - return (int) $db->loadResult(); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 3.9.0 + */ + public function __construct($config = []) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = [ + 'id', 'a.id', + 'email', 'a.email', + 'requested_at', 'a.requested_at', + 'request_type', 'a.request_type', + 'status', 'a.status', + ]; + } + + parent::__construct($config); + } + + /** + * Method to get a DatabaseQuery object for retrieving the data set from a database. + * + * @return DatabaseQuery + * + * @since 3.9.0 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select($this->getState('list.select', 'a.*')); + $query->from($db->quoteName('#__privacy_requests', 'a')); + + // Filter by status + $status = $this->getState('filter.status'); + + if (is_numeric($status)) { + $status = (int) $status; + $query->where($db->quoteName('a.status') . ' = :status') + ->bind(':status', $status, ParameterType::INTEGER); + } + + // Filter by request type + $requestType = $this->getState('filter.request_type', ''); + + if ($requestType) { + $query->where($db->quoteName('a.request_type') . ' = :requesttype') + ->bind(':requesttype', $requestType); + } + + // Filter by search in email + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . $search . '%'; + $query->where('(' . $db->quoteName('a.email') . ' LIKE :search)') + ->bind(':search', $search); + } + } + + // Handle the list ordering. + $ordering = $this->getState('list.ordering'); + $direction = $this->getState('list.direction'); + + if (!empty($ordering)) { + $query->order($db->escape($ordering) . ' ' . $db->escape($direction)); + } + + return $query; + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string + * + * @since 3.9.0 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.status'); + $id .= ':' . $this->getState('filter.request_type'); + + return parent::getStoreId($id); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState($ordering = 'a.id', $direction = 'desc') + { + // Load the filter state. + $this->setState( + 'filter.search', + $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search') + ); + + $this->setState( + 'filter.status', + $this->getUserStateFromRequest($this->context . '.filter.status', 'filter_status', '', 'int') + ); + + $this->setState( + 'filter.request_type', + $this->getUserStateFromRequest($this->context . '.filter.request_type', 'filter_request_type', '', 'string') + ); + + // Load the parameters. + $this->setState('params', ComponentHelper::getParams('com_privacy')); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to return number privacy requests older than X days. + * + * @return integer + * + * @since 3.9.0 + */ + public function getNumberUrgentRequests() + { + // Load the parameters. + $params = ComponentHelper::getComponent('com_privacy')->getParams(); + $notify = (int) $params->get('notify', 14); + $now = Factory::getDate()->toSql(); + $period = '-' . $notify; + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('COUNT(*)'); + $query->from($db->quoteName('#__privacy_requests')); + $query->where($db->quoteName('status') . ' = 1 '); + $query->where($query->dateAdd($db->quote($now), $period, 'DAY') . ' > ' . $db->quoteName('requested_at')); + $db->setQuery($query); + + return (int) $db->loadResult(); + } } diff --git a/administrator/components/com_privacy/src/Plugin/PrivacyPlugin.php b/administrator/components/com_privacy/src/Plugin/PrivacyPlugin.php index 0679a47148290..90acf5589e30c 100644 --- a/administrator/components/com_privacy/src/Plugin/PrivacyPlugin.php +++ b/administrator/components/com_privacy/src/Plugin/PrivacyPlugin.php @@ -1,4 +1,5 @@ name = $name; - $domain->description = $description; - - return $domain; - } - - /** - * Create an item object for an array - * - * @param array $data The array data to convert - * @param integer|null $itemId The ID of this item - * - * @return Item - * - * @since 3.9.0 - */ - protected function createItemFromArray(array $data, $itemId = null) - { - $item = new Item; - $item->id = $itemId; - - foreach ($data as $key => $value) - { - if (is_object($value)) - { - $value = (array) $value; - } - - if (is_array($value)) - { - $value = print_r($value, true); - } - - $field = new Field; - $field->name = $key; - $field->value = $value; - - $item->addField($field); - } - - return $item; - } - - /** - * Create an item object for a Table object - * - * @param Table $table The Table object to convert - * - * @return Item - * - * @since 3.9.0 - */ - protected function createItemForTable($table) - { - $data = []; - - foreach (array_keys($table->getFields()) as $fieldName) - { - $data[$fieldName] = $table->$fieldName; - } - - return $this->createItemFromArray($data, $table->{$table->getKeyName(false)}); - } - - /** - * Helper function to create the domain for the items custom fields. - * - * @param string $context The context - * @param array $items The items - * - * @return Domain - * - * @since 3.9.0 - */ - protected function createCustomFieldsDomain($context, $items = array()) - { - if (!is_array($items)) - { - $items = [$items]; - } - - $parts = FieldsHelper::extract($context); - - if (!$parts) - { - return []; - } - - $type = str_replace('com_', '', $parts[0]); - - $domain = $this->createDomain($type . '_' . $parts[1] . '_custom_fields', 'joomla_' . $type . '_' . $parts[1] . '_custom_fields_data'); - - foreach ($items as $item) - { - // Get item's fields, also preparing their value property for manual display - $fields = FieldsHelper::getFields($parts[0] . '.' . $parts[1], $item); - - foreach ($fields as $field) - { - $fieldValue = is_array($field->value) ? implode(', ', $field->value) : $field->value; - - $data = [ - $type . '_id' => $item->id, - 'field_name' => $field->name, - 'field_title' => $field->title, - 'field_value' => $fieldValue, - ]; - - $domain->addItem($this->createItemFromArray($data)); - } - } - - return $domain; - } + /** + * Database object + * + * @var \Joomla\Database\DatabaseDriver + * @since 3.9.0 + */ + protected $db; + + /** + * Affects constructor behaviour. If true, language files will be loaded automatically. + * + * @var boolean + * @since 3.9.0 + */ + protected $autoloadLanguage = true; + + /** + * Create a new domain object + * + * @param string $name The domain's name + * @param string $description The domain's description + * + * @return Domain + * + * @since 3.9.0 + */ + protected function createDomain($name, $description = '') + { + $domain = new Domain(); + $domain->name = $name; + $domain->description = $description; + + return $domain; + } + + /** + * Create an item object for an array + * + * @param array $data The array data to convert + * @param integer|null $itemId The ID of this item + * + * @return Item + * + * @since 3.9.0 + */ + protected function createItemFromArray(array $data, $itemId = null) + { + $item = new Item(); + $item->id = $itemId; + + foreach ($data as $key => $value) { + if (is_object($value)) { + $value = (array) $value; + } + + if (is_array($value)) { + $value = print_r($value, true); + } + + $field = new Field(); + $field->name = $key; + $field->value = $value; + + $item->addField($field); + } + + return $item; + } + + /** + * Create an item object for a Table object + * + * @param Table $table The Table object to convert + * + * @return Item + * + * @since 3.9.0 + */ + protected function createItemForTable($table) + { + $data = []; + + foreach (array_keys($table->getFields()) as $fieldName) { + $data[$fieldName] = $table->$fieldName; + } + + return $this->createItemFromArray($data, $table->{$table->getKeyName(false)}); + } + + /** + * Helper function to create the domain for the items custom fields. + * + * @param string $context The context + * @param array $items The items + * + * @return Domain + * + * @since 3.9.0 + */ + protected function createCustomFieldsDomain($context, $items = array()) + { + if (!is_array($items)) { + $items = [$items]; + } + + $parts = FieldsHelper::extract($context); + + if (!$parts) { + return []; + } + + $type = str_replace('com_', '', $parts[0]); + + $domain = $this->createDomain($type . '_' . $parts[1] . '_custom_fields', 'joomla_' . $type . '_' . $parts[1] . '_custom_fields_data'); + + foreach ($items as $item) { + // Get item's fields, also preparing their value property for manual display + $fields = FieldsHelper::getFields($parts[0] . '.' . $parts[1], $item); + + foreach ($fields as $field) { + $fieldValue = is_array($field->value) ? implode(', ', $field->value) : $field->value; + + $data = [ + $type . '_id' => $item->id, + 'field_name' => $field->name, + 'field_title' => $field->title, + 'field_value' => $fieldValue, + ]; + + $domain->addItem($this->createItemFromArray($data)); + } + } + + return $domain; + } } diff --git a/administrator/components/com_privacy/src/Removal/Status.php b/administrator/components/com_privacy/src/Removal/Status.php index 03b698de5b24e..a3a53f5ded7a2 100644 --- a/administrator/components/com_privacy/src/Removal/Status.php +++ b/administrator/components/com_privacy/src/Removal/Status.php @@ -1,4 +1,5 @@ ' . Text::_('COM_PRIVACY_STATUS_COMPLETED') . ''; + /** + * Render a status badge + * + * @param integer $status The item status + * + * @return string + * + * @since 3.9.0 + */ + public function statusLabel($status) + { + switch ($status) { + case 2: + return '' . Text::_('COM_PRIVACY_STATUS_COMPLETED') . ''; - case 1: - return '' . Text::_('COM_PRIVACY_STATUS_CONFIRMED') . ''; + case 1: + return '' . Text::_('COM_PRIVACY_STATUS_CONFIRMED') . ''; - case -1: - return '' . Text::_('COM_PRIVACY_STATUS_INVALID') . ''; + case -1: + return '' . Text::_('COM_PRIVACY_STATUS_INVALID') . ''; - default: - case 0: - return '' . Text::_('COM_PRIVACY_STATUS_PENDING') . ''; - } - } + default: + case 0: + return '' . Text::_('COM_PRIVACY_STATUS_PENDING') . ''; + } + } } diff --git a/administrator/components/com_privacy/src/Table/ConsentTable.php b/administrator/components/com_privacy/src/Table/ConsentTable.php index 56330ce800cbf..c743b6273f84e 100644 --- a/administrator/components/com_privacy/src/Table/ConsentTable.php +++ b/administrator/components/com_privacy/src/Table/ConsentTable.php @@ -1,4 +1,5 @@ id) - { - if (!$this->remind) - { - $this->remind = '0'; - } + // Set default values for new records + if (!$this->id) { + if (!$this->remind) { + $this->remind = '0'; + } - if (!$this->created) - { - $this->created = $date->toSql(); - } - } + if (!$this->created) { + $this->created = $date->toSql(); + } + } - return parent::store($updateNulls); - } + return parent::store($updateNulls); + } } diff --git a/administrator/components/com_privacy/src/Table/RequestTable.php b/administrator/components/com_privacy/src/Table/RequestTable.php index 8028385bffbbb..d772779df86c8 100644 --- a/administrator/components/com_privacy/src/Table/RequestTable.php +++ b/administrator/components/com_privacy/src/Table/RequestTable.php @@ -1,4 +1,5 @@ id) - { - if (!$this->status) - { - $this->status = '0'; - } + // Set default values for new records + if (!$this->id) { + if (!$this->status) { + $this->status = '0'; + } - if (!$this->requested_at) - { - $this->requested_at = $date->toSql(); - } + if (!$this->requested_at) { + $this->requested_at = $date->toSql(); + } - if (!$this->confirm_token_created_at) - { - $this->confirm_token_created_at = null; - } - } + if (!$this->confirm_token_created_at) { + $this->confirm_token_created_at = null; + } + } - return parent::store($updateNulls); - } + return parent::store($updateNulls); + } } diff --git a/administrator/components/com_privacy/src/View/Capabilities/HtmlView.php b/administrator/components/com_privacy/src/View/Capabilities/HtmlView.php index ca86b2fc1b540..108bea965b403 100644 --- a/administrator/components/com_privacy/src/View/Capabilities/HtmlView.php +++ b/administrator/components/com_privacy/src/View/Capabilities/HtmlView.php @@ -1,4 +1,5 @@ capabilities = $this->get('Capabilities'); - $this->state = $this->get('State'); + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see BaseHtmlView::loadTemplate() + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + // Initialise variables + $this->capabilities = $this->get('Capabilities'); + $this->state = $this->get('State'); - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new Genericdataexception(implode("\n", $errors), 500); - } + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new Genericdataexception(implode("\n", $errors), 500); + } - $this->addToolbar(); + $this->addToolbar(); - parent::display($tpl); - } + parent::display($tpl); + } - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.9.0 - */ - protected function addToolbar() - { - ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_CAPABILITIES'), 'lock'); + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.9.0 + */ + protected function addToolbar() + { + ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_CAPABILITIES'), 'lock'); - ToolbarHelper::preferences('com_privacy'); + ToolbarHelper::preferences('com_privacy'); - ToolbarHelper::help('Privacy:_Extension_Capabilities'); - } + ToolbarHelper::help('Privacy:_Extension_Capabilities'); + } } diff --git a/administrator/components/com_privacy/src/View/Consents/HtmlView.php b/administrator/components/com_privacy/src/View/Consents/HtmlView.php index 13be2edb3c840..63d8243b54ab5 100644 --- a/administrator/components/com_privacy/src/View/Consents/HtmlView.php +++ b/administrator/components/com_privacy/src/View/Consents/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->items = $model->getItems(); - $this->pagination = $model->getPagination(); - $this->state = $model->getState(); - $this->filterForm = $model->getFilterForm(); - $this->activeFilters = $model->getActiveFilters(); - - if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new Genericdataexception(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.9.0 - */ - protected function addToolbar() - { - ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_CONSENTS'), 'lock'); - - $bar = Toolbar::getInstance('toolbar'); - - // Add a button to invalidate a consent - if (!$this->isEmptyState) - { - $bar->appendButton( - 'Confirm', - 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE_CONFIRM_MSG', - 'trash', - 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE', - 'consents.invalidate', - true - ); - } - - // If the filter is restricted to a specific subject, show the "Invalidate all" button - if ($this->state->get('filter.subject') != '') - { - $bar->appendButton( - 'Confirm', - 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE_ALL_CONFIRM_MSG', - 'cancel', - 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE_ALL', - 'consents.invalidateAll', - false - ); - } - - ToolbarHelper::preferences('com_privacy'); - - ToolbarHelper::help('Privacy:_Consents'); - } + /** + * The active search tools filters + * + * @var array + * @since 3.9.0 + * @note Must be public to be accessed from the search tools layout + */ + public $activeFilters; + + /** + * Form instance containing the search tools filter form + * + * @var Form + * @since 3.9.0 + * @note Must be public to be accessed from the search tools layout + */ + public $filterForm; + + /** + * The items to display + * + * @var array + * @since 3.9.0 + */ + protected $items; + + /** + * The pagination object + * + * @var Pagination + * @since 3.9.0 + */ + protected $pagination; + + /** + * The state information + * + * @var CMSObject + * @since 3.9.0 + */ + protected $state; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see BaseHtmlView::loadTemplate() + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + /** @var ConsentsModel $model */ + $model = $this->getModel(); + $this->items = $model->getItems(); + $this->pagination = $model->getPagination(); + $this->state = $model->getState(); + $this->filterForm = $model->getFilterForm(); + $this->activeFilters = $model->getActiveFilters(); + + if (!count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new Genericdataexception(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.9.0 + */ + protected function addToolbar() + { + ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_CONSENTS'), 'lock'); + + $bar = Toolbar::getInstance('toolbar'); + + // Add a button to invalidate a consent + if (!$this->isEmptyState) { + $bar->appendButton( + 'Confirm', + 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE_CONFIRM_MSG', + 'trash', + 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE', + 'consents.invalidate', + true + ); + } + + // If the filter is restricted to a specific subject, show the "Invalidate all" button + if ($this->state->get('filter.subject') != '') { + $bar->appendButton( + 'Confirm', + 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE_ALL_CONFIRM_MSG', + 'cancel', + 'COM_PRIVACY_CONSENTS_TOOLBAR_INVALIDATE_ALL', + 'consents.invalidateAll', + false + ); + } + + ToolbarHelper::preferences('com_privacy'); + + ToolbarHelper::help('Privacy:_Consents'); + } } diff --git a/administrator/components/com_privacy/src/View/Export/XmlView.php b/administrator/components/com_privacy/src/View/Export/XmlView.php index 8fdee62f5a3c1..d2e341e22ed3c 100644 --- a/administrator/components/com_privacy/src/View/Export/XmlView.php +++ b/administrator/components/com_privacy/src/View/Export/XmlView.php @@ -1,4 +1,5 @@ getModel(); - - $exportData = $model->collectDataForExportRequest(); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $requestId = $model->getState($model->getName() . '.request_id'); - - // This document should always be downloaded - $this->document->setDownload(true); - $this->document->setName('export-request-' . $requestId); - - echo PrivacyHelper::renderDataAsXml($exportData); - } + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + * + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + /** @var ExportModel $model */ + $model = $this->getModel(); + + $exportData = $model->collectDataForExportRequest(); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $requestId = $model->getState($model->getName() . '.request_id'); + + // This document should always be downloaded + $this->document->setDownload(true); + $this->document->setName('export-request-' . $requestId); + + echo PrivacyHelper::renderDataAsXml($exportData); + } } diff --git a/administrator/components/com_privacy/src/View/Request/HtmlView.php b/administrator/components/com_privacy/src/View/Request/HtmlView.php index 0ca8c3de68cfc..adc1647c1cc48 100644 --- a/administrator/components/com_privacy/src/View/Request/HtmlView.php +++ b/administrator/components/com_privacy/src/View/Request/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->item = $model->getItem(); - $this->state = $model->getState(); - - // Variables only required for the default layout - if ($this->getLayout() === 'default') - { - /** @var \Joomla\Component\Actionlogs\Administrator\Model\ActionlogsModel $logsModel */ - $logsModel = $this->getModel('actionlogs'); - - $this->actionlogs = $logsModel->getLogsForItem('com_privacy.request', $this->item->id); - - // Load the com_actionlogs language strings for use in the layout - $lang = Factory::getLanguage(); - $lang->load('com_actionlogs', JPATH_ADMINISTRATOR) - || $lang->load('com_actionlogs', JPATH_ADMINISTRATOR . '/components/com_actionlogs'); - } - - // Variables only required for the edit layout - if ($this->getLayout() === 'edit') - { - $this->form = $this->get('Form'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.9.0 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - // Set the title and toolbar based on the layout - if ($this->getLayout() === 'edit') - { - ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_REQUEST_ADD_REQUEST'), 'lock'); - - ToolbarHelper::save('request.save'); - ToolbarHelper::cancel('request.cancel'); - ToolbarHelper::help('Privacy:_New_Information_Request'); - } - else - { - ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_REQUEST_SHOW_REQUEST'), 'lock'); - - $bar = Toolbar::getInstance('toolbar'); - - // Add transition and action buttons based on item status - switch ($this->item->status) - { - case '0': - $bar->appendButton('Standard', 'cancel-circle', 'COM_PRIVACY_TOOLBAR_INVALIDATE', 'request.invalidate', false); - - break; - - case '1': - $return = '&return=' . base64_encode('index.php?option=com_privacy&view=request&id=' . (int) $this->item->id); - - $bar->appendButton('Standard', 'apply', 'COM_PRIVACY_TOOLBAR_COMPLETE', 'request.complete', false); - $bar->appendButton('Standard', 'cancel-circle', 'COM_PRIVACY_TOOLBAR_INVALIDATE', 'request.invalidate', false); - - if ($this->item->request_type === 'export') - { - ToolbarHelper::link( - Route::_('index.php?option=com_privacy&task=request.export&format=xml&id=' . (int) $this->item->id . $return), - 'COM_PRIVACY_ACTION_EXPORT_DATA', - 'download' - ); - - if (Factory::getApplication()->get('mailonline', 1)) - { - ToolbarHelper::link( - Route::_( - 'index.php?option=com_privacy&task=request.emailexport&id=' . (int) $this->item->id . $return - . '&' . Session::getFormToken() . '=1' - ), - 'COM_PRIVACY_ACTION_EMAIL_EXPORT_DATA', - 'mail' - ); - } - } - - if ($this->item->request_type === 'remove') - { - $bar->appendButton('Standard', 'delete', 'COM_PRIVACY_ACTION_DELETE_DATA', 'request.remove', false); - } - - break; - - // Item is in a "locked" state and cannot transition - default: - break; - } - - ToolbarHelper::cancel('request.cancel', 'JTOOLBAR_CLOSE'); - ToolbarHelper::help('Privacy:_Review_Information_Request'); - } - } + /** + * The action logs for the item + * + * @var array + * @since 3.9.0 + */ + protected $actionlogs; + + /** + * The form object + * + * @var Form + * @since 3.9.0 + */ + protected $form; + + /** + * The item record + * + * @var CMSObject + * @since 3.9.0 + */ + protected $item; + + /** + * The state information + * + * @var CMSObject + * @since 3.9.0 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see BaseHtmlView::loadTemplate() + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + /** @var RequestsModel $model */ + $model = $this->getModel(); + $this->item = $model->getItem(); + $this->state = $model->getState(); + + // Variables only required for the default layout + if ($this->getLayout() === 'default') { + /** @var \Joomla\Component\Actionlogs\Administrator\Model\ActionlogsModel $logsModel */ + $logsModel = $this->getModel('actionlogs'); + + $this->actionlogs = $logsModel->getLogsForItem('com_privacy.request', $this->item->id); + + // Load the com_actionlogs language strings for use in the layout + $lang = Factory::getLanguage(); + $lang->load('com_actionlogs', JPATH_ADMINISTRATOR) + || $lang->load('com_actionlogs', JPATH_ADMINISTRATOR . '/components/com_actionlogs'); + } + + // Variables only required for the edit layout + if ($this->getLayout() === 'edit') { + $this->form = $this->get('Form'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.9.0 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + // Set the title and toolbar based on the layout + if ($this->getLayout() === 'edit') { + ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_REQUEST_ADD_REQUEST'), 'lock'); + + ToolbarHelper::save('request.save'); + ToolbarHelper::cancel('request.cancel'); + ToolbarHelper::help('Privacy:_New_Information_Request'); + } else { + ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_REQUEST_SHOW_REQUEST'), 'lock'); + + $bar = Toolbar::getInstance('toolbar'); + + // Add transition and action buttons based on item status + switch ($this->item->status) { + case '0': + $bar->appendButton('Standard', 'cancel-circle', 'COM_PRIVACY_TOOLBAR_INVALIDATE', 'request.invalidate', false); + + break; + + case '1': + $return = '&return=' . base64_encode('index.php?option=com_privacy&view=request&id=' . (int) $this->item->id); + + $bar->appendButton('Standard', 'apply', 'COM_PRIVACY_TOOLBAR_COMPLETE', 'request.complete', false); + $bar->appendButton('Standard', 'cancel-circle', 'COM_PRIVACY_TOOLBAR_INVALIDATE', 'request.invalidate', false); + + if ($this->item->request_type === 'export') { + ToolbarHelper::link( + Route::_('index.php?option=com_privacy&task=request.export&format=xml&id=' . (int) $this->item->id . $return), + 'COM_PRIVACY_ACTION_EXPORT_DATA', + 'download' + ); + + if (Factory::getApplication()->get('mailonline', 1)) { + ToolbarHelper::link( + Route::_( + 'index.php?option=com_privacy&task=request.emailexport&id=' . (int) $this->item->id . $return + . '&' . Session::getFormToken() . '=1' + ), + 'COM_PRIVACY_ACTION_EMAIL_EXPORT_DATA', + 'mail' + ); + } + } + + if ($this->item->request_type === 'remove') { + $bar->appendButton('Standard', 'delete', 'COM_PRIVACY_ACTION_DELETE_DATA', 'request.remove', false); + } + + break; + + // Item is in a "locked" state and cannot transition + default: + break; + } + + ToolbarHelper::cancel('request.cancel', 'JTOOLBAR_CLOSE'); + ToolbarHelper::help('Privacy:_Review_Information_Request'); + } + } } diff --git a/administrator/components/com_privacy/src/View/Requests/HtmlView.php b/administrator/components/com_privacy/src/View/Requests/HtmlView.php index 6e23cc33c2df6..874964cb7d0af 100644 --- a/administrator/components/com_privacy/src/View/Requests/HtmlView.php +++ b/administrator/components/com_privacy/src/View/Requests/HtmlView.php @@ -1,4 +1,5 @@ getModel(); - $this->items = $model->getItems(); - $this->pagination = $model->getPagination(); - $this->state = $model->getState(); - $this->filterForm = $model->getFilterForm(); - $this->activeFilters = $model->getActiveFilters(); - $this->urgentRequestAge = (int) ComponentHelper::getParams('com_privacy')->get('notify', 14); - $this->sendMailEnabled = (bool) Factory::getApplication()->get('mailonline', 1); - - if (!count($this->items) && $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new Genericdataexception(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.9.0 - */ - protected function addToolbar() - { - ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_REQUESTS'), 'lock'); - - // Requests can only be created if mail sending is enabled - if (Factory::getApplication()->get('mailonline', 1)) - { - ToolbarHelper::addNew('request.add'); - } - - ToolbarHelper::preferences('com_privacy'); - ToolbarHelper::help('Privacy:_Information_Requests'); - } + /** + * The active search tools filters + * + * @var array + * @since 3.9.0 + * @note Must be public to be accessed from the search tools layout + */ + public $activeFilters; + + /** + * Form instance containing the search tools filter form + * + * @var Form + * @since 3.9.0 + * @note Must be public to be accessed from the search tools layout + */ + public $filterForm; + + /** + * The items to display + * + * @var array + * @since 3.9.0 + */ + protected $items; + + /** + * The pagination object + * + * @var Pagination + * @since 3.9.0 + */ + protected $pagination; + + /** + * Flag indicating the site supports sending email + * + * @var boolean + * @since 3.9.0 + */ + protected $sendMailEnabled; + + /** + * The state information + * + * @var CMSObject + * @since 3.9.0 + */ + protected $state; + + /** + * The age of urgent requests + * + * @var integer + * @since 3.9.0 + */ + protected $urgentRequestAge; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see BaseHtmlView::loadTemplate() + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + /** @var RequestsModel $model */ + $model = $this->getModel(); + $this->items = $model->getItems(); + $this->pagination = $model->getPagination(); + $this->state = $model->getState(); + $this->filterForm = $model->getFilterForm(); + $this->activeFilters = $model->getActiveFilters(); + $this->urgentRequestAge = (int) ComponentHelper::getParams('com_privacy')->get('notify', 14); + $this->sendMailEnabled = (bool) Factory::getApplication()->get('mailonline', 1); + + if (!count($this->items) && $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new Genericdataexception(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.9.0 + */ + protected function addToolbar() + { + ToolbarHelper::title(Text::_('COM_PRIVACY_VIEW_REQUESTS'), 'lock'); + + // Requests can only be created if mail sending is enabled + if (Factory::getApplication()->get('mailonline', 1)) { + ToolbarHelper::addNew('request.add'); + } + + ToolbarHelper::preferences('com_privacy'); + ToolbarHelper::help('Privacy:_Information_Requests'); + } } diff --git a/administrator/components/com_privacy/tmpl/capabilities/default.php b/administrator/components/com_privacy/tmpl/capabilities/default.php index fe0bfc66ecc47..137ed9db13173 100644 --- a/administrator/components/com_privacy/tmpl/capabilities/default.php +++ b/administrator/components/com_privacy/tmpl/capabilities/default.php @@ -1,4 +1,5 @@
-
-

- -
- capabilities)) : ?> -
- - -
- - capabilities as $extension => $capabilities) : ?> -
- - -
- - -
- -
    - -
  • - -
- -
- - +
+

+ +
+ capabilities)) : ?> +
+ + +
+ + capabilities as $extension => $capabilities) : ?> +
+ + +
+ + +
+ +
    + +
  • + +
+ +
+ +
diff --git a/administrator/components/com_privacy/tmpl/consents/default.php b/administrator/components/com_privacy/tmpl/consents/default.php index f4240c1933ea2..0878d0b277689 100644 --- a/administrator/components/com_privacy/tmpl/consents/default.php +++ b/administrator/components/com_privacy/tmpl/consents/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); @@ -29,100 +30,100 @@ $now = Factory::getDate(); $stateIcons = array(-1 => 'delete', 0 => 'archive', 1 => 'publish'); $stateMsgs = array( - -1 => Text::_('COM_PRIVACY_CONSENTS_STATE_INVALIDATED'), - 0 => Text::_('COM_PRIVACY_CONSENTS_STATE_OBSOLETE'), - 1 => Text::_('COM_PRIVACY_CONSENTS_STATE_VALID') + -1 => Text::_('COM_PRIVACY_CONSENTS_STATE_INVALIDATED'), + 0 => Text::_('COM_PRIVACY_CONSENTS_STATE_OBSOLETE'), + 1 => Text::_('COM_PRIVACY_CONSENTS_STATE_VALID') ); ?>
-
- $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - items as $i => $item) : ?> - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - - - -
- id, false, 'cid', 'cb', $item->username); ?> - - - state]; ?>"> - - username; ?> - - name; ?> - - user_id; ?> - - subject); ?> - - body; ?> - - created), null, $now); ?> -
- created, Text::_('DATE_FORMAT_LC6')); ?> -
-
- id; ?> -
- +
+ $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + items as $i => $item) : ?> + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->username); ?> + + + state]; ?>"> + + username; ?> + + name; ?> + + user_id; ?> + + subject); ?> + + body; ?> + + created), null, $now); ?> +
+ created, Text::_('DATE_FORMAT_LC6')); ?> +
+
+ id; ?> +
+ - - - -
+ + + +
diff --git a/administrator/components/com_privacy/tmpl/consents/emptystate.php b/administrator/components/com_privacy/tmpl/consents/emptystate.php index 4527f0f230941..a76086b554fc5 100644 --- a/administrator/components/com_privacy/tmpl/consents/emptystate.php +++ b/administrator/components/com_privacy/tmpl/consents/emptystate.php @@ -1,4 +1,5 @@ 'COM_PRIVACY_CONSENTS', - 'formURL' => 'index.php?option=com_privacy&view=consents', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Privacy:_Consents', - 'icon' => 'icon-lock', + 'textPrefix' => 'COM_PRIVACY_CONSENTS', + 'formURL' => 'index.php?option=com_privacy&view=consents', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Privacy:_Consents', + 'icon' => 'icon-lock', ]; echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_privacy/tmpl/request/default.php b/administrator/components/com_privacy/tmpl/request/default.php index 8fa51ae5ad271..95ae325ba8ab9 100644 --- a/administrator/components/com_privacy/tmpl/request/default.php +++ b/administrator/components/com_privacy/tmpl/request/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?>
-
-
-
-

-
-
-
:
-
item->email; ?>
+
+
+
+

+
+
+
:
+
item->email; ?>
-
:
-
item->status); ?>
+
:
+
item->status); ?>
-
:
-
item->request_type); ?>
+
:
+
item->request_type); ?>
-
:
-
item->requested_at, Text::_('DATE_FORMAT_LC6')); ?>
-
-
-
-
-
-
-

-
- actionlogs)) : ?> -
- - -
- - - - - - - - - actionlogs as $i => $item) : ?> - - - - - - - -
- - - - - -
- - - log_date, Text::_('DATE_FORMAT_LC6')); ?> - - name; ?> -
- -
-
-
-
+
:
+
item->requested_at, Text::_('DATE_FORMAT_LC6')); ?>
+
+
+
+
+
+
+

+
+ actionlogs)) : ?> +
+ + +
+ + + + + + + + + actionlogs as $i => $item) : ?> + + + + + + + +
+ + + + + +
+ + + log_date, Text::_('DATE_FORMAT_LC6')); ?> + + name; ?> +
+ +
+
+
+
- - + +
diff --git a/administrator/components/com_privacy/tmpl/request/edit.php b/administrator/components/com_privacy/tmpl/request/edit.php index d38658acf3345..682e79313dd4d 100644 --- a/administrator/components/com_privacy/tmpl/request/edit.php +++ b/administrator/components/com_privacy/tmpl/request/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?>
-
-
-
-
- form->renderField('email'); ?> - form->renderField('status'); ?> - form->renderField('request_type'); ?> -
-
-
- - - -
+
+
+
+
+ form->renderField('email'); ?> + form->renderField('status'); ?> + form->renderField('request_type'); ?> +
+
+
+ + + +
diff --git a/administrator/components/com_privacy/tmpl/requests/default.php b/administrator/components/com_privacy/tmpl/requests/default.php index 9c7a9fa19fcd6..5f8c0c0a59f08 100644 --- a/administrator/components/com_privacy/tmpl/requests/default.php +++ b/administrator/components/com_privacy/tmpl/requests/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); @@ -34,96 +35,96 @@ ?>
-
- $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - items as $i => $item) : ?> - requested_at); - ?> - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - -
-
- status == 1 && $item->request_type === 'export') : ?> - - sendMailEnabled) : ?> - - - - status == 1 && $item->request_type === 'remove') : ?> - - -
-
- status); ?> - - status == 1 && $urgentRequestDate >= $itemRequestedAt) : ?> - - - - escape($item->email)); ?> - - - request_type); ?> - - -
- requested_at, Text::_('DATE_FORMAT_LC6')); ?> -
-
- id; ?> -
+
+ $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + items as $i => $item) : ?> + requested_at); + ?> + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + +
+
+ status == 1 && $item->request_type === 'export') : ?> + + sendMailEnabled) : ?> + + + + status == 1 && $item->request_type === 'remove') : ?> + + +
+
+ status); ?> + + status == 1 && $urgentRequestDate >= $itemRequestedAt) : ?> + + + + escape($item->email)); ?> + + + request_type); ?> + + +
+ requested_at, Text::_('DATE_FORMAT_LC6')); ?> +
+
+ id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
+ + + +
diff --git a/administrator/components/com_privacy/tmpl/requests/emptystate.php b/administrator/components/com_privacy/tmpl/requests/emptystate.php index c345a7be9d0f1..14eb521edf0ca 100644 --- a/administrator/components/com_privacy/tmpl/requests/emptystate.php +++ b/administrator/components/com_privacy/tmpl/requests/emptystate.php @@ -1,4 +1,5 @@ 'COM_PRIVACY_REQUESTS', - 'formURL' => 'index.php?option=com_privacy&view=requests', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Privacy:_Information_Requests', - 'icon' => 'icon-lock', + 'textPrefix' => 'COM_PRIVACY_REQUESTS', + 'formURL' => 'index.php?option=com_privacy&view=requests', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:Privacy:_Information_Requests', + 'icon' => 'icon-lock', ]; -if (Factory::getApplication()->get('mailonline', 1)) -{ - $displayData['createURL'] = 'index.php?option=com_privacy&task=request.add'; +if (Factory::getApplication()->get('mailonline', 1)) { + $displayData['createURL'] = 'index.php?option=com_privacy&task=request.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_redirect/helpers/redirect.php b/administrator/components/com_redirect/helpers/redirect.php index 0f6c31e9f6dae..5d155966520f4 100644 --- a/administrator/components/com_redirect/helpers/redirect.php +++ b/administrator/components/com_redirect/helpers/redirect.php @@ -1,13 +1,16 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; + /** * Redirect component helper. diff --git a/administrator/components/com_redirect/layouts/toolbar/batch.php b/administrator/components/com_redirect/layouts/toolbar/batch.php index 98e9e6de4b6ff..c2b2ead1992a5 100644 --- a/administrator/components/com_redirect/layouts/toolbar/batch.php +++ b/administrator/components/com_redirect/layouts/toolbar/batch.php @@ -1,4 +1,5 @@ diff --git a/administrator/components/com_redirect/services/provider.php b/administrator/components/com_redirect/services/provider.php index 54db6ae48a2a7..d52d7e8230510 100644 --- a/administrator/components/com_redirect/services/provider.php +++ b/administrator/components/com_redirect/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Redirect')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Redirect')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Redirect')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Redirect')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new RedirectComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new RedirectComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_redirect/src/Controller/DisplayController.php b/administrator/components/com_redirect/src/Controller/DisplayController.php index 541446f8dd621..430d595386a0e 100644 --- a/administrator/components/com_redirect/src/Controller/DisplayController.php +++ b/administrator/components/com_redirect/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'links'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached. + * @param mixed $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static|boolean This object to support chaining or false on failure. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $view = $this->input->get('view', 'links'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); - if ($view === 'links') - { - $pluginEnabled = PluginHelper::isEnabled('system', 'redirect'); - $collectUrlsEnabled = RedirectHelper::collectUrlsEnabled(); + if ($view === 'links') { + $pluginEnabled = PluginHelper::isEnabled('system', 'redirect'); + $collectUrlsEnabled = RedirectHelper::collectUrlsEnabled(); - // Show messages about the enabled plugin and if the plugin should collect URLs - if ($pluginEnabled && $collectUrlsEnabled) - { - $this->app->enqueueMessage(Text::sprintf('COM_REDIRECT_COLLECT_URLS_ENABLED', Text::_('COM_REDIRECT_PLUGIN_ENABLED')), 'notice'); - } - else - { - $redirectPluginId = RedirectHelper::getRedirectPluginId(); - $link = HTMLHelper::_( - 'link', - '#plugin' . $redirectPluginId . 'Modal', - Text::_('COM_REDIRECT_SYSTEM_PLUGIN'), - 'class="alert-link" data-bs-toggle="modal" id="title-' . $redirectPluginId . '"' - ); + // Show messages about the enabled plugin and if the plugin should collect URLs + if ($pluginEnabled && $collectUrlsEnabled) { + $this->app->enqueueMessage(Text::sprintf('COM_REDIRECT_COLLECT_URLS_ENABLED', Text::_('COM_REDIRECT_PLUGIN_ENABLED')), 'notice'); + } else { + $redirectPluginId = RedirectHelper::getRedirectPluginId(); + $link = HTMLHelper::_( + 'link', + '#plugin' . $redirectPluginId . 'Modal', + Text::_('COM_REDIRECT_SYSTEM_PLUGIN'), + 'class="alert-link" data-bs-toggle="modal" id="title-' . $redirectPluginId . '"' + ); - if ($pluginEnabled && !$collectUrlsEnabled) - { - $this->app->enqueueMessage( - Text::sprintf('COM_REDIRECT_COLLECT_MODAL_URLS_DISABLED', Text::_('COM_REDIRECT_PLUGIN_ENABLED'), $link), - 'notice' - ); - } - else - { - $this->app->enqueueMessage(Text::sprintf('COM_REDIRECT_PLUGIN_MODAL_DISABLED', $link), 'error'); - } - } - } + if ($pluginEnabled && !$collectUrlsEnabled) { + $this->app->enqueueMessage( + Text::sprintf('COM_REDIRECT_COLLECT_MODAL_URLS_DISABLED', Text::_('COM_REDIRECT_PLUGIN_ENABLED'), $link), + 'notice' + ); + } else { + $this->app->enqueueMessage(Text::sprintf('COM_REDIRECT_PLUGIN_MODAL_DISABLED', $link), 'error'); + } + } + } - // Check for edit form. - if ($view == 'link' && $layout == 'edit' && !$this->checkEditId('com_redirect.edit.link', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } + // Check for edit form. + if ($view == 'link' && $layout == 'edit' && !$this->checkEditId('com_redirect.edit.link', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_redirect&view=links', false)); + $this->setRedirect(Route::_('index.php?option=com_redirect&view=links', false)); - return false; - } + return false; + } - return parent::display(); - } + return parent::display(); + } } diff --git a/administrator/components/com_redirect/src/Controller/LinkController.php b/administrator/components/com_redirect/src/Controller/LinkController.php index e6b38f58b2e46..943ff69a495ac 100644 --- a/administrator/components/com_redirect/src/Controller/LinkController.php +++ b/administrator/components/com_redirect/src/Controller/LinkController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Redirect\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Redirect\Administrator\Controller; use Joomla\CMS\MVC\Controller\FormController; @@ -19,5 +19,5 @@ */ class LinkController extends FormController { - // Parent class access checks are sufficient for this controller. + // Parent class access checks are sufficient for this controller. } diff --git a/administrator/components/com_redirect/src/Controller/LinksController.php b/administrator/components/com_redirect/src/Controller/LinksController.php index f18202fe86e99..aebc413a5c2b5 100644 --- a/administrator/components/com_redirect/src/Controller/LinksController.php +++ b/administrator/components/com_redirect/src/Controller/LinksController.php @@ -1,4 +1,5 @@ checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - $this->app->enqueueMessage(Text::_('COM_REDIRECT_NO_ITEM_SELECTED'), 'warning'); - } - else - { - $newUrl = $this->input->getString('new_url'); - $comment = $this->input->getString('comment'); - - // Get the model. - $model = $this->getModel(); - - // Remove the items. - if (!$model->activate($ids, $newUrl, $comment)) - { - $this->app->enqueueMessage($model->getError(), 'warning'); - } - else - { - $this->setMessage(Text::plural('COM_REDIRECT_N_LINKS_UPDATED', count($ids))); - } - } - - $this->setRedirect('index.php?option=com_redirect&view=links'); - } - - /** - * Method to duplicate URLs in records. - * - * @return void - * - * @since 3.6.0 - */ - public function duplicateUrls() - { - // Check for request forgeries. - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - $this->app->enqueueMessage(Text::_('COM_REDIRECT_NO_ITEM_SELECTED'), 'warning'); - } - else - { - $newUrl = $this->input->getString('new_url'); - $comment = $this->input->getString('comment'); - - // Get the model. - $model = $this->getModel(); - - // Remove the items. - if (!$model->duplicateUrls($ids, $newUrl, $comment)) - { - $this->app->enqueueMessage($model->getError(), 'warning'); - } - else - { - $this->setMessage(Text::plural('COM_REDIRECT_N_LINKS_UPDATED', count($ids))); - } - } - - $this->setRedirect('index.php?option=com_redirect&view=links'); - } - - /** - * Proxy for getModel. - * - * @param string $name The name of the model. - * @param string $prefix The prefix of the model. - * @param array $config An array of settings. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model instance - * - * @since 1.6 - */ - public function getModel($name = 'Link', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Executes the batch process to add URLs to the database - * - * @return void - */ - public function batch() - { - // Check for request forgeries. - $this->checkToken(); - - $batch_urls_request = $this->input->post->get('batch_urls', array(), 'array'); - $batch_urls_lines = array_map('trim', explode("\n", $batch_urls_request[0])); - - $batch_urls = array(); - - foreach ($batch_urls_lines as $batch_urls_line) - { - if (!empty($batch_urls_line)) - { - $params = ComponentHelper::getParams('com_redirect'); - $separator = $params->get('separator', '|'); - - // Basic check to make sure the correct separator is being used - if (!\Joomla\String\StringHelper::strpos($batch_urls_line, $separator)) - { - $this->setMessage(Text::sprintf('COM_REDIRECT_NO_SEPARATOR_FOUND', $separator), 'error'); - $this->setRedirect('index.php?option=com_redirect&view=links'); - - return; - } - - $batch_urls[] = array_map('trim', explode($separator, $batch_urls_line)); - } - } - - // Set default message on error - overwrite if successful - $this->setMessage(Text::_('COM_REDIRECT_NO_ITEM_ADDED'), 'error'); - - if (!empty($batch_urls)) - { - $model = $this->getModel('Links'); - - // Execute the batch process - if ($model->batchProcess($batch_urls)) - { - $this->setMessage(Text::plural('COM_REDIRECT_N_LINKS_ADDED', count($batch_urls))); - } - } - - $this->setRedirect('index.php?option=com_redirect&view=links'); - } - - /** - * Clean out the unpublished links. - * - * @return void - * - * @since 3.5 - */ - public function purge() - { - // Check for request forgeries. - $this->checkToken(); - - $model = $this->getModel('Links'); - - if ($model->purge()) - { - $message = Text::_('COM_REDIRECT_CLEAR_SUCCESS'); - } - else - { - $message = Text::_('COM_REDIRECT_CLEAR_FAIL'); - } - - $this->setRedirect('index.php?option=com_redirect&view=links', $message); - } + /** + * Method to update a record. + * + * @return void + * + * @since 1.6 + */ + public function activate() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + $this->app->enqueueMessage(Text::_('COM_REDIRECT_NO_ITEM_SELECTED'), 'warning'); + } else { + $newUrl = $this->input->getString('new_url'); + $comment = $this->input->getString('comment'); + + // Get the model. + $model = $this->getModel(); + + // Remove the items. + if (!$model->activate($ids, $newUrl, $comment)) { + $this->app->enqueueMessage($model->getError(), 'warning'); + } else { + $this->setMessage(Text::plural('COM_REDIRECT_N_LINKS_UPDATED', count($ids))); + } + } + + $this->setRedirect('index.php?option=com_redirect&view=links'); + } + + /** + * Method to duplicate URLs in records. + * + * @return void + * + * @since 3.6.0 + */ + public function duplicateUrls() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + $this->app->enqueueMessage(Text::_('COM_REDIRECT_NO_ITEM_SELECTED'), 'warning'); + } else { + $newUrl = $this->input->getString('new_url'); + $comment = $this->input->getString('comment'); + + // Get the model. + $model = $this->getModel(); + + // Remove the items. + if (!$model->duplicateUrls($ids, $newUrl, $comment)) { + $this->app->enqueueMessage($model->getError(), 'warning'); + } else { + $this->setMessage(Text::plural('COM_REDIRECT_N_LINKS_UPDATED', count($ids))); + } + } + + $this->setRedirect('index.php?option=com_redirect&view=links'); + } + + /** + * Proxy for getModel. + * + * @param string $name The name of the model. + * @param string $prefix The prefix of the model. + * @param array $config An array of settings. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model instance + * + * @since 1.6 + */ + public function getModel($name = 'Link', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Executes the batch process to add URLs to the database + * + * @return void + */ + public function batch() + { + // Check for request forgeries. + $this->checkToken(); + + $batch_urls_request = $this->input->post->get('batch_urls', array(), 'array'); + $batch_urls_lines = array_map('trim', explode("\n", $batch_urls_request[0])); + + $batch_urls = array(); + + foreach ($batch_urls_lines as $batch_urls_line) { + if (!empty($batch_urls_line)) { + $params = ComponentHelper::getParams('com_redirect'); + $separator = $params->get('separator', '|'); + + // Basic check to make sure the correct separator is being used + if (!\Joomla\String\StringHelper::strpos($batch_urls_line, $separator)) { + $this->setMessage(Text::sprintf('COM_REDIRECT_NO_SEPARATOR_FOUND', $separator), 'error'); + $this->setRedirect('index.php?option=com_redirect&view=links'); + + return; + } + + $batch_urls[] = array_map('trim', explode($separator, $batch_urls_line)); + } + } + + // Set default message on error - overwrite if successful + $this->setMessage(Text::_('COM_REDIRECT_NO_ITEM_ADDED'), 'error'); + + if (!empty($batch_urls)) { + $model = $this->getModel('Links'); + + // Execute the batch process + if ($model->batchProcess($batch_urls)) { + $this->setMessage(Text::plural('COM_REDIRECT_N_LINKS_ADDED', count($batch_urls))); + } + } + + $this->setRedirect('index.php?option=com_redirect&view=links'); + } + + /** + * Clean out the unpublished links. + * + * @return void + * + * @since 3.5 + */ + public function purge() + { + // Check for request forgeries. + $this->checkToken(); + + $model = $this->getModel('Links'); + + if ($model->purge()) { + $message = Text::_('COM_REDIRECT_CLEAR_SUCCESS'); + } else { + $message = Text::_('COM_REDIRECT_CLEAR_FAIL'); + } + + $this->setRedirect('index.php?option=com_redirect&view=links', $message); + } } diff --git a/administrator/components/com_redirect/src/Extension/RedirectComponent.php b/administrator/components/com_redirect/src/Extension/RedirectComponent.php index 2f155999b68b6..3bf9ddfa191ce 100644 --- a/administrator/components/com_redirect/src/Extension/RedirectComponent.php +++ b/administrator/components/com_redirect/src/Extension/RedirectComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('redirect', new Redirect); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('redirect', new Redirect()); + } } diff --git a/administrator/components/com_redirect/src/Field/RedirectField.php b/administrator/components/com_redirect/src/Field/RedirectField.php index 719c7b3c79c2a..aaf2f20b4e78e 100644 --- a/administrator/components/com_redirect/src/Field/RedirectField.php +++ b/administrator/components/com_redirect/src/Field/RedirectField.php @@ -1,4 +1,5 @@ 'HTTP/1.1 100 Continue', - 101 => 'HTTP/1.1 101 Switching Protocols', - 102 => 'HTTP/1.1 102 Processing', - 103 => 'HTTP/1.1 103 Early Hints', - 200 => 'HTTP/1.1 200 OK', - 201 => 'HTTP/1.1 201 Created', - 202 => 'HTTP/1.1 202 Accepted', - 203 => 'HTTP/1.1 203 Non-Authoritative Information', - 204 => 'HTTP/1.1 204 No Content', - 205 => 'HTTP/1.1 205 Reset Content', - 206 => 'HTTP/1.1 206 Partial Content', - 207 => 'HTTP/1.1 207 Multi-Status', - 208 => 'HTTP/1.1 208 Already Reported', - 226 => 'HTTP/1.1 226 IM Used', - 300 => 'HTTP/1.1 300 Multiple Choices', - 301 => 'HTTP/1.1 301 Moved Permanently', - 302 => 'HTTP/1.1 302 Found', - 303 => 'HTTP/1.1 303 See other', - 304 => 'HTTP/1.1 304 Not Modified', - 305 => 'HTTP/1.1 305 Use Proxy', - 306 => 'HTTP/1.1 306 (Unused)', - 307 => 'HTTP/1.1 307 Temporary Redirect', - 308 => 'HTTP/1.1 308 Permanent Redirect', - 400 => 'HTTP/1.1 400 Bad Request', - 401 => 'HTTP/1.1 401 Unauthorized', - 402 => 'HTTP/1.1 402 Payment Required', - 403 => 'HTTP/1.1 403 Forbidden', - 404 => 'HTTP/1.1 404 Not Found', - 405 => 'HTTP/1.1 405 Method Not Allowed', - 406 => 'HTTP/1.1 406 Not Acceptable', - 407 => 'HTTP/1.1 407 Proxy Authentication Required', - 408 => 'HTTP/1.1 408 Request Timeout', - 409 => 'HTTP/1.1 409 Conflict', - 410 => 'HTTP/1.1 410 Gone', - 411 => 'HTTP/1.1 411 Length Required', - 412 => 'HTTP/1.1 412 Precondition Failed', - 413 => 'HTTP/1.1 413 Payload Too Large', - 414 => 'HTTP/1.1 414 URI Too Long', - 415 => 'HTTP/1.1 415 Unsupported Media Type', - 416 => 'HTTP/1.1 416 Requested Range Not Satisfiable', - 417 => 'HTTP/1.1 417 Expectation Failed', - 418 => 'HTTP/1.1 418 I\'m a teapot', - 421 => 'HTTP/1.1 421 Misdirected Request', - 422 => 'HTTP/1.1 422 Unprocessable Entity', - 423 => 'HTTP/1.1 423 Locked', - 424 => 'HTTP/1.1 424 Failed Dependency', - 425 => 'HTTP/1.1 425 Reserved for WebDAV advanced collections expired proposal', - 426 => 'HTTP/1.1 426 Upgrade Required', - 428 => 'HTTP/1.1 428 Precondition Required', - 429 => 'HTTP/1.1 429 Too Many Requests', - 431 => 'HTTP/1.1 431 Request Header Fields Too Large', - 451 => 'HTTP/1.1 451 Unavailable For Legal Reasons', - 500 => 'HTTP/1.1 500 Internal Server Error', - 501 => 'HTTP/1.1 501 Not Implemented', - 502 => 'HTTP/1.1 502 Bad Gateway', - 503 => 'HTTP/1.1 503 Service Unavailable', - 504 => 'HTTP/1.1 504 Gateway Timeout', - 505 => 'HTTP/1.1 505 HTTP Version Not Supported', - 506 => 'HTTP/1.1 506 Variant Also Negotiates (Experimental)', - 507 => 'HTTP/1.1 507 Insufficient Storage', - 508 => 'HTTP/1.1 508 Loop Detected', - 510 => 'HTTP/1.1 510 Not Extended', - 511 => 'HTTP/1.1 511 Network Authentication Required', - ); + /** + * A map of integer HTTP 1.1 response codes to the full HTTP Status for the headers. + * + * @var object + * @since 3.4 + * @link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + */ + protected $responseMap = array( + 100 => 'HTTP/1.1 100 Continue', + 101 => 'HTTP/1.1 101 Switching Protocols', + 102 => 'HTTP/1.1 102 Processing', + 103 => 'HTTP/1.1 103 Early Hints', + 200 => 'HTTP/1.1 200 OK', + 201 => 'HTTP/1.1 201 Created', + 202 => 'HTTP/1.1 202 Accepted', + 203 => 'HTTP/1.1 203 Non-Authoritative Information', + 204 => 'HTTP/1.1 204 No Content', + 205 => 'HTTP/1.1 205 Reset Content', + 206 => 'HTTP/1.1 206 Partial Content', + 207 => 'HTTP/1.1 207 Multi-Status', + 208 => 'HTTP/1.1 208 Already Reported', + 226 => 'HTTP/1.1 226 IM Used', + 300 => 'HTTP/1.1 300 Multiple Choices', + 301 => 'HTTP/1.1 301 Moved Permanently', + 302 => 'HTTP/1.1 302 Found', + 303 => 'HTTP/1.1 303 See other', + 304 => 'HTTP/1.1 304 Not Modified', + 305 => 'HTTP/1.1 305 Use Proxy', + 306 => 'HTTP/1.1 306 (Unused)', + 307 => 'HTTP/1.1 307 Temporary Redirect', + 308 => 'HTTP/1.1 308 Permanent Redirect', + 400 => 'HTTP/1.1 400 Bad Request', + 401 => 'HTTP/1.1 401 Unauthorized', + 402 => 'HTTP/1.1 402 Payment Required', + 403 => 'HTTP/1.1 403 Forbidden', + 404 => 'HTTP/1.1 404 Not Found', + 405 => 'HTTP/1.1 405 Method Not Allowed', + 406 => 'HTTP/1.1 406 Not Acceptable', + 407 => 'HTTP/1.1 407 Proxy Authentication Required', + 408 => 'HTTP/1.1 408 Request Timeout', + 409 => 'HTTP/1.1 409 Conflict', + 410 => 'HTTP/1.1 410 Gone', + 411 => 'HTTP/1.1 411 Length Required', + 412 => 'HTTP/1.1 412 Precondition Failed', + 413 => 'HTTP/1.1 413 Payload Too Large', + 414 => 'HTTP/1.1 414 URI Too Long', + 415 => 'HTTP/1.1 415 Unsupported Media Type', + 416 => 'HTTP/1.1 416 Requested Range Not Satisfiable', + 417 => 'HTTP/1.1 417 Expectation Failed', + 418 => 'HTTP/1.1 418 I\'m a teapot', + 421 => 'HTTP/1.1 421 Misdirected Request', + 422 => 'HTTP/1.1 422 Unprocessable Entity', + 423 => 'HTTP/1.1 423 Locked', + 424 => 'HTTP/1.1 424 Failed Dependency', + 425 => 'HTTP/1.1 425 Reserved for WebDAV advanced collections expired proposal', + 426 => 'HTTP/1.1 426 Upgrade Required', + 428 => 'HTTP/1.1 428 Precondition Required', + 429 => 'HTTP/1.1 429 Too Many Requests', + 431 => 'HTTP/1.1 431 Request Header Fields Too Large', + 451 => 'HTTP/1.1 451 Unavailable For Legal Reasons', + 500 => 'HTTP/1.1 500 Internal Server Error', + 501 => 'HTTP/1.1 501 Not Implemented', + 502 => 'HTTP/1.1 502 Bad Gateway', + 503 => 'HTTP/1.1 503 Service Unavailable', + 504 => 'HTTP/1.1 504 Gateway Timeout', + 505 => 'HTTP/1.1 505 HTTP Version Not Supported', + 506 => 'HTTP/1.1 506 Variant Also Negotiates (Experimental)', + 507 => 'HTTP/1.1 507 Insufficient Storage', + 508 => 'HTTP/1.1 508 Loop Detected', + 510 => 'HTTP/1.1 510 Not Extended', + 511 => 'HTTP/1.1 511 Network Authentication Required', + ); - /** - * Method to get the field input markup. - * - * @return array The field input markup. - * - * @since 3.4 - */ - protected function getOptions() - { - $options = array(); + /** + * Method to get the field input markup. + * + * @return array The field input markup. + * + * @since 3.4 + */ + protected function getOptions() + { + $options = array(); - foreach ($this->responseMap as $key => $value) - { - $options[] = HTMLHelper::_('select.option', $key, $value); - } + foreach ($this->responseMap as $key => $value) { + $options[] = HTMLHelper::_('select.option', $key, $value); + } - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $options); + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $options); - return $options; - } + return $options; + } } diff --git a/administrator/components/com_redirect/src/Helper/RedirectHelper.php b/administrator/components/com_redirect/src/Helper/RedirectHelper.php index f09310abd0b4d..a7f08454778bc 100644 --- a/administrator/components/com_redirect/src/Helper/RedirectHelper.php +++ b/administrator/components/com_redirect/src/Helper/RedirectHelper.php @@ -1,4 +1,5 @@ getQuery(true) - ->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) - ->where($db->quoteName('element') . ' = ' . $db->quote('redirect')); - $db->setQuery($query); + /** + * Gets the redirect system plugin extension id. + * + * @return integer The redirect system plugin extension id. + * + * @since 3.6.0 + */ + public static function getRedirectPluginId() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ->where($db->quoteName('element') . ' = ' . $db->quote('redirect')); + $db->setQuery($query); - try - { - $result = (int) $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } + try { + $result = (int) $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } - return $result; - } + return $result; + } - /** - * Checks whether the option "Collect URLs" is enabled for the output message - * - * @return boolean - * - * @since 3.4 - */ - public static function collectUrlsEnabled() - { - $collect_urls = false; + /** + * Checks whether the option "Collect URLs" is enabled for the output message + * + * @return boolean + * + * @since 3.4 + */ + public static function collectUrlsEnabled() + { + $collect_urls = false; - if (PluginHelper::isEnabled('system', 'redirect')) - { - $params = new Registry(PluginHelper::getPlugin('system', 'redirect')->params); - $collect_urls = (bool) $params->get('collect_urls', 1); - } + if (PluginHelper::isEnabled('system', 'redirect')) { + $params = new Registry(PluginHelper::getPlugin('system', 'redirect')->params); + $collect_urls = (bool) $params->get('collect_urls', 1); + } - return $collect_urls; - } + return $collect_urls; + } } diff --git a/administrator/components/com_redirect/src/Model/LinkModel.php b/administrator/components/com_redirect/src/Model/LinkModel.php index 7eff8659f7aa6..62c3d5531d80e 100644 --- a/administrator/components/com_redirect/src/Model/LinkModel.php +++ b/administrator/components/com_redirect/src/Model/LinkModel.php @@ -1,4 +1,5 @@ published != -2) - { - return false; - } - - return parent::canDelete($record); - } - - /** - * Method to get the record form. - * - * @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 \Joomla\CMS\Form\Form A JForm object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_redirect.link', 'link', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Modify the form based on access controls. - if ($this->canEditState((object) $data) != true) - { - // Disable fields for display. - $form->setFieldAttribute('published', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - // If in advanced mode then we make sure the new URL field is not compulsory and the header - // field compulsory in case people select non-3xx redirects - if (ComponentHelper::getParams('com_redirect')->get('mode', 0) == true) - { - $form->setFieldAttribute('new_url', 'required', 'false'); - $form->setFieldAttribute('header', 'required', 'true'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_redirect.edit.link.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_redirect.link', $data); - - return $data; - } - - /** - * Method to activate links. - * - * @param array &$pks An array of link ids. - * @param string $url The new URL to set for the redirect. - * @param string $comment A comment for the redirect links. - * - * @return boolean Returns true on success, false on failure. - * - * @since 1.6 - */ - public function activate(&$pks, $url, $comment = null) - { - $user = Factory::getUser(); - $db = $this->getDatabase(); - - // Sanitize the ids. - $pks = (array) $pks; - $pks = ArrayHelper::toInteger($pks); - - // Populate default comment if necessary. - $comment = (!empty($comment)) ? $comment : Text::sprintf('COM_REDIRECT_REDIRECTED_ON', HTMLHelper::_('date', time())); - - // Access checks. - if (!$user->authorise('core.edit', 'com_redirect')) - { - $pks = array(); - $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED')); - - return false; - } - - if (!empty($pks)) - { - // Update the link rows. - $query = $db->getQuery(true) - ->update($db->quoteName('#__redirect_links')) - ->set($db->quoteName('new_url') . ' = :url') - ->set($db->quoteName('published') . ' = 1') - ->set($db->quoteName('comment') . ' = :comment') - ->whereIn($db->quoteName('id'), $pks) - ->bind(':url', $url) - ->bind(':comment', $comment); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - return true; - } - - /** - * Method to batch update URLs to have new redirect urls and comments. Note will publish any unpublished URLs. - * - * @param array &$pks An array of link ids. - * @param string $url The new URL to set for the redirect. - * @param string $comment A comment for the redirect links. - * - * @return boolean Returns true on success, false on failure. - * - * @since 3.6.0 - */ - public function duplicateUrls(&$pks, $url, $comment = null) - { - $user = Factory::getUser(); - $db = $this->getDatabase(); - - // Sanitize the ids. - $pks = (array) $pks; - $pks = ArrayHelper::toInteger($pks); - - // Access checks. - if (!$user->authorise('core.edit', 'com_redirect')) - { - $pks = array(); - $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED')); - - return false; - } - - if (!empty($pks)) - { - $date = Factory::getDate()->toSql(); - - // Update the link rows. - $query = $db->getQuery(true) - ->update($db->quoteName('#__redirect_links')) - ->set($db->quoteName('new_url') . ' = :url') - ->set($db->quoteName('modified_date') . ' = :date') - ->set($db->quoteName('published') . ' = 1') - ->whereIn($db->quoteName('id'), $pks) - ->bind(':url', $url) - ->bind(':date', $date); - - if (!empty($comment)) - { - $query->set($db->quoteName('comment') . ' = ' . $db->quote($comment)); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - return true; - } + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_REDIRECT'; + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + if ($record->published != -2) { + return false; + } + + return parent::canDelete($record); + } + + /** + * Method to get the record form. + * + * @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 \Joomla\CMS\Form\Form A JForm object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_redirect.link', 'link', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Modify the form based on access controls. + if ($this->canEditState((object) $data) != true) { + // Disable fields for display. + $form->setFieldAttribute('published', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + // If in advanced mode then we make sure the new URL field is not compulsory and the header + // field compulsory in case people select non-3xx redirects + if (ComponentHelper::getParams('com_redirect')->get('mode', 0) == true) { + $form->setFieldAttribute('new_url', 'required', 'false'); + $form->setFieldAttribute('header', 'required', 'true'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_redirect.edit.link.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_redirect.link', $data); + + return $data; + } + + /** + * Method to activate links. + * + * @param array &$pks An array of link ids. + * @param string $url The new URL to set for the redirect. + * @param string $comment A comment for the redirect links. + * + * @return boolean Returns true on success, false on failure. + * + * @since 1.6 + */ + public function activate(&$pks, $url, $comment = null) + { + $user = Factory::getUser(); + $db = $this->getDatabase(); + + // Sanitize the ids. + $pks = (array) $pks; + $pks = ArrayHelper::toInteger($pks); + + // Populate default comment if necessary. + $comment = (!empty($comment)) ? $comment : Text::sprintf('COM_REDIRECT_REDIRECTED_ON', HTMLHelper::_('date', time())); + + // Access checks. + if (!$user->authorise('core.edit', 'com_redirect')) { + $pks = array(); + $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED')); + + return false; + } + + if (!empty($pks)) { + // Update the link rows. + $query = $db->getQuery(true) + ->update($db->quoteName('#__redirect_links')) + ->set($db->quoteName('new_url') . ' = :url') + ->set($db->quoteName('published') . ' = 1') + ->set($db->quoteName('comment') . ' = :comment') + ->whereIn($db->quoteName('id'), $pks) + ->bind(':url', $url) + ->bind(':comment', $comment); + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + return true; + } + + /** + * Method to batch update URLs to have new redirect urls and comments. Note will publish any unpublished URLs. + * + * @param array &$pks An array of link ids. + * @param string $url The new URL to set for the redirect. + * @param string $comment A comment for the redirect links. + * + * @return boolean Returns true on success, false on failure. + * + * @since 3.6.0 + */ + public function duplicateUrls(&$pks, $url, $comment = null) + { + $user = Factory::getUser(); + $db = $this->getDatabase(); + + // Sanitize the ids. + $pks = (array) $pks; + $pks = ArrayHelper::toInteger($pks); + + // Access checks. + if (!$user->authorise('core.edit', 'com_redirect')) { + $pks = array(); + $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED')); + + return false; + } + + if (!empty($pks)) { + $date = Factory::getDate()->toSql(); + + // Update the link rows. + $query = $db->getQuery(true) + ->update($db->quoteName('#__redirect_links')) + ->set($db->quoteName('new_url') . ' = :url') + ->set($db->quoteName('modified_date') . ' = :date') + ->set($db->quoteName('published') . ' = 1') + ->whereIn($db->quoteName('id'), $pks) + ->bind(':url', $url) + ->bind(':date', $date); + + if (!empty($comment)) { + $query->set($db->quoteName('comment') . ' = ' . $db->quote($comment)); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + return true; + } } diff --git a/administrator/components/com_redirect/src/Model/LinksModel.php b/administrator/components/com_redirect/src/Model/LinksModel.php index de012229f9b7d..eaa2b1a392b2f 100644 --- a/administrator/components/com_redirect/src/Model/LinksModel.php +++ b/administrator/components/com_redirect/src/Model/LinksModel.php @@ -1,4 +1,5 @@ getDatabase(); - - $query = $db->getQuery(true); - - $query->delete('#__redirect_links')->where($db->quoteName('published') . '= 0'); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\Exception $e) - { - return false; - } - - return true; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'a.old_url', $direction = 'asc') - { - // Load the parameters. - $params = ComponentHelper::getParams('com_redirect'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.http_status'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.*' - ) - ); - $query->from($db->quoteName('#__redirect_links', 'a')); - - // Filter by published state - $state = (string) $this->getState('filter.state'); - - if (is_numeric($state)) - { - $state = (int) $state; - $query->where($db->quoteName('a.published') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - elseif ($state === '') - { - $query->whereIn($db->quoteName('a.published'), [0,1]); - } - - // Filter the items over the HTTP status code header. - if ($httpStatusCode = $this->getState('filter.http_status')) - { - $httpStatusCode = (int) $httpStatusCode; - $query->where($db->quoteName('a.header') . ' = :header') - ->bind(':header', $httpStatusCode, ParameterType::INTEGER); - } - - // Filter the items over the search string if set. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%'); - $query->where( - '(' . $db->quoteName('old_url') . ' LIKE :oldurl' - . ' OR ' . $db->quoteName('new_url') . ' LIKE :newurl' - . ' OR ' . $db->quoteName('comment') . ' LIKE :comment' - . ' OR ' . $db->quoteName('referer') . ' LIKE :referer)' - ) - ->bind(':oldurl', $search) - ->bind(':newurl', $search) - ->bind(':comment', $search) - ->bind(':referer', $search); - } - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.old_url')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Add the entered URLs into the database - * - * @param array $batchUrls Array of URLs to enter into the database - * - * @return boolean - */ - public function batchProcess($batchUrls) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $params = ComponentHelper::getParams('com_redirect'); - $state = (int) $params->get('defaultImportState', 0); - $created = Factory::getDate()->toSql(); - - $columns = [ - 'old_url', - 'new_url', - 'referer', - 'comment', - 'hits', - 'published', - 'created_date', - 'modified_date', - ]; - - $values = [ - ':oldurl', - ':newurl', - $db->quote(''), - $db->quote(''), - 0, - ':state', - ':created', - ':modified', - ]; - - $query - ->insert($db->quoteName('#__redirect_links'), false) - ->columns($db->quoteName($columns)) - ->values(implode(', ', $values)) - ->bind(':oldurl', $old_url) - ->bind(':newurl', $new_url) - ->bind(':state', $state, ParameterType::INTEGER) - ->bind(':created', $created) - ->bind(':modified', $created); - - $db->setQuery($query); - - foreach ($batchUrls as $batch_url) - { - $old_url = $batch_url[0]; - - // Destination URL can also be an external URL - if (!empty($batch_url[1])) - { - $new_url = $batch_url[1]; - } - else - { - $new_url = ''; - } - - $db->execute(); - } - - return true; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'state', 'a.state', + 'old_url', 'a.old_url', + 'new_url', 'a.new_url', + 'referer', 'a.referer', + 'hits', 'a.hits', + 'created_date', 'a.created_date', + 'published', 'a.published', + 'header', 'a.header', 'http_status', + ); + } + + parent::__construct($config, $factory); + } + /** + * Removes all of the unpublished redirects from the table. + * + * @return boolean result of operation + * + * @since 3.5 + */ + public function purge() + { + $db = $this->getDatabase(); + + $query = $db->getQuery(true); + + $query->delete('#__redirect_links')->where($db->quoteName('published') . '= 0'); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\Exception $e) { + return false; + } + + return true; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.old_url', $direction = 'asc') + { + // Load the parameters. + $params = ComponentHelper::getParams('com_redirect'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.http_status'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.*' + ) + ); + $query->from($db->quoteName('#__redirect_links', 'a')); + + // Filter by published state + $state = (string) $this->getState('filter.state'); + + if (is_numeric($state)) { + $state = (int) $state; + $query->where($db->quoteName('a.published') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } elseif ($state === '') { + $query->whereIn($db->quoteName('a.published'), [0,1]); + } + + // Filter the items over the HTTP status code header. + if ($httpStatusCode = $this->getState('filter.http_status')) { + $httpStatusCode = (int) $httpStatusCode; + $query->where($db->quoteName('a.header') . ' = :header') + ->bind(':header', $httpStatusCode, ParameterType::INTEGER); + } + + // Filter the items over the search string if set. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', $db->escape(trim($search), true) . '%'); + $query->where( + '(' . $db->quoteName('old_url') . ' LIKE :oldurl' + . ' OR ' . $db->quoteName('new_url') . ' LIKE :newurl' + . ' OR ' . $db->quoteName('comment') . ' LIKE :comment' + . ' OR ' . $db->quoteName('referer') . ' LIKE :referer)' + ) + ->bind(':oldurl', $search) + ->bind(':newurl', $search) + ->bind(':comment', $search) + ->bind(':referer', $search); + } + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.old_url')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Add the entered URLs into the database + * + * @param array $batchUrls Array of URLs to enter into the database + * + * @return boolean + */ + public function batchProcess($batchUrls) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $params = ComponentHelper::getParams('com_redirect'); + $state = (int) $params->get('defaultImportState', 0); + $created = Factory::getDate()->toSql(); + + $columns = [ + 'old_url', + 'new_url', + 'referer', + 'comment', + 'hits', + 'published', + 'created_date', + 'modified_date', + ]; + + $values = [ + ':oldurl', + ':newurl', + $db->quote(''), + $db->quote(''), + 0, + ':state', + ':created', + ':modified', + ]; + + $query + ->insert($db->quoteName('#__redirect_links'), false) + ->columns($db->quoteName($columns)) + ->values(implode(', ', $values)) + ->bind(':oldurl', $old_url) + ->bind(':newurl', $new_url) + ->bind(':state', $state, ParameterType::INTEGER) + ->bind(':created', $created) + ->bind(':modified', $created); + + $db->setQuery($query); + + foreach ($batchUrls as $batch_url) { + $old_url = $batch_url[0]; + + // Destination URL can also be an external URL + if (!empty($batch_url[1])) { + $new_url = $batch_url[1]; + } else { + $new_url = ''; + } + + $db->execute(); + } + + return true; + } } diff --git a/administrator/components/com_redirect/src/Service/HTML/Redirect.php b/administrator/components/com_redirect/src/Service/HTML/Redirect.php index c7dfcb0da4690..acb733cca42ca 100644 --- a/administrator/components/com_redirect/src/Service/HTML/Redirect.php +++ b/administrator/components/com_redirect/src/Service/HTML/Redirect.php @@ -1,4 +1,5 @@ array('publish', 'links.unpublish', 'JENABLED', 'COM_REDIRECT_DISABLE_LINK'), - 0 => array('unpublish', 'links.publish', 'JDISABLED', 'COM_REDIRECT_ENABLE_LINK'), - 2 => array('archive', 'links.unpublish', 'JARCHIVED', 'JUNARCHIVE'), - -2 => array('trash', 'links.publish', 'JTRASHED', 'COM_REDIRECT_ENABLE_LINK'), - ); - - $state = ArrayHelper::getValue($states, (int) $value, $states[0]); - $icon = $state[0]; - - if ($canChange) - { - $html = '' - . ''; - } - - return $html; - } + /** + * Display the published or unpublished state of an item. + * + * @param int $value The state value. + * @param int $i The ID of the item. + * @param boolean $canChange An optional prefix for the task. + * + * @return string + * + * @since 1.6 + * + * @throws \InvalidArgumentException + */ + public function published($value = 0, $i = null, $canChange = true) + { + // Note: $i is required but has to be an optional argument in the function call due to argument order + if (null === $i) { + throw new \InvalidArgumentException('$i is a required argument in JHtmlRedirect::published'); + } + + // Array of image, task, title, action + $states = array( + 1 => array('publish', 'links.unpublish', 'JENABLED', 'COM_REDIRECT_DISABLE_LINK'), + 0 => array('unpublish', 'links.publish', 'JDISABLED', 'COM_REDIRECT_ENABLE_LINK'), + 2 => array('archive', 'links.unpublish', 'JARCHIVED', 'JUNARCHIVE'), + -2 => array('trash', 'links.publish', 'JTRASHED', 'COM_REDIRECT_ENABLE_LINK'), + ); + + $state = ArrayHelper::getValue($states, (int) $value, $states[0]); + $icon = $state[0]; + + if ($canChange) { + $html = '' + . ''; + } + + return $html; + } } diff --git a/administrator/components/com_redirect/src/Table/LinkTable.php b/administrator/components/com_redirect/src/Table/LinkTable.php index fdc4ced7c4015..78bea988abf01 100644 --- a/administrator/components/com_redirect/src/Table/LinkTable.php +++ b/administrator/components/com_redirect/src/Table/LinkTable.php @@ -1,4 +1,5 @@ setError($e->getMessage()); - - return false; - } - - $this->old_url = trim(rawurldecode($this->old_url)); - $this->new_url = trim(rawurldecode($this->new_url)); - - // Check for valid name. - if (empty($this->old_url)) - { - $this->setError(Text::_('COM_REDIRECT_ERROR_SOURCE_URL_REQUIRED')); - - return false; - } - - // Check for NOT NULL. - if (empty($this->referer)) - { - $this->referer = ''; - } - - // Check for valid name if not in advanced mode. - if (empty($this->new_url) && ComponentHelper::getParams('com_redirect')->get('mode', 0) == false) - { - $this->setError(Text::_('COM_REDIRECT_ERROR_DESTINATION_URL_REQUIRED')); - - return false; - } - elseif (empty($this->new_url) && ComponentHelper::getParams('com_redirect')->get('mode', 0) == true) - { - // Else if an empty URL and in redirect mode only throw the same error if the code is a 3xx status code - if ($this->header < 400 && $this->header >= 300) - { - $this->setError(Text::_('COM_REDIRECT_ERROR_DESTINATION_URL_REQUIRED')); - - return false; - } - } - - // Check for duplicates - if ($this->old_url == $this->new_url) - { - $this->setError(Text::_('COM_REDIRECT_ERROR_DUPLICATE_URLS')); - - return false; - } - - $db = $this->getDbo(); - - // Check for existing name - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->select($db->quoteName('old_url')) - ->from($db->quoteName('#__redirect_links')) - ->where($db->quoteName('old_url') . ' = :url') - ->bind(':url', $this->old_url); - $db->setQuery($query); - $urls = $db->loadAssocList(); - - foreach ($urls as $url) - { - if ($url['old_url'] === $this->old_url && (int) $url['id'] != (int) $this->id) - { - $this->setError(Text::_('COM_REDIRECT_ERROR_DUPLICATE_OLD_URL')); - - return false; - } - } - - return true; - } - - /** - * Overridden store method to set dates. - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function store($updateNulls = false) - { - $date = Factory::getDate()->toSql(); - - if (!$this->id) - { - // New record. - $this->created_date = $date; - $this->modified_date = $date; - } - - if (empty($this->modified_date)) - { - $this->modified_date = $this->created_date; - } - - return parent::store($updateNulls); - } + /** + * Constructor + * + * @param DatabaseDriver $db Database object. + * + * @since 1.6 + */ + public function __construct(DatabaseDriver $db) + { + parent::__construct('#__redirect_links', 'id', $db); + } + + /** + * Overloaded check function + * + * @return boolean + * + * @since 1.6 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + $this->old_url = trim(rawurldecode($this->old_url)); + $this->new_url = trim(rawurldecode($this->new_url)); + + // Check for valid name. + if (empty($this->old_url)) { + $this->setError(Text::_('COM_REDIRECT_ERROR_SOURCE_URL_REQUIRED')); + + return false; + } + + // Check for NOT NULL. + if (empty($this->referer)) { + $this->referer = ''; + } + + // Check for valid name if not in advanced mode. + if (empty($this->new_url) && ComponentHelper::getParams('com_redirect')->get('mode', 0) == false) { + $this->setError(Text::_('COM_REDIRECT_ERROR_DESTINATION_URL_REQUIRED')); + + return false; + } elseif (empty($this->new_url) && ComponentHelper::getParams('com_redirect')->get('mode', 0) == true) { + // Else if an empty URL and in redirect mode only throw the same error if the code is a 3xx status code + if ($this->header < 400 && $this->header >= 300) { + $this->setError(Text::_('COM_REDIRECT_ERROR_DESTINATION_URL_REQUIRED')); + + return false; + } + } + + // Check for duplicates + if ($this->old_url == $this->new_url) { + $this->setError(Text::_('COM_REDIRECT_ERROR_DUPLICATE_URLS')); + + return false; + } + + $db = $this->getDbo(); + + // Check for existing name + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->select($db->quoteName('old_url')) + ->from($db->quoteName('#__redirect_links')) + ->where($db->quoteName('old_url') . ' = :url') + ->bind(':url', $this->old_url); + $db->setQuery($query); + $urls = $db->loadAssocList(); + + foreach ($urls as $url) { + if ($url['old_url'] === $this->old_url && (int) $url['id'] != (int) $this->id) { + $this->setError(Text::_('COM_REDIRECT_ERROR_DUPLICATE_OLD_URL')); + + return false; + } + } + + return true; + } + + /** + * Overridden store method to set dates. + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function store($updateNulls = false) + { + $date = Factory::getDate()->toSql(); + + if (!$this->id) { + // New record. + $this->created_date = $date; + $this->modified_date = $date; + } + + if (empty($this->modified_date)) { + $this->modified_date = $this->created_date; + } + + return parent::store($updateNulls); + } } diff --git a/administrator/components/com_redirect/src/View/Link/HtmlView.php b/administrator/components/com_redirect/src/View/Link/HtmlView.php index c20f83e059c50..927c5daeecd60 100644 --- a/administrator/components/com_redirect/src/View/Link/HtmlView.php +++ b/administrator/components/com_redirect/src/View/Link/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $isNew = ($this->item->id == 0); - $canDo = ContentHelper::getActions('com_redirect'); - - ToolbarHelper::title($isNew ? Text::_('COM_REDIRECT_MANAGER_LINK_NEW') : Text::_('COM_REDIRECT_MANAGER_LINK_EDIT'), 'map-signs redirect'); - - $toolbarButtons = []; - - // If not checked out, can save the item. - if ($canDo->get('core.edit')) - { - ToolbarHelper::apply('link.apply'); - $toolbarButtons[] = ['save', 'link.save']; - } - - /** - * This component does not support Save as Copy due to uniqueness checks. - * While it can be done, it causes too much confusion if the user does - * not change the Old URL. - */ - if ($canDo->get('core.edit') && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'link.save2new']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('link.cancel'); - } - else - { - ToolbarHelper::cancel('link.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::help('Redirects:_New_or_Edit'); - } + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed False if unsuccessful, otherwise void. + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $isNew = ($this->item->id == 0); + $canDo = ContentHelper::getActions('com_redirect'); + + ToolbarHelper::title($isNew ? Text::_('COM_REDIRECT_MANAGER_LINK_NEW') : Text::_('COM_REDIRECT_MANAGER_LINK_EDIT'), 'map-signs redirect'); + + $toolbarButtons = []; + + // If not checked out, can save the item. + if ($canDo->get('core.edit')) { + ToolbarHelper::apply('link.apply'); + $toolbarButtons[] = ['save', 'link.save']; + } + + /** + * This component does not support Save as Copy due to uniqueness checks. + * While it can be done, it causes too much confusion if the user does + * not change the Old URL. + */ + if ($canDo->get('core.edit') && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'link.save2new']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('link.cancel'); + } else { + ToolbarHelper::cancel('link.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::help('Redirects:_New_or_Edit'); + } } diff --git a/administrator/components/com_redirect/src/View/Links/HtmlView.php b/administrator/components/com_redirect/src/View/Links/HtmlView.php index f04a0b8c8f45d..15db461f3d682 100644 --- a/administrator/components/com_redirect/src/View/Links/HtmlView.php +++ b/administrator/components/com_redirect/src/View/Links/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->params = ComponentHelper::getParams('com_redirect'); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - if (!(PluginHelper::isEnabled('system', 'redirect') && RedirectHelper::collectUrlsEnabled())) - { - $this->redirectPluginId = RedirectHelper::getRedirectPluginId(); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $state = $this->get('State'); - $canDo = ContentHelper::getActions('com_redirect'); - - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_REDIRECT_MANAGER_LINKS'), 'map-signs redirect'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('link.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $canDo->get('core.admin'))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($state->get('filter.state') != 2) - { - $childBar->publish('links.publish', 'JTOOLBAR_ENABLE')->listCheck(true); - $childBar->unpublish('links.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); - } - - if ($state->get('filter.state') != -1) - { - if ($state->get('filter.state') != 2) - { - $childBar->archive('links.archive')->listCheck(true); - } - elseif ($state->get('filter.state') == 2) - { - $childBar->unarchive('links.unarchive')->listCheck(true); - } - } - - if (!$state->get('filter.state') == -2) - { - $childBar->trash('links.trash')->listCheck(true); - } - } - - if ($state->get('filter.state') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('links.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if (!$this->isEmptyState && (!$state->get('filter.state') == -2 && $canDo->get('core.delete'))) - { - $toolbar->confirmButton('delete') - ->text('COM_REDIRECT_TOOLBAR_PURGE') - ->message('COM_REDIRECT_CONFIRM_PURGE') - ->task('links.purge'); - } - - if ($canDo->get('core.create')) - { - $toolbar->popupButton('batch') - ->text('JTOOLBAR_BULK_IMPORT') - ->selector('collapseModal') - ->listCheck(false); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences('com_redirect'); - } - - $toolbar->help('Redirects:_Links'); - } + /** + * True if "System - Redirect Plugin" is enabled + * + * @var boolean + */ + protected $enabled; + + /** + * True if "Collect URLs" is enabled + * + * @var boolean + */ + protected $collect_urls_enabled; + + /** + * The id of the redirect plugin in mysql + * + * @var integer + * @since 3.8.0 + */ + protected $redirectPluginId = 0; + + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * The model state + * + * @var \Joomla\Registry\Registry + */ + protected $params; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @throws GenericDataException + * @since 1.6 + */ + public function display($tpl = null) + { + // Set variables + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->params = ComponentHelper::getParams('com_redirect'); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + if (!(PluginHelper::isEnabled('system', 'redirect') && RedirectHelper::collectUrlsEnabled())) { + $this->redirectPluginId = RedirectHelper::getRedirectPluginId(); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $state = $this->get('State'); + $canDo = ContentHelper::getActions('com_redirect'); + + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_REDIRECT_MANAGER_LINKS'), 'map-signs redirect'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('link.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $canDo->get('core.admin'))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($state->get('filter.state') != 2) { + $childBar->publish('links.publish', 'JTOOLBAR_ENABLE')->listCheck(true); + $childBar->unpublish('links.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); + } + + if ($state->get('filter.state') != -1) { + if ($state->get('filter.state') != 2) { + $childBar->archive('links.archive')->listCheck(true); + } elseif ($state->get('filter.state') == 2) { + $childBar->unarchive('links.unarchive')->listCheck(true); + } + } + + if (!$state->get('filter.state') == -2) { + $childBar->trash('links.trash')->listCheck(true); + } + } + + if ($state->get('filter.state') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('links.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if (!$this->isEmptyState && (!$state->get('filter.state') == -2 && $canDo->get('core.delete'))) { + $toolbar->confirmButton('delete') + ->text('COM_REDIRECT_TOOLBAR_PURGE') + ->message('COM_REDIRECT_CONFIRM_PURGE') + ->task('links.purge'); + } + + if ($canDo->get('core.create')) { + $toolbar->popupButton('batch') + ->text('JTOOLBAR_BULK_IMPORT') + ->selector('collapseModal') + ->listCheck(false); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences('com_redirect'); + } + + $toolbar->help('Redirects:_Links'); + } } diff --git a/administrator/components/com_redirect/tmpl/link/edit.php b/administrator/components/com_redirect/tmpl/link/edit.php index 9f3320aabdcb1..9117e79dbd40e 100644 --- a/administrator/components/com_redirect/tmpl/link/edit.php +++ b/administrator/components/com_redirect/tmpl/link/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?> diff --git a/administrator/components/com_redirect/tmpl/links/default.php b/administrator/components/com_redirect/tmpl/links/default.php index 8be568281c1e4..c681dae183adb 100644 --- a/administrator/components/com_redirect/tmpl/links/default.php +++ b/administrator/components/com_redirect/tmpl/links/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); ?>
-
- $this)); ?> - redirectPluginId) : ?> - redirectPluginId . '&tmpl=component&layout=modal'); ?> - redirectPluginId . 'Modal', - array( - 'url' => $link, - 'title' => Text::_('COM_REDIRECT_EDIT_PLUGIN_SETTINGS'), - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => '70', - 'modalWidth' => '80', - 'closeButton' => false, - 'backdrop' => 'static', - 'keyboard' => false, - 'footer' => '' - . '' - . '' - ) - ); ?> - +
+ $this)); ?> + redirectPluginId) : ?> + redirectPluginId . '&tmpl=component&layout=modal'); ?> + redirectPluginId . 'Modal', + array( + 'url' => $link, + 'title' => Text::_('COM_REDIRECT_EDIT_PLUGIN_SETTINGS'), + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => '70', + 'modalWidth' => '80', + 'closeButton' => false, + 'backdrop' => 'static', + 'keyboard' => false, + 'footer' => '' + . '' + . '' + ) + ); ?> + - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - - - items as $i => $item) : - $canEdit = $user->authorise('core.edit', 'com_redirect'); - $canChange = $user->authorise('core.edit.state', 'com_redirect'); - ?> - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - - - -
- id, false, 'cid', 'cb', $item->old_url); ?> - - published, $i); ?> - - - - escape(str_replace(Uri::root(), '', rawurldecode($item->old_url))); ?> - - - escape(str_replace(Uri::root(), '', rawurldecode($item->old_url))); ?> - - - escape(rawurldecode($item->new_url)); ?> - - escape($item->referer); ?> - - created_date, Text::_('DATE_FORMAT_LC4')); ?> - - hits; ?> - - header; ?> - - id; ?> -
+ items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + items as $i => $item) : + $canEdit = $user->authorise('core.edit', 'com_redirect'); + $canChange = $user->authorise('core.edit.state', 'com_redirect'); + ?> + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->old_url); ?> + + published, $i); ?> + + + + escape(str_replace(Uri::root(), '', rawurldecode($item->old_url))); ?> + + + escape(str_replace(Uri::root(), '', rawurldecode($item->old_url))); ?> + + + escape(rawurldecode($item->new_url)); ?> + + escape($item->referer); ?> + + created_date, Text::_('DATE_FORMAT_LC4')); ?> + + hits; ?> + + header; ?> + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - items)) : ?> - loadTemplate('addform'); ?> - - - authorise('core.create', 'com_redirect') - && $user->authorise('core.edit', 'com_redirect') - && $user->authorise('core.edit.state', 'com_redirect')) : ?> - Text::_('COM_REDIRECT_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ), - $this->loadTemplate('batch_body') - ); ?> - + items)) : ?> + loadTemplate('addform'); ?> + + + authorise('core.create', 'com_redirect') + && $user->authorise('core.edit', 'com_redirect') + && $user->authorise('core.edit.state', 'com_redirect') +) : ?> + Text::_('COM_REDIRECT_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ), + $this->loadTemplate('batch_body') + ); ?> + - - - -
+ + + +
diff --git a/administrator/components/com_redirect/tmpl/links/default_addform.php b/administrator/components/com_redirect/tmpl/links/default_addform.php index 186b46583bd8d..6510ef5016108 100644 --- a/administrator/components/com_redirect/tmpl/links/default_addform.php +++ b/administrator/components/com_redirect/tmpl/links/default_addform.php @@ -1,4 +1,5 @@
-
- -
-
-
-
-
-
- -
-
- - - - -
-
-
-
- -
-
- -
-
- -
-
-
+
+ +
+
+
+
+
+
+ +
+
+ + + + +
+
+
+
+ +
+
+ +
+
+ +
+
+
diff --git a/administrator/components/com_redirect/tmpl/links/default_batch_body.php b/administrator/components/com_redirect/tmpl/links/default_batch_body.php index 58ff121d215a0..052eef41afe42 100644 --- a/administrator/components/com_redirect/tmpl/links/default_batch_body.php +++ b/administrator/components/com_redirect/tmpl/links/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; @@ -16,12 +18,12 @@ ?>
-
-
- -
- -
-
-
+
+
+ +
+ +
+
+
diff --git a/administrator/components/com_redirect/tmpl/links/default_batch_footer.php b/administrator/components/com_redirect/tmpl/links/default_batch_footer.php index 08b76e82053da..acd5826500778 100644 --- a/administrator/components/com_redirect/tmpl/links/default_batch_footer.php +++ b/administrator/components/com_redirect/tmpl/links/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/administrator/components/com_redirect/tmpl/links/emptystate.php b/administrator/components/com_redirect/tmpl/links/emptystate.php index 705387dd82823..752d956a46dc2 100644 --- a/administrator/components/com_redirect/tmpl/links/emptystate.php +++ b/administrator/components/com_redirect/tmpl/links/emptystate.php @@ -1,4 +1,5 @@ 'COM_REDIRECT', - 'formURL' => 'index.php?option=com_redirect&view=links', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Redirects:_Links', - 'icon' => 'icon-map-signs redirect', + 'textPrefix' => 'COM_REDIRECT', + 'formURL' => 'index.php?option=com_redirect&view=links', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help4.x:Redirects:_Links', + 'icon' => 'icon-map-signs redirect', ]; $user = Factory::getApplication()->getIdentity(); -if ($user->authorise('core.create', 'com_redirect')) -{ - $displayData['createURL'] = 'index.php?option=com_redirect&task=link.add'; +if ($user->authorise('core.create', 'com_redirect')) { + $displayData['createURL'] = 'index.php?option=com_redirect&task=link.add'; } -if ($user->authorise('core.create', 'com_redirect') - && $user->authorise('core.edit', 'com_redirect') - && $user->authorise('core.edit.state', 'com_redirect')) -{ - $displayData['formAppend'] = HTMLHelper::_( - 'bootstrap.renderModal', - 'collapseModal', - [ - 'title' => Text::_('COM_REDIRECT_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ], - $this->loadTemplate('batch_body') - ); +if ( + $user->authorise('core.create', 'com_redirect') + && $user->authorise('core.edit', 'com_redirect') + && $user->authorise('core.edit.state', 'com_redirect') +) { + $displayData['formAppend'] = HTMLHelper::_( + 'bootstrap.renderModal', + 'collapseModal', + [ + 'title' => Text::_('COM_REDIRECT_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ], + $this->loadTemplate('batch_body') + ); } ?> redirectPluginId) : ?> - redirectPluginId . '&tmpl=component&layout=modal'); ?> - redirectPluginId . 'Modal', - array( - 'url' => $link, - 'title' => Text::_('COM_REDIRECT_EDIT_PLUGIN_SETTINGS'), - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => '70', - 'modalWidth' => '80', - 'closeButton' => false, - 'backdrop' => 'static', - 'keyboard' => false, - 'footer' => '' - . '' - . '' - ) - ); ?> + redirectPluginId . '&tmpl=component&layout=modal'); ?> + redirectPluginId . 'Modal', + array( + 'url' => $link, + 'title' => Text::_('COM_REDIRECT_EDIT_PLUGIN_SETTINGS'), + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => '70', + 'modalWidth' => '80', + 'closeButton' => false, + 'backdrop' => 'static', + 'keyboard' => false, + 'footer' => '' + . '' + . '' + ) + ); ?>
- - + +
diff --git a/administrator/components/com_scheduler/services/provider.php b/administrator/components/com_scheduler/services/provider.php index 4511f0be44ed0..ded0221067825 100644 --- a/administrator/components/com_scheduler/services/provider.php +++ b/administrator/components/com_scheduler/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Scheduler')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Scheduler')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.1.0 + */ + public function register(Container $container) + { + /** + * Register the MVCFactory and ComponentDispatcherFactory providers to map + * 'MVCFactoryInterface' and 'ComponentDispatcherFactoryInterface' to their + * initializers and register them with the component's DI container. + */ + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Scheduler')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Scheduler')); - $container->set( - ComponentInterface::class, - function (Container $container) { - $component = new SchedulerComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new SchedulerComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_scheduler/src/Controller/DisplayController.php b/administrator/components/com_scheduler/src/Controller/DisplayController.php index eb79b7f7837a9..4cc6a36e13074 100644 --- a/administrator/components/com_scheduler/src/Controller/DisplayController.php +++ b/administrator/components/com_scheduler/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('layout', 'default'); + /** + * @var string + * @since 4.1.0 + */ + protected $default_view = 'tasks'; - // Check for edit form. - if ($layout === 'edit') - { - if (!$this->validateEntry()) - { - $tasksViewUrl = Route::_('index.php?option=com_scheduler&view=tasks', false); - $this->setRedirect($tasksViewUrl); + /** + * @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 InputFilter::clean()}. + * + * @return BaseController|boolean Returns either a BaseController object to support chaining, or false on failure + * + * @since 4.1.0 + * @throws \Exception + */ + public function display($cachable = false, $urlparams = array()) + { + $layout = $this->input->get('layout', 'default'); - return false; - } - } + // Check for edit form. + if ($layout === 'edit') { + if (!$this->validateEntry()) { + $tasksViewUrl = Route::_('index.php?option=com_scheduler&view=tasks', false); + $this->setRedirect($tasksViewUrl); - // Let the parent method take over - return parent::display($cachable, $urlparams); - } + return false; + } + } - /** - * Validates entry to the view - * - * @param string $layout The layout to validate entry for (defaults to 'edit') - * - * @return boolean True is entry is valid - * - * @since 4.1.0 - */ - private function validateEntry(string $layout = 'edit'): bool - { - $context = 'com_scheduler'; - $id = $this->input->getInt('id'); - $isValid = true; + // Let the parent method take over + return parent::display($cachable, $urlparams); + } - switch ($layout) - { - case 'edit': + /** + * Validates entry to the view + * + * @param string $layout The layout to validate entry for (defaults to 'edit') + * + * @return boolean True is entry is valid + * + * @since 4.1.0 + */ + private function validateEntry(string $layout = 'edit'): bool + { + $context = 'com_scheduler'; + $id = $this->input->getInt('id'); + $isValid = true; - // True if controller was called and verified permissions - $inEditList = $this->checkEditId("$context.edit.task", $id); - $isNew = ($id == 0); + switch ($layout) { + case 'edit': + // True if controller was called and verified permissions + $inEditList = $this->checkEditId("$context.edit.task", $id); + $isNew = ($id == 0); - // For new item, entry is invalid if task type was not selected through SelectView - if ($isNew && !$this->app->getUserState("$context.add.task.task_type")) - { - $this->setMessage((Text::_('COM_SCHEDULER_ERROR_FORBIDDEN_JUMP_TO_ADD_VIEW')), 'error'); - $isValid = false; - } - // For existing item, entry is invalid if TaskController has not granted access - elseif (!$inEditList) - { - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } + // For new item, entry is invalid if task type was not selected through SelectView + if ($isNew && !$this->app->getUserState("$context.add.task.task_type")) { + $this->setMessage((Text::_('COM_SCHEDULER_ERROR_FORBIDDEN_JUMP_TO_ADD_VIEW')), 'error'); + $isValid = false; + } elseif (!$inEditList) { + // For existing item, entry is invalid if TaskController has not granted access + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } - $isValid = false; - } - break; - default: - break; - } + $isValid = false; + } + break; + default: + break; + } - return $isValid; - } + return $isValid; + } } diff --git a/administrator/components/com_scheduler/src/Controller/TaskController.php b/administrator/components/com_scheduler/src/Controller/TaskController.php index 50e3409abaf76..13230493bf1af 100644 --- a/administrator/components/com_scheduler/src/Controller/TaskController.php +++ b/administrator/components/com_scheduler/src/Controller/TaskController.php @@ -1,4 +1,5 @@ app; - $input = $app->getInput(); - $validTaskOptions = SchedulerHelper::getTaskOptions(); - - $canAdd = parent::add(); - - if ($canAdd !== true) - { - return false; - } - - $taskType = $input->get('type'); - $taskOption = $validTaskOptions->findOption($taskType) ?: null; - - if (!$taskOption) - { - // ? : Is this the right redirect [review] - $redirectUrl = 'index.php?option=' . $this->option . '&view=select&layout=edit'; - $this->setRedirect(Route::_($redirectUrl, false)); - $app->enqueueMessage(Text::_('COM_SCHEDULER_ERROR_INVALID_TASK_TYPE'), 'warning'); - $canAdd = false; - } - - $app->setUserState('com_scheduler.add.task.task_type', $taskType); - $app->setUserState('com_scheduler.add.task.task_option', $taskOption); - - // @todo : Parameter array handling below? - - return $canAdd; - } - - /** - * Override parent cancel method to reset the add task state - * - * @param ?string $key Primary key from the URL param - * - * @return boolean True if access level checks pass - * - * @since 4.1.0 - */ - public function cancel($key = null): bool - { - $result = parent::cancel($key); - - $this->app->setUserState('com_scheduler.add.task.task_type', null); - $this->app->setUserState('com_scheduler.add.task.task_option', null); - - // ? Do we need to redirect based on URL's 'return' param? {@see ModuleController} - - return $result; - } - - /** - * Check if user has the authority to edit an asset - * - * @param array $data Array of input data - * @param string $key Name of key for primary key, defaults to 'id' - * - * @return boolean True if user is allowed to edit record - * - * @since 4.1.0 - */ - protected function allowEdit($data = array(), $key = 'id'): bool - { - // Extract the recordId from $data, will come in handy - $recordId = (int) $data[$key] ?? 0; - - /** - * Zero record (id:0), return component edit permission by calling parent controller method - * ?: Is this the right way to do this? - */ - if ($recordId === 0) - { - return parent::allowEdit($data, $key); - } - - // @todo : Check if this works as expected - return $this->app->getIdentity()->authorise('core.edit', 'com_scheduler.task.' . $recordId); - - } + /** + * Add a new record + * + * @return boolean + * @since 4.1.0 + * @throws \Exception + */ + public function add(): bool + { + /** @var AdministratorApplication $app */ + $app = $this->app; + $input = $app->getInput(); + $validTaskOptions = SchedulerHelper::getTaskOptions(); + + $canAdd = parent::add(); + + if ($canAdd !== true) { + return false; + } + + $taskType = $input->get('type'); + $taskOption = $validTaskOptions->findOption($taskType) ?: null; + + if (!$taskOption) { + // ? : Is this the right redirect [review] + $redirectUrl = 'index.php?option=' . $this->option . '&view=select&layout=edit'; + $this->setRedirect(Route::_($redirectUrl, false)); + $app->enqueueMessage(Text::_('COM_SCHEDULER_ERROR_INVALID_TASK_TYPE'), 'warning'); + $canAdd = false; + } + + $app->setUserState('com_scheduler.add.task.task_type', $taskType); + $app->setUserState('com_scheduler.add.task.task_option', $taskOption); + + // @todo : Parameter array handling below? + + return $canAdd; + } + + /** + * Override parent cancel method to reset the add task state + * + * @param ?string $key Primary key from the URL param + * + * @return boolean True if access level checks pass + * + * @since 4.1.0 + */ + public function cancel($key = null): bool + { + $result = parent::cancel($key); + + $this->app->setUserState('com_scheduler.add.task.task_type', null); + $this->app->setUserState('com_scheduler.add.task.task_option', null); + + // ? Do we need to redirect based on URL's 'return' param? {@see ModuleController} + + return $result; + } + + /** + * Check if user has the authority to edit an asset + * + * @param array $data Array of input data + * @param string $key Name of key for primary key, defaults to 'id' + * + * @return boolean True if user is allowed to edit record + * + * @since 4.1.0 + */ + protected function allowEdit($data = array(), $key = 'id'): bool + { + // Extract the recordId from $data, will come in handy + $recordId = (int) $data[$key] ?? 0; + + /** + * Zero record (id:0), return component edit permission by calling parent controller method + * ?: Is this the right way to do this? + */ + if ($recordId === 0) { + return parent::allowEdit($data, $key); + } + + // @todo : Check if this works as expected + return $this->app->getIdentity()->authorise('core.edit', 'com_scheduler.task.' . $recordId); + } } diff --git a/administrator/components/com_scheduler/src/Controller/TasksController.php b/administrator/components/com_scheduler/src/Controller/TasksController.php index 635e169d520ee..3bad911a4e9da 100644 --- a/administrator/components/com_scheduler/src/Controller/TasksController.php +++ b/administrator/components/com_scheduler/src/Controller/TasksController.php @@ -1,4 +1,5 @@ true]): BaseDatabaseModel - { - return parent::getModel($name, $prefix, $config); - } + /** + * Proxy for the parent method. + * + * @param string $name The name of the model. + * @param string $prefix The prefix for the PHP class name. + * @param array $config Array of configuration parameters. + * + * @return BaseDatabaseModel + * + * @since 4.1.0 + */ + public function getModel($name = 'Task', $prefix = 'Administrator', $config = ['ignore_request' => true]): BaseDatabaseModel + { + return parent::getModel($name, $prefix, $config); + } - /** - * Unlock a locked task, i.e., a task that is presumably still running but might have crashed and got stuck in the - * "locked" state. - * - * @return void - * - * @since 4.1.0 - */ - public function unlock(): void - { - // Check for request forgeries - $this->checkToken(); + /** + * Unlock a locked task, i.e., a task that is presumably still running but might have crashed and got stuck in the + * "locked" state. + * + * @return void + * + * @since 4.1.0 + */ + public function unlock(): void + { + // Check for request forgeries + $this->checkToken(); - /** @var integer[] $cid Items to publish (from request parameters). */ - $cid = (array) $this->input->get('cid', [], 'int'); + /** @var integer[] $cid Items to publish (from request parameters). */ + $cid = (array) $this->input->get('cid', [], 'int'); - // Remove zero values resulting from input filter - $cid = array_filter($cid); + // Remove zero values resulting from input filter + $cid = array_filter($cid); - if (empty($cid)) - { - $this->app->getLogger() - ->warning(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), array('category' => 'jerror')); - } - else - { - /** @var TaskModel $model */ - $model = $this->getModel(); + if (empty($cid)) { + $this->app->getLogger() + ->warning(Text::_($this->text_prefix . '_NO_ITEM_SELECTED'), array('category' => 'jerror')); + } else { + /** @var TaskModel $model */ + $model = $this->getModel(); - // Make sure the item IDs are integers - $cid = ArrayHelper::toInteger($cid); + // Make sure the item IDs are integers + $cid = ArrayHelper::toInteger($cid); - // Unlock the items. - try - { - $model->unlock($cid); - $errors = $model->getErrors(); - $noticeText = null; + // Unlock the items. + try { + $model->unlock($cid); + $errors = $model->getErrors(); + $noticeText = null; - if ($errors) - { - Factory::getApplication() - ->enqueueMessage(Text::plural($this->text_prefix . '_N_ITEMS_FAILED_UNLOCKING', \count($cid)), 'error'); - } - else - { - $noticeText = $this->text_prefix . '_N_ITEMS_UNLOCKED'; - } + if ($errors) { + Factory::getApplication() + ->enqueueMessage(Text::plural($this->text_prefix . '_N_ITEMS_FAILED_UNLOCKING', \count($cid)), 'error'); + } else { + $noticeText = $this->text_prefix . '_N_ITEMS_UNLOCKED'; + } - if (\count($cid)) - { - $this->setMessage(Text::plural($noticeText, \count($cid))); - } - } - catch (\Exception $e) - { - $this->setMessage($e->getMessage(), 'error'); - } - } + if (\count($cid)) { + $this->setMessage(Text::plural($noticeText, \count($cid))); + } + } catch (\Exception $e) { + $this->setMessage($e->getMessage(), 'error'); + } + } - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(), - false - ) - ); - } + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . $this->getRedirectToListAppend(), + false + ) + ); + } } diff --git a/administrator/components/com_scheduler/src/Event/ExecuteTaskEvent.php b/administrator/components/com_scheduler/src/Event/ExecuteTaskEvent.php index 31fb65508cd09..7d6d3d7c70035 100644 --- a/administrator/components/com_scheduler/src/Event/ExecuteTaskEvent.php +++ b/administrator/components/com_scheduler/src/Event/ExecuteTaskEvent.php @@ -1,4 +1,5 @@ arguments['resultSnapshot'] = $snapshot; + /** + * Sets the task result snapshot and stops event propagation. + * + * @param array $snapshot The task snapshot. + * + * @return void + * + * @since 4.1.0 + */ + public function setResult(array $snapshot = []): void + { + $this->arguments['resultSnapshot'] = $snapshot; - if (!empty($snapshot)) - { - $this->stopPropagation(); - } - } + if (!empty($snapshot)) { + $this->stopPropagation(); + } + } - /** - * @return integer The task's taskId. - * - * @since 4.1.0 - */ - public function getTaskId(): int - { - return $this->arguments['subject']->get('id'); - } + /** + * @return integer The task's taskId. + * + * @since 4.1.0 + */ + public function getTaskId(): int + { + return $this->arguments['subject']->get('id'); + } - /** - * @return string The task's 'type'. - * - * @since 4.1.0 - */ - public function getRoutineId(): string - { - return $this->arguments['subject']->get('type'); - } + /** + * @return string The task's 'type'. + * + * @since 4.1.0 + */ + public function getRoutineId(): string + { + return $this->arguments['subject']->get('type'); + } - /** - * Returns the snapshot of the triggered task if available, else an empty array - * - * @return array The task snapshot if available, else null - * - * @since 4.1.0 - */ - public function getResultSnapshot(): array - { - return $this->arguments['resultSnapshot'] ?? []; - } + /** + * Returns the snapshot of the triggered task if available, else an empty array + * + * @return array The task snapshot if available, else null + * + * @since 4.1.0 + */ + public function getResultSnapshot(): array + { + return $this->arguments['resultSnapshot'] ?? []; + } } diff --git a/administrator/components/com_scheduler/src/Extension/SchedulerComponent.php b/administrator/components/com_scheduler/src/Extension/SchedulerComponent.php index 9f0b58278f3be..ec411dca38d8e 100644 --- a/administrator/components/com_scheduler/src/Extension/SchedulerComponent.php +++ b/administrator/components/com_scheduler/src/Extension/SchedulerComponent.php @@ -1,4 +1,5 @@ [0, 59], - 'hours' => [0, 23], - 'days_week' => [1, 7], - 'days_month' => [1, 31], - 'months' => [1, 12], - ]; - - /** - * Response labels for the 'month' and 'days_week' subtypes. - * The labels are language constants translated when needed. - * - * @var string[][] - * @since 4.1.0 - */ - private const PREPARED_RESPONSE_LABELS = [ - 'months' => [ - 'JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE', - 'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER', - ], - 'days_week' => [ - 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', - 'FRIDAY', 'SATURDAY', 'SUNDAY', - ], - ]; - - /** - * The form field type. - * - * @var string - * - * @since 4.1.0 - */ - protected $type = 'cronIntervals'; - - /** - * The subtype of the CronIntervals field - * - * @var string - * @since 4.1.0 - */ - private $subtype; - - /** - * If true, field options will include a wildcard - * - * @var boolean - * @since 4.1.0 - */ - private $wildcard; - - /** - * If true, field will only have numeric labels (for days_week and months) - * - * @var boolean - * @since 4.1.0 - */ - private $onlyNumericLabels; - - /** - * Override the parent method to set deal with subtypes. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form - * field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for - * the field. For example if the field has `name="foo"` and the group value is - * set to "bar" then the full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @since 4.1.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null): bool - { - $parentResult = parent::setup($element, $value, $group); - - $subtype = ((string) $element['subtype'] ?? '') ?: null; - $wildcard = ((string) $element['wildcard'] ?? '') === 'true'; - $onlyNumericLabels = ((string) $element['onlyNumericLabels']) === 'true'; - - if (!($subtype && \in_array($subtype, self::SUBTYPES))) - { - return false; - } - - $this->subtype = $subtype; - $this->wildcard = $wildcard; - $this->onlyNumericLabels = $onlyNumericLabels; - - return $parentResult; - } - - /** - * Method to get field options - * - * @return array Array of objects representing options in the options list - * - * @since 4.1.0 - */ - protected function getOptions(): array - { - $subtype = $this->subtype; - $options = parent::getOptions(); - - if (!\in_array($subtype, self::SUBTYPES)) - { - return $options; - } - - if ($this->wildcard) - { - try - { - $options[] = HTMLHelper::_('select.option', '*', '*'); - } - catch (\InvalidArgumentException $e) - { - } - } - - [$optionLower, $optionUpper] = self::OPTIONS_RANGE[$subtype]; - - // If we need text labels, we translate them first - if (\array_key_exists($subtype, self::PREPARED_RESPONSE_LABELS) && !$this->onlyNumericLabels) - { - $labels = array_map( - static function (string $string): string { - return Text::_($string); - }, - self::PREPARED_RESPONSE_LABELS[$subtype] - ); - } - else - { - $labels = range(...self::OPTIONS_RANGE[$subtype]); - } - - for ([$i, $l] = [$optionLower, 0]; $i <= $optionUpper; $i++, $l++) - { - try - { - $options[] = HTMLHelper::_('select.option', (string) ($i), $labels[$l]); - } - catch (\InvalidArgumentException $e) - { - } - } - - return $options; - } + /** + * The subtypes supported by this field type. + * + * @var string[] + * + * @since 4.1.0 + */ + private const SUBTYPES = [ + 'minutes', + 'hours', + 'days_month', + 'months', + 'days_week', + ]; + + /** + * Count of predefined options for each subtype + * + * @var int[][] + * + * @since 4.1.0 + */ + private const OPTIONS_RANGE = [ + 'minutes' => [0, 59], + 'hours' => [0, 23], + 'days_week' => [1, 7], + 'days_month' => [1, 31], + 'months' => [1, 12], + ]; + + /** + * Response labels for the 'month' and 'days_week' subtypes. + * The labels are language constants translated when needed. + * + * @var string[][] + * @since 4.1.0 + */ + private const PREPARED_RESPONSE_LABELS = [ + 'months' => [ + 'JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE', + 'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER', + ], + 'days_week' => [ + 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', + 'FRIDAY', 'SATURDAY', 'SUNDAY', + ], + ]; + + /** + * The form field type. + * + * @var string + * + * @since 4.1.0 + */ + protected $type = 'cronIntervals'; + + /** + * The subtype of the CronIntervals field + * + * @var string + * @since 4.1.0 + */ + private $subtype; + + /** + * If true, field options will include a wildcard + * + * @var boolean + * @since 4.1.0 + */ + private $wildcard; + + /** + * If true, field will only have numeric labels (for days_week and months) + * + * @var boolean + * @since 4.1.0 + */ + private $onlyNumericLabels; + + /** + * Override the parent method to set deal with subtypes. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form + * field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for + * the field. For example if the field has `name="foo"` and the group value is + * set to "bar" then the full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 4.1.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null): bool + { + $parentResult = parent::setup($element, $value, $group); + + $subtype = ((string) $element['subtype'] ?? '') ?: null; + $wildcard = ((string) $element['wildcard'] ?? '') === 'true'; + $onlyNumericLabels = ((string) $element['onlyNumericLabels']) === 'true'; + + if (!($subtype && \in_array($subtype, self::SUBTYPES))) { + return false; + } + + $this->subtype = $subtype; + $this->wildcard = $wildcard; + $this->onlyNumericLabels = $onlyNumericLabels; + + return $parentResult; + } + + /** + * Method to get field options + * + * @return array Array of objects representing options in the options list + * + * @since 4.1.0 + */ + protected function getOptions(): array + { + $subtype = $this->subtype; + $options = parent::getOptions(); + + if (!\in_array($subtype, self::SUBTYPES)) { + return $options; + } + + if ($this->wildcard) { + try { + $options[] = HTMLHelper::_('select.option', '*', '*'); + } catch (\InvalidArgumentException $e) { + } + } + + [$optionLower, $optionUpper] = self::OPTIONS_RANGE[$subtype]; + + // If we need text labels, we translate them first + if (\array_key_exists($subtype, self::PREPARED_RESPONSE_LABELS) && !$this->onlyNumericLabels) { + $labels = array_map( + static function (string $string): string { + return Text::_($string); + }, + self::PREPARED_RESPONSE_LABELS[$subtype] + ); + } else { + $labels = range(...self::OPTIONS_RANGE[$subtype]); + } + + for ([$i, $l] = [$optionLower, 0]; $i <= $optionUpper; $i++, $l++) { + try { + $options[] = HTMLHelper::_('select.option', (string) ($i), $labels[$l]); + } catch (\InvalidArgumentException $e) { + } + } + + return $options; + } } diff --git a/administrator/components/com_scheduler/src/Field/ExecutionRuleField.php b/administrator/components/com_scheduler/src/Field/ExecutionRuleField.php index 6f877357d2099..bacb4750ec754 100644 --- a/administrator/components/com_scheduler/src/Field/ExecutionRuleField.php +++ b/administrator/components/com_scheduler/src/Field/ExecutionRuleField.php @@ -1,4 +1,5 @@ 'COM_SCHEDULER_EXECUTION_INTERVAL_MINUTES', - 'interval-hours' => 'COM_SCHEDULER_EXECUTION_INTERVAL_HOURS', - 'interval-days' => 'COM_SCHEDULER_EXECUTION_INTERVAL_DAYS', - 'interval-months' => 'COM_SCHEDULER_EXECUTION_INTERVAL_MONTHS', - 'cron-expression' => 'COM_SCHEDULER_EXECUTION_CRON_EXPRESSION', - 'manual' => 'COM_SCHEDULER_OPTION_EXECUTION_MANUAL_LABEL', - ]; + /** + * Available execution rules. + * + * @var string[] + * @since 4.1.0 + */ + protected $predefinedOptions = [ + 'interval-minutes' => 'COM_SCHEDULER_EXECUTION_INTERVAL_MINUTES', + 'interval-hours' => 'COM_SCHEDULER_EXECUTION_INTERVAL_HOURS', + 'interval-days' => 'COM_SCHEDULER_EXECUTION_INTERVAL_DAYS', + 'interval-months' => 'COM_SCHEDULER_EXECUTION_INTERVAL_MONTHS', + 'cron-expression' => 'COM_SCHEDULER_EXECUTION_CRON_EXPRESSION', + 'manual' => 'COM_SCHEDULER_OPTION_EXECUTION_MANUAL_LABEL', + ]; } diff --git a/administrator/components/com_scheduler/src/Field/IntervalField.php b/administrator/components/com_scheduler/src/Field/IntervalField.php index 7a7202e66f33d..1ca63a405f0aa 100644 --- a/administrator/components/com_scheduler/src/Field/IntervalField.php +++ b/administrator/components/com_scheduler/src/Field/IntervalField.php @@ -1,4 +1,5 @@ [minVal, maxVal] - * - * @var string[] - * @since 4.1.0 - */ - private const SUBTYPES = [ - 'minutes' => [1, 59], - 'hours' => [1, 23], - 'days' => [1, 30], - 'months' => [1, 12], - ]; + /** + * The subtypes supported by this field type => [minVal, maxVal] + * + * @var string[] + * @since 4.1.0 + */ + private const SUBTYPES = [ + 'minutes' => [1, 59], + 'hours' => [1, 23], + 'days' => [1, 30], + 'months' => [1, 12], + ]; - /** - * The allowable maximum value of the field. - * - * @var float - * @since 4.1.0 - */ - protected $max; + /** + * The allowable maximum value of the field. + * + * @var float + * @since 4.1.0 + */ + protected $max; - /** - * The allowable minimum value of the field. - * - * @var float - * @since 4.1.0 - */ - protected $min; + /** + * The allowable minimum value of the field. + * + * @var float + * @since 4.1.0 + */ + protected $min; - /** - * The step by which value of the field increased or decreased. - * - * @var float - * @since 4.1.0 - */ - protected $step = 1; + /** + * The step by which value of the field increased or decreased. + * + * @var float + * @since 4.1.0 + */ + protected $step = 1; - /** - * Override the parent method to set deal with subtypes. - * - * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form - * field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for - * the field. For example if the field has `name="foo"` and the group value is - * set to "bar" then the full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @since 4.1.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null): bool - { - $parentResult = FormField::setup($element, $value, $group); - $subtype = ((string) $element['subtype'] ?? '') ?: null; + /** + * Override the parent method to set deal with subtypes. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form + * field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for + * the field. For example if the field has `name="foo"` and the group value is + * set to "bar" then the full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 4.1.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null): bool + { + $parentResult = FormField::setup($element, $value, $group); + $subtype = ((string) $element['subtype'] ?? '') ?: null; - if (empty($subtype) || !\array_key_exists($subtype, self::SUBTYPES)) - { - return false; - } + if (empty($subtype) || !\array_key_exists($subtype, self::SUBTYPES)) { + return false; + } - [$this->min, $this->max] = self::SUBTYPES[$subtype]; + [$this->min, $this->max] = self::SUBTYPES[$subtype]; - return $parentResult; - } + return $parentResult; + } } diff --git a/administrator/components/com_scheduler/src/Field/TaskStateField.php b/administrator/components/com_scheduler/src/Field/TaskStateField.php index 4ea8ba93d8a6e..a9bca3e880a15 100644 --- a/administrator/components/com_scheduler/src/Field/TaskStateField.php +++ b/administrator/components/com_scheduler/src/Field/TaskStateField.php @@ -1,4 +1,5 @@ 'JTRASHED', - 0 => 'JDISABLED', - 1 => 'JENABLED', - '*' => 'JALL', - ]; + /** + * Available states + * + * @var string[] + * @since 4.1.0 + */ + protected $predefinedOptions = [ + -2 => 'JTRASHED', + 0 => 'JDISABLED', + 1 => 'JENABLED', + '*' => 'JALL', + ]; } diff --git a/administrator/components/com_scheduler/src/Field/TaskTypeField.php b/administrator/components/com_scheduler/src/Field/TaskTypeField.php index 86d2e23702a0a..a0332f25d7319 100644 --- a/administrator/components/com_scheduler/src/Field/TaskTypeField.php +++ b/administrator/components/com_scheduler/src/Field/TaskTypeField.php @@ -1,4 +1,5 @@ options, - 'title', - 1 - ); + // Get all available task types and sort by title + $types = ArrayHelper::sortObjects( + SchedulerHelper::getTaskOptions()->options, + 'title', + 1 + ); - // Closure to add a TaskOption as a option in $options: array + $addTypeAsOption = function (TaskOption $type) use (&$options) { + try { + $options[] = HTMLHelper::_('select.option', $type->id, $type->title); + } catch (\InvalidArgumentException $e) { + } + }; - // Call $addTypeAsOption on each type - array_map($addTypeAsOption, $types); + // Call $addTypeAsOption on each type + array_map($addTypeAsOption, $types); - return $options; - } + return $options; + } } diff --git a/administrator/components/com_scheduler/src/Field/WebcronLinkField.php b/administrator/components/com_scheduler/src/Field/WebcronLinkField.php index 597afee3e6428..0b46ff7ada0b2 100644 --- a/administrator/components/com_scheduler/src/Field/WebcronLinkField.php +++ b/administrator/components/com_scheduler/src/Field/WebcronLinkField.php @@ -1,4 +1,5 @@ task = \is_array($task) ? $task : ArrayHelper::fromObject($task); - $rule = $this->getFromTask('cron_rules'); - $this->rule = \is_string($rule) - ? (object) json_decode($rule) - : (\is_array($rule) ? (object) $rule : $rule); - $this->type = $this->rule->type; - } + /** + * @param array|object $task A task entry + * + * @since 4.1.0 + */ + public function __construct($task) + { + $this->task = \is_array($task) ? $task : ArrayHelper::fromObject($task); + $rule = $this->getFromTask('cron_rules'); + $this->rule = \is_string($rule) + ? (object) json_decode($rule) + : (\is_array($rule) ? (object) $rule : $rule); + $this->type = $this->rule->type; + } - /** - * Get a property from the task array - * - * @param string $property The property to get - * @param mixed $default The default value returned if property does not exist - * - * @return mixed - * - * @since 4.1.0 - */ - private function getFromTask(string $property, $default = null) - { - $property = ArrayHelper::getValue($this->task, $property); + /** + * Get a property from the task array + * + * @param string $property The property to get + * @param mixed $default The default value returned if property does not exist + * + * @return mixed + * + * @since 4.1.0 + */ + private function getFromTask(string $property, $default = null) + { + $property = ArrayHelper::getValue($this->task, $property); - return $property ?? $default; - } + return $property ?? $default; + } - /** - * @param boolean $string If true, an SQL formatted string is returned. - * @param boolean $basisNow If true, the current date-time is used as the basis for projecting the next - * execution. - * - * @return ?Date|string - * - * @since 4.1.0 - * @throws \Exception - */ - public function nextExec(bool $string = true, bool $basisNow = false) - { - // Exception handling here - switch ($this->type) - { - case 'interval': - $lastExec = Factory::getDate($basisNow ? 'now' : $this->getFromTask('last_execution'), 'UTC'); - $interval = new \DateInterval($this->rule->exp); - $nextExec = $lastExec->add($interval); - $nextExec = $string ? $nextExec->toSql() : $nextExec; - break; - case 'cron-expression': - // @todo: testing - $cExp = new CronExpression((string) $this->rule->exp); - $nextExec = $cExp->getNextRunDate('now', 0, false, 'UTC'); - $nextExec = $string ? $this->dateTimeToSql($nextExec) : $nextExec; - break; - default: - // 'manual' execution is handled here. - $nextExec = null; - } + /** + * @param boolean $string If true, an SQL formatted string is returned. + * @param boolean $basisNow If true, the current date-time is used as the basis for projecting the next + * execution. + * + * @return ?Date|string + * + * @since 4.1.0 + * @throws \Exception + */ + public function nextExec(bool $string = true, bool $basisNow = false) + { + // Exception handling here + switch ($this->type) { + case 'interval': + $lastExec = Factory::getDate($basisNow ? 'now' : $this->getFromTask('last_execution'), 'UTC'); + $interval = new \DateInterval($this->rule->exp); + $nextExec = $lastExec->add($interval); + $nextExec = $string ? $nextExec->toSql() : $nextExec; + break; + case 'cron-expression': + // @todo: testing + $cExp = new CronExpression((string) $this->rule->exp); + $nextExec = $cExp->getNextRunDate('now', 0, false, 'UTC'); + $nextExec = $string ? $this->dateTimeToSql($nextExec) : $nextExec; + break; + default: + // 'manual' execution is handled here. + $nextExec = null; + } - return $nextExec; - } + return $nextExec; + } - /** - * Returns a sql-formatted string for a DateTime object. - * Only needed for DateTime objects returned by CronExpression, JDate supports this as class method. - * - * @param \DateTime $dateTime A DateTime object to format - * - * @return string - * - * @since 4.1.0 - */ - private function dateTimeToSql(\DateTime $dateTime): string - { - static $db; - $db = $db ?? Factory::getContainer()->get(DatabaseDriver::class); + /** + * Returns a sql-formatted string for a DateTime object. + * Only needed for DateTime objects returned by CronExpression, JDate supports this as class method. + * + * @param \DateTime $dateTime A DateTime object to format + * + * @return string + * + * @since 4.1.0 + */ + private function dateTimeToSql(\DateTime $dateTime): string + { + static $db; + $db = $db ?? Factory::getContainer()->get(DatabaseDriver::class); - return $dateTime->format($db->getDateFormat()); - } + return $dateTime->format($db->getDateFormat()); + } } diff --git a/administrator/components/com_scheduler/src/Helper/SchedulerHelper.php b/administrator/components/com_scheduler/src/Helper/SchedulerHelper.php index 3e0f13a2cefe2..0726e1f9f7922 100644 --- a/administrator/components/com_scheduler/src/Helper/SchedulerHelper.php +++ b/administrator/components/com_scheduler/src/Helper/SchedulerHelper.php @@ -1,4 +1,5 @@ $options, - ] - ); + /** @var AdministratorApplication $app */ + $app = Factory::getApplication(); + $options = new TaskOptions(); + $event = AbstractEvent::create( + 'onTaskOptionsList', + [ + 'subject' => $options, + ] + ); - PluginHelper::importPlugin('task'); - $app->getDispatcher()->dispatch('onTaskOptionsList', $event); + PluginHelper::importPlugin('task'); + $app->getDispatcher()->dispatch('onTaskOptionsList', $event); - self::$taskOptionsCache = $options; + self::$taskOptionsCache = $options; - return $options; - } + return $options; + } } diff --git a/administrator/components/com_scheduler/src/Model/SelectModel.php b/administrator/components/com_scheduler/src/Model/SelectModel.php index ea92d6c816e90..8d0365bcf9f67 100644 --- a/administrator/components/com_scheduler/src/Model/SelectModel.php +++ b/administrator/components/com_scheduler/src/Model/SelectModel.php @@ -1,4 +1,5 @@ app = Factory::getApplication(); + /** + * SelectModel constructor. + * + * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). + * @param ?MVCFactoryInterface $factory The factory. + * + * @throws \Exception + * @since 4.1.0 + */ + public function __construct($config = array(), ?MVCFactoryInterface $factory = null) + { + $this->app = Factory::getApplication(); - parent::__construct($config, $factory); - } + parent::__construct($config, $factory); + } - /** - * @return TaskOption[] An array of TaskOption objects - * - * @throws \Exception - * @since 4.1.0 - */ - public function getItems(): array - { - return SchedulerHelper::getTaskOptions()->options; - } + /** + * @return TaskOption[] An array of TaskOption objects + * + * @throws \Exception + * @since 4.1.0 + */ + public function getItems(): array + { + return SchedulerHelper::getTaskOptions()->options; + } } diff --git a/administrator/components/com_scheduler/src/Model/TaskModel.php b/administrator/components/com_scheduler/src/Model/TaskModel.php index 5bdb8fe9be1fe..d375c2b1444bd 100644 --- a/administrator/components/com_scheduler/src/Model/TaskModel.php +++ b/administrator/components/com_scheduler/src/Model/TaskModel.php @@ -1,4 +1,5 @@ 1, - 'disabled' => 0, - 'trashed' => -2, - ]; - - /** - * The name of the database table with task records. - * - * @var string - * @since 4.1.0 - */ - public const TASK_TABLE = '#__scheduler_tasks'; - - /** - * Prefix used with controller messages - * - * @var string - * @since 4.1.0 - */ - protected $text_prefix = 'COM_SCHEDULER'; - - /** - * Type alias for content type - * - * @var string - * @since 4.1.0 - */ - public $typeAlias = 'com_scheduler.task'; - - /** - * The Application object, for convenience - * - * @var AdministratorApplication $app - * @since 4.1.0 - */ - protected $app; - - /** - * The event to trigger before unlocking the data. - * - * @var string - * @since 4.1.0 - */ - protected $event_before_unlock = null; - - /** - * The event to trigger after unlocking the data. - * - * @var string - * @since 4.1.0 - */ - protected $event_unlock = null; - - /** - * TaskModel constructor. Needed just to set $app - * - * @param array $config An array of configuration options - * @param MVCFactoryInterface|null $factory The factory - * @param FormFactoryInterface|null $formFactory The form factory - * - * @since 4.1.0 - * @throws \Exception - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, FormFactoryInterface $formFactory = null) - { - $config['events_map'] = $config['events_map'] ?? []; - - $config['events_map'] = array_merge( - [ - 'save' => 'task', - 'validate' => 'task', - 'unlock' => 'task', - ], - $config['events_map'] - ); - - if (isset($config['event_before_unlock'])) - { - $this->event_before_unlock = $config['event_before_unlock']; - } - elseif (empty($this->event_before_unlock)) - { - $this->event_before_unlock = 'onContentBeforeUnlock'; - } - - if (isset($config['event_unlock'])) - { - $this->event_unlock = $config['event_unlock']; - } - elseif (empty($this->event_unlock)) - { - $this->event_unlock = 'onContentUnlock'; - } - - $this->app = Factory::getApplication(); - - parent::__construct($config, $factory, $formFactory); - } - - /** - * Fetches the form object associated with this model. By default, - * loads the corresponding data from the DB and binds it with the form. - * - * @param array $data Data that needs to go into the form - * @param bool $loadData Should the form load its data from the DB? - * - * @return Form|boolean A JForm object on success, false on failure. - * - * @since 4.1.0 - * @throws \Exception - */ - public function getForm($data = array(), $loadData = true) - { - Form::addFieldPath(JPATH_ADMINISTRATOR . 'components/com_scheduler/src/Field'); - - /** - * loadForm() (defined by FormBehaviourTrait) also loads the form data by calling - * loadFormData() : $data [implemented here] and binds it to the form by calling - * $form->bind($data). - */ - $form = $this->loadForm('com_scheduler.task', 'task', ['control' => 'jform', 'load_data' => $loadData]); - - if (empty($form)) - { - return false; - } - - $user = $this->app->getIdentity(); - - // If new entry, set task type from state - if ($this->getState('task.id', 0) === 0 && $this->getState('task.type') !== null) - { - $form->setValue('type', null, $this->getState('task.type')); - } - - // @todo : Check if this is working as expected for new items (id == 0) - if (!$user->authorise('core.edit.state', 'com_scheduler.task.' . $this->getState('task.id'))) - { - // Disable fields - $form->setFieldAttribute('state', 'disabled', 'true'); - - // No "hacking" ._. - $form->setFieldAttribute('state', 'filter', 'unset'); - } - - return $form; - } - - /** - * Determine whether a record may be deleted taking into consideration - * the user's permissions over the record. - * - * @param object $record The database row/record in question - * - * @return boolean True if the record may be deleted - * - * @since 4.1.0 - * @throws \Exception - */ - protected function canDelete($record): bool - { - // Record doesn't exist, can't delete - if (empty($record->id)) - { - return false; - } - - return $this->app->getIdentity()->authorise('core.delete', 'com_scheduler.task.' . $record->id); - } - - /** - * Populate the model state, we use these instead of toying with input or the global state - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - protected function populateState(): void - { - $app = $this->app; - - $taskId = $app->getInput()->getInt('id'); - $taskType = $app->getUserState('com_scheduler.add.task.task_type'); - - // @todo: Remove this. Get the option through a helper call. - $taskOption = $app->getUserState('com_scheduler.add.task.task_option'); - - $this->setState('task.id', $taskId); - $this->setState('task.type', $taskType); - $this->setState('task.option', $taskOption); - - // Load component params, though com_scheduler does not (yet) have any params - $cParams = ComponentHelper::getParams($this->option); - $this->setState('params', $cParams); - } - - /** - * Don't need to define this method since the parent getTable() - * implicitly deduces $name and $prefix anyways. This makes the object - * more transparent though. - * - * @param string $name Name of the table - * @param string $prefix Class prefix - * @param array $options Model config array - * - * @return Table - * - * @since 4.1.0 - * @throws \Exception - */ - public function getTable($name = 'Task', $prefix = 'Table', $options = array()): Table - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Fetches the data to be injected into the form - * - * @return object Associative array of form data. - * - * @since 4.1.0 - * @throws \Exception - */ - protected function loadFormData() - { - $data = $this->app->getUserState('com_scheduler.edit.task.data', array()); - - // If the data from UserState is empty, we fetch it with getItem() - if (empty($data)) - { - /** @var CMSObject $data */ - $data = $this->getItem(); - - // @todo : further data processing goes here - - // For a fresh object, set exec-day and exec-time - if (!($data->id ?? 0)) - { - $data->execution_rules['exec-day'] = gmdate('d'); - $data->execution_rules['exec-time'] = gmdate('H:i'); - } - } - - // Let plugins manipulate the data - $this->preprocessData('com_scheduler.task', $data, 'task'); - - return $data; - } - - /** - * Overloads the parent getItem() method. - * - * @param integer $pk Primary key - * - * @return object|boolean Object on success, false on failure - * - * @since 4.1.0 - * @throws \Exception - */ - public function getItem($pk = null) - { - $item = parent::getItem($pk); - - if (!\is_object($item)) - { - return false; - } - - // Parent call leaves `execution_rules` and `cron_rules` JSON encoded - $item->set('execution_rules', json_decode($item->get('execution_rules', ''))); - $item->set('cron_rules', json_decode($item->get('cron_rules', ''))); - - $taskOption = SchedulerHelper::getTaskOptions()->findOption( - ($item->id ?? 0) ? ($item->type ?? 0) : $this->getState('task.type') - ); - - $item->set('taskOption', $taskOption); - - return $item; - } - - /** - * Get a task from the database, only if an exclusive "lock" on the task can be acquired. - * The method supports options to customise the limitations on the fetch. - * - * @param array $options Array with options to fetch the task: - * 1. `id`: Optional id of the task to fetch. - * 2. `allowDisabled`: If true, disabled tasks can also be fetched. - * (default: false) - * 3. `bypassScheduling`: If true, tasks that are not due can also be - * fetched. Should only be true if an `id` is targeted instead of the - * task queue. (default: false) - * 4. `allowConcurrent`: If true, fetches even when another task is - * running ('locked'). (default: false) - * 5. `includeCliExclusive`: If true, can also fetch CLI exclusive tasks. (default: true) - * - * @return ?\stdClass Task entry as in the database. - * - * @since 4.1.0 - * @throws UndefinedOptionsException|InvalidOptionsException - * @throws \RuntimeException - */ - public function getTask(array $options = []): ?\stdClass - { - $resolver = new OptionsResolver; - - try - { - $this->configureTaskGetterOptions($resolver); - } - catch (\Exception $e) - { - } - - try - { - $options = $resolver->resolve($options); - } - catch (\Exception $e) - { - if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) - { - throw $e; - } - } - - $db = $this->getDatabase(); - $now = Factory::getDate()->toSql(); - - // Get lock on the table to help with concurrency issues - $db->lockTable(self::TASK_TABLE); - - // If concurrency is not allowed, we only get a task if another one does not have a "lock" - if (!$options['allowConcurrent']) - { - // Get count of locked (presumed running) tasks - $lockCountQuery = $db->getQuery(true) - ->from($db->quoteName(self::TASK_TABLE)) - ->select('COUNT(id)') - ->where($db->quoteName('locked') . ' IS NOT NULL'); - - try - { - $runningCount = $db->setQuery($lockCountQuery)->loadResult(); - } - catch (\RuntimeException $e) - { - $db->unlockTables(); - - return null; - } - - if ($runningCount !== 0) - { - $db->unlockTables(); - - return null; - } - } - - $lockQuery = $db->getQuery(true); - - $lockQuery->update($db->quoteName(self::TASK_TABLE)) - ->set($db->quoteName('locked') . ' = :now1') - ->bind(':now1', $now); - - // Array of all active routine ids - $activeRoutines = array_map( - static function (TaskOption $taskOption): string - { - return $taskOption->id; - }, - SchedulerHelper::getTaskOptions()->options - ); - - // "Orphaned" tasks are not a part of the task queue! - $lockQuery->whereIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING); - - // If directed, exclude CLI exclusive tasks - if (!$options['includeCliExclusive']) - { - $lockQuery->where($db->quoteName('cli_exclusive') . ' = 0'); - } - - if (!$options['bypassScheduling']) - { - $lockQuery->where($db->quoteName('next_execution') . ' <= :now2') - ->bind(':now2', $now); - } - - if ($options['allowDisabled']) - { - $lockQuery->whereIn($db->quoteName('state'), [0, 1]); - } - else - { - $lockQuery->where($db->quoteName('state') . ' = 1'); - } - - if ($options['id'] > 0) - { - $lockQuery->where($db->quoteName('id') . ' = :taskId') - ->bind(':taskId', $options['id'], ParameterType::INTEGER); - } - // Pick from the front of the task queue if no 'id' is specified - else - { - // Get the id of the next task in the task queue - $idQuery = $db->getQuery(true) - ->from($db->quoteName(self::TASK_TABLE)) - ->select($db->quoteName('id')) - ->where($db->quoteName('state') . ' = 1') - ->order($db->quoteName('priority') . ' DESC') - ->order($db->quoteName('next_execution') . ' ASC') - ->setLimit(1); - - try - { - $ids = $db->setQuery($idQuery)->loadColumn(); - } - catch (\RuntimeException $e) - { - $db->unlockTables(); - - return null; - } - - if (count($ids) === 0) - { - $db->unlockTables(); - - return null; - } - - $lockQuery->whereIn($db->quoteName('id'), $ids); - } - - try - { - $db->setQuery($lockQuery)->execute(); - } - catch (\RuntimeException $e) - { - } - finally - { - $affectedRows = $db->getAffectedRows(); - - $db->unlockTables(); - } - - if ($affectedRows != 1) - { - /* - // @todo - // ? Fatal failure handling here? - // ! Question is, how? If we check for tasks running beyond there time here, we have no way of - // ! what's already been notified (since we're not auto-unlocking/recovering tasks anymore). - // The solution __may__ be in a "last_successful_finish" (or something) column. - */ - - return null; - } - - $getQuery = $db->getQuery(true); - - $getQuery->select('*') - ->from($db->quoteName(self::TASK_TABLE)) - ->where($db->quoteName('locked') . ' = :now') - ->bind(':now', $now); - - $task = $db->setQuery($getQuery)->loadObject(); - - $task->execution_rules = json_decode($task->execution_rules); - $task->cron_rules = json_decode($task->cron_rules); - - $task->taskOption = SchedulerHelper::getTaskOptions()->findOption($task->type); - - return $task; - } - - /** - * Set up an {@see OptionsResolver} to resolve options compatible with the {@see GetTask()} method. - * - * @param OptionsResolver $resolver The {@see OptionsResolver} instance to set up. - * - * @return OptionsResolver - * - * @since 4.1.0 - * @throws AccessException - */ - public static function configureTaskGetterOptions(OptionsResolver $resolver): OptionsResolver - { - $resolver->setDefaults( - [ - 'id' => 0, - 'allowDisabled' => false, - 'bypassScheduling' => false, - 'allowConcurrent' => false, - 'includeCliExclusive' => true, - ] - ) - ->setAllowedTypes('id', 'numeric') - ->setAllowedTypes('allowDisabled', 'bool') - ->setAllowedTypes('bypassScheduling', 'bool') - ->setAllowedTypes('allowConcurrent', 'bool') - ->setAllowedTypes('includeCliExclusive', 'bool'); - - return $resolver; - } - - /** - * @param array $data The form data - * - * @return boolean True on success, false on failure - * - * @since 4.1.0 - * @throws \Exception - */ - public function save($data): bool - { - $id = (int) ($data['id'] ?? $this->getState('task.id')); - $isNew = $id === 0; - - // Clean up execution rules - $data['execution_rules'] = $this->processExecutionRules($data['execution_rules']); - - // If a new entry, we'll have to put in place a pseudo-last_execution - if ($isNew) - { - $basisDayOfMonth = $data['execution_rules']['exec-day']; - [$basisHour, $basisMinute] = explode(':', $data['execution_rules']['exec-time']); - - $data['last_execution'] = Factory::getDate('now', 'GMT')->format('Y-m') - . "-$basisDayOfMonth $basisHour:$basisMinute:00"; - } - else - { - $data['last_execution'] = $this->getItem($id)->last_execution; - } - - // Build the `cron_rules` column from `execution_rules` - $data['cron_rules'] = $this->buildExecutionRules($data['execution_rules']); - - // `next_execution` would be null if scheduling is disabled with the "manual" rule! - $data['next_execution'] = (new ExecRuleHelper($data))->nextExec(); - - if ($isNew) - { - $data['last_execution'] = null; - } - - // If no params, we set as empty array. - // ? Is this the right place to do this - $data['params'] = $data['params'] ?? []; - - // Parent method takes care of saving to the table - return parent::save($data); - } - - /** - * Clean up and standardise execution rules - * - * @param array $unprocessedRules The form data [? can just replace with execution_interval] - * - * @return array Processed rules - * - * @since 4.1.0 - */ - private function processExecutionRules(array $unprocessedRules): array - { - $executionRules = $unprocessedRules; - - $ruleType = $executionRules['rule-type']; - $retainKeys = ['rule-type', $ruleType, 'exec-day', 'exec-time']; - $executionRules = array_intersect_key($executionRules, array_flip($retainKeys)); - - // Default to current date-time in UTC/GMT as the basis - $executionRules['exec-day'] = $executionRules['exec-day'] ?: (string) gmdate('d'); - $executionRules['exec-time'] = $executionRules['exec-time'] ?: (string) gmdate('H:i'); - - // If custom ruleset, sort it - // ? Is this necessary - if ($ruleType === 'cron-expression') - { - foreach ($executionRules['cron-expression'] as &$values) - { - sort($values); - } - } - - return $executionRules; - } - - /** - * Private method to build execution expression from input execution rules. - * This expression is used internally to determine execution times/conditions. - * - * @param array $executionRules Execution rules from the Task form, post-processing. - * - * @return array - * - * @since 4.1.0 - * @throws \Exception - */ - private function buildExecutionRules(array $executionRules): array - { - // Maps interval strings, use with sprintf($map[intType], $interval) - $intervalStringMap = [ - 'minutes' => 'PT%dM', - 'hours' => 'PT%dH', - 'days' => 'P%dD', - 'months' => 'P%dM', - 'years' => 'P%dY', - ]; - - $ruleType = $executionRules['rule-type']; - $ruleClass = strpos($ruleType, 'interval') === 0 ? 'interval' : $ruleType; - $buildExpression = ''; - - if ($ruleClass === 'interval') - { - // Rule type for intervals interval- - $intervalType = explode('-', $ruleType)[1]; - $interval = $executionRules["interval-$intervalType"]; - $buildExpression = sprintf($intervalStringMap[$intervalType], $interval); - } - - if ($ruleClass === 'cron-expression') - { - // ! custom matches are disabled in the form - $matches = $executionRules['cron-expression']; - $buildExpression .= $this->wildcardIfMatch($matches['minutes'], range(0, 59), true); - $buildExpression .= ' ' . $this->wildcardIfMatch($matches['hours'], range(0, 23), true); - $buildExpression .= ' ' . $this->wildcardIfMatch($matches['days_month'], range(1, 31), true); - $buildExpression .= ' ' . $this->wildcardIfMatch($matches['months'], range(1, 12), true); - $buildExpression .= ' ' . $this->wildcardIfMatch($matches['days_week'], range(0, 6), true); - } - - return [ - 'type' => $ruleClass, - 'exp' => $buildExpression, - ]; - } - - /** - * This method releases "locks" on a set of tasks from the database. - * These locks are pseudo-locks that are used to keep a track of running tasks. However, they require require manual - * intervention to release these locks in cases such as when a task process crashes, leaving the task "locked". - * - * @param array $pks A list of the primary keys to unlock. - * - * @return boolean True on success. - * - * @since 4.1.0 - * @throws \RuntimeException|\UnexpectedValueException|\BadMethodCallException - */ - public function unlock(array &$pks): bool - { - /** @var TaskTable $table */ - $table = $this->getTable(); - - $user = Factory::getApplication()->getIdentity(); - - $context = $this->option . '.' . $this->name; - - // Include the plugins for the change of state event. - PluginHelper::importPlugin($this->events_map['unlock']); - - // Access checks. - foreach ($pks as $i => $pk) - { - $table->reset(); - - if ($table->load($pk)) - { - if (!$this->canEditState($table)) - { - // Prune items that you can't change. - unset($pks[$i]); - Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); - - return false; - } - - // Prune items that are already at the given state. - $lockedColumnName = $table->getColumnAlias('locked'); - - if (property_exists($table, $lockedColumnName) && \is_null($table->get($lockedColumnName))) - { - unset($pks[$i]); - } - } - } - - // Check if there are items to change. - if (!\count($pks)) - { - return true; - } - - $event = AbstractEvent::create( - $this->event_before_unlock, - [ - 'subject' => $this, - 'context' => $context, - 'pks' => $pks, - ] - ); - - try - { - Factory::getApplication()->getDispatcher()->dispatch($this->event_before_unlock, $event); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Attempt to unlock the records. - if (!$table->unlock($pks, $user->id)) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the after unlock event - $event = AbstractEvent::create( - $this->event_unlock, - [ - 'subject' => $this, - 'context' => $context, - 'pks' => $pks, - ] - ); - - try - { - Factory::getApplication()->getDispatcher()->dispatch($this->event_unlock, $event); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Clear the component's cache - $this->cleanCache(); - - return true; - } - - /** - * Determine if an array is populated by all its possible values by comparison to a reference array, if found a - * match a wildcard '*' is returned. - * - * @param array $target The target array - * @param array $reference The reference array, populated by the complete set of possible values in $target - * @param bool $targetToInt If true, converts $target array values to integers before comparing - * - * @return string A wildcard string if $target is fully populated, else $target itself. - * - * @since 4.1.0 - */ - private function wildcardIfMatch(array $target, array $reference, bool $targetToInt = false): string - { - if ($targetToInt) - { - $target = array_map( - static function (string $x): int { - return (int) $x; - }, - $target - ); - } - - $isMatch = array_diff($reference, $target) === []; - - return $isMatch ? "*" : implode(',', $target); - } - - /** - * Method to allow derived classes to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 4.1.0 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content'): void - { - // Load the 'task' plugin group - PluginHelper::importPlugin('task'); - - // Let the parent method take over - parent::preprocessForm($form, $data, $group); - } + /** + * Maps logical states to their values in the DB + * ? Do we end up using this? + * + * @var array + * @since 4.1.0 + */ + protected const TASK_STATES = [ + 'enabled' => 1, + 'disabled' => 0, + 'trashed' => -2, + ]; + + /** + * The name of the database table with task records. + * + * @var string + * @since 4.1.0 + */ + public const TASK_TABLE = '#__scheduler_tasks'; + + /** + * Prefix used with controller messages + * + * @var string + * @since 4.1.0 + */ + protected $text_prefix = 'COM_SCHEDULER'; + + /** + * Type alias for content type + * + * @var string + * @since 4.1.0 + */ + public $typeAlias = 'com_scheduler.task'; + + /** + * The Application object, for convenience + * + * @var AdministratorApplication $app + * @since 4.1.0 + */ + protected $app; + + /** + * The event to trigger before unlocking the data. + * + * @var string + * @since 4.1.0 + */ + protected $event_before_unlock = null; + + /** + * The event to trigger after unlocking the data. + * + * @var string + * @since 4.1.0 + */ + protected $event_unlock = null; + + /** + * TaskModel constructor. Needed just to set $app + * + * @param array $config An array of configuration options + * @param MVCFactoryInterface|null $factory The factory + * @param FormFactoryInterface|null $formFactory The form factory + * + * @since 4.1.0 + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, FormFactoryInterface $formFactory = null) + { + $config['events_map'] = $config['events_map'] ?? []; + + $config['events_map'] = array_merge( + [ + 'save' => 'task', + 'validate' => 'task', + 'unlock' => 'task', + ], + $config['events_map'] + ); + + if (isset($config['event_before_unlock'])) { + $this->event_before_unlock = $config['event_before_unlock']; + } elseif (empty($this->event_before_unlock)) { + $this->event_before_unlock = 'onContentBeforeUnlock'; + } + + if (isset($config['event_unlock'])) { + $this->event_unlock = $config['event_unlock']; + } elseif (empty($this->event_unlock)) { + $this->event_unlock = 'onContentUnlock'; + } + + $this->app = Factory::getApplication(); + + parent::__construct($config, $factory, $formFactory); + } + + /** + * Fetches the form object associated with this model. By default, + * loads the corresponding data from the DB and binds it with the form. + * + * @param array $data Data that needs to go into the form + * @param bool $loadData Should the form load its data from the DB? + * + * @return Form|boolean A JForm object on success, false on failure. + * + * @since 4.1.0 + * @throws \Exception + */ + public function getForm($data = array(), $loadData = true) + { + Form::addFieldPath(JPATH_ADMINISTRATOR . 'components/com_scheduler/src/Field'); + + /** + * loadForm() (defined by FormBehaviourTrait) also loads the form data by calling + * loadFormData() : $data [implemented here] and binds it to the form by calling + * $form->bind($data). + */ + $form = $this->loadForm('com_scheduler.task', 'task', ['control' => 'jform', 'load_data' => $loadData]); + + if (empty($form)) { + return false; + } + + $user = $this->app->getIdentity(); + + // If new entry, set task type from state + if ($this->getState('task.id', 0) === 0 && $this->getState('task.type') !== null) { + $form->setValue('type', null, $this->getState('task.type')); + } + + // @todo : Check if this is working as expected for new items (id == 0) + if (!$user->authorise('core.edit.state', 'com_scheduler.task.' . $this->getState('task.id'))) { + // Disable fields + $form->setFieldAttribute('state', 'disabled', 'true'); + + // No "hacking" ._. + $form->setFieldAttribute('state', 'filter', 'unset'); + } + + return $form; + } + + /** + * Determine whether a record may be deleted taking into consideration + * the user's permissions over the record. + * + * @param object $record The database row/record in question + * + * @return boolean True if the record may be deleted + * + * @since 4.1.0 + * @throws \Exception + */ + protected function canDelete($record): bool + { + // Record doesn't exist, can't delete + if (empty($record->id)) { + return false; + } + + return $this->app->getIdentity()->authorise('core.delete', 'com_scheduler.task.' . $record->id); + } + + /** + * Populate the model state, we use these instead of toying with input or the global state + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + protected function populateState(): void + { + $app = $this->app; + + $taskId = $app->getInput()->getInt('id'); + $taskType = $app->getUserState('com_scheduler.add.task.task_type'); + + // @todo: Remove this. Get the option through a helper call. + $taskOption = $app->getUserState('com_scheduler.add.task.task_option'); + + $this->setState('task.id', $taskId); + $this->setState('task.type', $taskType); + $this->setState('task.option', $taskOption); + + // Load component params, though com_scheduler does not (yet) have any params + $cParams = ComponentHelper::getParams($this->option); + $this->setState('params', $cParams); + } + + /** + * Don't need to define this method since the parent getTable() + * implicitly deduces $name and $prefix anyways. This makes the object + * more transparent though. + * + * @param string $name Name of the table + * @param string $prefix Class prefix + * @param array $options Model config array + * + * @return Table + * + * @since 4.1.0 + * @throws \Exception + */ + public function getTable($name = 'Task', $prefix = 'Table', $options = array()): Table + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Fetches the data to be injected into the form + * + * @return object Associative array of form data. + * + * @since 4.1.0 + * @throws \Exception + */ + protected function loadFormData() + { + $data = $this->app->getUserState('com_scheduler.edit.task.data', array()); + + // If the data from UserState is empty, we fetch it with getItem() + if (empty($data)) { + /** @var CMSObject $data */ + $data = $this->getItem(); + + // @todo : further data processing goes here + + // For a fresh object, set exec-day and exec-time + if (!($data->id ?? 0)) { + $data->execution_rules['exec-day'] = gmdate('d'); + $data->execution_rules['exec-time'] = gmdate('H:i'); + } + } + + // Let plugins manipulate the data + $this->preprocessData('com_scheduler.task', $data, 'task'); + + return $data; + } + + /** + * Overloads the parent getItem() method. + * + * @param integer $pk Primary key + * + * @return object|boolean Object on success, false on failure + * + * @since 4.1.0 + * @throws \Exception + */ + public function getItem($pk = null) + { + $item = parent::getItem($pk); + + if (!\is_object($item)) { + return false; + } + + // Parent call leaves `execution_rules` and `cron_rules` JSON encoded + $item->set('execution_rules', json_decode($item->get('execution_rules', ''))); + $item->set('cron_rules', json_decode($item->get('cron_rules', ''))); + + $taskOption = SchedulerHelper::getTaskOptions()->findOption( + ($item->id ?? 0) ? ($item->type ?? 0) : $this->getState('task.type') + ); + + $item->set('taskOption', $taskOption); + + return $item; + } + + /** + * Get a task from the database, only if an exclusive "lock" on the task can be acquired. + * The method supports options to customise the limitations on the fetch. + * + * @param array $options Array with options to fetch the task: + * 1. `id`: Optional id of the task to fetch. + * 2. `allowDisabled`: If true, disabled tasks can also be fetched. + * (default: false) + * 3. `bypassScheduling`: If true, tasks that are not due can also be + * fetched. Should only be true if an `id` is targeted instead of the + * task queue. (default: false) + * 4. `allowConcurrent`: If true, fetches even when another task is + * running ('locked'). (default: false) + * 5. `includeCliExclusive`: If true, can also fetch CLI exclusive tasks. (default: true) + * + * @return ?\stdClass Task entry as in the database. + * + * @since 4.1.0 + * @throws UndefinedOptionsException|InvalidOptionsException + * @throws \RuntimeException + */ + public function getTask(array $options = []): ?\stdClass + { + $resolver = new OptionsResolver(); + + try { + $this->configureTaskGetterOptions($resolver); + } catch (\Exception $e) { + } + + try { + $options = $resolver->resolve($options); + } catch (\Exception $e) { + if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) { + throw $e; + } + } + + $db = $this->getDatabase(); + $now = Factory::getDate()->toSql(); + + // Get lock on the table to help with concurrency issues + $db->lockTable(self::TASK_TABLE); + + // If concurrency is not allowed, we only get a task if another one does not have a "lock" + if (!$options['allowConcurrent']) { + // Get count of locked (presumed running) tasks + $lockCountQuery = $db->getQuery(true) + ->from($db->quoteName(self::TASK_TABLE)) + ->select('COUNT(id)') + ->where($db->quoteName('locked') . ' IS NOT NULL'); + + try { + $runningCount = $db->setQuery($lockCountQuery)->loadResult(); + } catch (\RuntimeException $e) { + $db->unlockTables(); + + return null; + } + + if ($runningCount !== 0) { + $db->unlockTables(); + + return null; + } + } + + $lockQuery = $db->getQuery(true); + + $lockQuery->update($db->quoteName(self::TASK_TABLE)) + ->set($db->quoteName('locked') . ' = :now1') + ->bind(':now1', $now); + + // Array of all active routine ids + $activeRoutines = array_map( + static function (TaskOption $taskOption): string { + return $taskOption->id; + }, + SchedulerHelper::getTaskOptions()->options + ); + + // "Orphaned" tasks are not a part of the task queue! + $lockQuery->whereIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING); + + // If directed, exclude CLI exclusive tasks + if (!$options['includeCliExclusive']) { + $lockQuery->where($db->quoteName('cli_exclusive') . ' = 0'); + } + + if (!$options['bypassScheduling']) { + $lockQuery->where($db->quoteName('next_execution') . ' <= :now2') + ->bind(':now2', $now); + } + + if ($options['allowDisabled']) { + $lockQuery->whereIn($db->quoteName('state'), [0, 1]); + } else { + $lockQuery->where($db->quoteName('state') . ' = 1'); + } + + if ($options['id'] > 0) { + $lockQuery->where($db->quoteName('id') . ' = :taskId') + ->bind(':taskId', $options['id'], ParameterType::INTEGER); + } else { + // Pick from the front of the task queue if no 'id' is specified + // Get the id of the next task in the task queue + $idQuery = $db->getQuery(true) + ->from($db->quoteName(self::TASK_TABLE)) + ->select($db->quoteName('id')) + ->where($db->quoteName('state') . ' = 1') + ->order($db->quoteName('priority') . ' DESC') + ->order($db->quoteName('next_execution') . ' ASC') + ->setLimit(1); + + try { + $ids = $db->setQuery($idQuery)->loadColumn(); + } catch (\RuntimeException $e) { + $db->unlockTables(); + + return null; + } + + if (count($ids) === 0) { + $db->unlockTables(); + + return null; + } + + $lockQuery->whereIn($db->quoteName('id'), $ids); + } + + try { + $db->setQuery($lockQuery)->execute(); + } catch (\RuntimeException $e) { + } finally { + $affectedRows = $db->getAffectedRows(); + + $db->unlockTables(); + } + + if ($affectedRows != 1) { + /* + // @todo + // ? Fatal failure handling here? + // ! Question is, how? If we check for tasks running beyond there time here, we have no way of + // ! what's already been notified (since we're not auto-unlocking/recovering tasks anymore). + // The solution __may__ be in a "last_successful_finish" (or something) column. + */ + + return null; + } + + $getQuery = $db->getQuery(true); + + $getQuery->select('*') + ->from($db->quoteName(self::TASK_TABLE)) + ->where($db->quoteName('locked') . ' = :now') + ->bind(':now', $now); + + $task = $db->setQuery($getQuery)->loadObject(); + + $task->execution_rules = json_decode($task->execution_rules); + $task->cron_rules = json_decode($task->cron_rules); + + $task->taskOption = SchedulerHelper::getTaskOptions()->findOption($task->type); + + return $task; + } + + /** + * Set up an {@see OptionsResolver} to resolve options compatible with the {@see GetTask()} method. + * + * @param OptionsResolver $resolver The {@see OptionsResolver} instance to set up. + * + * @return OptionsResolver + * + * @since 4.1.0 + * @throws AccessException + */ + public static function configureTaskGetterOptions(OptionsResolver $resolver): OptionsResolver + { + $resolver->setDefaults( + [ + 'id' => 0, + 'allowDisabled' => false, + 'bypassScheduling' => false, + 'allowConcurrent' => false, + 'includeCliExclusive' => true, + ] + ) + ->setAllowedTypes('id', 'numeric') + ->setAllowedTypes('allowDisabled', 'bool') + ->setAllowedTypes('bypassScheduling', 'bool') + ->setAllowedTypes('allowConcurrent', 'bool') + ->setAllowedTypes('includeCliExclusive', 'bool'); + + return $resolver; + } + + /** + * @param array $data The form data + * + * @return boolean True on success, false on failure + * + * @since 4.1.0 + * @throws \Exception + */ + public function save($data): bool + { + $id = (int) ($data['id'] ?? $this->getState('task.id')); + $isNew = $id === 0; + + // Clean up execution rules + $data['execution_rules'] = $this->processExecutionRules($data['execution_rules']); + + // If a new entry, we'll have to put in place a pseudo-last_execution + if ($isNew) { + $basisDayOfMonth = $data['execution_rules']['exec-day']; + [$basisHour, $basisMinute] = explode(':', $data['execution_rules']['exec-time']); + + $data['last_execution'] = Factory::getDate('now', 'GMT')->format('Y-m') + . "-$basisDayOfMonth $basisHour:$basisMinute:00"; + } else { + $data['last_execution'] = $this->getItem($id)->last_execution; + } + + // Build the `cron_rules` column from `execution_rules` + $data['cron_rules'] = $this->buildExecutionRules($data['execution_rules']); + + // `next_execution` would be null if scheduling is disabled with the "manual" rule! + $data['next_execution'] = (new ExecRuleHelper($data))->nextExec(); + + if ($isNew) { + $data['last_execution'] = null; + } + + // If no params, we set as empty array. + // ? Is this the right place to do this + $data['params'] = $data['params'] ?? []; + + // Parent method takes care of saving to the table + return parent::save($data); + } + + /** + * Clean up and standardise execution rules + * + * @param array $unprocessedRules The form data [? can just replace with execution_interval] + * + * @return array Processed rules + * + * @since 4.1.0 + */ + private function processExecutionRules(array $unprocessedRules): array + { + $executionRules = $unprocessedRules; + + $ruleType = $executionRules['rule-type']; + $retainKeys = ['rule-type', $ruleType, 'exec-day', 'exec-time']; + $executionRules = array_intersect_key($executionRules, array_flip($retainKeys)); + + // Default to current date-time in UTC/GMT as the basis + $executionRules['exec-day'] = $executionRules['exec-day'] ?: (string) gmdate('d'); + $executionRules['exec-time'] = $executionRules['exec-time'] ?: (string) gmdate('H:i'); + + // If custom ruleset, sort it + // ? Is this necessary + if ($ruleType === 'cron-expression') { + foreach ($executionRules['cron-expression'] as &$values) { + sort($values); + } + } + + return $executionRules; + } + + /** + * Private method to build execution expression from input execution rules. + * This expression is used internally to determine execution times/conditions. + * + * @param array $executionRules Execution rules from the Task form, post-processing. + * + * @return array + * + * @since 4.1.0 + * @throws \Exception + */ + private function buildExecutionRules(array $executionRules): array + { + // Maps interval strings, use with sprintf($map[intType], $interval) + $intervalStringMap = [ + 'minutes' => 'PT%dM', + 'hours' => 'PT%dH', + 'days' => 'P%dD', + 'months' => 'P%dM', + 'years' => 'P%dY', + ]; + + $ruleType = $executionRules['rule-type']; + $ruleClass = strpos($ruleType, 'interval') === 0 ? 'interval' : $ruleType; + $buildExpression = ''; + + if ($ruleClass === 'interval') { + // Rule type for intervals interval- + $intervalType = explode('-', $ruleType)[1]; + $interval = $executionRules["interval-$intervalType"]; + $buildExpression = sprintf($intervalStringMap[$intervalType], $interval); + } + + if ($ruleClass === 'cron-expression') { + // ! custom matches are disabled in the form + $matches = $executionRules['cron-expression']; + $buildExpression .= $this->wildcardIfMatch($matches['minutes'], range(0, 59), true); + $buildExpression .= ' ' . $this->wildcardIfMatch($matches['hours'], range(0, 23), true); + $buildExpression .= ' ' . $this->wildcardIfMatch($matches['days_month'], range(1, 31), true); + $buildExpression .= ' ' . $this->wildcardIfMatch($matches['months'], range(1, 12), true); + $buildExpression .= ' ' . $this->wildcardIfMatch($matches['days_week'], range(0, 6), true); + } + + return [ + 'type' => $ruleClass, + 'exp' => $buildExpression, + ]; + } + + /** + * This method releases "locks" on a set of tasks from the database. + * These locks are pseudo-locks that are used to keep a track of running tasks. However, they require require manual + * intervention to release these locks in cases such as when a task process crashes, leaving the task "locked". + * + * @param array $pks A list of the primary keys to unlock. + * + * @return boolean True on success. + * + * @since 4.1.0 + * @throws \RuntimeException|\UnexpectedValueException|\BadMethodCallException + */ + public function unlock(array &$pks): bool + { + /** @var TaskTable $table */ + $table = $this->getTable(); + + $user = Factory::getApplication()->getIdentity(); + + $context = $this->option . '.' . $this->name; + + // Include the plugins for the change of state event. + PluginHelper::importPlugin($this->events_map['unlock']); + + // Access checks. + foreach ($pks as $i => $pk) { + $table->reset(); + + if ($table->load($pk)) { + if (!$this->canEditState($table)) { + // Prune items that you can't change. + unset($pks[$i]); + Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); + + return false; + } + + // Prune items that are already at the given state. + $lockedColumnName = $table->getColumnAlias('locked'); + + if (property_exists($table, $lockedColumnName) && \is_null($table->get($lockedColumnName))) { + unset($pks[$i]); + } + } + } + + // Check if there are items to change. + if (!\count($pks)) { + return true; + } + + $event = AbstractEvent::create( + $this->event_before_unlock, + [ + 'subject' => $this, + 'context' => $context, + 'pks' => $pks, + ] + ); + + try { + Factory::getApplication()->getDispatcher()->dispatch($this->event_before_unlock, $event); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Attempt to unlock the records. + if (!$table->unlock($pks, $user->id)) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the after unlock event + $event = AbstractEvent::create( + $this->event_unlock, + [ + 'subject' => $this, + 'context' => $context, + 'pks' => $pks, + ] + ); + + try { + Factory::getApplication()->getDispatcher()->dispatch($this->event_unlock, $event); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Clear the component's cache + $this->cleanCache(); + + return true; + } + + /** + * Determine if an array is populated by all its possible values by comparison to a reference array, if found a + * match a wildcard '*' is returned. + * + * @param array $target The target array + * @param array $reference The reference array, populated by the complete set of possible values in $target + * @param bool $targetToInt If true, converts $target array values to integers before comparing + * + * @return string A wildcard string if $target is fully populated, else $target itself. + * + * @since 4.1.0 + */ + private function wildcardIfMatch(array $target, array $reference, bool $targetToInt = false): string + { + if ($targetToInt) { + $target = array_map( + static function (string $x): int { + return (int) $x; + }, + $target + ); + } + + $isMatch = array_diff($reference, $target) === []; + + return $isMatch ? "*" : implode(',', $target); + } + + /** + * Method to allow derived classes to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 4.1.0 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content'): void + { + // Load the 'task' plugin group + PluginHelper::importPlugin('task'); + + // Let the parent method take over + parent::preprocessForm($form, $data, $group); + } } diff --git a/administrator/components/com_scheduler/src/Model/TasksModel.php b/administrator/components/com_scheduler/src/Model/TasksModel.php index e8f635d636830..36b1539d5bc18 100644 --- a/administrator/components/com_scheduler/src/Model/TasksModel.php +++ b/administrator/components/com_scheduler/src/Model/TasksModel.php @@ -1,4 +1,5 @@ getState('filter.search'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.type'); - $id .= ':' . $this->getState('filter.orphaned'); - $id .= ':' . $this->getState('filter.due'); - $id .= ':' . $this->getState('filter.locked'); - $id .= ':' . $this->getState('filter.trigger'); - $id .= ':' . $this->getState('list.select'); - - return parent::getStoreId($id); - } - - /** - * Method to create a query for a list of items. - * - * @return QueryInterface - * - * @since 4.1.0 - * @throws \Exception - */ - protected function getListQuery(): QueryInterface - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - /** - * Select the required fields from the table. - * ? Do we need all these defaults ? - * ? Does 'list.select' exist ? - */ - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.asset_id'), - $db->quoteName('a.title'), - $db->quoteName('a.type'), - $db->quoteName('a.execution_rules'), - $db->quoteName('a.state'), - $db->quoteName('a.last_exit_code'), - $db->quoteName('a.locked'), - $db->quoteName('a.last_execution'), - $db->quoteName('a.next_execution'), - $db->quoteName('a.times_executed'), - $db->quoteName('a.times_failed'), - $db->quoteName('a.priority'), - $db->quoteName('a.ordering'), - $db->quoteName('a.note'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - ] - ) - ) - ->select( - [ - $db->quoteName('uc.name', 'editor'), - ] - ) - ->from($db->quoteName('#__scheduler_tasks', 'a')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); - - // Filters go below - $filterCount = 0; - - /** - * Extends query if already filtered. - * - * @param string $outerGlue - * @param array $conditions - * @param string $innerGlue - * - * @since 4.1.0 - */ - $extendWhereIfFiltered = static function ( - string $outerGlue, - array $conditions, - string $innerGlue - ) use ($query, &$filterCount) { - if ($filterCount++) - { - $query->extendWhere($outerGlue, $conditions, $innerGlue); - } - else - { - $query->where($conditions, $innerGlue); - } - - }; - - // Filter over ID, title (redundant to search, but) --- - if (is_numeric($id = $this->getState('filter.id'))) - { - $filterCount++; - $id = (int) $id; - $query->where($db->qn('a.id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - } - elseif ($title = $this->getState('filter.title')) - { - $filterCount++; - $match = "%$title%"; - $query->where($db->qn('a.title') . ' LIKE :match') - ->bind(':match', $match); - } - - // Filter orphaned (-1: exclude, 0: include, 1: only) ---- - $filterOrphaned = (int) $this->getState('filter.orphaned'); - - if ($filterOrphaned !== 0) - { - $filterCount++; - $taskOptions = SchedulerHelper::getTaskOptions(); - - // Array of all active routine ids - $activeRoutines = array_map( - static function (TaskOption $taskOption): string - { - return $taskOption->id; - }, - $taskOptions->options - ); - - if ($filterOrphaned === -1) - { - $query->whereIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING); - } - else - { - $query->whereNotIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING); - } - } - - // Filter over state ---- - $state = $this->getState('filter.state'); - - if ($state !== '*') - { - $filterCount++; - - if (is_numeric($state)) - { - $state = (int) $state; - - $query->where($db->quoteName('a.state') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - else - { - $query->whereIn($db->quoteName('a.state'), [0, 1]); - } - } - - // Filter over type ---- - $typeFilter = $this->getState('filter.type'); - - if ($typeFilter) - { - $filterCount++; - $query->where($db->quotename('a.type') . '= :type') - ->bind(':type', $typeFilter); - } - - // Filter over exit code ---- - $exitCode = $this->getState('filter.last_exit_code'); - - if (is_numeric($exitCode)) - { - $filterCount++; - $exitCode = (int) $exitCode; - $query->where($db->quoteName('a.last_exit_code') . '= :last_exit_code') - ->bind(':last_exit_code', $exitCode, ParameterType::INTEGER); - } - - // Filter due (-1: exclude, 0: include, 1: only) ---- - $due = $this->getState('filter.due'); - - if (is_numeric($due) && $due != 0) - { - $now = Factory::getDate('now', 'GMT')->toSql(); - $operator = $due == 1 ? ' <= ' : ' > '; - $filterCount++; - $query->where($db->qn('a.next_execution') . $operator . ':now') - ->bind(':now', $now); - } - - /* - * Filter locked --- - * Locks can be either hard locks or soft locks. Locks that have expired (exceeded the task timeout) are soft - * locks. Hard-locked tasks are assumed to be running. Soft-locked tasks are assumed to have suffered a fatal - * failure. - * {-2: exclude-all, -1: exclude-hard-locked, 0: include, 1: include-only-locked, 2: include-only-soft-locked} - */ - $locked = $this->getState('filter.locked'); - - if (is_numeric($locked) && $locked != 0) - { - $now = Factory::getDate('now', 'GMT'); - $timeout = ComponentHelper::getParams('com_scheduler')->get('timeout', 300); - $timeout = new \DateInterval(sprintf('PT%dS', $timeout)); - $timeoutThreshold = (clone $now)->sub($timeout)->toSql(); - $now = $now->toSql(); - - switch ($locked) - { - case -2: - $query->where($db->qn('a.locked') . 'IS NULL'); - break; - case -1: - $extendWhereIfFiltered( - 'AND', - [ - $db->qn('a.locked') . ' IS NULL', - $db->qn('a.locked') . ' < :threshold', - ], - 'OR' - ); - $query->bind(':threshold', $timeoutThreshold); - break; - case 1: - $query->where($db->qn('a.locked') . ' IS NOT NULL'); - break; - case 2: - $query->where($db->qn('a.locked') . ' < :threshold') - ->bind(':threshold', $timeoutThreshold); - } - } - - // Filter over search string if set (title, type title, note, id) ---- - $searchStr = $this->getState('filter.search'); - - if (!empty($searchStr)) - { - // Allow search by ID - if (stripos($searchStr, 'id:') === 0) - { - // Add array support [?] - $id = (int) substr($searchStr, 3); - $query->where($db->quoteName('a.id') . '= :id') - ->bind(':id', $id, ParameterType::INTEGER); - } - // Search by type is handled exceptionally in _getList() [@todo: remove refs] - elseif (stripos($searchStr, 'type:') !== 0) - { - $searchStr = "%$searchStr%"; - - // Bind keys to query - $query->bind(':title', $searchStr) - ->bind(':note', $searchStr); - $conditions = [ - $db->quoteName('a.title') . ' LIKE :title', - $db->quoteName('a.note') . ' LIKE :note', - ]; - $extendWhereIfFiltered('AND', $conditions, 'OR'); - } - } - - // Add list ordering clause. ---- - // @todo implement multi-column ordering someway - $multiOrdering = $this->state->get('list.multi_ordering'); - - if (!$multiOrdering || !\is_array($multiOrdering)) - { - $orderCol = $this->state->get('list.ordering', 'a.title'); - $orderDir = $this->state->get('list.direction', 'asc'); - - // Type title ordering is handled exceptionally in _getList() - if ($orderCol !== 'j.type_title') - { - $query->order($db->quoteName($orderCol) . ' ' . $orderDir); - - // If ordering by type or state, also order by title. - if (\in_array($orderCol, ['a.type', 'a.state', 'a.priority'])) - { - // @todo : Test if things are working as expected - $query->order($db->quoteName('a.title') . ' ' . $orderDir); - } - } - } - else - { - // @todo Should add quoting here - $query->order($multiOrdering); - } - - return $query; - } - - /** - * Overloads the parent _getList() method. - * Takes care of attaching TaskOption objects and sorting by type titles. - * - * @param DatabaseQuery $query The database query to get the list with - * @param int $limitstart The list offset - * @param int $limit Number of list items to fetch - * - * @return object[] - * - * @since 4.1.0 - * @throws \Exception - */ - protected function _getList($query, $limitstart = 0, $limit = 0): array - { - // Get stuff from the model state - $listOrder = $this->getState('list.ordering', 'a.title'); - $listDirectionN = strtolower($this->getState('list.direction', 'asc')) == 'desc' ? -1 : 1; - - // Set limit parameters and get object list - $query->setLimit($limit, $limitstart); - $this->getDatabase()->setQuery($query); - - // Return optionally an extended class. - // @todo: Use something other than CMSObject.. - if ($this->getState('list.customClass')) - { - $responseList = array_map( - static function (array $arr) { - $o = new CMSObject; - - foreach ($arr as $k => $v) - { - $o->{$k} = $v; - } - - return $o; - }, - $this->getDatabase()->loadAssocList() ?: [] - ); - } - else - { - $responseList = $this->getDatabase()->loadObjectList(); - } - - // Attach TaskOptions objects and a safe type title - $this->attachTaskOptions($responseList); - - // If ordering by non-db fields, we need to sort here in code - if ($listOrder == 'j.type_title') - { - $responseList = ArrayHelper::sortObjects($responseList, 'safeTypeTitle', $listDirectionN, true, false); - } - - return $responseList; - } - - /** - * For an array of items, attaches TaskOption objects and (safe) type titles to each. - * - * @param array $items Array of items, passed by reference - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - private function attachTaskOptions(array $items): void - { - $taskOptions = SchedulerHelper::getTaskOptions(); - - foreach ($items as $item) - { - $item->taskOption = $taskOptions->findOption($item->type); - $item->safeTypeTitle = $item->taskOption->title ?? Text::_('JGLOBAL_NONAPPLICABLE'); - } - } - - /** - * Proxy for the parent method. - * Sets ordering defaults. - * - * @param string $ordering Field to order/sort list by - * @param string $direction Direction in which to sort list - * - * @return void - * @since 4.1.0 - */ - protected function populateState($ordering = 'a.title', $direction = 'ASC'): void - { - // Call the parent method - parent::populateState($ordering, $direction); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface|null $factory The factory. + * + * @since 4.1.0 + * @throws \Exception + * @see \JControllerLegacy + */ + public function __construct($config = [], MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = [ + 'id', 'a.id', + 'asset_id', 'a.asset_id', + 'title', 'a.title', + 'type', 'a.type', + 'type_title', 'j.type_title', + 'state', 'a.state', + 'last_exit_code', 'a.last_exit_code', + 'last_execution', 'a.last_execution', + 'next_execution', 'a.next_execution', + 'times_executed', 'a.times_executed', + 'times_failed', 'a.times_failed', + 'ordering', 'a.ordering', + 'priority', 'a.priority', + 'note', 'a.note', + 'created', 'a.created', + 'created_by', 'a.created_by', + ]; + } + + parent::__construct($config, $factory); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 4.1.0 + */ + protected function getStoreId($id = ''): string + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.type'); + $id .= ':' . $this->getState('filter.orphaned'); + $id .= ':' . $this->getState('filter.due'); + $id .= ':' . $this->getState('filter.locked'); + $id .= ':' . $this->getState('filter.trigger'); + $id .= ':' . $this->getState('list.select'); + + return parent::getStoreId($id); + } + + /** + * Method to create a query for a list of items. + * + * @return QueryInterface + * + * @since 4.1.0 + * @throws \Exception + */ + protected function getListQuery(): QueryInterface + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + /** + * Select the required fields from the table. + * ? Do we need all these defaults ? + * ? Does 'list.select' exist ? + */ + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.asset_id'), + $db->quoteName('a.title'), + $db->quoteName('a.type'), + $db->quoteName('a.execution_rules'), + $db->quoteName('a.state'), + $db->quoteName('a.last_exit_code'), + $db->quoteName('a.locked'), + $db->quoteName('a.last_execution'), + $db->quoteName('a.next_execution'), + $db->quoteName('a.times_executed'), + $db->quoteName('a.times_failed'), + $db->quoteName('a.priority'), + $db->quoteName('a.ordering'), + $db->quoteName('a.note'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + ] + ) + ) + ->select( + [ + $db->quoteName('uc.name', 'editor'), + ] + ) + ->from($db->quoteName('#__scheduler_tasks', 'a')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); + + // Filters go below + $filterCount = 0; + + /** + * Extends query if already filtered. + * + * @param string $outerGlue + * @param array $conditions + * @param string $innerGlue + * + * @since 4.1.0 + */ + $extendWhereIfFiltered = static function ( + string $outerGlue, + array $conditions, + string $innerGlue + ) use ( + $query, + &$filterCount +) { + if ($filterCount++) { + $query->extendWhere($outerGlue, $conditions, $innerGlue); + } else { + $query->where($conditions, $innerGlue); + } + }; + + // Filter over ID, title (redundant to search, but) --- + if (is_numeric($id = $this->getState('filter.id'))) { + $filterCount++; + $id = (int) $id; + $query->where($db->qn('a.id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + } elseif ($title = $this->getState('filter.title')) { + $filterCount++; + $match = "%$title%"; + $query->where($db->qn('a.title') . ' LIKE :match') + ->bind(':match', $match); + } + + // Filter orphaned (-1: exclude, 0: include, 1: only) ---- + $filterOrphaned = (int) $this->getState('filter.orphaned'); + + if ($filterOrphaned !== 0) { + $filterCount++; + $taskOptions = SchedulerHelper::getTaskOptions(); + + // Array of all active routine ids + $activeRoutines = array_map( + static function (TaskOption $taskOption): string { + return $taskOption->id; + }, + $taskOptions->options + ); + + if ($filterOrphaned === -1) { + $query->whereIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING); + } else { + $query->whereNotIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING); + } + } + + // Filter over state ---- + $state = $this->getState('filter.state'); + + if ($state !== '*') { + $filterCount++; + + if (is_numeric($state)) { + $state = (int) $state; + + $query->where($db->quoteName('a.state') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } else { + $query->whereIn($db->quoteName('a.state'), [0, 1]); + } + } + + // Filter over type ---- + $typeFilter = $this->getState('filter.type'); + + if ($typeFilter) { + $filterCount++; + $query->where($db->quotename('a.type') . '= :type') + ->bind(':type', $typeFilter); + } + + // Filter over exit code ---- + $exitCode = $this->getState('filter.last_exit_code'); + + if (is_numeric($exitCode)) { + $filterCount++; + $exitCode = (int) $exitCode; + $query->where($db->quoteName('a.last_exit_code') . '= :last_exit_code') + ->bind(':last_exit_code', $exitCode, ParameterType::INTEGER); + } + + // Filter due (-1: exclude, 0: include, 1: only) ---- + $due = $this->getState('filter.due'); + + if (is_numeric($due) && $due != 0) { + $now = Factory::getDate('now', 'GMT')->toSql(); + $operator = $due == 1 ? ' <= ' : ' > '; + $filterCount++; + $query->where($db->qn('a.next_execution') . $operator . ':now') + ->bind(':now', $now); + } + + /* + * Filter locked --- + * Locks can be either hard locks or soft locks. Locks that have expired (exceeded the task timeout) are soft + * locks. Hard-locked tasks are assumed to be running. Soft-locked tasks are assumed to have suffered a fatal + * failure. + * {-2: exclude-all, -1: exclude-hard-locked, 0: include, 1: include-only-locked, 2: include-only-soft-locked} + */ + $locked = $this->getState('filter.locked'); + + if (is_numeric($locked) && $locked != 0) { + $now = Factory::getDate('now', 'GMT'); + $timeout = ComponentHelper::getParams('com_scheduler')->get('timeout', 300); + $timeout = new \DateInterval(sprintf('PT%dS', $timeout)); + $timeoutThreshold = (clone $now)->sub($timeout)->toSql(); + $now = $now->toSql(); + + switch ($locked) { + case -2: + $query->where($db->qn('a.locked') . 'IS NULL'); + break; + case -1: + $extendWhereIfFiltered( + 'AND', + [ + $db->qn('a.locked') . ' IS NULL', + $db->qn('a.locked') . ' < :threshold', + ], + 'OR' + ); + $query->bind(':threshold', $timeoutThreshold); + break; + case 1: + $query->where($db->qn('a.locked') . ' IS NOT NULL'); + break; + case 2: + $query->where($db->qn('a.locked') . ' < :threshold') + ->bind(':threshold', $timeoutThreshold); + } + } + + // Filter over search string if set (title, type title, note, id) ---- + $searchStr = $this->getState('filter.search'); + + if (!empty($searchStr)) { + // Allow search by ID + if (stripos($searchStr, 'id:') === 0) { + // Add array support [?] + $id = (int) substr($searchStr, 3); + $query->where($db->quoteName('a.id') . '= :id') + ->bind(':id', $id, ParameterType::INTEGER); + } elseif (stripos($searchStr, 'type:') !== 0) { + // Search by type is handled exceptionally in _getList() [@todo: remove refs] + $searchStr = "%$searchStr%"; + + // Bind keys to query + $query->bind(':title', $searchStr) + ->bind(':note', $searchStr); + $conditions = [ + $db->quoteName('a.title') . ' LIKE :title', + $db->quoteName('a.note') . ' LIKE :note', + ]; + $extendWhereIfFiltered('AND', $conditions, 'OR'); + } + } + + // Add list ordering clause. ---- + // @todo implement multi-column ordering someway + $multiOrdering = $this->state->get('list.multi_ordering'); + + if (!$multiOrdering || !\is_array($multiOrdering)) { + $orderCol = $this->state->get('list.ordering', 'a.title'); + $orderDir = $this->state->get('list.direction', 'asc'); + + // Type title ordering is handled exceptionally in _getList() + if ($orderCol !== 'j.type_title') { + $query->order($db->quoteName($orderCol) . ' ' . $orderDir); + + // If ordering by type or state, also order by title. + if (\in_array($orderCol, ['a.type', 'a.state', 'a.priority'])) { + // @todo : Test if things are working as expected + $query->order($db->quoteName('a.title') . ' ' . $orderDir); + } + } + } else { + // @todo Should add quoting here + $query->order($multiOrdering); + } + + return $query; + } + + /** + * Overloads the parent _getList() method. + * Takes care of attaching TaskOption objects and sorting by type titles. + * + * @param DatabaseQuery $query The database query to get the list with + * @param int $limitstart The list offset + * @param int $limit Number of list items to fetch + * + * @return object[] + * + * @since 4.1.0 + * @throws \Exception + */ + protected function _getList($query, $limitstart = 0, $limit = 0): array + { + // Get stuff from the model state + $listOrder = $this->getState('list.ordering', 'a.title'); + $listDirectionN = strtolower($this->getState('list.direction', 'asc')) == 'desc' ? -1 : 1; + + // Set limit parameters and get object list + $query->setLimit($limit, $limitstart); + $this->getDatabase()->setQuery($query); + + // Return optionally an extended class. + // @todo: Use something other than CMSObject.. + if ($this->getState('list.customClass')) { + $responseList = array_map( + static function (array $arr) { + $o = new CMSObject(); + + foreach ($arr as $k => $v) { + $o->{$k} = $v; + } + + return $o; + }, + $this->getDatabase()->loadAssocList() ?: [] + ); + } else { + $responseList = $this->getDatabase()->loadObjectList(); + } + + // Attach TaskOptions objects and a safe type title + $this->attachTaskOptions($responseList); + + // If ordering by non-db fields, we need to sort here in code + if ($listOrder == 'j.type_title') { + $responseList = ArrayHelper::sortObjects($responseList, 'safeTypeTitle', $listDirectionN, true, false); + } + + return $responseList; + } + + /** + * For an array of items, attaches TaskOption objects and (safe) type titles to each. + * + * @param array $items Array of items, passed by reference + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + private function attachTaskOptions(array $items): void + { + $taskOptions = SchedulerHelper::getTaskOptions(); + + foreach ($items as $item) { + $item->taskOption = $taskOptions->findOption($item->type); + $item->safeTypeTitle = $item->taskOption->title ?? Text::_('JGLOBAL_NONAPPLICABLE'); + } + } + + /** + * Proxy for the parent method. + * Sets ordering defaults. + * + * @param string $ordering Field to order/sort list by + * @param string $direction Direction in which to sort list + * + * @return void + * @since 4.1.0 + */ + protected function populateState($ordering = 'a.title', $direction = 'ASC'): void + { + // Call the parent method + parent::populateState($ordering, $direction); + } } diff --git a/administrator/components/com_scheduler/src/Rule/ExecutionRulesRule.php b/administrator/components/com_scheduler/src/Rule/ExecutionRulesRule.php index 56f149993cb3a..294f0d4c19cd1 100644 --- a/administrator/components/com_scheduler/src/Rule/ExecutionRulesRule.php +++ b/administrator/components/com_scheduler/src/Rule/ExecutionRulesRule.php @@ -1,4 +1,5 @@ ` tag for the form - * field object. - * @param mixed $value The form field value to validate. - * @param ?string $group The field name group control value. This acts as an array container for the - * field. For example if the field has `name="foo"` and the group value is set - * to "bar" then the full field name would end up being "bar[foo]". - * @param ?Registry $input An optional Registry object with the entire data set to validate against - * the entire form. - * @param ?Form $form The form object for which the field is being tested. - * - * @return boolean - * - * @since 4.1.0 - */ - public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null): bool - { - $fieldName = (string) $element['name']; - $ruleType = $input->get(self::RULE_TYPE_FIELD); + /** + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form + * field object. + * @param mixed $value The form field value to validate. + * @param ?string $group The field name group control value. This acts as an array container for the + * field. For example if the field has `name="foo"` and the group value is set + * to "bar" then the full field name would end up being "bar[foo]". + * @param ?Registry $input An optional Registry object with the entire data set to validate against + * the entire form. + * @param ?Form $form The form object for which the field is being tested. + * + * @return boolean + * + * @since 4.1.0 + */ + public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null): bool + { + $fieldName = (string) $element['name']; + $ruleType = $input->get(self::RULE_TYPE_FIELD); - if ($ruleType === $fieldName || ($ruleType === 'custom' && $group === self::CUSTOM_RULE_GROUP)) - { - return $this->validateField($element, $value, $group, $form); - } + if ($ruleType === $fieldName || ($ruleType === 'custom' && $group === self::CUSTOM_RULE_GROUP)) { + return $this->validateField($element, $value, $group, $form); + } - return true; - } + return true; + } - /** - * @param \SimpleXMLElement $element The SimpleXMLElement for the field. - * @param mixed $value The field value. - * @param ?string $group The form field group the element belongs to. - * @param Form|null $form The Form object against which the field is tested/ - * - * @return boolean True if field is valid - * - * @since 4.1.0 - */ - private function validateField(\SimpleXMLElement $element, $value, ?string $group = null, ?Form $form = null): bool - { - $elementType = (string) $element['type']; + /** + * @param \SimpleXMLElement $element The SimpleXMLElement for the field. + * @param mixed $value The field value. + * @param ?string $group The form field group the element belongs to. + * @param Form|null $form The Form object against which the field is tested/ + * + * @return boolean True if field is valid + * + * @since 4.1.0 + */ + private function validateField(\SimpleXMLElement $element, $value, ?string $group = null, ?Form $form = null): bool + { + $elementType = (string) $element['type']; - // If element is of cron type, we test against options and return - if ($elementType === 'cron') - { - return (new OptionsRule)->test($element, $value, $group, null, $form); - } + // If element is of cron type, we test against options and return + if ($elementType === 'cron') { + return (new OptionsRule())->test($element, $value, $group, null, $form); + } - // Test for a positive integer value and return - return filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]); - } + // Test for a positive integer value and return + return filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]); + } } diff --git a/administrator/components/com_scheduler/src/Scheduler/Scheduler.php b/administrator/components/com_scheduler/src/Scheduler/Scheduler.php index ea1c68f8dc4f3..851b325ced560 100644 --- a/administrator/components/com_scheduler/src/Scheduler/Scheduler.php +++ b/administrator/components/com_scheduler/src/Scheduler/Scheduler.php @@ -1,4 +1,5 @@ 'COM_SCHEDULER_SCHEDULER_TASK_COMPLETE', - Status::WILL_RESUME => 'COM_SCHEDULER_SCHEDULER_TASK_WILL_RESUME', - Status::NO_LOCK => 'COM_SCHEDULER_SCHEDULER_TASK_LOCKED', - Status::NO_RUN => 'COM_SCHEDULER_SCHEDULER_TASK_UNLOCKED', - Status::NO_ROUTINE => 'COM_SCHEDULER_SCHEDULER_TASK_ROUTINE_NA', - ]; - - /** - * Filters for the task queue. Can be used with fetchTaskRecords(). - * - * @since 4.1.0 - * @todo remove? - */ - public const TASK_QUEUE_FILTERS = [ - 'due' => 1, - 'locked' => -1, - ]; - - /** - * List config for the task queue. Can be used with fetchTaskRecords(). - * - * @since 4.1.0 - * @todo remove? - */ - public const TASK_QUEUE_LIST_CONFIG = [ - 'multi_ordering' => ['a.priority DESC ', 'a.next_execution ASC'], - ]; - - /** - * Run a scheduled task. - * Runs a single due task from the task queue by default if $id and $title are not passed. - * - * @param array $options Array with options to configure the method's behavior. Supports: - * 1. `id`: (Optional) ID of the task to run. - * 2. `allowDisabled`: Allow running disabled tasks. - * 3. `allowConcurrent`: Allow concurrent execution, i.e., running the task when another - * task may be running. - * - * @return ?Task The task executed or null if not exists - * - * @since 4.1.0 - * @throws \RuntimeException - */ - public function runTask(array $options): ?Task - { - $resolver = new OptionsResolver; - - try - { - $this->configureTaskRunnerOptions($resolver); - } - catch (\Exception $e) - { - } - - try - { - $options = $resolver->resolve($options); - } - catch (\Exception $e) - { - if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) - { - throw $e; - } - } - - /** @var CMSApplication $app */ - $app = Factory::getApplication(); - - // ? Sure about inferring scheduling bypass? - $task = $this->getTask( - [ - 'id' => (int) $options['id'], - 'allowDisabled' => $options['allowDisabled'], - 'bypassScheduling' => (int) $options['id'] !== 0, - 'allowConcurrent' => $options['allowConcurrent'], - 'includeCliExclusive' => ($app->isClient('cli')), - ] - ); - - // ? Should this be logged? (probably, if an ID is passed?) - if (empty($task)) - { - return null; - } - - $app->getLanguage()->load('com_scheduler', JPATH_ADMINISTRATOR); - - $options['text_entry_format'] = '{DATE} {TIME} {PRIORITY} {MESSAGE}'; - $options['text_file'] = 'joomla_scheduler.php'; - Log::addLogger($options, Log::ALL, $task->logCategory); - - $taskId = $task->get('id'); - $taskTitle = $task->get('title'); - - $task->log(Text::sprintf('COM_SCHEDULER_SCHEDULER_TASK_START', $taskId, $taskTitle), 'info'); - - // Let's try to avoid time-outs - if (\function_exists('set_time_limit')) - { - set_time_limit(0); - } - - try - { - $task->run(); - } - catch (\Exception $e) - { - // We suppress the exception here, it's still accessible with `$task->getContent()['exception']`. - } - - $executionSnapshot = $task->getContent(); - $exitCode = $executionSnapshot['status'] ?? Status::NO_EXIT; - $netDuration = $executionSnapshot['netDuration'] ?? 0; - $duration = $executionSnapshot['duration'] ?? 0; - - if (\array_key_exists($exitCode, self::LOG_TEXT)) - { - $level = in_array($exitCode, [Status::OK, Status::WILL_RESUME]) ? 'info' : 'warning'; - $task->log(Text::sprintf(self::LOG_TEXT[$exitCode], $taskId, $duration, $netDuration), $level); - - return $task; - } - - $task->log( - Text::sprintf('COM_SCHEDULER_SCHEDULER_TASK_UNKNOWN_EXIT', $taskId, $duration, $netDuration, $exitCode), - 'warning' - ); - - return $task; - } - - /** - * Set up an {@see OptionsResolver} to resolve options compatible with {@see runTask}. - * - * @param OptionsResolver $resolver The {@see OptionsResolver} instance to set up. - * - * @return void - * - * @since 4.1.0 - * @throws AccessException - */ - protected function configureTaskRunnerOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults( - [ - 'id' => 0, - 'allowDisabled' => false, - 'allowConcurrent' => false, - ] - ) - ->setAllowedTypes('id', 'numeric') - ->setAllowedTypes('allowDisabled', 'bool') - ->setAllowedTypes('allowConcurrent', 'bool'); - } - - /** - * Get the next task which is due to run, limit to a specific task when ID is given - * - * @param array $options Options for the getter, see {@see TaskModel::getTask()}. - * ! should probably also support a non-locking getter. - * - * @return Task $task The task to execute - * - * @since 4.1.0 - * @throws \RuntimeException - */ - public function getTask(array $options = []): ?Task - { - $resolver = new OptionsResolver; - - try - { - TaskModel::configureTaskGetterOptions($resolver); - } - catch (\Exception $e) - { - } - - try - { - $options = $resolver->resolve($options); - } - catch (\Exception $e) - { - if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) - { - throw $e; - } - } - - try - { - /** @var SchedulerComponent $component */ - $component = Factory::getApplication()->bootComponent('com_scheduler'); - - /** @var TaskModel $model */ - $model = $component->getMVCFactory()->createModel('Task', 'Administrator', ['ignore_request' => true]); - } - catch (\Exception $e) - { - } - - if (!isset($model)) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $task = $model->getTask($options); - - if (empty($task)) - { - return null; - } - - return new Task($task); - } - - /** - * Fetches a single scheduled task in a Task instance. - * If no id or title is specified, a due task is returned. - * - * @param int $id The task ID. - * @param bool $allowDisabled Allow disabled/trashed tasks? - * - * @return ?object A matching task record, if it exists - * - * @since 4.1.0 - * @throws \RuntimeException - */ - public function fetchTaskRecord(int $id = 0, bool $allowDisabled = false): ?object - { - $filters = []; - $listConfig = ['limit' => 1]; - - if ($id > 0) - { - $filters['id'] = $id; - } - else - { - // Filters and list config for scheduled task queue - $filters['due'] = 1; - $filters['locked'] = -1; - $listConfig['multi_ordering'] = [ - 'a.priority DESC', - 'a.next_execution ASC', - ]; - } - - if ($allowDisabled) - { - $filters['state'] = ''; - } - - return $this->fetchTaskRecords($filters, $listConfig)[0] ?? null; - } - - /** - * @param array $filters The filters to set to the model - * @param array $listConfig The list config (ordering, etc.) to set to the model - * - * @return array - * - * @since 4.1.0 - * @throws \RunTimeException - */ - public function fetchTaskRecords(array $filters, array $listConfig): array - { - $model = null; - - try - { - /** @var SchedulerComponent $component */ - $component = Factory::getApplication()->bootComponent('com_scheduler'); - - /** @var TasksModel $model */ - $model = $component->getMVCFactory() - ->createModel('Tasks', 'Administrator', ['ignore_request' => true]); - } - catch (\Exception $e) - { - } - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $model->setState('list.select', 'a.*'); - - // Default to only enabled tasks - if (!isset($filters['state'])) - { - $model->setState('filter.state', 1); - } - - // Default to including orphaned tasks - $model->setState('filter.orphaned', 0); - - // Default to ordering by ID - $model->setState('list.ordering', 'a.id'); - $model->setState('list.direction', 'ASC'); - - // List options - foreach ($listConfig as $key => $value) - { - $model->setState('list.' . $key, $value); - } - - // Filter options - foreach ($filters as $type => $filter) - { - $model->setState('filter.' . $type, $filter); - } - - return $model->getItems() ?: []; - } + private const LOG_TEXT = [ + Status::OK => 'COM_SCHEDULER_SCHEDULER_TASK_COMPLETE', + Status::WILL_RESUME => 'COM_SCHEDULER_SCHEDULER_TASK_WILL_RESUME', + Status::NO_LOCK => 'COM_SCHEDULER_SCHEDULER_TASK_LOCKED', + Status::NO_RUN => 'COM_SCHEDULER_SCHEDULER_TASK_UNLOCKED', + Status::NO_ROUTINE => 'COM_SCHEDULER_SCHEDULER_TASK_ROUTINE_NA', + ]; + + /** + * Filters for the task queue. Can be used with fetchTaskRecords(). + * + * @since 4.1.0 + * @todo remove? + */ + public const TASK_QUEUE_FILTERS = [ + 'due' => 1, + 'locked' => -1, + ]; + + /** + * List config for the task queue. Can be used with fetchTaskRecords(). + * + * @since 4.1.0 + * @todo remove? + */ + public const TASK_QUEUE_LIST_CONFIG = [ + 'multi_ordering' => ['a.priority DESC ', 'a.next_execution ASC'], + ]; + + /** + * Run a scheduled task. + * Runs a single due task from the task queue by default if $id and $title are not passed. + * + * @param array $options Array with options to configure the method's behavior. Supports: + * 1. `id`: (Optional) ID of the task to run. + * 2. `allowDisabled`: Allow running disabled tasks. + * 3. `allowConcurrent`: Allow concurrent execution, i.e., running the task when another + * task may be running. + * + * @return ?Task The task executed or null if not exists + * + * @since 4.1.0 + * @throws \RuntimeException + */ + public function runTask(array $options): ?Task + { + $resolver = new OptionsResolver(); + + try { + $this->configureTaskRunnerOptions($resolver); + } catch (\Exception $e) { + } + + try { + $options = $resolver->resolve($options); + } catch (\Exception $e) { + if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) { + throw $e; + } + } + + /** @var CMSApplication $app */ + $app = Factory::getApplication(); + + // ? Sure about inferring scheduling bypass? + $task = $this->getTask( + [ + 'id' => (int) $options['id'], + 'allowDisabled' => $options['allowDisabled'], + 'bypassScheduling' => (int) $options['id'] !== 0, + 'allowConcurrent' => $options['allowConcurrent'], + 'includeCliExclusive' => ($app->isClient('cli')), + ] + ); + + // ? Should this be logged? (probably, if an ID is passed?) + if (empty($task)) { + return null; + } + + $app->getLanguage()->load('com_scheduler', JPATH_ADMINISTRATOR); + + $options['text_entry_format'] = '{DATE} {TIME} {PRIORITY} {MESSAGE}'; + $options['text_file'] = 'joomla_scheduler.php'; + Log::addLogger($options, Log::ALL, $task->logCategory); + + $taskId = $task->get('id'); + $taskTitle = $task->get('title'); + + $task->log(Text::sprintf('COM_SCHEDULER_SCHEDULER_TASK_START', $taskId, $taskTitle), 'info'); + + // Let's try to avoid time-outs + if (\function_exists('set_time_limit')) { + set_time_limit(0); + } + + try { + $task->run(); + } catch (\Exception $e) { + // We suppress the exception here, it's still accessible with `$task->getContent()['exception']`. + } + + $executionSnapshot = $task->getContent(); + $exitCode = $executionSnapshot['status'] ?? Status::NO_EXIT; + $netDuration = $executionSnapshot['netDuration'] ?? 0; + $duration = $executionSnapshot['duration'] ?? 0; + + if (\array_key_exists($exitCode, self::LOG_TEXT)) { + $level = in_array($exitCode, [Status::OK, Status::WILL_RESUME]) ? 'info' : 'warning'; + $task->log(Text::sprintf(self::LOG_TEXT[$exitCode], $taskId, $duration, $netDuration), $level); + + return $task; + } + + $task->log( + Text::sprintf('COM_SCHEDULER_SCHEDULER_TASK_UNKNOWN_EXIT', $taskId, $duration, $netDuration, $exitCode), + 'warning' + ); + + return $task; + } + + /** + * Set up an {@see OptionsResolver} to resolve options compatible with {@see runTask}. + * + * @param OptionsResolver $resolver The {@see OptionsResolver} instance to set up. + * + * @return void + * + * @since 4.1.0 + * @throws AccessException + */ + protected function configureTaskRunnerOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults( + [ + 'id' => 0, + 'allowDisabled' => false, + 'allowConcurrent' => false, + ] + ) + ->setAllowedTypes('id', 'numeric') + ->setAllowedTypes('allowDisabled', 'bool') + ->setAllowedTypes('allowConcurrent', 'bool'); + } + + /** + * Get the next task which is due to run, limit to a specific task when ID is given + * + * @param array $options Options for the getter, see {@see TaskModel::getTask()}. + * ! should probably also support a non-locking getter. + * + * @return Task $task The task to execute + * + * @since 4.1.0 + * @throws \RuntimeException + */ + public function getTask(array $options = []): ?Task + { + $resolver = new OptionsResolver(); + + try { + TaskModel::configureTaskGetterOptions($resolver); + } catch (\Exception $e) { + } + + try { + $options = $resolver->resolve($options); + } catch (\Exception $e) { + if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) { + throw $e; + } + } + + try { + /** @var SchedulerComponent $component */ + $component = Factory::getApplication()->bootComponent('com_scheduler'); + + /** @var TaskModel $model */ + $model = $component->getMVCFactory()->createModel('Task', 'Administrator', ['ignore_request' => true]); + } catch (\Exception $e) { + } + + if (!isset($model)) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $task = $model->getTask($options); + + if (empty($task)) { + return null; + } + + return new Task($task); + } + + /** + * Fetches a single scheduled task in a Task instance. + * If no id or title is specified, a due task is returned. + * + * @param int $id The task ID. + * @param bool $allowDisabled Allow disabled/trashed tasks? + * + * @return ?object A matching task record, if it exists + * + * @since 4.1.0 + * @throws \RuntimeException + */ + public function fetchTaskRecord(int $id = 0, bool $allowDisabled = false): ?object + { + $filters = []; + $listConfig = ['limit' => 1]; + + if ($id > 0) { + $filters['id'] = $id; + } else { + // Filters and list config for scheduled task queue + $filters['due'] = 1; + $filters['locked'] = -1; + $listConfig['multi_ordering'] = [ + 'a.priority DESC', + 'a.next_execution ASC', + ]; + } + + if ($allowDisabled) { + $filters['state'] = ''; + } + + return $this->fetchTaskRecords($filters, $listConfig)[0] ?? null; + } + + /** + * @param array $filters The filters to set to the model + * @param array $listConfig The list config (ordering, etc.) to set to the model + * + * @return array + * + * @since 4.1.0 + * @throws \RunTimeException + */ + public function fetchTaskRecords(array $filters, array $listConfig): array + { + $model = null; + + try { + /** @var SchedulerComponent $component */ + $component = Factory::getApplication()->bootComponent('com_scheduler'); + + /** @var TasksModel $model */ + $model = $component->getMVCFactory() + ->createModel('Tasks', 'Administrator', ['ignore_request' => true]); + } catch (\Exception $e) { + } + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $model->setState('list.select', 'a.*'); + + // Default to only enabled tasks + if (!isset($filters['state'])) { + $model->setState('filter.state', 1); + } + + // Default to including orphaned tasks + $model->setState('filter.orphaned', 0); + + // Default to ordering by ID + $model->setState('list.ordering', 'a.id'); + $model->setState('list.direction', 'ASC'); + + // List options + foreach ($listConfig as $key => $value) { + $model->setState('list.' . $key, $value); + } + + // Filter options + foreach ($filters as $type => $filter) { + $model->setState('filter.' . $type, $filter); + } + + return $model->getItems() ?: []; + } } diff --git a/administrator/components/com_scheduler/src/Table/TaskTable.php b/administrator/components/com_scheduler/src/Table/TaskTable.php index d06b91ec0b516..ac8533c6cc4c7 100644 --- a/administrator/components/com_scheduler/src/Table/TaskTable.php +++ b/administrator/components/com_scheduler/src/Table/TaskTable.php @@ -1,4 +1,5 @@ setColumnAlias('published', 'state'); - - parent::__construct('#__scheduler_tasks', 'id', $db); - } - - /** - * Overloads {@see Table::check()} to perform sanity checks on properties and make sure they're - * safe to store. - * - * @return boolean True if checks pass. - * - * @since 4.1.0 - * @throws \Exception - */ - public function check(): bool - { - try - { - parent::check(); - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage()); - - return false; - } - - $this->title = htmlspecialchars_decode($this->title, ENT_QUOTES); - - // Set created date if not set. - // ? Might not need since the constructor already sets this - if (!(int) $this->created) - { - $this->created = Factory::getDate()->toSql(); - } - - // @todo : Add more checks if needed - - return true; - } - - /** - * Override {@see Table::store()} to update null fields as a default, which is needed when DATETIME - * fields need to be updated to NULL. This override is needed because {@see AdminModel::save()} does not - * expose an option to pass true to Table::store(). Also ensures the `created` and `created_by` fields are - * set. - * - * @param boolean $updateNulls True to update fields even if they're null. - * - * @return boolean True if successful. - * - * @since 4.1.0 - * @throws \Exception - */ - public function store($updateNulls = true): bool - { - $isNew = empty($this->getId()); - - // Set creation date if not set for a new item. - if ($isNew && empty($this->created)) - { - $this->created = Factory::getDate()->toSql(); - } - - // Set `created_by` if not set for a new item. - if ($isNew && empty($this->created_by)) - { - $this->created_by = Factory::getApplication()->getIdentity()->id; - } - - // @todo : Should we add modified, modified_by fields? [ ] - - return parent::store($updateNulls); - } - - /** - * Returns the asset name of the entry as it appears in the {@see Asset} table. - * - * @return string The asset name. - * - * @since 4.1.0 - */ - protected function _getAssetName(): string - { - $k = $this->_tbl_key; - - return 'com_scheduler.task.' . (int) $this->$k; - } - - /** - * Override {@see Table::bind()} to bind some fields even if they're null given they're present in $src. - * This override is needed specifically for DATETIME fields, of which the `next_execution` field is updated to - * null if a task is configured to execute only on manual trigger. - * - * @param array|object $src An associative array or object to bind to the Table instance. - * @param array|string $ignore An optional array or space separated list of properties to ignore while binding. - * - * @return boolean - * - * @since 4.1.0 - */ - public function bind($src, $ignore = array()): bool - { - $fields = ['next_execution']; - - foreach ($fields as $field) - { - if (\array_key_exists($field, $src) && \is_null($src[$field])) - { - $this->$field = $src[$field]; - } - } - - return parent::bind($src, $ignore); - } - - /** - * Release pseudo-locks on a set of task records. If an empty set is passed, this method releases lock on its - * instance primary key, if available. - * - * @param integer[] $pks An optional array of primary key values to update. If not set the instance property - * value is used. - * @param ?int $userId ID of the user unlocking the tasks. - * - * @return boolean True on success; false if $pks is empty. - * - * @since 4.1.0 - * @throws QueryTypeAlreadyDefinedException|\UnexpectedValueException|\BadMethodCallException - */ - public function unlock(array $pks = [], ?int $userId = null): bool - { - // Pre-processing by observers - $event = AbstractEvent::create( - 'onTaskBeforeUnlock', - [ - 'subject' => $this, - 'pks' => $pks, - 'userId' => $userId, - ] - ); - - $this->getDispatcher()->dispatch('onTaskBeforeUnlock', $event); - - // Some pre-processing before we can work with the keys. - if (!empty($pks)) - { - foreach ($pks as $key => $pk) - { - if (!\is_array($pk)) - { - $pks[$key] = array($this->_tbl_key => $pk); - } - } - } - - // If there are no primary keys set check to see if the instance key is set and use that. - if (empty($pks)) - { - $pk = []; - - foreach ($this->_tbl_keys as $key) - { - if ($this->$key) - { - $pk[$key] = $this->$key; - } - // We don't have a full primary key - return false. - else - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_NO_ROWS_SELECTED')); - - return false; - } - } - - $pks = [$pk]; - } - - $lockedField = $this->getColumnAlias('locked'); - - foreach ($pks as $pk) - { - // Update the publishing state for rows with the given primary keys. - $query = $this->_db->getQuery(true) - ->update($this->_tbl) - ->set($this->_db->quoteName($lockedField) . ' = NULL'); - - // Build the WHERE clause for the primary keys. - $this->appendPrimaryKeys($query, $pk); - - $this->_db->setQuery($query); - - try - { - $this->_db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // If the Table instance value is in the list of primary keys that were set, set the instance. - $ours = true; - - foreach ($this->_tbl_keys as $key) - { - if ($this->$key != $pk[$key]) - { - $ours = false; - } - } - - if ($ours) - { - $this->$lockedField = null; - } - } - - // Pre-processing by observers - $event = AbstractEvent::create( - 'onTaskAfterUnlock', - [ - 'subject' => $this, - 'pks' => $pks, - 'userId' => $userId, - ] - ); - - $this->getDispatcher()->dispatch('onTaskAfterUnlock', $event); - - return true; - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.1.1 + */ + protected $_supportNullValue = true; + + /** + * Ensure params are json encoded by the bind method. + * + * @var string[] + * @since 4.1.0 + */ + protected $_jsonEncode = ['params', 'execution_rules', 'cron_rules']; + + /** + * The 'created' column. + * + * @var string + * @since 4.1.0 + */ + public $created; + + /** + * The 'title' column. + * + * @var string + * @since 4.1.0 + */ + public $title; + + /** + * @var string + * @since 4.1.0 + */ + public $typeAlias = 'com_scheduler.task'; + + /** + * TaskTable constructor override, needed to pass the DB table name and primary key to {@see Table::__construct()}. + * + * @param DatabaseDriver $db A database connector object. + * + * @since 4.1.0 + */ + public function __construct(DatabaseDriver $db) + { + $this->setColumnAlias('published', 'state'); + + parent::__construct('#__scheduler_tasks', 'id', $db); + } + + /** + * Overloads {@see Table::check()} to perform sanity checks on properties and make sure they're + * safe to store. + * + * @return boolean True if checks pass. + * + * @since 4.1.0 + * @throws \Exception + */ + public function check(): bool + { + try { + parent::check(); + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage()); + + return false; + } + + $this->title = htmlspecialchars_decode($this->title, ENT_QUOTES); + + // Set created date if not set. + // ? Might not need since the constructor already sets this + if (!(int) $this->created) { + $this->created = Factory::getDate()->toSql(); + } + + // @todo : Add more checks if needed + + return true; + } + + /** + * Override {@see Table::store()} to update null fields as a default, which is needed when DATETIME + * fields need to be updated to NULL. This override is needed because {@see AdminModel::save()} does not + * expose an option to pass true to Table::store(). Also ensures the `created` and `created_by` fields are + * set. + * + * @param boolean $updateNulls True to update fields even if they're null. + * + * @return boolean True if successful. + * + * @since 4.1.0 + * @throws \Exception + */ + public function store($updateNulls = true): bool + { + $isNew = empty($this->getId()); + + // Set creation date if not set for a new item. + if ($isNew && empty($this->created)) { + $this->created = Factory::getDate()->toSql(); + } + + // Set `created_by` if not set for a new item. + if ($isNew && empty($this->created_by)) { + $this->created_by = Factory::getApplication()->getIdentity()->id; + } + + // @todo : Should we add modified, modified_by fields? [ ] + + return parent::store($updateNulls); + } + + /** + * Returns the asset name of the entry as it appears in the {@see Asset} table. + * + * @return string The asset name. + * + * @since 4.1.0 + */ + protected function _getAssetName(): string + { + $k = $this->_tbl_key; + + return 'com_scheduler.task.' . (int) $this->$k; + } + + /** + * Override {@see Table::bind()} to bind some fields even if they're null given they're present in $src. + * This override is needed specifically for DATETIME fields, of which the `next_execution` field is updated to + * null if a task is configured to execute only on manual trigger. + * + * @param array|object $src An associative array or object to bind to the Table instance. + * @param array|string $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return boolean + * + * @since 4.1.0 + */ + public function bind($src, $ignore = array()): bool + { + $fields = ['next_execution']; + + foreach ($fields as $field) { + if (\array_key_exists($field, $src) && \is_null($src[$field])) { + $this->$field = $src[$field]; + } + } + + return parent::bind($src, $ignore); + } + + /** + * Release pseudo-locks on a set of task records. If an empty set is passed, this method releases lock on its + * instance primary key, if available. + * + * @param integer[] $pks An optional array of primary key values to update. If not set the instance property + * value is used. + * @param ?int $userId ID of the user unlocking the tasks. + * + * @return boolean True on success; false if $pks is empty. + * + * @since 4.1.0 + * @throws QueryTypeAlreadyDefinedException|\UnexpectedValueException|\BadMethodCallException + */ + public function unlock(array $pks = [], ?int $userId = null): bool + { + // Pre-processing by observers + $event = AbstractEvent::create( + 'onTaskBeforeUnlock', + [ + 'subject' => $this, + 'pks' => $pks, + 'userId' => $userId, + ] + ); + + $this->getDispatcher()->dispatch('onTaskBeforeUnlock', $event); + + // Some pre-processing before we can work with the keys. + if (!empty($pks)) { + foreach ($pks as $key => $pk) { + if (!\is_array($pk)) { + $pks[$key] = array($this->_tbl_key => $pk); + } + } + } + + // If there are no primary keys set check to see if the instance key is set and use that. + if (empty($pks)) { + $pk = []; + + foreach ($this->_tbl_keys as $key) { + if ($this->$key) { + $pk[$key] = $this->$key; + } else { + // We don't have a full primary key - return false. + $this->setError(Text::_('JLIB_DATABASE_ERROR_NO_ROWS_SELECTED')); + + return false; + } + } + + $pks = [$pk]; + } + + $lockedField = $this->getColumnAlias('locked'); + + foreach ($pks as $pk) { + // Update the publishing state for rows with the given primary keys. + $query = $this->_db->getQuery(true) + ->update($this->_tbl) + ->set($this->_db->quoteName($lockedField) . ' = NULL'); + + // Build the WHERE clause for the primary keys. + $this->appendPrimaryKeys($query, $pk); + + $this->_db->setQuery($query); + + try { + $this->_db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // If the Table instance value is in the list of primary keys that were set, set the instance. + $ours = true; + + foreach ($this->_tbl_keys as $key) { + if ($this->$key != $pk[$key]) { + $ours = false; + } + } + + if ($ours) { + $this->$lockedField = null; + } + } + + // Pre-processing by observers + $event = AbstractEvent::create( + 'onTaskAfterUnlock', + [ + 'subject' => $this, + 'pks' => $pks, + 'userId' => $userId, + ] + ); + + $this->getDispatcher()->dispatch('onTaskAfterUnlock', $event); + + return true; + } } diff --git a/administrator/components/com_scheduler/src/Task/Status.php b/administrator/components/com_scheduler/src/Task/Status.php index 50f4673577deb..da35c8479c86d 100644 --- a/administrator/components/com_scheduler/src/Task/Status.php +++ b/administrator/components/com_scheduler/src/Task/Status.php @@ -1,4 +1,5 @@ 'trashed', - self::STATE_DISABLED => 'disabled', - self::STATE_ENABLED => 'enabled', - ]; - - /** - * The task snapshot - * - * @var array - * @since 4.1.0 - */ - protected $snapshot = []; - - /** - * @var Registry - * @since 4.1.0 - */ - protected $taskRegistry; - - /** - * @var string - * @since 4.1.0 - */ - public $logCategory; - - /** - * @var CMSApplication - * @since 4.1.0 - */ - protected $app; - - /** - * @var DatabaseInterface - * @since 4.1.0 - */ - protected $db; - - /** - * Maps task exit codes to events which should be dispatched when the task finishes. - * 'NA' maps to the event for general task failures. - * - * @var string[] - * @since 4.1.0 - */ - protected const EVENTS_MAP = [ - Status::OK => 'onTaskExecuteSuccess', - Status::NO_ROUTINE => 'onTaskRoutineNotFound', - Status::WILL_RESUME => 'onTaskRoutineWillResume', - 'NA' => 'onTaskExecuteFailure', - ]; - - /** - * Constructor for {@see Task}. - * - * @param object $record A task from {@see TaskTable}. - * - * @since 4.1.0 - * @throws \Exception - */ - public function __construct(object $record) - { - // Workaround because Registry dumps private properties otherwise. - $taskOption = $record->taskOption; - $record->params = json_decode($record->params, true); - - $this->taskRegistry = new Registry($record); - - $this->set('taskOption', $taskOption); - $this->app = Factory::getApplication(); - $this->db = Factory::getContainer()->get(DatabaseDriver::class); - $this->setLogger(Log::createDelegatedLogger()); - $this->logCategory = 'task' . $this->get('id'); - - if ($this->get('params.individual_log')) - { - $logFile = $this->get('params.log_file') ?? 'task_' . $this->get('id') . '.log.php'; - - $options['text_entry_format'] = '{DATE} {TIME} {PRIORITY} {MESSAGE}'; - $options['text_file'] = $logFile; - Log::addLogger($options, Log::ALL, [$this->logCategory]); - } - } - - /** - * Get the task as a data object that can be stored back in the database. - * ! This method should be removed or changed as part of a better API implementation for the driver. - * - * @return object - * - * @since 4.1.0 - */ - public function getRecord(): object - { - // ! Probably, an array instead - $recObject = $this->taskRegistry->toObject(); - - $recObject->cron_rules = (array) $recObject->cron_rules; - - return $recObject; - } - - /** - * Execute the task. - * - * @return boolean True if success - * - * @since 4.1.0 - * @throws \Exception - */ - public function run(): bool - { - /** - * We try to acquire the lock here, only if we don't already have one. - * We do this, so we can support two ways of running tasks: - * 1. Directly through {@see Scheduler}, which optimises acquiring a lock while fetching from the task queue. - * 2. Running a task without a pre-acquired lock. - * ! This needs some more thought, for whether it should be allowed or if the single-query optimisation - * should be used everywhere, although it doesn't make sense in the context of fetching - * a task when it doesn't need to be run. This might be solved if we force a re-fetch - * with the lock or do it here ourselves (using acquireLock as a proxy to the model's - * getter). - */ - if ($this->get('locked') === null) - { - $this->acquireLock(); - } - - // Exit early if task routine is not available - if (!SchedulerHelper::getTaskOptions()->findOption($this->get('type'))) - { - $this->snapshot['status'] = Status::NO_ROUTINE; - $this->skipExecution(); - $this->dispatchExitEvent(); - - return $this->isSuccess(); - } - - $this->snapshot['status'] = Status::RUNNING; - $this->snapshot['taskStart'] = $this->snapshot['taskStart'] ?? microtime(true); - $this->snapshot['netDuration'] = 0; - - /** @var ExecuteTaskEvent $event */ - $event = AbstractEvent::create( - 'onExecuteTask', - [ - 'eventClass' => ExecuteTaskEvent::class, - 'subject' => $this, - 'routineId' => $this->get('type'), - 'langConstPrefix' => $this->get('taskOption')->langConstPrefix, - 'params' => $this->get('params'), - ] - ); - - PluginHelper::importPlugin('task'); - - try - { - $this->app->getDispatcher()->dispatch('onExecuteTask', $event); - } - catch (\Exception $e) - { - // Suppress the exception for now, we'll throw it again once it's safe - $this->log(Text::sprintf('COM_SCHEDULER_TASK_ROUTINE_EXCEPTION', $e->getMessage()), 'error'); - $this->snapshot['exception'] = $e; - $this->snapshot['status'] = Status::KNOCKOUT; - } - - $resultSnapshot = $event->getResultSnapshot(); - - $this->snapshot['taskEnd'] = microtime(true); - $this->snapshot['netDuration'] = $this->snapshot['taskEnd'] - $this->snapshot['taskStart']; - $this->snapshot = array_merge($this->snapshot, $resultSnapshot); - - // @todo make the ExecRuleHelper usage less ugly, perhaps it should be composed into Task - // Update object state. - $this->set('last_execution', Factory::getDate('@' . (int) $this->snapshot['taskStart'])->toSql()); - $this->set('last_exit_code', $this->snapshot['status']); - - if ($this->snapshot['status'] !== Status::WILL_RESUME) - { - $this->set('next_execution', (new ExecRuleHelper($this->taskRegistry->toObject()))->nextExec()); - $this->set('times_executed', $this->get('times_executed') + 1); - } - else - { - /** - * Resumable tasks need special handling. - * - * They are rescheduled as soon as possible to let their next step to be executed without - * a very large temporal gap to the previous step. - * - * Moreover, the times executed does NOT increase for each step. It will increase once, - * after the last step, when they return Status::OK. - */ - $this->set('next_execution', Factory::getDate('now', 'UTC')->sub(new \DateInterval('PT1M'))->toSql()); - } - - // The only acceptable "successful" statuses are either clean exit or resuming execution. - if (!in_array($this->snapshot['status'], [Status::WILL_RESUME, Status::OK])) - { - $this->set('times_failed', $this->get('times_failed') + 1); - } - - if (!$this->releaseLock()) - { - $this->snapshot['status'] = Status::NO_RELEASE; - } - - $this->dispatchExitEvent(); - - if (!empty($this->snapshot['exception'])) - { - throw $this->snapshot['exception']; - } - - return $this->isSuccess(); - } - - /** - * Get the task execution snapshot. - * ! Access locations will need updates once a more robust Snapshot container is implemented. - * - * @return array - * - * @since 4.1.0 - */ - public function getContent(): array - { - return $this->snapshot; - } - - /** - * Acquire a pseudo-lock on the task record. - * ! At the moment, this method is not used anywhere as task locks are already - * acquired when they're fetched. As such this method is not functional and should - * not be reviewed until it is updated. - * - * @return boolean - * - * @since 4.1.0 - * @throws \Exception - */ - public function acquireLock(): bool - { - $db = $this->db; - $query = $db->getQuery(true); - $id = $this->get('id'); - $now = Factory::getDate('now', 'GMT'); - - $timeout = ComponentHelper::getParams('com_scheduler')->get('timeout', 300); - $timeout = new \DateInterval(sprintf('PT%dS', $timeout)); - $timeoutThreshold = (clone $now)->sub($timeout)->toSql(); - $now = $now->toSql(); - - // @todo update or remove this method - $query->update($db->qn('#__scheduler_tasks')) - ->set('locked = :now') - ->where($db->qn('id') . ' = :taskId') - ->extendWhere( - 'AND', - [ - $db->qn('locked') . ' < :threshold', - $db->qn('locked') . 'IS NULL', - ], - 'OR' - ) - ->bind(':taskId', $id, ParameterType::INTEGER) - ->bind(':now', $now) - ->bind(':threshold', $timeoutThreshold); - - try - { - $db->lockTable('#__scheduler_tasks'); - $db->setQuery($query)->execute(); - } - catch (\RuntimeException $e) - { - return false; - } - finally - { - $db->unlockTables(); - } - - if ($db->getAffectedRows() === 0) - { - return false; - } - - $this->set('locked', $now); - - return true; - } - - /** - * Remove the pseudo-lock and optionally update the task record. - * - * @param bool $update If true, the record is updated with the snapshot - * - * @return boolean - * - * @since 4.1.0 - * @throws \Exception - */ - public function releaseLock(bool $update = true): bool - { - $db = $this->db; - $query = $db->getQuery(true); - $id = $this->get('id'); - - $query->update($db->qn('#__scheduler_tasks', 't')) - ->set('locked = NULL') - ->where($db->qn('id') . ' = :taskId') - ->where($db->qn('locked') . ' IS NOT NULL') - ->bind(':taskId', $id, ParameterType::INTEGER); - - if ($update) - { - $exitCode = $this->get('last_exit_code'); - $lastExec = $this->get('last_execution'); - $nextExec = $this->get('next_execution'); - $timesFailed = $this->get('times_failed'); - $timesExecuted = $this->get('times_executed'); - - $query->set( - [ - 'last_exit_code = :exitCode', - 'last_execution = :lastExec', - 'next_execution = :nextExec', - 'times_executed = :times_executed', - 'times_failed = :times_failed', - ] - ) - ->bind(':exitCode', $exitCode, ParameterType::INTEGER) - ->bind(':lastExec', $lastExec) - ->bind(':nextExec', $nextExec) - ->bind(':times_executed', $timesExecuted) - ->bind(':times_failed', $timesFailed); - } - - try - { - $db->setQuery($query)->execute(); - } - catch (\RuntimeException $e) - { - return false; - } - - if (!$db->getAffectedRows()) - { - return false; - } - - $this->set('locked', null); - - return true; - } - - /** - * @param string $message Log message - * @param string $priority Log level, defaults to 'info' - * - * @return void - * - * @since 4.1.0 - * @throws InvalidArgumentException - */ - public function log(string $message, string $priority = 'info'): void - { - $this->logger->log($priority, $message, ['category' => $this->logCategory]); - } - - /** - * Advance the task entry's next calculated execution, effectively skipping the current execution. - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - public function skipExecution(): void - { - $db = $this->db; - $query = $db->getQuery(true); - - $id = $this->get('id'); - $nextExec = (new ExecRuleHelper($this->taskRegistry->toObject()))->nextExec(true, true); - - $query->update($db->qn('#__scheduler_tasks', 't')) - ->set('t.next_execution = :nextExec') - ->where('t.id = :id') - ->bind(':nextExec', $nextExec) - ->bind(':id', $id); - - try - { - $db->setQuery($query)->execute(); - } - catch (\RuntimeException $e) - { - } - - $this->set('next_execution', $nextExec); - } - - /** - * Handles task exit (dispatch event). - * - * @return void - * - * @since 4.1.0 - * - * @throws \UnexpectedValueException|\BadMethodCallException - */ - protected function dispatchExitEvent(): void - { - $exitCode = $this->snapshot['status'] ?? 'NA'; - $eventName = self::EVENTS_MAP[$exitCode] ?? self::EVENTS_MAP['NA']; - - $event = AbstractEvent::create( - $eventName, - [ - 'subject' => $this, - ] - ); - - $this->app->getDispatcher()->dispatch($eventName, $event); - } - - /** - * Was the task successful? - * - * @return boolean True if the task was successful. - * @since 4.1.0 - */ - public function isSuccess(): bool - { - return in_array(($this->snapshot['status'] ?? null), [Status::OK, Status::WILL_RESUME]); - } - - /** - * Set a task property. This method is a proxy to {@see Registry::set()}. - * - * @param string $path Registry path of the task property. - * @param mixed $value The value to set to the property. - * @param ?string $separator The key separator. - * - * @return mixed|null - * - * @since 4.1.0 - */ - protected function set(string $path, $value, string $separator = null) - { - return $this->taskRegistry->set($path, $value, $separator); - } - - /** - * Get a task property. This method is a proxy to {@see Registry::get()}. - * - * @param string $path Registry path of the task property. - * @param mixed $default Default property to return, if the actual value is null. - * - * @return mixed The task property. - * - * @since 4.1.0 - */ - public function get(string $path, $default = null) - { - return $this->taskRegistry->get($path, $default); - } - - /** - * Static method to determine whether an enumerated task state (as a string) is valid. - * - * @param string $state The task state (enumerated, as a string). - * - * @return boolean - * - * @since 4.1.0 - */ - public static function isValidState(string $state): bool - { - if (!is_numeric($state)) - { - return false; - } - - // Takes care of interpreting as float/int - $state = $state + 0; - - return ArrayHelper::getValue(self::STATE_MAP, $state) !== null; - } - - /** - * Static method to determine whether a task id is valid. Note that this does not - * validate ids against the database, but only verifies that an id may exist. - * - * @param string $id The task id (as a string). - * - * @return boolean - * - * @since 4.1.0 - */ - public static function isValidId(string $id): bool - { - $id = is_numeric($id) ? ($id + 0) : $id; - - if (!\is_int($id) || $id <= 0) - { - return false; - } - - return true; - } + use LoggerAwareTrait; + + /** + * Enumerated state for enabled tasks. + * + * @since 4.1.0 + */ + public const STATE_ENABLED = 1; + + /** + * Enumerated state for disabled tasks. + * + * @since 4.1.0 + */ + public const STATE_DISABLED = 0; + + /** + * Enumerated state for trashed tasks. + * + * @since 4.1.0 + */ + public const STATE_TRASHED = -2; + + /** + * Map state enumerations to logical language adjectives. + * + * @since 4.1.0 + */ + public const STATE_MAP = [ + self::STATE_TRASHED => 'trashed', + self::STATE_DISABLED => 'disabled', + self::STATE_ENABLED => 'enabled', + ]; + + /** + * The task snapshot + * + * @var array + * @since 4.1.0 + */ + protected $snapshot = []; + + /** + * @var Registry + * @since 4.1.0 + */ + protected $taskRegistry; + + /** + * @var string + * @since 4.1.0 + */ + public $logCategory; + + /** + * @var CMSApplication + * @since 4.1.0 + */ + protected $app; + + /** + * @var DatabaseInterface + * @since 4.1.0 + */ + protected $db; + + /** + * Maps task exit codes to events which should be dispatched when the task finishes. + * 'NA' maps to the event for general task failures. + * + * @var string[] + * @since 4.1.0 + */ + protected const EVENTS_MAP = [ + Status::OK => 'onTaskExecuteSuccess', + Status::NO_ROUTINE => 'onTaskRoutineNotFound', + Status::WILL_RESUME => 'onTaskRoutineWillResume', + 'NA' => 'onTaskExecuteFailure', + ]; + + /** + * Constructor for {@see Task}. + * + * @param object $record A task from {@see TaskTable}. + * + * @since 4.1.0 + * @throws \Exception + */ + public function __construct(object $record) + { + // Workaround because Registry dumps private properties otherwise. + $taskOption = $record->taskOption; + $record->params = json_decode($record->params, true); + + $this->taskRegistry = new Registry($record); + + $this->set('taskOption', $taskOption); + $this->app = Factory::getApplication(); + $this->db = Factory::getContainer()->get(DatabaseDriver::class); + $this->setLogger(Log::createDelegatedLogger()); + $this->logCategory = 'task' . $this->get('id'); + + if ($this->get('params.individual_log')) { + $logFile = $this->get('params.log_file') ?? 'task_' . $this->get('id') . '.log.php'; + + $options['text_entry_format'] = '{DATE} {TIME} {PRIORITY} {MESSAGE}'; + $options['text_file'] = $logFile; + Log::addLogger($options, Log::ALL, [$this->logCategory]); + } + } + + /** + * Get the task as a data object that can be stored back in the database. + * ! This method should be removed or changed as part of a better API implementation for the driver. + * + * @return object + * + * @since 4.1.0 + */ + public function getRecord(): object + { + // ! Probably, an array instead + $recObject = $this->taskRegistry->toObject(); + + $recObject->cron_rules = (array) $recObject->cron_rules; + + return $recObject; + } + + /** + * Execute the task. + * + * @return boolean True if success + * + * @since 4.1.0 + * @throws \Exception + */ + public function run(): bool + { + /** + * We try to acquire the lock here, only if we don't already have one. + * We do this, so we can support two ways of running tasks: + * 1. Directly through {@see Scheduler}, which optimises acquiring a lock while fetching from the task queue. + * 2. Running a task without a pre-acquired lock. + * ! This needs some more thought, for whether it should be allowed or if the single-query optimisation + * should be used everywhere, although it doesn't make sense in the context of fetching + * a task when it doesn't need to be run. This might be solved if we force a re-fetch + * with the lock or do it here ourselves (using acquireLock as a proxy to the model's + * getter). + */ + if ($this->get('locked') === null) { + $this->acquireLock(); + } + + // Exit early if task routine is not available + if (!SchedulerHelper::getTaskOptions()->findOption($this->get('type'))) { + $this->snapshot['status'] = Status::NO_ROUTINE; + $this->skipExecution(); + $this->dispatchExitEvent(); + + return $this->isSuccess(); + } + + $this->snapshot['status'] = Status::RUNNING; + $this->snapshot['taskStart'] = $this->snapshot['taskStart'] ?? microtime(true); + $this->snapshot['netDuration'] = 0; + + /** @var ExecuteTaskEvent $event */ + $event = AbstractEvent::create( + 'onExecuteTask', + [ + 'eventClass' => ExecuteTaskEvent::class, + 'subject' => $this, + 'routineId' => $this->get('type'), + 'langConstPrefix' => $this->get('taskOption')->langConstPrefix, + 'params' => $this->get('params'), + ] + ); + + PluginHelper::importPlugin('task'); + + try { + $this->app->getDispatcher()->dispatch('onExecuteTask', $event); + } catch (\Exception $e) { + // Suppress the exception for now, we'll throw it again once it's safe + $this->log(Text::sprintf('COM_SCHEDULER_TASK_ROUTINE_EXCEPTION', $e->getMessage()), 'error'); + $this->snapshot['exception'] = $e; + $this->snapshot['status'] = Status::KNOCKOUT; + } + + $resultSnapshot = $event->getResultSnapshot(); + + $this->snapshot['taskEnd'] = microtime(true); + $this->snapshot['netDuration'] = $this->snapshot['taskEnd'] - $this->snapshot['taskStart']; + $this->snapshot = array_merge($this->snapshot, $resultSnapshot); + + // @todo make the ExecRuleHelper usage less ugly, perhaps it should be composed into Task + // Update object state. + $this->set('last_execution', Factory::getDate('@' . (int) $this->snapshot['taskStart'])->toSql()); + $this->set('last_exit_code', $this->snapshot['status']); + + if ($this->snapshot['status'] !== Status::WILL_RESUME) { + $this->set('next_execution', (new ExecRuleHelper($this->taskRegistry->toObject()))->nextExec()); + $this->set('times_executed', $this->get('times_executed') + 1); + } else { + /** + * Resumable tasks need special handling. + * + * They are rescheduled as soon as possible to let their next step to be executed without + * a very large temporal gap to the previous step. + * + * Moreover, the times executed does NOT increase for each step. It will increase once, + * after the last step, when they return Status::OK. + */ + $this->set('next_execution', Factory::getDate('now', 'UTC')->sub(new \DateInterval('PT1M'))->toSql()); + } + + // The only acceptable "successful" statuses are either clean exit or resuming execution. + if (!in_array($this->snapshot['status'], [Status::WILL_RESUME, Status::OK])) { + $this->set('times_failed', $this->get('times_failed') + 1); + } + + if (!$this->releaseLock()) { + $this->snapshot['status'] = Status::NO_RELEASE; + } + + $this->dispatchExitEvent(); + + if (!empty($this->snapshot['exception'])) { + throw $this->snapshot['exception']; + } + + return $this->isSuccess(); + } + + /** + * Get the task execution snapshot. + * ! Access locations will need updates once a more robust Snapshot container is implemented. + * + * @return array + * + * @since 4.1.0 + */ + public function getContent(): array + { + return $this->snapshot; + } + + /** + * Acquire a pseudo-lock on the task record. + * ! At the moment, this method is not used anywhere as task locks are already + * acquired when they're fetched. As such this method is not functional and should + * not be reviewed until it is updated. + * + * @return boolean + * + * @since 4.1.0 + * @throws \Exception + */ + public function acquireLock(): bool + { + $db = $this->db; + $query = $db->getQuery(true); + $id = $this->get('id'); + $now = Factory::getDate('now', 'GMT'); + + $timeout = ComponentHelper::getParams('com_scheduler')->get('timeout', 300); + $timeout = new \DateInterval(sprintf('PT%dS', $timeout)); + $timeoutThreshold = (clone $now)->sub($timeout)->toSql(); + $now = $now->toSql(); + + // @todo update or remove this method + $query->update($db->qn('#__scheduler_tasks')) + ->set('locked = :now') + ->where($db->qn('id') . ' = :taskId') + ->extendWhere( + 'AND', + [ + $db->qn('locked') . ' < :threshold', + $db->qn('locked') . 'IS NULL', + ], + 'OR' + ) + ->bind(':taskId', $id, ParameterType::INTEGER) + ->bind(':now', $now) + ->bind(':threshold', $timeoutThreshold); + + try { + $db->lockTable('#__scheduler_tasks'); + $db->setQuery($query)->execute(); + } catch (\RuntimeException $e) { + return false; + } finally { + $db->unlockTables(); + } + + if ($db->getAffectedRows() === 0) { + return false; + } + + $this->set('locked', $now); + + return true; + } + + /** + * Remove the pseudo-lock and optionally update the task record. + * + * @param bool $update If true, the record is updated with the snapshot + * + * @return boolean + * + * @since 4.1.0 + * @throws \Exception + */ + public function releaseLock(bool $update = true): bool + { + $db = $this->db; + $query = $db->getQuery(true); + $id = $this->get('id'); + + $query->update($db->qn('#__scheduler_tasks', 't')) + ->set('locked = NULL') + ->where($db->qn('id') . ' = :taskId') + ->where($db->qn('locked') . ' IS NOT NULL') + ->bind(':taskId', $id, ParameterType::INTEGER); + + if ($update) { + $exitCode = $this->get('last_exit_code'); + $lastExec = $this->get('last_execution'); + $nextExec = $this->get('next_execution'); + $timesFailed = $this->get('times_failed'); + $timesExecuted = $this->get('times_executed'); + + $query->set( + [ + 'last_exit_code = :exitCode', + 'last_execution = :lastExec', + 'next_execution = :nextExec', + 'times_executed = :times_executed', + 'times_failed = :times_failed', + ] + ) + ->bind(':exitCode', $exitCode, ParameterType::INTEGER) + ->bind(':lastExec', $lastExec) + ->bind(':nextExec', $nextExec) + ->bind(':times_executed', $timesExecuted) + ->bind(':times_failed', $timesFailed); + } + + try { + $db->setQuery($query)->execute(); + } catch (\RuntimeException $e) { + return false; + } + + if (!$db->getAffectedRows()) { + return false; + } + + $this->set('locked', null); + + return true; + } + + /** + * @param string $message Log message + * @param string $priority Log level, defaults to 'info' + * + * @return void + * + * @since 4.1.0 + * @throws InvalidArgumentException + */ + public function log(string $message, string $priority = 'info'): void + { + $this->logger->log($priority, $message, ['category' => $this->logCategory]); + } + + /** + * Advance the task entry's next calculated execution, effectively skipping the current execution. + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + public function skipExecution(): void + { + $db = $this->db; + $query = $db->getQuery(true); + + $id = $this->get('id'); + $nextExec = (new ExecRuleHelper($this->taskRegistry->toObject()))->nextExec(true, true); + + $query->update($db->qn('#__scheduler_tasks', 't')) + ->set('t.next_execution = :nextExec') + ->where('t.id = :id') + ->bind(':nextExec', $nextExec) + ->bind(':id', $id); + + try { + $db->setQuery($query)->execute(); + } catch (\RuntimeException $e) { + } + + $this->set('next_execution', $nextExec); + } + + /** + * Handles task exit (dispatch event). + * + * @return void + * + * @since 4.1.0 + * + * @throws \UnexpectedValueException|\BadMethodCallException + */ + protected function dispatchExitEvent(): void + { + $exitCode = $this->snapshot['status'] ?? 'NA'; + $eventName = self::EVENTS_MAP[$exitCode] ?? self::EVENTS_MAP['NA']; + + $event = AbstractEvent::create( + $eventName, + [ + 'subject' => $this, + ] + ); + + $this->app->getDispatcher()->dispatch($eventName, $event); + } + + /** + * Was the task successful? + * + * @return boolean True if the task was successful. + * @since 4.1.0 + */ + public function isSuccess(): bool + { + return in_array(($this->snapshot['status'] ?? null), [Status::OK, Status::WILL_RESUME]); + } + + /** + * Set a task property. This method is a proxy to {@see Registry::set()}. + * + * @param string $path Registry path of the task property. + * @param mixed $value The value to set to the property. + * @param ?string $separator The key separator. + * + * @return mixed|null + * + * @since 4.1.0 + */ + protected function set(string $path, $value, string $separator = null) + { + return $this->taskRegistry->set($path, $value, $separator); + } + + /** + * Get a task property. This method is a proxy to {@see Registry::get()}. + * + * @param string $path Registry path of the task property. + * @param mixed $default Default property to return, if the actual value is null. + * + * @return mixed The task property. + * + * @since 4.1.0 + */ + public function get(string $path, $default = null) + { + return $this->taskRegistry->get($path, $default); + } + + /** + * Static method to determine whether an enumerated task state (as a string) is valid. + * + * @param string $state The task state (enumerated, as a string). + * + * @return boolean + * + * @since 4.1.0 + */ + public static function isValidState(string $state): bool + { + if (!is_numeric($state)) { + return false; + } + + // Takes care of interpreting as float/int + $state = $state + 0; + + return ArrayHelper::getValue(self::STATE_MAP, $state) !== null; + } + + /** + * Static method to determine whether a task id is valid. Note that this does not + * validate ids against the database, but only verifies that an id may exist. + * + * @param string $id The task id (as a string). + * + * @return boolean + * + * @since 4.1.0 + */ + public static function isValidId(string $id): bool + { + $id = is_numeric($id) ? ($id + 0) : $id; + + if (!\is_int($id) || $id <= 0) { + return false; + } + + return true; + } } diff --git a/administrator/components/com_scheduler/src/Task/TaskOption.php b/administrator/components/com_scheduler/src/Task/TaskOption.php index 05fc2ecdd05d8..802d479c1d785 100644 --- a/administrator/components/com_scheduler/src/Task/TaskOption.php +++ b/administrator/components/com_scheduler/src/Task/TaskOption.php @@ -1,4 +1,5 @@ id = $type; - $this->title = Text::_("${langConstPrefix}_TITLE"); - $this->desc = Text::_("${langConstPrefix}_DESC"); - $this->langConstPrefix = $langConstPrefix; - } + /** + * TaskOption constructor. + * + * @param string $type A unique ID string for a plugin task routine. + * @param string $langConstPrefix The Language constant prefix $p. Expects $p . _TITLE and $p . _DESC to exist. + * + * @since 4.1.0 + */ + public function __construct(string $type, string $langConstPrefix) + { + $this->id = $type; + $this->title = Text::_("${langConstPrefix}_TITLE"); + $this->desc = Text::_("${langConstPrefix}_DESC"); + $this->langConstPrefix = $langConstPrefix; + } - /** - * Magic method to allow read-only access to private properties. - * - * @param string $name The object property requested. - * - * @return ?string - * - * @since 4.1.0 - */ - public function __get(string $name) - { - if (property_exists($this, $name)) - { - return $this->$name; - } + /** + * Magic method to allow read-only access to private properties. + * + * @param string $name The object property requested. + * + * @return ?string + * + * @since 4.1.0 + */ + public function __get(string $name) + { + if (property_exists($this, $name)) { + return $this->$name; + } - // Trigger a deprecation for the 'type' property (replaced with {@see id}). - if ($name === 'type') - { - try - { - Log::add( - sprintf( - 'The %1$s property is deprecated. Use %2$s instead.', - $name, - 'id' - ), - Log::WARNING, - 'deprecated' - ); - } - catch (\RuntimeException $e) - { - // Pass - } + // Trigger a deprecation for the 'type' property (replaced with {@see id}). + if ($name === 'type') { + try { + Log::add( + sprintf( + 'The %1$s property is deprecated. Use %2$s instead.', + $name, + 'id' + ), + Log::WARNING, + 'deprecated' + ); + } catch (\RuntimeException $e) { + // Pass + } - return $this->id; - } + return $this->id; + } - return null; - } + return null; + } } diff --git a/administrator/components/com_scheduler/src/Task/TaskOptions.php b/administrator/components/com_scheduler/src/Task/TaskOptions.php index 4488aab494d7d..4cc909df5e5e7 100644 --- a/administrator/components/com_scheduler/src/Task/TaskOptions.php +++ b/administrator/components/com_scheduler/src/Task/TaskOptions.php @@ -1,4 +1,5 @@ 'languageConstantPrefix', ... ] - * - * @return void - * - * @since 4.1.0 - */ - public function addOptions(array $taskRoutines): void - { - foreach ($taskRoutines as $routineId => $langConstPrefix) - { - $this->options[] = new TaskOption($routineId, $langConstPrefix); - } - } + /** + * A plugin can support several task routines + * This method is used by a plugin's onTaskOptionsList subscriber to advertise supported routines. + * + * @param array $taskRoutines An associative array of {@var TaskOption} constructor argument pairs: + * [ 'routineId' => 'languageConstantPrefix', ... ] + * + * @return void + * + * @since 4.1.0 + */ + public function addOptions(array $taskRoutines): void + { + foreach ($taskRoutines as $routineId => $langConstPrefix) { + $this->options[] = new TaskOption($routineId, $langConstPrefix); + } + } - /** - * @param ?string $routineId A unique identifier for a plugin task routine - * - * @return ?TaskOption A matching TaskOption if available, null otherwise - * - * @since 4.1.0 - */ - public function findOption(?string $routineId): ?TaskOption - { - if ($routineId === null) - { - return null; - } + /** + * @param ?string $routineId A unique identifier for a plugin task routine + * + * @return ?TaskOption A matching TaskOption if available, null otherwise + * + * @since 4.1.0 + */ + public function findOption(?string $routineId): ?TaskOption + { + if ($routineId === null) { + return null; + } - foreach ($this->options as $option) - { - if ($option->id === $routineId) - { - return $option; - } - } + foreach ($this->options as $option) { + if ($option->id === $routineId) { + return $option; + } + } - return null; - } + return null; + } } diff --git a/administrator/components/com_scheduler/src/Traits/TaskPluginTrait.php b/administrator/components/com_scheduler/src/Traits/TaskPluginTrait.php index 16f3ac1481542..6c45c09c12839 100644 --- a/administrator/components/com_scheduler/src/Traits/TaskPluginTrait.php +++ b/administrator/components/com_scheduler/src/Traits/TaskPluginTrait.php @@ -1,4 +1,5 @@ snapshot['logCategory'] = $event->getArgument('subject')->logCategory; - $this->snapshot['plugin'] = $this->_name; - $this->snapshot['startTime'] = microtime(true); - $this->snapshot['status'] = Status::RUNNING; - } - - /** - * Set information to {@see $snapshot} when ending a routine. This information includes the routine exit code and - * timing information. - * - * @param ExecuteTaskEvent $event The event - * @param ?int $exitCode The task exit code - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - protected function endRoutine(ExecuteTaskEvent $event, int $exitCode): void - { - if (!$this instanceof CMSPlugin) - { - return; - } - - $this->snapshot['endTime'] = $endTime = microtime(true); - $this->snapshot['duration'] = $endTime - $this->snapshot['startTime']; - $this->snapshot['status'] = $exitCode ?? Status::OK; - $event->setResult($this->snapshot); - } - - /** - * Enhance the task form with routine-specific fields from an XML file declared through the TASKS_MAP constant. - * If a plugin only supports the task form and does not need additional logic, this method can be mapped to the - * `onContentPrepareForm` event through {@see SubscriberInterface::getSubscribedEvents()} and will take care - * of injecting the fields without additional logic in the plugin class. - * - * @param EventInterface|Form $context The onContentPrepareForm event or the Form object. - * @param mixed $data The form data, required when $context is a {@see Form} instance. - * - * @return boolean True if the form was successfully enhanced or the context was not relevant. - * - * @since 4.1.0 - * @throws \Exception - */ - public function enhanceTaskItemForm($context, $data = null): bool - { - if ($context instanceof EventInterface) - { - /** @var Form $form */ - $form = $context->getArgument('0'); - $data = $context->getArgument('1'); - } - elseif ($context instanceof Form) - { - $form = $context; - } - else - { - throw new \InvalidArgumentException( - sprintf( - 'Argument 0 of %1$s must be an instance of %2$s or %3$s', - __METHOD__, - EventInterface::class, - Form::class - ) - ); - } - - if ($form->getName() !== 'com_scheduler.task') - { - return true; - } - - $routineId = $this->getRoutineId($form, $data); - $isSupported = \array_key_exists($routineId, self::TASKS_MAP); - $enhancementFormName = self::TASKS_MAP[$routineId]['form'] ?? ''; - - // Return if routine is not supported by the plugin or the routine does not have a form linked in TASKS_MAP. - if (!$isSupported || \strlen($enhancementFormName) === 0) - { - return true; - } - - // We expect the form XML in "{PLUGIN_PATH}/forms/{FORM_NAME}.xml" - $path = JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name; - $enhancementFormFile = $path . '/forms/' . $enhancementFormName . '.xml'; - - try - { - $enhancementFormFile = Path::check($enhancementFormFile); - } - catch (\Exception $e) - { - return false; - } - - if (is_file($enhancementFormFile)) - { - return $form->loadFile($enhancementFormFile); - } - - return false; - } - - /** - * Advertise the task routines supported by the plugin. This method should be mapped to the `onTaskOptionsList`, - * enabling the plugin to advertise its routines without any custom logic.
- * **Note:** This method expects the `TASKS_MAP` class constant to have relevant information. - * - * @param EventInterface $event onTaskOptionsList Event - * - * @return void - * - * @since 4.1.0 - */ - public function advertiseRoutines(EventInterface $event): void - { - $options = []; - - foreach (self::TASKS_MAP as $routineId => $details) - { - // Sanity check against non-compliant plugins - if (isset($details['langConstPrefix'])) - { - $options[$routineId] = $details['langConstPrefix']; - } - } - - $subject = $event->getArgument('subject'); - $subject->addOptions($options); - } - - /** - * Get the relevant task routine ID in the context of a form event, e.g., the `onContentPrepareForm` event. - * - * @param Form $form The form - * @param mixed $data The data - * - * @return string - * - * @since 4.1.0 - * @throws \Exception - */ - protected function getRoutineId(Form $form, $data): string - { - /* - * Depending on when the form is loaded, the ID may either be in $data or the data already bound to the form. - * $data can also either be an object or an array. - */ - $routineId = $data->taskOption->id ?? $data->type ?? $data['type'] ?? $form->getValue('type') ?? $data['taskOption']->id ?? ''; - - // If we're unable to find a routineId, it might be in the form input. - if (empty($routineId)) - { - $app = $this->getApplication() ?? ($this->app ?? Factory::getApplication()); - $form = $app->getInput()->get('jform', []); - $routineId = ArrayHelper::getValue($form, 'type', '', 'STRING'); - } - - return $routineId; - } - - /** - * Add a log message to the task log. - * - * @param string $message The log message - * @param string $priority The log message priority - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - * @todo : use dependency injection here (starting from the Task & Scheduler classes). - */ - protected function logTask(string $message, string $priority = 'info'): void - { - static $langLoaded; - static $priorityMap = [ - 'debug' => Log::DEBUG, - 'error' => Log::ERROR, - 'info' => Log::INFO, - 'notice' => Log::NOTICE, - 'warning' => Log::WARNING, - ]; - - if (!$langLoaded) - { - $app = $this->getApplication() ?? ($this->app ?? Factory::getApplication()); - $app->getLanguage()->load('com_scheduler', JPATH_ADMINISTRATOR); - $langLoaded = true; - } - - $category = $this->snapshot['logCategory']; - - Log::add(Text::_('COM_SCHEDULER_ROUTINE_LOG_PREFIX') . $message, $priorityMap[$priority] ?? Log::INFO, $category); - } - - /** - * Handler for *standard* task routines. Standard routines are mapped to valid class methods 'method' through - * `static::TASKS_MAP`. These methods are expected to take a single argument (the Event) and return an integer - * return status (see {@see Status}). For a plugin that maps each of its task routines to valid methods and does - * not need non-standard handling, this method can be mapped to the `onExecuteTask` event through - * {@see SubscriberInterface::getSubscribedEvents()}, which would allow it to then check if the event wants to - * execute a routine offered by the parent plugin, call the routine and do some other housework without any code - * in the parent classes.
- * **Compatible routine method signature:**   ({@see ExecuteTaskEvent::class}, ...): int - * - * @param ExecuteTaskEvent $event The `onExecuteTask` event. - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - public function standardRoutineHandler(ExecuteTaskEvent $event): void - { - if (!\array_key_exists($event->getRoutineId(), self::TASKS_MAP)) - { - return; - } - - $this->startRoutine($event); - $routineId = $event->getRoutineId(); - $methodName = (string) self::TASKS_MAP[$routineId]['method'] ?? ''; - $exitCode = Status::NO_EXIT; - - // We call the mapped method if it exists and confirms to the ($event) -> int signature. - if (!empty($methodName) && ($staticReflection = new \ReflectionClass($this))->hasMethod($methodName)) - { - $method = $staticReflection->getMethod($methodName); - - // Might need adjustments here for PHP8 named parameters. - if (!($method->getNumberOfRequiredParameters() === 1) - || !$method->getParameters()[0]->hasType() - || $method->getParameters()[0]->getType()->getName() !== ExecuteTaskEvent::class - || !$method->hasReturnType() - || $method->getReturnType()->getName() !== 'int') - { - $this->logTask( - sprintf( - 'Incorrect routine method signature for %1$s(). See checks in %2$s()', - $method->getName(), - __METHOD__ - ), - 'error' - ); - - return; - } - - try - { - // Enable invocation of private/protected methods. - $method->setAccessible(true); - $exitCode = $method->invoke($this, $event); - } - catch (\ReflectionException $e) - { - // @todo replace with language string (?) - $this->logTask('Exception when calling routine: ' . $e->getMessage(), 'error'); - $exitCode = Status::NO_RUN; - } - } - else - { - $this->logTask( - sprintf( - 'Incorrectly configured TASKS_MAP in class %s. Missing valid method for `routine_id` %s', - static::class, - $routineId - ), - 'error' - ); - } - - /** - * Closure to validate a status against {@see Status} - * - * @since 4.1.0 - */ - $validateStatus = static function (int $statusCode): bool { - return \in_array( - $statusCode, - (new \ReflectionClass(Status::class))->getConstants() - ); - }; - - // Validate the exit code. - if (!\is_int($exitCode) || !$validateStatus($exitCode)) - { - $exitCode = Status::INVALID_EXIT; - } - - $this->endRoutine($event, $exitCode); - } + /** + * A snapshot of the routine state. + * + * @var array + * @since 4.1.0 + */ + protected $snapshot = []; + + /** + * Set information to {@see $snapshot} when initializing a routine. + * + * @param ExecuteTaskEvent $event The onExecuteTask event. + * + * @return void + * + * @since 4.1.0 + */ + protected function startRoutine(ExecuteTaskEvent $event): void + { + if (!$this instanceof CMSPlugin) { + return; + } + + $this->snapshot['logCategory'] = $event->getArgument('subject')->logCategory; + $this->snapshot['plugin'] = $this->_name; + $this->snapshot['startTime'] = microtime(true); + $this->snapshot['status'] = Status::RUNNING; + } + + /** + * Set information to {@see $snapshot} when ending a routine. This information includes the routine exit code and + * timing information. + * + * @param ExecuteTaskEvent $event The event + * @param ?int $exitCode The task exit code + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + protected function endRoutine(ExecuteTaskEvent $event, int $exitCode): void + { + if (!$this instanceof CMSPlugin) { + return; + } + + $this->snapshot['endTime'] = $endTime = microtime(true); + $this->snapshot['duration'] = $endTime - $this->snapshot['startTime']; + $this->snapshot['status'] = $exitCode ?? Status::OK; + $event->setResult($this->snapshot); + } + + /** + * Enhance the task form with routine-specific fields from an XML file declared through the TASKS_MAP constant. + * If a plugin only supports the task form and does not need additional logic, this method can be mapped to the + * `onContentPrepareForm` event through {@see SubscriberInterface::getSubscribedEvents()} and will take care + * of injecting the fields without additional logic in the plugin class. + * + * @param EventInterface|Form $context The onContentPrepareForm event or the Form object. + * @param mixed $data The form data, required when $context is a {@see Form} instance. + * + * @return boolean True if the form was successfully enhanced or the context was not relevant. + * + * @since 4.1.0 + * @throws \Exception + */ + public function enhanceTaskItemForm($context, $data = null): bool + { + if ($context instanceof EventInterface) { + /** @var Form $form */ + $form = $context->getArgument('0'); + $data = $context->getArgument('1'); + } elseif ($context instanceof Form) { + $form = $context; + } else { + throw new \InvalidArgumentException( + sprintf( + 'Argument 0 of %1$s must be an instance of %2$s or %3$s', + __METHOD__, + EventInterface::class, + Form::class + ) + ); + } + + if ($form->getName() !== 'com_scheduler.task') { + return true; + } + + $routineId = $this->getRoutineId($form, $data); + $isSupported = \array_key_exists($routineId, self::TASKS_MAP); + $enhancementFormName = self::TASKS_MAP[$routineId]['form'] ?? ''; + + // Return if routine is not supported by the plugin or the routine does not have a form linked in TASKS_MAP. + if (!$isSupported || \strlen($enhancementFormName) === 0) { + return true; + } + + // We expect the form XML in "{PLUGIN_PATH}/forms/{FORM_NAME}.xml" + $path = JPATH_PLUGINS . '/' . $this->_type . '/' . $this->_name; + $enhancementFormFile = $path . '/forms/' . $enhancementFormName . '.xml'; + + try { + $enhancementFormFile = Path::check($enhancementFormFile); + } catch (\Exception $e) { + return false; + } + + if (is_file($enhancementFormFile)) { + return $form->loadFile($enhancementFormFile); + } + + return false; + } + + /** + * Advertise the task routines supported by the plugin. This method should be mapped to the `onTaskOptionsList`, + * enabling the plugin to advertise its routines without any custom logic.
+ * **Note:** This method expects the `TASKS_MAP` class constant to have relevant information. + * + * @param EventInterface $event onTaskOptionsList Event + * + * @return void + * + * @since 4.1.0 + */ + public function advertiseRoutines(EventInterface $event): void + { + $options = []; + + foreach (self::TASKS_MAP as $routineId => $details) { + // Sanity check against non-compliant plugins + if (isset($details['langConstPrefix'])) { + $options[$routineId] = $details['langConstPrefix']; + } + } + + $subject = $event->getArgument('subject'); + $subject->addOptions($options); + } + + /** + * Get the relevant task routine ID in the context of a form event, e.g., the `onContentPrepareForm` event. + * + * @param Form $form The form + * @param mixed $data The data + * + * @return string + * + * @since 4.1.0 + * @throws \Exception + */ + protected function getRoutineId(Form $form, $data): string + { + /* + * Depending on when the form is loaded, the ID may either be in $data or the data already bound to the form. + * $data can also either be an object or an array. + */ + $routineId = $data->taskOption->id ?? $data->type ?? $data['type'] ?? $form->getValue('type') ?? $data['taskOption']->id ?? ''; + + // If we're unable to find a routineId, it might be in the form input. + if (empty($routineId)) { + $app = $this->getApplication() ?? ($this->app ?? Factory::getApplication()); + $form = $app->getInput()->get('jform', []); + $routineId = ArrayHelper::getValue($form, 'type', '', 'STRING'); + } + + return $routineId; + } + + /** + * Add a log message to the task log. + * + * @param string $message The log message + * @param string $priority The log message priority + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + * @todo : use dependency injection here (starting from the Task & Scheduler classes). + */ + protected function logTask(string $message, string $priority = 'info'): void + { + static $langLoaded; + static $priorityMap = [ + 'debug' => Log::DEBUG, + 'error' => Log::ERROR, + 'info' => Log::INFO, + 'notice' => Log::NOTICE, + 'warning' => Log::WARNING, + ]; + + if (!$langLoaded) { + $app = $this->getApplication() ?? ($this->app ?? Factory::getApplication()); + $app->getLanguage()->load('com_scheduler', JPATH_ADMINISTRATOR); + $langLoaded = true; + } + + $category = $this->snapshot['logCategory']; + + Log::add(Text::_('COM_SCHEDULER_ROUTINE_LOG_PREFIX') . $message, $priorityMap[$priority] ?? Log::INFO, $category); + } + + /** + * Handler for *standard* task routines. Standard routines are mapped to valid class methods 'method' through + * `static::TASKS_MAP`. These methods are expected to take a single argument (the Event) and return an integer + * return status (see {@see Status}). For a plugin that maps each of its task routines to valid methods and does + * not need non-standard handling, this method can be mapped to the `onExecuteTask` event through + * {@see SubscriberInterface::getSubscribedEvents()}, which would allow it to then check if the event wants to + * execute a routine offered by the parent plugin, call the routine and do some other housework without any code + * in the parent classes.
+ * **Compatible routine method signature:**   ({@see ExecuteTaskEvent::class}, ...): int + * + * @param ExecuteTaskEvent $event The `onExecuteTask` event. + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + public function standardRoutineHandler(ExecuteTaskEvent $event): void + { + if (!\array_key_exists($event->getRoutineId(), self::TASKS_MAP)) { + return; + } + + $this->startRoutine($event); + $routineId = $event->getRoutineId(); + $methodName = (string) self::TASKS_MAP[$routineId]['method'] ?? ''; + $exitCode = Status::NO_EXIT; + + // We call the mapped method if it exists and confirms to the ($event) -> int signature. + if (!empty($methodName) && ($staticReflection = new \ReflectionClass($this))->hasMethod($methodName)) { + $method = $staticReflection->getMethod($methodName); + + // Might need adjustments here for PHP8 named parameters. + if ( + !($method->getNumberOfRequiredParameters() === 1) + || !$method->getParameters()[0]->hasType() + || $method->getParameters()[0]->getType()->getName() !== ExecuteTaskEvent::class + || !$method->hasReturnType() + || $method->getReturnType()->getName() !== 'int' + ) { + $this->logTask( + sprintf( + 'Incorrect routine method signature for %1$s(). See checks in %2$s()', + $method->getName(), + __METHOD__ + ), + 'error' + ); + + return; + } + + try { + // Enable invocation of private/protected methods. + $method->setAccessible(true); + $exitCode = $method->invoke($this, $event); + } catch (\ReflectionException $e) { + // @todo replace with language string (?) + $this->logTask('Exception when calling routine: ' . $e->getMessage(), 'error'); + $exitCode = Status::NO_RUN; + } + } else { + $this->logTask( + sprintf( + 'Incorrectly configured TASKS_MAP in class %s. Missing valid method for `routine_id` %s', + static::class, + $routineId + ), + 'error' + ); + } + + /** + * Closure to validate a status against {@see Status} + * + * @since 4.1.0 + */ + $validateStatus = static function (int $statusCode): bool { + return \in_array( + $statusCode, + (new \ReflectionClass(Status::class))->getConstants() + ); + }; + + // Validate the exit code. + if (!\is_int($exitCode) || !$validateStatus($exitCode)) { + $exitCode = Status::INVALID_EXIT; + } + + $this->endRoutine($event, $exitCode); + } } diff --git a/administrator/components/com_scheduler/src/View/Select/HtmlView.php b/administrator/components/com_scheduler/src/View/Select/HtmlView.php index c2d35d3a44b12..04d9b5d32dea4 100644 --- a/administrator/components/com_scheduler/src/View/Select/HtmlView.php +++ b/administrator/components/com_scheduler/src/View/Select/HtmlView.php @@ -1,4 +1,5 @@ app = Factory::getApplication(); - - parent::__construct($config); - } - - /** - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - public function display($tpl = null): void - { - $this->state = $this->get('State'); - $this->items = $this->get('Items'); - $this->modalLink = ''; - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.1.0 - */ - protected function addToolbar(): void - { - /* - * Get the global Toolbar instance - * @todo : Replace usage with ToolbarFactoryInterface. but how? - * Probably some changes in the core, since mod_menu calls and renders the getInstance() toolbar - */ - $toolbar = Toolbar::getInstance(); - - // Add page title - ToolbarHelper::title(Text::_('COM_SCHEDULER_MANAGER_TASKS'), 'clock'); - - $toolbar->linkButton('cancel') - ->url('index.php?option=com_scheduler') - ->buttonClass('btn btn-danger') - ->icon('icon-times') - ->text(Text::_('JCANCEL')); - } + /** + * @var AdministratorApplication + * @since 4.1.0 + */ + protected $app; + + /** + * The model state + * + * @var CMSObject + * @since 4.1.0 + */ + protected $state; + + /** + * An array of items + * + * @var TaskOption[] + * @since 4.1.0 + */ + protected $items; + + /** + * A suffix for links for modal use [?] + * + * @var string + * @since 4.1.0 + */ + protected $modalLink; + + /** + * HtmlView constructor. + * + * @param array $config A named configuration array for object construction. + * name: the name (optional) of the view (defaults to the view class name suffix). + * charset: the character set to use for display + * escape: the name (optional) of the function to use for escaping strings + * base_path: the parent path (optional) of the `views` directory (defaults to the component + * folder) template_plath: the path (optional) of the layout directory (defaults to + * base_path + /views/ + view name helper_path: the path (optional) of the helper files + * (defaults to base_path + /helpers/) layout: the layout (optional) to use to display the + * view + * + * @since 4.1.0 + * @throws \Exception + */ + public function __construct($config = []) + { + $this->app = Factory::getApplication(); + + parent::__construct($config); + } + + /** + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + public function display($tpl = null): void + { + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->modalLink = ''; + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.1.0 + */ + protected function addToolbar(): void + { + /* + * Get the global Toolbar instance + * @todo : Replace usage with ToolbarFactoryInterface. but how? + * Probably some changes in the core, since mod_menu calls and renders the getInstance() toolbar + */ + $toolbar = Toolbar::getInstance(); + + // Add page title + ToolbarHelper::title(Text::_('COM_SCHEDULER_MANAGER_TASKS'), 'clock'); + + $toolbar->linkButton('cancel') + ->url('index.php?option=com_scheduler') + ->buttonClass('btn btn-danger') + ->icon('icon-times') + ->text(Text::_('JCANCEL')); + } } diff --git a/administrator/components/com_scheduler/src/View/Task/HtmlView.php b/administrator/components/com_scheduler/src/View/Task/HtmlView.php index 6023fc9715f3b..08c9df9621df4 100644 --- a/administrator/components/com_scheduler/src/View/Task/HtmlView.php +++ b/administrator/components/com_scheduler/src/View/Task/HtmlView.php @@ -1,4 +1,5 @@ app = Factory::getApplication(); - parent::__construct($config); - } - - /** - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - public function display($tpl = null): void - { - /* - * Will call the getForm() method of TaskModel - */ - $this->form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - $this->canDo = ContentHelper::getActions('com_scheduler', 'task', $this->item->id); - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Adds the page title and toolbar - * - * @return void - * - * @since 4.1.0 - */ - protected function addToolbar(): void - { - $app = $this->app; - - $app->getInput()->set('hidemainmenu', true); - $isNew = ($this->item->id == 0); - $canDo = $this->canDo; - - /* - * Get the toolbar object instance - * !! @todo : Replace usage with ToolbarFactoryInterface - */ - $toolbar = Toolbar::getInstance(); - - ToolbarHelper::title($isNew ? Text::_('COM_SCHEDULER_MANAGER_TASK_NEW') : Text::_('COM_SCHEDULER_MANAGER_TASK_EDIT'), 'clock'); - - if (($isNew && $canDo->get('core.create')) || (!$isNew && $canDo->get('core.edit'))) - { - $toolbar->apply('task.apply'); - $toolbar->save('task.save'); - } - - // @todo | ? : Do we need save2new, save2copy? - - $toolbar->cancel('task.cancel', $isNew ? 'JTOOLBAR_CANCEL' : 'JTOOLBAR_CLOSE'); - $toolbar->help('Scheduled_Tasks:_Edit'); - } + /** + * @var AdministratorApplication $app + * @since 4.1.0 + */ + protected $app; + + /** + * The Form object + * + * @var Form + * @since 4.1.0 + */ + protected $form; + + /** + * The active item + * + * @var object + * @since 4.1.0 + */ + protected $item; + + /** + * The model state + * + * @var CMSObject + * @since 4.1.0 + */ + protected $state; + + /** + * The actions the user is authorised to perform + * + * @var CMSObject + * @since 4.1.0 + */ + protected $canDo; + + /** + * Overloads the parent constructor. + * Just needed to fetch the Application object. + * + * @param array $config A named configuration array for object construction. + * name: the name (optional) of the view (defaults to the view class name suffix). + * charset: the character set to use for display + * escape: the name (optional) of the function to use for escaping strings + * base_path: the parent path (optional) of the `views` directory (defaults to the + * component folder) template_plath: the path (optional) of the layout directory (defaults + * to base_path + /views/ + view name helper_path: the path (optional) of the helper files + * (defaults to base_path + /helpers/) layout: the layout (optional) to use to display the + * view + * + * @since 4.1.0 + * @throws \Exception + */ + public function __construct($config = array()) + { + $this->app = Factory::getApplication(); + parent::__construct($config); + } + + /** + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + public function display($tpl = null): void + { + /* + * Will call the getForm() method of TaskModel + */ + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + $this->canDo = ContentHelper::getActions('com_scheduler', 'task', $this->item->id); + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Adds the page title and toolbar + * + * @return void + * + * @since 4.1.0 + */ + protected function addToolbar(): void + { + $app = $this->app; + + $app->getInput()->set('hidemainmenu', true); + $isNew = ($this->item->id == 0); + $canDo = $this->canDo; + + /* + * Get the toolbar object instance + * !! @todo : Replace usage with ToolbarFactoryInterface + */ + $toolbar = Toolbar::getInstance(); + + ToolbarHelper::title($isNew ? Text::_('COM_SCHEDULER_MANAGER_TASK_NEW') : Text::_('COM_SCHEDULER_MANAGER_TASK_EDIT'), 'clock'); + + if (($isNew && $canDo->get('core.create')) || (!$isNew && $canDo->get('core.edit'))) { + $toolbar->apply('task.apply'); + $toolbar->save('task.save'); + } + + // @todo | ? : Do we need save2new, save2copy? + + $toolbar->cancel('task.cancel', $isNew ? 'JTOOLBAR_CANCEL' : 'JTOOLBAR_CLOSE'); + $toolbar->help('Scheduled_Tasks:_Edit'); + } } diff --git a/administrator/components/com_scheduler/src/View/Tasks/HtmlView.php b/administrator/components/com_scheduler/src/View/Tasks/HtmlView.php index a7fd01bde0670..6f958d1576ea2 100644 --- a/administrator/components/com_scheduler/src/View/Tasks/HtmlView.php +++ b/administrator/components/com_scheduler/src/View/Tasks/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('empty_state'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // We don't need toolbar in the modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.1.0 - * @throws \Exception - */ - protected function addToolbar(): void - { - $canDo = ContentHelper::getActions('com_scheduler'); - $user = Factory::getApplication()->getIdentity(); - - /* - * Get the toolbar object instance - * !! @todo : Replace usage with ToolbarFactoryInterface - */ - $toolbar = Toolbar::getInstance(); - - ToolbarHelper::title(Text::_('COM_SCHEDULER_MANAGER_TASKS'), 'clock'); - - if ($canDo->get('core.create')) - { - $toolbar->linkButton('new', 'JTOOLBAR_NEW') - ->url('index.php?option=com_scheduler&view=select&layout=default') - ->buttonClass('btn btn-success') - ->icon('icon-new'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) - { - /** @var DropdownButton $dropdown */ - $dropdown = $toolbar->dropdownButton('status-group') - ->toggleSplit(false) - ->text('JTOOLBAR_CHANGE_STATUS') - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - // Add the batch Enable, Disable and Trash buttons if privileged - if ($canDo->get('core.edit.state')) - { - $childBar->publish('tasks.publish', 'JTOOLBAR_ENABLE')->listCheck(true); - $childBar->unpublish('tasks.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); - - if ($canDo->get('core.admin')) - { - $childBar->checkin('tasks.checkin')->listCheck(true); - } - - $childBar->checkin('tasks.unlock', 'COM_SCHEDULER_TOOLBAR_UNLOCK')->listCheck(true)->icon('icon-unlock'); - - // We don't want the batch Trash button if displayed entries are all trashed - if ($this->state->get('filter.state') != -2) - { - $childBar->trash('tasks.trash')->listCheck(true); - } - } - } - - // Add "Empty Trash" button if filtering by trashed. - if ($this->state->get('filter.state') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('tasks.delete') - ->message('JGLOBAL_CONFIRM_DELETE') - ->text('JTOOLBAR_EMPTY_TRASH') - ->listCheck(true); - } - - // Link to component preferences if user has admin privileges - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences('com_scheduler'); - } - - $toolbar->help('Scheduled_Tasks'); - } + /** + * Array of task items. + * + * @var array + * @since 4.1.0 + */ + protected $items; + + /** + * The pagination object. + * + * @var Pagination + * @since 4.1.0 + * @todo Test pagination. + */ + protected $pagination; + + /** + * The model state. + * + * @var CMSObject + * @since 4.1.0 + */ + protected $state; + + /** + * A Form object for search filters. + * + * @var Form + * @since 4.1.0 + */ + public $filterForm; + + /** + * The active search filters. + * + * @var array + * @since 4.1.0 + */ + public $activeFilters; + + /** + * Is this view in an empty state? + * + * @var boolean + * @since 4.1.0 + */ + private $isEmptyState = false; + + /** + * @inheritDoc + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + public function display($tpl = null): void + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('empty_state'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // We don't need toolbar in the modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.1.0 + * @throws \Exception + */ + protected function addToolbar(): void + { + $canDo = ContentHelper::getActions('com_scheduler'); + $user = Factory::getApplication()->getIdentity(); + + /* + * Get the toolbar object instance + * !! @todo : Replace usage with ToolbarFactoryInterface + */ + $toolbar = Toolbar::getInstance(); + + ToolbarHelper::title(Text::_('COM_SCHEDULER_MANAGER_TASKS'), 'clock'); + + if ($canDo->get('core.create')) { + $toolbar->linkButton('new', 'JTOOLBAR_NEW') + ->url('index.php?option=com_scheduler&view=select&layout=default') + ->buttonClass('btn btn-success') + ->icon('icon-new'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) { + /** @var DropdownButton $dropdown */ + $dropdown = $toolbar->dropdownButton('status-group') + ->toggleSplit(false) + ->text('JTOOLBAR_CHANGE_STATUS') + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + // Add the batch Enable, Disable and Trash buttons if privileged + if ($canDo->get('core.edit.state')) { + $childBar->publish('tasks.publish', 'JTOOLBAR_ENABLE')->listCheck(true); + $childBar->unpublish('tasks.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); + + if ($canDo->get('core.admin')) { + $childBar->checkin('tasks.checkin')->listCheck(true); + } + + $childBar->checkin('tasks.unlock', 'COM_SCHEDULER_TOOLBAR_UNLOCK')->listCheck(true)->icon('icon-unlock'); + + // We don't want the batch Trash button if displayed entries are all trashed + if ($this->state->get('filter.state') != -2) { + $childBar->trash('tasks.trash')->listCheck(true); + } + } + } + + // Add "Empty Trash" button if filtering by trashed. + if ($this->state->get('filter.state') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('tasks.delete') + ->message('JGLOBAL_CONFIRM_DELETE') + ->text('JTOOLBAR_EMPTY_TRASH') + ->listCheck(true); + } + + // Link to component preferences if user has admin privileges + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences('com_scheduler'); + } + + $toolbar->help('Scheduled_Tasks'); + } } diff --git a/administrator/components/com_scheduler/tmpl/select/default.php b/administrator/components/com_scheduler/tmpl/select/default.php index efffafd4f13ef..680860340fed5 100644 --- a/administrator/components/com_scheduler/tmpl/select/default.php +++ b/administrator/components/com_scheduler/tmpl/select/default.php @@ -1,4 +1,5 @@
-
-
- -
- -
- -
-
-
-
+
+
+ +
+ +
+ +
+
+
+
-
- -
- - -
-

- -

+
+ +
+ + +
+

+ +

- -
+ +
- - items as $item) : ?> - - id; ?> - escape($item->title); ?> - escape(strip_tags($item->desc)), 200); ?> - - -
-

-

- -

-
- - - -
- - -
-
+ + items as $item) : ?> + + id; ?> + escape($item->title); ?> + escape(strip_tags($item->desc)), 200); ?> + + +
+

+

+ +

+
+ + + +
+ + +
+
diff --git a/administrator/components/com_scheduler/tmpl/select/modal.php b/administrator/components/com_scheduler/tmpl/select/modal.php index 745adeeef782e..5c2d100569e8a 100644 --- a/administrator/components/com_scheduler/tmpl/select/modal.php +++ b/administrator/components/com_scheduler/tmpl/select/modal.php @@ -1,4 +1,5 @@
- setLayout('default'); ?> - - loadTemplate(); - } - catch (Exception $e) - { - die('Exception while loading template..'); - } - ?> + setLayout('default'); ?> + + loadTemplate(); + } catch (Exception $e) { + die('Exception while loading template..'); + } + ?>
diff --git a/administrator/components/com_scheduler/tmpl/task/edit.php b/administrator/components/com_scheduler/tmpl/task/edit.php index 184fcb1dfb47c..7d91554d3a53f 100644 --- a/administrator/components/com_scheduler/tmpl/task/edit.php +++ b/administrator/components/com_scheduler/tmpl/task/edit.php @@ -1,4 +1,5 @@ $fieldset) : - if ($name === 'task_params') : - unset($advancedFieldsets[$name]); - continue; - endif; + if ($name === 'task_params') : + unset($advancedFieldsets[$name]); + continue; + endif; - $this->ignore_fieldsets[] = $fieldset->name; + $this->ignore_fieldsets[] = $fieldset->name; endforeach; ?>
- - - - - -
- 'general')); ?> - - - item->id) ? Text::_('COM_SCHEDULER_NEW_TASK') : Text::_('COM_SCHEDULER_EDIT_TASK') - ); - ?> -
-
- - item->taskOption): - /** @var TaskOption $taskOption */ - $taskOption = $this->item->taskOption; ?> -
-

- title ?> -

- fieldset = 'description'; - $short_description = Text::_($taskOption->desc); - $long_description = LayoutHelper::render('joomla.edit.fieldset', $this); - - if (!$long_description) - { - $truncated = HTMLHelper::_('string.truncate', $short_description, 550, true, false); - - if (strlen($truncated) > 500) - { - $long_description = $short_description; - $short_description = HTMLHelper::_('string.truncate', $truncated, 250); - - if ($short_description == $long_description) - { - $long_description = ''; - } - } - } - ?> -

- -

- - - -

- -
- - enqueueMessage(Text::_('COM_SCHEDULER_WARNING_EXISTING_TASK_TYPE_NOT_FOUND'), 'warning'); - ?> - -
- - form->renderFieldset('basic'); ?> -
- -
- - form->renderFieldset('custom-cron-rules'); ?> -
- -
- -
- form->renderFieldset('aside'); ?> -
-
- - - -
-
- -
-
- - - - -
-
-
- - form->renderFieldset('priority') ?> -
- -
- label ?: 'COM_SCHEDULER_FIELDSET_' . $fieldset->name) ?> - form->renderFieldset($fieldset->name) ?> -
- -
-
- - - - -
-
-
- - form->renderFieldset('exec_hist'); ?> -
-
-
- - - - -
-
-
- - form->renderFieldset('details'); ?> -
-
-
- - - - canDo->get('core.admin')) : ?> - -
- -
- form->getInput('rules'); ?> -
-
- - - - form->getInput('context'); ?> - - -
+ method="post" name="adminForm" id="task-form" + aria-label="item->id === 0 ? 'NEW' : 'EDIT'), true); ?>" + class="form-validate"> + + + + + +
+ 'general')); ?> + + + item->id) ? Text::_('COM_SCHEDULER_NEW_TASK') : Text::_('COM_SCHEDULER_EDIT_TASK') + ); + ?> +
+
+ + item->taskOption) : + /** @var TaskOption $taskOption */ + $taskOption = $this->item->taskOption; ?> +
+

+ title ?> +

+ fieldset = 'description'; + $short_description = Text::_($taskOption->desc); + $long_description = LayoutHelper::render('joomla.edit.fieldset', $this); + + if (!$long_description) { + $truncated = HTMLHelper::_('string.truncate', $short_description, 550, true, false); + + if (strlen($truncated) > 500) { + $long_description = $short_description; + $short_description = HTMLHelper::_('string.truncate', $truncated, 250); + + if ($short_description == $long_description) { + $long_description = ''; + } + } + } + ?> +

+ +

+ + + +

+ +
+ + enqueueMessage(Text::_('COM_SCHEDULER_WARNING_EXISTING_TASK_TYPE_NOT_FOUND'), 'warning'); + ?> + +
+ + form->renderFieldset('basic'); ?> +
+ +
+ + form->renderFieldset('custom-cron-rules'); ?> +
+ +
+ +
+ form->renderFieldset('aside'); ?> +
+
+ + + +
+
+ +
+
+ + + + +
+
+
+ + form->renderFieldset('priority') ?> +
+ +
+ label ?: 'COM_SCHEDULER_FIELDSET_' . $fieldset->name) ?> + form->renderFieldset($fieldset->name) ?> +
+ +
+
+ + + + +
+
+
+ + form->renderFieldset('exec_hist'); ?> +
+
+
+ + + + +
+
+
+ + form->renderFieldset('details'); ?> +
+
+
+ + + + canDo->get('core.admin')) : ?> + +
+ +
+ form->getInput('rules'); ?> +
+
+ + + + form->getInput('context'); ?> + + +
diff --git a/administrator/components/com_scheduler/tmpl/tasks/default.php b/administrator/components/com_scheduler/tmpl/tasks/default.php index ec6f238b2a889..c33990606550a 100644 --- a/administrator/components/com_scheduler/tmpl/tasks/default.php +++ b/administrator/components/com_scheduler/tmpl/tasks/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect') - ->useScript('com_scheduler.test-task') - ->useStyle('com_scheduler.admin-view-tasks-css'); + ->useScript('multiselect') + ->useScript('com_scheduler.test-task') + ->useStyle('com_scheduler.admin-view-tasks-css'); Text::script('COM_SCHEDULER_TEST_RUN_TITLE'); Text::script('COM_SCHEDULER_TEST_RUN_TASK'); @@ -42,14 +43,11 @@ Text::script('JLIB_JS_AJAX_ERROR_NO_CONTENT'); Text::script('JLIB_JS_AJAX_ERROR_PARSE'); -try -{ - /** @var CMSWebApplicationInterface $app */ - $app = Factory::getApplication(); -} -catch (Exception $e) -{ - die('Failed to get app'); +try { + /** @var CMSWebApplicationInterface $app */ + $app = Factory::getApplication(); +} catch (Exception $e) { + die('Failed to get app'); } $user = $app->getIdentity(); @@ -60,237 +58,235 @@ $section = null; $mode = false; -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_scheduler&task=tasks.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_scheduler&task=tasks.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } $this->document->addScriptOptions('com_scheduler.test-task.token', Session::getFormToken()); ?>
-
- $this)); - ?> - - - items)): ?> - -
- - -
- - - - items)): ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true" > - items as $i => $item): - $canCreate = $user->authorise('core.create', 'com_scheduler'); - $canEdit = $user->authorise('core.edit', 'com_scheduler'); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_scheduler') && $canCheckin; - ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - - - - - - -
- id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - - - state, $i, 'tasks.', $canChange); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'tasks.', $canCheckin); ?> - - locked) : ?> - $canChange, 'prefix' => 'tasks.', - 'active_class' => 'none fa fa-running border-dark text-body', - 'inactive_class' => 'none fa fa-running', 'tip' => true, 'translate' => false, - 'active_title' => Text::sprintf('COM_SCHEDULER_RUNNING_SINCE', HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5')), - 'inactive_title' => Text::sprintf('COM_SCHEDULER_RUNNING_SINCE', HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5')), - ]); ?> - - - - escape($item->title); ?> - - - escape($item->title); ?> - - last_exit_code, [Status::OK, Status::WILL_RESUME])): ?> - -
- last_exit_code); ?> -
- -
- - note): ?> - - escape($item->note)); ?> - - -
- escape($item->safeTypeTitle); ?> - - last_execution ? HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5') : '-'; ?> - - - - priority === -1) : ?> - - priority === 0) : ?> - - priority === 1) : ?> - - - - id; ?> -
- - pagination->getListFooter(); - - // Modal for test runs - $modalparams = [ - 'title' => '', - ]; - - $modalbody = '
'; - - echo HTMLHelper::_('bootstrap.renderModal', 'scheduler-test-modal', $modalparams, $modalbody); - - ?> - - - - - - -
+ id="adminForm"> +
+ $this)); + ?> + + + items)) : ?> + +
+ + +
+ + + + items)) : ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true" > + items as $i => $item) : + $canCreate = $user->authorise('core.create', 'com_scheduler'); + $canEdit = $user->authorise('core.edit', 'com_scheduler'); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_scheduler') && $canCheckin; + ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + + + state, $i, 'tasks.', $canChange); ?> + + checked_out) : ?> + editor, $item->checked_out_time, 'tasks.', $canCheckin); ?> + + locked) : ?> + $canChange, 'prefix' => 'tasks.', + 'active_class' => 'none fa fa-running border-dark text-body', + 'inactive_class' => 'none fa fa-running', 'tip' => true, 'translate' => false, + 'active_title' => Text::sprintf('COM_SCHEDULER_RUNNING_SINCE', HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5')), + 'inactive_title' => Text::sprintf('COM_SCHEDULER_RUNNING_SINCE', HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5')), + ]); ?> + + + + escape($item->title); ?> + + + escape($item->title); ?> + + last_exit_code, [Status::OK, Status::WILL_RESUME])) : ?> + +
+ last_exit_code); ?> +
+ +
+ + note) : ?> + + escape($item->note)); ?> + + +
+ escape($item->safeTypeTitle); ?> + + last_execution ? HTMLHelper::_('date', $item->last_execution, 'DATE_FORMAT_LC5') : '-'; ?> + + + + priority === -1) : ?> + + priority === 0) : ?> + + priority === 1) : ?> + + + + id; ?> +
+ + pagination->getListFooter(); + + // Modal for test runs + $modalparams = [ + 'title' => '', + ]; + + $modalbody = '
'; + + echo HTMLHelper::_('bootstrap.renderModal', 'scheduler-test-modal', $modalparams, $modalbody); + + ?> + + + + + + +
diff --git a/administrator/components/com_scheduler/tmpl/tasks/empty_state.php b/administrator/components/com_scheduler/tmpl/tasks/empty_state.php index adbcc38835678..96e2eb0958ca4 100644 --- a/administrator/components/com_scheduler/tmpl/tasks/empty_state.php +++ b/administrator/components/com_scheduler/tmpl/tasks/empty_state.php @@ -1,4 +1,5 @@ 'COM_SCHEDULER', - 'formURL' => 'index.php?option=com_scheduler&task=task.add', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/J4.x:Task_Scheduler', - 'icon' => 'icon-clock clock', + 'textPrefix' => 'COM_SCHEDULER', + 'formURL' => 'index.php?option=com_scheduler&task=task.add', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/J4.x:Task_Scheduler', + 'icon' => 'icon-clock clock', ]; -if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_scheduler')) -{ - $displayData['createURL'] = 'index.php?option=com_scheduler&view=select&layout=default'; +if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_scheduler')) { + $displayData['createURL'] = 'index.php?option=com_scheduler&view=select&layout=default'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_tags/services/provider.php b/administrator/components/com_tags/services/provider.php index 669a7013d295c..6352e87ae35fa 100644 --- a/administrator/components/com_tags/services/provider.php +++ b/administrator/components/com_tags/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Tags')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Tags')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Tags')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new TagsComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Tags')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Tags')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Tags')); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new TagsComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_tags/src/Controller/DisplayController.php b/administrator/components/com_tags/src/Controller/DisplayController.php index 366c926efd126..b6b15ba734c4b 100644 --- a/administrator/components/com_tags/src/Controller/DisplayController.php +++ b/administrator/components/com_tags/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'tags'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); + /** + * Method to display a view. + * + * @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()}. + * + * @return static|boolean This object to support chaining or false on failure. + * + * @since 3.1 + */ + public function display($cachable = false, $urlparams = false) + { + $view = $this->input->get('view', 'tags'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); - // Check for edit form. - if ($view == 'tag' && $layout == 'edit' && !$this->checkEditId('com_tags.edit.tag', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } + // Check for edit form. + if ($view == 'tag' && $layout == 'edit' && !$this->checkEditId('com_tags.edit.tag', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_tags&view=tags', false)); + $this->setRedirect(Route::_('index.php?option=com_tags&view=tags', false)); - return false; - } + return false; + } - parent::display(); + parent::display(); - return $this; - } + return $this; + } } diff --git a/administrator/components/com_tags/src/Controller/TagController.php b/administrator/components/com_tags/src/Controller/TagController.php index 1abe0e5f90aba..26485516e0533 100644 --- a/administrator/components/com_tags/src/Controller/TagController.php +++ b/administrator/components/com_tags/src/Controller/TagController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Tags\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Tags\Administrator\Controller; use Joomla\CMS\MVC\Controller\FormController; use Joomla\CMS\Versioning\VersionableControllerTrait; @@ -20,57 +20,57 @@ */ class TagController extends FormController { - use VersionableControllerTrait; + use VersionableControllerTrait; - /** - * Method to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 3.1 - */ - protected function allowAdd($data = array()) - { - return $this->app->getIdentity()->authorise('core.create', 'com_tags'); - } + /** + * Method to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 3.1 + */ + protected function allowAdd($data = array()) + { + return $this->app->getIdentity()->authorise('core.create', 'com_tags'); + } - /** - * Method to check if you can edit a record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 3.1 - */ - protected function allowEdit($data = array(), $key = 'id') - { - // Since there is no asset tracking and no categories, revert to the component permissions. - return parent::allowEdit($data, $key); - } + /** + * Method to check if you can edit a record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 3.1 + */ + protected function allowEdit($data = array(), $key = 'id') + { + // Since there is no asset tracking and no categories, revert to the component permissions. + return parent::allowEdit($data, $key); + } - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True if successful, false otherwise and internal error is set. - * - * @since 3.1 - */ - public function batch($model = null) - { - $this->checkToken(); + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True if successful, false otherwise and internal error is set. + * + * @since 3.1 + */ + public function batch($model = null) + { + $this->checkToken(); - // Set the model - $model = $this->getModel('Tag'); + // Set the model + $model = $this->getModel('Tag'); - // Preset the redirect - $this->setRedirect('index.php?option=com_tags&view=tags'); + // Preset the redirect + $this->setRedirect('index.php?option=com_tags&view=tags'); - return parent::batch($model); - } + return parent::batch($model); + } } diff --git a/administrator/components/com_tags/src/Controller/TagsController.php b/administrator/components/com_tags/src/Controller/TagsController.php index 78c4fefd802cb..e642d9041ff86 100644 --- a/administrator/components/com_tags/src/Controller/TagsController.php +++ b/administrator/components/com_tags/src/Controller/TagsController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Rebuild the nested set tree. - * - * @return boolean False on failure or error, true on success. - * - * @since 3.1 - */ - public function rebuild() - { - $this->checkToken(); - - $this->setRedirect(Route::_('index.php?option=com_tags&view=tags', false)); - - /** @var \Joomla\Component\Tags\Administrator\Model\TagModel $model */ - $model = $this->getModel(); - - if ($model->rebuild()) - { - // Rebuild succeeded. - $this->setMessage(Text::_('COM_TAGS_REBUILD_SUCCESS')); - - return true; - } - else - { - // Rebuild failed. - $this->setMessage(Text::_('COM_TAGS_REBUILD_FAILURE')); - - return false; - } - } - - /** - * Method to get the JSON-encoded amount of published tags for quickicons - * - * @return void - * - * @since 4.1.0 - */ - public function getQuickiconContent() - { - $model = $this->getModel('tags'); - - $model->setState('filter.published', 1); - - $amount = (int) $model->getTotal(); - - $result = []; - - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_TAGS_N_QUICKICON_SRONLY', $amount); - $result['name'] = Text::plural('COM_TAGS_N_QUICKICON', $amount); - - echo new JsonResponse($result); - } + /** + * Proxy for getModel + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config An optional associative array of configuration settings. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 3.1 + */ + public function getModel($name = 'Tag', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Rebuild the nested set tree. + * + * @return boolean False on failure or error, true on success. + * + * @since 3.1 + */ + public function rebuild() + { + $this->checkToken(); + + $this->setRedirect(Route::_('index.php?option=com_tags&view=tags', false)); + + /** @var \Joomla\Component\Tags\Administrator\Model\TagModel $model */ + $model = $this->getModel(); + + if ($model->rebuild()) { + // Rebuild succeeded. + $this->setMessage(Text::_('COM_TAGS_REBUILD_SUCCESS')); + + return true; + } else { + // Rebuild failed. + $this->setMessage(Text::_('COM_TAGS_REBUILD_FAILURE')); + + return false; + } + } + + /** + * Method to get the JSON-encoded amount of published tags for quickicons + * + * @return void + * + * @since 4.1.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('tags'); + + $model->setState('filter.published', 1); + + $amount = (int) $model->getTotal(); + + $result = []; + + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_TAGS_N_QUICKICON_SRONLY', $amount); + $result['name'] = Text::plural('COM_TAGS_N_QUICKICON', $amount); + + echo new JsonResponse($result); + } } diff --git a/administrator/components/com_tags/src/Extension/TagsComponent.php b/administrator/components/com_tags/src/Extension/TagsComponent.php index 4ca170b8e0082..de46fd2425dda 100644 --- a/administrator/components/com_tags/src/Extension/TagsComponent.php +++ b/administrator/components/com_tags/src/Extension/TagsComponent.php @@ -1,4 +1,5 @@ 'batchAccess', - 'language_id' => 'batchLanguage', - ); - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. - * - * @since 3.1 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->published != -2) - { - return false; - } - - return parent::canDelete($record); - } - - /** - * Auto-populate the model state. - * - * @note Calling getState in this method will result in recursion. - * - * @return void - * - * @since 3.1 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - $parentId = $app->input->getInt('parent_id'); - $this->setState('tag.parent_id', $parentId); - - // Load the User state. - $pk = $app->input->getInt('id'); - $this->setState($this->getName() . '.id', $pk); - - // Load the parameters. - $params = ComponentHelper::getParams('com_tags'); - $this->setState('params', $params); - } - - /** - * Method to get a tag. - * - * @param integer $pk An optional id of the object to get, otherwise the id from the model state is used. - * - * @return mixed Tag data object on success, false on failure. - * - * @since 3.1 - */ - public function getItem($pk = null) - { - if ($result = parent::getItem($pk)) - { - // Prime required properties. - if (empty($result->id)) - { - $result->parent_id = $this->getState('tag.parent_id'); - } - - // Convert the metadata field to an array. - $registry = new Registry($result->metadata); - $result->metadata = $registry->toArray(); - - // Convert the images field to an array. - $registry = new Registry($result->images); - $result->images = $registry->toArray(); - - // Convert the urls field to an array. - $registry = new Registry($result->urls); - $result->urls = $registry->toArray(); - - // Convert the modified date to local user time for display in the form. - $tz = new \DateTimeZone(Factory::getApplication()->get('offset')); - - if ((int) $result->modified_time) - { - $date = new Date($result->modified_time); - $date->setTimezone($tz); - $result->modified_time = $date->toSql(true); - } - else - { - $result->modified_time = null; - } - } - - return $result; - } - - /** - * Method to get the row form. - * - * @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 bool|\Joomla\CMS\Form\Form A Form object on success, false on failure - * - * @since 3.1 - */ - public function getForm($data = array(), $loadData = true) - { - $jinput = Factory::getApplication()->input; - - // Get the form. - $form = $this->loadForm('com_tags.tag', 'tag', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - $user = Factory::getUser(); - - if (!$user->authorise('core.edit.state', 'com_tags' . $jinput->get('id'))) - { - // Disable fields for display. - $form->setFieldAttribute('ordering', 'disabled', 'true'); - $form->setFieldAttribute('published', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('ordering', 'filter', 'unset'); - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 3.1 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_tags.edit.tag.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_tags.tag', $data); - - return $data; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function save($data) - { - /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ - $table = $this->getTable(); - $input = Factory::getApplication()->input; - $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState($this->getName() . '.id'); - $isNew = true; - $context = $this->option . '.' . $this->name; - - // Include the plugins for the save events. - PluginHelper::importPlugin($this->events_map['save']); - - try - { - // Load the row if saving an existing tag. - if ($pk > 0) - { - $table->load($pk); - $isNew = false; - } - - // Set the new parent id if parent id not matched OR while New/Save as Copy . - if ($table->parent_id != $data['parent_id'] || $data['id'] == 0) - { - $table->setLocation($data['parent_id'], 'last-child'); - } - - // Alter the title for save as copy - if ($input->get('task') == 'save2copy') - { - $origTable = $this->getTable(); - $origTable->load($input->getInt('id')); - - if ($data['title'] == $origTable->title) - { - list($title, $alias) = $this->generateNewTitle($data['parent_id'], $data['alias'], $data['title']); - $data['title'] = $title; - $data['alias'] = $alias; - } - elseif ($data['alias'] == $origTable->alias) - { - $data['alias'] = ''; - } - - $data['published'] = 0; - } - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Prepare the row for saving - $this->prepareTable($table); - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, $table, $isNew, $data)); - - if (in_array(false, $result, true)) - { - $this->setError($table->getError()); - - return false; - } - - // Store the data. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the after save event. - Factory::getApplication()->triggerEvent($this->event_after_save, array($context, $table, $isNew)); - - // Rebuild the path for the tag: - if (!$table->rebuildPath($table->id)) - { - $this->setError($table->getError()); - - return false; - } - - // Rebuild the paths of the tag's children: - if (!$table->rebuild($table->id, $table->lft, $table->level, $table->path)) - { - $this->setError($table->getError()); - - return false; - } - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - $this->setState($this->getName() . '.id', $table->id); - $this->setState($this->getName() . '.new', $isNew); - - // Clear the cache - $this->cleanCache(); - - return true; - } - - /** - * Prepare and sanitise the table data prior to saving. - * - * @param \Joomla\CMS\Table\Table $table A Table object. - * - * @return void - * - * @since 1.6 - */ - protected function prepareTable($table) - { - // Increment the content version number. - $table->version++; - } - - /** - * Method rebuild the entire nested set tree. - * - * @return boolean False on failure or error, true otherwise. - * - * @since 3.1 - */ - public function rebuild() - { - // Get an instance of the table object. - /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ - - $table = $this->getTable(); - - if (!$table->rebuild()) - { - $this->setError($table->getError()); - - return false; - } - - // Clear the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to save the reordered nested set tree. - * First we save the new order values in the lft values of the changed ids. - * Then we invoke the table rebuild to implement the new ordering. - * - * @param array $idArray An array of primary key ids. - * @param integer $lftArray The lft value - * - * @return boolean False on failure or error, True otherwise - * - * @since 3.1 - */ - public function saveorder($idArray = null, $lftArray = null) - { - // Get an instance of the table object. - /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ - - $table = $this->getTable(); - - if (!$table->saveorder($idArray, $lftArray)) - { - $this->setError($table->getError()); - - return false; - } - - // Clear the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to change the title & alias. - * - * @param integer $parentId The id of the parent. - * @param string $alias The alias. - * @param string $title The title. - * - * @return array Contains the modified title and alias. - * - * @since 3.1 - */ - protected function generateNewTitle($parentId, $alias, $title) - { - // Alter the title & alias - /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ - - $table = $this->getTable(); - - while ($table->load(array('alias' => $alias, 'parent_id' => $parentId))) - { - $title = ($table->title != $title) ? $title : StringHelper::increment($title); - $alias = StringHelper::increment($alias, 'dash'); - } - - return array($title, $alias); - } + use VersionableModelTrait; + + /** + * @var string The prefix to use with controller messages. + * @since 3.1 + */ + protected $text_prefix = 'COM_TAGS'; + + /** + * @var string The type alias for this content type. + * @since 3.2 + */ + public $typeAlias = 'com_tags.tag'; + + /** + * Allowed batch commands + * + * @var array + * @since 3.7.0 + */ + protected $batch_commands = array( + 'assetgroup_id' => 'batchAccess', + 'language_id' => 'batchLanguage', + ); + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 3.1 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->published != -2) { + return false; + } + + return parent::canDelete($record); + } + + /** + * Auto-populate the model state. + * + * @note Calling getState in this method will result in recursion. + * + * @return void + * + * @since 3.1 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + $parentId = $app->input->getInt('parent_id'); + $this->setState('tag.parent_id', $parentId); + + // Load the User state. + $pk = $app->input->getInt('id'); + $this->setState($this->getName() . '.id', $pk); + + // Load the parameters. + $params = ComponentHelper::getParams('com_tags'); + $this->setState('params', $params); + } + + /** + * Method to get a tag. + * + * @param integer $pk An optional id of the object to get, otherwise the id from the model state is used. + * + * @return mixed Tag data object on success, false on failure. + * + * @since 3.1 + */ + public function getItem($pk = null) + { + if ($result = parent::getItem($pk)) { + // Prime required properties. + if (empty($result->id)) { + $result->parent_id = $this->getState('tag.parent_id'); + } + + // Convert the metadata field to an array. + $registry = new Registry($result->metadata); + $result->metadata = $registry->toArray(); + + // Convert the images field to an array. + $registry = new Registry($result->images); + $result->images = $registry->toArray(); + + // Convert the urls field to an array. + $registry = new Registry($result->urls); + $result->urls = $registry->toArray(); + + // Convert the modified date to local user time for display in the form. + $tz = new \DateTimeZone(Factory::getApplication()->get('offset')); + + if ((int) $result->modified_time) { + $date = new Date($result->modified_time); + $date->setTimezone($tz); + $result->modified_time = $date->toSql(true); + } else { + $result->modified_time = null; + } + } + + return $result; + } + + /** + * Method to get the row form. + * + * @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 bool|\Joomla\CMS\Form\Form A Form object on success, false on failure + * + * @since 3.1 + */ + public function getForm($data = array(), $loadData = true) + { + $jinput = Factory::getApplication()->input; + + // Get the form. + $form = $this->loadForm('com_tags.tag', 'tag', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + $user = Factory::getUser(); + + if (!$user->authorise('core.edit.state', 'com_tags' . $jinput->get('id'))) { + // Disable fields for display. + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('published', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 3.1 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_tags.edit.tag.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_tags.tag', $data); + + return $data; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function save($data) + { + /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ + $table = $this->getTable(); + $input = Factory::getApplication()->input; + $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState($this->getName() . '.id'); + $isNew = true; + $context = $this->option . '.' . $this->name; + + // Include the plugins for the save events. + PluginHelper::importPlugin($this->events_map['save']); + + try { + // Load the row if saving an existing tag. + if ($pk > 0) { + $table->load($pk); + $isNew = false; + } + + // Set the new parent id if parent id not matched OR while New/Save as Copy . + if ($table->parent_id != $data['parent_id'] || $data['id'] == 0) { + $table->setLocation($data['parent_id'], 'last-child'); + } + + // Alter the title for save as copy + if ($input->get('task') == 'save2copy') { + $origTable = $this->getTable(); + $origTable->load($input->getInt('id')); + + if ($data['title'] == $origTable->title) { + list($title, $alias) = $this->generateNewTitle($data['parent_id'], $data['alias'], $data['title']); + $data['title'] = $title; + $data['alias'] = $alias; + } elseif ($data['alias'] == $origTable->alias) { + $data['alias'] = ''; + } + + $data['published'] = 0; + } + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Prepare the row for saving + $this->prepareTable($table); + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, $table, $isNew, $data)); + + if (in_array(false, $result, true)) { + $this->setError($table->getError()); + + return false; + } + + // Store the data. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the after save event. + Factory::getApplication()->triggerEvent($this->event_after_save, array($context, $table, $isNew)); + + // Rebuild the path for the tag: + if (!$table->rebuildPath($table->id)) { + $this->setError($table->getError()); + + return false; + } + + // Rebuild the paths of the tag's children: + if (!$table->rebuild($table->id, $table->lft, $table->level, $table->path)) { + $this->setError($table->getError()); + + return false; + } + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + $this->setState($this->getName() . '.id', $table->id); + $this->setState($this->getName() . '.new', $isNew); + + // Clear the cache + $this->cleanCache(); + + return true; + } + + /** + * Prepare and sanitise the table data prior to saving. + * + * @param \Joomla\CMS\Table\Table $table A Table object. + * + * @return void + * + * @since 1.6 + */ + protected function prepareTable($table) + { + // Increment the content version number. + $table->version++; + } + + /** + * Method rebuild the entire nested set tree. + * + * @return boolean False on failure or error, true otherwise. + * + * @since 3.1 + */ + public function rebuild() + { + // Get an instance of the table object. + /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ + + $table = $this->getTable(); + + if (!$table->rebuild()) { + $this->setError($table->getError()); + + return false; + } + + // Clear the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to save the reordered nested set tree. + * First we save the new order values in the lft values of the changed ids. + * Then we invoke the table rebuild to implement the new ordering. + * + * @param array $idArray An array of primary key ids. + * @param integer $lftArray The lft value + * + * @return boolean False on failure or error, True otherwise + * + * @since 3.1 + */ + public function saveorder($idArray = null, $lftArray = null) + { + // Get an instance of the table object. + /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ + + $table = $this->getTable(); + + if (!$table->saveorder($idArray, $lftArray)) { + $this->setError($table->getError()); + + return false; + } + + // Clear the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to change the title & alias. + * + * @param integer $parentId The id of the parent. + * @param string $alias The alias. + * @param string $title The title. + * + * @return array Contains the modified title and alias. + * + * @since 3.1 + */ + protected function generateNewTitle($parentId, $alias, $title) + { + // Alter the title & alias + /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ + + $table = $this->getTable(); + + while ($table->load(array('alias' => $alias, 'parent_id' => $parentId))) { + $title = ($table->title != $title) ? $title : StringHelper::increment($title); + $alias = StringHelper::increment($alias, 'dash'); + } + + return array($title, $alias); + } } diff --git a/administrator/components/com_tags/src/Model/TagsModel.php b/administrator/components/com_tags/src/Model/TagsModel.php index 254075adcd502..81738cc0ca69c 100644 --- a/administrator/components/com_tags/src/Model/TagsModel.php +++ b/administrator/components/com_tags/src/Model/TagsModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest($this->context . '.filter.extension', 'extension', 'com_content', 'cmd'); - - $this->setState('filter.extension', $extension); - $parts = explode('.', $extension); - - // Extract the component name - $this->setState('filter.component', $parts[0]); - - // Extract the optional section name - $this->setState('filter.section', (count($parts) > 1) ? $parts[1] : null); - - // Load the parameters. - $params = ComponentHelper::getParams('com_tags'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 3.1 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.extension'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.level'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.language'); - - return parent::getStoreId($id); - } - - /** - * Method to create a query for a list of items. - * - * @return string - * - * @since 3.1 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $user = Factory::getUser(); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.id, a.title, a.alias, a.note, a.published, a.access, a.description' . - ', a.checked_out, a.checked_out_time, a.created_user_id' . - ', a.path, a.parent_id, a.level, a.lft, a.rgt' . - ', a.language' - ) - ); - $query->from($db->quoteName('#__tags', 'a')) - ->where($db->quoteName('a.alias') . ' <> ' . $db->quote('root')); - - // Join over the language - $query->select( - [ - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image', 'language_image'), - ] - ) - ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); - - // Join over the users for the checked out user. - $query->select($db->quoteName('uc.name', 'editor')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); - - // Join over the users for the author. - $query->select($db->quoteName('ua.name', 'author_name')) - ->join('LEFT', $db->quoteName('#__users', 'ua'), $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_user_id')) - ->select($db->quoteName('ug.title', 'access_title')) - ->join('LEFT', $db->quoteName('#__viewlevels', 'ug'), $db->quoteName('ug.id') . ' = ' . $db->quoteName('a.access')); - - // Count Items - $subQueryCountTaggedItems = $db->getQuery(true); - $subQueryCountTaggedItems - ->select('COUNT(' . $db->quoteName('tag_map.content_item_id') . ')') - ->from($db->quoteName('#__contentitem_tag_map', 'tag_map')) - ->where($db->quoteName('tag_map.tag_id') . ' = ' . $db->quoteName('a.id')); - $query->select('(' . (string) $subQueryCountTaggedItems . ') AS ' . $db->quoteName('countTaggedItems')); - - // Filter on the level. - if ($level = (int) $this->getState('filter.level')) - { - $query->where($db->quoteName('a.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } - - // Filter by access level. - if ($access = (int) $this->getState('filter.access')) - { - $query->where($db->quoteName('a.access') . ' = :access') - ->bind(':access', $access, ParameterType::INTEGER); - } - - // Implement View Level Access - if (!$user->authorise('core.admin')) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('a.access'), $groups); - } - - // Filter by published state - $published = (string) $this->getState('filter.published'); - - if (is_numeric($published)) - { - $published = (int) $published; - $query->where($db->quoteName('a.published') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - } - elseif ($published === '') - { - $query->whereIn($db->quoteName('a.published'), [0, 1]); - } - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.title') . ' LIKE :title', - $db->quoteName('a.alias') . ' LIKE :alias', - $db->quoteName('a.note') . ' LIKE :note', - - ], - 'OR' - ); - $query->bind(':title', $search) - ->bind(':alias', $search) - ->bind(':note', $search); - } - } - - // Filter on the language. - if ($language = $this->getState('filter.language')) - { - $query->where($db->quoteName('a.language') . ' = :language') - ->bind(':language', $language); - } - - // Add the list ordering clause - $listOrdering = $this->getState('list.ordering', 'a.lft'); - $listDirn = $db->escape($this->getState('list.direction', 'ASC')); - - if ($listOrdering == 'a.access') - { - $query->order('a.access ' . $listDirn . ', a.lft ' . $listDirn); - } - else - { - $query->order($db->escape($listOrdering) . ' ' . $listDirn); - } - - return $query; - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 3.0.1 - */ - public function getItems() - { - $items = parent::getItems(); - - if ($items != false) - { - $extension = $this->getState('filter.extension'); - - $this->countItems($items, $extension); - } - - return $items; - } - - /** - * Method to load the countItems method from the extensions - * - * @param \stdClass[] &$items The category items - * @param string $extension The category extension - * - * @return void - * - * @since 3.5 - */ - public function countItems(&$items, $extension) - { - $parts = explode('.', $extension); - - if (count($parts) < 2) - { - return; - } - - $component = Factory::getApplication()->bootComponent($parts[0]); - - if ($component instanceof TagServiceInterface) - { - $component->countTagItems($items, $extension); - } - } - - /** - * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. - * - * @return DatabaseQuery - * - * @since 4.0.0 - */ - protected function getEmptyStateQuery() - { - $query = parent::getEmptyStateQuery(); - - $db = $this->getDatabase(); - - $query->where($db->quoteName('alias') . ' != ' . $db->quote('root')); - - return $query; - } + /** + * Constructor. + * + * @param MVCFactoryInterface $factory The factory. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', + 'a.id', + 'title', + 'a.title', + 'alias', + 'a.alias', + 'published', + 'a.published', + 'access', + 'a.access', + 'access_level', + 'language', + 'a.language', + 'checked_out', + 'a.checked_out', + 'checked_out_time', + 'a.checked_out_time', + 'created_time', + 'a.created_time', + 'created_user_id', + 'a.created_user_id', + 'lft', + 'a.lft', + 'rgt', + 'a.rgt', + 'level', + 'a.level', + 'path', + 'a.path', + 'countTaggedItems', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.1 + */ + protected function populateState($ordering = 'a.lft', $direction = 'asc') + { + $extension = $this->getUserStateFromRequest($this->context . '.filter.extension', 'extension', 'com_content', 'cmd'); + + $this->setState('filter.extension', $extension); + $parts = explode('.', $extension); + + // Extract the component name + $this->setState('filter.component', $parts[0]); + + // Extract the optional section name + $this->setState('filter.section', (count($parts) > 1) ? $parts[1] : null); + + // Load the parameters. + $params = ComponentHelper::getParams('com_tags'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 3.1 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.extension'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.level'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.language'); + + return parent::getStoreId($id); + } + + /** + * Method to create a query for a list of items. + * + * @return string + * + * @since 3.1 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $user = Factory::getUser(); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.id, a.title, a.alias, a.note, a.published, a.access, a.description' . + ', a.checked_out, a.checked_out_time, a.created_user_id' . + ', a.path, a.parent_id, a.level, a.lft, a.rgt' . + ', a.language' + ) + ); + $query->from($db->quoteName('#__tags', 'a')) + ->where($db->quoteName('a.alias') . ' <> ' . $db->quote('root')); + + // Join over the language + $query->select( + [ + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image', 'language_image'), + ] + ) + ->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.language')); + + // Join over the users for the checked out user. + $query->select($db->quoteName('uc.name', 'editor')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('a.checked_out')); + + // Join over the users for the author. + $query->select($db->quoteName('ua.name', 'author_name')) + ->join('LEFT', $db->quoteName('#__users', 'ua'), $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_user_id')) + ->select($db->quoteName('ug.title', 'access_title')) + ->join('LEFT', $db->quoteName('#__viewlevels', 'ug'), $db->quoteName('ug.id') . ' = ' . $db->quoteName('a.access')); + + // Count Items + $subQueryCountTaggedItems = $db->getQuery(true); + $subQueryCountTaggedItems + ->select('COUNT(' . $db->quoteName('tag_map.content_item_id') . ')') + ->from($db->quoteName('#__contentitem_tag_map', 'tag_map')) + ->where($db->quoteName('tag_map.tag_id') . ' = ' . $db->quoteName('a.id')); + $query->select('(' . (string) $subQueryCountTaggedItems . ') AS ' . $db->quoteName('countTaggedItems')); + + // Filter on the level. + if ($level = (int) $this->getState('filter.level')) { + $query->where($db->quoteName('a.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } + + // Filter by access level. + if ($access = (int) $this->getState('filter.access')) { + $query->where($db->quoteName('a.access') . ' = :access') + ->bind(':access', $access, ParameterType::INTEGER); + } + + // Implement View Level Access + if (!$user->authorise('core.admin')) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('a.access'), $groups); + } + + // Filter by published state + $published = (string) $this->getState('filter.published'); + + if (is_numeric($published)) { + $published = (int) $published; + $query->where($db->quoteName('a.published') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + } elseif ($published === '') { + $query->whereIn($db->quoteName('a.published'), [0, 1]); + } + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.title') . ' LIKE :title', + $db->quoteName('a.alias') . ' LIKE :alias', + $db->quoteName('a.note') . ' LIKE :note', + + ], + 'OR' + ); + $query->bind(':title', $search) + ->bind(':alias', $search) + ->bind(':note', $search); + } + } + + // Filter on the language. + if ($language = $this->getState('filter.language')) { + $query->where($db->quoteName('a.language') . ' = :language') + ->bind(':language', $language); + } + + // Add the list ordering clause + $listOrdering = $this->getState('list.ordering', 'a.lft'); + $listDirn = $db->escape($this->getState('list.direction', 'ASC')); + + if ($listOrdering == 'a.access') { + $query->order('a.access ' . $listDirn . ', a.lft ' . $listDirn); + } else { + $query->order($db->escape($listOrdering) . ' ' . $listDirn); + } + + return $query; + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 3.0.1 + */ + public function getItems() + { + $items = parent::getItems(); + + if ($items != false) { + $extension = $this->getState('filter.extension'); + + $this->countItems($items, $extension); + } + + return $items; + } + + /** + * Method to load the countItems method from the extensions + * + * @param \stdClass[] &$items The category items + * @param string $extension The category extension + * + * @return void + * + * @since 3.5 + */ + public function countItems(&$items, $extension) + { + $parts = explode('.', $extension); + + if (count($parts) < 2) { + return; + } + + $component = Factory::getApplication()->bootComponent($parts[0]); + + if ($component instanceof TagServiceInterface) { + $component->countTagItems($items, $extension); + } + } + + /** + * Manipulate the query to be used to evaluate if this is an Empty State to provide specific conditions for this extension. + * + * @return DatabaseQuery + * + * @since 4.0.0 + */ + protected function getEmptyStateQuery() + { + $query = parent::getEmptyStateQuery(); + + $db = $this->getDatabase(); + + $query->where($db->quoteName('alias') . ' != ' . $db->quote('root')); + + return $query; + } } diff --git a/administrator/components/com_tags/src/Table/TagTable.php b/administrator/components/com_tags/src/Table/TagTable.php index 5e65a182664ae..8c74add08c42e 100644 --- a/administrator/components/com_tags/src/Table/TagTable.php +++ b/administrator/components/com_tags/src/Table/TagTable.php @@ -1,4 +1,5 @@ typeAlias = 'com_tags.tag'; - - parent::__construct('#__tags', 'id', $db); - } - - /** - * Overloaded check method to ensure data integrity. - * - * @return boolean True on success. - * - * @since 3.1 - * @throws \UnexpectedValueException - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Check for valid name. - if (trim($this->title) == '') - { - throw new \UnexpectedValueException('The title is empty'); - } - - if (empty($this->alias)) - { - $this->alias = $this->title; - } - - $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); - - if (trim(str_replace('-', '', $this->alias)) == '') - { - $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); - } - - // Check the publish down date is not earlier than publish up. - if (!empty($this->publish_down) && !empty($this->publish_up) && $this->publish_down < $this->publish_up) - { - throw new \UnexpectedValueException('End publish date is before start publish date.'); - } - - // Clean up description -- eliminate quotes and <> brackets - if (!empty($this->metadesc)) - { - // Only process if not empty - $bad_characters = array("\"", '<', '>'); - $this->metadesc = StringHelper::str_ireplace($bad_characters, '', $this->metadesc); - } - - if (empty($this->path)) - { - $this->path = ''; - } - - if (empty($this->hits)) - { - $this->hits = 0; - } - - if (empty($this->params)) - { - $this->params = '{}'; - } - - if (empty($this->metadesc)) - { - $this->metadesc = ''; - } - - if (empty($this->metakey)) - { - $this->metakey = ''; - } - - if (empty($this->metadata)) - { - $this->metadata = '{}'; - } - - if (empty($this->urls)) - { - $this->urls = '{}'; - } - - if (empty($this->images)) - { - $this->images = '{}'; - } - - if (!(int) $this->checked_out_time) - { - $this->checked_out_time = null; - } - - if (!(int) $this->publish_up) - { - $this->publish_up = null; - } - - if (!(int) $this->publish_down) - { - $this->publish_down = null; - } - - return true; - } - - /** - * Overridden \JTable::store to set modified data and user id. - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function store($updateNulls = true) - { - $date = Factory::getDate(); - $user = Factory::getUser(); - - if ($this->id) - { - // Existing item - $this->modified_user_id = $user->get('id'); - $this->modified_time = $date->toSql(); - } - else - { - // New tag. A tag created and created_by field can be set by the user, - // so we don't touch either of these if they are set. - if (!(int) $this->created_time) - { - $this->created_time = $date->toSql(); - } - - if (empty($this->created_user_id)) - { - $this->created_user_id = $user->get('id'); - } - - if (!(int) $this->modified_time) - { - $this->modified_time = $this->created_time; - } - - if (empty($this->modified_user_id)) - { - $this->modified_user_id = $this->created_user_id; - } - } - - // Verify that the alias is unique - $table = new static($this->getDbo()); - - if ($table->load(array('alias' => $this->alias)) && ($table->id != $this->id || $this->id == 0)) - { - $this->setError(Text::_('COM_TAGS_ERROR_UNIQUE_ALIAS')); - - return false; - } - - return parent::store($updateNulls); - } - - /** - * Method to delete a node and, optionally, its child nodes from the table. - * - * @param integer $pk The primary key of the node to delete. - * @param boolean $children True to delete child nodes, false to move them up a level. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function delete($pk = null, $children = false) - { - $return = parent::delete($pk, $children); - - if ($return) - { - $helper = new TagsHelper; - $helper->tagDeleteInstances($pk); - } - - return $return; - } - - /** - * Get the type alias for the history table - * - * @return string The alias as described above - * - * @since 4.0.0 - */ - public function getTypeAlias() - { - return $this->typeAlias; - } + /** + * An array of key names to be json encoded in the bind function + * + * @var array + * @since 4.0.0 + */ + protected $_jsonEncode = ['params', 'metadata', 'urls', 'images']; + + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Constructor + * + * @param DatabaseDriver $db A database connector object + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = 'com_tags.tag'; + + parent::__construct('#__tags', 'id', $db); + } + + /** + * Overloaded check method to ensure data integrity. + * + * @return boolean True on success. + * + * @since 3.1 + * @throws \UnexpectedValueException + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Check for valid name. + if (trim($this->title) == '') { + throw new \UnexpectedValueException('The title is empty'); + } + + if (empty($this->alias)) { + $this->alias = $this->title; + } + + $this->alias = ApplicationHelper::stringURLSafe($this->alias, $this->language); + + if (trim(str_replace('-', '', $this->alias)) == '') { + $this->alias = Factory::getDate()->format('Y-m-d-H-i-s'); + } + + // Check the publish down date is not earlier than publish up. + if (!empty($this->publish_down) && !empty($this->publish_up) && $this->publish_down < $this->publish_up) { + throw new \UnexpectedValueException('End publish date is before start publish date.'); + } + + // Clean up description -- eliminate quotes and <> brackets + if (!empty($this->metadesc)) { + // Only process if not empty + $bad_characters = array("\"", '<', '>'); + $this->metadesc = StringHelper::str_ireplace($bad_characters, '', $this->metadesc); + } + + if (empty($this->path)) { + $this->path = ''; + } + + if (empty($this->hits)) { + $this->hits = 0; + } + + if (empty($this->params)) { + $this->params = '{}'; + } + + if (empty($this->metadesc)) { + $this->metadesc = ''; + } + + if (empty($this->metakey)) { + $this->metakey = ''; + } + + if (empty($this->metadata)) { + $this->metadata = '{}'; + } + + if (empty($this->urls)) { + $this->urls = '{}'; + } + + if (empty($this->images)) { + $this->images = '{}'; + } + + if (!(int) $this->checked_out_time) { + $this->checked_out_time = null; + } + + if (!(int) $this->publish_up) { + $this->publish_up = null; + } + + if (!(int) $this->publish_down) { + $this->publish_down = null; + } + + return true; + } + + /** + * Overridden \JTable::store to set modified data and user id. + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function store($updateNulls = true) + { + $date = Factory::getDate(); + $user = Factory::getUser(); + + if ($this->id) { + // Existing item + $this->modified_user_id = $user->get('id'); + $this->modified_time = $date->toSql(); + } else { + // New tag. A tag created and created_by field can be set by the user, + // so we don't touch either of these if they are set. + if (!(int) $this->created_time) { + $this->created_time = $date->toSql(); + } + + if (empty($this->created_user_id)) { + $this->created_user_id = $user->get('id'); + } + + if (!(int) $this->modified_time) { + $this->modified_time = $this->created_time; + } + + if (empty($this->modified_user_id)) { + $this->modified_user_id = $this->created_user_id; + } + } + + // Verify that the alias is unique + $table = new static($this->getDbo()); + + if ($table->load(array('alias' => $this->alias)) && ($table->id != $this->id || $this->id == 0)) { + $this->setError(Text::_('COM_TAGS_ERROR_UNIQUE_ALIAS')); + + return false; + } + + return parent::store($updateNulls); + } + + /** + * Method to delete a node and, optionally, its child nodes from the table. + * + * @param integer $pk The primary key of the node to delete. + * @param boolean $children True to delete child nodes, false to move them up a level. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function delete($pk = null, $children = false) + { + $return = parent::delete($pk, $children); + + if ($return) { + $helper = new TagsHelper(); + $helper->tagDeleteInstances($pk); + } + + return $return; + } + + /** + * Get the type alias for the history table + * + * @return string The alias as described above + * + * @since 4.0.0 + */ + public function getTypeAlias() + { + return $this->typeAlias; + } } diff --git a/administrator/components/com_tags/src/View/Tag/HtmlView.php b/administrator/components/com_tags/src/View/Tag/HtmlView.php index 80a2745b25dfc..905e9ad436541 100644 --- a/administrator/components/com_tags/src/View/Tag/HtmlView.php +++ b/administrator/components/com_tags/src/View/Tag/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @since 3.1 - * - * @return void - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = $this->getCurrentUser(); - $userId = $user->get('id'); - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); - - $canDo = ContentHelper::getActions('com_tags'); - - ToolbarHelper::title($isNew ? Text::_('COM_TAGS_MANAGER_TAG_NEW') : Text::_('COM_TAGS_MANAGER_TAG_EDIT'), 'tag'); - - // Build the actions for new and existing records. - if ($isNew) - { - ToolbarHelper::apply('tag.apply'); - ToolbarHelper::saveGroup( - [ - ['save', 'tag.save'], - ['save2new', 'tag.save2new'] - ], - 'btn-success' - ); - - ToolbarHelper::cancel('tag.cancel'); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_user_id == $userId); - - $toolbarButtons = []; - - // Can't save the record if it's checked out and editable - if (!$checkedOut && $itemEditable) - { - ToolbarHelper::apply('tag.apply'); - $toolbarButtons[] = ['save', 'tag.save']; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'tag.save2new']; - } - } - - // If checked out, we can still save - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'tag.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel('tag.cancel', 'JTOOLBAR_CLOSE'); - - if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $itemEditable) - { - ToolbarHelper::versions('com_tags.tag', $this->item->id); - } - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Tags:_New_or_Edit'); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * Flag if an association exists + * + * @var boolean + */ + protected $assoc; + + /** + * The actions the user is authorised to perform + * + * @var CMSObject + * + * @since 4.0.0 + */ + protected $canDo; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @since 3.1 + * + * @return void + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $userId = $user->get('id'); + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $userId); + + $canDo = ContentHelper::getActions('com_tags'); + + ToolbarHelper::title($isNew ? Text::_('COM_TAGS_MANAGER_TAG_NEW') : Text::_('COM_TAGS_MANAGER_TAG_EDIT'), 'tag'); + + // Build the actions for new and existing records. + if ($isNew) { + ToolbarHelper::apply('tag.apply'); + ToolbarHelper::saveGroup( + [ + ['save', 'tag.save'], + ['save2new', 'tag.save2new'] + ], + 'btn-success' + ); + + ToolbarHelper::cancel('tag.cancel'); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_user_id == $userId); + + $toolbarButtons = []; + + // Can't save the record if it's checked out and editable + if (!$checkedOut && $itemEditable) { + ToolbarHelper::apply('tag.apply'); + $toolbarButtons[] = ['save', 'tag.save']; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'tag.save2new']; + } + } + + // If checked out, we can still save + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'tag.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel('tag.cancel', 'JTOOLBAR_CLOSE'); + + if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $itemEditable) { + ToolbarHelper::versions('com_tags.tag', $this->item->id); + } + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Tags:_New_or_Edit'); + } } diff --git a/administrator/components/com_tags/src/View/Tags/HtmlView.php b/administrator/components/com_tags/src/View/Tags/HtmlView.php index 3616b1fbcaf5e..3f92a51ba1e1f 100644 --- a/administrator/components/com_tags/src/View/Tags/HtmlView.php +++ b/administrator/components/com_tags/src/View/Tags/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Preprocess the list of items to find ordering divisions. - foreach ($this->items as &$item) - { - $this->ordering[$item->parent_id][] = $item->id; - } - - // We don't need toolbar in the modal window. - if ($this->getLayout() !== 'modal') - { - $this->addToolbar(); - - // We do not need to filter by language when multilingual is disabled - if (!Multilanguage::isEnabled()) - { - unset($this->activeFilters['language']); - $this->filterForm->removeField('language', 'filter'); - } - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 3.1 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_tags'); - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_TAGS_MANAGER_TAGS'), 'tags'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('tag.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('tags.publish')->listCheck(true); - $childBar->unpublish('tags.unpublish')->listCheck(true); - $childBar->archive('tags.archive')->listCheck(true); - } - - if ($user->authorise('core.admin')) - { - $childBar->checkin('tags.checkin')->listCheck(true); - } - - if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) - { - $childBar->trash('tags.trash')->listCheck(true); - } - - // Add a batch button - if ($canDo->get('core.create') && $canDo->get('core.edit') && $canDo->get('core.edit.state')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - } - - if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('tags.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences('com_tags'); - } - - $toolbar->help('Tags'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Preprocess the list of items to find ordering divisions. + foreach ($this->items as &$item) { + $this->ordering[$item->parent_id][] = $item->id; + } + + // We don't need toolbar in the modal window. + if ($this->getLayout() !== 'modal') { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 3.1 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_tags'); + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_TAGS_MANAGER_TAGS'), 'tags'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('tag.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $user->authorise('core.admin'))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state')) { + $childBar->publish('tags.publish')->listCheck(true); + $childBar->unpublish('tags.unpublish')->listCheck(true); + $childBar->archive('tags.archive')->listCheck(true); + } + + if ($user->authorise('core.admin')) { + $childBar->checkin('tags.checkin')->listCheck(true); + } + + if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) { + $childBar->trash('tags.trash')->listCheck(true); + } + + // Add a batch button + if ($canDo->get('core.create') && $canDo->get('core.edit') && $canDo->get('core.edit.state')) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + } + + if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('tags.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences('com_tags'); + } + + $toolbar->help('Tags'); + } } diff --git a/administrator/components/com_tags/tmpl/tag/edit.php b/administrator/components/com_tags/tmpl/tag/edit.php index d4cd2270515e3..f451c3cb0f425 100644 --- a/administrator/components/com_tags/tmpl/tag/edit.php +++ b/administrator/components/com_tags/tmpl/tag/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); // Fieldsets to not automatically render by /layouts/joomla/edit/params.php $this->ignore_fieldsets = ['jmetadata']; @@ -27,50 +28,50 @@
- + -
- 'details', 'recall' => true, 'breakpoint' => 768]); ?> +
+ 'details', 'recall' => true, 'breakpoint' => 768]); ?> - -
-
-
- form->getLabel('description'); ?> - form->getInput('description'); ?> -
-
-
- -
-
- + +
+
+
+ form->getLabel('description'); ?> + form->getInput('description'); ?> +
+
+
+ +
+
+ - + - -
-
-
- -
- -
-
-
-
-
- -
- -
-
-
-
- + +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+ - -
- - + +
+ +
diff --git a/administrator/components/com_tags/tmpl/tags/default.php b/administrator/components/com_tags/tmpl/tags/default.php index c3e455a2b66dc..b7799fd6df4ba 100644 --- a/administrator/components/com_tags/tmpl/tags/default.php +++ b/administrator/components/com_tags/tmpl/tags/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $app = Factory::getApplication(); $user = Factory::getUser(); @@ -35,249 +36,240 @@ $section = null; $mode = false; -if (count($parts) > 1) -{ - $section = $parts[1]; - $inflector = Inflector::getInstance(); +if (count($parts) > 1) { + $section = $parts[1]; + $inflector = Inflector::getInstance(); - if (!$inflector->isPlural($section)) - { - $section = $inflector->toPlural($section); - } + if (!$inflector->isPlural($section)) { + $section = $inflector->toPlural($section); + } } -if ($section === 'categories') -{ - $mode = true; - $section = $component; - $component = 'com_categories'; +if ($section === 'categories') { + $mode = true; + $section = $component; + $component = 'com_categories'; } -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_tags&task=tags.saveOrderAjax&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_tags&task=tags.saveOrderAjax&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
-
- $this)); - ?> - items)) : ?> -
- - -
- - - - - - - - - +
+ $this)); + ?> + items)) : ?> +
+ + +
+ +
- , - , - -
- - - - - - - -
+ + + + + + + - items[0]) && property_exists($this->items[0], 'count_published')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_archived')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> - - + items[0]) && property_exists($this->items[0], 'count_published')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_archived')) : ?> + + + items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> + + - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items as $i => $item) : - $orderkey = array_search($item->id, $this->ordering[$item->parent_id]); - $canCreate = $user->authorise('core.create', 'com_tags'); - $canEdit = $user->authorise('core.edit', 'com_tags'); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_tags') && $canCheckin; + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true"> + items as $i => $item) : + $orderkey = array_search($item->id, $this->ordering[$item->parent_id]); + $canCreate = $user->authorise('core.create', 'com_tags'); + $canEdit = $user->authorise('core.edit', 'com_tags'); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_tags') && $canCheckin; - // Get the parents of item for sorting - if ($item->level > 1) - { - $parentsStr = ''; - $_currentParentId = $item->parent_id; - $parentsStr = ' ' . $_currentParentId; - for ($j = 0; $j < $item->level; $j++) - { - foreach ($this->ordering as $k => $v) - { - $v = implode('-', $v); - $v = '-' . $v . '-'; - if (strpos($v, '-' . $_currentParentId . '-') !== false) - { - $parentsStr .= ' ' . $k; - $_currentParentId = $k; - break; - } - } - } - } - else - { - $parentsStr = ''; - } - ?> - - - - - + // Get the parents of item for sorting + if ($item->level > 1) { + $parentsStr = ''; + $_currentParentId = $item->parent_id; + $parentsStr = ' ' . $_currentParentId; + for ($j = 0; $j < $item->level; $j++) { + foreach ($this->ordering as $k => $v) { + $v = implode('-', $v); + $v = '-' . $v . '-'; + if (strpos($v, '-' . $_currentParentId . '-') !== false) { + $parentsStr .= ' ' . $k; + $_currentParentId = $k; + break; + } + } + } + } else { + $parentsStr = ''; + } + ?> + + + + + - items[0]) && property_exists($this->items[0], 'count_published')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_archived')) : ?> - - - items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> - - - - - - - - - - - -
+ , + , + +
+ + + + + + + + - - - - - - - - + + + + + + + + - - - state->get('list.direction'), $this->state->get('list.ordering')); ?> - - - - -
+ + + state->get('list.direction'), $this->state->get('list.ordering')); ?> + + + + +
- id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - published, $i, 'tags.', $canChange); ?> - - $item->level)); ?> - checked_out) : ?> - editor, $item->checked_out_time, 'tags.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - -
- note)) : ?> - escape($item->alias)); ?> - - escape($item->alias), $this->escape($item->note)); ?> - -
-
+ id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + published, $i, 'tags.', $canChange); ?> + + $item->level)); ?> + checked_out) : ?> + editor, $item->checked_out_time, 'tags.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + +
+ note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + +
+
- - count_published; ?> - - - count_unpublished; ?> - - - count_archived; ?> - - - count_trashed; ?> - - escape($item->access_title); ?> - - - - - countTaggedItems; ?> - - - id; ?> -
+ items[0]) && property_exists($this->items[0], 'count_published')) : ?> + + + count_published; ?> + + + items[0]) && property_exists($this->items[0], 'count_unpublished')) : ?> + + + count_unpublished; ?> + + + items[0]) && property_exists($this->items[0], 'count_archived')) : ?> + + + count_archived; ?> + + + items[0]) && property_exists($this->items[0], 'count_trashed')) : ?> + + + count_trashed; ?> + + + + escape($item->access_title); ?> + + + + + + + + + countTaggedItems; ?> + + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - authorise('core.create', 'com_tags') - && $user->authorise('core.edit', 'com_tags') - && $user->authorise('core.edit.state', 'com_tags')) : ?> - Text::_('COM_TAGS_BATCH_OPTIONS'), - 'footer' => $this->loadTemplate('batch_footer'), - ), - $this->loadTemplate('batch_body') - ); ?> - - + + authorise('core.create', 'com_tags') + && $user->authorise('core.edit', 'com_tags') + && $user->authorise('core.edit.state', 'com_tags') +) : ?> + Text::_('COM_TAGS_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ), + $this->loadTemplate('batch_body') + ); ?> + + - - - -
+ + + +
diff --git a/administrator/components/com_tags/tmpl/tags/default_batch_body.php b/administrator/components/com_tags/tmpl/tags/default_batch_body.php index bc82733e4def3..cffa2753fb27e 100644 --- a/administrator/components/com_tags/tmpl/tags/default_batch_body.php +++ b/administrator/components/com_tags/tmpl/tags/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Multilanguage; @@ -15,18 +17,18 @@ ?>
-
- -
-
- -
-
- -
-
- -
-
-
+
+ +
+
+ +
+
+ +
+
+ +
+
+
diff --git a/administrator/components/com_tags/tmpl/tags/default_batch_footer.php b/administrator/components/com_tags/tmpl/tags/default_batch_footer.php index a30292feb9cd3..c448ec1ba0dd8 100644 --- a/administrator/components/com_tags/tmpl/tags/default_batch_footer.php +++ b/administrator/components/com_tags/tmpl/tags/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/administrator/components/com_tags/tmpl/tags/emptystate.php b/administrator/components/com_tags/tmpl/tags/emptystate.php index 04eb96caa97c5..2b49d1b63bcda 100644 --- a/administrator/components/com_tags/tmpl/tags/emptystate.php +++ b/administrator/components/com_tags/tmpl/tags/emptystate.php @@ -1,4 +1,5 @@ 'COM_TAGS', - 'formURL' => 'index.php?option=com_tags&task=tag.add', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/J3.x:How_To_Use_Content_Tags_in_Joomla!', - 'icon' => 'icon-tags tags', + 'textPrefix' => 'COM_TAGS', + 'formURL' => 'index.php?option=com_tags&task=tag.add', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/J3.x:How_To_Use_Content_Tags_in_Joomla!', + 'icon' => 'icon-tags tags', ]; -if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_tags')) -{ - $displayData['createURL'] = 'index.php?option=com_tags&task=tag.add'; +if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_tags')) { + $displayData['createURL'] = 'index.php?option=com_tags&task=tag.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_templates/helpers/template.php b/administrator/components/com_templates/helpers/template.php index 38115180eef74..873db920a2c28 100644 --- a/administrator/components/com_templates/helpers/template.php +++ b/administrator/components/com_templates/helpers/template.php @@ -1,13 +1,16 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; + /** * Template Helper class. diff --git a/administrator/components/com_templates/helpers/templates.php b/administrator/components/com_templates/helpers/templates.php index 6ff305326e12f..5f719455936ad 100644 --- a/administrator/components/com_templates/helpers/templates.php +++ b/administrator/components/com_templates/helpers/templates.php @@ -1,13 +1,16 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; + /** * Templates component helper. diff --git a/administrator/components/com_templates/services/provider.php b/administrator/components/com_templates/services/provider.php index 737e8e5fcd45b..af2dff277bdb8 100644 --- a/administrator/components/com_templates/services/provider.php +++ b/administrator/components/com_templates/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Templates')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Templates')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Templates')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Templates')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new TemplatesComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new TemplatesComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_templates/src/Controller/DisplayController.php b/administrator/components/com_templates/src/Controller/DisplayController.php index d7eec744efa42..a253dcebe27e9 100644 --- a/administrator/components/com_templates/src/Controller/DisplayController.php +++ b/administrator/components/com_templates/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'styles'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param boolean $urlparams An array of safe URL parameters and their variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static|boolean This object to support chaining or false on failure. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $view = $this->input->get('view', 'styles'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); - // For JSON requests - if ($this->app->getDocument()->getType() == 'json') - { - return parent::display(); - } + // For JSON requests + if ($this->app->getDocument()->getType() == 'json') { + return parent::display(); + } - // Check for edit form. - if ($view == 'style' && $layout == 'edit' && !$this->checkEditId('com_templates.edit.style', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } + // Check for edit form. + if ($view == 'style' && $layout == 'edit' && !$this->checkEditId('com_templates.edit.style', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } - $this->setRedirect(Route::_('index.php?option=com_templates&view=styles', false)); + $this->setRedirect(Route::_('index.php?option=com_templates&view=styles', false)); - return false; - } + return false; + } - return parent::display(); - } + return parent::display(); + } } diff --git a/administrator/components/com_templates/src/Controller/StyleController.php b/administrator/components/com_templates/src/Controller/StyleController.php index 748af6bcb6e7b..c324ca8f09eaf 100644 --- a/administrator/components/com_templates/src/Controller/StyleController.php +++ b/administrator/components/com_templates/src/Controller/StyleController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Templates\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Templates\Administrator\Controller; use Joomla\CMS\Form\Form; use Joomla\CMS\Language\Text; @@ -21,135 +21,124 @@ */ class StyleController extends FormController { - /** - * The prefix to use with controller messages. - * - * @var string - * @since 1.6 - */ - protected $text_prefix = 'COM_TEMPLATES_STYLE'; - - /** - * Method to save a template style. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean True if successful, false otherwise. - * - * @since 1.6 - */ - public function save($key = null, $urlVar = null) - { - $this->checkToken(); - - if ($this->app->getDocument()->getType() === 'json') - { - $model = $this->getModel('Style', 'Administrator'); - $table = $model->getTable(); - $data = $this->input->post->get('params', array(), 'array'); - $checkin = $table->hasField('checked_out'); - $context = $this->option . '.edit.' . $this->context; - - $item = $model->getItem($this->app->getTemplate(true)->id); - - // Setting received params - $item->set('params', $data); - - $data = $item->getProperties(); - unset($data['xml']); - - $key = $table->getKeyName(); - - // Access check. - if (!$this->allowSave($data, $key)) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return false; - } - - Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_templates/forms'); - - // Validate the posted data. - // Sometimes the form needs some posted data, such as for plugins and modules. - $form = $model->getForm($data, false); - - if (!$form) - { - $this->app->enqueueMessage($model->getError(), 'error'); - - return false; - } - - // Test whether the data is valid. - $validData = $model->validate($form, $data); - - if ($validData === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $this->app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Save the data in the session. - $this->app->setUserState($context . '.data', $data); - - return false; - } - - if (!isset($validData['tags'])) - { - $validData['tags'] = null; - } - - // Attempt to save the data. - if (!$model->save($validData)) - { - // Save the data in the session. - $this->app->setUserState($context . '.data', $validData); - - $this->app->enqueueMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - - return false; - } - - // Save succeeded, so check-in the record. - if ($checkin && $model->checkin($validData[$key]) === false) - { - // Save the data in the session. - $this->app->setUserState($context . '.data', $validData); - - // Check-in failed, so go back to the record and display a notice. - $this->app->enqueueMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'error'); - - return false; - } - - // Redirect the user and adjust session state - // Set the record data in the session. - $recordId = $model->getState($this->context . '.id'); - $this->holdEditId($context, $recordId); - $this->app->setUserState($context . '.data', null); - $model->checkout($recordId); - - // Invoke the postSave method to allow for the child class to access the model. - $this->postSaveHook($model, $validData); + /** + * The prefix to use with controller messages. + * + * @var string + * @since 1.6 + */ + protected $text_prefix = 'COM_TEMPLATES_STYLE'; + + /** + * Method to save a template style. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 1.6 + */ + public function save($key = null, $urlVar = null) + { + $this->checkToken(); + + if ($this->app->getDocument()->getType() === 'json') { + $model = $this->getModel('Style', 'Administrator'); + $table = $model->getTable(); + $data = $this->input->post->get('params', array(), 'array'); + $checkin = $table->hasField('checked_out'); + $context = $this->option . '.edit.' . $this->context; + + $item = $model->getItem($this->app->getTemplate(true)->id); + + // Setting received params + $item->set('params', $data); + + $data = $item->getProperties(); + unset($data['xml']); + + $key = $table->getKeyName(); + + // Access check. + if (!$this->allowSave($data, $key)) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return false; + } + + Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_templates/forms'); + + // Validate the posted data. + // Sometimes the form needs some posted data, such as for plugins and modules. + $form = $model->getForm($data, false); + + if (!$form) { + $this->app->enqueueMessage($model->getError(), 'error'); + + return false; + } + + // Test whether the data is valid. + $validData = $model->validate($form, $data); + + if ($validData === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $this->app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Save the data in the session. + $this->app->setUserState($context . '.data', $data); + + return false; + } + + if (!isset($validData['tags'])) { + $validData['tags'] = null; + } + + // Attempt to save the data. + if (!$model->save($validData)) { + // Save the data in the session. + $this->app->setUserState($context . '.data', $validData); + + $this->app->enqueueMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); + + return false; + } + + // Save succeeded, so check-in the record. + if ($checkin && $model->checkin($validData[$key]) === false) { + // Save the data in the session. + $this->app->setUserState($context . '.data', $validData); + + // Check-in failed, so go back to the record and display a notice. + $this->app->enqueueMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'error'); + + return false; + } + + // Redirect the user and adjust session state + // Set the record data in the session. + $recordId = $model->getState($this->context . '.id'); + $this->holdEditId($context, $recordId); + $this->app->setUserState($context . '.data', null); + $model->checkout($recordId); + + // Invoke the postSave method to allow for the child class to access the model. + $this->postSaveHook($model, $validData); - return true; - } + return true; + } - return parent::save($key, $urlVar); - } + return parent::save($key, $urlVar); + } } diff --git a/administrator/components/com_templates/src/Controller/StylesController.php b/administrator/components/com_templates/src/Controller/StylesController.php index 2a3afaa530ed2..016ad3df9b6bf 100644 --- a/administrator/components/com_templates/src/Controller/StylesController.php +++ b/administrator/components/com_templates/src/Controller/StylesController.php @@ -1,4 +1,5 @@ checkToken(); - - $pks = (array) $this->input->post->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $pks = array_filter($pks); - - try - { - if (empty($pks)) - { - throw new \Exception(Text::_('COM_TEMPLATES_NO_TEMPLATE_SELECTED')); - } - - $model = $this->getModel(); - $model->duplicate($pks); - $this->setMessage(Text::_('COM_TEMPLATES_SUCCESS_DUPLICATED')); - } - catch (\Exception $e) - { - $this->app->enqueueMessage($e->getMessage(), 'error'); - } - - $this->setRedirect('index.php?option=com_templates&view=styles'); - } - - /** - * Proxy for getModel. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return BaseDatabaseModel - * - * @since 1.6 - */ - public function getModel($name = 'Style', $prefix = 'Administrator', $config = array()) - { - return parent::getModel($name, $prefix, array('ignore_request' => true)); - } - - /** - * Method to set the home template for a client. - * - * @return void - * - * @since 1.6 - */ - public function setDefault() - { - // Check for request forgeries - $this->checkToken(); - - $pks = (array) $this->input->post->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $pks = array_filter($pks); - - try - { - if (empty($pks)) - { - throw new \Exception(Text::_('COM_TEMPLATES_NO_TEMPLATE_SELECTED')); - } - - // Pop off the first element. - $id = array_shift($pks); - - /** @var \Joomla\Component\Templates\Administrator\Model\StyleModel $model */ - $model = $this->getModel(); - $model->setHome($id); - $this->setMessage(Text::_('COM_TEMPLATES_SUCCESS_HOME_SET')); - } - catch (\Exception $e) - { - $this->setMessage($e->getMessage(), 'warning'); - } - - $this->setRedirect('index.php?option=com_templates&view=styles'); - } - - /** - * Method to unset the default template for a client and for a language - * - * @return void - * - * @since 1.6 - */ - public function unsetDefault() - { - // Check for request forgeries - $this->checkToken('request'); - - $pks = (array) $this->input->get->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $pks = array_filter($pks); - - try - { - if (empty($pks)) - { - throw new \Exception(Text::_('COM_TEMPLATES_NO_TEMPLATE_SELECTED')); - } - - // Pop off the first element. - $id = array_shift($pks); - - /** @var \Joomla\Component\Templates\Administrator\Model\StyleModel $model */ - $model = $this->getModel(); - $model->unsetHome($id); - $this->setMessage(Text::_('COM_TEMPLATES_SUCCESS_HOME_UNSET')); - } - catch (\Exception $e) - { - $this->setMessage($e->getMessage(), 'warning'); - } - - $this->setRedirect('index.php?option=com_templates&view=styles'); - } + /** + * Method to clone and existing template style. + * + * @return void + */ + public function duplicate() + { + // Check for request forgeries + $this->checkToken(); + + $pks = (array) $this->input->post->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $pks = array_filter($pks); + + try { + if (empty($pks)) { + throw new \Exception(Text::_('COM_TEMPLATES_NO_TEMPLATE_SELECTED')); + } + + $model = $this->getModel(); + $model->duplicate($pks); + $this->setMessage(Text::_('COM_TEMPLATES_SUCCESS_DUPLICATED')); + } catch (\Exception $e) { + $this->app->enqueueMessage($e->getMessage(), 'error'); + } + + $this->setRedirect('index.php?option=com_templates&view=styles'); + } + + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return BaseDatabaseModel + * + * @since 1.6 + */ + public function getModel($name = 'Style', $prefix = 'Administrator', $config = array()) + { + return parent::getModel($name, $prefix, array('ignore_request' => true)); + } + + /** + * Method to set the home template for a client. + * + * @return void + * + * @since 1.6 + */ + public function setDefault() + { + // Check for request forgeries + $this->checkToken(); + + $pks = (array) $this->input->post->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $pks = array_filter($pks); + + try { + if (empty($pks)) { + throw new \Exception(Text::_('COM_TEMPLATES_NO_TEMPLATE_SELECTED')); + } + + // Pop off the first element. + $id = array_shift($pks); + + /** @var \Joomla\Component\Templates\Administrator\Model\StyleModel $model */ + $model = $this->getModel(); + $model->setHome($id); + $this->setMessage(Text::_('COM_TEMPLATES_SUCCESS_HOME_SET')); + } catch (\Exception $e) { + $this->setMessage($e->getMessage(), 'warning'); + } + + $this->setRedirect('index.php?option=com_templates&view=styles'); + } + + /** + * Method to unset the default template for a client and for a language + * + * @return void + * + * @since 1.6 + */ + public function unsetDefault() + { + // Check for request forgeries + $this->checkToken('request'); + + $pks = (array) $this->input->get->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $pks = array_filter($pks); + + try { + if (empty($pks)) { + throw new \Exception(Text::_('COM_TEMPLATES_NO_TEMPLATE_SELECTED')); + } + + // Pop off the first element. + $id = array_shift($pks); + + /** @var \Joomla\Component\Templates\Administrator\Model\StyleModel $model */ + $model = $this->getModel(); + $model->unsetHome($id); + $this->setMessage(Text::_('COM_TEMPLATES_SUCCESS_HOME_UNSET')); + } catch (\Exception $e) { + $this->setMessage($e->getMessage(), 'warning'); + } + + $this->setRedirect('index.php?option=com_templates&view=styles'); + } } diff --git a/administrator/components/com_templates/src/Controller/TemplateController.php b/administrator/components/com_templates/src/Controller/TemplateController.php index 15643a5cfe776..01bb5a6d073a8 100644 --- a/administrator/components/com_templates/src/Controller/TemplateController.php +++ b/administrator/components/com_templates/src/Controller/TemplateController.php @@ -1,4 +1,5 @@ registerTask('apply', 'save'); - $this->registerTask('unpublish', 'publish'); - $this->registerTask('publish', 'publish'); - $this->registerTask('deleteOverrideHistory', 'publish'); - } - - /** - * Method for closing the template. - * - * @return void - * - * @since 3.2 - */ - public function cancel() - { - $this->setRedirect(Route::_('index.php?option=com_templates&view=templates', false)); - } - - /** - * Method for closing a file. - * - * @return void - * - * @since 3.2 - */ - public function close() - { - $file = base64_encode('home'); - $id = (int) $this->input->get('id', 0, 'int'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . - $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - - /** - * Marked as Checked/Unchecked of override history. - * - * @return void - * - * @since 4.0.0 - */ - public function publish() - { - // Check for request forgeries. - $this->checkToken(); - - $file = $this->input->get('file'); - $id = $this->input->get('id'); - - $ids = (array) $this->input->get('cid', array(), 'string'); - $values = array('publish' => 1, 'unpublish' => 0, 'deleteOverrideHistory' => -3); - $task = $this->getTask(); - $value = ArrayHelper::getValue($values, $task, 0, 'int'); - - if (empty($ids)) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_NO_FILE_SELECTED'), 'warning'); - } - else - { - /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - - // Change the state of the records. - if (!$model->publish($ids, $value, $id)) - { - $this->setMessage(implode('
', $model->getErrors()), 'warning'); - } - else - { - if ($value === 1) - { - $ntext = 'COM_TEMPLATES_N_OVERRIDE_CHECKED'; - } - elseif ($value === 0) - { - $ntext = 'COM_TEMPLATES_N_OVERRIDE_UNCHECKED'; - } - elseif ($value === -3) - { - $ntext = 'COM_TEMPLATES_N_OVERRIDE_DELETED'; - } - - $this->setMessage(Text::plural($ntext, count($ids))); - } - } - - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . - $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - - /** - * Method for copying the template. - * - * @return boolean true on success, false otherwise - * - * @since 3.2 - */ - public function copy() - { - // Check for request forgeries - $this->checkToken(); - - $app = $this->app; - $this->input->set('installtype', 'folder'); - $newNameRaw = $this->input->get('new_name', null, 'string'); - // Only accept letters, numbers and underscore for template name - $newName = preg_replace('/[^a-zA-Z0-9_]/', '', $newNameRaw); - $templateID = (int) $this->input->getInt('id', 0); - $file = (string) $this->input->get('file', '', 'cmd'); - - // Access check. - if (!$this->allowEdit()) - { - $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return false; - } - - $this->setRedirect('index.php?option=com_templates&view=template&id=' . $templateID . '&file=' . $file); - - /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel('Template', 'Administrator'); - $model->setState('new_name', $newName); - $model->setState('tmp_prefix', uniqid('template_copy_')); - $model->setState('to_path', $app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); - - // Process only if we have a new name entered - if (strlen($newName) > 0) - { - if (!$this->app->getIdentity()->authorise('core.create', 'com_templates')) - { - // User is not authorised to delete - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_CREATE_NOT_PERMITTED'), 'error'); - - return false; - } - - // Check that new name is valid - if (($newNameRaw !== null) && ($newName !== $newNameRaw)) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); - - return false; - } - - // Check that new name doesn't already exist - if (!$model->checkNewName()) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_DUPLICATE_TEMPLATE_NAME'), 'error'); - - return false; - } - - // Check that from name does exist and get the folder name - $fromName = $model->getFromName(); - - if (!$fromName) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); - - return false; - } - - // Call model's copy method - if (!$model->copy()) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_COPY'), 'error'); - - return false; - } - - // Call installation model - $this->input->set('install_directory', $app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); - - /** @var \Joomla\Component\Installer\Administrator\Model\InstallModel $installModel */ - $installModel = $this->app->bootComponent('com_installer') - ->getMVCFactory()->createModel('Install', 'Administrator'); - $this->app->getLanguage()->load('com_installer'); - - if (!$installModel->install()) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_INSTALL'), 'error'); - - return false; - } - - $this->setMessage(Text::sprintf('COM_TEMPLATES_COPY_SUCCESS', $newName)); - $model->cleanup(); - - return true; - } - - $this->setMessage(Text::sprintf('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); - - return false; - } - - /** - * 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 (note, the empty array is atypical compared to other models). - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. - * - * @since 3.2 - */ - public function getModel($name = 'Template', $prefix = 'Administrator', $config = array()) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to check if you can add a new record. - * - * @return boolean - * - * @since 3.2 - */ - protected function allowEdit() - { - return $this->app->getIdentity()->authorise('core.admin'); - } - - /** - * Saves a template source file. - * - * @return void - * - * @since 3.2 - */ - public function save() - { - // Check for request forgeries. - $this->checkToken(); - - $data = $this->input->post->get('jform', array(), 'array'); - $task = $this->getTask(); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $fileName = (string) $this->input->getCmd('file', ''); - $explodeArray = explode(':', str_replace('//', '/', base64_decode($fileName))); - - // Access check. - if (!$this->allowEdit()) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - // Match the stored id's with the submitted. - if (empty($data['extension_id']) || empty($data['filename'])) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_ID_FILENAME_MISMATCH'), 'error'); - - return; - } - elseif ($data['extension_id'] != $model->getState('extension.id')) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_ID_FILENAME_MISMATCH'), 'error'); - - return; - } - elseif (str_ends_with(end($explodeArray), Path::clean($data['filename'], '/'))) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_ID_FILENAME_MISMATCH'), 'error'); - - return; - } - - // Validate the posted data. - $form = $model->getForm(); - - if (!$form) - { - $this->setMessage($model->getError(), 'error'); - - return; - } - - $data = $model->validate($form, $data); - - // Check for validation errors. - if ($data === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $this->app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Redirect back to the edit screen. - $url = 'index.php?option=com_templates&view=template&id=' . $model->getState('extension.id') . '&file=' . $fileName . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - - return; - } - - // Attempt to save the data. - if (!$model->save($data)) - { - // Redirect back to the edit screen. - $this->setMessage(Text::sprintf('JERROR_SAVE_FAILED', $model->getError()), 'warning'); - $url = 'index.php?option=com_templates&view=template&id=' . $model->getState('extension.id') . '&file=' . $fileName . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - - return; - } - - $this->setMessage(Text::_('COM_TEMPLATES_FILE_SAVE_SUCCESS')); - - // Redirect the user based on the chosen task. - switch ($task) - { - case 'apply': - // Redirect back to the edit screen. - $url = 'index.php?option=com_templates&view=template&id=' . $model->getState('extension.id') . '&file=' . $fileName . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - break; - - default: - // Redirect to the list screen. - $file = base64_encode('home'); - $id = (int) $this->input->get('id', 0, 'int'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - break; - } - } - - /** - * Method for creating override. - * - * @return void - * - * @since 3.2 - */ - public function overrides() - { - // Check for request forgeries. - $this->checkToken('get'); - - /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $file = (string) $this->input->getCmd('file', ''); - $override = (string) InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ) - ->clean(base64_decode($this->input->getBase64('folder', '')), 'path'); - $id = (int) $this->input->get('id', 0, 'int'); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - $model->createOverride($override); - - // Redirect back to the edit screen. - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - - /** - * Method for deleting a file. - * - * @return void - * - * @since 3.2 - */ - public function delete() - { - // Check for request forgeries - $this->checkToken(); - - /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if (base64_decode(urldecode($file)) == '/index.php') - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INDEX_DELETE'), 'warning'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - elseif (base64_decode(urldecode($file)) == '/joomla.asset.json') - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_ASSET_FILE_DELETE'), 'warning'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - elseif ($model->deleteFile($file)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_DELETE_SUCCESS')); - $file = base64_encode('home'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_DELETE'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for creating a new file. - * - * @return void - * - * @since 3.2 - */ - public function createFile() - { - // Check for request forgeries - $this->checkToken(); - - /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->get('file', '', 'cmd'); - $name = (string) $this->input->get('name', '', 'cmd'); - $location = (string) InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ) - ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); - $type = (string) $this->input->get('type', '', 'cmd'); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if ($type == 'null') - { - $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_TYPE'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - elseif (!preg_match('/^[a-zA-Z0-9-_]+$/', $name)) - { - $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_NAME'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - elseif ($model->createFile($name, $type, $location)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_CREATE_SUCCESS')); - $file = urlencode(base64_encode($location . '/' . $name . '.' . $type)); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_CREATE'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for uploading a file. - * - * @return void - * - * @since 3.2 - */ - public function uploadFile() - { - // Check for request forgeries - $this->checkToken(); - - /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - $upload = $this->input->files->get('files'); - $location = (string) InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ) - ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if ($return = $model->uploadFile($upload, $location)) - { - $this->setMessage(Text::sprintf('COM_TEMPLATES_FILE_UPLOAD_SUCCESS', $upload['name'])); - $redirect = base64_encode($return); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $redirect . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_UPLOAD'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for creating a new folder. - * - * @return void - * - * @since 3.2 - */ - public function createFolder() - { - // Check for request forgeries - $this->checkToken(); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - $name = $this->input->get('name'); - $location = (string) InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ) - ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if (!preg_match('/^[a-zA-Z0-9-_.]+$/', $name)) - { - $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FOLDER_NAME'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - elseif ($model->createFolder($name, $location)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FOLDER_CREATE_SUCCESS')); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FOLDER_CREATE'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for deleting a folder. - * - * @return void - * - * @since 3.2 - */ - public function deleteFolder() - { - // Check for request forgeries - $this->checkToken(); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $id = (int) $this->input->get('id', 0, 'int'); - $isMedia = (int) $this->input->get('isMedia', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - $location = (string) InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ) - ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if (empty($location)) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_ROOT_DELETE'), 'warning'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - elseif ($model->deleteFolder($location)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FOLDER_DELETE_SUCCESS')); - - if (stristr(base64_decode($file), $location) != false) - { - $file = base64_encode('home'); - } - - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_FOLDER_DELETE_ERROR'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for renaming a file. - * - * @return void - * - * @since 3.2 - */ - public function renameFile() - { - // Check for request forgeries - $this->checkToken(); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - $id = (int) $this->input->get('id', 0, 'int'); - $isMedia = (int) $this->input->get('isMedia', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - $newName = $this->input->get('new_name'); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if (base64_decode(urldecode($file)) == '/index.php') - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_RENAME_INDEX'), 'warning'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - elseif (base64_decode(urldecode($file)) == '/joomla.asset.json') - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_RENAME_ASSET_FILE'), 'warning'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - elseif (!preg_match('/^[a-zA-Z0-9-_]+$/', $newName)) - { - $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_NAME'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - elseif ($rename = $model->renameFile($file, $newName)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_RENAME_SUCCESS')); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $rename . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_RENAME'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for cropping an image. - * - * @return void - * - * @since 3.2 - */ - public function cropImage() - { - // Check for request forgeries - $this->checkToken(); - - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->get('file', '', 'cmd'); - $x = $this->input->get('x'); - $y = $this->input->get('y'); - $w = $this->input->get('w'); - $h = $this->input->get('h'); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if (empty($w) && empty($h) && empty($x) && empty($y)) - { - $this->setMessage(Text::_('COM_TEMPLATES_CROP_AREA_ERROR'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - elseif ($model->cropImage($file, $w, $h, $x, $y)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_CROP_SUCCESS')); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_CROP_ERROR'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for resizing an image. - * - * @return void - * - * @since 3.2 - */ - public function resizeImage() - { - // Check for request forgeries - $this->checkToken(); - - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - $width = $this->input->get('width'); - $height = $this->input->get('height'); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if ($model->resizeImage($file, $width, $height)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_RESIZE_SUCCESS')); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_RESIZE_ERROR'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for copying a file. - * - * @return void - * - * @since 3.2 - */ - public function copyFile() - { - // Check for request forgeries - $this->checkToken(); - - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - $newName = $this->input->get('new_name'); - $location = (string) InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ) - ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if (!preg_match('/^[a-zA-Z0-9-_]+$/', $newName)) - { - $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_NAME'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - elseif ($model->copyFile($newName, $location, $file)) - { - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_COPY_FAIL'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Method for extracting an archive file. - * - * @return void - * - * @since 3.2 - */ - public function extractArchive() - { - // Check for request forgeries - $this->checkToken(); - - $id = (int) $this->input->get('id', 0, 'int'); - $file = (string) $this->input->getCmd('file', ''); - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return; - } - - if ($model->extractArchive($file)) - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_EXTRACT_SUCCESS')); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file; - $this->setRedirect(Route::_($url, false)); - } - else - { - $this->setMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_EXTRACT_FAIL'), 'error'); - $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file; - $this->setRedirect(Route::_($url, false)); - } - } - - /** - * Fetch and report updates in \JSON format, for AJAX requests - * - * @return void - * - * @since 4.0.0 - */ - public function ajax() - { - $app = $this->app; - - if (!Session::checkToken('get')) - { - $app->setHeader('status', 403, true); - $app->sendHeaders(); - echo Text::_('JINVALID_TOKEN_NOTICE'); - $app->close(); - } - - // Checks status of installer override plugin. - if (!PluginHelper::isEnabled('installer', 'override')) - { - $error = array('installerOverride' => 'disabled'); - - echo json_encode($error); - - $app->close(); - } - - /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel(); - - $result = $model->getUpdatedList(true, true); - - echo json_encode($result); - - $app->close(); - } - - - /** - * Method for creating a child template. - * - * @return boolean true on success, false otherwise - * - * @since 4.1.0 - */ - public function child() - { - // Check for request forgeries - $this->checkToken(); - - // Access check. - if (!$this->allowEdit()) - { - $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - - return false; - } - - $this->input->set('installtype', 'folder'); - $newNameRaw = $this->input->get('new_name', null, 'string'); - - // Only accept letters, numbers and underscore for template name - $newName = preg_replace('/[^a-zA-Z0-9_]/', '', $newNameRaw); - $templateID = (int) $this->input->getInt('id', 0); - $file = (string) $this->input->get('file', '', 'cmd'); - $extraStyles = (array) $this->input->get('style_ids', [], 'array'); - - $this->setRedirect('index.php?option=com_templates&view=template&id=' . $templateID . '&file=' . $file); - - /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ - $model = $this->getModel('Template', 'Administrator'); - $model->setState('new_name', $newName); - $model->setState('tmp_prefix', uniqid('template_child_')); - $model->setState('to_path', $this->app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); - - // Process only if we have a new name entered - if (!strlen($newName)) { - $this->setMessage(Text::sprintf('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); - - return false; - } - - // Process only if user is allowed to create child template - if (!$this->app->getIdentity()->authorise('core.create', 'com_templates')) { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_CREATE_NOT_PERMITTED'), 'error'); - - return false; - } - - // Check that new name is valid - if (($newNameRaw !== null) && ($newName !== $newNameRaw)) { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); - - return false; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 1.6 + * @see BaseController + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('apply', 'save'); + $this->registerTask('unpublish', 'publish'); + $this->registerTask('publish', 'publish'); + $this->registerTask('deleteOverrideHistory', 'publish'); + } + + /** + * Method for closing the template. + * + * @return void + * + * @since 3.2 + */ + public function cancel() + { + $this->setRedirect(Route::_('index.php?option=com_templates&view=templates', false)); + } + + /** + * Method for closing a file. + * + * @return void + * + * @since 3.2 + */ + public function close() + { + $file = base64_encode('home'); + $id = (int) $this->input->get('id', 0, 'int'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . + $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + + /** + * Marked as Checked/Unchecked of override history. + * + * @return void + * + * @since 4.0.0 + */ + public function publish() + { + // Check for request forgeries. + $this->checkToken(); + + $file = $this->input->get('file'); + $id = $this->input->get('id'); + + $ids = (array) $this->input->get('cid', array(), 'string'); + $values = array('publish' => 1, 'unpublish' => 0, 'deleteOverrideHistory' => -3); + $task = $this->getTask(); + $value = ArrayHelper::getValue($values, $task, 0, 'int'); + + if (empty($ids)) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_NO_FILE_SELECTED'), 'warning'); + } else { + /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + + // Change the state of the records. + if (!$model->publish($ids, $value, $id)) { + $this->setMessage(implode('
', $model->getErrors()), 'warning'); + } else { + if ($value === 1) { + $ntext = 'COM_TEMPLATES_N_OVERRIDE_CHECKED'; + } elseif ($value === 0) { + $ntext = 'COM_TEMPLATES_N_OVERRIDE_UNCHECKED'; + } elseif ($value === -3) { + $ntext = 'COM_TEMPLATES_N_OVERRIDE_DELETED'; + } + + $this->setMessage(Text::plural($ntext, count($ids))); + } + } + + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . + $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + + /** + * Method for copying the template. + * + * @return boolean true on success, false otherwise + * + * @since 3.2 + */ + public function copy() + { + // Check for request forgeries + $this->checkToken(); + + $app = $this->app; + $this->input->set('installtype', 'folder'); + $newNameRaw = $this->input->get('new_name', null, 'string'); + // Only accept letters, numbers and underscore for template name + $newName = preg_replace('/[^a-zA-Z0-9_]/', '', $newNameRaw); + $templateID = (int) $this->input->getInt('id', 0); + $file = (string) $this->input->get('file', '', 'cmd'); + + // Access check. + if (!$this->allowEdit()) { + $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return false; + } + + $this->setRedirect('index.php?option=com_templates&view=template&id=' . $templateID . '&file=' . $file); + + /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel('Template', 'Administrator'); + $model->setState('new_name', $newName); + $model->setState('tmp_prefix', uniqid('template_copy_')); + $model->setState('to_path', $app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); + + // Process only if we have a new name entered + if (strlen($newName) > 0) { + if (!$this->app->getIdentity()->authorise('core.create', 'com_templates')) { + // User is not authorised to delete + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_CREATE_NOT_PERMITTED'), 'error'); + + return false; + } + + // Check that new name is valid + if (($newNameRaw !== null) && ($newName !== $newNameRaw)) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); + + return false; + } + + // Check that new name doesn't already exist + if (!$model->checkNewName()) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_DUPLICATE_TEMPLATE_NAME'), 'error'); + + return false; + } + + // Check that from name does exist and get the folder name + $fromName = $model->getFromName(); + + if (!$fromName) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); + + return false; + } + + // Call model's copy method + if (!$model->copy()) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_COPY'), 'error'); + + return false; + } + + // Call installation model + $this->input->set('install_directory', $app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); + + /** @var \Joomla\Component\Installer\Administrator\Model\InstallModel $installModel */ + $installModel = $this->app->bootComponent('com_installer') + ->getMVCFactory()->createModel('Install', 'Administrator'); + $this->app->getLanguage()->load('com_installer'); + + if (!$installModel->install()) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_INSTALL'), 'error'); + + return false; + } + + $this->setMessage(Text::sprintf('COM_TEMPLATES_COPY_SUCCESS', $newName)); + $model->cleanup(); + + return true; + } + + $this->setMessage(Text::sprintf('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); + + return false; + } + + /** + * 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 (note, the empty array is atypical compared to other models). + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 3.2 + */ + public function getModel($name = 'Template', $prefix = 'Administrator', $config = array()) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to check if you can add a new record. + * + * @return boolean + * + * @since 3.2 + */ + protected function allowEdit() + { + return $this->app->getIdentity()->authorise('core.admin'); + } + + /** + * Saves a template source file. + * + * @return void + * + * @since 3.2 + */ + public function save() + { + // Check for request forgeries. + $this->checkToken(); + + $data = $this->input->post->get('jform', array(), 'array'); + $task = $this->getTask(); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $fileName = (string) $this->input->getCmd('file', ''); + $explodeArray = explode(':', str_replace('//', '/', base64_decode($fileName))); + + // Access check. + if (!$this->allowEdit()) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + // Match the stored id's with the submitted. + if (empty($data['extension_id']) || empty($data['filename'])) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_ID_FILENAME_MISMATCH'), 'error'); + + return; + } elseif ($data['extension_id'] != $model->getState('extension.id')) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_ID_FILENAME_MISMATCH'), 'error'); + + return; + } elseif (str_ends_with(end($explodeArray), Path::clean($data['filename'], '/'))) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_ID_FILENAME_MISMATCH'), 'error'); + + return; + } + + // Validate the posted data. + $form = $model->getForm(); + + if (!$form) { + $this->setMessage($model->getError(), 'error'); + + return; + } + + $data = $model->validate($form, $data); + + // Check for validation errors. + if ($data === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $this->app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Redirect back to the edit screen. + $url = 'index.php?option=com_templates&view=template&id=' . $model->getState('extension.id') . '&file=' . $fileName . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + + return; + } + + // Attempt to save the data. + if (!$model->save($data)) { + // Redirect back to the edit screen. + $this->setMessage(Text::sprintf('JERROR_SAVE_FAILED', $model->getError()), 'warning'); + $url = 'index.php?option=com_templates&view=template&id=' . $model->getState('extension.id') . '&file=' . $fileName . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + + return; + } + + $this->setMessage(Text::_('COM_TEMPLATES_FILE_SAVE_SUCCESS')); + + // Redirect the user based on the chosen task. + switch ($task) { + case 'apply': + // Redirect back to the edit screen. + $url = 'index.php?option=com_templates&view=template&id=' . $model->getState('extension.id') . '&file=' . $fileName . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + break; + + default: + // Redirect to the list screen. + $file = base64_encode('home'); + $id = (int) $this->input->get('id', 0, 'int'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + break; + } + } + + /** + * Method for creating override. + * + * @return void + * + * @since 3.2 + */ + public function overrides() + { + // Check for request forgeries. + $this->checkToken('get'); + + /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $file = (string) $this->input->getCmd('file', ''); + $override = (string) InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ) + ->clean(base64_decode($this->input->getBase64('folder', '')), 'path'); + $id = (int) $this->input->get('id', 0, 'int'); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + $model->createOverride($override); + + // Redirect back to the edit screen. + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + + /** + * Method for deleting a file. + * + * @return void + * + * @since 3.2 + */ + public function delete() + { + // Check for request forgeries + $this->checkToken(); + + /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if (base64_decode(urldecode($file)) == '/index.php') { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INDEX_DELETE'), 'warning'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } elseif (base64_decode(urldecode($file)) == '/joomla.asset.json') { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_ASSET_FILE_DELETE'), 'warning'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } elseif ($model->deleteFile($file)) { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_DELETE_SUCCESS')); + $file = base64_encode('home'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_DELETE'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for creating a new file. + * + * @return void + * + * @since 3.2 + */ + public function createFile() + { + // Check for request forgeries + $this->checkToken(); + + /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->get('file', '', 'cmd'); + $name = (string) $this->input->get('name', '', 'cmd'); + $location = (string) InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ) + ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); + $type = (string) $this->input->get('type', '', 'cmd'); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if ($type == 'null') { + $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_TYPE'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } elseif (!preg_match('/^[a-zA-Z0-9-_]+$/', $name)) { + $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_NAME'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } elseif ($model->createFile($name, $type, $location)) { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_CREATE_SUCCESS')); + $file = urlencode(base64_encode($location . '/' . $name . '.' . $type)); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_CREATE'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for uploading a file. + * + * @return void + * + * @since 3.2 + */ + public function uploadFile() + { + // Check for request forgeries + $this->checkToken(); + + /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + $upload = $this->input->files->get('files'); + $location = (string) InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ) + ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if ($return = $model->uploadFile($upload, $location)) { + $this->setMessage(Text::sprintf('COM_TEMPLATES_FILE_UPLOAD_SUCCESS', $upload['name'])); + $redirect = base64_encode($return); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $redirect . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_UPLOAD'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for creating a new folder. + * + * @return void + * + * @since 3.2 + */ + public function createFolder() + { + // Check for request forgeries + $this->checkToken(); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + $name = $this->input->get('name'); + $location = (string) InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ) + ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if (!preg_match('/^[a-zA-Z0-9-_.]+$/', $name)) { + $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FOLDER_NAME'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } elseif ($model->createFolder($name, $location)) { + $this->setMessage(Text::_('COM_TEMPLATES_FOLDER_CREATE_SUCCESS')); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FOLDER_CREATE'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for deleting a folder. + * + * @return void + * + * @since 3.2 + */ + public function deleteFolder() + { + // Check for request forgeries + $this->checkToken(); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $id = (int) $this->input->get('id', 0, 'int'); + $isMedia = (int) $this->input->get('isMedia', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + $location = (string) InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ) + ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if (empty($location)) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_ROOT_DELETE'), 'warning'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } elseif ($model->deleteFolder($location)) { + $this->setMessage(Text::_('COM_TEMPLATES_FOLDER_DELETE_SUCCESS')); + + if (stristr(base64_decode($file), $location) != false) { + $file = base64_encode('home'); + } + + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_FOLDER_DELETE_ERROR'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for renaming a file. + * + * @return void + * + * @since 3.2 + */ + public function renameFile() + { + // Check for request forgeries + $this->checkToken(); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + $id = (int) $this->input->get('id', 0, 'int'); + $isMedia = (int) $this->input->get('isMedia', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + $newName = $this->input->get('new_name'); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if (base64_decode(urldecode($file)) == '/index.php') { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_RENAME_INDEX'), 'warning'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } elseif (base64_decode(urldecode($file)) == '/joomla.asset.json') { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_RENAME_ASSET_FILE'), 'warning'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } elseif (!preg_match('/^[a-zA-Z0-9-_]+$/', $newName)) { + $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_NAME'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } elseif ($rename = $model->renameFile($file, $newName)) { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_RENAME_SUCCESS')); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $rename . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_FILE_RENAME'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $isMedia; + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for cropping an image. + * + * @return void + * + * @since 3.2 + */ + public function cropImage() + { + // Check for request forgeries + $this->checkToken(); + + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->get('file', '', 'cmd'); + $x = $this->input->get('x'); + $y = $this->input->get('y'); + $w = $this->input->get('w'); + $h = $this->input->get('h'); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if (empty($w) && empty($h) && empty($x) && empty($y)) { + $this->setMessage(Text::_('COM_TEMPLATES_CROP_AREA_ERROR'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } elseif ($model->cropImage($file, $w, $h, $x, $y)) { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_CROP_SUCCESS')); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_CROP_ERROR'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for resizing an image. + * + * @return void + * + * @since 3.2 + */ + public function resizeImage() + { + // Check for request forgeries + $this->checkToken(); + + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + $width = $this->input->get('width'); + $height = $this->input->get('height'); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if ($model->resizeImage($file, $width, $height)) { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_RESIZE_SUCCESS')); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_RESIZE_ERROR'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for copying a file. + * + * @return void + * + * @since 3.2 + */ + public function copyFile() + { + // Check for request forgeries + $this->checkToken(); + + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + $newName = $this->input->get('new_name'); + $location = (string) InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ) + ->clean(base64_decode($this->input->getBase64('address', '')), 'path'); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if (!preg_match('/^[a-zA-Z0-9-_]+$/', $newName)) { + $this->setMessage(Text::_('COM_TEMPLATES_INVALID_FILE_NAME'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } elseif ($model->copyFile($newName, $location, $file)) { + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_COPY_FAIL'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file . '&isMedia=' . $this->input->getInt('isMedia', 0); + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Method for extracting an archive file. + * + * @return void + * + * @since 3.2 + */ + public function extractArchive() + { + // Check for request forgeries + $this->checkToken(); + + $id = (int) $this->input->get('id', 0, 'int'); + $file = (string) $this->input->getCmd('file', ''); + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return; + } + + if ($model->extractArchive($file)) { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_EXTRACT_SUCCESS')); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file; + $this->setRedirect(Route::_($url, false)); + } else { + $this->setMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_EXTRACT_FAIL'), 'error'); + $url = 'index.php?option=com_templates&view=template&id=' . $id . '&file=' . $file; + $this->setRedirect(Route::_($url, false)); + } + } + + /** + * Fetch and report updates in \JSON format, for AJAX requests + * + * @return void + * + * @since 4.0.0 + */ + public function ajax() + { + $app = $this->app; + + if (!Session::checkToken('get')) { + $app->setHeader('status', 403, true); + $app->sendHeaders(); + echo Text::_('JINVALID_TOKEN_NOTICE'); + $app->close(); + } + + // Checks status of installer override plugin. + if (!PluginHelper::isEnabled('installer', 'override')) { + $error = array('installerOverride' => 'disabled'); + + echo json_encode($error); + + $app->close(); + } + + /** @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel(); + + $result = $model->getUpdatedList(true, true); + + echo json_encode($result); + + $app->close(); + } + + + /** + * Method for creating a child template. + * + * @return boolean true on success, false otherwise + * + * @since 4.1.0 + */ + public function child() + { + // Check for request forgeries + $this->checkToken(); + + // Access check. + if (!$this->allowEdit()) { + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); + + return false; + } + + $this->input->set('installtype', 'folder'); + $newNameRaw = $this->input->get('new_name', null, 'string'); + + // Only accept letters, numbers and underscore for template name + $newName = preg_replace('/[^a-zA-Z0-9_]/', '', $newNameRaw); + $templateID = (int) $this->input->getInt('id', 0); + $file = (string) $this->input->get('file', '', 'cmd'); + $extraStyles = (array) $this->input->get('style_ids', [], 'array'); + + $this->setRedirect('index.php?option=com_templates&view=template&id=' . $templateID . '&file=' . $file); + + /* @var \Joomla\Component\Templates\Administrator\Model\TemplateModel $model */ + $model = $this->getModel('Template', 'Administrator'); + $model->setState('new_name', $newName); + $model->setState('tmp_prefix', uniqid('template_child_')); + $model->setState('to_path', $this->app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); + + // Process only if we have a new name entered + if (!strlen($newName)) { + $this->setMessage(Text::sprintf('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); + + return false; + } + + // Process only if user is allowed to create child template + if (!$this->app->getIdentity()->authorise('core.create', 'com_templates')) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_CREATE_NOT_PERMITTED'), 'error'); + + return false; + } + + // Check that new name is valid + if (($newNameRaw !== null) && ($newName !== $newNameRaw)) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_TEMPLATE_NAME'), 'error'); + + return false; + } - // Check that new name doesn't already exist - if (!$model->checkNewName()) { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_DUPLICATE_TEMPLATE_NAME'), 'error'); + // Check that new name doesn't already exist + if (!$model->checkNewName()) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_DUPLICATE_TEMPLATE_NAME'), 'error'); - return false; - } + return false; + } - // Check that from name does exist and get the folder name - $fromName = $model->getFromName(); + // Check that from name does exist and get the folder name + $fromName = $model->getFromName(); - if (!$fromName) - { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); + if (!$fromName) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); - return false; - } + return false; + } - // Call model's copy method - if (!$model->child()) { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_COPY'), 'error'); + // Call model's copy method + if (!$model->child()) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_COPY'), 'error'); - return false; - } + return false; + } - // Call installation model - $this->input->set('install_directory', $this->app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); + // Call installation model + $this->input->set('install_directory', $this->app->get('tmp_path') . '/' . $model->getState('tmp_prefix')); - /** @var \Joomla\Component\Installer\Administrator\Model\InstallModel $installModel */ - $installModel = $this->app->bootComponent('com_installer') - ->getMVCFactory()->createModel('Install', 'Administrator'); - $this->app->getLanguage()->load('com_installer'); + /** @var \Joomla\Component\Installer\Administrator\Model\InstallModel $installModel */ + $installModel = $this->app->bootComponent('com_installer') + ->getMVCFactory()->createModel('Install', 'Administrator'); + $this->app->getLanguage()->load('com_installer'); - if (!$installModel->install()) { - $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_INSTALL'), 'error'); + if (!$installModel->install()) { + $this->setMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_INSTALL'), 'error'); - return false; - } + return false; + } - $this->setMessage(Text::sprintf('COM_TEMPLATES_CHILD_SUCCESS', $newName)); - $model->cleanup(); + $this->setMessage(Text::sprintf('COM_TEMPLATES_CHILD_SUCCESS', $newName)); + $model->cleanup(); - if (\count($extraStyles) > 0) - { - $model->setState('stylesToCopy', $extraStyles); - $model->copyStyles(); - } + if (\count($extraStyles) > 0) { + $model->setState('stylesToCopy', $extraStyles); + $model->copyStyles(); + } - return true; - } + return true; + } } diff --git a/administrator/components/com_templates/src/Extension/TemplatesComponent.php b/administrator/components/com_templates/src/Extension/TemplatesComponent.php index e0736517413f1..ee201bb096a4d 100644 --- a/administrator/components/com_templates/src/Extension/TemplatesComponent.php +++ b/administrator/components/com_templates/src/Extension/TemplatesComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('templates', new Templates); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('templates', new Templates()); + } } diff --git a/administrator/components/com_templates/src/Field/TemplatelocationField.php b/administrator/components/com_templates/src/Field/TemplatelocationField.php index cf4793eb5506c..17b13d18ad3dc 100644 --- a/administrator/components/com_templates/src/Field/TemplatelocationField.php +++ b/administrator/components/com_templates/src/Field/TemplatelocationField.php @@ -1,4 +1,5 @@ getUserStateFromRequest('com_templates.styles.client_id', 'client_id', '0', 'string'); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 1.6 + */ + public function getOptions() + { + // Get the client_id filter from the user state. + $clientId = Factory::getApplication()->getUserStateFromRequest('com_templates.styles.client_id', 'client_id', '0', 'string'); - // Get the templates for the selected client_id. - $options = TemplatesHelper::getTemplateOptions($clientId); + // Get the templates for the selected client_id. + $options = TemplatesHelper::getTemplateOptions($clientId); - // Merge into the parent options. - return array_merge(parent::getOptions(), $options); - } + // Merge into the parent options. + return array_merge(parent::getOptions(), $options); + } } diff --git a/administrator/components/com_templates/src/Helper/TemplateHelper.php b/administrator/components/com_templates/src/Helper/TemplateHelper.php index dbbfd061ff811..2a666abd5ea62 100644 --- a/administrator/components/com_templates/src/Helper/TemplateHelper.php +++ b/administrator/components/com_templates/src/Helper/TemplateHelper.php @@ -1,4 +1,5 @@ enqueueMessage(Text::_('COM_TEMPLATES_ERROR_UPLOAD_INPUT'), 'error'); - - return false; - } - - // Media file names should never have executable extensions buried in them. - $executable = array( - 'exe', 'phtml','java', 'perl', 'py', 'asp','dll', 'go', 'jar', - 'ade', 'adp', 'bat', 'chm', 'cmd', 'com', 'cpl', 'hta', 'ins', 'isp', - 'jse', 'lib', 'mde', 'msc', 'msp', 'mst', 'pif', 'scr', 'sct', 'shb', - 'sys', 'vb', 'vbe', 'vbs', 'vxd', 'wsc', 'wsf', 'wsh' - ); - $explodedFileName = explode('.', $file['name']); - - if (count($explodedFileName) > 2) - { - foreach ($executable as $extensionName) - { - if (in_array($extensionName, $explodedFileName)) - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EXECUTABLE'), 'error'); - - return false; - } - } - } - - if ($file['name'] !== File::makeSafe($file['name']) || preg_match('/\s/', File::makeSafe($file['name']))) - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNFILENAME'), 'error'); - - return false; - } - - $format = strtolower(File::getExt($file['name'])); - - $imageTypes = explode(',', $params->get('image_formats')); - $sourceTypes = explode(',', $params->get('source_formats')); - $fontTypes = explode(',', $params->get('font_formats')); - $archiveTypes = explode(',', $params->get('compressed_formats')); - - $allowable = array_merge($imageTypes, $sourceTypes, $fontTypes, $archiveTypes); - - if ($format == '' || $format == false || (!in_array($format, $allowable))) - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNFILETYPE'), 'error'); - - return false; - } - - if (in_array($format, $archiveTypes)) - { - $zip = new \ZipArchive; - - if ($zip->open($file['tmp_name']) === true) - { - for ($i = 0; $i < $zip->numFiles; $i++) - { - $entry = $zip->getNameIndex($i); - $endString = substr($entry, -1); - - if ($endString != DIRECTORY_SEPARATOR) - { - $explodeArray = explode('.', $entry); - $ext = end($explodeArray); - - if (!in_array($ext, $allowable)) - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_UNSUPPORTED_ARCHIVE'), 'error'); - - return false; - } - } - } - } - else - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_OPEN_FAIL'), 'error'); - - return false; - } - } - - // Max upload size set to 2 MB for Template Manager - $maxSize = (int) ($params->get('upload_limit') * 1024 * 1024); - - if ($maxSize > 0 && (int) $file['size'] > $maxSize) - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNFILETOOLARGE'), 'error'); - - return false; - } - - $xss_check = file_get_contents($file['tmp_name'], false, null, -1, 256); - $html_tags = array( - 'abbr', 'acronym', 'address', 'applet', 'area', 'audioscope', 'base', 'basefont', 'bdo', 'bgsound', 'big', 'blackface', 'blink', 'blockquote', - 'body', 'bq', 'br', 'button', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'comment', 'custom', 'dd', 'del', 'dfn', 'dir', 'div', - 'dl', 'dt', 'em', 'embed', 'fieldset', 'fn', 'font', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', - 'iframe', 'ilayer', 'img', 'input', 'ins', 'isindex', 'keygen', 'kbd', 'label', 'layer', 'legend', 'li', 'limittext', 'link', 'listing', - 'map', 'marquee', 'menu', 'meta', 'multicol', 'nobr', 'noembed', 'noframes', 'noscript', 'nosmartquotes', 'object', 'ol', 'optgroup', 'option', - 'param', 'plaintext', 'pre', 'rt', 'ruby', 's', 'samp', 'script', 'select', 'server', 'shadow', 'sidebar', 'small', 'spacer', 'span', 'strike', - 'strong', 'style', 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'title', 'tr', 'tt', 'ul', 'var', 'wbr', 'xml', - 'xmp', '!DOCTYPE', '!--' - ); - - foreach ($html_tags as $tag) - { - // A tag is '' - if (stristr($xss_check, '<' . $tag . ' ') || stristr($xss_check, '<' . $tag . '>')) - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNIEXSS'), 'error'); - - return false; - } - } - - return true; - } + /** + * Checks if the file is an image + * + * @param string $fileName The filename + * + * @return boolean + * + * @since 3.2 + */ + public static function getTypeIcon($fileName) + { + // Get file extension + return strtolower(substr($fileName, strrpos($fileName, '.') + 1)); + } + + /** + * Checks if the file can be uploaded + * + * @param array $file File information + * @param string $err An error message to be returned + * + * @return boolean + * + * @since 3.2 + */ + public static function canUpload($file, $err = '') + { + $params = ComponentHelper::getParams('com_templates'); + + if (empty($file['name'])) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_UPLOAD_INPUT'), 'error'); + + return false; + } + + // Media file names should never have executable extensions buried in them. + $executable = array( + 'exe', 'phtml','java', 'perl', 'py', 'asp','dll', 'go', 'jar', + 'ade', 'adp', 'bat', 'chm', 'cmd', 'com', 'cpl', 'hta', 'ins', 'isp', + 'jse', 'lib', 'mde', 'msc', 'msp', 'mst', 'pif', 'scr', 'sct', 'shb', + 'sys', 'vb', 'vbe', 'vbs', 'vxd', 'wsc', 'wsf', 'wsh' + ); + $explodedFileName = explode('.', $file['name']); + + if (count($explodedFileName) > 2) { + foreach ($executable as $extensionName) { + if (in_array($extensionName, $explodedFileName)) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EXECUTABLE'), 'error'); + + return false; + } + } + } + + if ($file['name'] !== File::makeSafe($file['name']) || preg_match('/\s/', File::makeSafe($file['name']))) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNFILENAME'), 'error'); + + return false; + } + + $format = strtolower(File::getExt($file['name'])); + + $imageTypes = explode(',', $params->get('image_formats')); + $sourceTypes = explode(',', $params->get('source_formats')); + $fontTypes = explode(',', $params->get('font_formats')); + $archiveTypes = explode(',', $params->get('compressed_formats')); + + $allowable = array_merge($imageTypes, $sourceTypes, $fontTypes, $archiveTypes); + + if ($format == '' || $format == false || (!in_array($format, $allowable))) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNFILETYPE'), 'error'); + + return false; + } + + if (in_array($format, $archiveTypes)) { + $zip = new \ZipArchive(); + + if ($zip->open($file['tmp_name']) === true) { + for ($i = 0; $i < $zip->numFiles; $i++) { + $entry = $zip->getNameIndex($i); + $endString = substr($entry, -1); + + if ($endString != DIRECTORY_SEPARATOR) { + $explodeArray = explode('.', $entry); + $ext = end($explodeArray); + + if (!in_array($ext, $allowable)) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_UNSUPPORTED_ARCHIVE'), 'error'); + + return false; + } + } + } + } else { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_OPEN_FAIL'), 'error'); + + return false; + } + } + + // Max upload size set to 2 MB for Template Manager + $maxSize = (int) ($params->get('upload_limit') * 1024 * 1024); + + if ($maxSize > 0 && (int) $file['size'] > $maxSize) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNFILETOOLARGE'), 'error'); + + return false; + } + + $xss_check = file_get_contents($file['tmp_name'], false, null, -1, 256); + $html_tags = array( + 'abbr', 'acronym', 'address', 'applet', 'area', 'audioscope', 'base', 'basefont', 'bdo', 'bgsound', 'big', 'blackface', 'blink', 'blockquote', + 'body', 'bq', 'br', 'button', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'comment', 'custom', 'dd', 'del', 'dfn', 'dir', 'div', + 'dl', 'dt', 'em', 'embed', 'fieldset', 'fn', 'font', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', + 'iframe', 'ilayer', 'img', 'input', 'ins', 'isindex', 'keygen', 'kbd', 'label', 'layer', 'legend', 'li', 'limittext', 'link', 'listing', + 'map', 'marquee', 'menu', 'meta', 'multicol', 'nobr', 'noembed', 'noframes', 'noscript', 'nosmartquotes', 'object', 'ol', 'optgroup', 'option', + 'param', 'plaintext', 'pre', 'rt', 'ruby', 's', 'samp', 'script', 'select', 'server', 'shadow', 'sidebar', 'small', 'spacer', 'span', 'strike', + 'strong', 'style', 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'title', 'tr', 'tt', 'ul', 'var', 'wbr', 'xml', + 'xmp', '!DOCTYPE', '!--' + ); + + foreach ($html_tags as $tag) { + // A tag is '' + if (stristr($xss_check, '<' . $tag . ' ') || stristr($xss_check, '<' . $tag . '>')) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_WARNIEXSS'), 'error'); + + return false; + } + } + + return true; + } } diff --git a/administrator/components/com_templates/src/Helper/TemplatesHelper.php b/administrator/components/com_templates/src/Helper/TemplatesHelper.php index 53a50bdb8d0a1..c5c059183c61b 100644 --- a/administrator/components/com_templates/src/Helper/TemplatesHelper.php +++ b/administrator/components/com_templates/src/Helper/TemplatesHelper.php @@ -1,4 +1,5 @@ getQuery(true); - - $query->select($db->quoteName('element', 'value')) - ->select($db->quoteName('name', 'text')) - ->select($db->quoteName('extension_id', 'e_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('template')) - ->where($db->quoteName('enabled') . ' = 1') - ->order($db->quoteName('client_id') . ' ASC') - ->order($db->quoteName('name') . ' ASC'); - - if ($clientId != '*') - { - $clientId = (int) $clientId; - $query->where($db->quoteName('client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); - } - - $db->setQuery($query); - $options = $db->loadObjectList(); - - return $options; - } - - /** - * @param string $templateBaseDir - * @param string $templateDir - * - * @return boolean|CMSObject - */ - public static function parseXMLTemplateFile($templateBaseDir, $templateDir) - { - $data = new CMSObject; - - // Check of the xml file exists - $filePath = Path::clean($templateBaseDir . '/templates/' . $templateDir . '/templateDetails.xml'); - - if (is_file($filePath)) - { - $xml = Installer::parseXMLInstallFile($filePath); - - if ($xml['type'] != 'template') - { - return false; - } - - foreach ($xml as $key => $value) - { - $data->set($key, $value); - } - } - - return $data; - } - - /** - * @param integer $clientId - * @param string $templateDir - * - * @return boolean|array - * - * @since 3.0 - */ - public static function getPositions($clientId, $templateDir) - { - $positions = array(); - - $templateBaseDir = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; - $filePath = Path::clean($templateBaseDir . '/templates/' . $templateDir . '/templateDetails.xml'); - - if (is_file($filePath)) - { - // Read the file to see if it's a valid component XML file - $xml = simplexml_load_file($filePath); - - if (!$xml) - { - return false; - } - - // Check for a valid XML root tag. - - // Extensions use 'extension' as the root tag. Languages use 'metafile' instead - - if ($xml->getName() != 'extension' && $xml->getName() != 'metafile') - { - unset($xml); - - return false; - } - - $positions = (array) $xml->positions; - - if (isset($positions['position'])) - { - $positions = (array) $positions['position']; - } - else - { - $positions = array(); - } - } - - return $positions; - } + /** + * Get a list of filter options for the application clients. + * + * @return array An array of HtmlOption elements. + */ + public static function getClientOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '0', Text::_('JSITE')); + $options[] = HTMLHelper::_('select.option', '1', Text::_('JADMINISTRATOR')); + + return $options; + } + + /** + * Get a list of filter options for the templates with styles. + * + * @param mixed $clientId The CMS client id (0:site | 1:administrator) or '*' for all. + * + * @return array + */ + public static function getTemplateOptions($clientId = '*') + { + // Build the filter options. + $db = Factory::getDbo(); + $query = $db->getQuery(true); + + $query->select($db->quoteName('element', 'value')) + ->select($db->quoteName('name', 'text')) + ->select($db->quoteName('extension_id', 'e_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('template')) + ->where($db->quoteName('enabled') . ' = 1') + ->order($db->quoteName('client_id') . ' ASC') + ->order($db->quoteName('name') . ' ASC'); + + if ($clientId != '*') { + $clientId = (int) $clientId; + $query->where($db->quoteName('client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); + } + + $db->setQuery($query); + $options = $db->loadObjectList(); + + return $options; + } + + /** + * @param string $templateBaseDir + * @param string $templateDir + * + * @return boolean|CMSObject + */ + public static function parseXMLTemplateFile($templateBaseDir, $templateDir) + { + $data = new CMSObject(); + + // Check of the xml file exists + $filePath = Path::clean($templateBaseDir . '/templates/' . $templateDir . '/templateDetails.xml'); + + if (is_file($filePath)) { + $xml = Installer::parseXMLInstallFile($filePath); + + if ($xml['type'] != 'template') { + return false; + } + + foreach ($xml as $key => $value) { + $data->set($key, $value); + } + } + + return $data; + } + + /** + * @param integer $clientId + * @param string $templateDir + * + * @return boolean|array + * + * @since 3.0 + */ + public static function getPositions($clientId, $templateDir) + { + $positions = array(); + + $templateBaseDir = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; + $filePath = Path::clean($templateBaseDir . '/templates/' . $templateDir . '/templateDetails.xml'); + + if (is_file($filePath)) { + // Read the file to see if it's a valid component XML file + $xml = simplexml_load_file($filePath); + + if (!$xml) { + return false; + } + + // Check for a valid XML root tag. + + // Extensions use 'extension' as the root tag. Languages use 'metafile' instead + + if ($xml->getName() != 'extension' && $xml->getName() != 'metafile') { + unset($xml); + + return false; + } + + $positions = (array) $xml->positions; + + if (isset($positions['position'])) { + $positions = (array) $positions['position']; + } else { + $positions = array(); + } + } + + return $positions; + } } diff --git a/administrator/components/com_templates/src/Model/StyleModel.php b/administrator/components/com_templates/src/Model/StyleModel.php index 427101683fa8a..605388050f2d2 100644 --- a/administrator/components/com_templates/src/Model/StyleModel.php +++ b/administrator/components/com_templates/src/Model/StyleModel.php @@ -1,4 +1,5 @@ 'onExtensionBeforeDelete', - 'event_after_delete' => 'onExtensionAfterDelete', - 'event_before_save' => 'onExtensionBeforeSave', - 'event_after_save' => 'onExtensionAfterSave', - 'events_map' => array('delete' => 'extension', 'save' => 'extension') - ), $config - ); - - parent::__construct($config, $factory); - } - - /** - * Method to auto-populate the model state. - * - * @note Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - // Load the User state. - $pk = $app->input->getInt('id'); - $this->setState('style.id', $pk); - - // Load the parameters. - $params = ComponentHelper::getParams('com_templates'); - $this->setState('params', $params); - } - - /** - * Method to delete rows. - * - * @param array &$pks An array of item ids. - * - * @return boolean Returns true on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function delete(&$pks) - { - $pks = (array) $pks; - $user = Factory::getUser(); - $table = $this->getTable(); - $context = $this->option . '.' . $this->name; - - PluginHelper::importPlugin($this->events_map['delete']); - - // Iterate the items to delete each one. - foreach ($pks as $pk) - { - if ($table->load($pk)) - { - // Access checks. - if (!$user->authorise('core.delete', 'com_templates')) - { - throw new \Exception(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED')); - } - - // You should not delete a default style - if ($table->home != '0') - { - Factory::getApplication()->enqueueMessage(Text::_('COM_TEMPLATES_STYLE_CANNOT_DELETE_DEFAULT_STYLE'), 'error'); - - return false; - } - - // Trigger the before delete event. - $result = Factory::getApplication()->triggerEvent($this->event_before_delete, array($context, $table)); - - if (in_array(false, $result, true) || !$table->delete($pk)) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the after delete event. - Factory::getApplication()->triggerEvent($this->event_after_delete, array($context, $table)); - } - else - { - $this->setError($table->getError()); - - return false; - } - } - - // Clean cache - $this->cleanCache(); - - return true; - } - - /** - * Method to duplicate styles. - * - * @param array &$pks An array of primary key IDs. - * - * @return boolean True if successful. - * - * @throws \Exception - */ - public function duplicate(&$pks) - { - $user = Factory::getUser(); - - // Access checks. - if (!$user->authorise('core.create', 'com_templates')) - { - throw new \Exception(Text::_('JERROR_CORE_CREATE_NOT_PERMITTED')); - } - - $context = $this->option . '.' . $this->name; - - // Include the plugins for the save events. - PluginHelper::importPlugin($this->events_map['save']); - - $table = $this->getTable(); - - foreach ($pks as $pk) - { - if ($table->load($pk, true)) - { - // Reset the id to create a new record. - $table->id = 0; - - // Reset the home (don't want dupes of that field). - $table->home = 0; - - // Alter the title. - $m = null; - $table->title = $this->generateNewTitle(null, null, $table->title); - - if (!$table->check()) - { - throw new \Exception($table->getError()); - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, true)); - - if (in_array(false, $result, true) || !$table->store()) - { - throw new \Exception($table->getError()); - } - - // Trigger the after save event. - Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, true)); - } - else - { - throw new \Exception($table->getError()); - } - } - - // Clean cache - $this->cleanCache(); - - return true; - } - - /** - * Method to change the title. - * - * @param integer $categoryId The id of the category. - * @param string $alias The alias. - * @param string $title The title. - * - * @return string New title. - * - * @since 1.7.1 - */ - protected function generateNewTitle($categoryId, $alias, $title) - { - // Alter the title - $table = $this->getTable(); - - while ($table->load(array('title' => $title))) - { - $title = StringHelper::increment($title); - } - - return $title; - } - - /** - * Method to get the record form. - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // The folder and element vars are passed when saving the form. - if (empty($data)) - { - $item = $this->getItem(); - $clientId = $item->client_id; - $template = $item->template; - } - else - { - $clientId = ArrayHelper::getValue($data, 'client_id'); - $template = ArrayHelper::getValue($data, 'template'); - } - - // Add the default fields directory - $baseFolder = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; - Form::addFieldPath($baseFolder . '/templates/' . $template . '/field'); - - // These variables are used to add data from the plugin XML files. - $this->setState('item.client_id', $clientId); - $this->setState('item.template', $template); - - // Get the form. - $form = $this->loadForm('com_templates.style', 'style', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Modify the form based on access controls. - if (!$this->canEditState((object) $data)) - { - // Disable fields for display. - $form->setFieldAttribute('home', 'disabled', 'true'); - - // Disable fields while saving. - // The controller has already verified this is a record you can edit. - $form->setFieldAttribute('home', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_templates.edit.style.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_templates.style', $data); - - return $data; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - */ - public function getItem($pk = null) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('style.id'); - - if (!isset($this->_cache[$pk])) - { - // Get a row instance. - $table = $this->getTable(); - - // Attempt to load the row. - $return = $table->load($pk); - - // Check for a table object error. - if ($return === false && $table->getError()) - { - $this->setError($table->getError()); - - return false; - } - - // Convert to the \Joomla\CMS\Object\CMSObject before adding other data. - $properties = $table->getProperties(1); - $this->_cache[$pk] = ArrayHelper::toObject($properties, CMSObject::class); - - // Convert the params field to an array. - $registry = new Registry($table->params); - $this->_cache[$pk]->params = $registry->toArray(); - - // Get the template XML. - $client = ApplicationHelper::getClientInfo($table->client_id); - $path = Path::clean($client->path . '/templates/' . $table->template . '/templateDetails.xml'); - - if (file_exists($path)) - { - $this->_cache[$pk]->xml = simplexml_load_file($path); - } - else - { - $this->_cache[$pk]->xml = null; - } - } - - return $this->_cache[$pk]; - } - - /** - * Method to allow derived classes to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $clientId = $this->getState('item.client_id'); - $template = $this->getState('item.template'); - $lang = Factory::getLanguage(); - $client = ApplicationHelper::getClientInfo($clientId); - - if (!$form->loadFile('style_' . $client->name, true)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - $formFile = Path::clean($client->path . '/templates/' . $template . '/templateDetails.xml'); - - // Load the core and/or local language file(s). - $lang->load('tpl_' . $template, $client->path) - || (!empty($data->parent) && $lang->load('tpl_' . $data->parent, $client->path)) - || (!empty($data->parent) && $lang->load('tpl_' . $data->parent, $client->path . '/templates/' . $data->parent)) - || $lang->load('tpl_' . $template, $client->path . '/templates/' . $template); - - if (file_exists($formFile)) - { - // Get the template form. - if (!$form->loadFile($formFile, false, '//config')) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - } - - // Disable home field if it is default style - - if ((is_array($data) && array_key_exists('home', $data) && $data['home'] == '1') - || (is_object($data) && isset($data->home) && $data->home == '1')) - { - $form->setFieldAttribute('home', 'readonly', 'true'); - } - - if ($client->name === 'site' && !Multilanguage::isEnabled()) - { - $form->setFieldAttribute('home', 'type', 'radio'); - $form->setFieldAttribute('home', 'layout', 'joomla.form.field.radio.switcher'); - } - - // Attempt to load the xml file. - if (!$xml = simplexml_load_file($formFile)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Get the help data from the XML file if present. - $help = $xml->xpath('/extension/help'); - - if (!empty($help)) - { - $helpKey = trim((string) $help[0]['key']); - $helpURL = trim((string) $help[0]['url']); - - $this->helpKey = $helpKey ?: $this->helpKey; - $this->helpURL = $helpURL ?: $this->helpURL; - } - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - */ - public function save($data) - { - // Detect disabled extension - $extension = Table::getInstance('Extension', 'Joomla\\CMS\\Table\\'); - - if ($extension->load(array('enabled' => 0, 'type' => 'template', 'element' => $data['template'], 'client_id' => $data['client_id']))) - { - $this->setError(Text::_('COM_TEMPLATES_ERROR_SAVE_DISABLED_TEMPLATE')); - - return false; - } - - $app = Factory::getApplication(); - $table = $this->getTable(); - $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('style.id'); - $isNew = true; - - // Include the extension plugins for the save events. - PluginHelper::importPlugin($this->events_map['save']); - - // Load the row if saving an existing record. - if ($pk > 0) - { - $table->load($pk); - $isNew = false; - } - - if ($app->input->get('task') == 'save2copy') - { - $data['title'] = $this->generateNewTitle(null, null, $data['title']); - $data['home'] = 0; - $data['assigned'] = ''; - } - - // Bind the data. - if (!$table->bind($data)) - { - $this->setError($table->getError()); - - return false; - } - - // Prepare the row for saving - $this->prepareTable($table); - - // Check the data. - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array('com_templates.style', &$table, $isNew)); - - // Store the data. - if (in_array(false, $result, true) || !$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - $user = Factory::getUser(); - - if ($user->authorise('core.edit', 'com_menus') && $table->client_id == 0) - { - $n = 0; - $db = $this->getDatabase(); - $user = Factory::getUser(); - $tableId = (int) $table->id; - $userId = (int) $user->id; - - if (!empty($data['assigned']) && is_array($data['assigned'])) - { - $data['assigned'] = ArrayHelper::toInteger($data['assigned']); - - // Update the mapping for menu items that this style IS assigned to. - $query = $db->getQuery(true) - ->update($db->quoteName('#__menu')) - ->set($db->quoteName('template_style_id') . ' = :newtsid') - ->whereIn($db->quoteName('id'), $data['assigned']) - ->where($db->quoteName('template_style_id') . ' != :tsid') - ->where('(' . $db->quoteName('checked_out') . ' IS NULL OR ' . $db->quoteName('checked_out') . ' = :userid)') - ->bind(':userid', $userId, ParameterType::INTEGER) - ->bind(':newtsid', $tableId, ParameterType::INTEGER) - ->bind(':tsid', $tableId, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - $n += $db->getAffectedRows(); - } - - // Remove style mappings for menu items this style is NOT assigned to. - // If unassigned then all existing maps will be removed. - $query = $db->getQuery(true) - ->update($db->quoteName('#__menu')) - ->set($db->quoteName('template_style_id') . ' = 0'); - - if (!empty($data['assigned'])) - { - $query->whereNotIn($db->quoteName('id'), $data['assigned']); - } - - $query->where($db->quoteName('template_style_id') . ' = :templatestyleid') - ->where('(' . $db->quoteName('checked_out') . ' IS NULL OR ' . $db->quoteName('checked_out') . ' = :userid)') - ->bind(':userid', $userId, ParameterType::INTEGER) - ->bind(':templatestyleid', $tableId, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - $n += $db->getAffectedRows(); - - if ($n > 0) - { - $app->enqueueMessage(Text::plural('COM_TEMPLATES_MENU_CHANGED', $n)); - } - } - - // Clean the cache. - $this->cleanCache(); - - // Trigger the after save event. - Factory::getApplication()->triggerEvent($this->event_after_save, array('com_templates.style', &$table, $isNew)); - - $this->setState('style.id', $table->id); - - return true; - } - - /** - * Method to set a template style as home. - * - * @param integer $id The primary key ID for the style. - * - * @return boolean True if successful. - * - * @throws \Exception - */ - public function setHome($id = 0) - { - $user = Factory::getUser(); - $db = $this->getDatabase(); - - // Access checks. - if (!$user->authorise('core.edit.state', 'com_templates')) - { - throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); - } - - $style = $this->getTable(); - - if (!$style->load((int) $id)) - { - throw new \Exception(Text::_('COM_TEMPLATES_ERROR_STYLE_NOT_FOUND')); - } - - // Detect disabled extension - $extension = Table::getInstance('Extension', 'Joomla\\CMS\\Table\\'); - - if ($extension->load(array('enabled' => 0, 'type' => 'template', 'element' => $style->template, 'client_id' => $style->client_id))) - { - throw new \Exception(Text::_('COM_TEMPLATES_ERROR_SAVE_DISABLED_TEMPLATE')); - } - - $clientId = (int) $style->client_id; - $id = (int) $id; - - // Reset the home fields for the client_id. - $query = $db->getQuery(true) - ->update($db->quoteName('#__template_styles')) - ->set($db->quoteName('home') . ' = ' . $db->quote('0')) - ->where($db->quoteName('client_id') . ' = :clientid') - ->where($db->quoteName('home') . ' = ' . $db->quote('1')) - ->bind(':clientid', $clientId, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - // Set the new home style. - $query = $db->getQuery(true) - ->update($db->quoteName('#__template_styles')) - ->set($db->quoteName('home') . ' = ' . $db->quote('1')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - // Clean the cache. - $this->cleanCache(); - - return true; - } - - /** - * Method to unset a template style as default for a language. - * - * @param integer $id The primary key ID for the style. - * - * @return boolean True if successful. - * - * @throws \Exception - */ - public function unsetHome($id = 0) - { - $user = Factory::getUser(); - $db = $this->getDatabase(); - - // Access checks. - if (!$user->authorise('core.edit.state', 'com_templates')) - { - throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); - } - - $id = (int) $id; - - // Lookup the client_id. - $query = $db->getQuery(true) - ->select($db->quoteName(['client_id', 'home'])) - ->from($db->quoteName('#__template_styles')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $style = $db->loadObject(); - - if (!is_numeric($style->client_id)) - { - throw new \Exception(Text::_('COM_TEMPLATES_ERROR_STYLE_NOT_FOUND')); - } - elseif ($style->home == '1') - { - throw new \Exception(Text::_('COM_TEMPLATES_ERROR_CANNOT_UNSET_DEFAULT_STYLE')); - } - - // Set the new home style. - $query = $db->getQuery(true) - ->update($db->quoteName('#__template_styles')) - ->set($db->quoteName('home') . ' = ' . $db->quote('0')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - - // Clean the cache. - $this->cleanCache(); - - return true; - } - - /** - * Get the necessary data to load an item help screen. - * - * @return object An object with key, url, and local properties for loading the item help screen. - * - * @since 1.6 - */ - public function getHelp() - { - return (object) array('key' => $this->helpKey, 'url' => $this->helpURL); - } - - /** - * Returns the back end template for the given style. - * - * @param int $styleId The style id - * - * @return stdClass - * - * @since 4.2.0 - */ - public function getAdminTemplate(int $styleId): stdClass - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName(['s.template', 's.params', 's.inheritable', 's.parent'])) - ->from($db->quoteName('#__template_styles', 's')) - ->join( - 'LEFT', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.type') . ' = ' . $db->quote('template') - . ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quoteName('s.template') - . ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('s.client_id') - ) - ->where( - [ - $db->quoteName('s.client_id') . ' = 1', - $db->quoteName('s.home') . ' = ' . $db->quote('1'), - ] - ); - - if ($styleId) - { - $query->extendWhere( - 'OR', - [ - $db->quoteName('s.client_id') . ' = 1', - $db->quoteName('s.id') . ' = :style', - $db->quoteName('e.enabled') . ' = 1', - ] - ) - ->bind(':style', $styleId, ParameterType::INTEGER); - } - - $query->order($db->quoteName('s.home')); - $db->setQuery($query); - - return $db->loadObject(); - } - - /** - * Returns the front end templates. - * - * @return array - * - * @since 4.2.0 - */ - public function getSiteTemplates(): array - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName(['id', 'home', 'template', 's.params', 'inheritable', 'parent'])) - ->from($db->quoteName('#__template_styles', 's')) - ->where( - [ - $db->quoteName('s.client_id') . ' = 0', - $db->quoteName('e.enabled') . ' = 1', - ] - ) - ->join( - 'LEFT', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.element') . ' = ' . $db->quoteName('s.template') - . ' AND ' . $db->quoteName('e.type') . ' = ' . $db->quote('template') - . ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('s.client_id') - ); - - $db->setQuery($query); - - return $db->loadObjectList('id'); - } - - /** - * Custom clean cache method - * - * @param string $group The cache group - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 1.6 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('com_templates'); - parent::cleanCache('_system'); - } + /** + * The help screen key for the module. + * + * @var string + * @since 1.6 + */ + protected $helpKey = 'Templates:_Edit_Style'; + + /** + * The help screen base URL for the module. + * + * @var string + * @since 1.6 + */ + protected $helpURL; + + /** + * Item cache. + * + * @var array + * @since 1.6 + */ + private $_cache = array(); + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + $config = array_merge( + array( + 'event_before_delete' => 'onExtensionBeforeDelete', + 'event_after_delete' => 'onExtensionAfterDelete', + 'event_before_save' => 'onExtensionBeforeSave', + 'event_after_save' => 'onExtensionAfterSave', + 'events_map' => array('delete' => 'extension', 'save' => 'extension') + ), + $config + ); + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * @note Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the User state. + $pk = $app->input->getInt('id'); + $this->setState('style.id', $pk); + + // Load the parameters. + $params = ComponentHelper::getParams('com_templates'); + $this->setState('params', $params); + } + + /** + * Method to delete rows. + * + * @param array &$pks An array of item ids. + * + * @return boolean Returns true on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function delete(&$pks) + { + $pks = (array) $pks; + $user = Factory::getUser(); + $table = $this->getTable(); + $context = $this->option . '.' . $this->name; + + PluginHelper::importPlugin($this->events_map['delete']); + + // Iterate the items to delete each one. + foreach ($pks as $pk) { + if ($table->load($pk)) { + // Access checks. + if (!$user->authorise('core.delete', 'com_templates')) { + throw new \Exception(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED')); + } + + // You should not delete a default style + if ($table->home != '0') { + Factory::getApplication()->enqueueMessage(Text::_('COM_TEMPLATES_STYLE_CANNOT_DELETE_DEFAULT_STYLE'), 'error'); + + return false; + } + + // Trigger the before delete event. + $result = Factory::getApplication()->triggerEvent($this->event_before_delete, array($context, $table)); + + if (in_array(false, $result, true) || !$table->delete($pk)) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the after delete event. + Factory::getApplication()->triggerEvent($this->event_after_delete, array($context, $table)); + } else { + $this->setError($table->getError()); + + return false; + } + } + + // Clean cache + $this->cleanCache(); + + return true; + } + + /** + * Method to duplicate styles. + * + * @param array &$pks An array of primary key IDs. + * + * @return boolean True if successful. + * + * @throws \Exception + */ + public function duplicate(&$pks) + { + $user = Factory::getUser(); + + // Access checks. + if (!$user->authorise('core.create', 'com_templates')) { + throw new \Exception(Text::_('JERROR_CORE_CREATE_NOT_PERMITTED')); + } + + $context = $this->option . '.' . $this->name; + + // Include the plugins for the save events. + PluginHelper::importPlugin($this->events_map['save']); + + $table = $this->getTable(); + + foreach ($pks as $pk) { + if ($table->load($pk, true)) { + // Reset the id to create a new record. + $table->id = 0; + + // Reset the home (don't want dupes of that field). + $table->home = 0; + + // Alter the title. + $m = null; + $table->title = $this->generateNewTitle(null, null, $table->title); + + if (!$table->check()) { + throw new \Exception($table->getError()); + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($context, &$table, true)); + + if (in_array(false, $result, true) || !$table->store()) { + throw new \Exception($table->getError()); + } + + // Trigger the after save event. + Factory::getApplication()->triggerEvent($this->event_after_save, array($context, &$table, true)); + } else { + throw new \Exception($table->getError()); + } + } + + // Clean cache + $this->cleanCache(); + + return true; + } + + /** + * Method to change the title. + * + * @param integer $categoryId The id of the category. + * @param string $alias The alias. + * @param string $title The title. + * + * @return string New title. + * + * @since 1.7.1 + */ + protected function generateNewTitle($categoryId, $alias, $title) + { + // Alter the title + $table = $this->getTable(); + + while ($table->load(array('title' => $title))) { + $title = StringHelper::increment($title); + } + + return $title; + } + + /** + * Method to get the record form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // The folder and element vars are passed when saving the form. + if (empty($data)) { + $item = $this->getItem(); + $clientId = $item->client_id; + $template = $item->template; + } else { + $clientId = ArrayHelper::getValue($data, 'client_id'); + $template = ArrayHelper::getValue($data, 'template'); + } + + // Add the default fields directory + $baseFolder = $clientId ? JPATH_ADMINISTRATOR : JPATH_SITE; + Form::addFieldPath($baseFolder . '/templates/' . $template . '/field'); + + // These variables are used to add data from the plugin XML files. + $this->setState('item.client_id', $clientId); + $this->setState('item.template', $template); + + // Get the form. + $form = $this->loadForm('com_templates.style', 'style', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Modify the form based on access controls. + if (!$this->canEditState((object) $data)) { + // Disable fields for display. + $form->setFieldAttribute('home', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is a record you can edit. + $form->setFieldAttribute('home', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_templates.edit.style.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_templates.style', $data); + + return $data; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + */ + public function getItem($pk = null) + { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('style.id'); + + if (!isset($this->_cache[$pk])) { + // Get a row instance. + $table = $this->getTable(); + + // Attempt to load the row. + $return = $table->load($pk); + + // Check for a table object error. + if ($return === false && $table->getError()) { + $this->setError($table->getError()); + + return false; + } + + // Convert to the \Joomla\CMS\Object\CMSObject before adding other data. + $properties = $table->getProperties(1); + $this->_cache[$pk] = ArrayHelper::toObject($properties, CMSObject::class); + + // Convert the params field to an array. + $registry = new Registry($table->params); + $this->_cache[$pk]->params = $registry->toArray(); + + // Get the template XML. + $client = ApplicationHelper::getClientInfo($table->client_id); + $path = Path::clean($client->path . '/templates/' . $table->template . '/templateDetails.xml'); + + if (file_exists($path)) { + $this->_cache[$pk]->xml = simplexml_load_file($path); + } else { + $this->_cache[$pk]->xml = null; + } + } + + return $this->_cache[$pk]; + } + + /** + * Method to allow derived classes to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $clientId = $this->getState('item.client_id'); + $template = $this->getState('item.template'); + $lang = Factory::getLanguage(); + $client = ApplicationHelper::getClientInfo($clientId); + + if (!$form->loadFile('style_' . $client->name, true)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + $formFile = Path::clean($client->path . '/templates/' . $template . '/templateDetails.xml'); + + // Load the core and/or local language file(s). + $lang->load('tpl_' . $template, $client->path) + || (!empty($data->parent) && $lang->load('tpl_' . $data->parent, $client->path)) + || (!empty($data->parent) && $lang->load('tpl_' . $data->parent, $client->path . '/templates/' . $data->parent)) + || $lang->load('tpl_' . $template, $client->path . '/templates/' . $template); + + if (file_exists($formFile)) { + // Get the template form. + if (!$form->loadFile($formFile, false, '//config')) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + } + + // Disable home field if it is default style + + if ( + (is_array($data) && array_key_exists('home', $data) && $data['home'] == '1') + || (is_object($data) && isset($data->home) && $data->home == '1') + ) { + $form->setFieldAttribute('home', 'readonly', 'true'); + } + + if ($client->name === 'site' && !Multilanguage::isEnabled()) { + $form->setFieldAttribute('home', 'type', 'radio'); + $form->setFieldAttribute('home', 'layout', 'joomla.form.field.radio.switcher'); + } + + // Attempt to load the xml file. + if (!$xml = simplexml_load_file($formFile)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Get the help data from the XML file if present. + $help = $xml->xpath('/extension/help'); + + if (!empty($help)) { + $helpKey = trim((string) $help[0]['key']); + $helpURL = trim((string) $help[0]['url']); + + $this->helpKey = $helpKey ?: $this->helpKey; + $this->helpURL = $helpURL ?: $this->helpURL; + } + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + */ + public function save($data) + { + // Detect disabled extension + $extension = Table::getInstance('Extension', 'Joomla\\CMS\\Table\\'); + + if ($extension->load(array('enabled' => 0, 'type' => 'template', 'element' => $data['template'], 'client_id' => $data['client_id']))) { + $this->setError(Text::_('COM_TEMPLATES_ERROR_SAVE_DISABLED_TEMPLATE')); + + return false; + } + + $app = Factory::getApplication(); + $table = $this->getTable(); + $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('style.id'); + $isNew = true; + + // Include the extension plugins for the save events. + PluginHelper::importPlugin($this->events_map['save']); + + // Load the row if saving an existing record. + if ($pk > 0) { + $table->load($pk); + $isNew = false; + } + + if ($app->input->get('task') == 'save2copy') { + $data['title'] = $this->generateNewTitle(null, null, $data['title']); + $data['home'] = 0; + $data['assigned'] = ''; + } + + // Bind the data. + if (!$table->bind($data)) { + $this->setError($table->getError()); + + return false; + } + + // Prepare the row for saving + $this->prepareTable($table); + + // Check the data. + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array('com_templates.style', &$table, $isNew)); + + // Store the data. + if (in_array(false, $result, true) || !$table->store()) { + $this->setError($table->getError()); + + return false; + } + + $user = Factory::getUser(); + + if ($user->authorise('core.edit', 'com_menus') && $table->client_id == 0) { + $n = 0; + $db = $this->getDatabase(); + $user = Factory::getUser(); + $tableId = (int) $table->id; + $userId = (int) $user->id; + + if (!empty($data['assigned']) && is_array($data['assigned'])) { + $data['assigned'] = ArrayHelper::toInteger($data['assigned']); + + // Update the mapping for menu items that this style IS assigned to. + $query = $db->getQuery(true) + ->update($db->quoteName('#__menu')) + ->set($db->quoteName('template_style_id') . ' = :newtsid') + ->whereIn($db->quoteName('id'), $data['assigned']) + ->where($db->quoteName('template_style_id') . ' != :tsid') + ->where('(' . $db->quoteName('checked_out') . ' IS NULL OR ' . $db->quoteName('checked_out') . ' = :userid)') + ->bind(':userid', $userId, ParameterType::INTEGER) + ->bind(':newtsid', $tableId, ParameterType::INTEGER) + ->bind(':tsid', $tableId, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + $n += $db->getAffectedRows(); + } + + // Remove style mappings for menu items this style is NOT assigned to. + // If unassigned then all existing maps will be removed. + $query = $db->getQuery(true) + ->update($db->quoteName('#__menu')) + ->set($db->quoteName('template_style_id') . ' = 0'); + + if (!empty($data['assigned'])) { + $query->whereNotIn($db->quoteName('id'), $data['assigned']); + } + + $query->where($db->quoteName('template_style_id') . ' = :templatestyleid') + ->where('(' . $db->quoteName('checked_out') . ' IS NULL OR ' . $db->quoteName('checked_out') . ' = :userid)') + ->bind(':userid', $userId, ParameterType::INTEGER) + ->bind(':templatestyleid', $tableId, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + $n += $db->getAffectedRows(); + + if ($n > 0) { + $app->enqueueMessage(Text::plural('COM_TEMPLATES_MENU_CHANGED', $n)); + } + } + + // Clean the cache. + $this->cleanCache(); + + // Trigger the after save event. + Factory::getApplication()->triggerEvent($this->event_after_save, array('com_templates.style', &$table, $isNew)); + + $this->setState('style.id', $table->id); + + return true; + } + + /** + * Method to set a template style as home. + * + * @param integer $id The primary key ID for the style. + * + * @return boolean True if successful. + * + * @throws \Exception + */ + public function setHome($id = 0) + { + $user = Factory::getUser(); + $db = $this->getDatabase(); + + // Access checks. + if (!$user->authorise('core.edit.state', 'com_templates')) { + throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); + } + + $style = $this->getTable(); + + if (!$style->load((int) $id)) { + throw new \Exception(Text::_('COM_TEMPLATES_ERROR_STYLE_NOT_FOUND')); + } + + // Detect disabled extension + $extension = Table::getInstance('Extension', 'Joomla\\CMS\\Table\\'); + + if ($extension->load(array('enabled' => 0, 'type' => 'template', 'element' => $style->template, 'client_id' => $style->client_id))) { + throw new \Exception(Text::_('COM_TEMPLATES_ERROR_SAVE_DISABLED_TEMPLATE')); + } + + $clientId = (int) $style->client_id; + $id = (int) $id; + + // Reset the home fields for the client_id. + $query = $db->getQuery(true) + ->update($db->quoteName('#__template_styles')) + ->set($db->quoteName('home') . ' = ' . $db->quote('0')) + ->where($db->quoteName('client_id') . ' = :clientid') + ->where($db->quoteName('home') . ' = ' . $db->quote('1')) + ->bind(':clientid', $clientId, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + // Set the new home style. + $query = $db->getQuery(true) + ->update($db->quoteName('#__template_styles')) + ->set($db->quoteName('home') . ' = ' . $db->quote('1')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + // Clean the cache. + $this->cleanCache(); + + return true; + } + + /** + * Method to unset a template style as default for a language. + * + * @param integer $id The primary key ID for the style. + * + * @return boolean True if successful. + * + * @throws \Exception + */ + public function unsetHome($id = 0) + { + $user = Factory::getUser(); + $db = $this->getDatabase(); + + // Access checks. + if (!$user->authorise('core.edit.state', 'com_templates')) { + throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); + } + + $id = (int) $id; + + // Lookup the client_id. + $query = $db->getQuery(true) + ->select($db->quoteName(['client_id', 'home'])) + ->from($db->quoteName('#__template_styles')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $style = $db->loadObject(); + + if (!is_numeric($style->client_id)) { + throw new \Exception(Text::_('COM_TEMPLATES_ERROR_STYLE_NOT_FOUND')); + } elseif ($style->home == '1') { + throw new \Exception(Text::_('COM_TEMPLATES_ERROR_CANNOT_UNSET_DEFAULT_STYLE')); + } + + // Set the new home style. + $query = $db->getQuery(true) + ->update($db->quoteName('#__template_styles')) + ->set($db->quoteName('home') . ' = ' . $db->quote('0')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + + // Clean the cache. + $this->cleanCache(); + + return true; + } + + /** + * Get the necessary data to load an item help screen. + * + * @return object An object with key, url, and local properties for loading the item help screen. + * + * @since 1.6 + */ + public function getHelp() + { + return (object) array('key' => $this->helpKey, 'url' => $this->helpURL); + } + + /** + * Returns the back end template for the given style. + * + * @param int $styleId The style id + * + * @return stdClass + * + * @since 4.2.0 + */ + public function getAdminTemplate(int $styleId): stdClass + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['s.template', 's.params', 's.inheritable', 's.parent'])) + ->from($db->quoteName('#__template_styles', 's')) + ->join( + 'LEFT', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.type') . ' = ' . $db->quote('template') + . ' AND ' . $db->quoteName('e.element') . ' = ' . $db->quoteName('s.template') + . ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('s.client_id') + ) + ->where( + [ + $db->quoteName('s.client_id') . ' = 1', + $db->quoteName('s.home') . ' = ' . $db->quote('1'), + ] + ); + + if ($styleId) { + $query->extendWhere( + 'OR', + [ + $db->quoteName('s.client_id') . ' = 1', + $db->quoteName('s.id') . ' = :style', + $db->quoteName('e.enabled') . ' = 1', + ] + ) + ->bind(':style', $styleId, ParameterType::INTEGER); + } + + $query->order($db->quoteName('s.home')); + $db->setQuery($query); + + return $db->loadObject(); + } + + /** + * Returns the front end templates. + * + * @return array + * + * @since 4.2.0 + */ + public function getSiteTemplates(): array + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['id', 'home', 'template', 's.params', 'inheritable', 'parent'])) + ->from($db->quoteName('#__template_styles', 's')) + ->where( + [ + $db->quoteName('s.client_id') . ' = 0', + $db->quoteName('e.enabled') . ' = 1', + ] + ) + ->join( + 'LEFT', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.element') . ' = ' . $db->quoteName('s.template') + . ' AND ' . $db->quoteName('e.type') . ' = ' . $db->quote('template') + . ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('s.client_id') + ); + + $db->setQuery($query); + + return $db->loadObjectList('id'); + } + + /** + * Custom clean cache method + * + * @param string $group The cache group + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 1.6 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('com_templates'); + parent::cleanCache('_system'); + } } diff --git a/administrator/components/com_templates/src/Model/StylesModel.php b/administrator/components/com_templates/src/Model/StylesModel.php index c635eaabb4614..96d8345970bda 100644 --- a/administrator/components/com_templates/src/Model/StylesModel.php +++ b/administrator/components/com_templates/src/Model/StylesModel.php @@ -1,4 +1,5 @@ isClient('api')) - { - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('filter.template', $this->getUserStateFromRequest($this->context . '.filter.template', 'filter_template', '', 'string')); - $this->setState('filter.menuitem', $this->getUserStateFromRequest($this->context . '.filter.menuitem', 'filter_menuitem', '', 'cmd')); - - // Special case for the client id. - $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); - $clientId = !in_array($clientId, [0, 1]) ? 0 : $clientId; - $this->setState('client_id', $clientId); - } - - // Load the parameters. - $params = ComponentHelper::getParams('com_templates'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('client_id'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.template'); - $id .= ':' . $this->getState('filter.menuitem'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - */ - protected function getListQuery() - { - $clientId = (int) $this->getState('client_id'); - - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.template'), - $db->quoteName('a.title'), - $db->quoteName('a.home'), - $db->quoteName('a.client_id'), - $db->quoteName('l.title', 'language_title'), - $db->quoteName('l.image'), - $db->quoteName('l.sef', 'language_sef'), - ] - ) - ) - ->select( - [ - 'COUNT(' . $db->quoteName('m.template_style_id') . ') AS assigned', - $db->quoteName('extension_id', 'e_id'), - ] - ) - ->from($db->quoteName('#__template_styles', 'a')) - ->where($db->quoteName('a.client_id') . ' = :clientid') - ->bind(':clientid', $clientId, ParameterType::INTEGER); - - // Join on menus. - $query->join('LEFT', $db->quoteName('#__menu', 'm'), $db->quoteName('m.template_style_id') . ' = ' . $db->quoteName('a.id')) - ->group( - [ - $db->quoteName('a.id'), - $db->quoteName('a.template'), - $db->quoteName('a.title'), - $db->quoteName('a.home'), - $db->quoteName('a.client_id'), - $db->quoteName('l.title'), - $db->quoteName('l.image'), - $db->quoteName('l.sef'), - $db->quoteName('e.extension_id'), - ] - ); - - // Join over the language. - $query->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.home')); - - // Filter by extension enabled. - $query->join( - 'LEFT', - $db->quoteName('#__extensions', 'e'), - $db->quoteName('e.element') . ' = ' . $db->quoteName('a.template') - . ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('a.client_id') - ) - ->where( - [ - $db->quoteName('e.enabled') . ' = 1', - $db->quoteName('e.type') . ' = ' . $db->quote('template'), - ] - ); - - // Filter by template. - if ($template = $this->getState('filter.template')) - { - $query->where($db->quoteName('a.template') . ' = :template') - ->bind(':template', $template); - } - - // Filter by menuitem. - $menuItemId = $this->getState('filter.menuitem'); - - if ($clientId === 0 && is_numeric($menuItemId)) - { - // If user selected the templates styles that are not assigned to any page. - if ((int) $menuItemId === -1) - { - // Only custom template styles overrides not assigned to any menu item. - $query->where( - [ - $db->quoteName('a.home') . ' = ' . $db->quote('0'), - $db->quoteName('m.id') . ' IS NULL', - ] - ); - } - // If user selected the templates styles assigned to particular pages. - else - { - // Subquery to get the language of the selected menu item. - $menuItemId = (int) $menuItemId; - $menuItemLanguageSubQuery = $db->getQuery(true); - $menuItemLanguageSubQuery->select($db->quoteName('language')) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('id') . ' = :menuitemid'); - $query->bind(':menuitemid', $menuItemId, ParameterType::INTEGER); - - // Subquery to get the language of the selected menu item. - $templateStylesMenuItemsSubQuery = $db->getQuery(true); - $templateStylesMenuItemsSubQuery->select($db->quoteName('id')) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('template_style_id') . ' = ' . $db->quoteName('a.id')); - - // Main query where clause. - $query->where('(' . - // Default template style (fallback template style to all menu items). - $db->quoteName('a.home') . ' = ' . $db->quote('1') . ' OR ' . - // Default template style for specific language (fallback template style to the selected menu item language). - $db->quoteName('a.home') . ' IN (' . $menuItemLanguageSubQuery . ') OR ' . - // Custom template styles override (only if assigned to the selected menu item). - '(' . $db->quoteName('a.home') . ' = ' . $db->quote('0') . ' AND ' . $menuItemId . ' IN (' . $templateStylesMenuItemsSubQuery . '))' . - ')' - ); - } - } - - // Filter by search in title. - if ($search = $this->getState('filter.search')) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . StringHelper::strtolower($search) . '%'; - $query->extendWhere( - 'AND', - [ - 'LOWER(' . $db->quoteName('a.template') . ') LIKE :template', - 'LOWER(' . $db->quoteName('a.title') . ') LIKE :title', - ], - 'OR' - ) - ->bind(':template', $search) - ->bind(':title', $search); - } - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.template')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'template', 'a.template', + 'home', 'a.home', + 'menuitem', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.template', $direction = 'asc') + { + $app = Factory::getApplication(); + + if (!$app->isClient('api')) { + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('filter.template', $this->getUserStateFromRequest($this->context . '.filter.template', 'filter_template', '', 'string')); + $this->setState('filter.menuitem', $this->getUserStateFromRequest($this->context . '.filter.menuitem', 'filter_menuitem', '', 'cmd')); + + // Special case for the client id. + $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); + $clientId = !in_array($clientId, [0, 1]) ? 0 : $clientId; + $this->setState('client_id', $clientId); + } + + // Load the parameters. + $params = ComponentHelper::getParams('com_templates'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('client_id'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.template'); + $id .= ':' . $this->getState('filter.menuitem'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + */ + protected function getListQuery() + { + $clientId = (int) $this->getState('client_id'); + + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.template'), + $db->quoteName('a.title'), + $db->quoteName('a.home'), + $db->quoteName('a.client_id'), + $db->quoteName('l.title', 'language_title'), + $db->quoteName('l.image'), + $db->quoteName('l.sef', 'language_sef'), + ] + ) + ) + ->select( + [ + 'COUNT(' . $db->quoteName('m.template_style_id') . ') AS assigned', + $db->quoteName('extension_id', 'e_id'), + ] + ) + ->from($db->quoteName('#__template_styles', 'a')) + ->where($db->quoteName('a.client_id') . ' = :clientid') + ->bind(':clientid', $clientId, ParameterType::INTEGER); + + // Join on menus. + $query->join('LEFT', $db->quoteName('#__menu', 'm'), $db->quoteName('m.template_style_id') . ' = ' . $db->quoteName('a.id')) + ->group( + [ + $db->quoteName('a.id'), + $db->quoteName('a.template'), + $db->quoteName('a.title'), + $db->quoteName('a.home'), + $db->quoteName('a.client_id'), + $db->quoteName('l.title'), + $db->quoteName('l.image'), + $db->quoteName('l.sef'), + $db->quoteName('e.extension_id'), + ] + ); + + // Join over the language. + $query->join('LEFT', $db->quoteName('#__languages', 'l'), $db->quoteName('l.lang_code') . ' = ' . $db->quoteName('a.home')); + + // Filter by extension enabled. + $query->join( + 'LEFT', + $db->quoteName('#__extensions', 'e'), + $db->quoteName('e.element') . ' = ' . $db->quoteName('a.template') + . ' AND ' . $db->quoteName('e.client_id') . ' = ' . $db->quoteName('a.client_id') + ) + ->where( + [ + $db->quoteName('e.enabled') . ' = 1', + $db->quoteName('e.type') . ' = ' . $db->quote('template'), + ] + ); + + // Filter by template. + if ($template = $this->getState('filter.template')) { + $query->where($db->quoteName('a.template') . ' = :template') + ->bind(':template', $template); + } + + // Filter by menuitem. + $menuItemId = $this->getState('filter.menuitem'); + + if ($clientId === 0 && is_numeric($menuItemId)) { + // If user selected the templates styles that are not assigned to any page. + if ((int) $menuItemId === -1) { + // Only custom template styles overrides not assigned to any menu item. + $query->where( + [ + $db->quoteName('a.home') . ' = ' . $db->quote('0'), + $db->quoteName('m.id') . ' IS NULL', + ] + ); + } else { + // If user selected the templates styles assigned to particular pages. + // Subquery to get the language of the selected menu item. + $menuItemId = (int) $menuItemId; + $menuItemLanguageSubQuery = $db->getQuery(true); + $menuItemLanguageSubQuery->select($db->quoteName('language')) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('id') . ' = :menuitemid'); + $query->bind(':menuitemid', $menuItemId, ParameterType::INTEGER); + + // Subquery to get the language of the selected menu item. + $templateStylesMenuItemsSubQuery = $db->getQuery(true); + $templateStylesMenuItemsSubQuery->select($db->quoteName('id')) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('template_style_id') . ' = ' . $db->quoteName('a.id')); + + // Main query where clause. + $query->where('(' . + // Default template style (fallback template style to all menu items). + $db->quoteName('a.home') . ' = ' . $db->quote('1') . ' OR ' . + // Default template style for specific language (fallback template style to the selected menu item language). + $db->quoteName('a.home') . ' IN (' . $menuItemLanguageSubQuery . ') OR ' . + // Custom template styles override (only if assigned to the selected menu item). + '(' . $db->quoteName('a.home') . ' = ' . $db->quote('0') . ' AND ' . $menuItemId . ' IN (' . $templateStylesMenuItemsSubQuery . '))' . + ')'); + } + } + + // Filter by search in title. + if ($search = $this->getState('filter.search')) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . StringHelper::strtolower($search) . '%'; + $query->extendWhere( + 'AND', + [ + 'LOWER(' . $db->quoteName('a.template') . ') LIKE :template', + 'LOWER(' . $db->quoteName('a.title') . ') LIKE :title', + ], + 'OR' + ) + ->bind(':template', $search) + ->bind(':title', $search); + } + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.template')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } } diff --git a/administrator/components/com_templates/src/Model/TemplateModel.php b/administrator/components/com_templates/src/Model/TemplateModel.php index 50267364f0195..f9e3c14c9870c 100644 --- a/administrator/components/com_templates/src/Model/TemplateModel.php +++ b/administrator/components/com_templates/src/Model/TemplateModel.php @@ -1,4 +1,5 @@ getTemplate()) - { - $path = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? 'site' : 'administrator') . DIRECTORY_SEPARATOR . $this->template->element, '', $path); - $path = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? '' : 'administrator' . DIRECTORY_SEPARATOR) . 'templates' . DIRECTORY_SEPARATOR . $this->template->element, '', $path); - $temp->name = $name; - $temp->id = urlencode(base64_encode(str_replace('\\', '//', $path))); - - return $temp; - } - } - - /** - * Method to store file information. - * - * @param string $path The base path. - * @param string $name The file name. - * @param stdClass $template The std class object of template. - * - * @return object stdClass object. - * - * @since 4.0.0 - */ - protected function storeFileInfo($path, $name, $template) - { - $temp = new \stdClass; - $temp->id = base64_encode($path . $name); - $temp->client = $template->client_id; - $temp->template = $template->element; - $temp->extension_id = $template->extension_id; - - if ($coreFile = $this->getCoreFile($path . $name, $template->client_id)) - { - $temp->coreFile = md5_file($coreFile); - } - else - { - $temp->coreFile = null; - } - - return $temp; - } - - /** - * Method to get all template list. - * - * @return object stdClass object - * - * @since 4.0.0 - */ - public function getTemplateList() - { - // Get a db connection. - $db = $this->getDatabase(); - - // Create a new query object. - $query = $db->getQuery(true); - - // Select the required fields from the table - $query->select( - $this->getState( - 'list.select', - 'a.extension_id, a.name, a.element, a.client_id' - ) - ); - - $query->from($db->quoteName('#__extensions', 'a')) - ->where($db->quoteName('a.enabled') . ' = 1') - ->where($db->quoteName('a.type') . ' = ' . $db->quote('template')); - - // Reset the query. - $db->setQuery($query); - - // Load the results as a list of stdClass objects. - $results = $db->loadObjectList(); - - return $results; - } - - /** - * Method to get all updated file list. - * - * @param boolean $state The optional parameter if you want unchecked list. - * @param boolean $all The optional parameter if you want all list. - * @param boolean $cleanup The optional parameter if you want to clean record which is no more required. - * - * @return object stdClass object - * - * @since 4.0.0 - */ - public function getUpdatedList($state = false, $all = false, $cleanup = false) - { - // Get a db connection. - $db = $this->getDatabase(); - - // Create a new query object. - $query = $db->getQuery(true); - - // Select the required fields from the table - $query->select( - $this->getState( - 'list.select', - 'a.template, a.hash_id, a.extension_id, a.state, a.action, a.client_id, a.created_date, a.modified_date' - ) - ); - - $template = $this->getTemplate(); - - $query->from($db->quoteName('#__template_overrides', 'a')); - - if (!$all) - { - $teid = (int) $template->extension_id; - $query->where($db->quoteName('extension_id') . ' = :teid') - ->bind(':teid', $teid, ParameterType::INTEGER); - } - - if ($state) - { - $query->where($db->quoteName('state') . ' = 0'); - } - - $query->order($db->quoteName('a.modified_date') . ' DESC'); - - // Reset the query. - $db->setQuery($query); - - // Load the results as a list of stdClass objects. - $pks = $db->loadObjectList(); - - if ($state) - { - return $pks; - } - - $results = array(); - - foreach ($pks as $pk) - { - $client = ApplicationHelper::getClientInfo($pk->client_id); - $path = Path::clean($client->path . '/templates/' . $pk->template . base64_decode($pk->hash_id)); - - if (file_exists($path)) - { - $results[] = $pk; - } - elseif ($cleanup) - { - $cleanupIds = array(); - $cleanupIds[] = $pk->hash_id; - $this->publish($cleanupIds, -3, $pk->extension_id); - } - } - - return $results; - } - - /** - * Method to get a list of all the core files of override files. - * - * @return array An array of all core files. - * - * @since 4.0.0 - */ - public function getCoreList() - { - // Get list of all templates - $templates = $this->getTemplateList(); - - // Initialize the array variable to store core file list. - $this->coreFileList = array(); - - $app = Factory::getApplication(); - - foreach ($templates as $template) - { - $client = ApplicationHelper::getClientInfo($template->client_id); - $element = Path::clean($client->path . '/templates/' . $template->element . '/'); - $path = Path::clean($element . 'html/'); - - if (is_dir($path)) - { - $this->prepareCoreFiles($path, $element, $template); - } - } - - // Sort list of stdClass array. - usort( - $this->coreFileList, - function ($a, $b) - { - return strcmp($a->id, $b->id); - } - ); - - return $this->coreFileList; - } - - /** - * Prepare core files. - * - * @param string $dir The path of the directory to scan. - * @param string $element The path of the template element. - * @param \stdClass $template The stdClass object of template. - * - * @return array - * - * @since 4.0.0 - */ - public function prepareCoreFiles($dir, $element, $template) - { - $dirFiles = scandir($dir); - - foreach ($dirFiles as $key => $value) - { - if (in_array($value, array('.', '..', 'node_modules'))) - { - continue; - } - - if (is_dir($dir . $value)) - { - $relativePath = str_replace($element, '', $dir . $value); - $this->prepareCoreFiles($dir . $value . '/', $element, $template); - } - else - { - $ext = pathinfo($dir . $value, PATHINFO_EXTENSION); - $allowedFormat = $this->checkFormat($ext); - - if ($allowedFormat === true) - { - $relativePath = str_replace($element, '', $dir); - $info = $this->storeFileInfo('/' . $relativePath, $value, $template); - - if ($info) - { - $this->coreFileList[] = $info; - } - } - } - } - } - - /** - * Method to update status of list. - * - * @param array $ids The base path. - * @param array $value The file name. - * @param integer $exid The template extension id. - * - * @return integer Number of files changed. - * - * @since 4.0.0 - */ - public function publish($ids, $value, $exid) - { - $db = $this->getDatabase(); - - foreach ($ids as $id) - { - if ($value === -3) - { - $deleteQuery = $db->getQuery(true) - ->delete($db->quoteName('#__template_overrides')) - ->where($db->quoteName('hash_id') . ' = :hashid') - ->where($db->quoteName('extension_id') . ' = :exid') - ->bind(':hashid', $id) - ->bind(':exid', $exid, ParameterType::INTEGER); - - try - { - // Set the query using our newly populated query object and execute it. - $db->setQuery($deleteQuery); - $result = $db->execute(); - } - catch (\RuntimeException $e) - { - return $e; - } - } - elseif ($value === 1 || $value === 0) - { - $updateQuery = $db->getQuery(true) - ->update($db->quoteName('#__template_overrides')) - ->set($db->quoteName('state') . ' = :state') - ->where($db->quoteName('hash_id') . ' = :hashid') - ->where($db->quoteName('extension_id') . ' = :exid') - ->bind(':state', $value, ParameterType::INTEGER) - ->bind(':hashid', $id) - ->bind(':exid', $exid, ParameterType::INTEGER); - - try - { - // Set the query using our newly populated query object and execute it. - $db->setQuery($updateQuery); - $result = $db->execute(); - } - catch (\RuntimeException $e) - { - return $e; - } - } - } - - return $result; - } - - /** - * Method to get a list of all the files to edit in a template. - * - * @return array A nested array of relevant files. - * - * @since 1.6 - */ - public function getFiles() - { - $result = array(); - - if ($template = $this->getTemplate()) - { - $app = Factory::getApplication(); - $client = ApplicationHelper::getClientInfo($template->client_id); - $path = Path::clean($client->path . '/templates/' . $template->element . '/'); - $lang = Factory::getLanguage(); - - // Load the core and/or local language file(s). - $lang->load('tpl_' . $template->element, $client->path) - || (!empty($template->xmldata->parent) && $lang->load('tpl_' . $template->xmldata->parent, $client->path)) - || $lang->load('tpl_' . $template->element, $client->path . '/templates/' . $template->element) - || (!empty($template->xmldata->parent) && $lang->load('tpl_' . $template->xmldata->parent, $client->path . '/templates/' . $template->xmldata->parent)); - $this->element = $path; - - if (!is_writable($path)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_DIRECTORY_NOT_WRITABLE'), 'error'); - } - - if (is_dir($path)) - { - $result = $this->getDirectoryTree($path); - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_TEMPLATE_FOLDER_NOT_FOUND'), 'error'); - - return false; - } - - // Clean up override history - $this->getUpdatedList(false, true, true); - } - - return $result; - } - - /** - * Get the directory tree. - * - * @param string $dir The path of the directory to scan - * - * @return array - * - * @since 3.2 - */ - public function getDirectoryTree($dir) - { - $result = array(); - - $dirFiles = scandir($dir); - - foreach ($dirFiles as $key => $value) - { - if (!in_array($value, array('.', '..', 'node_modules'))) - { - if (is_dir($dir . $value)) - { - $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? 'site' : 'administrator') . DIRECTORY_SEPARATOR . $this->template->element, '', $dir . $value); - $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? '' : 'administrator' . DIRECTORY_SEPARATOR) .'templates' . DIRECTORY_SEPARATOR . $this->template->element, '', $relativePath); - $result[str_replace('\\', '//', $relativePath)] = $this->getDirectoryTree($dir . $value . '/'); - } - else - { - $ext = pathinfo($dir . $value, PATHINFO_EXTENSION); - $allowedFormat = $this->checkFormat($ext); - - if ($allowedFormat == true) - { - $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . 'media'. DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? 'site' : 'administrator') . DIRECTORY_SEPARATOR . $this->template->element, '', $dir . $value); - $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? '' : 'administrator' . DIRECTORY_SEPARATOR) . 'templates' . DIRECTORY_SEPARATOR . $this->template->element, '', $relativePath); - $result[] = $this->getFile($relativePath, $value); - } - } - } - } - - return $result; - } - - /** - * Method to get the core file of override file - * - * @param string $file Override file - * @param integer $client_id Client Id - * - * @return string $corefile The full path and file name for the target file, or boolean false if the file is not found in any of the paths. - * - * @since 4.0.0 - */ - public function getCoreFile($file, $client_id) - { - $app = Factory::getApplication(); - $filePath = Path::clean($file); - $explodeArray = explode(DIRECTORY_SEPARATOR, $filePath); - - // Only allow html/ folder - if ($explodeArray['1'] !== 'html') - { - return false; - } - - $fileName = basename($filePath); - $type = $explodeArray['2']; - $client = ApplicationHelper::getClientInfo($client_id); - - $componentPath = Path::clean($client->path . '/components/'); - $modulePath = Path::clean($client->path . '/modules/'); - $layoutPath = Path::clean(JPATH_ROOT . '/layouts/'); - - // For modules - if (stristr($type, 'mod_') !== false) - { - $folder = $explodeArray['2']; - $htmlPath = Path::clean($modulePath . $folder . '/tmpl/'); - $fileName = $this->getSafeName($fileName); - $coreFile = Path::find($htmlPath, $fileName); - - return $coreFile; - } - elseif (stristr($type, 'com_') !== false) - { - // For components - $folder = $explodeArray['2']; - $subFolder = $explodeArray['3']; - $fileName = $this->getSafeName($fileName); - - // The new scheme, if a view has a tmpl folder - $newHtmlPath = Path::clean($componentPath . $folder . '/tmpl/' . $subFolder . '/'); - - if (!$coreFile = Path::find($newHtmlPath, $fileName)) - { - // The old scheme, the views are directly in the component/tmpl folder - $oldHtmlPath = Path::clean($componentPath . $folder . '/views/' . $subFolder . '/tmpl/'); - $coreFile = Path::find($oldHtmlPath, $fileName); - - return $coreFile; - } - - return $coreFile; - } - elseif (stristr($type, 'layouts') !== false) - { - // For Layouts - $subtype = $explodeArray['3']; - - if (stristr($subtype, 'com_')) - { - $folder = $explodeArray['3']; - $subFolder = array_slice($explodeArray, 4, -1); - $subFolder = implode(DIRECTORY_SEPARATOR, $subFolder); - $htmlPath = Path::clean($componentPath . $folder . '/layouts/' . $subFolder); - $fileName = $this->getSafeName($fileName); - $coreFile = Path::find($htmlPath, $fileName); - - return $coreFile; - } - elseif (stristr($subtype, 'joomla') || stristr($subtype, 'libraries') || stristr($subtype, 'plugins')) - { - $subFolder = array_slice($explodeArray, 3, -1); - $subFolder = implode(DIRECTORY_SEPARATOR, $subFolder); - $htmlPath = Path::clean($layoutPath . $subFolder); - $fileName = $this->getSafeName($fileName); - $coreFile = Path::find($htmlPath, $fileName); - - return $coreFile; - } - } - - return false; - } - - /** - * Creates a safe file name for the given name. - * - * @param string $name The filename - * - * @return string $fileName The filtered name without Date - * - * @since 4.0.0 - */ - private function getSafeName($name) - { - if (strpos($name, '-') !== false && preg_match('/[0-9]/', $name)) - { - // Get the extension - $extension = File::getExt($name); - - // Remove ( Date ) from file - $explodeArray = explode('-', $name); - $size = count($explodeArray); - $date = $explodeArray[$size - 2] . '-' . str_replace('.' . $extension, '', $explodeArray[$size - 1]); - - if ($this->validateDate($date)) - { - $nameWithoutExtension = implode('-', array_slice($explodeArray, 0, -2)); - - // Filtered name - $name = $nameWithoutExtension . '.' . $extension; - } - } - - return $name; - } - - /** - * Validate Date in file name. - * - * @param string $date Date to validate. - * - * @return boolean Return true if date is valid and false if not. - * - * @since 4.0.0 - */ - private function validateDate($date) - { - $format = 'Ymd-His'; - $valid = Date::createFromFormat($format, $date); - - return $valid && $valid->format($format) === $date; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - */ - protected function populateState() - { - $app = Factory::getApplication(); - - // Load the User state. - $pk = $app->input->getInt('id'); - $this->setState('extension.id', $pk); - - // Load the parameters. - $params = ComponentHelper::getParams('com_templates'); - $this->setState('params', $params); - } - - /** - * Method to get the template information. - * - * @return mixed Object if successful, false if not and internal error is set. - * - * @since 1.6 - */ - public function &getTemplate() - { - if (empty($this->template)) - { - $pk = (int) $this->getState('extension.id'); - $db = $this->getDatabase(); - $app = Factory::getApplication(); - - // Get the template information. - $query = $db->getQuery(true) - ->select($db->quoteName(['extension_id', 'client_id', 'element', 'name', 'manifest_cache'])) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('extension_id') . ' = :pk') - ->where($db->quoteName('type') . ' = ' . $db->quote('template')) - ->bind(':pk', $pk, ParameterType::INTEGER); - $db->setQuery($query); - - try - { - $result = $db->loadObject(); - } - catch (\RuntimeException $e) - { - $app->enqueueMessage($e->getMessage(), 'warning'); - $this->template = false; - - return false; - } - - if (empty($result)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EXTENSION_RECORD_NOT_FOUND'), 'error'); - $this->template = false; - } - else - { - $this->template = $result; - - // Client ID is not always an integer, so enforce here - $this->template->client_id = (int) $this->template->client_id; - - if (!isset($this->template->xmldata)) - { - $this->template->xmldata = TemplatesHelper::parseXMLTemplateFile($this->template->client_id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $this->template->name); - } - } - } - - return $this->template; - } - - /** - * Method to check if new template name already exists - * - * @return boolean true if name is not used, false otherwise - * - * @since 2.5 - */ - public function checkNewName() - { - $db = $this->getDatabase(); - $name = $this->getState('new_name'); - $query = $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('name') . ' = :name') - ->bind(':name', $name); - $db->setQuery($query); - - return ($db->loadResult() == 0); - } - - /** - * Method to check if new template name already exists - * - * @return string name of current template - * - * @since 2.5 - */ - public function getFromName() - { - return $this->getTemplate()->element; - } - - /** - * Method to check if new template name already exists - * - * @return boolean true if name is not used, false otherwise - * - * @since 2.5 - */ - public function copy() - { - $app = Factory::getApplication(); - - if ($template = $this->getTemplate()) - { - $client = ApplicationHelper::getClientInfo($template->client_id); - $fromPath = Path::clean($client->path . '/templates/' . $template->element . '/'); - - // Delete new folder if it exists - $toPath = $this->getState('to_path'); - - if (Folder::exists($toPath)) - { - if (!Folder::delete($toPath)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); - - return false; - } - } - - // Copy all files from $fromName template to $newName folder - if (!Folder::copy($fromPath, $toPath)) - { - return false; - } - - // Check manifest for additional files - $manifest = simplexml_load_file($toPath . '/templateDetails.xml'); - - // Copy language files from global folder - if ($languages = $manifest->languages) - { - $folder = (string) $languages->attributes()->folder; - $languageFiles = $languages->language; - - Folder::create($toPath . '/' . $folder . '/' . $languageFiles->attributes()->tag); - - foreach ($languageFiles as $languageFile) - { - $src = Path::clean($client->path . '/language/' . $languageFile); - $dst = Path::clean($toPath . '/' . $folder . '/' . $languageFile); - - if (File::exists($src)) - { - File::copy($src, $dst); - } - } - } - - // Copy media files - if ($media = $manifest->media) - { - $folder = (string) $media->attributes()->folder; - $destination = (string) $media->attributes()->destination; - - Folder::copy(JPATH_SITE . '/media/' . $destination, $toPath . '/' . $folder); - } - - // Adjust to new template name - if (!$this->fixTemplateName()) - { - return false; - } - - return true; - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); - - return false; - } - } - - /** - * Method to delete tmp folder - * - * @return boolean true if delete successful, false otherwise - * - * @since 2.5 - */ - public function cleanup() - { - // Clear installation messages - $app = Factory::getApplication(); - $app->setUserState('com_installer.message', ''); - $app->setUserState('com_installer.extension_message', ''); - - // Delete temporary directory - return Folder::delete($this->getState('to_path')); - } - - /** - * Method to rename the template in the XML files and rename the language files - * - * @return boolean true if successful, false otherwise - * - * @since 2.5 - */ - protected function fixTemplateName() - { - // Rename Language files - // Get list of language files - $result = true; - $files = Folder::files($this->getState('to_path'), '\.ini$', true, true); - $newName = strtolower($this->getState('new_name')); - $template = $this->getTemplate(); - $oldName = $template->element; - $manifest = json_decode($template->manifest_cache); - - foreach ($files as $file) - { - $newFile = '/' . str_replace($oldName, $newName, basename($file)); - $result = File::move($file, dirname($file) . $newFile) && $result; - } - - // Edit XML file - $xmlFile = $this->getState('to_path') . '/templateDetails.xml'; - - if (File::exists($xmlFile)) - { - $contents = file_get_contents($xmlFile); - $pattern[] = '#\s*' . $manifest->name . '\s*#i'; - $replace[] = '' . $newName . ''; - $pattern[] = '##'; - $replace[] = ''; - $pattern[] = '##'; - $replace[] = ''; - $contents = preg_replace($pattern, $replace, $contents); - $result = File::write($xmlFile, $contents) && $result; - } - - return $result; - } - - /** - * Method to get the record form. - * - * @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 Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - $app = Factory::getApplication(); - - // Codemirror or Editor None should be enabled - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('COUNT(*)') - ->from('#__extensions as a') - ->where( - '(a.name =' . $db->quote('plg_editors_codemirror') . - ' AND a.enabled = 1) OR (a.name =' . - $db->quote('plg_editors_none') . - ' AND a.enabled = 1)' - ); - $db->setQuery($query); - $state = $db->loadResult(); - - if ((int) $state < 1) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EDITOR_DISABLED'), 'warning'); - } - - // Get the form. - $form = $this->loadForm('com_templates.source', 'source', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - $data = $this->getSource(); - - $this->preprocessData('com_templates.source', $data); - - return $data; - } - - /** - * Method to get a single record. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function &getSource() - { - $app = Factory::getApplication(); - $item = new \stdClass; - - if (!$this->template) - { - $this->getTemplate(); - } - - if ($this->template) - { - $input = Factory::getApplication()->input; - $fileName = base64_decode($input->get('file')); - $fileName = str_replace('//', '/', $fileName); - $isMedia = $input->getInt('isMedia', 0); - - $fileName = $isMedia ? Path::clean(JPATH_ROOT . '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element . $fileName) - : Path::clean(JPATH_ROOT . ($this->template->client_id === 0 ? '' : '/administrator') . '/templates/' . $this->template->element . $fileName); - - try - { - $filePath = Path::check($fileName); - } - catch (\Exception $e) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_FILE_NOT_FOUND'), 'error'); - - return; - } - - if (file_exists($filePath)) - { - $item->extension_id = $this->getState('extension.id'); - $item->filename = Path::clean($fileName); - $item->source = file_get_contents($filePath); - $item->filePath = Path::clean($filePath); - $ds = DIRECTORY_SEPARATOR; - $cleanFileName = str_replace(JPATH_ROOT . ($this->template->client_id === 1 ? $ds . 'administrator' . $ds : $ds) . 'templates' . $ds . $this->template->element, '', $fileName); - - if ($coreFile = $this->getCoreFile($cleanFileName, $this->template->client_id)) - { - $item->coreFile = $coreFile; - $item->core = file_get_contents($coreFile); - } - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_FILE_NOT_FOUND'), 'error'); - } - } - - return $item; - } - - /** - * Method to store the source file contents. - * - * @param array $data The source data to save. - * - * @return boolean True on success, false otherwise and internal error set. - * - * @since 1.6 - */ - public function save($data) - { - // Get the template. - $template = $this->getTemplate(); - - if (empty($template)) - { - return false; - } - - $app = Factory::getApplication(); - $fileName = base64_decode($app->input->get('file')); - $isMedia = $app->input->getInt('isMedia', 0); - $fileName = $isMedia ? JPATH_ROOT . '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element . $fileName : - JPATH_ROOT . '/' . ($this->template->client_id === 0 ? '' : 'administrator/') . 'templates/' . $this->template->element . $fileName; - - $filePath = Path::clean($fileName); - - // Include the extension plugins for the save events. - PluginHelper::importPlugin('extension'); - - $user = get_current_user(); - chown($filePath, $user); - Path::setPermissions($filePath, '0644'); - - // Try to make the template file writable. - if (!is_writable($filePath)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_FILE_NOT_WRITABLE'), 'warning'); - $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_FILE_PERMISSIONS', Path::getPermissions($filePath)), 'warning'); - - if (!Path::isOwner($filePath)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_CHECK_FILE_OWNERSHIP'), 'warning'); - } - - return false; - } - - // Make sure EOL is Unix - $data['source'] = str_replace(array("\r\n", "\r"), "\n", $data['source']); - - // If the asset file for the template ensure we have valid template so we don't instantly destroy it - if ($fileName === '/joomla.asset.json' && json_decode($data['source']) === null) - { - $this->setError(Text::_('COM_TEMPLATES_ERROR_ASSET_FILE_INVALID_JSON')); - - return false; - } - - $return = File::write($filePath, $data['source']); - - if (!$return) - { - $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_ERROR_FAILED_TO_SAVE_FILENAME', $fileName), 'error'); - - return false; - } - - // Get the extension of the changed file. - $explodeArray = explode('.', $fileName); - $ext = end($explodeArray); - - if ($ext == 'less') - { - $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_COMPILE_LESS', $fileName)); - } - - return true; - } - - /** - * Get overrides folder. - * - * @param string $name The name of override. - * @param string $path Location of override. - * - * @return object containing override name and path. - * - * @since 3.2 - */ - public function getOverridesFolder($name,$path) - { - $folder = new \stdClass; - $folder->name = $name; - $folder->path = base64_encode($path . $name); - - return $folder; - } - - /** - * Get a list of overrides. - * - * @return array containing overrides. - * - * @since 3.2 - */ - public function getOverridesList() - { - if ($template = $this->getTemplate()) - { - $client = ApplicationHelper::getClientInfo($template->client_id); - $componentPath = Path::clean($client->path . '/components/'); - $modulePath = Path::clean($client->path . '/modules/'); - $pluginPath = Path::clean(JPATH_ROOT . '/plugins/'); - $layoutPath = Path::clean(JPATH_ROOT . '/layouts/'); - $components = Folder::folders($componentPath); - - foreach ($components as $component) - { - // Collect the folders with views - $folders = Folder::folders($componentPath . '/' . $component, '^view[s]?$', false, true); - $folders = array_merge($folders, Folder::folders($componentPath . '/' . $component, '^tmpl?$', false, true)); - - if (!$folders) - { - continue; - } - - foreach ($folders as $folder) - { - // The subfolders are views - $views = Folder::folders($folder); - - foreach ($views as $view) - { - // The old scheme, if a view has a tmpl folder - $path = $folder . '/' . $view . '/tmpl'; - - // The new scheme, the views are directly in the component/tmpl folder - if (!is_dir($path) && substr($folder, -4) == 'tmpl') - { - $path = $folder . '/' . $view; - } - - // Check if the folder exists - if (!is_dir($path)) - { - continue; - } - - $result['components'][$component][] = $this->getOverridesFolder($view, Path::clean($folder . '/')); - } - } - } - - foreach (Folder::folders($pluginPath) as $pluginGroup) - { - foreach (Folder::folders($pluginPath . '/' . $pluginGroup) as $plugin) - { - if (file_exists($pluginPath . '/' . $pluginGroup . '/' . $plugin . '/tmpl/')) - { - $pluginLayoutPath = Path::clean($pluginPath . '/' . $pluginGroup . '/'); - $result['plugins'][$pluginGroup][] = $this->getOverridesFolder($plugin, $pluginLayoutPath); - } - } - } - - $modules = Folder::folders($modulePath); - - foreach ($modules as $module) - { - $result['modules'][] = $this->getOverridesFolder($module, $modulePath); - } - - $layoutFolders = Folder::folders($layoutPath); - - foreach ($layoutFolders as $layoutFolder) - { - $layoutFolderPath = Path::clean($layoutPath . '/' . $layoutFolder . '/'); - $layouts = Folder::folders($layoutFolderPath); - - foreach ($layouts as $layout) - { - $result['layouts'][$layoutFolder][] = $this->getOverridesFolder($layout, $layoutFolderPath); - } - } - - // Check for layouts in component folders - foreach ($components as $component) - { - if (file_exists($componentPath . '/' . $component . '/layouts/')) - { - $componentLayoutPath = Path::clean($componentPath . '/' . $component . '/layouts/'); - - if ($componentLayoutPath) - { - $layouts = Folder::folders($componentLayoutPath); - - foreach ($layouts as $layout) - { - $result['layouts'][$component][] = $this->getOverridesFolder($layout, $componentLayoutPath); - } - } - } - } - } - - if (!empty($result)) - { - return $result; - } - } - - /** - * Create overrides. - * - * @param string $override The override location. - * - * @return boolean true if override creation is successful, false otherwise - * - * @since 3.2 - */ - public function createOverride($override) - { - if ($template = $this->getTemplate()) - { - $app = Factory::getApplication(); - $explodeArray = explode(DIRECTORY_SEPARATOR, $override); - $name = end($explodeArray); - $client = ApplicationHelper::getClientInfo($template->client_id); - - if (stristr($name, 'mod_') != false) - { - $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/' . $name); - } - elseif (stristr($override, 'com_') != false) - { - $size = count($explodeArray); - - $url = Path::clean($explodeArray[$size - 3] . '/' . $explodeArray[$size - 1]); - - if ($explodeArray[$size - 2] == 'layouts') - { - $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/layouts/' . $url); - } - else - { - $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/' . $url); - } - } - elseif (stripos($override, Path::clean(JPATH_ROOT . '/plugins/')) === 0) - { - $size = count($explodeArray); - $layoutPath = Path::clean('plg_' . $explodeArray[$size - 2] . '_' . $explodeArray[$size - 1]); - $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/' . $layoutPath); - } - else - { - $layoutPath = implode('/', array_slice($explodeArray, -2)); - $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/layouts/' . $layoutPath); - } - - // Check Html folder, create if not exist - if (!Folder::exists($htmlPath)) - { - if (!Folder::create($htmlPath)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_ERROR'), 'error'); - - return false; - } - } - - if (stristr($name, 'mod_') != false) - { - $return = $this->createTemplateOverride(Path::clean($override . '/tmpl'), $htmlPath); - } - elseif (stristr($override, 'com_') != false && stristr($override, 'layouts') == false) - { - $path = $override . '/tmpl'; - - // View can also be in the top level folder - if (!is_dir($path)) - { - $path = $override; - } - - $return = $this->createTemplateOverride(Path::clean($path), $htmlPath); - } - elseif (stripos($override, Path::clean(JPATH_ROOT . '/plugins/')) === 0) - { - $return = $this->createTemplateOverride(Path::clean($override . '/tmpl'), $htmlPath); - } - else - { - $return = $this->createTemplateOverride($override, $htmlPath); - } - - if ($return) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_OVERRIDE_CREATED') . str_replace(JPATH_ROOT, '', $htmlPath)); - - return true; - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_OVERRIDE_FAILED'), 'error'); - - return false; - } - } - } - - /** - * Create override folder & file - * - * @param string $overridePath The override location - * @param string $htmlPath The html location - * - * @return boolean True on success. False otherwise. - */ - public function createTemplateOverride($overridePath, $htmlPath) - { - $return = false; - - if (empty($overridePath) || empty($htmlPath)) - { - return $return; - } - - // Get list of template folders - $folders = Folder::folders($overridePath, null, true, true); - - if (!empty($folders)) - { - foreach ($folders as $folder) - { - $htmlFolder = $htmlPath . str_replace($overridePath, '', $folder); - - if (!Folder::exists($htmlFolder)) - { - Folder::create($htmlFolder); - } - } - } - - // Get list of template files (Only get *.php file for template file) - $files = Folder::files($overridePath, '.php', true, true); - - if (empty($files)) - { - return true; - } - - foreach ($files as $file) - { - $overrideFilePath = str_replace($overridePath, '', $file); - $htmlFilePath = $htmlPath . $overrideFilePath; - - if (File::exists($htmlFilePath)) - { - // Generate new unique file name base on current time - $today = Factory::getDate(); - $htmlFilePath = File::stripExt($htmlFilePath) . '-' . $today->format('Ymd-His') . '.' . File::getExt($htmlFilePath); - } - - $return = File::copy($file, $htmlFilePath, '', true); - } - - return $return; - } - - /** - * Delete a particular file. - * - * @param string $file The relative location of the file. - * - * @return boolean True if file deletion is successful, false otherwise - * - * @since 3.2 - */ - public function deleteFile($file) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $filePath = $this->getBasePath() . urldecode(base64_decode($file)); - - $return = File::delete($filePath); - - if (!$return) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_DELETE_ERROR'), 'error'); - - return false; - } - - return true; - } - } - - /** - * Create new file. - * - * @param string $name The name of file. - * @param string $type The extension of the file. - * @param string $location Location for the new file. - * - * @return boolean true if file created successfully, false otherwise - * - * @since 3.2 - */ - public function createFile($name, $type, $location) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $base = $this->getBasePath(); - - if (file_exists(Path::clean($base . '/' . $location . '/' . $name . '.' . $type))) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); - - return false; - } - - if (!fopen(Path::clean($base . '/' . $location . '/' . $name . '.' . $type), 'x')) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_CREATE_ERROR'), 'error'); - - return false; - } - - // Check if the format is allowed and will be showed in the backend - $check = $this->checkFormat($type); - - // Add a message if we are not allowed to show this file in the backend. - if (!$check) - { - $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_WARNING_FORMAT_WILL_NOT_BE_VISIBLE', $type), 'warning'); - } - - return true; - } - } - - /** - * Upload new file. - * - * @param array $file The uploaded file array. - * @param string $location Location for the new file. - * - * @return boolean True if file uploaded successfully, false otherwise - * - * @since 3.2 - */ - public function uploadFile($file, $location) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $path = $this->getBasePath(); - $fileName = File::makeSafe($file['name']); - - $err = null; - - if (!TemplateHelper::canUpload($file, $err)) - { - // Can't upload the file - return false; - } - - if (file_exists(Path::clean($path . '/' . $location . '/' . $file['name']))) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); - - return false; - } - - if (!File::upload($file['tmp_name'], Path::clean($path . '/' . $location . '/' . $fileName))) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_UPLOAD_ERROR'), 'error'); - - return false; - } - - $url = Path::clean($location . '/' . $fileName); - - return $url; - } - } - - /** - * Create new folder. - * - * @param string $name The name of the new folder. - * @param string $location Location for the new folder. - * - * @return boolean True if override folder is created successfully, false otherwise - * - * @since 3.2 - */ - public function createFolder($name, $location) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $path = Path::clean($location . '/'); - $base = $this->getBasePath(); - - if (file_exists(Path::clean($base . $path . $name))) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_EXISTS'), 'error'); - - return false; - } - - if (!Folder::create(Path::clean($base . $path . $name))) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_CREATE_ERROR'), 'error'); - - return false; - } - - return true; - } - } - - /** - * Delete a folder. - * - * @param string $location The name and location of the folder. - * - * @return boolean True if override folder is deleted successfully, false otherwise - * - * @since 3.2 - */ - public function deleteFolder($location) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $base = $this->getBasePath(); - $path = Path::clean($location . '/'); - - if (!file_exists($base . $path)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_NOT_EXISTS'), 'error'); - - return false; - } - - $return = Folder::delete($base . $path); - - if (!$return) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_DELETE_ERROR'), 'error'); - - return false; - } - - return true; - } - } - - /** - * Rename a file. - * - * @param string $file The name and location of the old file - * @param string $name The new name of the file. - * - * @return string Encoded string containing the new file location. - * - * @since 3.2 - */ - public function renameFile($file, $name) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $path = $this->getBasePath(); - $fileName = base64_decode($file); - $explodeArray = explode('.', $fileName); - $type = end($explodeArray); - $explodeArray = explode('/', $fileName); - $newName = str_replace(end($explodeArray), $name . '.' . $type, $fileName); - - if (file_exists($path . $newName)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); - - return false; - } - - if (!rename($path . $fileName, $path . $newName)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_RENAME_ERROR'), 'error'); - - return false; - } - - return base64_encode($newName); - } - } - - /** - * Get an image address, height and width. - * - * @return array an associative array containing image address, height and width. - * - * @since 3.2 - */ - public function getImage() - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $fileName = base64_decode($app->input->get('file')); - $path = $this->getBasePath(); - - $uri = Uri::root(false) . ltrim(str_replace(JPATH_ROOT, '', $this->getBasePath()), '/'); - - if (file_exists(Path::clean($path . $fileName))) - { - $JImage = new Image(Path::clean($path . $fileName)); - $image['address'] = $uri . $fileName; - $image['path'] = $fileName; - $image['height'] = $JImage->getHeight(); - $image['width'] = $JImage->getWidth(); - } - - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_IMAGE_FILE_NOT_FOUND'), 'error'); - - return false; - } - - return $image; - } - } - - /** - * Crop an image. - * - * @param string $file The name and location of the file - * @param string $w width. - * @param string $h height. - * @param string $x x-coordinate. - * @param string $y y-coordinate. - * - * @return boolean true if image cropped successfully, false otherwise. - * - * @since 3.2 - */ - public function cropImage($file, $w, $h, $x, $y) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $path = $this->getBasePath() . base64_decode($file); - - try - { - $image = new Image($path); - $properties = $image->getImageFileProperties($path); - - switch ($properties->mime) - { - case 'image/webp': - $imageType = \IMAGETYPE_WEBP; - break; - case 'image/png': - $imageType = \IMAGETYPE_PNG; - break; - case 'image/gif': - $imageType = \IMAGETYPE_GIF; - break; - default: - $imageType = \IMAGETYPE_JPEG; - } - - $image->crop($w, $h, $x, $y, false); - $image->toFile($path, $imageType); - - return true; - } - catch (\Exception $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - } - } - } - - /** - * Resize an image. - * - * @param string $file The name and location of the file - * @param string $width The new width of the image. - * @param string $height The new height of the image. - * - * @return boolean true if image resize successful, false otherwise. - * - * @since 3.2 - */ - public function resizeImage($file, $width, $height) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $path = $this->getBasePath() . base64_decode($file); - - try - { - $image = new Image($path); - $properties = $image->getImageFileProperties($path); - - switch ($properties->mime) - { - case 'image/webp': - $imageType = \IMAGETYPE_WEBP; - break; - case 'image/png': - $imageType = \IMAGETYPE_PNG; - break; - case 'image/gif': - $imageType = \IMAGETYPE_GIF; - break; - default: - $imageType = \IMAGETYPE_JPEG; - } - - $image->resize($width, $height, false, Image::SCALE_FILL); - $image->toFile($path, $imageType); - - return true; - } - catch (\Exception $e) - { - $app->enqueueMessage($e->getMessage(), 'error'); - } - } - } - - /** - * Template preview. - * - * @return object object containing the id of the template. - * - * @since 3.2 - */ - public function getPreview() - { - $app = Factory::getApplication(); - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->select($db->quoteName(['id', 'client_id'])); - $query->from($db->quoteName('#__template_styles')); - $query->where($db->quoteName('template') . ' = :template') - ->bind(':template', $this->template->element); - - $db->setQuery($query); - - try - { - $result = $db->loadObject(); - } - catch (\RuntimeException $e) - { - $app->enqueueMessage($e->getMessage(), 'warning'); - } - - if (empty($result)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EXTENSION_RECORD_NOT_FOUND'), 'warning'); - } - else - { - return $result; - } - } - - /** - * Rename a file. - * - * @return mixed array on success, false on failure - * - * @since 3.2 - */ - public function getFont() - { - if ($template = $this->getTemplate()) - { - $app = Factory::getApplication(); - $client = ApplicationHelper::getClientInfo($template->client_id); - $relPath = base64_decode($app->input->get('file')); - $explodeArray = explode('/', $relPath); - $fileName = end($explodeArray); - $path = $this->getBasePath() . base64_decode($app->input->get('file')); - - if (stristr($client->path, 'administrator') == false) - { - $folder = '/templates/'; - } - else - { - $folder = '/administrator/templates/'; - } - - $uri = Uri::root(true) . $folder . $template->element; - - if (file_exists(Path::clean($path))) - { - $font['address'] = $uri . $relPath; - - $font['rel_path'] = $relPath; - - $font['name'] = $fileName; - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_FONT_FILE_NOT_FOUND'), 'error'); - - return false; - } - - return $font; - } - } - - /** - * Copy a file. - * - * @param string $newName The name of the copied file - * @param string $location The final location where the file is to be copied - * @param string $file The name and location of the file - * - * @return boolean true if image resize successful, false otherwise. - * - * @since 3.2 - */ - public function copyFile($newName, $location, $file) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $relPath = base64_decode($file); - $explodeArray = explode('.', $relPath); - $ext = end($explodeArray); - $path = $this->getBasePath(); - $newPath = Path::clean($path . $location . '/' . $newName . '.' . $ext); - - if (file_exists($newPath)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); - - return false; - } - - if (File::copy($path . $relPath, $newPath)) - { - $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_FILE_COPY_SUCCESS', $newName . '.' . $ext)); - - return true; - } - else - { - return false; - } - } - } - - /** - * Get the compressed files. - * - * @return array if file exists, false otherwise - * - * @since 3.2 - */ - public function getArchive() - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $path = $this->getBasePath() . base64_decode($app->input->get('file')); - - if (file_exists(Path::clean($path))) - { - $files = array(); - $zip = new \ZipArchive; - - if ($zip->open($path) === true) - { - for ($i = 0; $i < $zip->numFiles; $i++) - { - $entry = $zip->getNameIndex($i); - $files[] = $entry; - } - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_OPEN_FAIL'), 'error'); - - return false; - } - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_FONT_FILE_NOT_FOUND'), 'error'); - - return false; - } - - return $files; - } - } - - /** - * Extract contents of an archive file. - * - * @param string $file The name and location of the file - * - * @return boolean true if image extraction is successful, false otherwise. - * - * @since 3.2 - */ - public function extractArchive($file) - { - if ($this->getTemplate()) - { - $app = Factory::getApplication(); - $relPath = base64_decode($file); - $explodeArray = explode('/', $relPath); - $fileName = end($explodeArray); - $path = $this->getBasePath() . base64_decode($file); - - if (file_exists(Path::clean($path . '/' . $fileName))) - { - $zip = new \ZipArchive; - - if ($zip->open(Path::clean($path . '/' . $fileName)) === true) - { - for ($i = 0; $i < $zip->numFiles; $i++) - { - $entry = $zip->getNameIndex($i); - - if (file_exists(Path::clean($path . '/' . $entry))) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_EXISTS'), 'error'); - - return false; - } - } - - $zip->extractTo($path); - - return true; - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_OPEN_FAIL'), 'error'); - - return false; - } - } - else - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_NOT_FOUND'), 'error'); - - return false; - } - } - } - - /** - * Check if the extension is allowed and will be shown in the template manager - * - * @param string $ext The extension to check if it is allowed - * - * @return boolean true if the extension is allowed false otherwise - * - * @since 3.6.0 - */ - protected function checkFormat($ext) - { - if (!isset($this->allowedFormats)) - { - $params = ComponentHelper::getParams('com_templates'); - $imageTypes = explode(',', $params->get('image_formats')); - $sourceTypes = explode(',', $params->get('source_formats')); - $fontTypes = explode(',', $params->get('font_formats')); - $archiveTypes = explode(',', $params->get('compressed_formats')); - - $this->allowedFormats = array_merge($imageTypes, $sourceTypes, $fontTypes, $archiveTypes); - $this->allowedFormats = array_map('strtolower', $this->allowedFormats); - } - - return in_array(strtolower($ext), $this->allowedFormats); - } - - /** - * Method to get a list of all the files to edit in a template's media folder. - * - * @return array A nested array of relevant files. - * - * @since 4.1.0 - */ - public function getMediaFiles() - { - $result = []; - $template = $this->getTemplate(); - - if (!isset($template->xmldata)) - { - $template->xmldata = TemplatesHelper::parseXMLTemplateFile($template->client_id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $template->name); - } - - if (!isset($template->xmldata->inheritable) || (isset($template->xmldata->parent) && $template->xmldata->parent === '')) - { - return $result; - } - - $app = Factory::getApplication(); - $path = Path::clean(JPATH_ROOT . '/media/templates/' . ($template->client_id === 0 ? 'site' : 'administrator') . '/' . $template->element . '/'); - $this->mediaElement = $path; - - if (!is_writable($path)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_DIRECTORY_NOT_WRITABLE'), 'error'); - } - - if (is_dir($path)) - { - $result = $this->getDirectoryTree($path); - } - - return $result; - } - - /** - * Method to resolve the base folder. - * - * @return string The absolute path for the base. - * - * @since 4.1.0 - */ - private function getBasePath() - { - $app = Factory::getApplication(); - $isMedia = $app->input->getInt('isMedia', 0); - - return $isMedia ? JPATH_ROOT . '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element : - JPATH_ROOT . '/' . ($this->template->client_id === 0 ? '' : 'administrator/') . 'templates/' . $this->template->element; - } - - /** - * Method to create the templateDetails.xml for the child template - * - * @return boolean true if name is not used, false otherwise - * - * @since 4.1.0 - */ - public function child() - { - $app = Factory::getApplication(); - $template = $this->getTemplate(); - - if (!(array) $template) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); - - return false; - } - - $client = ApplicationHelper::getClientInfo($template->client_id); - $fromPath = Path::clean($client->path . '/templates/' . $template->element . '/templateDetails.xml'); - - // Delete new folder if it exists - $toPath = $this->getState('to_path'); - - if (Folder::exists($toPath)) - { - if (!Folder::delete($toPath)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); - - return false; - } - } - else - { - if (!Folder::create($toPath)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); - - return false; - } - } - - // Copy the template definition from the parent template - if (!File::copy($fromPath, $toPath . '/templateDetails.xml')) - { - return false; - } - - // Check manifest for additional files - $newName = strtolower($this->getState('new_name')); - $template = $this->getTemplate(); - - // Edit XML file - $xmlFile = Path::clean($this->getState('to_path') . '/templateDetails.xml'); - - if (!File::exists($xmlFile)) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); - - return false; - } - - try - { - $xml = simplexml_load_string(file_get_contents($xmlFile)); - } - catch (\Exception $e) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_READ'), 'error'); - - return false; - } - - $user = Factory::getUser(); - unset($xml->languages); - unset($xml->media); - unset($xml->files); - unset($xml->parent); - unset($xml->inheritable); - - // Remove the update parts - unset($xml->update); - unset($xml->updateservers); - - if (isset($xml->creationDate)) - { - $xml->creationDate = (new Date('now'))->format('F Y'); - } - else - { - $xml->addChild('creationDate', (new Date('now'))->format('F Y')); - } - - if (isset($xml->author)) - { - $xml->author = $user->name; - } - else - { - $xml->addChild('author', $user->name); - } - - if (isset($xml->authorEmail)) - { - $xml->authorEmail = $user->email; - } - else - { - $xml->addChild('authorEmail', $user->email); - } - - $files = $xml->addChild('files'); - $files->addChild('filename', 'templateDetails.xml'); - - // Media folder - $media = $xml->addChild('media'); - $media->addAttribute('folder', 'media'); - $media->addAttribute('destination', 'templates/' . ($template->client_id === 0 ? 'site/' : 'administrator/') . $template->element . '_' . $newName); - $media->addChild('folder', 'css'); - $media->addChild('folder', 'js'); - $media->addChild('folder', 'images'); - $media->addChild('folder', 'html'); - $media->addChild('folder', 'scss'); - - $xml->name = $template->element . '_' . $newName; - $xml->inheritable = 0; - $files = $xml->addChild('parent', $template->element); - - $dom = new \DOMDocument; - $dom->preserveWhiteSpace = false; - $dom->formatOutput = true; - $dom->loadXML($xml->asXML()); - - $result = File::write($xmlFile, $dom->saveXML()); - - if (!$result) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); - - return false; - } - - // Create an empty media folder structure - if (!Folder::create($toPath . '/media') - || !Folder::create($toPath . '/media/css') - || !Folder::create($toPath . '/media/js') - || !Folder::create($toPath . '/media/images') - || !Folder::create($toPath . '/media/html/tinymce') - || !Folder::create($toPath . '/media/scss')) - { - return false; - } - - return true; - } - - /** - * Method to get the parent template existing styles - * - * @return array array of id,titles of the styles - * - * @since 4.1.3 - */ - public function getAllTemplateStyles() - { - $template = $this->getTemplate(); - - if (empty($template->xmldata->inheritable)) - { - return []; - } - - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->select($db->quoteName(['id', 'title'])) - ->from($db->quoteName('#__template_styles')) - ->where($db->quoteName('client_id') . ' = :client_id', 'AND') - ->where($db->quoteName('template') . ' = :template') - ->orWhere($db->quoteName('parent') . ' = :parent') - ->bind(':client_id', $template->client_id, ParameterType::INTEGER) - ->bind(':template', $template->element) - ->bind(':parent', $template->element); - - $db->setQuery($query); - - return $db->loadObjectList(); - } - - /** - * Method to copy selected styles to the child template - * - * @return boolean true if name is not used, false otherwise - * - * @since 4.1.3 - */ - public function copyStyles() - { - $app = Factory::getApplication(); - $template = $this->getTemplate(); - $newName = strtolower($this->getState('new_name')); - $applyStyles = $this->getState('stylesToCopy'); - - // Get a db connection. - $db = $this->getDatabase(); - - // Create a new query object. - $query = $db->getQuery(true); - - $query->select($db->quoteName(['title', 'params'])) - ->from($db->quoteName('#__template_styles')) - ->whereIn($db->quoteName('id'), ArrayHelper::toInteger($applyStyles)); - // Reset the query using our newly populated query object. - $db->setQuery($query); - - try - { - $parentStyle = $db->loadObjectList(); - } - catch (\Exception $e) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_STYLE_NOT_FOUND'), 'error'); - - return false; - } - - foreach ($parentStyle as $style) - { - $query = $db->getQuery(true); - $styleName = Text::sprintf('COM_TEMPLATES_COPY_CHILD_TEMPLATE_STYLES', ucfirst($template->element . '_' . $newName), $style->title); - - // Insert columns and values - $columns = ['id', 'template', 'client_id', 'home', 'title', 'inheritable', 'parent', 'params']; - $values = [0, $db->quote($template->element . '_' . $newName), (int) $template->client_id, $db->quote('0'), $db->quote($styleName), 0, $db->quote($template->element), $db->quote($style->params)]; - - $query - ->insert($db->quoteName('#__template_styles')) - ->columns($db->quoteName($columns)) - ->values(implode(',', $values)); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\Exception $e) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_READ'), 'error'); - - return false; - } - } - - return true; - } + /** + * The information in a template + * + * @var \stdClass + * @since 1.6 + */ + protected $template = null; + + /** + * The path to the template + * + * @var string + * @since 3.2 + */ + protected $element = null; + + /** + * The path to the static assets + * + * @var string + * @since 4.1.0 + */ + protected $mediaElement = null; + + /** + * Internal method to get file properties. + * + * @param string $path The base path. + * @param string $name The file name. + * + * @return object + * + * @since 1.6 + */ + protected function getFile($path, $name) + { + $temp = new \stdClass(); + + if ($this->getTemplate()) { + $path = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? 'site' : 'administrator') . DIRECTORY_SEPARATOR . $this->template->element, '', $path); + $path = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? '' : 'administrator' . DIRECTORY_SEPARATOR) . 'templates' . DIRECTORY_SEPARATOR . $this->template->element, '', $path); + $temp->name = $name; + $temp->id = urlencode(base64_encode(str_replace('\\', '//', $path))); + + return $temp; + } + } + + /** + * Method to store file information. + * + * @param string $path The base path. + * @param string $name The file name. + * @param stdClass $template The std class object of template. + * + * @return object stdClass object. + * + * @since 4.0.0 + */ + protected function storeFileInfo($path, $name, $template) + { + $temp = new \stdClass(); + $temp->id = base64_encode($path . $name); + $temp->client = $template->client_id; + $temp->template = $template->element; + $temp->extension_id = $template->extension_id; + + if ($coreFile = $this->getCoreFile($path . $name, $template->client_id)) { + $temp->coreFile = md5_file($coreFile); + } else { + $temp->coreFile = null; + } + + return $temp; + } + + /** + * Method to get all template list. + * + * @return object stdClass object + * + * @since 4.0.0 + */ + public function getTemplateList() + { + // Get a db connection. + $db = $this->getDatabase(); + + // Create a new query object. + $query = $db->getQuery(true); + + // Select the required fields from the table + $query->select( + $this->getState( + 'list.select', + 'a.extension_id, a.name, a.element, a.client_id' + ) + ); + + $query->from($db->quoteName('#__extensions', 'a')) + ->where($db->quoteName('a.enabled') . ' = 1') + ->where($db->quoteName('a.type') . ' = ' . $db->quote('template')); + + // Reset the query. + $db->setQuery($query); + + // Load the results as a list of stdClass objects. + $results = $db->loadObjectList(); + + return $results; + } + + /** + * Method to get all updated file list. + * + * @param boolean $state The optional parameter if you want unchecked list. + * @param boolean $all The optional parameter if you want all list. + * @param boolean $cleanup The optional parameter if you want to clean record which is no more required. + * + * @return object stdClass object + * + * @since 4.0.0 + */ + public function getUpdatedList($state = false, $all = false, $cleanup = false) + { + // Get a db connection. + $db = $this->getDatabase(); + + // Create a new query object. + $query = $db->getQuery(true); + + // Select the required fields from the table + $query->select( + $this->getState( + 'list.select', + 'a.template, a.hash_id, a.extension_id, a.state, a.action, a.client_id, a.created_date, a.modified_date' + ) + ); + + $template = $this->getTemplate(); + + $query->from($db->quoteName('#__template_overrides', 'a')); + + if (!$all) { + $teid = (int) $template->extension_id; + $query->where($db->quoteName('extension_id') . ' = :teid') + ->bind(':teid', $teid, ParameterType::INTEGER); + } + + if ($state) { + $query->where($db->quoteName('state') . ' = 0'); + } + + $query->order($db->quoteName('a.modified_date') . ' DESC'); + + // Reset the query. + $db->setQuery($query); + + // Load the results as a list of stdClass objects. + $pks = $db->loadObjectList(); + + if ($state) { + return $pks; + } + + $results = array(); + + foreach ($pks as $pk) { + $client = ApplicationHelper::getClientInfo($pk->client_id); + $path = Path::clean($client->path . '/templates/' . $pk->template . base64_decode($pk->hash_id)); + + if (file_exists($path)) { + $results[] = $pk; + } elseif ($cleanup) { + $cleanupIds = array(); + $cleanupIds[] = $pk->hash_id; + $this->publish($cleanupIds, -3, $pk->extension_id); + } + } + + return $results; + } + + /** + * Method to get a list of all the core files of override files. + * + * @return array An array of all core files. + * + * @since 4.0.0 + */ + public function getCoreList() + { + // Get list of all templates + $templates = $this->getTemplateList(); + + // Initialize the array variable to store core file list. + $this->coreFileList = array(); + + $app = Factory::getApplication(); + + foreach ($templates as $template) { + $client = ApplicationHelper::getClientInfo($template->client_id); + $element = Path::clean($client->path . '/templates/' . $template->element . '/'); + $path = Path::clean($element . 'html/'); + + if (is_dir($path)) { + $this->prepareCoreFiles($path, $element, $template); + } + } + + // Sort list of stdClass array. + usort( + $this->coreFileList, + function ($a, $b) { + return strcmp($a->id, $b->id); + } + ); + + return $this->coreFileList; + } + + /** + * Prepare core files. + * + * @param string $dir The path of the directory to scan. + * @param string $element The path of the template element. + * @param \stdClass $template The stdClass object of template. + * + * @return array + * + * @since 4.0.0 + */ + public function prepareCoreFiles($dir, $element, $template) + { + $dirFiles = scandir($dir); + + foreach ($dirFiles as $key => $value) { + if (in_array($value, array('.', '..', 'node_modules'))) { + continue; + } + + if (is_dir($dir . $value)) { + $relativePath = str_replace($element, '', $dir . $value); + $this->prepareCoreFiles($dir . $value . '/', $element, $template); + } else { + $ext = pathinfo($dir . $value, PATHINFO_EXTENSION); + $allowedFormat = $this->checkFormat($ext); + + if ($allowedFormat === true) { + $relativePath = str_replace($element, '', $dir); + $info = $this->storeFileInfo('/' . $relativePath, $value, $template); + + if ($info) { + $this->coreFileList[] = $info; + } + } + } + } + } + + /** + * Method to update status of list. + * + * @param array $ids The base path. + * @param array $value The file name. + * @param integer $exid The template extension id. + * + * @return integer Number of files changed. + * + * @since 4.0.0 + */ + public function publish($ids, $value, $exid) + { + $db = $this->getDatabase(); + + foreach ($ids as $id) { + if ($value === -3) { + $deleteQuery = $db->getQuery(true) + ->delete($db->quoteName('#__template_overrides')) + ->where($db->quoteName('hash_id') . ' = :hashid') + ->where($db->quoteName('extension_id') . ' = :exid') + ->bind(':hashid', $id) + ->bind(':exid', $exid, ParameterType::INTEGER); + + try { + // Set the query using our newly populated query object and execute it. + $db->setQuery($deleteQuery); + $result = $db->execute(); + } catch (\RuntimeException $e) { + return $e; + } + } elseif ($value === 1 || $value === 0) { + $updateQuery = $db->getQuery(true) + ->update($db->quoteName('#__template_overrides')) + ->set($db->quoteName('state') . ' = :state') + ->where($db->quoteName('hash_id') . ' = :hashid') + ->where($db->quoteName('extension_id') . ' = :exid') + ->bind(':state', $value, ParameterType::INTEGER) + ->bind(':hashid', $id) + ->bind(':exid', $exid, ParameterType::INTEGER); + + try { + // Set the query using our newly populated query object and execute it. + $db->setQuery($updateQuery); + $result = $db->execute(); + } catch (\RuntimeException $e) { + return $e; + } + } + } + + return $result; + } + + /** + * Method to get a list of all the files to edit in a template. + * + * @return array A nested array of relevant files. + * + * @since 1.6 + */ + public function getFiles() + { + $result = array(); + + if ($template = $this->getTemplate()) { + $app = Factory::getApplication(); + $client = ApplicationHelper::getClientInfo($template->client_id); + $path = Path::clean($client->path . '/templates/' . $template->element . '/'); + $lang = Factory::getLanguage(); + + // Load the core and/or local language file(s). + $lang->load('tpl_' . $template->element, $client->path) + || (!empty($template->xmldata->parent) && $lang->load('tpl_' . $template->xmldata->parent, $client->path)) + || $lang->load('tpl_' . $template->element, $client->path . '/templates/' . $template->element) + || (!empty($template->xmldata->parent) && $lang->load('tpl_' . $template->xmldata->parent, $client->path . '/templates/' . $template->xmldata->parent)); + $this->element = $path; + + if (!is_writable($path)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_DIRECTORY_NOT_WRITABLE'), 'error'); + } + + if (is_dir($path)) { + $result = $this->getDirectoryTree($path); + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_TEMPLATE_FOLDER_NOT_FOUND'), 'error'); + + return false; + } + + // Clean up override history + $this->getUpdatedList(false, true, true); + } + + return $result; + } + + /** + * Get the directory tree. + * + * @param string $dir The path of the directory to scan + * + * @return array + * + * @since 3.2 + */ + public function getDirectoryTree($dir) + { + $result = array(); + + $dirFiles = scandir($dir); + + foreach ($dirFiles as $key => $value) { + if (!in_array($value, array('.', '..', 'node_modules'))) { + if (is_dir($dir . $value)) { + $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? 'site' : 'administrator') . DIRECTORY_SEPARATOR . $this->template->element, '', $dir . $value); + $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? '' : 'administrator' . DIRECTORY_SEPARATOR) . 'templates' . DIRECTORY_SEPARATOR . $this->template->element, '', $relativePath); + $result[str_replace('\\', '//', $relativePath)] = $this->getDirectoryTree($dir . $value . '/'); + } else { + $ext = pathinfo($dir . $value, PATHINFO_EXTENSION); + $allowedFormat = $this->checkFormat($ext); + + if ($allowedFormat == true) { + $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . 'media' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? 'site' : 'administrator') . DIRECTORY_SEPARATOR . $this->template->element, '', $dir . $value); + $relativePath = str_replace(JPATH_ROOT . DIRECTORY_SEPARATOR . ($this->template->client_id === 0 ? '' : 'administrator' . DIRECTORY_SEPARATOR) . 'templates' . DIRECTORY_SEPARATOR . $this->template->element, '', $relativePath); + $result[] = $this->getFile($relativePath, $value); + } + } + } + } + + return $result; + } + + /** + * Method to get the core file of override file + * + * @param string $file Override file + * @param integer $client_id Client Id + * + * @return string $corefile The full path and file name for the target file, or boolean false if the file is not found in any of the paths. + * + * @since 4.0.0 + */ + public function getCoreFile($file, $client_id) + { + $app = Factory::getApplication(); + $filePath = Path::clean($file); + $explodeArray = explode(DIRECTORY_SEPARATOR, $filePath); + + // Only allow html/ folder + if ($explodeArray['1'] !== 'html') { + return false; + } + + $fileName = basename($filePath); + $type = $explodeArray['2']; + $client = ApplicationHelper::getClientInfo($client_id); + + $componentPath = Path::clean($client->path . '/components/'); + $modulePath = Path::clean($client->path . '/modules/'); + $layoutPath = Path::clean(JPATH_ROOT . '/layouts/'); + + // For modules + if (stristr($type, 'mod_') !== false) { + $folder = $explodeArray['2']; + $htmlPath = Path::clean($modulePath . $folder . '/tmpl/'); + $fileName = $this->getSafeName($fileName); + $coreFile = Path::find($htmlPath, $fileName); + + return $coreFile; + } elseif (stristr($type, 'com_') !== false) { + // For components + $folder = $explodeArray['2']; + $subFolder = $explodeArray['3']; + $fileName = $this->getSafeName($fileName); + + // The new scheme, if a view has a tmpl folder + $newHtmlPath = Path::clean($componentPath . $folder . '/tmpl/' . $subFolder . '/'); + + if (!$coreFile = Path::find($newHtmlPath, $fileName)) { + // The old scheme, the views are directly in the component/tmpl folder + $oldHtmlPath = Path::clean($componentPath . $folder . '/views/' . $subFolder . '/tmpl/'); + $coreFile = Path::find($oldHtmlPath, $fileName); + + return $coreFile; + } + + return $coreFile; + } elseif (stristr($type, 'layouts') !== false) { + // For Layouts + $subtype = $explodeArray['3']; + + if (stristr($subtype, 'com_')) { + $folder = $explodeArray['3']; + $subFolder = array_slice($explodeArray, 4, -1); + $subFolder = implode(DIRECTORY_SEPARATOR, $subFolder); + $htmlPath = Path::clean($componentPath . $folder . '/layouts/' . $subFolder); + $fileName = $this->getSafeName($fileName); + $coreFile = Path::find($htmlPath, $fileName); + + return $coreFile; + } elseif (stristr($subtype, 'joomla') || stristr($subtype, 'libraries') || stristr($subtype, 'plugins')) { + $subFolder = array_slice($explodeArray, 3, -1); + $subFolder = implode(DIRECTORY_SEPARATOR, $subFolder); + $htmlPath = Path::clean($layoutPath . $subFolder); + $fileName = $this->getSafeName($fileName); + $coreFile = Path::find($htmlPath, $fileName); + + return $coreFile; + } + } + + return false; + } + + /** + * Creates a safe file name for the given name. + * + * @param string $name The filename + * + * @return string $fileName The filtered name without Date + * + * @since 4.0.0 + */ + private function getSafeName($name) + { + if (strpos($name, '-') !== false && preg_match('/[0-9]/', $name)) { + // Get the extension + $extension = File::getExt($name); + + // Remove ( Date ) from file + $explodeArray = explode('-', $name); + $size = count($explodeArray); + $date = $explodeArray[$size - 2] . '-' . str_replace('.' . $extension, '', $explodeArray[$size - 1]); + + if ($this->validateDate($date)) { + $nameWithoutExtension = implode('-', array_slice($explodeArray, 0, -2)); + + // Filtered name + $name = $nameWithoutExtension . '.' . $extension; + } + } + + return $name; + } + + /** + * Validate Date in file name. + * + * @param string $date Date to validate. + * + * @return boolean Return true if date is valid and false if not. + * + * @since 4.0.0 + */ + private function validateDate($date) + { + $format = 'Ymd-His'; + $valid = Date::createFromFormat($format, $date); + + return $valid && $valid->format($format) === $date; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the User state. + $pk = $app->input->getInt('id'); + $this->setState('extension.id', $pk); + + // Load the parameters. + $params = ComponentHelper::getParams('com_templates'); + $this->setState('params', $params); + } + + /** + * Method to get the template information. + * + * @return mixed Object if successful, false if not and internal error is set. + * + * @since 1.6 + */ + public function &getTemplate() + { + if (empty($this->template)) { + $pk = (int) $this->getState('extension.id'); + $db = $this->getDatabase(); + $app = Factory::getApplication(); + + // Get the template information. + $query = $db->getQuery(true) + ->select($db->quoteName(['extension_id', 'client_id', 'element', 'name', 'manifest_cache'])) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('extension_id') . ' = :pk') + ->where($db->quoteName('type') . ' = ' . $db->quote('template')) + ->bind(':pk', $pk, ParameterType::INTEGER); + $db->setQuery($query); + + try { + $result = $db->loadObject(); + } catch (\RuntimeException $e) { + $app->enqueueMessage($e->getMessage(), 'warning'); + $this->template = false; + + return false; + } + + if (empty($result)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EXTENSION_RECORD_NOT_FOUND'), 'error'); + $this->template = false; + } else { + $this->template = $result; + + // Client ID is not always an integer, so enforce here + $this->template->client_id = (int) $this->template->client_id; + + if (!isset($this->template->xmldata)) { + $this->template->xmldata = TemplatesHelper::parseXMLTemplateFile($this->template->client_id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $this->template->name); + } + } + } + + return $this->template; + } + + /** + * Method to check if new template name already exists + * + * @return boolean true if name is not used, false otherwise + * + * @since 2.5 + */ + public function checkNewName() + { + $db = $this->getDatabase(); + $name = $this->getState('new_name'); + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('name') . ' = :name') + ->bind(':name', $name); + $db->setQuery($query); + + return ($db->loadResult() == 0); + } + + /** + * Method to check if new template name already exists + * + * @return string name of current template + * + * @since 2.5 + */ + public function getFromName() + { + return $this->getTemplate()->element; + } + + /** + * Method to check if new template name already exists + * + * @return boolean true if name is not used, false otherwise + * + * @since 2.5 + */ + public function copy() + { + $app = Factory::getApplication(); + + if ($template = $this->getTemplate()) { + $client = ApplicationHelper::getClientInfo($template->client_id); + $fromPath = Path::clean($client->path . '/templates/' . $template->element . '/'); + + // Delete new folder if it exists + $toPath = $this->getState('to_path'); + + if (Folder::exists($toPath)) { + if (!Folder::delete($toPath)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); + + return false; + } + } + + // Copy all files from $fromName template to $newName folder + if (!Folder::copy($fromPath, $toPath)) { + return false; + } + + // Check manifest for additional files + $manifest = simplexml_load_file($toPath . '/templateDetails.xml'); + + // Copy language files from global folder + if ($languages = $manifest->languages) { + $folder = (string) $languages->attributes()->folder; + $languageFiles = $languages->language; + + Folder::create($toPath . '/' . $folder . '/' . $languageFiles->attributes()->tag); + + foreach ($languageFiles as $languageFile) { + $src = Path::clean($client->path . '/language/' . $languageFile); + $dst = Path::clean($toPath . '/' . $folder . '/' . $languageFile); + + if (File::exists($src)) { + File::copy($src, $dst); + } + } + } + + // Copy media files + if ($media = $manifest->media) { + $folder = (string) $media->attributes()->folder; + $destination = (string) $media->attributes()->destination; + + Folder::copy(JPATH_SITE . '/media/' . $destination, $toPath . '/' . $folder); + } + + // Adjust to new template name + if (!$this->fixTemplateName()) { + return false; + } + + return true; + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); + + return false; + } + } + + /** + * Method to delete tmp folder + * + * @return boolean true if delete successful, false otherwise + * + * @since 2.5 + */ + public function cleanup() + { + // Clear installation messages + $app = Factory::getApplication(); + $app->setUserState('com_installer.message', ''); + $app->setUserState('com_installer.extension_message', ''); + + // Delete temporary directory + return Folder::delete($this->getState('to_path')); + } + + /** + * Method to rename the template in the XML files and rename the language files + * + * @return boolean true if successful, false otherwise + * + * @since 2.5 + */ + protected function fixTemplateName() + { + // Rename Language files + // Get list of language files + $result = true; + $files = Folder::files($this->getState('to_path'), '\.ini$', true, true); + $newName = strtolower($this->getState('new_name')); + $template = $this->getTemplate(); + $oldName = $template->element; + $manifest = json_decode($template->manifest_cache); + + foreach ($files as $file) { + $newFile = '/' . str_replace($oldName, $newName, basename($file)); + $result = File::move($file, dirname($file) . $newFile) && $result; + } + + // Edit XML file + $xmlFile = $this->getState('to_path') . '/templateDetails.xml'; + + if (File::exists($xmlFile)) { + $contents = file_get_contents($xmlFile); + $pattern[] = '#\s*' . $manifest->name . '\s*#i'; + $replace[] = '' . $newName . ''; + $pattern[] = '##'; + $replace[] = ''; + $pattern[] = '##'; + $replace[] = ''; + $contents = preg_replace($pattern, $replace, $contents); + $result = File::write($xmlFile, $contents) && $result; + } + + return $result; + } + + /** + * Method to get the record form. + * + * @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 Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + $app = Factory::getApplication(); + + // Codemirror or Editor None should be enabled + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from('#__extensions as a') + ->where( + '(a.name =' . $db->quote('plg_editors_codemirror') . + ' AND a.enabled = 1) OR (a.name =' . + $db->quote('plg_editors_none') . + ' AND a.enabled = 1)' + ); + $db->setQuery($query); + $state = $db->loadResult(); + + if ((int) $state < 1) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EDITOR_DISABLED'), 'warning'); + } + + // Get the form. + $form = $this->loadForm('com_templates.source', 'source', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + $data = $this->getSource(); + + $this->preprocessData('com_templates.source', $data); + + return $data; + } + + /** + * Method to get a single record. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function &getSource() + { + $app = Factory::getApplication(); + $item = new \stdClass(); + + if (!$this->template) { + $this->getTemplate(); + } + + if ($this->template) { + $input = Factory::getApplication()->input; + $fileName = base64_decode($input->get('file')); + $fileName = str_replace('//', '/', $fileName); + $isMedia = $input->getInt('isMedia', 0); + + $fileName = $isMedia ? Path::clean(JPATH_ROOT . '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element . $fileName) + : Path::clean(JPATH_ROOT . ($this->template->client_id === 0 ? '' : '/administrator') . '/templates/' . $this->template->element . $fileName); + + try { + $filePath = Path::check($fileName); + } catch (\Exception $e) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_FILE_NOT_FOUND'), 'error'); + + return; + } + + if (file_exists($filePath)) { + $item->extension_id = $this->getState('extension.id'); + $item->filename = Path::clean($fileName); + $item->source = file_get_contents($filePath); + $item->filePath = Path::clean($filePath); + $ds = DIRECTORY_SEPARATOR; + $cleanFileName = str_replace(JPATH_ROOT . ($this->template->client_id === 1 ? $ds . 'administrator' . $ds : $ds) . 'templates' . $ds . $this->template->element, '', $fileName); + + if ($coreFile = $this->getCoreFile($cleanFileName, $this->template->client_id)) { + $item->coreFile = $coreFile; + $item->core = file_get_contents($coreFile); + } + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_FILE_NOT_FOUND'), 'error'); + } + } + + return $item; + } + + /** + * Method to store the source file contents. + * + * @param array $data The source data to save. + * + * @return boolean True on success, false otherwise and internal error set. + * + * @since 1.6 + */ + public function save($data) + { + // Get the template. + $template = $this->getTemplate(); + + if (empty($template)) { + return false; + } + + $app = Factory::getApplication(); + $fileName = base64_decode($app->input->get('file')); + $isMedia = $app->input->getInt('isMedia', 0); + $fileName = $isMedia ? JPATH_ROOT . '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element . $fileName : + JPATH_ROOT . '/' . ($this->template->client_id === 0 ? '' : 'administrator/') . 'templates/' . $this->template->element . $fileName; + + $filePath = Path::clean($fileName); + + // Include the extension plugins for the save events. + PluginHelper::importPlugin('extension'); + + $user = get_current_user(); + chown($filePath, $user); + Path::setPermissions($filePath, '0644'); + + // Try to make the template file writable. + if (!is_writable($filePath)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_SOURCE_FILE_NOT_WRITABLE'), 'warning'); + $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_FILE_PERMISSIONS', Path::getPermissions($filePath)), 'warning'); + + if (!Path::isOwner($filePath)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_CHECK_FILE_OWNERSHIP'), 'warning'); + } + + return false; + } + + // Make sure EOL is Unix + $data['source'] = str_replace(array("\r\n", "\r"), "\n", $data['source']); + + // If the asset file for the template ensure we have valid template so we don't instantly destroy it + if ($fileName === '/joomla.asset.json' && json_decode($data['source']) === null) { + $this->setError(Text::_('COM_TEMPLATES_ERROR_ASSET_FILE_INVALID_JSON')); + + return false; + } + + $return = File::write($filePath, $data['source']); + + if (!$return) { + $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_ERROR_FAILED_TO_SAVE_FILENAME', $fileName), 'error'); + + return false; + } + + // Get the extension of the changed file. + $explodeArray = explode('.', $fileName); + $ext = end($explodeArray); + + if ($ext == 'less') { + $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_COMPILE_LESS', $fileName)); + } + + return true; + } + + /** + * Get overrides folder. + * + * @param string $name The name of override. + * @param string $path Location of override. + * + * @return object containing override name and path. + * + * @since 3.2 + */ + public function getOverridesFolder($name, $path) + { + $folder = new \stdClass(); + $folder->name = $name; + $folder->path = base64_encode($path . $name); + + return $folder; + } + + /** + * Get a list of overrides. + * + * @return array containing overrides. + * + * @since 3.2 + */ + public function getOverridesList() + { + if ($template = $this->getTemplate()) { + $client = ApplicationHelper::getClientInfo($template->client_id); + $componentPath = Path::clean($client->path . '/components/'); + $modulePath = Path::clean($client->path . '/modules/'); + $pluginPath = Path::clean(JPATH_ROOT . '/plugins/'); + $layoutPath = Path::clean(JPATH_ROOT . '/layouts/'); + $components = Folder::folders($componentPath); + + foreach ($components as $component) { + // Collect the folders with views + $folders = Folder::folders($componentPath . '/' . $component, '^view[s]?$', false, true); + $folders = array_merge($folders, Folder::folders($componentPath . '/' . $component, '^tmpl?$', false, true)); + + if (!$folders) { + continue; + } + + foreach ($folders as $folder) { + // The subfolders are views + $views = Folder::folders($folder); + + foreach ($views as $view) { + // The old scheme, if a view has a tmpl folder + $path = $folder . '/' . $view . '/tmpl'; + + // The new scheme, the views are directly in the component/tmpl folder + if (!is_dir($path) && substr($folder, -4) == 'tmpl') { + $path = $folder . '/' . $view; + } + + // Check if the folder exists + if (!is_dir($path)) { + continue; + } + + $result['components'][$component][] = $this->getOverridesFolder($view, Path::clean($folder . '/')); + } + } + } + + foreach (Folder::folders($pluginPath) as $pluginGroup) { + foreach (Folder::folders($pluginPath . '/' . $pluginGroup) as $plugin) { + if (file_exists($pluginPath . '/' . $pluginGroup . '/' . $plugin . '/tmpl/')) { + $pluginLayoutPath = Path::clean($pluginPath . '/' . $pluginGroup . '/'); + $result['plugins'][$pluginGroup][] = $this->getOverridesFolder($plugin, $pluginLayoutPath); + } + } + } + + $modules = Folder::folders($modulePath); + + foreach ($modules as $module) { + $result['modules'][] = $this->getOverridesFolder($module, $modulePath); + } + + $layoutFolders = Folder::folders($layoutPath); + + foreach ($layoutFolders as $layoutFolder) { + $layoutFolderPath = Path::clean($layoutPath . '/' . $layoutFolder . '/'); + $layouts = Folder::folders($layoutFolderPath); + + foreach ($layouts as $layout) { + $result['layouts'][$layoutFolder][] = $this->getOverridesFolder($layout, $layoutFolderPath); + } + } + + // Check for layouts in component folders + foreach ($components as $component) { + if (file_exists($componentPath . '/' . $component . '/layouts/')) { + $componentLayoutPath = Path::clean($componentPath . '/' . $component . '/layouts/'); + + if ($componentLayoutPath) { + $layouts = Folder::folders($componentLayoutPath); + + foreach ($layouts as $layout) { + $result['layouts'][$component][] = $this->getOverridesFolder($layout, $componentLayoutPath); + } + } + } + } + } + + if (!empty($result)) { + return $result; + } + } + + /** + * Create overrides. + * + * @param string $override The override location. + * + * @return boolean true if override creation is successful, false otherwise + * + * @since 3.2 + */ + public function createOverride($override) + { + if ($template = $this->getTemplate()) { + $app = Factory::getApplication(); + $explodeArray = explode(DIRECTORY_SEPARATOR, $override); + $name = end($explodeArray); + $client = ApplicationHelper::getClientInfo($template->client_id); + + if (stristr($name, 'mod_') != false) { + $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/' . $name); + } elseif (stristr($override, 'com_') != false) { + $size = count($explodeArray); + + $url = Path::clean($explodeArray[$size - 3] . '/' . $explodeArray[$size - 1]); + + if ($explodeArray[$size - 2] == 'layouts') { + $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/layouts/' . $url); + } else { + $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/' . $url); + } + } elseif (stripos($override, Path::clean(JPATH_ROOT . '/plugins/')) === 0) { + $size = count($explodeArray); + $layoutPath = Path::clean('plg_' . $explodeArray[$size - 2] . '_' . $explodeArray[$size - 1]); + $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/' . $layoutPath); + } else { + $layoutPath = implode('/', array_slice($explodeArray, -2)); + $htmlPath = Path::clean($client->path . '/templates/' . $template->element . '/html/layouts/' . $layoutPath); + } + + // Check Html folder, create if not exist + if (!Folder::exists($htmlPath)) { + if (!Folder::create($htmlPath)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_ERROR'), 'error'); + + return false; + } + } + + if (stristr($name, 'mod_') != false) { + $return = $this->createTemplateOverride(Path::clean($override . '/tmpl'), $htmlPath); + } elseif (stristr($override, 'com_') != false && stristr($override, 'layouts') == false) { + $path = $override . '/tmpl'; + + // View can also be in the top level folder + if (!is_dir($path)) { + $path = $override; + } + + $return = $this->createTemplateOverride(Path::clean($path), $htmlPath); + } elseif (stripos($override, Path::clean(JPATH_ROOT . '/plugins/')) === 0) { + $return = $this->createTemplateOverride(Path::clean($override . '/tmpl'), $htmlPath); + } else { + $return = $this->createTemplateOverride($override, $htmlPath); + } + + if ($return) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_OVERRIDE_CREATED') . str_replace(JPATH_ROOT, '', $htmlPath)); + + return true; + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_OVERRIDE_FAILED'), 'error'); + + return false; + } + } + } + + /** + * Create override folder & file + * + * @param string $overridePath The override location + * @param string $htmlPath The html location + * + * @return boolean True on success. False otherwise. + */ + public function createTemplateOverride($overridePath, $htmlPath) + { + $return = false; + + if (empty($overridePath) || empty($htmlPath)) { + return $return; + } + + // Get list of template folders + $folders = Folder::folders($overridePath, null, true, true); + + if (!empty($folders)) { + foreach ($folders as $folder) { + $htmlFolder = $htmlPath . str_replace($overridePath, '', $folder); + + if (!Folder::exists($htmlFolder)) { + Folder::create($htmlFolder); + } + } + } + + // Get list of template files (Only get *.php file for template file) + $files = Folder::files($overridePath, '.php', true, true); + + if (empty($files)) { + return true; + } + + foreach ($files as $file) { + $overrideFilePath = str_replace($overridePath, '', $file); + $htmlFilePath = $htmlPath . $overrideFilePath; + + if (File::exists($htmlFilePath)) { + // Generate new unique file name base on current time + $today = Factory::getDate(); + $htmlFilePath = File::stripExt($htmlFilePath) . '-' . $today->format('Ymd-His') . '.' . File::getExt($htmlFilePath); + } + + $return = File::copy($file, $htmlFilePath, '', true); + } + + return $return; + } + + /** + * Delete a particular file. + * + * @param string $file The relative location of the file. + * + * @return boolean True if file deletion is successful, false otherwise + * + * @since 3.2 + */ + public function deleteFile($file) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $filePath = $this->getBasePath() . urldecode(base64_decode($file)); + + $return = File::delete($filePath); + + if (!$return) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_DELETE_ERROR'), 'error'); + + return false; + } + + return true; + } + } + + /** + * Create new file. + * + * @param string $name The name of file. + * @param string $type The extension of the file. + * @param string $location Location for the new file. + * + * @return boolean true if file created successfully, false otherwise + * + * @since 3.2 + */ + public function createFile($name, $type, $location) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $base = $this->getBasePath(); + + if (file_exists(Path::clean($base . '/' . $location . '/' . $name . '.' . $type))) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); + + return false; + } + + if (!fopen(Path::clean($base . '/' . $location . '/' . $name . '.' . $type), 'x')) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_CREATE_ERROR'), 'error'); + + return false; + } + + // Check if the format is allowed and will be showed in the backend + $check = $this->checkFormat($type); + + // Add a message if we are not allowed to show this file in the backend. + if (!$check) { + $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_WARNING_FORMAT_WILL_NOT_BE_VISIBLE', $type), 'warning'); + } + + return true; + } + } + + /** + * Upload new file. + * + * @param array $file The uploaded file array. + * @param string $location Location for the new file. + * + * @return boolean True if file uploaded successfully, false otherwise + * + * @since 3.2 + */ + public function uploadFile($file, $location) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $path = $this->getBasePath(); + $fileName = File::makeSafe($file['name']); + + $err = null; + + if (!TemplateHelper::canUpload($file, $err)) { + // Can't upload the file + return false; + } + + if (file_exists(Path::clean($path . '/' . $location . '/' . $file['name']))) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); + + return false; + } + + if (!File::upload($file['tmp_name'], Path::clean($path . '/' . $location . '/' . $fileName))) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_UPLOAD_ERROR'), 'error'); + + return false; + } + + $url = Path::clean($location . '/' . $fileName); + + return $url; + } + } + + /** + * Create new folder. + * + * @param string $name The name of the new folder. + * @param string $location Location for the new folder. + * + * @return boolean True if override folder is created successfully, false otherwise + * + * @since 3.2 + */ + public function createFolder($name, $location) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $path = Path::clean($location . '/'); + $base = $this->getBasePath(); + + if (file_exists(Path::clean($base . $path . $name))) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_EXISTS'), 'error'); + + return false; + } + + if (!Folder::create(Path::clean($base . $path . $name))) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_CREATE_ERROR'), 'error'); + + return false; + } + + return true; + } + } + + /** + * Delete a folder. + * + * @param string $location The name and location of the folder. + * + * @return boolean True if override folder is deleted successfully, false otherwise + * + * @since 3.2 + */ + public function deleteFolder($location) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $base = $this->getBasePath(); + $path = Path::clean($location . '/'); + + if (!file_exists($base . $path)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_NOT_EXISTS'), 'error'); + + return false; + } + + $return = Folder::delete($base . $path); + + if (!$return) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FOLDER_DELETE_ERROR'), 'error'); + + return false; + } + + return true; + } + } + + /** + * Rename a file. + * + * @param string $file The name and location of the old file + * @param string $name The new name of the file. + * + * @return string Encoded string containing the new file location. + * + * @since 3.2 + */ + public function renameFile($file, $name) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $path = $this->getBasePath(); + $fileName = base64_decode($file); + $explodeArray = explode('.', $fileName); + $type = end($explodeArray); + $explodeArray = explode('/', $fileName); + $newName = str_replace(end($explodeArray), $name . '.' . $type, $fileName); + + if (file_exists($path . $newName)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); + + return false; + } + + if (!rename($path . $fileName, $path . $newName)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_RENAME_ERROR'), 'error'); + + return false; + } + + return base64_encode($newName); + } + } + + /** + * Get an image address, height and width. + * + * @return array an associative array containing image address, height and width. + * + * @since 3.2 + */ + public function getImage() + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $fileName = base64_decode($app->input->get('file')); + $path = $this->getBasePath(); + + $uri = Uri::root(false) . ltrim(str_replace(JPATH_ROOT, '', $this->getBasePath()), '/'); + + if (file_exists(Path::clean($path . $fileName))) { + $JImage = new Image(Path::clean($path . $fileName)); + $image['address'] = $uri . $fileName; + $image['path'] = $fileName; + $image['height'] = $JImage->getHeight(); + $image['width'] = $JImage->getWidth(); + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_IMAGE_FILE_NOT_FOUND'), 'error'); + + return false; + } + + return $image; + } + } + + /** + * Crop an image. + * + * @param string $file The name and location of the file + * @param string $w width. + * @param string $h height. + * @param string $x x-coordinate. + * @param string $y y-coordinate. + * + * @return boolean true if image cropped successfully, false otherwise. + * + * @since 3.2 + */ + public function cropImage($file, $w, $h, $x, $y) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $path = $this->getBasePath() . base64_decode($file); + + try { + $image = new Image($path); + $properties = $image->getImageFileProperties($path); + + switch ($properties->mime) { + case 'image/webp': + $imageType = \IMAGETYPE_WEBP; + break; + case 'image/png': + $imageType = \IMAGETYPE_PNG; + break; + case 'image/gif': + $imageType = \IMAGETYPE_GIF; + break; + default: + $imageType = \IMAGETYPE_JPEG; + } + + $image->crop($w, $h, $x, $y, false); + $image->toFile($path, $imageType); + + return true; + } catch (\Exception $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + } + } + } + + /** + * Resize an image. + * + * @param string $file The name and location of the file + * @param string $width The new width of the image. + * @param string $height The new height of the image. + * + * @return boolean true if image resize successful, false otherwise. + * + * @since 3.2 + */ + public function resizeImage($file, $width, $height) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $path = $this->getBasePath() . base64_decode($file); + + try { + $image = new Image($path); + $properties = $image->getImageFileProperties($path); + + switch ($properties->mime) { + case 'image/webp': + $imageType = \IMAGETYPE_WEBP; + break; + case 'image/png': + $imageType = \IMAGETYPE_PNG; + break; + case 'image/gif': + $imageType = \IMAGETYPE_GIF; + break; + default: + $imageType = \IMAGETYPE_JPEG; + } + + $image->resize($width, $height, false, Image::SCALE_FILL); + $image->toFile($path, $imageType); + + return true; + } catch (\Exception $e) { + $app->enqueueMessage($e->getMessage(), 'error'); + } + } + } + + /** + * Template preview. + * + * @return object object containing the id of the template. + * + * @since 3.2 + */ + public function getPreview() + { + $app = Factory::getApplication(); + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select($db->quoteName(['id', 'client_id'])); + $query->from($db->quoteName('#__template_styles')); + $query->where($db->quoteName('template') . ' = :template') + ->bind(':template', $this->template->element); + + $db->setQuery($query); + + try { + $result = $db->loadObject(); + } catch (\RuntimeException $e) { + $app->enqueueMessage($e->getMessage(), 'warning'); + } + + if (empty($result)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_EXTENSION_RECORD_NOT_FOUND'), 'warning'); + } else { + return $result; + } + } + + /** + * Rename a file. + * + * @return mixed array on success, false on failure + * + * @since 3.2 + */ + public function getFont() + { + if ($template = $this->getTemplate()) { + $app = Factory::getApplication(); + $client = ApplicationHelper::getClientInfo($template->client_id); + $relPath = base64_decode($app->input->get('file')); + $explodeArray = explode('/', $relPath); + $fileName = end($explodeArray); + $path = $this->getBasePath() . base64_decode($app->input->get('file')); + + if (stristr($client->path, 'administrator') == false) { + $folder = '/templates/'; + } else { + $folder = '/administrator/templates/'; + } + + $uri = Uri::root(true) . $folder . $template->element; + + if (file_exists(Path::clean($path))) { + $font['address'] = $uri . $relPath; + + $font['rel_path'] = $relPath; + + $font['name'] = $fileName; + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_FONT_FILE_NOT_FOUND'), 'error'); + + return false; + } + + return $font; + } + } + + /** + * Copy a file. + * + * @param string $newName The name of the copied file + * @param string $location The final location where the file is to be copied + * @param string $file The name and location of the file + * + * @return boolean true if image resize successful, false otherwise. + * + * @since 3.2 + */ + public function copyFile($newName, $location, $file) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $relPath = base64_decode($file); + $explodeArray = explode('.', $relPath); + $ext = end($explodeArray); + $path = $this->getBasePath(); + $newPath = Path::clean($path . $location . '/' . $newName . '.' . $ext); + + if (file_exists($newPath)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_EXISTS'), 'error'); + + return false; + } + + if (File::copy($path . $relPath, $newPath)) { + $app->enqueueMessage(Text::sprintf('COM_TEMPLATES_FILE_COPY_SUCCESS', $newName . '.' . $ext)); + + return true; + } else { + return false; + } + } + } + + /** + * Get the compressed files. + * + * @return array if file exists, false otherwise + * + * @since 3.2 + */ + public function getArchive() + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $path = $this->getBasePath() . base64_decode($app->input->get('file')); + + if (file_exists(Path::clean($path))) { + $files = array(); + $zip = new \ZipArchive(); + + if ($zip->open($path) === true) { + for ($i = 0; $i < $zip->numFiles; $i++) { + $entry = $zip->getNameIndex($i); + $files[] = $entry; + } + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_OPEN_FAIL'), 'error'); + + return false; + } + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_FONT_FILE_NOT_FOUND'), 'error'); + + return false; + } + + return $files; + } + } + + /** + * Extract contents of an archive file. + * + * @param string $file The name and location of the file + * + * @return boolean true if image extraction is successful, false otherwise. + * + * @since 3.2 + */ + public function extractArchive($file) + { + if ($this->getTemplate()) { + $app = Factory::getApplication(); + $relPath = base64_decode($file); + $explodeArray = explode('/', $relPath); + $fileName = end($explodeArray); + $path = $this->getBasePath() . base64_decode($file); + + if (file_exists(Path::clean($path . '/' . $fileName))) { + $zip = new \ZipArchive(); + + if ($zip->open(Path::clean($path . '/' . $fileName)) === true) { + for ($i = 0; $i < $zip->numFiles; $i++) { + $entry = $zip->getNameIndex($i); + + if (file_exists(Path::clean($path . '/' . $entry))) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_EXISTS'), 'error'); + + return false; + } + } + + $zip->extractTo($path); + + return true; + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_OPEN_FAIL'), 'error'); + + return false; + } + } else { + $app->enqueueMessage(Text::_('COM_TEMPLATES_FILE_ARCHIVE_NOT_FOUND'), 'error'); + + return false; + } + } + } + + /** + * Check if the extension is allowed and will be shown in the template manager + * + * @param string $ext The extension to check if it is allowed + * + * @return boolean true if the extension is allowed false otherwise + * + * @since 3.6.0 + */ + protected function checkFormat($ext) + { + if (!isset($this->allowedFormats)) { + $params = ComponentHelper::getParams('com_templates'); + $imageTypes = explode(',', $params->get('image_formats')); + $sourceTypes = explode(',', $params->get('source_formats')); + $fontTypes = explode(',', $params->get('font_formats')); + $archiveTypes = explode(',', $params->get('compressed_formats')); + + $this->allowedFormats = array_merge($imageTypes, $sourceTypes, $fontTypes, $archiveTypes); + $this->allowedFormats = array_map('strtolower', $this->allowedFormats); + } + + return in_array(strtolower($ext), $this->allowedFormats); + } + + /** + * Method to get a list of all the files to edit in a template's media folder. + * + * @return array A nested array of relevant files. + * + * @since 4.1.0 + */ + public function getMediaFiles() + { + $result = []; + $template = $this->getTemplate(); + + if (!isset($template->xmldata)) { + $template->xmldata = TemplatesHelper::parseXMLTemplateFile($template->client_id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $template->name); + } + + if (!isset($template->xmldata->inheritable) || (isset($template->xmldata->parent) && $template->xmldata->parent === '')) { + return $result; + } + + $app = Factory::getApplication(); + $path = Path::clean(JPATH_ROOT . '/media/templates/' . ($template->client_id === 0 ? 'site' : 'administrator') . '/' . $template->element . '/'); + $this->mediaElement = $path; + + if (!is_writable($path)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_DIRECTORY_NOT_WRITABLE'), 'error'); + } + + if (is_dir($path)) { + $result = $this->getDirectoryTree($path); + } + + return $result; + } + + /** + * Method to resolve the base folder. + * + * @return string The absolute path for the base. + * + * @since 4.1.0 + */ + private function getBasePath() + { + $app = Factory::getApplication(); + $isMedia = $app->input->getInt('isMedia', 0); + + return $isMedia ? JPATH_ROOT . '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element : + JPATH_ROOT . '/' . ($this->template->client_id === 0 ? '' : 'administrator/') . 'templates/' . $this->template->element; + } + + /** + * Method to create the templateDetails.xml for the child template + * + * @return boolean true if name is not used, false otherwise + * + * @since 4.1.0 + */ + public function child() + { + $app = Factory::getApplication(); + $template = $this->getTemplate(); + + if (!(array) $template) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); + + return false; + } + + $client = ApplicationHelper::getClientInfo($template->client_id); + $fromPath = Path::clean($client->path . '/templates/' . $template->element . '/templateDetails.xml'); + + // Delete new folder if it exists + $toPath = $this->getState('to_path'); + + if (Folder::exists($toPath)) { + if (!Folder::delete($toPath)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); + + return false; + } + } else { + if (!Folder::create($toPath)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); + + return false; + } + } + + // Copy the template definition from the parent template + if (!File::copy($fromPath, $toPath . '/templateDetails.xml')) { + return false; + } + + // Check manifest for additional files + $newName = strtolower($this->getState('new_name')); + $template = $this->getTemplate(); + + // Edit XML file + $xmlFile = Path::clean($this->getState('to_path') . '/templateDetails.xml'); + + if (!File::exists($xmlFile)) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_INVALID_FROM_NAME'), 'error'); + + return false; + } + + try { + $xml = simplexml_load_string(file_get_contents($xmlFile)); + } catch (\Exception $e) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_READ'), 'error'); + + return false; + } + + $user = Factory::getUser(); + unset($xml->languages); + unset($xml->media); + unset($xml->files); + unset($xml->parent); + unset($xml->inheritable); + + // Remove the update parts + unset($xml->update); + unset($xml->updateservers); + + if (isset($xml->creationDate)) { + $xml->creationDate = (new Date('now'))->format('F Y'); + } else { + $xml->addChild('creationDate', (new Date('now'))->format('F Y')); + } + + if (isset($xml->author)) { + $xml->author = $user->name; + } else { + $xml->addChild('author', $user->name); + } + + if (isset($xml->authorEmail)) { + $xml->authorEmail = $user->email; + } else { + $xml->addChild('authorEmail', $user->email); + } + + $files = $xml->addChild('files'); + $files->addChild('filename', 'templateDetails.xml'); + + // Media folder + $media = $xml->addChild('media'); + $media->addAttribute('folder', 'media'); + $media->addAttribute('destination', 'templates/' . ($template->client_id === 0 ? 'site/' : 'administrator/') . $template->element . '_' . $newName); + $media->addChild('folder', 'css'); + $media->addChild('folder', 'js'); + $media->addChild('folder', 'images'); + $media->addChild('folder', 'html'); + $media->addChild('folder', 'scss'); + + $xml->name = $template->element . '_' . $newName; + $xml->inheritable = 0; + $files = $xml->addChild('parent', $template->element); + + $dom = new \DOMDocument(); + $dom->preserveWhiteSpace = false; + $dom->formatOutput = true; + $dom->loadXML($xml->asXML()); + + $result = File::write($xmlFile, $dom->saveXML()); + + if (!$result) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_WRITE'), 'error'); + + return false; + } + + // Create an empty media folder structure + if ( + !Folder::create($toPath . '/media') + || !Folder::create($toPath . '/media/css') + || !Folder::create($toPath . '/media/js') + || !Folder::create($toPath . '/media/images') + || !Folder::create($toPath . '/media/html/tinymce') + || !Folder::create($toPath . '/media/scss') + ) { + return false; + } + + return true; + } + + /** + * Method to get the parent template existing styles + * + * @return array array of id,titles of the styles + * + * @since 4.1.3 + */ + public function getAllTemplateStyles() + { + $template = $this->getTemplate(); + + if (empty($template->xmldata->inheritable)) { + return []; + } + + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select($db->quoteName(['id', 'title'])) + ->from($db->quoteName('#__template_styles')) + ->where($db->quoteName('client_id') . ' = :client_id', 'AND') + ->where($db->quoteName('template') . ' = :template') + ->orWhere($db->quoteName('parent') . ' = :parent') + ->bind(':client_id', $template->client_id, ParameterType::INTEGER) + ->bind(':template', $template->element) + ->bind(':parent', $template->element); + + $db->setQuery($query); + + return $db->loadObjectList(); + } + + /** + * Method to copy selected styles to the child template + * + * @return boolean true if name is not used, false otherwise + * + * @since 4.1.3 + */ + public function copyStyles() + { + $app = Factory::getApplication(); + $template = $this->getTemplate(); + $newName = strtolower($this->getState('new_name')); + $applyStyles = $this->getState('stylesToCopy'); + + // Get a db connection. + $db = $this->getDatabase(); + + // Create a new query object. + $query = $db->getQuery(true); + + $query->select($db->quoteName(['title', 'params'])) + ->from($db->quoteName('#__template_styles')) + ->whereIn($db->quoteName('id'), ArrayHelper::toInteger($applyStyles)); + // Reset the query using our newly populated query object. + $db->setQuery($query); + + try { + $parentStyle = $db->loadObjectList(); + } catch (\Exception $e) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_STYLE_NOT_FOUND'), 'error'); + + return false; + } + + foreach ($parentStyle as $style) { + $query = $db->getQuery(true); + $styleName = Text::sprintf('COM_TEMPLATES_COPY_CHILD_TEMPLATE_STYLES', ucfirst($template->element . '_' . $newName), $style->title); + + // Insert columns and values + $columns = ['id', 'template', 'client_id', 'home', 'title', 'inheritable', 'parent', 'params']; + $values = [0, $db->quote($template->element . '_' . $newName), (int) $template->client_id, $db->quote('0'), $db->quote($styleName), 0, $db->quote($template->element), $db->quote($style->params)]; + + $query + ->insert($db->quoteName('#__template_styles')) + ->columns($db->quoteName($columns)) + ->values(implode(',', $values)); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\Exception $e) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_ERROR_COULD_NOT_READ'), 'error'); + + return false; + } + } + + return true; + } } diff --git a/administrator/components/com_templates/src/Model/TemplatesModel.php b/administrator/components/com_templates/src/Model/TemplatesModel.php index 13ef6e3644480..2e02145a99c05 100644 --- a/administrator/components/com_templates/src/Model/TemplatesModel.php +++ b/administrator/components/com_templates/src/Model/TemplatesModel.php @@ -1,4 +1,5 @@ client_id); - $item->xmldata = TemplatesHelper::parseXMLTemplateFile($client->path, $item->element); - $num = $this->updated($item->extension_id); - - if ($num) - { - $item->updated = $num; - } - } - - return $items; - } - - /** - * Check if template extension have any updated override. - * - * @param integer $exid Extension id of template. - * - * @return boolean False if records not found/else integer. - * - * @since 4.0.0 - */ - public function updated($exid) - { - $db = $this->getDatabase(); - - // Select the required fields from the table - $query = $db->getQuery(true) - ->select($db->quoteName('template')) - ->from($db->quoteName('#__template_overrides')) - ->where($db->quoteName('extension_id') . ' = :extensionid') - ->where($db->quoteName('state') . ' = 0') - ->bind(':extensionid', $exid, ParameterType::INTEGER); - - // Reset the query. - $db->setQuery($query); - - // Load the results as a list of stdClass objects. - $num = count($db->loadObjectList()); - - if ($num > 0) - { - return $num; - } - - return false; - } - - /** - * Build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.extension_id, a.name, a.element, a.client_id' - ) - ); - $clientId = (int) $this->getState('client_id'); - $query->from($db->quoteName('#__extensions', 'a')) - ->where($db->quoteName('a.client_id') . ' = :clientid') - ->where($db->quoteName('a.enabled') . ' = 1') - ->where($db->quoteName('a.type') . ' = ' . $db->quote('template')) - ->bind(':clientid', $clientId, ParameterType::INTEGER); - - // Filter by search in title. - if ($search = $this->getState('filter.search')) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . StringHelper::strtolower($search) . '%'; - $query->extendWhere( - 'AND', - [ - 'LOWER(' . $db->quoteName('a.element') . ') LIKE :element', - 'LOWER(' . $db->quoteName('a.name') . ') LIKE :name', - ], - 'OR' - ) - ->bind(':element', $search) - ->bind(':name', $search); - } - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.element')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('client_id'); - $id .= ':' . $this->getState('filter.search'); - - return parent::getStoreId($id); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'a.element', $direction = 'asc') - { - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - - // Special case for the client id. - $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); - $clientId = (!in_array($clientId, array (0, 1))) ? 0 : $clientId; - $this->setState('client_id', $clientId); - - // Load the parameters. - $params = ComponentHelper::getParams('com_templates'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'folder', 'a.folder', + 'element', 'a.element', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'state', 'a.state', + 'enabled', 'a.enabled', + 'ordering', 'a.ordering', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Override parent getItems to add extra XML metadata. + * + * @return array + * + * @since 1.6 + */ + public function getItems() + { + $items = parent::getItems(); + + foreach ($items as &$item) { + $client = ApplicationHelper::getClientInfo($item->client_id); + $item->xmldata = TemplatesHelper::parseXMLTemplateFile($client->path, $item->element); + $num = $this->updated($item->extension_id); + + if ($num) { + $item->updated = $num; + } + } + + return $items; + } + + /** + * Check if template extension have any updated override. + * + * @param integer $exid Extension id of template. + * + * @return boolean False if records not found/else integer. + * + * @since 4.0.0 + */ + public function updated($exid) + { + $db = $this->getDatabase(); + + // Select the required fields from the table + $query = $db->getQuery(true) + ->select($db->quoteName('template')) + ->from($db->quoteName('#__template_overrides')) + ->where($db->quoteName('extension_id') . ' = :extensionid') + ->where($db->quoteName('state') . ' = 0') + ->bind(':extensionid', $exid, ParameterType::INTEGER); + + // Reset the query. + $db->setQuery($query); + + // Load the results as a list of stdClass objects. + $num = count($db->loadObjectList()); + + if ($num > 0) { + return $num; + } + + return false; + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.extension_id, a.name, a.element, a.client_id' + ) + ); + $clientId = (int) $this->getState('client_id'); + $query->from($db->quoteName('#__extensions', 'a')) + ->where($db->quoteName('a.client_id') . ' = :clientid') + ->where($db->quoteName('a.enabled') . ' = 1') + ->where($db->quoteName('a.type') . ' = ' . $db->quote('template')) + ->bind(':clientid', $clientId, ParameterType::INTEGER); + + // Filter by search in title. + if ($search = $this->getState('filter.search')) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . StringHelper::strtolower($search) . '%'; + $query->extendWhere( + 'AND', + [ + 'LOWER(' . $db->quoteName('a.element') . ') LIKE :element', + 'LOWER(' . $db->quoteName('a.name') . ') LIKE :name', + ], + 'OR' + ) + ->bind(':element', $search) + ->bind(':name', $search); + } + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.element')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('client_id'); + $id .= ':' . $this->getState('filter.search'); + + return parent::getStoreId($id); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.element', $direction = 'asc') + { + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + + // Special case for the client id. + $clientId = (int) $this->getUserStateFromRequest($this->context . '.client_id', 'client_id', 0, 'int'); + $clientId = (!in_array($clientId, array (0, 1))) ? 0 : $clientId; + $this->setState('client_id', $clientId); + + // Load the parameters. + $params = ComponentHelper::getParams('com_templates'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } } diff --git a/administrator/components/com_templates/src/Service/HTML/Templates.php b/administrator/components/com_templates/src/Service/HTML/Templates.php index 6d7fd3632d220..45a38d1e0439a 100644 --- a/administrator/components/com_templates/src/Service/HTML/Templates.php +++ b/administrator/components/com_templates/src/Service/HTML/Templates.php @@ -1,4 +1,5 @@ client_id); - - if (!isset($template->xmldata)) - { - $template->xmldata = TemplatesHelper::parseXMLTemplateFile($client->id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $template->name); - } - - if ((isset($template->xmldata->inheritable) && (bool) $template->xmldata->inheritable) || isset($template->xmldata->parent)) - { - if (isset($template->xmldata->parent) && (string) $template->xmldata->parent !== '' && file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_thumbnail.png')) - { - if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png')) - { - $html = HTMLHelper::_('image', 'media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW')); - $html = ''; - } - elseif ((file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_preview.png'))) - { - $html = HTMLHelper::_('image', 'media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW')); - $html = ''; - } - else - { - $html = HTMLHelper::_('image', 'template_thumb.svg', Text::_('COM_TEMPLATES_PREVIEW'), ['style' => 'width:200px; height:120px;']); - } - } - elseif (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png')) - { - $html = HTMLHelper::_('image', 'media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW')); - - if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png')) - { - $html = ''; - } - } - else - { - $html = HTMLHelper::_('image', 'template_thumb.svg', Text::_('COM_TEMPLATES_PREVIEW'), ['style' => 'width:200px; height:120px;']); - } - } - elseif (file_exists($client->path . '/templates/' . $template->element . '/template_thumbnail.png')) - { - $html = HTMLHelper::_('image', (($template->client_id == 0) ? Uri::root(true) : Uri::root(true) . '/administrator/') . '/templates/' . $template->element . '/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW'), [], false, -1); - - if (file_exists($client->path . '/templates/' . $template->element . '/template_preview.png')) - { - $html = ''; - } - } - else - { - $html = HTMLHelper::_('image', 'template_thumb.svg', Text::_('COM_TEMPLATES_PREVIEW'), ['style' => 'width:200px; height:120px;']); - } - - return $html; - } - - /** - * Renders the html for the modal linked to thumb. - * - * @param string|object $template The name of the template or the template object. - * @param integer $clientId The application client ID the template applies to - * - * @return string The html string - * - * @since 3.4 - * - * @deprecated 5.0 The argument $template should be object and $clientId will be removed - */ - public function thumbModal($template, $clientId = 0) - { - if (is_string($template)) - { - return HTMLHelper::_('image', 'template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW'), [], true, -1); - } - - $html = ''; - $thumb = ''; - $preview = ''; - $client = ApplicationHelper::getClientInfo($template->client_id); - - if (!isset($template->xmldata)) - { - $template->xmldata = TemplatesHelper::parseXMLTemplateFile($client->id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $template->name); - } - - if ((isset($template->xmldata->inheritable) && (bool) $template->xmldata->inheritable) || isset($template->xmldata->parent)) - { - if (isset($template->xmldata->parent) && (string) $template->xmldata->parent !== '') - { - if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png')) - { - $thumb = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . 'administrator') . 'media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png'; - - if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element. '/images/template_preview.png')) - { - $preview = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . '/administrator') . '/media/templates/' . $client->name . '/' . $template->element. '/images/template_preview.png'; - } - } - else - { - $thumb = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . 'administrator') . 'media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_thumbnail.png'; - - if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent. '/images/template_preview.png')) - { - $preview = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . '/administrator') . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent. '/images/template_preview.png'; - } - } - } - elseif (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png')) - { - $thumb = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . '/administrator') . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png'; - - if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png')) - { - $preview = Uri::root(true) . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png'; - } - } - } - elseif (file_exists($client->path . '/templates/' . $template->element . '/template_thumbnail.png')) - { - $thumb = (($template->client_id == 0) ? Uri::root(true) : Uri::root(true) . 'administrator') . '/templates/' . $template->element . '/template_thumbnail.png'; - - if (file_exists($client->path . '/templates/' . $template->element . '/template_preview.png')) - { - $preview = (($template->client_id == 0) ? Uri::root(true) : Uri::root(true) . '/administrator') . '/templates/' . $template->element . '/template_preview.png'; - } - } - - if ($thumb !== '' && $preview !== '') - { - $footer = ''; - - $html .= HTMLHelper::_( - 'bootstrap.renderModal', - $template->name . '-Modal', - array( - 'title' => Text::sprintf('COM_TEMPLATES_SCREENSHOT', ucfirst($template->name)), - 'height' => '500px', - 'width' => '800px', - 'footer' => $footer, - ), - '
' . $template->name . '
' - ); - } - - return $html; - } + /** + * Display the thumb for the template. + * + * @param string|object $template The name of the template or the template object. + * @param integer $clientId The application client ID the template applies to + * + * @return string The html string + * + * @since 1.6 + * + * @deprecated 5.0 The argument $template should be object and $clientId will be removed + */ + public function thumb($template, $clientId = 0) + { + if (is_string($template)) { + return HTMLHelper::_('image', 'template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW'), [], true, -1); + } + + $client = ApplicationHelper::getClientInfo($template->client_id); + + if (!isset($template->xmldata)) { + $template->xmldata = TemplatesHelper::parseXMLTemplateFile($client->id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $template->name); + } + + if ((isset($template->xmldata->inheritable) && (bool) $template->xmldata->inheritable) || isset($template->xmldata->parent)) { + if (isset($template->xmldata->parent) && (string) $template->xmldata->parent !== '' && file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_thumbnail.png')) { + if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png')) { + $html = HTMLHelper::_('image', 'media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW')); + $html = ''; + } elseif ((file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_preview.png'))) { + $html = HTMLHelper::_('image', 'media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW')); + $html = ''; + } else { + $html = HTMLHelper::_('image', 'template_thumb.svg', Text::_('COM_TEMPLATES_PREVIEW'), ['style' => 'width:200px; height:120px;']); + } + } elseif (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png')) { + $html = HTMLHelper::_('image', 'media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW')); + + if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png')) { + $html = ''; + } + } else { + $html = HTMLHelper::_('image', 'template_thumb.svg', Text::_('COM_TEMPLATES_PREVIEW'), ['style' => 'width:200px; height:120px;']); + } + } elseif (file_exists($client->path . '/templates/' . $template->element . '/template_thumbnail.png')) { + $html = HTMLHelper::_('image', (($template->client_id == 0) ? Uri::root(true) : Uri::root(true) . '/administrator/') . '/templates/' . $template->element . '/template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW'), [], false, -1); + + if (file_exists($client->path . '/templates/' . $template->element . '/template_preview.png')) { + $html = ''; + } + } else { + $html = HTMLHelper::_('image', 'template_thumb.svg', Text::_('COM_TEMPLATES_PREVIEW'), ['style' => 'width:200px; height:120px;']); + } + + return $html; + } + + /** + * Renders the html for the modal linked to thumb. + * + * @param string|object $template The name of the template or the template object. + * @param integer $clientId The application client ID the template applies to + * + * @return string The html string + * + * @since 3.4 + * + * @deprecated 5.0 The argument $template should be object and $clientId will be removed + */ + public function thumbModal($template, $clientId = 0) + { + if (is_string($template)) { + return HTMLHelper::_('image', 'template_thumbnail.png', Text::_('COM_TEMPLATES_PREVIEW'), [], true, -1); + } + + $html = ''; + $thumb = ''; + $preview = ''; + $client = ApplicationHelper::getClientInfo($template->client_id); + + if (!isset($template->xmldata)) { + $template->xmldata = TemplatesHelper::parseXMLTemplateFile($client->id === 0 ? JPATH_ROOT : JPATH_ROOT . '/administrator', $template->name); + } + + if ((isset($template->xmldata->inheritable) && (bool) $template->xmldata->inheritable) || isset($template->xmldata->parent)) { + if (isset($template->xmldata->parent) && (string) $template->xmldata->parent !== '') { + if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png')) { + $thumb = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . 'administrator') . 'media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png'; + + if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png')) { + $preview = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . '/administrator') . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png'; + } + } else { + $thumb = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . 'administrator') . 'media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_thumbnail.png'; + + if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_preview.png')) { + $preview = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . '/administrator') . '/media/templates/' . $client->name . '/' . (string) $template->xmldata->parent . '/images/template_preview.png'; + } + } + } elseif (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png')) { + $thumb = ($template->client_id == 0 ? Uri::root(true) : Uri::root(true) . '/administrator') . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_thumbnail.png'; + + if (file_exists(JPATH_ROOT . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png')) { + $preview = Uri::root(true) . '/media/templates/' . $client->name . '/' . $template->element . '/images/template_preview.png'; + } + } + } elseif (file_exists($client->path . '/templates/' . $template->element . '/template_thumbnail.png')) { + $thumb = (($template->client_id == 0) ? Uri::root(true) : Uri::root(true) . 'administrator') . '/templates/' . $template->element . '/template_thumbnail.png'; + + if (file_exists($client->path . '/templates/' . $template->element . '/template_preview.png')) { + $preview = (($template->client_id == 0) ? Uri::root(true) : Uri::root(true) . '/administrator') . '/templates/' . $template->element . '/template_preview.png'; + } + } + + if ($thumb !== '' && $preview !== '') { + $footer = ''; + + $html .= HTMLHelper::_( + 'bootstrap.renderModal', + $template->name . '-Modal', + array( + 'title' => Text::sprintf('COM_TEMPLATES_SCREENSHOT', ucfirst($template->name)), + 'height' => '500px', + 'width' => '800px', + 'footer' => $footer, + ), + '
' . $template->name . '
' + ); + } + + return $html; + } } diff --git a/administrator/components/com_templates/src/Table/StyleTable.php b/administrator/components/com_templates/src/Table/StyleTable.php index 857dd4f01f3c4..f0a7a5c298145 100644 --- a/administrator/components/com_templates/src/Table/StyleTable.php +++ b/administrator/components/com_templates/src/Table/StyleTable.php @@ -1,4 +1,5 @@ home == '1') - { - $this->setError(Text::_('COM_TEMPLATES_ERROR_CANNOT_UNSET_DEFAULT_STYLE')); - - return false; - } - - return parent::bind($array, $ignore); - } - - /** - * Overloaded check method to ensure data integrity. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (empty($this->title)) - { - $this->setError(Text::_('COM_TEMPLATES_ERROR_STYLE_REQUIRES_TITLE')); - - return false; - } - - return true; - } - - /** - * Overloaded store method to ensure unicity of default style. - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function store($updateNulls = false) - { - if ($this->home != '0') - { - $clientId = (int) $this->client_id; - $query = $this->_db->getQuery(true) - ->update($this->_db->quoteName('#__template_styles')) - ->set($this->_db->quoteName('home') . ' = ' . $this->_db->quote('0')) - ->where($this->_db->quoteName('client_id') . ' = :clientid') - ->where($this->_db->quoteName('home') . ' = :home') - ->bind(':clientid', $clientId, ParameterType::INTEGER) - ->bind(':home', $this->home); - $this->_db->setQuery($query); - $this->_db->execute(); - } - - return parent::store($updateNulls); - } - - /** - * Overloaded store method to unsure existence of a default style for a template. - * - * @param mixed $pk An optional primary key value to delete. If not set the instance property value is used. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function delete($pk = null) - { - $k = $this->_tbl_key; - $pk = is_null($pk) ? $this->$k : $pk; - - if (!is_null($pk)) - { - $clientId = (int) $this->client_id; - $query = $this->_db->getQuery(true) - ->select($this->_db->quoteName('id')) - ->from($this->_db->quoteName('#__template_styles')) - ->where($this->_db->quoteName('client_id') . ' = :clientid') - ->where($this->_db->quoteName('template') . ' = :template') - ->bind(':template', $this->template) - ->bind(':clientid', $clientId, ParameterType::INTEGER); - $this->_db->setQuery($query); - $results = $this->_db->loadColumn(); - - if (count($results) == 1 && $results[0] == $pk) - { - $this->setError(Text::_('COM_TEMPLATES_ERROR_CANNOT_DELETE_LAST_STYLE')); - - return false; - } - } - - return parent::delete($pk); - } + /** + * Constructor + * + * @param DatabaseDriver $db A database connector object + * + * @since 1.6 + */ + public function __construct(DatabaseDriver $db) + { + parent::__construct('#__template_styles', 'id', $db); + } + + /** + * Overloaded bind function to pre-process the params. + * + * @param array $array Named array + * @param mixed $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return null|string null if operation was satisfactory, otherwise returns an error + * + * @since 1.6 + */ + public function bind($array, $ignore = '') + { + if (isset($array['params']) && is_array($array['params'])) { + $registry = new Registry($array['params']); + $array['params'] = (string) $registry; + } + + // Verify that the default style is not unset + if ($array['home'] == '0' && $this->home == '1') { + $this->setError(Text::_('COM_TEMPLATES_ERROR_CANNOT_UNSET_DEFAULT_STYLE')); + + return false; + } + + return parent::bind($array, $ignore); + } + + /** + * Overloaded check method to ensure data integrity. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (empty($this->title)) { + $this->setError(Text::_('COM_TEMPLATES_ERROR_STYLE_REQUIRES_TITLE')); + + return false; + } + + return true; + } + + /** + * Overloaded store method to ensure unicity of default style. + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function store($updateNulls = false) + { + if ($this->home != '0') { + $clientId = (int) $this->client_id; + $query = $this->_db->getQuery(true) + ->update($this->_db->quoteName('#__template_styles')) + ->set($this->_db->quoteName('home') . ' = ' . $this->_db->quote('0')) + ->where($this->_db->quoteName('client_id') . ' = :clientid') + ->where($this->_db->quoteName('home') . ' = :home') + ->bind(':clientid', $clientId, ParameterType::INTEGER) + ->bind(':home', $this->home); + $this->_db->setQuery($query); + $this->_db->execute(); + } + + return parent::store($updateNulls); + } + + /** + * Overloaded store method to unsure existence of a default style for a template. + * + * @param mixed $pk An optional primary key value to delete. If not set the instance property value is used. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function delete($pk = null) + { + $k = $this->_tbl_key; + $pk = is_null($pk) ? $this->$k : $pk; + + if (!is_null($pk)) { + $clientId = (int) $this->client_id; + $query = $this->_db->getQuery(true) + ->select($this->_db->quoteName('id')) + ->from($this->_db->quoteName('#__template_styles')) + ->where($this->_db->quoteName('client_id') . ' = :clientid') + ->where($this->_db->quoteName('template') . ' = :template') + ->bind(':template', $this->template) + ->bind(':clientid', $clientId, ParameterType::INTEGER); + $this->_db->setQuery($query); + $results = $this->_db->loadColumn(); + + if (count($results) == 1 && $results[0] == $pk) { + $this->setError(Text::_('COM_TEMPLATES_ERROR_CANNOT_DELETE_LAST_STYLE')); + + return false; + } + } + + return parent::delete($pk); + } } diff --git a/administrator/components/com_templates/src/View/Style/HtmlView.php b/administrator/components/com_templates/src/View/Style/HtmlView.php index 8f315cd13deed..3a80a7735b8fc 100644 --- a/administrator/components/com_templates/src/View/Style/HtmlView.php +++ b/administrator/components/com_templates/src/View/Style/HtmlView.php @@ -1,4 +1,5 @@ item = $this->get('Item'); - $this->state = $this->get('State'); - $this->form = $this->get('Form'); - $this->canDo = ContentHelper::getActions('com_templates'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $isNew = ($this->item->id == 0); - $canDo = $this->canDo; - - ToolbarHelper::title( - $isNew ? Text::_('COM_TEMPLATES_MANAGER_ADD_STYLE') - : Text::_('COM_TEMPLATES_MANAGER_EDIT_STYLE'), 'paint-brush thememanager' - ); - - $toolbarButtons = []; - - // If not checked out, can save the item. - if ($canDo->get('core.edit')) - { - ToolbarHelper::apply('style.apply'); - $toolbarButtons[] = ['save', 'style.save']; - } - - // If an existing item, can save to a copy. - if (!$isNew && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'style.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('style.cancel'); - } - else - { - ToolbarHelper::cancel('style.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - - // Get the help information for the template item. - $lang = Factory::getLanguage(); - $help = $this->get('Help'); - - if ($lang->hasKey($help->url)) - { - $debug = $lang->setDebug(false); - $url = Text::_($help->url); - $lang->setDebug($debug); - } - else - { - $url = null; - } - - ToolbarHelper::help($help->key, false, $url); - } + /** + * The CMSObject (on success, false on failure) + * + * @var CMSObject + */ + protected $item; + + /** + * The form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The actions the user is authorised to perform + * + * @var CMSObject + * + * @since 4.0.0 + */ + protected $canDo; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + $this->form = $this->get('Form'); + $this->canDo = ContentHelper::getActions('com_templates'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $isNew = ($this->item->id == 0); + $canDo = $this->canDo; + + ToolbarHelper::title( + $isNew ? Text::_('COM_TEMPLATES_MANAGER_ADD_STYLE') + : Text::_('COM_TEMPLATES_MANAGER_EDIT_STYLE'), + 'paint-brush thememanager' + ); + + $toolbarButtons = []; + + // If not checked out, can save the item. + if ($canDo->get('core.edit')) { + ToolbarHelper::apply('style.apply'); + $toolbarButtons[] = ['save', 'style.save']; + } + + // If an existing item, can save to a copy. + if (!$isNew && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'style.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('style.cancel'); + } else { + ToolbarHelper::cancel('style.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + + // Get the help information for the template item. + $lang = Factory::getLanguage(); + $help = $this->get('Help'); + + if ($lang->hasKey($help->url)) { + $debug = $lang->setDebug(false); + $url = Text::_($help->url); + $lang->setDebug($debug); + } else { + $url = null; + } + + ToolbarHelper::help($help->key, false, $url); + } } diff --git a/administrator/components/com_templates/src/View/Style/JsonView.php b/administrator/components/com_templates/src/View/Style/JsonView.php index edc7dd229f897..f019d9cd92796 100644 --- a/administrator/components/com_templates/src/View/Style/JsonView.php +++ b/administrator/components/com_templates/src/View/Style/JsonView.php @@ -1,4 +1,5 @@ item = $this->get('Item'); - } - catch (\Exception $e) - { - $app = Factory::getApplication(); - $app->enqueueMessage($e->getMessage(), 'error'); + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + * + * @since 1.6 + */ + public function display($tpl = null) + { + try { + $this->item = $this->get('Item'); + } catch (\Exception $e) { + $app = Factory::getApplication(); + $app->enqueueMessage($e->getMessage(), 'error'); - return false; - } + return false; + } - $paramsList = $this->item->getProperties(); + $paramsList = $this->item->getProperties(); - unset($paramsList['xml']); + unset($paramsList['xml']); - $paramsList = json_encode($paramsList); + $paramsList = json_encode($paramsList); - return $paramsList; - } + return $paramsList; + } } diff --git a/administrator/components/com_templates/src/View/Styles/HtmlView.php b/administrator/components/com_templates/src/View/Styles/HtmlView.php index 15783e618dfa4..5dfdb107dd08d 100644 --- a/administrator/components/com_templates/src/View/Styles/HtmlView.php +++ b/administrator/components/com_templates/src/View/Styles/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->total = $this->get('Total'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->preview = ComponentHelper::getParams('com_templates')->get('template_positions_display'); - - // Remove the menu item filter for administrator styles. - if ((int) $this->state->get('client_id') !== 0) - { - unset($this->activeFilters['menuitem']); - $this->filterForm->removeField('menuitem', 'filter'); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_templates'); - $clientId = (int) $this->get('State')->get('client_id'); - - // Add a shortcut to the templates list view. - ToolbarHelper::link('index.php?option=com_templates&view=templates&client_id=' . $clientId, 'COM_TEMPLATES_MANAGER_TEMPLATES', 'icon-code thememanager'); - - // Set the title. - if ($clientId === 1) - { - ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_STYLES_ADMIN'), 'paint-brush thememanager'); - } - else - { - ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_STYLES_SITE'), 'paint-brush thememanager'); - } - - if ($canDo->get('core.edit.state')) - { - ToolbarHelper::makeDefault('styles.setDefault', 'COM_TEMPLATES_TOOLBAR_SET_HOME'); - ToolbarHelper::divider(); - } - - if ($canDo->get('core.create')) - { - ToolbarHelper::custom('styles.duplicate', 'copy', '', 'JTOOLBAR_DUPLICATE', true); - ToolbarHelper::divider(); - } - - if ($canDo->get('core.delete')) - { - ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'styles.delete', 'JTOOLBAR_DELETE'); - ToolbarHelper::divider(); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_templates'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Templates:_Styles'); - } + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is the parameter enabled to show template positions in the frontend? + * + * @var boolean + * @since 4.0.0 + */ + public $preview; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->total = $this->get('Total'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->preview = ComponentHelper::getParams('com_templates')->get('template_positions_display'); + + // Remove the menu item filter for administrator styles. + if ((int) $this->state->get('client_id') !== 0) { + unset($this->activeFilters['menuitem']); + $this->filterForm->removeField('menuitem', 'filter'); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_templates'); + $clientId = (int) $this->get('State')->get('client_id'); + + // Add a shortcut to the templates list view. + ToolbarHelper::link('index.php?option=com_templates&view=templates&client_id=' . $clientId, 'COM_TEMPLATES_MANAGER_TEMPLATES', 'icon-code thememanager'); + + // Set the title. + if ($clientId === 1) { + ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_STYLES_ADMIN'), 'paint-brush thememanager'); + } else { + ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_STYLES_SITE'), 'paint-brush thememanager'); + } + + if ($canDo->get('core.edit.state')) { + ToolbarHelper::makeDefault('styles.setDefault', 'COM_TEMPLATES_TOOLBAR_SET_HOME'); + ToolbarHelper::divider(); + } + + if ($canDo->get('core.create')) { + ToolbarHelper::custom('styles.duplicate', 'copy', '', 'JTOOLBAR_DUPLICATE', true); + ToolbarHelper::divider(); + } + + if ($canDo->get('core.delete')) { + ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'styles.delete', 'JTOOLBAR_DELETE'); + ToolbarHelper::divider(); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_templates'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Templates:_Styles'); + } } diff --git a/administrator/components/com_templates/src/View/Template/HtmlView.php b/administrator/components/com_templates/src/View/Template/HtmlView.php index 84992c390493c..9b430a038deda 100644 --- a/administrator/components/com_templates/src/View/Template/HtmlView.php +++ b/administrator/components/com_templates/src/View/Template/HtmlView.php @@ -1,4 +1,5 @@ file = $app->input->get('file'); - $this->fileName = InputFilter::getInstance()->clean(base64_decode($this->file), 'string'); - $explodeArray = explode('.', $this->fileName); - $ext = end($explodeArray); - $this->files = $this->get('Files'); - $this->mediaFiles = $this->get('MediaFiles'); - $this->state = $this->get('State'); - $this->template = $this->get('Template'); - $this->preview = $this->get('Preview'); - $this->pluginState = PluginHelper::isEnabled('installer', 'override'); - $this->updatedList = $this->get('UpdatedList'); - $this->styles = $this->get('AllTemplateStyles'); - $this->stylesHTML = ''; - - $params = ComponentHelper::getParams('com_templates'); - $imageTypes = explode(',', $params->get('image_formats')); - $sourceTypes = explode(',', $params->get('source_formats')); - $fontTypes = explode(',', $params->get('font_formats')); - $archiveTypes = explode(',', $params->get('compressed_formats')); - - if (in_array($ext, $sourceTypes)) - { - $this->form = $this->get('Form'); - $this->form->setFieldAttribute('source', 'syntax', $ext); - $this->source = $this->get('Source'); - $this->type = 'file'; - } - elseif (in_array($ext, $imageTypes)) - { - try - { - $this->image = $this->get('Image'); - $this->type = 'image'; - } - catch (\RuntimeException $exception) - { - $app->enqueueMessage(Text::_('COM_TEMPLATES_GD_EXTENSION_NOT_AVAILABLE')); - $this->type = 'home'; - } - } - elseif (in_array($ext, $fontTypes)) - { - $this->font = $this->get('Font'); - $this->type = 'font'; - } - elseif (in_array($ext, $archiveTypes)) - { - $this->archive = $this->get('Archive'); - $this->type = 'archive'; - } - else - { - $this->type = 'home'; - } - - $this->overridesList = $this->get('OverridesList'); - $this->id = $this->state->get('extension.id'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - $app->enqueueMessage(implode("\n", $errors)); - - return false; - } - - $this->addToolbar(); - - if (!$this->getCurrentUser()->authorise('core.admin')) - { - $this->setLayout('readonly'); - } - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @since 1.6 - * - * @return void - */ - protected function addToolbar() - { - $app = Factory::getApplication(); - $user = $this->getCurrentUser(); - $app->input->set('hidemainmenu', true); - - // User is global SuperUser - $isSuperUser = $user->authorise('core.admin'); - - // Get the toolbar object instance - $bar = Toolbar::getInstance('toolbar'); - $explodeArray = explode('.', $this->fileName); - $ext = end($explodeArray); - - ToolbarHelper::title(Text::sprintf('COM_TEMPLATES_MANAGER_VIEW_TEMPLATE', ucfirst($this->template->name)), 'icon-code thememanager'); - - // Only show file edit buttons for global SuperUser - if ($isSuperUser) - { - // Add an Apply and save button - if ($this->type === 'file') - { - ToolbarHelper::apply('template.apply'); - ToolbarHelper::save('template.save'); - } - // Add a Crop and Resize button - elseif ($this->type === 'image') - { - ToolbarHelper::custom('template.cropImage', 'icon-crop', '', 'COM_TEMPLATES_BUTTON_CROP', false); - ToolbarHelper::modal('resizeModal', 'icon-expand', 'COM_TEMPLATES_BUTTON_RESIZE'); - } - // Add an extract button - elseif ($this->type === 'archive') - { - ToolbarHelper::custom('template.extractArchive', 'chevron-down', '', 'COM_TEMPLATES_BUTTON_EXTRACT_ARCHIVE', false); - } - // Add a copy/child template button - elseif ($this->type === 'home') - { - if (isset($this->template->xmldata->inheritable) && (string) $this->template->xmldata->inheritable === '1') - { - ToolbarHelper::modal('childModal', 'icon-copy', 'COM_TEMPLATES_BUTTON_TEMPLATE_CHILD', false); - } - elseif (!isset($this->template->xmldata->parent) || $this->template->xmldata->parent == '') - { - ToolbarHelper::modal('copyModal', 'icon-copy', 'COM_TEMPLATES_BUTTON_COPY_TEMPLATE', false); - } - } - } - - // Add a Template preview button - if ($this->type === 'home') - { - $client = (int) $this->preview->client_id === 1 ? 'administrator/' : ''; - $bar->linkButton('preview') - ->icon('icon-image') - ->text('COM_TEMPLATES_BUTTON_PREVIEW') - ->url(Uri::root() . $client . 'index.php?tp=1&templateStyle=' . $this->preview->id) - ->attributes(['target' => '_new']); - } - - // Only show file manage buttons for global SuperUser - if ($isSuperUser) - { - if ($this->type === 'home') - { - // Add Manage folders button - ToolbarHelper::modal('folderModal', 'icon-folder icon white', 'COM_TEMPLATES_BUTTON_FOLDERS'); - - // Add a new file button - ToolbarHelper::modal('fileModal', 'icon-file', 'COM_TEMPLATES_BUTTON_FILE'); - } - else - { - // Add a Rename file Button - ToolbarHelper::modal('renameModal', 'icon-sync', 'COM_TEMPLATES_BUTTON_RENAME_FILE'); - - // Add a Delete file Button - ToolbarHelper::modal('deleteModal', 'icon-times', 'COM_TEMPLATES_BUTTON_DELETE_FILE', 'btn-danger'); - } - } - - if (count($this->updatedList) !== 0 && $this->pluginState) - { - ToolbarHelper::custom('template.deleteOverrideHistory', 'times', '', 'COM_TEMPLATES_BUTTON_DELETE_LIST_ENTRY', true, 'updateForm'); - } - - if ($this->type === 'home') - { - ToolbarHelper::cancel('template.cancel', 'JTOOLBAR_CLOSE'); - } - else - { - ToolbarHelper::cancel('template.close', 'COM_TEMPLATES_BUTTON_CLOSE_FILE'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Templates:_Customise'); - } - - /** - * Method for creating the collapsible tree. - * - * @param array $array The value of the present node for recursion - * - * @return string - * - * @note Uses recursion - * @since 3.2 - */ - protected function directoryTree($array) - { - $temp = $this->files; - $this->files = $array; - $txt = $this->loadTemplate('tree'); - $this->files = $temp; - - return $txt; - } - - /** - * Method for listing the folder tree in modals. - * - * @param array $array The value of the present node for recursion - * - * @return string - * - * @note Uses recursion - * @since 3.2 - */ - protected function folderTree($array) - { - $temp = $this->files; - $this->files = $array; - $txt = $this->loadTemplate('folders'); - $this->files = $temp; - - return $txt; - } - - /** - * Method for creating the collapsible tree. - * - * @param array $array The value of the present node for recursion - * - * @return string - * - * @note Uses recursion - * @since 4.1.0 - */ - protected function mediaTree($array) - { - $temp = $this->mediaFiles; - $this->mediaFiles = $array; - $txt = $this->loadTemplate('tree_media'); - $this->mediaFiles = $temp; - - return $txt; - } - - /** - * Method for listing the folder tree in modals. - * - * @param array $array The value of the present node for recursion - * - * @return string - * - * @note Uses recursion - * @since 4.1.0 - */ - protected function mediaFolderTree($array) - { - $temp = $this->mediaFiles; - $this->mediaFiles = $array; - $txt = $this->loadTemplate('media_folders'); - $this->mediaFiles = $temp; - - return $txt; - } + /** + * The Model state + * + * @var CMSObject + */ + protected $state; + + /** + * The template details + * + * @var \stdClass|false + */ + protected $template; + + /** + * For loading the source form + * + * @var Form + */ + protected $form; + + /** + * For loading source file contents + * + * @var array + */ + protected $source; + + /** + * Extension id + * + * @var integer + */ + protected $id; + + /** + * Encrypted file path + * + * @var string + */ + protected $file; + + /** + * List of available overrides + * + * @var array + */ + protected $overridesList; + + /** + * Name of the present file + * + * @var string + */ + protected $fileName; + + /** + * Type of the file - image, source, font + * + * @var string + */ + protected $type; + + /** + * For loading image information + * + * @var array + */ + protected $image; + + /** + * Template id for showing preview button + * + * @var \stdClass + */ + protected $preview; + + /** + * For loading font information + * + * @var array + */ + protected $font; + + /** + * A nested array containing list of files and folders + * + * @var array + */ + protected $files; + + /** + * An array containing a list of compressed files + * + * @var array + */ + protected $archive; + + /** + * The state of installer override plugin. + * + * @var array + * + * @since 4.0.0 + */ + protected $pluginState; + + /** + * A nested array containing list of files and folders in the media folder + * + * @var array + * + * @since 4.1.0 + */ + protected $mediaFiles; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void|boolean + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $this->file = $app->input->get('file'); + $this->fileName = InputFilter::getInstance()->clean(base64_decode($this->file), 'string'); + $explodeArray = explode('.', $this->fileName); + $ext = end($explodeArray); + $this->files = $this->get('Files'); + $this->mediaFiles = $this->get('MediaFiles'); + $this->state = $this->get('State'); + $this->template = $this->get('Template'); + $this->preview = $this->get('Preview'); + $this->pluginState = PluginHelper::isEnabled('installer', 'override'); + $this->updatedList = $this->get('UpdatedList'); + $this->styles = $this->get('AllTemplateStyles'); + $this->stylesHTML = ''; + + $params = ComponentHelper::getParams('com_templates'); + $imageTypes = explode(',', $params->get('image_formats')); + $sourceTypes = explode(',', $params->get('source_formats')); + $fontTypes = explode(',', $params->get('font_formats')); + $archiveTypes = explode(',', $params->get('compressed_formats')); + + if (in_array($ext, $sourceTypes)) { + $this->form = $this->get('Form'); + $this->form->setFieldAttribute('source', 'syntax', $ext); + $this->source = $this->get('Source'); + $this->type = 'file'; + } elseif (in_array($ext, $imageTypes)) { + try { + $this->image = $this->get('Image'); + $this->type = 'image'; + } catch (\RuntimeException $exception) { + $app->enqueueMessage(Text::_('COM_TEMPLATES_GD_EXTENSION_NOT_AVAILABLE')); + $this->type = 'home'; + } + } elseif (in_array($ext, $fontTypes)) { + $this->font = $this->get('Font'); + $this->type = 'font'; + } elseif (in_array($ext, $archiveTypes)) { + $this->archive = $this->get('Archive'); + $this->type = 'archive'; + } else { + $this->type = 'home'; + } + + $this->overridesList = $this->get('OverridesList'); + $this->id = $this->state->get('extension.id'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + $app->enqueueMessage(implode("\n", $errors)); + + return false; + } + + $this->addToolbar(); + + if (!$this->getCurrentUser()->authorise('core.admin')) { + $this->setLayout('readonly'); + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @since 1.6 + * + * @return void + */ + protected function addToolbar() + { + $app = Factory::getApplication(); + $user = $this->getCurrentUser(); + $app->input->set('hidemainmenu', true); + + // User is global SuperUser + $isSuperUser = $user->authorise('core.admin'); + + // Get the toolbar object instance + $bar = Toolbar::getInstance('toolbar'); + $explodeArray = explode('.', $this->fileName); + $ext = end($explodeArray); + + ToolbarHelper::title(Text::sprintf('COM_TEMPLATES_MANAGER_VIEW_TEMPLATE', ucfirst($this->template->name)), 'icon-code thememanager'); + + // Only show file edit buttons for global SuperUser + if ($isSuperUser) { + // Add an Apply and save button + if ($this->type === 'file') { + ToolbarHelper::apply('template.apply'); + ToolbarHelper::save('template.save'); + } elseif ($this->type === 'image') { + // Add a Crop and Resize button + ToolbarHelper::custom('template.cropImage', 'icon-crop', '', 'COM_TEMPLATES_BUTTON_CROP', false); + ToolbarHelper::modal('resizeModal', 'icon-expand', 'COM_TEMPLATES_BUTTON_RESIZE'); + } elseif ($this->type === 'archive') { + // Add an extract button + ToolbarHelper::custom('template.extractArchive', 'chevron-down', '', 'COM_TEMPLATES_BUTTON_EXTRACT_ARCHIVE', false); + } elseif ($this->type === 'home') { + // Add a copy/child template button + if (isset($this->template->xmldata->inheritable) && (string) $this->template->xmldata->inheritable === '1') { + ToolbarHelper::modal('childModal', 'icon-copy', 'COM_TEMPLATES_BUTTON_TEMPLATE_CHILD', false); + } elseif (!isset($this->template->xmldata->parent) || $this->template->xmldata->parent == '') { + ToolbarHelper::modal('copyModal', 'icon-copy', 'COM_TEMPLATES_BUTTON_COPY_TEMPLATE', false); + } + } + } + + // Add a Template preview button + if ($this->type === 'home') { + $client = (int) $this->preview->client_id === 1 ? 'administrator/' : ''; + $bar->linkButton('preview') + ->icon('icon-image') + ->text('COM_TEMPLATES_BUTTON_PREVIEW') + ->url(Uri::root() . $client . 'index.php?tp=1&templateStyle=' . $this->preview->id) + ->attributes(['target' => '_new']); + } + + // Only show file manage buttons for global SuperUser + if ($isSuperUser) { + if ($this->type === 'home') { + // Add Manage folders button + ToolbarHelper::modal('folderModal', 'icon-folder icon white', 'COM_TEMPLATES_BUTTON_FOLDERS'); + + // Add a new file button + ToolbarHelper::modal('fileModal', 'icon-file', 'COM_TEMPLATES_BUTTON_FILE'); + } else { + // Add a Rename file Button + ToolbarHelper::modal('renameModal', 'icon-sync', 'COM_TEMPLATES_BUTTON_RENAME_FILE'); + + // Add a Delete file Button + ToolbarHelper::modal('deleteModal', 'icon-times', 'COM_TEMPLATES_BUTTON_DELETE_FILE', 'btn-danger'); + } + } + + if (count($this->updatedList) !== 0 && $this->pluginState) { + ToolbarHelper::custom('template.deleteOverrideHistory', 'times', '', 'COM_TEMPLATES_BUTTON_DELETE_LIST_ENTRY', true, 'updateForm'); + } + + if ($this->type === 'home') { + ToolbarHelper::cancel('template.cancel', 'JTOOLBAR_CLOSE'); + } else { + ToolbarHelper::cancel('template.close', 'COM_TEMPLATES_BUTTON_CLOSE_FILE'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Templates:_Customise'); + } + + /** + * Method for creating the collapsible tree. + * + * @param array $array The value of the present node for recursion + * + * @return string + * + * @note Uses recursion + * @since 3.2 + */ + protected function directoryTree($array) + { + $temp = $this->files; + $this->files = $array; + $txt = $this->loadTemplate('tree'); + $this->files = $temp; + + return $txt; + } + + /** + * Method for listing the folder tree in modals. + * + * @param array $array The value of the present node for recursion + * + * @return string + * + * @note Uses recursion + * @since 3.2 + */ + protected function folderTree($array) + { + $temp = $this->files; + $this->files = $array; + $txt = $this->loadTemplate('folders'); + $this->files = $temp; + + return $txt; + } + + /** + * Method for creating the collapsible tree. + * + * @param array $array The value of the present node for recursion + * + * @return string + * + * @note Uses recursion + * @since 4.1.0 + */ + protected function mediaTree($array) + { + $temp = $this->mediaFiles; + $this->mediaFiles = $array; + $txt = $this->loadTemplate('tree_media'); + $this->mediaFiles = $temp; + + return $txt; + } + + /** + * Method for listing the folder tree in modals. + * + * @param array $array The value of the present node for recursion + * + * @return string + * + * @note Uses recursion + * @since 4.1.0 + */ + protected function mediaFolderTree($array) + { + $temp = $this->mediaFiles; + $this->mediaFiles = $array; + $txt = $this->loadTemplate('media_folders'); + $this->mediaFiles = $temp; + + return $txt; + } } diff --git a/administrator/components/com_templates/src/View/Templates/HtmlView.php b/administrator/components/com_templates/src/View/Templates/HtmlView.php index fbbed69c8023c..127f5bfe781f3 100644 --- a/administrator/components/com_templates/src/View/Templates/HtmlView.php +++ b/administrator/components/com_templates/src/View/Templates/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->total = $this->get('Total'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->preview = ComponentHelper::getParams('com_templates')->get('template_positions_display'); - $this->file = base64_encode('home'); - $this->pluginState = PluginHelper::isEnabled('installer', 'override'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_templates'); - $clientId = (int) $this->get('State')->get('client_id'); - - // Add a shortcut to the styles list view. - ToolbarHelper::link('index.php?option=com_templates&view=styles&client_id=' . $clientId, 'COM_TEMPLATES_MANAGER_STYLES_BUTTON', 'brush thememanager'); - - // Set the title. - if ($clientId === 1) - { - ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_TEMPLATES_ADMIN'), 'icon-code thememanager'); - } - else - { - ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_TEMPLATES_SITE'), 'icon-code thememanager'); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_templates'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Templates:_Templates'); - } + /** + * The list of templates + * + * @var array + * @since 1.6 + */ + protected $items; + + /** + * The pagination object + * + * @var object + * @since 1.6 + */ + protected $pagination; + + /** + * The model state + * + * @var object + * @since 1.6 + */ + protected $state; + + /** + * @var string + * @since 3.2 + */ + protected $file; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is the parameter enabled to show template positions in the frontend? + * + * @var boolean + * @since 4.0.0 + */ + public $preview; + + /** + * The state of installer override plugin. + * + * @var array + * + * @since 4.0.0 + */ + protected $pluginState; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->total = $this->get('Total'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->preview = ComponentHelper::getParams('com_templates')->get('template_positions_display'); + $this->file = base64_encode('home'); + $this->pluginState = PluginHelper::isEnabled('installer', 'override'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_templates'); + $clientId = (int) $this->get('State')->get('client_id'); + + // Add a shortcut to the styles list view. + ToolbarHelper::link('index.php?option=com_templates&view=styles&client_id=' . $clientId, 'COM_TEMPLATES_MANAGER_STYLES_BUTTON', 'brush thememanager'); + + // Set the title. + if ($clientId === 1) { + ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_TEMPLATES_ADMIN'), 'icon-code thememanager'); + } else { + ToolbarHelper::title(Text::_('COM_TEMPLATES_MANAGER_TEMPLATES_SITE'), 'icon-code thememanager'); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_templates'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Templates:_Templates'); + } } diff --git a/administrator/components/com_templates/tmpl/style/edit.php b/administrator/components/com_templates/tmpl/style/edit.php index f0aa6a48d4c67..92103a45ea775 100644 --- a/administrator/components/com_templates/tmpl/style/edit.php +++ b/administrator/components/com_templates/tmpl/style/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $this->useCoreUI = true; @@ -27,90 +28,90 @@
- - -
- 'details', 'recall' => true, 'breakpoint' => 768]); ?> - - - -
-
-

- item->template); ?> -

-
- - item->client_id == 0 ? Text::_('JSITE') : Text::_('JADMINISTRATOR'); ?> - -
-
-

item->xml->description); ?>

- fieldset = 'description'; - $description = LayoutHelper::render('joomla.edit.fieldset', $this); - ?> - -

- - - -

- -
- fieldset = 'basic'; - $html = LayoutHelper::render('joomla.edit.fieldset', $this); - echo $html ? '
' . $html : ''; - ?> -
-
- fields = array( - 'home', - 'client_id', - 'template' - ); - ?> - - form->renderField('inheritable'); ?> - form->renderField('parent'); ?> -
-
- - - - -
- -
- -
-
- - - - fieldsets = array(); - $this->ignore_fieldsets = array('basic', 'description'); - echo LayoutHelper::render('joomla.edit.params', $this); - ?> - - authorise('core.edit', 'com_menus') && $this->item->client_id == 0 && $this->canDo->get('core.edit.state')) : ?> - -
- -
- loadTemplate('assignment'); ?> -
-
- - - - - - - -
+ + +
+ 'details', 'recall' => true, 'breakpoint' => 768]); ?> + + + +
+
+

+ item->template); ?> +

+
+ + item->client_id == 0 ? Text::_('JSITE') : Text::_('JADMINISTRATOR'); ?> + +
+
+

item->xml->description); ?>

+ fieldset = 'description'; + $description = LayoutHelper::render('joomla.edit.fieldset', $this); + ?> + +

+ + + +

+ +
+ fieldset = 'basic'; + $html = LayoutHelper::render('joomla.edit.fieldset', $this); + echo $html ? '
' . $html : ''; + ?> +
+
+ fields = array( + 'home', + 'client_id', + 'template' + ); + ?> + + form->renderField('inheritable'); ?> + form->renderField('parent'); ?> +
+
+ + + + +
+ +
+ +
+
+ + + + fieldsets = array(); + $this->ignore_fieldsets = array('basic', 'description'); + echo LayoutHelper::render('joomla.edit.params', $this); + ?> + + authorise('core.edit', 'com_menus') && $this->item->client_id == 0 && $this->canDo->get('core.edit.state')) : ?> + +
+ +
+ loadTemplate('assignment'); ?> +
+
+ + + + + + + +
diff --git a/administrator/components/com_templates/tmpl/style/edit_assignment.php b/administrator/components/com_templates/tmpl/style/edit_assignment.php index b0a03841f953e..bd3cae0f46d2d 100644 --- a/administrator/components/com_templates/tmpl/style/edit_assignment.php +++ b/administrator/components/com_templates/tmpl/style/edit_assignment.php @@ -1,4 +1,5 @@
- +
diff --git a/administrator/components/com_templates/tmpl/styles/default.php b/administrator/components/com_templates/tmpl/styles/default.php index a610932134949..371221698e528 100644 --- a/administrator/components/com_templates/tmpl/styles/default.php +++ b/administrator/components/com_templates/tmpl/styles/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $clientId = (int) $this->state->get('client_id', 0); @@ -27,127 +28,127 @@ $listDirn = $this->escape($this->state->get('list.direction')); ?>
-
-
-
- $this, 'options' => array('selectorFieldName' => 'client_id'))); ?> - total > 0) : ?> - - - - - - - - - - - - - - - - - items as $i => $item) : - $canCreate = $user->authorise('core.create', 'com_templates'); - $canEdit = $user->authorise('core.edit', 'com_templates'); - $canChange = $user->authorise('core.edit.state', 'com_templates'); - ?> - - - - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - - - -
- id, false, 'cid', 'cb', $item->title); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - - - preview) : ?> - client_id === 1 ? 'administrator' : 'site'; ?> - - - - - - - - - home == '0' || $item->home == '1') : ?> - home != '0', $i, 'styles.', $canChange && $item->home != '1'); ?> - - - image) : ?> - image . '.gif', $item->language_title, array('title' => Text::sprintf('COM_TEMPLATES_GRID_UNSET_LANGUAGE', $item->language_title)), true); ?> - - home; ?> - - - - image) : ?> - image . '.gif', $item->language_title, array('title' => $item->language_title), true); ?> - - home; ?> - - - - home == '1') : ?> - - home != '0' && $item->home != '1') : ?> - escape($item->language_title)); ?> - assigned > 0) : ?> - escape($item->assigned)); ?> - - - - - - escape($item->template)); ?> - - - id; ?> -
+
+
+
+ $this, 'options' => array('selectorFieldName' => 'client_id'))); ?> + total > 0) : ?> + + + + + + + + + + + + + + + + + items as $i => $item) : + $canCreate = $user->authorise('core.create', 'com_templates'); + $canEdit = $user->authorise('core.edit', 'com_templates'); + $canChange = $user->authorise('core.edit.state', 'com_templates'); + ?> + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + + + +
+ id, false, 'cid', 'cb', $item->title); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + + + preview) : ?> + client_id === 1 ? 'administrator' : 'site'; ?> + + + + + + + + + home == '0' || $item->home == '1') : ?> + home != '0', $i, 'styles.', $canChange && $item->home != '1'); ?> + + + image) : ?> + image . '.gif', $item->language_title, array('title' => Text::sprintf('COM_TEMPLATES_GRID_UNSET_LANGUAGE', $item->language_title)), true); ?> + + home; ?> + + + + image) : ?> + image . '.gif', $item->language_title, array('title' => $item->language_title), true); ?> + + home; ?> + + + + home == '1') : ?> + + home != '0' && $item->home != '1') : ?> + escape($item->language_title)); ?> + assigned > 0) : ?> + escape($item->assigned)); ?> + + + + + + escape($item->template)); ?> + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
-
-
+ + + +
+
+
diff --git a/administrator/components/com_templates/tmpl/template/default.php b/administrator/components/com_templates/tmpl/template/default.php index 4320ad44335b3..7985896073716 100644 --- a/administrator/components/com_templates/tmpl/template/default.php +++ b/administrator/components/com_templates/tmpl/template/default.php @@ -1,4 +1,5 @@ useScript('form.validate') - ->useScript('keepalive') - ->useScript('diff') - ->useScript('com_templates.admin-template-compare') - ->useScript('com_templates.admin-template-toggle-switch'); + ->useScript('keepalive') + ->useScript('diff') + ->useScript('com_templates.admin-template-compare') + ->useScript('com_templates.admin-template-toggle-switch'); // No access if not global SuperUser -if (!Factory::getUser()->authorise('core.admin')) -{ - Factory::getApplication()->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'danger'); +if (!Factory::getUser()->authorise('core.admin')) { + Factory::getApplication()->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'danger'); } -if ($this->type == 'image') -{ - $wa->usePreset('cropperjs'); +if ($this->type == 'image') { + $wa->usePreset('cropperjs'); } $wa->useStyle('com_templates.admin-templates') - ->useScript('com_templates.admin-templates'); + ->useScript('com_templates.admin-templates'); -if ($this->type == 'font') -{ - $wa->addInlineStyle(" +if ($this->type == 'font') { + $wa->addInlineStyle(" @font-face { font-family: previewFont; src: url('" . $this->font['address'] . "') @@ -59,411 +57,411 @@ ?>
- 'editor', 'recall' => true, 'breakpoint' => 768]); ?> - -
-
- type == 'file') : ?> -

get('isMedia', 0) ? '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element . str_replace('//', '/', base64_decode($this->file)) : '/' . ($this->template->client_id === 0 ? '' : 'administrator/') . 'templates/' . $this->template->element . str_replace('//', '/', base64_decode($this->file))), $this->template->element); ?>

- - - type == 'image') : ?> -

image['path'], $this->template->element); ?>

- - - type == 'font') : ?> -

font['rel_path'], $this->template->element); ?>

- - -
- type == 'file' && !empty($this->source->coreFile)) : ?> -
-
- form->renderField('show_core'); ?> - form->renderField('show_diff'); ?> -
-
- -
-
- -
-
- type == 'home') : ?> - -
- - -

-

- - - -

-
- type == 'file') : ?> -
-
- source->filename); ?> - source->coreFile)) : ?> -

- -
- -
- form->getInput('source'); ?> -
- - - form->getInput('extension_id'); ?> - form->getInput('filename'); ?> -
-
- source->coreFile)) : ?> - source->coreFile); ?> - source->filePath); ?> -
-

-
- form->getInput('core'); ?> -
-
-
-

-
-
-
-
-
-
- -
- type == 'archive') : ?> - -
- - - -
- type == 'image') : ?> - escape(basename($this->image['address'])); ?> - -
-
- - - - - - - - -
-
- type == 'font') : ?> -
-
-
-

H1. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

-

H2. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

-

H3. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

-

H4. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

-
H5. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML
-
H6. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML
-

Bold. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

-

Italics. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

-

Unordered List

-
    -
  • Item
  • -
  • Item
  • -
  • Item
    -
      -
    • Item
    • -
    • Item
    • -
    • Item
      -
        -
      • Item
      • -
      • Item
      • -
      • Item
      • -
      -
    • -
    -
  • -
-

Ordered List

-
    -
  1. Item
  2. -
  3. Item
  4. -
  5. Item
    -
      -
    • Item
    • -
    • Item
    • -
    • Item
      -
        -
      • Item
      • -
      • Item
      • -
      • Item
      • -
      -
    • -
    -
  6. -
- - -
-
-
- -
-
-
- - -
-
-
- -
    - - overridesList['modules'] as $module) : ?> -
  • - path - . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&isMedia=' . $input->get('isMedia', 0) . '&' . $token; - ?> - -  name; ?> - -
  • - -
-
-
-
-
- -
    - - overridesList['components'] as $key => $value) : ?> -
  • - -   - -
      - -
    • - path - . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&isMedia=' . $input->get('isMedia', 0) . '&' . $token; - ?> - -  name; ?> - -
    • - -
    -
  • - -
-
-
-
-
- -
    - - overridesList['plugins'] as $key => $group) : ?> -
  • - -   - -
      - -
    • - path - . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&isMedia=' . $input->get('isMedia', 0) . '&' . $token; - ?> - - name; ?> - -
    • - -
    -
  • - -
-
-
-
-
- -
    - - overridesList['layouts'] as $key => $value) : ?> -
  • - -   - -
      - -
    • - path - . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&' . $token . '&isMedia=' . $input->get('isMedia', 0); - ?> - -  name; ?> - -
    • - -
    -
  • - -
-
-
-
- + 'editor', 'recall' => true, 'breakpoint' => 768]); ?> + +
+
+ type == 'file') : ?> +

get('isMedia', 0) ? '/media/templates/' . ($this->template->client_id === 0 ? 'site' : 'administrator') . '/' . $this->template->element . str_replace('//', '/', base64_decode($this->file)) : '/' . ($this->template->client_id === 0 ? '' : 'administrator/') . 'templates/' . $this->template->element . str_replace('//', '/', base64_decode($this->file))), $this->template->element); ?>

+ + + type == 'image') : ?> +

image['path'], $this->template->element); ?>

+ + + type == 'font') : ?> +

font['rel_path'], $this->template->element); ?>

+ + +
+ type == 'file' && !empty($this->source->coreFile)) : ?> +
+
+ form->renderField('show_core'); ?> + form->renderField('show_diff'); ?> +
+
+ +
+
+ +
+
+ type == 'home') : ?> + +
+ + +

+

+ + + +

+
+ type == 'file') : ?> +
+
+ source->filename); ?> + source->coreFile)) : ?> +

+ +
+ +
+ form->getInput('source'); ?> +
+ + + form->getInput('extension_id'); ?> + form->getInput('filename'); ?> +
+
+ source->coreFile)) : ?> + source->coreFile); ?> + source->filePath); ?> +
+

+
+ form->getInput('core'); ?> +
+
+
+

+
+
+
+
+
+
+ +
+ type == 'archive') : ?> + +
+ + + +
+ type == 'image') : ?> + escape(basename($this->image['address'])); ?> + +
+
+ + + + + + + + +
+
+ type == 'font') : ?> +
+
+
+

H1. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

+

H2. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

+

H3. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

+

H4. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

+
H5. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML
+
H6. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML
+

Bold. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

+

Italics. Quickly gaze at Joomla! views from HTML, CSS, JavaScript and XML

+

Unordered List

+
    +
  • Item
  • +
  • Item
  • +
  • Item
    +
      +
    • Item
    • +
    • Item
    • +
    • Item
      +
        +
      • Item
      • +
      • Item
      • +
      • Item
      • +
      +
    • +
    +
  • +
+

Ordered List

+
    +
  1. Item
  2. +
  3. Item
  4. +
  5. Item
    +
      +
    • Item
    • +
    • Item
    • +
    • Item
      +
        +
      • Item
      • +
      • Item
      • +
      • Item
      • +
      +
    • +
    +
  6. +
+ + +
+
+
+ +
+
+
+ + +
+
+
+ +
    + + overridesList['modules'] as $module) : ?> +
  • + path + . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&isMedia=' . $input->get('isMedia', 0) . '&' . $token; + ?> + +  name; ?> + +
  • + +
+
+
+
+
+ +
    + + overridesList['components'] as $key => $value) : ?> +
  • + +   + +
      + +
    • + path + . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&isMedia=' . $input->get('isMedia', 0) . '&' . $token; + ?> + +  name; ?> + +
    • + +
    +
  • + +
+
+
+
+
+ +
    + + overridesList['plugins'] as $key => $group) : ?> +
  • + +   + +
      + +
    • + path + . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&isMedia=' . $input->get('isMedia', 0) . '&' . $token; + ?> + + name; ?> + +
    • + +
    +
  • + +
+
+
+
+
+ +
    + + overridesList['layouts'] as $key => $value) : ?> +
  • + +   + +
      + +
    • + path + . '&id=' . $input->getInt('id') . '&file=' . $this->file . '&' . $token . '&isMedia=' . $input->get('isMedia', 0); + ?> + +  name; ?> + +
    • + +
    +
  • + +
+
+
+
+ - pluginState) : ?> - - loadTemplate('updated_files'); ?> - - + pluginState) : ?> + + loadTemplate('updated_files'); ?> + + - -
-
- loadTemplate('description'); ?> -
-
- + +
+
+ loadTemplate('description'); ?> +
+
+ - + - template->xmldata->inheritable) && (string) $this->template->xmldata->inheritable === '1' ? 'child' : 'copy'; - $copyModalData = array( - 'selector' => $taskName . 'Modal', - 'params' => array( - 'title' => Text::_('COM_TEMPLATES_TEMPLATE_' . strtoupper($taskName)), - 'footer' => $this->loadTemplate('modal_' . $taskName . '_footer') - ), - 'body' => $this->loadTemplate('modal_' . $taskName . '_body') - ); - ?> -
- - -
- type != 'home') : ?> - 'renameModal', - 'params' => array( - 'title' => Text::sprintf('COM_TEMPLATES_RENAME_FILE', str_replace('//', '/', $this->fileName)), - 'footer' => $this->loadTemplate('modal_rename_footer') - ), - 'body' => $this->loadTemplate('modal_rename_body') - ); - ?> -
- - -
- - type != 'home') : ?> - 'deleteModal', - 'params' => array( - 'title' => Text::_('COM_TEMPLATES_ARE_YOU_SURE'), - 'footer' => $this->loadTemplate('modal_delete_footer') - ), - 'body' => $this->loadTemplate('modal_delete_body') - ); - ?> - - - 'fileModal', - 'params' => array( - 'title' => Text::_('COM_TEMPLATES_NEW_FILE_HEADER'), - 'footer' => $this->loadTemplate('modal_file_footer'), - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - ), - 'body' => $this->loadTemplate('modal_file_body') - ); - ?> - - 'folderModal', - 'params' => array( - 'title' => Text::_('COM_TEMPLATES_MANAGE_FOLDERS'), - 'footer' => $this->loadTemplate('modal_folder_footer'), - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - ), - 'body' => $this->loadTemplate('modal_folder_body') - ); - ?> - - type == 'image') : ?> - 'resizeModal', - 'params' => array( - 'title' => Text::_('COM_TEMPLATES_RESIZE_IMAGE'), - 'footer' => $this->loadTemplate('modal_resize_footer') - ), - 'body' => $this->loadTemplate('modal_resize_body') - ); - ?> -
- - -
- + template->xmldata->inheritable) && (string) $this->template->xmldata->inheritable === '1' ? 'child' : 'copy'; + $copyModalData = array( + 'selector' => $taskName . 'Modal', + 'params' => array( + 'title' => Text::_('COM_TEMPLATES_TEMPLATE_' . strtoupper($taskName)), + 'footer' => $this->loadTemplate('modal_' . $taskName . '_footer') + ), + 'body' => $this->loadTemplate('modal_' . $taskName . '_body') + ); + ?> +
+ + +
+ type != 'home') : ?> + 'renameModal', + 'params' => array( + 'title' => Text::sprintf('COM_TEMPLATES_RENAME_FILE', str_replace('//', '/', $this->fileName)), + 'footer' => $this->loadTemplate('modal_rename_footer') + ), + 'body' => $this->loadTemplate('modal_rename_body') + ); + ?> +
+ + +
+ + type != 'home') : ?> + 'deleteModal', + 'params' => array( + 'title' => Text::_('COM_TEMPLATES_ARE_YOU_SURE'), + 'footer' => $this->loadTemplate('modal_delete_footer') + ), + 'body' => $this->loadTemplate('modal_delete_body') + ); + ?> + + + 'fileModal', + 'params' => array( + 'title' => Text::_('COM_TEMPLATES_NEW_FILE_HEADER'), + 'footer' => $this->loadTemplate('modal_file_footer'), + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + ), + 'body' => $this->loadTemplate('modal_file_body') + ); + ?> + + 'folderModal', + 'params' => array( + 'title' => Text::_('COM_TEMPLATES_MANAGE_FOLDERS'), + 'footer' => $this->loadTemplate('modal_folder_footer'), + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + ), + 'body' => $this->loadTemplate('modal_folder_body') + ); + ?> + + type == 'image') : ?> + 'resizeModal', + 'params' => array( + 'title' => Text::_('COM_TEMPLATES_RESIZE_IMAGE'), + 'footer' => $this->loadTemplate('modal_resize_footer') + ), + 'body' => $this->loadTemplate('modal_resize_body') + ); + ?> +
+ + +
+
diff --git a/administrator/components/com_templates/tmpl/template/default_description.php b/administrator/components/com_templates/tmpl/template/default_description.php index 24119159bcbb6..72afa2cbde22c 100644 --- a/administrator/components/com_templates/tmpl/template/default_description.php +++ b/administrator/components/com_templates/tmpl/template/default_description.php @@ -1,4 +1,5 @@
-
- template); ?> - template); ?> -
-

template->element); ?>

- template->client_id); ?> -

template->xmldata = TemplatesHelper::parseXMLTemplateFile($client->path, $this->template->element); ?>

-

template->xmldata->get('description')); ?>

+
+ template); ?> + template); ?> +
+

template->element); ?>

+ template->client_id); ?> +

template->xmldata = TemplatesHelper::parseXMLTemplateFile($client->path, $this->template->element); ?>

+

template->xmldata->get('description')); ?>

diff --git a/administrator/components/com_templates/tmpl/template/default_folders.php b/administrator/components/com_templates/tmpl/template/default_folders.php index 400efb3cb1f6f..1675dab6e7b1f 100644 --- a/administrator/components/com_templates/tmpl/template/default_folders.php +++ b/administrator/components/com_templates/tmpl/template/default_folders.php @@ -1,4 +1,5 @@ diff --git a/administrator/components/com_templates/tmpl/template/default_media_folders.php b/administrator/components/com_templates/tmpl/template/default_media_folders.php index 38ea9c2d0fafa..f3491ab3a8fa8 100644 --- a/administrator/components/com_templates/tmpl/template/default_media_folders.php +++ b/administrator/components/com_templates/tmpl/template/default_media_folders.php @@ -1,4 +1,5 @@ mediaFiles)) -{ - return; +if (!count($this->mediaFiles)) { + return; } ksort($this->mediaFiles, SORT_STRING); ?> diff --git a/administrator/components/com_templates/tmpl/template/default_modal_child_body.php b/administrator/components/com_templates/tmpl/template/default_modal_child_body.php index 3b9b83e6414a4..507b0767372af 100644 --- a/administrator/components/com_templates/tmpl/template/default_modal_child_body.php +++ b/administrator/components/com_templates/tmpl/template/default_modal_child_body.php @@ -1,4 +1,5 @@ styles) > 0) -{ - foreach ($this->styles as $style) - { - $options[] = HTMLHelper::_('select.option', $style->id, $style->title, 'value', 'text'); - } +if (count($this->styles) > 0) { + foreach ($this->styles as $style) { + $options[] = HTMLHelper::_('select.option', $style->id, $style->title, 'value', 'text'); + } } $fancySelectData = [ - 'autocomplete' => 'off', - 'autofocus' => false, - 'class' => '', - 'description' => '', - 'disabled' => false, - 'group' => false, - 'id' => 'style_ids', - 'hidden' => false, - 'hint' => '', - 'label' => '', - 'labelclass' => '', - 'onchange' => '', - 'onclick' => '', - 'multiple' => true, - 'pattern' => '', - 'readonly' => false, - 'repeat' => false, - 'required' => false, - 'size' => 4, - 'spellcheck' => false, - 'validate' => '', - 'value' => '0', - 'options' => $options, - 'dataAttributes' => [], - 'dataAttribute' => '', - 'name' => 'style_ids[]', + 'autocomplete' => 'off', + 'autofocus' => false, + 'class' => '', + 'description' => '', + 'disabled' => false, + 'group' => false, + 'id' => 'style_ids', + 'hidden' => false, + 'hint' => '', + 'label' => '', + 'labelclass' => '', + 'onchange' => '', + 'onclick' => '', + 'multiple' => true, + 'pattern' => '', + 'readonly' => false, + 'repeat' => false, + 'required' => false, + 'size' => 4, + 'spellcheck' => false, + 'validate' => '', + 'value' => '0', + 'options' => $options, + 'dataAttributes' => [], + 'dataAttribute' => '', + 'name' => 'style_ids[]', ]; ?>
-
-
-
-
- -
-
- - - - -
-
-
-
- -
-
- - - - -
-
-
-
+
+
+
+
+ +
+
+ + + + +
+
+
+
+ +
+
+ + + + +
+
+
+
diff --git a/administrator/components/com_templates/tmpl/template/default_modal_child_footer.php b/administrator/components/com_templates/tmpl/template/default_modal_child_footer.php index 8f0188e2952ec..f1c4dfb6d72f3 100644 --- a/administrator/components/com_templates/tmpl/template/default_modal_child_footer.php +++ b/administrator/components/com_templates/tmpl/template/default_modal_child_footer.php @@ -1,4 +1,5 @@
-
-
-
-
- -
-
- - - - -
-
-
-
+
+
+
+
+ +
+
+ + + + +
+
+
+
diff --git a/administrator/components/com_templates/tmpl/template/default_modal_copy_footer.php b/administrator/components/com_templates/tmpl/template/default_modal_copy_footer.php index 9eeeff74b40a7..17d0f3f17a4de 100644 --- a/administrator/components/com_templates/tmpl/template/default_modal_copy_footer.php +++ b/administrator/components/com_templates/tmpl/template/default_modal_copy_footer.php @@ -1,4 +1,5 @@
-
-
-

fileName)); ?>

-
-
+
+
+

fileName)); ?>

+
+
diff --git a/administrator/components/com_templates/tmpl/template/default_modal_delete_footer.php b/administrator/components/com_templates/tmpl/template/default_modal_delete_footer.php index 89a1b12fcd307..9895aadd69b81 100644 --- a/administrator/components/com_templates/tmpl/template/default_modal_delete_footer.php +++ b/administrator/components/com_templates/tmpl/template/default_modal_delete_footer.php @@ -1,4 +1,5 @@ input; ?>
- - - - - - - + + + + + + +
diff --git a/administrator/components/com_templates/tmpl/template/default_modal_file_body.php b/administrator/components/com_templates/tmpl/template/default_modal_file_body.php index d3dbea84b4f32..926cd1fec345d 100644 --- a/administrator/components/com_templates/tmpl/template/default_modal_file_body.php +++ b/administrator/components/com_templates/tmpl/template/default_modal_file_body.php @@ -19,87 +19,87 @@ $input = Factory::getApplication()->input; ?>
-
-
- -
-
-
- - -
-
- - -
- - - - -
-
-
- - -
- - - -
- state->get('params')->get('upload_limit'); ?> - - -
- type != 'home') : ?> -
-
-
- - - - - -
- -
- -
-
-
+
+
+ +
+
+
+ + +
+
+ + +
+ + + + +
+
+
+ + +
+ + + +
+ state->get('params')->get('upload_limit'); ?> + + +
+ type != 'home') : ?> +
+
+
+ + + + + +
+ +
+ +
+
+
diff --git a/administrator/components/com_templates/tmpl/template/default_modal_file_footer.php b/administrator/components/com_templates/tmpl/template/default_modal_file_footer.php index 25a77c25295b8..2a58ea613436e 100644 --- a/administrator/components/com_templates/tmpl/template/default_modal_file_footer.php +++ b/administrator/components/com_templates/tmpl/template/default_modal_file_footer.php @@ -1,4 +1,5 @@ input; ?>
-
-
- -
-
-
- - - - - -
- -
-
-
-
+
+
+ +
+
+
+ + + + + +
+ +
+
+
+
diff --git a/administrator/components/com_templates/tmpl/template/default_modal_folder_footer.php b/administrator/components/com_templates/tmpl/template/default_modal_folder_footer.php index 3f0c071ceb171..1fbf670f9b7b3 100644 --- a/administrator/components/com_templates/tmpl/template/default_modal_folder_footer.php +++ b/administrator/components/com_templates/tmpl/template/default_modal_folder_footer.php @@ -1,4 +1,5 @@ input; ?>
-
- - - - - -
+
+ + + + + +
diff --git a/administrator/components/com_templates/tmpl/template/default_modal_rename_body.php b/administrator/components/com_templates/tmpl/template/default_modal_rename_body.php index d954ebb64d839..531e6d7f49c57 100644 --- a/administrator/components/com_templates/tmpl/template/default_modal_rename_body.php +++ b/administrator/components/com_templates/tmpl/template/default_modal_rename_body.php @@ -1,4 +1,5 @@
-
-
-
-
- -
-
-
- - .fileName); ?> -
-
-
-
-
+
+
+
+
+ +
+
+
+ + .fileName); ?> +
+
+
+
+
diff --git a/administrator/components/com_templates/tmpl/template/default_modal_rename_footer.php b/administrator/components/com_templates/tmpl/template/default_modal_rename_footer.php index fd6c82fb5222e..d0bd1b966997b 100644 --- a/administrator/components/com_templates/tmpl/template/default_modal_rename_footer.php +++ b/administrator/components/com_templates/tmpl/template/default_modal_rename_footer.php @@ -1,4 +1,5 @@
-
-
-
-
- -
-
- -
-
-
-
- -
-
- -
-
-
-
+
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
diff --git a/administrator/components/com_templates/tmpl/template/default_modal_resize_footer.php b/administrator/components/com_templates/tmpl/template/default_modal_resize_footer.php index 204efeeaca772..9b4601b2ea1f9 100644 --- a/administrator/components/com_templates/tmpl/template/default_modal_resize_footer.php +++ b/administrator/components/com_templates/tmpl/template/default_modal_resize_footer.php @@ -1,4 +1,5 @@ diff --git a/administrator/components/com_templates/tmpl/template/default_tree_media.php b/administrator/components/com_templates/tmpl/template/default_tree_media.php index c32114297f9e1..32c014bda09fd 100644 --- a/administrator/components/com_templates/tmpl/template/default_tree_media.php +++ b/administrator/components/com_templates/tmpl/template/default_tree_media.php @@ -1,4 +1,5 @@ mediaFiles)) -{ - return; +if (!count($this->mediaFiles)) { + return; } ksort($this->mediaFiles, SORT_STRING); ?>
    - mediaFiles as $key => $value) : ?> - - fileName); - $count = 0; + mediaFiles as $key => $value) : ?> + + fileName); + $count = 0; - $keyArrayCount = count($keyArray); + $keyArrayCount = count($keyArray); - if (count($fileArray) >= $keyArrayCount) - { - for ($i = 0; $i < $keyArrayCount; $i++) - { - if ($keyArray[$i] === $fileArray[$i]) - { - $count++; - } - } + if (count($fileArray) >= $keyArrayCount) { + for ($i = 0; $i < $keyArrayCount; $i++) { + if ($keyArray[$i] === $fileArray[$i]) { + $count++; + } + } - if ($count === $keyArrayCount) - { - $class = 'folder show'; - } - else - { - $class = 'folder'; - } - } - else - { - $class = 'folder'; - } + if ($count === $keyArrayCount) { + $class = 'folder show'; + } else { + $class = 'folder'; + } + } else { + $class = 'folder'; + } - ?> -
  • - -  escape(end($explodeArray)); ?> - - mediaTree($value); ?> -
  • - - -
  • - -  escape($value->name); ?> - -
  • - - + ?> +
  • + +  escape(end($explodeArray)); ?> + + mediaTree($value); ?> +
  • + + +
  • + +  escape($value->name); ?> + +
  • + +
diff --git a/administrator/components/com_templates/tmpl/template/default_updated_files.php b/administrator/components/com_templates/tmpl/template/default_updated_files.php index 385122cbd610f..699340f344efa 100644 --- a/administrator/components/com_templates/tmpl/template/default_updated_files.php +++ b/administrator/components/com_templates/tmpl/template/default_updated_files.php @@ -1,4 +1,5 @@
-
-
- updatedList) !== 0) : ?> - - - - - - - - - - - - - updatedList as $i => $value) : ?> - - - - - - - - - - -
- - - - - - - - - - - -
- hash_id, false, 'cid', 'cb', '', 'updateForm'); ?> - - state, $i, 'template.', 1, 'cb', null, null, 'updateForm'); ?> - - hash_id); ?> - - created_date; ?> - 0 ? HTMLHelper::_('date', $created_date, Text::_('DATE_FORMAT_FILTER_DATETIME')) : '-'; ?> - - modified_date)) : ?> - - - modified_date, Text::_('DATE_FORMAT_FILTER_DATETIME')); ?> - - - action; ?> -
- - - - -
- - -
- -
-
+
+
+ updatedList) !== 0) : ?> + + + + + + + + + + + + + updatedList as $i => $value) : ?> + + + + + + + + + + +
+ + + + + + + + + + + +
+ hash_id, false, 'cid', 'cb', '', 'updateForm'); ?> + + state, $i, 'template.', 1, 'cb', null, null, 'updateForm'); ?> + + hash_id); ?> + + created_date; ?> + 0 ? HTMLHelper::_('date', $created_date, Text::_('DATE_FORMAT_FILTER_DATETIME')) : '-'; ?> + + modified_date)) : ?> + + + modified_date, Text::_('DATE_FORMAT_FILTER_DATETIME')); ?> + + + action; ?> +
+ + + + +
+ + +
+ +
+
diff --git a/administrator/components/com_templates/tmpl/template/readonly.php b/administrator/components/com_templates/tmpl/template/readonly.php index 157cff97362c5..0d9f62bcce178 100644 --- a/administrator/components/com_templates/tmpl/template/readonly.php +++ b/administrator/components/com_templates/tmpl/template/readonly.php @@ -1,4 +1,5 @@ input; ?>
- 'description', 'recall' => true, 'breakpoint' => 768]); ?> - -
-
- loadTemplate('description'); ?> -
-
- - - - + 'description', 'recall' => true, 'breakpoint' => 768]); ?> + +
+
+ loadTemplate('description'); ?> +
+
+ + + +
diff --git a/administrator/components/com_templates/tmpl/templates/default.php b/administrator/components/com_templates/tmpl/templates/default.php index 42959af23cca7..231f899bccd57 100644 --- a/administrator/components/com_templates/tmpl/templates/default.php +++ b/administrator/components/com_templates/tmpl/templates/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); @@ -26,117 +27,117 @@ ?>
-
-
-
- $this, 'options' => array('selectorFieldName' => 'client_id'))); ?> - total > 0) : ?> - - - - - - - - - - pluginState) : ?> - - - - - - items as $i => $item) : ?> - - - - - - - pluginState) : ?> - - - - - -
- , - , - -
- - - - - - - - - - - -
- - - - - name)); ?> -
- preview) : ?> - client_id === 1 ? 'administrator' : 'site'; ?> - - - - - - - -
- xmldata->inheritable) && $item->xmldata->inheritable) : ?> -
- - -
- - xmldata->parent) && (string) $item->xmldata->parent !== '') : ?> -
- - xmldata->parent); ?> -
- -
- escape($item->xmldata->get('version')); ?> - - escape($item->xmldata->get('creationDate')); ?> - - xmldata->get('author')) : ?> -
escape($author); ?>
- - — - - xmldata->get('authorEmail')) : ?> -
escape($email); ?>
- - xmldata->get('authorUrl')) : ?> - - -
- updated)) : ?> - updated); ?> - - - -
+
+
+
+ $this, 'options' => array('selectorFieldName' => 'client_id'))); ?> + total > 0) : ?> + + + + + + + + + + pluginState) : ?> + + + + + + items as $i => $item) : ?> + + + + + + + pluginState) : ?> + + + + + +
+ , + , + +
+ + + + + + + + + + + +
+ + + + + name)); ?> +
+ preview) : ?> + client_id === 1 ? 'administrator' : 'site'; ?> + + + + + + + +
+ xmldata->inheritable) && $item->xmldata->inheritable) : ?> +
+ + +
+ + xmldata->parent) && (string) $item->xmldata->parent !== '') : ?> +
+ + xmldata->parent); ?> +
+ +
+ escape($item->xmldata->get('version')); ?> + + escape($item->xmldata->get('creationDate')); ?> + + xmldata->get('author')) : ?> +
escape($author); ?>
+ + — + + xmldata->get('authorEmail')) : ?> +
escape($email); ?>
+ + xmldata->get('authorUrl')) : ?> + + +
+ updated)) : ?> + updated); ?> + + + +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
-
-
+ + + +
+
+
diff --git a/administrator/components/com_users/helpers/debug.php b/administrator/components/com_users/helpers/debug.php index d1dc18a415293..f5f81670928f8 100644 --- a/administrator/components/com_users/helpers/debug.php +++ b/administrator/components/com_users/helpers/debug.php @@ -1,13 +1,14 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt - */ -defined('_JEXEC') or die; + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + */ use Joomla\Component\Users\Administrator\Helper\DebugHelper; diff --git a/administrator/components/com_users/helpers/users.php b/administrator/components/com_users/helpers/users.php index f0d556c782f20..c56a1fef95d07 100644 --- a/administrator/components/com_users/helpers/users.php +++ b/administrator/components/com_users/helpers/users.php @@ -1,13 +1,16 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; + /** * Users component helper. diff --git a/administrator/components/com_users/postinstall/multifactorauth.php b/administrator/components/com_users/postinstall/multifactorauth.php index d0989284c8ab8..26bc66f3aaa1b 100644 --- a/administrator/components/com_users/postinstall/multifactorauth.php +++ b/administrator/components/com_users/postinstall/multifactorauth.php @@ -1,4 +1,5 @@ get('DatabaseDriver'); - $coreMfaPlugins = ['email', 'totp', 'webauthn', 'yubikey']; + /** @var DatabaseDriver $db */ + $db = Factory::getContainer()->get('DatabaseDriver'); + $coreMfaPlugins = ['email', 'totp', 'webauthn', 'yubikey']; - $query = $db->getQuery(true) - ->update($db->quoteName('#__extensions')) - ->set($db->quoteName('enabled') . ' = 1') - ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) - ->where($db->quoteName('folder') . ' = ' . $db->quote('multifactorauth')) - ->whereIn($db->quoteName('element'), $coreMfaPlugins, ParameterType::STRING); - $db->setQuery($query); - $db->execute(); + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('multifactorauth')) + ->whereIn($db->quoteName('element'), $coreMfaPlugins, ParameterType::STRING); + $db->setQuery($query); + $db->execute(); - $url = 'index.php?option=com_plugins&filter[folder]=multifactorauth'; - Factory::getApplication()->redirect($url); + $url = 'index.php?option=com_plugins&filter[folder]=multifactorauth'; + Factory::getApplication()->redirect($url); } diff --git a/administrator/components/com_users/services/provider.php b/administrator/components/com_users/services/provider.php index b2efe40383c44..d444883bf62c0 100644 --- a/administrator/components/com_users/services/provider.php +++ b/administrator/components/com_users/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Users')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Users')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Users')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Users')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Users')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Users')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new UsersComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - $component->setRegistry($container->get(Registry::class)); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new UsersComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + $component->setRegistry($container->get(Registry::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_users/src/Controller/CallbackController.php b/administrator/components/com_users/src/Controller/CallbackController.php index debe38bdd938a..63ee61ad2162a 100644 --- a/administrator/components/com_users/src/Controller/CallbackController.php +++ b/administrator/components/com_users/src/Controller/CallbackController.php @@ -1,4 +1,5 @@ registerDefaultTask('callback'); - } + $this->registerDefaultTask('callback'); + } - /** - * Implement a callback feature, typically used for OAuth2 authentication - * - * @param bool $cachable Can this view be cached - * @param array|bool $urlparams An array of safe url parameters and their variable types, for valid values see - * {@link JFilterInput::clean()}. - * - * @return void - * @since 4.2.0 - */ - public function callback($cachable = false, $urlparams = false): void - { - $app = $this->app; + /** + * Implement a callback feature, typically used for OAuth2 authentication + * + * @param bool $cachable Can this view be cached + * @param array|bool $urlparams An array of safe url parameters and their variable types, for valid values see + * {@link JFilterInput::clean()}. + * + * @return void + * @since 4.2.0 + */ + public function callback($cachable = false, $urlparams = false): void + { + $app = $this->app; - // Get the Method and make sure it's non-empty - $method = $this->input->getCmd('method', ''); + // Get the Method and make sure it's non-empty + $method = $this->input->getCmd('method', ''); - if (empty($method)) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + if (empty($method)) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } - PluginHelper::importPlugin('multifactorauth'); + PluginHelper::importPlugin('multifactorauth'); - $event = new Callback($method); - $this->app->getDispatcher()->dispatch($event->getName(), $event); + $event = new Callback($method); + $this->app->getDispatcher()->dispatch($event->getName(), $event); - /** - * The first plugin to handle the request should either redirect or close the application. If we are still here - * no plugin handled the request successfully. Show an error. - */ - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + /** + * The first plugin to handle the request should either redirect or close the application. If we are still here + * no plugin handled the request successfully. Show an error. + */ + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } } diff --git a/administrator/components/com_users/src/Controller/CaptiveController.php b/administrator/components/com_users/src/Controller/CaptiveController.php index 384840b98a0f0..a4d9e8ab798e9 100644 --- a/administrator/components/com_users/src/Controller/CaptiveController.php +++ b/administrator/components/com_users/src/Controller/CaptiveController.php @@ -1,4 +1,5 @@ registerTask('captive', 'display'); - } - - /** - * Displays the captive login page - * - * @param boolean $cachable Ignored. This page is never cached. - * @param boolean|array $urlparams Ignored. This page is never cached. - * - * @return void - * @throws Exception - * @since 4.2.0 - */ - public function display($cachable = false, $urlparams = false): void - { - $user = $this->app->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - - // Only allow logged in Users - if ($user->guest) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - // Get the view object - $viewLayout = $this->input->get('layout', 'default', 'string'); - $view = $this->getView('Captive', 'html', '', - [ - 'base_path' => $this->basePath, - 'layout' => $viewLayout, - ] - ); - - $view->document = $this->app->getDocument(); - - // If we're already logged in go to the site's home page - if ((int) $this->app->getSession()->get('com_users.mfa_checked', 0) === 1) - { - $url = Route::_('index.php?option=com_users&task=methods.display', false); - - $this->setRedirect($url); - } - - // Pass the model to the view - /** @var CaptiveModel $model */ - $model = $this->getModel('Captive'); - $view->setModel($model, true); - - /** @var BackupcodesModel $codesModel */ - $codesModel = $this->getModel('Backupcodes'); - $view->setModel($codesModel, false); - - try - { - // Suppress all modules on the page except those explicitly allowed - $model->suppressAllModules(); - } - catch (Exception $e) - { - // If we can't kill the modules we can still survive. - } - - // Pass the MFA record ID to the model - $recordId = $this->input->getInt('record_id', null); - $model->setState('record_id', $recordId); - - // Do not go through $this->display() because it overrides the model. - $view->display(); - } - - /** - * Validate the MFA code entered by the user - * - * @param bool $cachable Ignored. This page is never cached. - * @param array $urlparameters Ignored. This page is never cached. - * - * @return void - * @throws Exception - * @since 4.2.0 - */ - public function validate($cachable = false, $urlparameters = []) - { - // CSRF Check - $this->checkToken($this->input->getMethod()); - - // Get the MFA parameters from the request - $recordId = $this->input->getInt('record_id', null); - $code = $this->input->get('code', null, 'raw'); - /** @var CaptiveModel $model */ - $model = $this->getModel('Captive'); - - // Validate the MFA record - $model->setState('record_id', $recordId); - $record = $model->getRecord(); - - if (empty($record)) - { - $event = new NotifyActionLog('onComUsersCaptiveValidateInvalidMethod'); - $this->app->getDispatcher()->dispatch($event->getName(), $event); - - throw new RuntimeException(Text::_('COM_USERS_MFA_INVALID_METHOD'), 500); - } - - // Validate the code - $user = $this->app->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - - $event = new Validate($record, $user, $code); - $results = $this->app - ->getDispatcher() - ->dispatch($event->getName(), $event) - ->getArgument('result', []); - - $isValidCode = false; - - if ($record->method === 'backupcodes') - { - /** @var BackupcodesModel $codesModel */ - $codesModel = $this->getModel('Backupcodes'); - $results = [$codesModel->isBackupCode($code, $user)]; - /** - * This is required! Do not remove! - * - * There is a store() call below. It saves the in-memory MFA record to the database. That includes the - * options key which contains the configuration of the Method. For backup codes, these are the actual codes - * you can use. When we check for a backup code validity we also "burn" it, i.e. we remove it from the - * options table and save that to the database. However, this DOES NOT update the $record here. Therefore - * the call to saveRecord() would overwrite the database contents with a record that _includes_ the backup - * code we had just burned. As a result the single use backup codes end up being multiple use. - * - * By doing a getRecord() here, right after we have "burned" any correct backup codes, we resolve this - * issue. The loaded record will reflect the database contents where the options DO NOT include the code we - * just used. Therefore the call to store() will result in the correct database state, i.e. the used backup - * code being removed. - */ - $record = $model->getRecord(); - } - - $isValidCode = array_reduce( - $results, - function (bool $carry, $result) - { - return $carry || boolval($result); - }, - false - ); - - if (!$isValidCode) - { - // The code is wrong. Display an error and go back. - $captiveURL = Route::_('index.php?option=com_users&view=captive&record_id=' . $recordId, false); - $message = Text::_('COM_USERS_MFA_INVALID_CODE'); - $this->setRedirect($captiveURL, $message, 'error'); - - $event = new NotifyActionLog('onComUsersCaptiveValidateFailed', [$record->title]); - $this->app->getDispatcher()->dispatch($event->getName(), $event); - - return; - } - - // Update the Last Used, UA and IP columns - $jNow = Date::getInstance(); + /** + * Public constructor + * + * @param array $config Plugin configuration + * @param MVCFactoryInterface|null $factory MVC Factory for the com_users component + * @param CMSApplication|null $app CMS application object + * @param Input|null $input Joomla CMS input object + * + * @since 4.2.0 + */ + public function __construct(array $config = [], MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('captive', 'display'); + } + + /** + * Displays the captive login page + * + * @param boolean $cachable Ignored. This page is never cached. + * @param boolean|array $urlparams Ignored. This page is never cached. + * + * @return void + * @throws Exception + * @since 4.2.0 + */ + public function display($cachable = false, $urlparams = false): void + { + $user = $this->app->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + // Only allow logged in Users + if ($user->guest) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + // Get the view object + $viewLayout = $this->input->get('layout', 'default', 'string'); + $view = $this->getView( + 'Captive', + 'html', + '', + [ + 'base_path' => $this->basePath, + 'layout' => $viewLayout, + ] + ); + + $view->document = $this->app->getDocument(); + + // If we're already logged in go to the site's home page + if ((int) $this->app->getSession()->get('com_users.mfa_checked', 0) === 1) { + $url = Route::_('index.php?option=com_users&task=methods.display', false); + + $this->setRedirect($url); + } + + // Pass the model to the view + /** @var CaptiveModel $model */ + $model = $this->getModel('Captive'); + $view->setModel($model, true); + + /** @var BackupcodesModel $codesModel */ + $codesModel = $this->getModel('Backupcodes'); + $view->setModel($codesModel, false); + + try { + // Suppress all modules on the page except those explicitly allowed + $model->suppressAllModules(); + } catch (Exception $e) { + // If we can't kill the modules we can still survive. + } + + // Pass the MFA record ID to the model + $recordId = $this->input->getInt('record_id', null); + $model->setState('record_id', $recordId); + + // Do not go through $this->display() because it overrides the model. + $view->display(); + } + + /** + * Validate the MFA code entered by the user + * + * @param bool $cachable Ignored. This page is never cached. + * @param array $urlparameters Ignored. This page is never cached. + * + * @return void + * @throws Exception + * @since 4.2.0 + */ + public function validate($cachable = false, $urlparameters = []) + { + // CSRF Check + $this->checkToken($this->input->getMethod()); + + // Get the MFA parameters from the request + $recordId = $this->input->getInt('record_id', null); + $code = $this->input->get('code', null, 'raw'); + /** @var CaptiveModel $model */ + $model = $this->getModel('Captive'); + + // Validate the MFA record + $model->setState('record_id', $recordId); + $record = $model->getRecord(); + + if (empty($record)) { + $event = new NotifyActionLog('onComUsersCaptiveValidateInvalidMethod'); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + throw new RuntimeException(Text::_('COM_USERS_MFA_INVALID_METHOD'), 500); + } + + // Validate the code + $user = $this->app->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + $event = new Validate($record, $user, $code); + $results = $this->app + ->getDispatcher() + ->dispatch($event->getName(), $event) + ->getArgument('result', []); + + $isValidCode = false; + + if ($record->method === 'backupcodes') { + /** @var BackupcodesModel $codesModel */ + $codesModel = $this->getModel('Backupcodes'); + $results = [$codesModel->isBackupCode($code, $user)]; + /** + * This is required! Do not remove! + * + * There is a store() call below. It saves the in-memory MFA record to the database. That includes the + * options key which contains the configuration of the Method. For backup codes, these are the actual codes + * you can use. When we check for a backup code validity we also "burn" it, i.e. we remove it from the + * options table and save that to the database. However, this DOES NOT update the $record here. Therefore + * the call to saveRecord() would overwrite the database contents with a record that _includes_ the backup + * code we had just burned. As a result the single use backup codes end up being multiple use. + * + * By doing a getRecord() here, right after we have "burned" any correct backup codes, we resolve this + * issue. The loaded record will reflect the database contents where the options DO NOT include the code we + * just used. Therefore the call to store() will result in the correct database state, i.e. the used backup + * code being removed. + */ + $record = $model->getRecord(); + } + + $isValidCode = array_reduce( + $results, + function (bool $carry, $result) { + return $carry || boolval($result); + }, + false + ); + + if (!$isValidCode) { + // The code is wrong. Display an error and go back. + $captiveURL = Route::_('index.php?option=com_users&view=captive&record_id=' . $recordId, false); + $message = Text::_('COM_USERS_MFA_INVALID_CODE'); + $this->setRedirect($captiveURL, $message, 'error'); + + $event = new NotifyActionLog('onComUsersCaptiveValidateFailed', [$record->title]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + return; + } + + // Update the Last Used, UA and IP columns + $jNow = Date::getInstance(); // phpcs:ignore $record->last_used = $jNow->toSql(); - $record->store(); + $record->store(); - // Flag the user as fully logged in - $session = $this->app->getSession(); - $session->set('com_users.mfa_checked', 1); - $session->set('com_users.mandatory_mfa_setup', 0); + // Flag the user as fully logged in + $session = $this->app->getSession(); + $session->set('com_users.mfa_checked', 1); + $session->set('com_users.mandatory_mfa_setup', 0); - // Get the return URL stored by the plugin in the session - $returnUrl = $session->get('com_users.return_url', ''); + // Get the return URL stored by the plugin in the session + $returnUrl = $session->get('com_users.return_url', ''); - // If the return URL is not set or not internal to this site redirect to the site's front page - if (empty($returnUrl) || !Uri::isInternal($returnUrl)) - { - $returnUrl = Uri::base(); - } + // If the return URL is not set or not internal to this site redirect to the site's front page + if (empty($returnUrl) || !Uri::isInternal($returnUrl)) { + $returnUrl = Uri::base(); + } - $this->setRedirect($returnUrl); + $this->setRedirect($returnUrl); - $event = new NotifyActionLog('onComUsersCaptiveValidateSuccess', [$record->title]); - $this->app->getDispatcher()->dispatch($event->getName(), $event); - } + $event = new NotifyActionLog('onComUsersCaptiveValidateSuccess', [$record->title]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + } } diff --git a/administrator/components/com_users/src/Controller/DisplayController.php b/administrator/components/com_users/src/Controller/DisplayController.php index 018bbde7b6782..cbb9042dc22b7 100644 --- a/administrator/components/com_users/src/Controller/DisplayController.php +++ b/administrator/components/com_users/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ get('core.admin'); - - // Default permissions. - default: - return true; - } - } - - /** - * Method to display a view. - * - * @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 \Joomla\CMS\Filter\InputFilter::clean()}. - * - * @return BaseController|boolean This object to support chaining or false on failure. - * - * @since 1.5 - */ - public function display($cachable = false, $urlparams = array()) - { - $view = $this->input->get('view', 'users'); - $layout = $this->input->get('layout', 'default'); - $id = $this->input->getInt('id'); - - if (!$this->canView($view)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - // Check for edit form. - if ($view == 'user' && $layout == 'edit' && !$this->checkEditId('com_users.edit.user', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_users&view=users', false)); - - return false; - } - elseif ($view == 'group' && $layout == 'edit' && !$this->checkEditId('com_users.edit.group', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_users&view=groups', false)); - - return false; - } - elseif ($view == 'level' && $layout == 'edit' && !$this->checkEditId('com_users.edit.level', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_users&view=levels', false)); - - return false; - } - elseif ($view == 'note' && $layout == 'edit' && !$this->checkEditId('com_users.edit.note', $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } - - $this->setRedirect(Route::_('index.php?option=com_users&view=notes', false)); - - return false; - } - elseif (in_array($view, ['captive', 'callback', 'methods', 'method'])) - { - $controller = $this->factory->createController($view, 'Administrator', [], $this->app, $this->input); - $task = $this->input->get('task', ''); - - return $controller->execute($task); - } - - return parent::display($cachable, $urlparams); - } + /** + * The default view. + * + * @var string + * @since 1.6 + */ + protected $default_view = 'users'; + + /** + * Checks whether a user can see this view. + * + * @param string $view The view name. + * + * @return boolean + * + * @since 1.6 + */ + protected function canView($view) + { + $canDo = ContentHelper::getActions('com_users'); + + switch ($view) { + // Special permissions. + case 'groups': + case 'group': + case 'levels': + case 'level': + return $canDo->get('core.admin'); + + // Default permissions. + default: + return true; + } + } + + /** + * Method to display a view. + * + * @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 \Joomla\CMS\Filter\InputFilter::clean()}. + * + * @return BaseController|boolean This object to support chaining or false on failure. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = array()) + { + $view = $this->input->get('view', 'users'); + $layout = $this->input->get('layout', 'default'); + $id = $this->input->getInt('id'); + + if (!$this->canView($view)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + // Check for edit form. + if ($view == 'user' && $layout == 'edit' && !$this->checkEditId('com_users.edit.user', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_users&view=users', false)); + + return false; + } elseif ($view == 'group' && $layout == 'edit' && !$this->checkEditId('com_users.edit.group', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_users&view=groups', false)); + + return false; + } elseif ($view == 'level' && $layout == 'edit' && !$this->checkEditId('com_users.edit.level', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_users&view=levels', false)); + + return false; + } elseif ($view == 'note' && $layout == 'edit' && !$this->checkEditId('com_users.edit.note', $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } + + $this->setRedirect(Route::_('index.php?option=com_users&view=notes', false)); + + return false; + } elseif (in_array($view, ['captive', 'callback', 'methods', 'method'])) { + $controller = $this->factory->createController($view, 'Administrator', [], $this->app, $this->input); + $task = $this->input->get('task', ''); + + return $controller->execute($task); + } + + return parent::display($cachable, $urlparams); + } } diff --git a/administrator/components/com_users/src/Controller/GroupController.php b/administrator/components/com_users/src/Controller/GroupController.php index d2d55aafcd778..f1555ca2a34d4 100644 --- a/administrator/components/com_users/src/Controller/GroupController.php +++ b/administrator/components/com_users/src/Controller/GroupController.php @@ -1,4 +1,5 @@ app->getIdentity()->authorise('core.admin', $this->option) && parent::allowSave($data, $key)); - } + /** + * Method to check if you can save a new or existing record. + * + * Overrides Joomla\CMS\MVC\Controller\FormController::allowSave to check the core.admin permission. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowSave($data, $key = 'id') + { + return ($this->app->getIdentity()->authorise('core.admin', $this->option) && parent::allowSave($data, $key)); + } - /** - * Overrides Joomla\CMS\MVC\Controller\FormController::allowEdit - * - * Checks that non-Super Admins are not editing Super Admins. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - // Check if this group is a Super Admin - if (Access::checkGroup($data[$key], 'core.admin')) - { - // If I'm not a Super Admin, then disallow the edit. - if (!$this->app->getIdentity()->authorise('core.admin')) - { - return false; - } - } + /** + * Overrides Joomla\CMS\MVC\Controller\FormController::allowEdit + * + * Checks that non-Super Admins are not editing Super Admins. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + // Check if this group is a Super Admin + if (Access::checkGroup($data[$key], 'core.admin')) { + // If I'm not a Super Admin, then disallow the edit. + if (!$this->app->getIdentity()->authorise('core.admin')) { + return false; + } + } - return parent::allowEdit($data, $key); - } + return parent::allowEdit($data, $key); + } } diff --git a/administrator/components/com_users/src/Controller/GroupsController.php b/administrator/components/com_users/src/Controller/GroupsController.php index 9b71d8870add9..3ce083fbd078c 100644 --- a/administrator/components/com_users/src/Controller/GroupsController.php +++ b/administrator/components/com_users/src/Controller/GroupsController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + namespace Joomla\Component\Users\Administrator\Controller; use Joomla\CMS\Access\Exception\NotAllowed; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Controller\AdminController; -\defined('_JEXEC') or die; - /** * User groups list controller class. * @@ -21,120 +21,115 @@ */ class GroupsController extends AdminController { - /** - * @var string The prefix to use with controller messages. - * @since 1.6 - */ - protected $text_prefix = 'COM_USERS_GROUPS'; + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_USERS_GROUPS'; - /** - * Proxy for getModel. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return object The model. - * - * @since 1.6 - */ - public function getModel($name = 'Group', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 1.6 + */ + public function getModel($name = 'Group', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } - /** - * Removes an item. - * - * Overrides Joomla\CMS\MVC\Controller\AdminController::delete to check the core.admin permission. - * - * @return void - * - * @since 1.6 - */ - public function delete() - { - if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + /** + * Removes an item. + * + * Overrides Joomla\CMS\MVC\Controller\AdminController::delete to check the core.admin permission. + * + * @return void + * + * @since 1.6 + */ + public function delete() + { + if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } - parent::delete(); - } + parent::delete(); + } - /** - * Method to publish a list of records. - * - * Overrides Joomla\CMS\MVC\Controller\AdminController::publish to check the core.admin permission. - * - * @return void - * - * @since 1.6 - */ - public function publish() - { - if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + /** + * Method to publish a list of records. + * + * Overrides Joomla\CMS\MVC\Controller\AdminController::publish to check the core.admin permission. + * + * @return void + * + * @since 1.6 + */ + public function publish() + { + if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } - parent::publish(); - } + parent::publish(); + } - /** - * Changes the order of one or more records. - * - * Overrides Joomla\CMS\MVC\Controller\AdminController::reorder to check the core.admin permission. - * - * @return boolean True on success - * - * @since 1.6 - */ - public function reorder() - { - if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + /** + * Changes the order of one or more records. + * + * Overrides Joomla\CMS\MVC\Controller\AdminController::reorder to check the core.admin permission. + * + * @return boolean True on success + * + * @since 1.6 + */ + public function reorder() + { + if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } - return parent::reorder(); - } + return parent::reorder(); + } - /** - * Method to save the submitted ordering values for records. - * - * Overrides Joomla\CMS\MVC\Controller\AdminController::saveorder to check the core.admin permission. - * - * @return boolean True on success - * - * @since 1.6 - */ - public function saveorder() - { - if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + /** + * Method to save the submitted ordering values for records. + * + * Overrides Joomla\CMS\MVC\Controller\AdminController::saveorder to check the core.admin permission. + * + * @return boolean True on success + * + * @since 1.6 + */ + public function saveorder() + { + if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } - return parent::saveorder(); - } + return parent::saveorder(); + } - /** - * Check in of one or more records. - * - * Overrides Joomla\CMS\MVC\Controller\AdminController::checkin to check the core.admin permission. - * - * @return boolean True on success - * - * @since 1.6 - */ - public function checkin() - { - if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } + /** + * Check in of one or more records. + * + * Overrides Joomla\CMS\MVC\Controller\AdminController::checkin to check the core.admin permission. + * + * @return boolean True on success + * + * @since 1.6 + */ + public function checkin() + { + if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } - return parent::checkin(); - } + return parent::checkin(); + } } diff --git a/administrator/components/com_users/src/Controller/LevelController.php b/administrator/components/com_users/src/Controller/LevelController.php index 4028dd10fb9ea..d7ac126707eeb 100644 --- a/administrator/components/com_users/src/Controller/LevelController.php +++ b/administrator/components/com_users/src/Controller/LevelController.php @@ -1,4 +1,5 @@ app->getIdentity()->authorise('core.admin', $this->option) && parent::allowSave($data, $key)); - } - - /** - * Overrides JControllerForm::allowEdit - * - * Checks that non-Super Admins are not editing Super Admins. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 3.8.8 - */ - protected function allowEdit($data = array(), $key = 'id') - { - // Check for if Super Admin can edit - $viewLevel = $this->getModel('Level', 'Administrator')->getItem((int) $data['id']); - - // If this group is super admin and this user is not super admin, canEdit is false - if (!$this->app->getIdentity()->authorise('core.admin') && $viewLevel->rules && Access::checkGroup($viewLevel->rules[0], 'core.admin')) - { - $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(), false - ) - ); - - return false; - } - - return parent::allowEdit($data, $key); - } - - /** - * Removes an item. - * - * Overrides Joomla\CMS\MVC\Controller\FormController::delete to check the core.admin permission. - * - * @return void - * - * @since 1.6 - */ - public function delete() - { - // Check for request forgeries. - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - elseif (empty($ids)) - { - $this->setMessage(Text::_('COM_USERS_NO_LEVELS_SELECTED'), 'warning'); - } - else - { - // Get the model. - $model = $this->getModel(); - - // Remove the items. - if ($model->delete($ids)) - { - $this->setMessage(Text::plural('COM_USERS_N_LEVELS_DELETED', count($ids))); - } - } - - $this->setRedirect('index.php?option=com_users&view=levels'); - } + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_USERS_LEVEL'; + + /** + * Method to check if you can save a new or existing record. + * + * Overrides Joomla\CMS\MVC\Controller\FormController::allowSave to check the core.admin permission. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowSave($data, $key = 'id') + { + return ($this->app->getIdentity()->authorise('core.admin', $this->option) && parent::allowSave($data, $key)); + } + + /** + * Overrides JControllerForm::allowEdit + * + * Checks that non-Super Admins are not editing Super Admins. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 3.8.8 + */ + protected function allowEdit($data = array(), $key = 'id') + { + // Check for if Super Admin can edit + $viewLevel = $this->getModel('Level', 'Administrator')->getItem((int) $data['id']); + + // If this group is super admin and this user is not super admin, canEdit is false + if (!$this->app->getIdentity()->authorise('core.admin') && $viewLevel->rules && Access::checkGroup($viewLevel->rules[0], 'core.admin')) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_EDIT_NOT_PERMITTED'), 'error'); + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . $this->getRedirectToListAppend(), + false + ) + ); + + return false; + } + + return parent::allowEdit($data, $key); + } + + /** + * Removes an item. + * + * Overrides Joomla\CMS\MVC\Controller\FormController::delete to check the core.admin permission. + * + * @return void + * + * @since 1.6 + */ + public function delete() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (!$this->app->getIdentity()->authorise('core.admin', $this->option)) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } elseif (empty($ids)) { + $this->setMessage(Text::_('COM_USERS_NO_LEVELS_SELECTED'), 'warning'); + } else { + // Get the model. + $model = $this->getModel(); + + // Remove the items. + if ($model->delete($ids)) { + $this->setMessage(Text::plural('COM_USERS_N_LEVELS_DELETED', count($ids))); + } + } + + $this->setRedirect('index.php?option=com_users&view=levels'); + } } diff --git a/administrator/components/com_users/src/Controller/LevelsController.php b/administrator/components/com_users/src/Controller/LevelsController.php index d4ffb80051d27..0496bc63ed011 100644 --- a/administrator/components/com_users/src/Controller/LevelsController.php +++ b/administrator/components/com_users/src/Controller/LevelsController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Users\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Users\Administrator\Controller; use Joomla\CMS\MVC\Controller\AdminController; @@ -19,25 +19,25 @@ */ class LevelsController extends AdminController { - /** - * @var string The prefix to use with controller messages. - * @since 1.6 - */ - protected $text_prefix = 'COM_USERS_LEVELS'; + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_USERS_LEVELS'; - /** - * Proxy for getModel. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. - * - * @since 1.6 - */ - public function getModel($name = 'Level', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6 + */ + public function getModel($name = 'Level', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_users/src/Controller/MailController.php b/administrator/components/com_users/src/Controller/MailController.php index efd0d59520040..2ddc744b427e9 100644 --- a/administrator/components/com_users/src/Controller/MailController.php +++ b/administrator/components/com_users/src/Controller/MailController.php @@ -1,4 +1,5 @@ app->get('massmailoff', 0) == 1) - { - $this->app->redirect(Route::_('index.php', false)); - } + /** + * Send the mail + * + * @return void + * + * @since 1.6 + */ + public function send() + { + // Redirect to admin index if mass mailer disabled in conf + if ($this->app->get('massmailoff', 0) == 1) { + $this->app->redirect(Route::_('index.php', false)); + } - // Check for request forgeries. - $this->checkToken('request'); + // Check for request forgeries. + $this->checkToken('request'); - $model = $this->getModel('Mail'); + $model = $this->getModel('Mail'); - if ($model->send()) - { - $type = 'message'; - } - else - { - $type = 'error'; - } + if ($model->send()) { + $type = 'message'; + } else { + $type = 'error'; + } - $msg = $model->getError(); - $this->setRedirect('index.php?option=com_users&view=mail', $msg, $type); - } + $msg = $model->getError(); + $this->setRedirect('index.php?option=com_users&view=mail', $msg, $type); + } - /** - * Cancel the mail - * - * @return void - * - * @since 1.6 - */ - public function cancel() - { - // Check for request forgeries. - $this->checkToken('request'); + /** + * Cancel the mail + * + * @return void + * + * @since 1.6 + */ + public function cancel() + { + // Check for request forgeries. + $this->checkToken('request'); - // Clear data from session. - $this->app->setUserState('com_users.display.mail.data', null); + // Clear data from session. + $this->app->setUserState('com_users.display.mail.data', null); - $this->setRedirect('index.php?option=com_users&view=users'); - } + $this->setRedirect('index.php?option=com_users&view=users'); + } } diff --git a/administrator/components/com_users/src/Controller/MethodController.php b/administrator/components/com_users/src/Controller/MethodController.php index 18918db7f0afa..6c25750e0e7a2 100644 --- a/administrator/components/com_users/src/Controller/MethodController.php +++ b/administrator/components/com_users/src/Controller/MethodController.php @@ -1,4 +1,5 @@ assertLoggedInUser(); - - // Make sure I am allowed to edit the specified user - $userId = $this->input->getInt('user_id', null); - $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); - - $this->assertCanEdit($user); - - // Also make sure the Method really does exist - $method = $this->input->getCmd('method'); - $this->assertMethodExists($method); - - /** @var MethodModel $model */ - $model = $this->getModel('Method'); - $model->setState('method', $method); - - // Pass the return URL to the view - $returnURL = $this->input->getBase64('returnurl'); - $viewLayout = $this->input->get('layout', 'default', 'string'); - $view = $this->getView('Method', 'html'); - $view->setLayout($viewLayout); - $view->returnURL = $returnURL; - $view->user = $user; - $view->document = $this->app->getDocument(); - - $view->setModel($model, true); - - $event = new NotifyActionLog('onComUsersControllerMethodBeforeAdd', [$user, $method]); - $this->app->getDispatcher()->dispatch($event->getName(), $event); - - $view->display(); - } - - /** - * Edit an existing MFA Method - * - * @param boolean $cachable Ignored. This page is never cached. - * @param boolean|array $urlparams Ignored. This page is never cached. - * - * @return void - * @throws Exception - * @since 4.2.0 - */ - public function edit($cachable = false, $urlparams = []): void - { - $this->assertLoggedInUser(); - - // Make sure I am allowed to edit the specified user - $userId = $this->input->getInt('user_id', null); - $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); - - $this->assertCanEdit($user); - - // Also make sure the Method really does exist - $id = $this->input->getInt('id'); - $record = $this->assertValidRecordId($id, $user); - - if ($id <= 0) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - /** @var MethodModel $model */ - $model = $this->getModel('Method'); - $model->setState('id', $id); - - // Pass the return URL to the view - $returnURL = $this->input->getBase64('returnurl'); - $viewLayout = $this->input->get('layout', 'default', 'string'); - $view = $this->getView('Method', 'html'); - $view->setLayout($viewLayout); - $view->returnURL = $returnURL; - $view->user = $user; - $view->document = $this->app->getDocument(); - - $view->setModel($model, true); - - $event = new NotifyActionLog('onComUsersControllerMethodBeforeEdit', [$id, $user]); - $this->app->getDispatcher()->dispatch($event->getName(), $event); - - $view->display(); - } - - /** - * Regenerate backup codes - * - * @param boolean $cachable Ignored. This page is never cached. - * @param boolean|array $urlparams Ignored. This page is never cached. - * - * @return void - * @throws Exception - * @since 4.2.0 - */ - public function regenerateBackupCodes($cachable = false, $urlparams = []): void - { - $this->assertLoggedInUser(); - - $this->checkToken($this->input->getMethod()); - - // Make sure I am allowed to edit the specified user - $userId = $this->input->getInt('user_id', null); - $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); - $this->assertCanEdit($user); - - /** @var BackupcodesModel $model */ - $model = $this->getModel('Backupcodes'); - $model->regenerateBackupCodes($user); - - $backupCodesRecord = $model->getBackupCodesRecord($user); - - // Redirect - $redirectUrl = 'index.php?option=com_users&task=method.edit&user_id=' . $userId . '&id=' . $backupCodesRecord->id; - $returnURL = $this->input->getBase64('returnurl'); - - if (!empty($returnURL)) - { - $redirectUrl .= '&returnurl=' . $returnURL; - } - - $this->setRedirect(Route::_($redirectUrl, false)); - - $event = new NotifyActionLog('onComUsersControllerMethodAfterRegenerateBackupCodes'); - $this->app->getDispatcher()->dispatch($event->getName(), $event); - } - - /** - * Delete an existing MFA Method - * - * @param boolean $cachable Ignored. This page is never cached. - * @param boolean|array $urlparams Ignored. This page is never cached. - * - * @return void - * @since 4.2.0 - */ - public function delete($cachable = false, $urlparams = []): void - { - $this->assertLoggedInUser(); - - $this->checkToken($this->input->getMethod()); - - // Make sure I am allowed to edit the specified user - $userId = $this->input->getInt('user_id', null); - $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); - $this->assertCanDelete($user); - - // Also make sure the Method really does exist - $id = $this->input->getInt('id'); - $record = $this->assertValidRecordId($id, $user); - - if ($id <= 0) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - $type = null; - $message = null; - - $event = new NotifyActionLog('onComUsersControllerMethodBeforeDelete', [$id, $user]); - $this->app->getDispatcher()->dispatch($event->getName(), $event); - - try - { - $record->delete(); - } - catch (Exception $e) - { - $message = $e->getMessage(); - $type = 'error'; - } - - // Redirect - $url = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $userId, false); - $returnURL = $this->input->getBase64('returnurl'); - - if (!empty($returnURL)) - { - $url = base64_decode($returnURL); - } - - $this->setRedirect($url, $message, $type); - } - - /** - * Save the MFA Method - * - * @param boolean $cachable Ignored. This page is never cached. - * @param boolean|array $urlparams Ignored. This page is never cached. - * - * @return void - * @since 4.2.0 - */ - public function save($cachable = false, $urlparams = []): void - { - $this->assertLoggedInUser(); - - $this->checkToken($this->input->getMethod()); - - // Make sure I am allowed to edit the specified user - $userId = $this->input->getInt('user_id', null); - $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); - $this->assertCanEdit($user); - - // Redirect - $url = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $userId, false); - $returnURL = $this->input->getBase64('returnurl'); - - if (!empty($returnURL)) - { - $url = base64_decode($returnURL); - } - - // The record must either be new (ID zero) or exist - $id = $this->input->getInt('id', 0); - $record = $this->assertValidRecordId($id, $user); - - // If it's a new record we need to read the Method from the request and update the (not yet created) record. - if ($record->id == 0) - { - $methodName = $this->input->getCmd('method'); - $this->assertMethodExists($methodName); - $record->method = $methodName; - } - - /** @var MethodModel $model */ - $model = $this->getModel('Method'); - - // Ask the plugin to validate the input by calling onUserMultifactorSaveSetup - $result = []; - $input = $this->app->input; - - $event = new NotifyActionLog('onComUsersControllerMethodBeforeSave', [$id, $user]); - $this->app->getDispatcher()->dispatch($event->getName(), $event); - - try - { - $event = new SaveSetup($record, $input); - $pluginResults = $this->app - ->getDispatcher() - ->dispatch($event->getName(), $event) - ->getArgument('result', []); - - foreach ($pluginResults as $pluginResult) - { - $result = array_merge($result, $pluginResult); - } - } - catch (RuntimeException $e) - { - // Go back to the edit page - $nonSefUrl = 'index.php?option=com_users&task=method.'; - - if ($id) - { - $nonSefUrl .= 'edit&id=' . (int) $id; - } - else - { - $nonSefUrl .= 'add&method=' . $record->method; - } - - $nonSefUrl .= '&user_id=' . $userId; - - if (!empty($returnURL)) - { - $nonSefUrl .= '&returnurl=' . urlencode($returnURL); - } - - $url = Route::_($nonSefUrl, false); - $this->setRedirect($url, $e->getMessage(), 'error'); - - return; - } - - // Update the record's options with the plugin response - $title = $this->input->getString('title', null); - $title = trim($title); - - if (empty($title)) - { - $method = $model->getMethod($record->method); - $title = $method['display']; - } - - // Update the record's "default" flag - $default = $this->input->getBool('default', false); - $record->title = $title; - $record->options = $result; - $record->default = $default ? 1 : 0; - - // Ask the model to save the record - $saved = $record->store(); - - if (!$saved) - { - // Go back to the edit page - $nonSefUrl = 'index.php?option=com_users&task=method.'; - - if ($id) - { - $nonSefUrl .= 'edit&id=' . (int) $id; - } - else - { - $nonSefUrl .= 'add'; - } - - $nonSefUrl .= '&user_id=' . $userId; - - if (!empty($returnURL)) - { - $nonSefUrl .= '&returnurl=' . urlencode($returnURL); - } - - $url = Route::_($nonSefUrl, false); - $this->setRedirect($url, $record->getError(), 'error'); - - return; - } - - $this->setRedirect($url); - } - - /** - * Assert that the provided ID is a valid record identified for the given user - * - * @param int $id Record ID to check - * @param User|null $user User record. Null to use current user. - * - * @return MfaTable The loaded record - * @since 4.2.0 - */ - private function assertValidRecordId($id, ?User $user = null): MfaTable - { - if (is_null($user)) - { - $user = $this->app->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - /** @var MethodModel $model */ - $model = $this->getModel('Method'); - - $model->setState('id', $id); - - $record = $model->getRecord($user); + /** + * Public constructor + * + * @param array $config Plugin configuration + * @param MVCFactoryInterface|null $factory MVC Factory for the com_users component + * @param CMSApplication|null $app CMS application object + * @param Input|null $input Joomla CMS input object + * + * @since 4.2.0 + */ + public function __construct(array $config = [], MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null) + { + // We have to tell Joomla what is the name of the view, otherwise it defaults to the name of the *component*. + $config['default_view'] = 'method'; + $config['default_task'] = 'add'; + + parent::__construct($config, $factory, $app, $input); + } + + /** + * Execute a task by triggering a Method in the derived class. + * + * @param string $task The task to perform. If no matching task is found, the '__default' task is executed, if + * defined. + * + * @return mixed The value returned by the called Method. + * + * @throws Exception + * @since 4.2.0 + */ + public function execute($task) + { + if (empty($task) || $task === 'display') { + $task = 'add'; + } + + return parent::execute($task); + } + + /** + * Add a new MFA Method + * + * @param boolean $cachable Ignored. This page is never cached. + * @param boolean|array $urlparams Ignored. This page is never cached. + * + * @return void + * @throws Exception + * @since 4.2.0 + */ + public function add($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + + $this->assertCanEdit($user); + + // Also make sure the Method really does exist + $method = $this->input->getCmd('method'); + $this->assertMethodExists($method); + + /** @var MethodModel $model */ + $model = $this->getModel('Method'); + $model->setState('method', $method); + + // Pass the return URL to the view + $returnURL = $this->input->getBase64('returnurl'); + $viewLayout = $this->input->get('layout', 'default', 'string'); + $view = $this->getView('Method', 'html'); + $view->setLayout($viewLayout); + $view->returnURL = $returnURL; + $view->user = $user; + $view->document = $this->app->getDocument(); + + $view->setModel($model, true); + + $event = new NotifyActionLog('onComUsersControllerMethodBeforeAdd', [$user, $method]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + $view->display(); + } + + /** + * Edit an existing MFA Method + * + * @param boolean $cachable Ignored. This page is never cached. + * @param boolean|array $urlparams Ignored. This page is never cached. + * + * @return void + * @throws Exception + * @since 4.2.0 + */ + public function edit($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + + $this->assertCanEdit($user); + + // Also make sure the Method really does exist + $id = $this->input->getInt('id'); + $record = $this->assertValidRecordId($id, $user); + + if ($id <= 0) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + /** @var MethodModel $model */ + $model = $this->getModel('Method'); + $model->setState('id', $id); + + // Pass the return URL to the view + $returnURL = $this->input->getBase64('returnurl'); + $viewLayout = $this->input->get('layout', 'default', 'string'); + $view = $this->getView('Method', 'html'); + $view->setLayout($viewLayout); + $view->returnURL = $returnURL; + $view->user = $user; + $view->document = $this->app->getDocument(); + + $view->setModel($model, true); + + $event = new NotifyActionLog('onComUsersControllerMethodBeforeEdit', [$id, $user]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + $view->display(); + } + + /** + * Regenerate backup codes + * + * @param boolean $cachable Ignored. This page is never cached. + * @param boolean|array $urlparams Ignored. This page is never cached. + * + * @return void + * @throws Exception + * @since 4.2.0 + */ + public function regenerateBackupCodes($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + $this->checkToken($this->input->getMethod()); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + $this->assertCanEdit($user); + + /** @var BackupcodesModel $model */ + $model = $this->getModel('Backupcodes'); + $model->regenerateBackupCodes($user); + + $backupCodesRecord = $model->getBackupCodesRecord($user); + + // Redirect + $redirectUrl = 'index.php?option=com_users&task=method.edit&user_id=' . $userId . '&id=' . $backupCodesRecord->id; + $returnURL = $this->input->getBase64('returnurl'); + + if (!empty($returnURL)) { + $redirectUrl .= '&returnurl=' . $returnURL; + } + + $this->setRedirect(Route::_($redirectUrl, false)); + + $event = new NotifyActionLog('onComUsersControllerMethodAfterRegenerateBackupCodes'); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + } + + /** + * Delete an existing MFA Method + * + * @param boolean $cachable Ignored. This page is never cached. + * @param boolean|array $urlparams Ignored. This page is never cached. + * + * @return void + * @since 4.2.0 + */ + public function delete($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + $this->checkToken($this->input->getMethod()); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + $this->assertCanDelete($user); + + // Also make sure the Method really does exist + $id = $this->input->getInt('id'); + $record = $this->assertValidRecordId($id, $user); + + if ($id <= 0) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $type = null; + $message = null; + + $event = new NotifyActionLog('onComUsersControllerMethodBeforeDelete', [$id, $user]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + try { + $record->delete(); + } catch (Exception $e) { + $message = $e->getMessage(); + $type = 'error'; + } + + // Redirect + $url = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $userId, false); + $returnURL = $this->input->getBase64('returnurl'); + + if (!empty($returnURL)) { + $url = base64_decode($returnURL); + } + + $this->setRedirect($url, $message, $type); + } + + /** + * Save the MFA Method + * + * @param boolean $cachable Ignored. This page is never cached. + * @param boolean|array $urlparams Ignored. This page is never cached. + * + * @return void + * @since 4.2.0 + */ + public function save($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + $this->checkToken($this->input->getMethod()); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + $this->assertCanEdit($user); + + // Redirect + $url = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $userId, false); + $returnURL = $this->input->getBase64('returnurl'); + + if (!empty($returnURL)) { + $url = base64_decode($returnURL); + } + + // The record must either be new (ID zero) or exist + $id = $this->input->getInt('id', 0); + $record = $this->assertValidRecordId($id, $user); + + // If it's a new record we need to read the Method from the request and update the (not yet created) record. + if ($record->id == 0) { + $methodName = $this->input->getCmd('method'); + $this->assertMethodExists($methodName); + $record->method = $methodName; + } + + /** @var MethodModel $model */ + $model = $this->getModel('Method'); + + // Ask the plugin to validate the input by calling onUserMultifactorSaveSetup + $result = []; + $input = $this->app->input; + + $event = new NotifyActionLog('onComUsersControllerMethodBeforeSave', [$id, $user]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + try { + $event = new SaveSetup($record, $input); + $pluginResults = $this->app + ->getDispatcher() + ->dispatch($event->getName(), $event) + ->getArgument('result', []); + + foreach ($pluginResults as $pluginResult) { + $result = array_merge($result, $pluginResult); + } + } catch (RuntimeException $e) { + // Go back to the edit page + $nonSefUrl = 'index.php?option=com_users&task=method.'; + + if ($id) { + $nonSefUrl .= 'edit&id=' . (int) $id; + } else { + $nonSefUrl .= 'add&method=' . $record->method; + } + + $nonSefUrl .= '&user_id=' . $userId; + + if (!empty($returnURL)) { + $nonSefUrl .= '&returnurl=' . urlencode($returnURL); + } + + $url = Route::_($nonSefUrl, false); + $this->setRedirect($url, $e->getMessage(), 'error'); + + return; + } + + // Update the record's options with the plugin response + $title = $this->input->getString('title', null); + $title = trim($title); + + if (empty($title)) { + $method = $model->getMethod($record->method); + $title = $method['display']; + } + + // Update the record's "default" flag + $default = $this->input->getBool('default', false); + $record->title = $title; + $record->options = $result; + $record->default = $default ? 1 : 0; + + // Ask the model to save the record + $saved = $record->store(); + + if (!$saved) { + // Go back to the edit page + $nonSefUrl = 'index.php?option=com_users&task=method.'; + + if ($id) { + $nonSefUrl .= 'edit&id=' . (int) $id; + } else { + $nonSefUrl .= 'add'; + } + + $nonSefUrl .= '&user_id=' . $userId; + + if (!empty($returnURL)) { + $nonSefUrl .= '&returnurl=' . urlencode($returnURL); + } + + $url = Route::_($nonSefUrl, false); + $this->setRedirect($url, $record->getError(), 'error'); + + return; + } + + $this->setRedirect($url); + } + + /** + * Assert that the provided ID is a valid record identified for the given user + * + * @param int $id Record ID to check + * @param User|null $user User record. Null to use current user. + * + * @return MfaTable The loaded record + * @since 4.2.0 + */ + private function assertValidRecordId($id, ?User $user = null): MfaTable + { + if (is_null($user)) { + $user = $this->app->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + /** @var MethodModel $model */ + $model = $this->getModel('Method'); + + $model->setState('id', $id); + + $record = $model->getRecord($user); // phpcs:ignore if (is_null($record) || ($record->id != $id) || ($record->user_id != $user->id)) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - return $record; - } - - /** - * Assert that the user can add / edit MFA methods. - * - * @param User|null $user User record. Null to use current user. - * - * @return void - * @throws RuntimeException|Exception - * @since 4.2.0 - */ - private function assertCanEdit(?User $user = null): void - { - if (!MfaHelper::canAddEditMethod($user)) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - } - - /** - * Assert that the user can delete MFA records / disable MFA. - * - * @param User|null $user User record. Null to use current user. - * - * @return void - * @throws RuntimeException|Exception - * @since 4.2.0 - */ - private function assertCanDelete(?User $user = null): void - { - if (!MfaHelper::canDeleteMethod($user)) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - } - - /** - * Assert that the specified MFA Method exists, is activated and enabled for the current user - * - * @param string|null $method The Method to check - * - * @return void - * @since 4.2.0 - */ - private function assertMethodExists(?string $method): void - { - /** @var MethodModel $model */ - $model = $this->getModel('Method'); - - if (empty($method) || !$model->methodExists($method)) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - } - - /** - * Assert that there is a logged in user. - * - * @return void - * @since 4.2.0 - */ - private function assertLoggedInUser(): void - { - $user = $this->app->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - - if ($user->guest) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - } + { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + return $record; + } + + /** + * Assert that the user can add / edit MFA methods. + * + * @param User|null $user User record. Null to use current user. + * + * @return void + * @throws RuntimeException|Exception + * @since 4.2.0 + */ + private function assertCanEdit(?User $user = null): void + { + if (!MfaHelper::canAddEditMethod($user)) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + } + + /** + * Assert that the user can delete MFA records / disable MFA. + * + * @param User|null $user User record. Null to use current user. + * + * @return void + * @throws RuntimeException|Exception + * @since 4.2.0 + */ + private function assertCanDelete(?User $user = null): void + { + if (!MfaHelper::canDeleteMethod($user)) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + } + + /** + * Assert that the specified MFA Method exists, is activated and enabled for the current user + * + * @param string|null $method The Method to check + * + * @return void + * @since 4.2.0 + */ + private function assertMethodExists(?string $method): void + { + /** @var MethodModel $model */ + $model = $this->getModel('Method'); + + if (empty($method) || !$model->methodExists($method)) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + } + + /** + * Assert that there is a logged in user. + * + * @return void + * @since 4.2.0 + */ + private function assertLoggedInUser(): void + { + $user = $this->app->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + if ($user->guest) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/administrator/components/com_users/src/Controller/MethodsController.php b/administrator/components/com_users/src/Controller/MethodsController.php index 92a6fd2b20cc9..4eb1909eed1e0 100644 --- a/administrator/components/com_users/src/Controller/MethodsController.php +++ b/administrator/components/com_users/src/Controller/MethodsController.php @@ -1,4 +1,5 @@ assertLoggedInUser(); - - $this->checkToken($this->input->getMethod()); - - // Make sure I am allowed to edit the specified user - $userId = $this->input->getInt('user_id', null); - $user = ($userId === null) - ? $this->app->getIdentity() - : Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); - $user = $user ?? Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - - if (!MfaHelper::canDeleteMethod($user)) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - // Delete all MFA Methods for the user - /** @var MethodsModel $model */ - $model = $this->getModel('Methods'); - $type = null; - $message = null; - - $event = new NotifyActionLog('onComUsersControllerMethodsBeforeDisable', [$user]); - $this->app->getDispatcher()->dispatch($event->getName(), $event); - - try - { - $model->deleteAll($user); - } - catch (Exception $e) - { - $message = $e->getMessage(); - $type = 'error'; - } - - // Redirect + /** + * Public constructor + * + * @param array $config Plugin configuration + * @param MVCFactoryInterface|null $factory MVC Factory for the com_users component + * @param CMSApplication|null $app CMS application object + * @param Input|null $input Joomla CMS input object + * + * @since 4.2.0 + */ + public function __construct($config = [], MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null) + { + // We have to tell Joomla what is the name of the view, otherwise it defaults to the name of the *component*. + $config['default_view'] = 'Methods'; + + parent::__construct($config, $factory, $app, $input); + } + + /** + * Disable Multi-factor Authentication for the current user + * + * @param bool $cachable Can this view be cached + * @param array $urlparams An array of safe url parameters and their variable types, for valid values see + * {@link JFilterInput::clean()}. + * + * @return void + * @since 4.2.0 + */ + public function disable($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + $this->checkToken($this->input->getMethod()); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = ($userId === null) + ? $this->app->getIdentity() + : Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + $user = $user ?? Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + if (!MfaHelper::canDeleteMethod($user)) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + // Delete all MFA Methods for the user + /** @var MethodsModel $model */ + $model = $this->getModel('Methods'); + $type = null; + $message = null; + + $event = new NotifyActionLog('onComUsersControllerMethodsBeforeDisable', [$user]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + try { + $model->deleteAll($user); + } catch (Exception $e) { + $message = $e->getMessage(); + $type = 'error'; + } + + // Redirect // phpcs:ignore $url = Route::_('index.php?option=com_users&task=methods.display&user_id=' . $userId, false); - $returnURL = $this->input->getBase64('returnurl'); - - if (!empty($returnURL)) - { - $url = base64_decode($returnURL); - } - - $this->setRedirect($url, $message, $type); - } - - /** - * List all available Multi-factor Authentication Methods available and guide the user to setting them up - * - * @param bool $cachable Can this view be cached - * @param array $urlparams An array of safe url parameters and their variable types, for valid values see - * {@link JFilterInput::clean()}. - * - * @return void - * @since 4.2.0 - */ - public function display($cachable = false, $urlparams = []): void - { - $this->assertLoggedInUser(); - - // Make sure I am allowed to edit the specified user - $userId = $this->input->getInt('user_id', null); - $user = ($userId === null) - ? $this->app->getIdentity() - : Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); - $user = $user ?? Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - - if (!MfaHelper::canShowConfigurationInterface($user)) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - $returnURL = $this->input->getBase64('returnurl'); - $viewLayout = $this->input->get('layout', 'default', 'string'); - $view = $this->getView('Methods', 'html'); - $view->setLayout($viewLayout); - $view->returnURL = $returnURL; - $view->user = $user; - $view->document = $this->app->getDocument(); - - $methodsModel = $this->getModel('Methods'); - $view->setModel($methodsModel, true); - - $backupCodesModel = $this->getModel('Backupcodes'); - $view->setModel($backupCodesModel, false); - - $view->display(); - } - - /** - * Disable Multi-factor Authentication for the current user - * - * @param bool $cachable Can this view be cached - * @param array $urlparams An array of safe url parameters and their variable types, for valid values see - * {@link JFilterInput::clean()}. - * - * @return void - * @since 4.2.0 - */ - public function doNotShowThisAgain($cachable = false, $urlparams = []): void - { - $this->assertLoggedInUser(); - - $this->checkToken($this->input->getMethod()); - - // Make sure I am allowed to edit the specified user - $userId = $this->input->getInt('user_id', null); - $user = ($userId === null) - ? $this->app->getIdentity() - : Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); - $user = $user ?? Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - - if (!MfaHelper::canAddEditMethod($user)) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - $event = new NotifyActionLog('onComUsersControllerMethodsBeforeDoNotShowThisAgain', [$user]); - $this->app->getDispatcher()->dispatch($event->getName(), $event); - - /** @var MethodsModel $model */ - $model = $this->getModel('Methods'); - $model->setFlag($user, true); - - // Redirect - $url = Uri::base(); - $returnURL = $this->input->getBase64('returnurl'); - - if (!empty($returnURL)) - { - $url = base64_decode($returnURL); - } - - $this->setRedirect($url); - } - - /** - * Assert that there is a user currently logged in - * - * @return void - * @since 4.2.0 - */ - private function assertLoggedInUser(): void - { - $user = $this->app->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - - if ($user->guest) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - } + $returnURL = $this->input->getBase64('returnurl'); + + if (!empty($returnURL)) { + $url = base64_decode($returnURL); + } + + $this->setRedirect($url, $message, $type); + } + + /** + * List all available Multi-factor Authentication Methods available and guide the user to setting them up + * + * @param bool $cachable Can this view be cached + * @param array $urlparams An array of safe url parameters and their variable types, for valid values see + * {@link JFilterInput::clean()}. + * + * @return void + * @since 4.2.0 + */ + public function display($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = ($userId === null) + ? $this->app->getIdentity() + : Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + $user = $user ?? Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + if (!MfaHelper::canShowConfigurationInterface($user)) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $returnURL = $this->input->getBase64('returnurl'); + $viewLayout = $this->input->get('layout', 'default', 'string'); + $view = $this->getView('Methods', 'html'); + $view->setLayout($viewLayout); + $view->returnURL = $returnURL; + $view->user = $user; + $view->document = $this->app->getDocument(); + + $methodsModel = $this->getModel('Methods'); + $view->setModel($methodsModel, true); + + $backupCodesModel = $this->getModel('Backupcodes'); + $view->setModel($backupCodesModel, false); + + $view->display(); + } + + /** + * Disable Multi-factor Authentication for the current user + * + * @param bool $cachable Can this view be cached + * @param array $urlparams An array of safe url parameters and their variable types, for valid values see + * {@link JFilterInput::clean()}. + * + * @return void + * @since 4.2.0 + */ + public function doNotShowThisAgain($cachable = false, $urlparams = []): void + { + $this->assertLoggedInUser(); + + $this->checkToken($this->input->getMethod()); + + // Make sure I am allowed to edit the specified user + $userId = $this->input->getInt('user_id', null); + $user = ($userId === null) + ? $this->app->getIdentity() + : Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); + $user = $user ?? Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + if (!MfaHelper::canAddEditMethod($user)) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $event = new NotifyActionLog('onComUsersControllerMethodsBeforeDoNotShowThisAgain', [$user]); + $this->app->getDispatcher()->dispatch($event->getName(), $event); + + /** @var MethodsModel $model */ + $model = $this->getModel('Methods'); + $model->setFlag($user, true); + + // Redirect + $url = Uri::base(); + $returnURL = $this->input->getBase64('returnurl'); + + if (!empty($returnURL)) { + $url = base64_decode($returnURL); + } + + $this->setRedirect($url); + } + + /** + * Assert that there is a user currently logged in + * + * @return void + * @since 4.2.0 + */ + private function assertLoggedInUser(): void + { + $user = $this->app->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + if ($user->guest) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/administrator/components/com_users/src/Controller/NoteController.php b/administrator/components/com_users/src/Controller/NoteController.php index 34b5ccc374fe6..30e3e3f9113a6 100644 --- a/administrator/components/com_users/src/Controller/NoteController.php +++ b/administrator/components/com_users/src/Controller/NoteController.php @@ -1,4 +1,5 @@ input->get('u_id', 0, 'int'); - - if ($userId) - { - $append .= '&u_id=' . $userId; - } - - return $append; - } + use VersionableControllerTrait; + + /** + * The prefix to use with controller messages. + * + * @var string + * @since 2.5 + */ + protected $text_prefix = 'COM_USERS_NOTE'; + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $key The name of the primary key variable. + * + * @return string The arguments to append to the redirect URL. + * + * @since 2.5 + */ + protected function getRedirectToItemAppend($recordId = null, $key = 'id') + { + $append = parent::getRedirectToItemAppend($recordId, $key); + + $userId = $this->input->get('u_id', 0, 'int'); + + if ($userId) { + $append .= '&u_id=' . $userId; + } + + return $append; + } } diff --git a/administrator/components/com_users/src/Controller/NotesController.php b/administrator/components/com_users/src/Controller/NotesController.php index cb49b92318e7d..71030d6eeb7af 100644 --- a/administrator/components/com_users/src/Controller/NotesController.php +++ b/administrator/components/com_users/src/Controller/NotesController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, $config); - } + /** + * 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 \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 2.5 + */ + public function getModel($name = 'Note', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } } diff --git a/administrator/components/com_users/src/Controller/UserController.php b/administrator/components/com_users/src/Controller/UserController.php index 5694b46d2f1e4..d72c8c844dc61 100644 --- a/administrator/components/com_users/src/Controller/UserController.php +++ b/administrator/components/com_users/src/Controller/UserController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Users\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Users\Administrator\Controller; use Joomla\CMS\Access\Access; use Joomla\CMS\MVC\Controller\FormController; @@ -23,139 +23,132 @@ */ class UserController extends FormController { - /** - * @var string The prefix to use with controller messages. - * @since 1.6 - */ - protected $text_prefix = 'COM_USERS_USER'; - - /** - * Overrides Joomla\CMS\MVC\Controller\FormController::allowEdit - * - * Checks that non-Super Admins are not editing Super Admins. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean True if allowed, false otherwise. - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - // Check if this person is a Super Admin - if (Access::check($data[$key], 'core.admin')) - { - // If I'm not a Super Admin, then disallow the edit. - if (!$this->app->getIdentity()->authorise('core.admin')) - { - return false; - } - } - - // Allow users to edit their own account - if (isset($data[$key]) && (int) $this->app->getIdentity()->id === (int) $data[$key]) - { - return true; - } - - return parent::allowEdit($data, $key); - } - - /** - * Override parent cancel to redirect when using status edit account. - * - * @param string $key The name of the primary key of the URL variable. - * - * @return boolean True if access level checks pass, false otherwise. - * - * @since 4.0.0 - */ - public function cancel($key = null) - { - $result = parent::cancel(); - - if ($return = $this->input->get('return', '', 'BASE64')) - { - $return = base64_decode($return); - - // Don't redirect to an external URL. - if (!Uri::isInternal($return)) - { - $return = Uri::base(); - } - - $this->app->redirect($return); - } - - return $result; - } - - /** - * Override parent save to redirect when using status edit account. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean True if successful, false otherwise. - * - * @since 4.0.0 - */ - public function save($key = null, $urlVar = null) - { - $result = parent::save($key, $urlVar); - - $task = $this->getTask(); - - if ($task === 'save' && $return = $this->input->get('return', '', 'BASE64')) - { - $return = base64_decode($return); - - // Don't redirect to an external URL. - if (!Uri::isInternal($return)) - { - $return = Uri::base(); - } - - $this->setRedirect($return); - } - - return $result; - } - - /** - * Method to run batch operations. - * - * @param object $model The model. - * - * @return boolean True on success, false on failure - * - * @since 2.5 - */ - public function batch($model = null) - { - $this->checkToken(); - - // Set the model - $model = $this->getModel('User', 'Administrator', array()); - - // Preset the redirect - $this->setRedirect(Route::_('index.php?option=com_users&view=users' . $this->getRedirectToListAppend(), false)); - - return parent::batch($model); - } - - /** - * Function that allows child controller access to model data after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 3.1 - */ - protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) - { - } + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_USERS_USER'; + + /** + * Overrides Joomla\CMS\MVC\Controller\FormController::allowEdit + * + * Checks that non-Super Admins are not editing Super Admins. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean True if allowed, false otherwise. + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + // Check if this person is a Super Admin + if (Access::check($data[$key], 'core.admin')) { + // If I'm not a Super Admin, then disallow the edit. + if (!$this->app->getIdentity()->authorise('core.admin')) { + return false; + } + } + + // Allow users to edit their own account + if (isset($data[$key]) && (int) $this->app->getIdentity()->id === (int) $data[$key]) { + return true; + } + + return parent::allowEdit($data, $key); + } + + /** + * Override parent cancel to redirect when using status edit account. + * + * @param string $key The name of the primary key of the URL variable. + * + * @return boolean True if access level checks pass, false otherwise. + * + * @since 4.0.0 + */ + public function cancel($key = null) + { + $result = parent::cancel(); + + if ($return = $this->input->get('return', '', 'BASE64')) { + $return = base64_decode($return); + + // Don't redirect to an external URL. + if (!Uri::isInternal($return)) { + $return = Uri::base(); + } + + $this->app->redirect($return); + } + + return $result; + } + + /** + * Override parent save to redirect when using status edit account. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 4.0.0 + */ + public function save($key = null, $urlVar = null) + { + $result = parent::save($key, $urlVar); + + $task = $this->getTask(); + + if ($task === 'save' && $return = $this->input->get('return', '', 'BASE64')) { + $return = base64_decode($return); + + // Don't redirect to an external URL. + if (!Uri::isInternal($return)) { + $return = Uri::base(); + } + + $this->setRedirect($return); + } + + return $result; + } + + /** + * Method to run batch operations. + * + * @param object $model The model. + * + * @return boolean True on success, false on failure + * + * @since 2.5 + */ + public function batch($model = null) + { + $this->checkToken(); + + // Set the model + $model = $this->getModel('User', 'Administrator', array()); + + // Preset the redirect + $this->setRedirect(Route::_('index.php?option=com_users&view=users' . $this->getRedirectToListAppend(), false)); + + return parent::batch($model); + } + + /** + * Function that allows child controller access to model data after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 3.1 + */ + protected function postSaveHook(BaseDatabaseModel $model, $validData = array()) + { + } } diff --git a/administrator/components/com_users/src/Controller/UsersController.php b/administrator/components/com_users/src/Controller/UsersController.php index f5d9f0b07e7eb..feb029cb001b5 100644 --- a/administrator/components/com_users/src/Controller/UsersController.php +++ b/administrator/components/com_users/src/Controller/UsersController.php @@ -1,4 +1,5 @@ registerTask('block', 'changeBlock'); - $this->registerTask('unblock', 'changeBlock'); - } - - /** - * Proxy for getModel. - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return object The model. - * - * @since 1.6 - */ - public function getModel($name = 'User', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to change the block status on a record. - * - * @return void - * - * @since 1.6 - */ - public function changeBlock() - { - // Check for request forgeries. - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - $values = array('block' => 1, 'unblock' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($values, $task, 0, 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - $this->setMessage(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED'), 'warning'); - } - else - { - // Get the model. - $model = $this->getModel(); - - // Change the state of the records. - if (!$model->block($ids, $value)) - { - $this->setMessage($model->getError(), 'error'); - } - else - { - if ($value == 1) - { - $this->setMessage(Text::plural('COM_USERS_N_USERS_BLOCKED', count($ids))); - } - elseif ($value == 0) - { - $this->setMessage(Text::plural('COM_USERS_N_USERS_UNBLOCKED', count($ids))); - } - } - } - - $this->setRedirect('index.php?option=com_users&view=users'); - } - - /** - * Method to activate a record. - * - * @return void - * - * @since 1.6 - */ - public function activate() - { - // Check for request forgeries. - $this->checkToken(); - - $ids = (array) $this->input->get('cid', array(), 'int'); - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - if (empty($ids)) - { - $this->setMessage(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED'), 'error'); - } - else - { - // Get the model. - $model = $this->getModel(); - - // Change the state of the records. - if (!$model->activate($ids)) - { - $this->setMessage($model->getError(), 'error'); - } - else - { - $this->setMessage(Text::plural('COM_USERS_N_USERS_ACTIVATED', count($ids))); - } - } - - $this->setRedirect('index.php?option=com_users&view=users'); - } - - /** - * Method to get the number of active users - * - * @return void - * - * @since 4.0.0 - */ - public function getQuickiconContent() - { - $model = $this->getModel('Users'); - - $model->setState('filter.state', 0); - - $amount = (int) $model->getTotal(); - - $result = []; - - $result['amount'] = $amount; - $result['sronly'] = Text::plural('COM_USERS_N_QUICKICON_SRONLY', $amount); - $result['name'] = Text::plural('COM_USERS_N_QUICKICON', $amount); - - echo new JsonResponse($result); - } + /** + * @var string The prefix to use with controller messages. + * @since 1.6 + */ + protected $text_prefix = 'COM_USERS_USERS'; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The CMSApplication for the dispatcher + * @param Input $input Input + * + * @since 1.6 + * @see BaseController + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('block', 'changeBlock'); + $this->registerTask('unblock', 'changeBlock'); + } + + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return object The model. + * + * @since 1.6 + */ + public function getModel($name = 'User', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to change the block status on a record. + * + * @return void + * + * @since 1.6 + */ + public function changeBlock() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + $values = array('block' => 1, 'unblock' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($values, $task, 0, 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + $this->setMessage(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED'), 'warning'); + } else { + // Get the model. + $model = $this->getModel(); + + // Change the state of the records. + if (!$model->block($ids, $value)) { + $this->setMessage($model->getError(), 'error'); + } else { + if ($value == 1) { + $this->setMessage(Text::plural('COM_USERS_N_USERS_BLOCKED', count($ids))); + } elseif ($value == 0) { + $this->setMessage(Text::plural('COM_USERS_N_USERS_UNBLOCKED', count($ids))); + } + } + } + + $this->setRedirect('index.php?option=com_users&view=users'); + } + + /** + * Method to activate a record. + * + * @return void + * + * @since 1.6 + */ + public function activate() + { + // Check for request forgeries. + $this->checkToken(); + + $ids = (array) $this->input->get('cid', array(), 'int'); + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + if (empty($ids)) { + $this->setMessage(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED'), 'error'); + } else { + // Get the model. + $model = $this->getModel(); + + // Change the state of the records. + if (!$model->activate($ids)) { + $this->setMessage($model->getError(), 'error'); + } else { + $this->setMessage(Text::plural('COM_USERS_N_USERS_ACTIVATED', count($ids))); + } + } + + $this->setRedirect('index.php?option=com_users&view=users'); + } + + /** + * Method to get the number of active users + * + * @return void + * + * @since 4.0.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('Users'); + + $model->setState('filter.state', 0); + + $amount = (int) $model->getTotal(); + + $result = []; + + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_USERS_N_QUICKICON_SRONLY', $amount); + $result['name'] = Text::plural('COM_USERS_N_QUICKICON', $amount); + + echo new JsonResponse($result); + } } diff --git a/administrator/components/com_users/src/DataShape/CaptiveRenderOptions.php b/administrator/components/com_users/src/DataShape/CaptiveRenderOptions.php index 273bb73de9ed7..4542388548552 100644 --- a/administrator/components/com_users/src/DataShape/CaptiveRenderOptions.php +++ b/administrator/components/com_users/src/DataShape/CaptiveRenderOptions.php @@ -1,4 +1,5 @@ field_type = $value; - } - - /** - * Setter for the input_attributes property. - * - * @param array $value The value to set - * - * @return void - * @@since 4.2.0 - */ + } + + /** + * Setter for the input_attributes property. + * + * @param array $value The value to set + * + * @return void + * @@since 4.2.0 + */ // phpcs:ignore protected function setInput_attributes(array $value) - { - $forbiddenAttributes = ['id', 'type', 'name', 'value']; + { + $forbiddenAttributes = ['id', 'type', 'name', 'value']; - foreach ($forbiddenAttributes as $key) - { - if (isset($value[$key])) - { - unset($value[$key]); - } - } + foreach ($forbiddenAttributes as $key) { + if (isset($value[$key])) { + unset($value[$key]); + } + } // phpcs:ignore $this->input_attributes = $value; - } + } } diff --git a/administrator/components/com_users/src/DataShape/MethodDescriptor.php b/administrator/components/com_users/src/DataShape/MethodDescriptor.php index 4988c573b6d21..2b9ee51080615 100644 --- a/administrator/components/com_users/src/DataShape/MethodDescriptor.php +++ b/administrator/components/com_users/src/DataShape/MethodDescriptor.php @@ -1,4 +1,5 @@ active[$record->id] = $record; - } + /** + * Adds an active MFA method + * + * @param MfaTable $record The MFA method record to add + * + * @return void + * @since 4.2.0 + */ + public function addActiveMethod(MfaTable $record) + { + $this->active[$record->id] = $record; + } } diff --git a/administrator/components/com_users/src/DataShape/SetupRenderOptions.php b/administrator/components/com_users/src/DataShape/SetupRenderOptions.php index fcf612b05c805..3034fd67333bb 100644 --- a/administrator/components/com_users/src/DataShape/SetupRenderOptions.php +++ b/administrator/components/com_users/src/DataShape/SetupRenderOptions.php @@ -1,4 +1,5 @@ custom HTML). See above - * - * @var array - * @since 4.2.0 - */ + /** + * Any tabular data to display (label => custom HTML). See above + * + * @var array + * @since 4.2.0 + */ // phpcs:ignore protected $tabular_data = []; - /** - * Hidden fields to include in the form (name => value) - * - * @var array - * @since 4.2.0 - */ + /** + * Hidden fields to include in the form (name => value) + * + * @var array + * @since 4.2.0 + */ // phpcs:ignore protected $hidden_data = []; - /** - * How to render the MFA setup code field. "input" (HTML input element) or "custom" (custom HTML) - * - * @var string - * @since 4.2.0 - */ + /** + * How to render the MFA setup code field. "input" (HTML input element) or "custom" (custom HTML) + * + * @var string + * @since 4.2.0 + */ // phpcs:ignore protected $field_type = 'input'; - /** - * The type attribute for the HTML input box. Typically "text" or "password". Use any HTML5 input type. - * - * @var string - * @since 4.2.0 - */ + /** + * The type attribute for the HTML input box. Typically "text" or "password". Use any HTML5 input type. + * + * @var string + * @since 4.2.0 + */ // phpcs:ignore protected $input_type = 'text'; - /** - * Attributes other than type and id which will be added to the HTML input box. - * - * @var array - * @@since 4.2.0 - */ + /** + * Attributes other than type and id which will be added to the HTML input box. + * + * @var array + * @@since 4.2.0 + */ // phpcs:ignore protected $input_attributes = []; - /** - * Pre-filled value for the HTML input box. Typically used for fixed codes, the fixed YubiKey ID etc. - * - * @var string - * @since 4.2.0 - */ + /** + * Pre-filled value for the HTML input box. Typically used for fixed codes, the fixed YubiKey ID etc. + * + * @var string + * @since 4.2.0 + */ // phpcs:ignore protected $input_value = ''; - /** - * Placeholder text for the HTML input box. Leave empty if you don't need it. - * - * @var string - * @since 4.2.0 - */ - protected $placeholder = ''; + /** + * Placeholder text for the HTML input box. Leave empty if you don't need it. + * + * @var string + * @since 4.2.0 + */ + protected $placeholder = ''; - /** - * Label to show above the HTML input box. Leave empty if you don't need it. - * - * @var string - * @since 4.2.0 - */ - protected $label = ''; + /** + * Label to show above the HTML input box. Leave empty if you don't need it. + * + * @var string + * @since 4.2.0 + */ + protected $label = ''; - /** - * Custom HTML. Only used when field_type = custom. - * - * @var string - * @since 4.2.0 - */ - protected $html = ''; + /** + * Custom HTML. Only used when field_type = custom. + * + * @var string + * @since 4.2.0 + */ + protected $html = ''; - /** - * Should I show the submit button (apply the MFA setup)? - * - * @var boolean - * @since 4.2.0 - */ + /** + * Should I show the submit button (apply the MFA setup)? + * + * @var boolean + * @since 4.2.0 + */ // phpcs:ignore protected $show_submit = true; - /** - * Additional CSS classes for the submit button (apply the MFA setup) - * - * @var string - * @since 4.2.0 - */ + /** + * Additional CSS classes for the submit button (apply the MFA setup) + * + * @var string + * @since 4.2.0 + */ // phpcs:ignore protected $submit_class = ''; - /** - * Icon class to use for the submit button - * - * @var string - * @since 4.2.0 - */ + /** + * Icon class to use for the submit button + * + * @var string + * @since 4.2.0 + */ // phpcs:ignore protected $submit_icon = 'icon icon-ok'; - /** - * Language key to use for the text on the submit button - * - * @var string - * @since 4.2.0 - */ + /** + * Language key to use for the text on the submit button + * + * @var string + * @since 4.2.0 + */ // phpcs:ignore protected $submit_text = 'JSAVE'; - /** - * Custom HTML to display below the MFA setup form - * - * @var string - * @since 4.2.0 - */ + /** + * Custom HTML to display below the MFA setup form + * + * @var string + * @since 4.2.0 + */ // phpcs:ignore protected $post_message = ''; - /** - * A URL with help content for this Method to display to the user - * - * @var string - * @since 4.2.0 - */ + /** + * A URL with help content for this Method to display to the user + * + * @var string + * @since 4.2.0 + */ // phpcs:ignore protected $help_url = ''; - /** - * Setter for the field_type property - * - * @param string $value One of self::FIELD_INPUT, self::FIELD_CUSTOM - * - * @since 4.2.0 - * @throws InvalidArgumentException - */ + /** + * Setter for the field_type property + * + * @param string $value One of self::FIELD_INPUT, self::FIELD_CUSTOM + * + * @since 4.2.0 + * @throws InvalidArgumentException + */ // phpcs:ignore protected function setField_type($value) - { - if (!in_array($value, [self::FIELD_INPUT, self::FIELD_CUSTOM])) - { - throw new InvalidArgumentException('Invalid value for property field_type.'); - } + { + if (!in_array($value, [self::FIELD_INPUT, self::FIELD_CUSTOM])) { + throw new InvalidArgumentException('Invalid value for property field_type.'); + } // phpcs:ignore $this->field_type = $value; - } + } - /** - * Setter for the input_attributes property. - * - * @param array $value The value to set - * - * @return void - * @@since 4.2.0 - */ + /** + * Setter for the input_attributes property. + * + * @param array $value The value to set + * + * @return void + * @@since 4.2.0 + */ // phpcs:ignore protected function setInput_attributes(array $value) - { - $forbiddenAttributes = ['id', 'type', 'name', 'value']; + { + $forbiddenAttributes = ['id', 'type', 'name', 'value']; - foreach ($forbiddenAttributes as $key) - { - if (isset($value[$key])) - { - unset($value[$key]); - } - } + foreach ($forbiddenAttributes as $key) { + if (isset($value[$key])) { + unset($value[$key]); + } + } // phpcs:ignore $this->input_attributes = $value; - } + } } diff --git a/administrator/components/com_users/src/Dispatcher/Dispatcher.php b/administrator/components/com_users/src/Dispatcher/Dispatcher.php index 2a3b6e79e2773..dc05ceb7654ac 100644 --- a/administrator/components/com_users/src/Dispatcher/Dispatcher.php +++ b/administrator/components/com_users/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ input->getCmd('task'); - $view = $this->input->getCmd('view'); - $layout = $this->input->getCmd('layout'); - $allowedTasks = ['user.edit', 'user.apply', 'user.save', 'user.cancel']; + /** + * Override checkAccess to allow users edit profile without having to have core.manager permission + * + * @return void + * + * @since 4.0.0 + */ + protected function checkAccess() + { + $task = $this->input->getCmd('task'); + $view = $this->input->getCmd('view'); + $layout = $this->input->getCmd('layout'); + $allowedTasks = ['user.edit', 'user.apply', 'user.save', 'user.cancel']; - // Allow users to edit their own account - if (in_array($task, $allowedTasks, true) || ($view === 'user' && $layout === 'edit')) - { - $user = $this->app->getIdentity(); - $id = $this->input->getInt('id'); + // Allow users to edit their own account + if (in_array($task, $allowedTasks, true) || ($view === 'user' && $layout === 'edit')) { + $user = $this->app->getIdentity(); + $id = $this->input->getInt('id'); - if ((int) $user->id === $id) - { - return; - } - } + if ((int) $user->id === $id) { + return; + } + } - /** - * Special case: Multi-factor Authentication - * - * We allow access to all MFA views and tasks. Access control for MFA tasks is performed in - * the Controllers since what is allowed depends on who is logged in and whose account you - * are trying to modify. Implementing these checks in the Dispatcher would violate the - * separation of concerns. - */ - $allowedViews = ['callback', 'captive', 'method', 'methods']; - $isAllowedTask = array_reduce( - $allowedViews, - function ($carry, $taskPrefix) use ($task) - { - return $carry || strpos($task ?? '', $taskPrefix . '.') === 0; - }, - false - ); + /** + * Special case: Multi-factor Authentication + * + * We allow access to all MFA views and tasks. Access control for MFA tasks is performed in + * the Controllers since what is allowed depends on who is logged in and whose account you + * are trying to modify. Implementing these checks in the Dispatcher would violate the + * separation of concerns. + */ + $allowedViews = ['callback', 'captive', 'method', 'methods']; + $isAllowedTask = array_reduce( + $allowedViews, + function ($carry, $taskPrefix) use ($task) { + return $carry || strpos($task ?? '', $taskPrefix . '.') === 0; + }, + false + ); - if (in_array(strtolower($view ?? ''), $allowedViews) || $isAllowedTask) - { - return; - } + if (in_array(strtolower($view ?? ''), $allowedViews) || $isAllowedTask) { + return; + } - parent::checkAccess(); - } + parent::checkAccess(); + } } diff --git a/administrator/components/com_users/src/Extension/UsersComponent.php b/administrator/components/com_users/src/Extension/UsersComponent.php index 0b9dcd89fcf01..3b519c2d7b6cf 100644 --- a/administrator/components/com_users/src/Extension/UsersComponent.php +++ b/administrator/components/com_users/src/Extension/UsersComponent.php @@ -1,4 +1,5 @@ getRegistry()->register('users', new Users); - } + /** + * Booting the extension. This is the function to set up the environment of the extension like + * registering new class loaders, etc. + * + * If required, some initial set up can be done from services of the container, eg. + * registering HTML services. + * + * @param ContainerInterface $container The container + * + * @return void + * + * @since 4.0.0 + */ + public function boot(ContainerInterface $container) + { + $this->getRegistry()->register('users', new Users()); + } - /** - * Returns a valid section for the given section. If it is not valid then null is returned. - * - * @param string $section The section to get the mapping for - * @param object|null $item The content item or null - * - * @return string|null The new section or null - * - * @since 4.0.0 - */ - public function validateSection($section, $item = null) - { - if (Factory::getApplication()->isClient('site')) - { - switch ($section) - { - case 'registration': - case 'profile': - return 'user'; - } - } + /** + * Returns a valid section for the given section. If it is not valid then null is returned. + * + * @param string $section The section to get the mapping for + * @param object|null $item The content item or null + * + * @return string|null The new section or null + * + * @since 4.0.0 + */ + public function validateSection($section, $item = null) + { + if (Factory::getApplication()->isClient('site')) { + switch ($section) { + case 'registration': + case 'profile': + return 'user'; + } + } - if ($section === 'user') - { - return $section; - } + if ($section === 'user') { + return $section; + } - // We don't know other sections. - return null; - } + // We don't know other sections. + return null; + } - /** - * Returns valid contexts. - * - * @return array Associative array with contexts as keys and translated strings as values - * - * @since 4.0.0 - */ - public function getContexts(): array - { - $language = Factory::getApplication()->getLanguage(); - $language->load('com_users', JPATH_ADMINISTRATOR); + /** + * Returns valid contexts. + * + * @return array Associative array with contexts as keys and translated strings as values + * + * @since 4.0.0 + */ + public function getContexts(): array + { + $language = Factory::getApplication()->getLanguage(); + $language->load('com_users', JPATH_ADMINISTRATOR); - return [ - 'com_users.user' => $language->_('COM_USERS'), - ]; - } + return [ + 'com_users.user' => $language->_('COM_USERS'), + ]; + } } diff --git a/administrator/components/com_users/src/Field/GroupparentField.php b/administrator/components/com_users/src/Field/GroupparentField.php index 8bf8532860612..bfce429260ac5 100644 --- a/administrator/components/com_users/src/Field/GroupparentField.php +++ b/administrator/components/com_users/src/Field/GroupparentField.php @@ -1,4 +1,5 @@ $userGroupsOptionsData) - { - if ((int) $userGroupsOptionsData->parent_id === (int) $fatherId) - { - unset($userGroupsOptions[$userGroupsOptionsId]); + /** + * Method to clean the Usergroup Options from all children starting by a given father + * + * @param array $userGroupsOptions The usergroup options to clean + * @param integer $fatherId The father ID to start with + * + * @return array The cleaned field options + * + * @since 3.9.4 + */ + private function cleanOptionsChildrenByFather($userGroupsOptions, $fatherId) + { + foreach ($userGroupsOptions as $userGroupsOptionsId => $userGroupsOptionsData) { + if ((int) $userGroupsOptionsData->parent_id === (int) $fatherId) { + unset($userGroupsOptions[$userGroupsOptionsId]); - $userGroupsOptions = $this->cleanOptionsChildrenByFather($userGroupsOptions, $userGroupsOptionsId); - } - } + $userGroupsOptions = $this->cleanOptionsChildrenByFather($userGroupsOptions, $userGroupsOptionsId); + } + } - return $userGroupsOptions; - } + return $userGroupsOptions; + } - /** - * Method to get the field options. - * - * @return array The field option objects - * - * @since 1.6 - */ - protected function getOptions() - { - $options = UserGroupsHelper::getInstance()->getAll(); - $currentGroupId = (int) Factory::getApplication()->input->get('id', 0, 'int'); + /** + * Method to get the field options. + * + * @return array The field option objects + * + * @since 1.6 + */ + protected function getOptions() + { + $options = UserGroupsHelper::getInstance()->getAll(); + $currentGroupId = (int) Factory::getApplication()->input->get('id', 0, 'int'); - // Prevent to set yourself as parent - if ($currentGroupId) - { - unset($options[$currentGroupId]); - } + // Prevent to set yourself as parent + if ($currentGroupId) { + unset($options[$currentGroupId]); + } - // We should not remove any groups when we are creating a new group - if ($currentGroupId !== 0) - { - // Prevent parenting direct children and children of children of this item. - $options = $this->cleanOptionsChildrenByFather($options, $currentGroupId); - } + // We should not remove any groups when we are creating a new group + if ($currentGroupId !== 0) { + // Prevent parenting direct children and children of children of this item. + $options = $this->cleanOptionsChildrenByFather($options, $currentGroupId); + } - $options = array_values($options); - $isSuperAdmin = Factory::getUser()->authorise('core.admin'); + $options = array_values($options); + $isSuperAdmin = Factory::getUser()->authorise('core.admin'); - // Pad the option text with spaces using depth level as a multiplier. - for ($i = 0, $n = count($options); $i < $n; $i++) - { - // Show groups only if user is super admin or group is not super admin - if ($isSuperAdmin || !Access::checkGroup($options[$i]->id, 'core.admin')) - { - $options[$i]->value = $options[$i]->id; - $options[$i]->text = str_repeat('- ', $options[$i]->level) . $options[$i]->title; - } - else - { - unset($options[$i]); - } - } + // Pad the option text with spaces using depth level as a multiplier. + for ($i = 0, $n = count($options); $i < $n; $i++) { + // Show groups only if user is super admin or group is not super admin + if ($isSuperAdmin || !Access::checkGroup($options[$i]->id, 'core.admin')) { + $options[$i]->value = $options[$i]->id; + $options[$i]->text = str_repeat('- ', $options[$i]->level) . $options[$i]->title; + } else { + unset($options[$i]); + } + } - // Merge any additional options in the XML definition. - return array_merge(parent::getOptions(), $options); - } + // Merge any additional options in the XML definition. + return array_merge(parent::getOptions(), $options); + } } diff --git a/administrator/components/com_users/src/Field/LevelsField.php b/administrator/components/com_users/src/Field/LevelsField.php index ddf70480a85c5..ee78f7a752b8f 100644 --- a/administrator/components/com_users/src/Field/LevelsField.php +++ b/administrator/components/com_users/src/Field/LevelsField.php @@ -1,4 +1,5 @@ getQuery(true) - ->select('name AS text, element AS value') - ->from('#__extensions') - ->where('enabled >= 1') - ->where('type =' . $db->quote('component')); - - $items = $db->setQuery($query)->loadObjectList(); - - if (count($items)) - { - $lang = Factory::getLanguage(); - - foreach ($items as &$item) - { - // Load language - $extension = $item->value; - $source = JPATH_ADMINISTRATOR . '/components/' . $extension; - $lang->load("$extension.sys", JPATH_ADMINISTRATOR) - || $lang->load("$extension.sys", $source); - - // Translate component name - $item->text = Text::_($item->text); - } - - // Sort by component name - $items = ArrayHelper::sortObjects($items, 'text', 1, true, true); - } - - return $items; - } - - /** - * Get a list of the actions for the component or code actions. - * - * @param string $component The name of the component. - * - * @return array - * - * @since 1.6 - */ - public static function getDebugActions($component = null) - { - $actions = array(); - - // Try to get actions for the component - if (!empty($component)) - { - $component_actions = Access::getActionsFromFile(JPATH_ADMINISTRATOR . '/components/' . $component . '/access.xml'); - - if (!empty($component_actions)) - { - foreach ($component_actions as &$action) - { - $descr = (string) $action->title; - - if (!empty($action->description)) - { - $descr = (string) $action->description; - } - - $actions[$action->title] = array($action->name, $descr); - } - } - } - - // Use default actions from configuration if no component selected or component doesn't have actions - if (empty($actions)) - { - $filename = JPATH_ADMINISTRATOR . '/components/com_config/forms/application.xml'; - - if (is_file($filename)) - { - $xml = simplexml_load_file($filename); - - foreach ($xml->children()->fieldset as $fieldset) - { - if ('permissions' == (string) $fieldset['name']) - { - foreach ($fieldset->children() as $field) - { - if ('rules' == (string) $field['name']) - { - foreach ($field->children() as $action) - { - $descr = (string) $action['title']; - - if (isset($action['description']) && !empty($action['description'])) - { - $descr = (string) $action['description']; - } - - $actions[(string) $action['title']] = array( - (string) $action['name'], - $descr - ); - } - - break; - } - } - } - } - - // Load language - $lang = Factory::getLanguage(); - $extension = 'com_config'; - $source = JPATH_ADMINISTRATOR . '/components/' . $extension; - - $lang->load($extension, JPATH_ADMINISTRATOR, null, false, false) - || $lang->load($extension, $source, null, false, false) - || $lang->load($extension, JPATH_ADMINISTRATOR, $lang->getDefault(), false, false) - || $lang->load($extension, $source, $lang->getDefault(), false, false); - } - } - - return $actions; - } - - /** - * Get a list of filter options for the levels. - * - * @return array An array of \JHtmlOption elements. - */ - public static function getLevelsOptions() - { - // Build the filter options. - $options = array(); - $options[] = HTMLHelper::_('select.option', '1', Text::sprintf('COM_USERS_OPTION_LEVEL_COMPONENT', 1)); - $options[] = HTMLHelper::_('select.option', '2', Text::sprintf('COM_USERS_OPTION_LEVEL_CATEGORY', 2)); - $options[] = HTMLHelper::_('select.option', '3', Text::sprintf('COM_USERS_OPTION_LEVEL_DEEPER', 3)); - $options[] = HTMLHelper::_('select.option', '4', '4'); - $options[] = HTMLHelper::_('select.option', '5', '5'); - $options[] = HTMLHelper::_('select.option', '6', '6'); - - return $options; - } + /** + * Get a list of the components. + * + * @return array + * + * @since 1.6 + */ + public static function getComponents() + { + // Initialise variable. + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('name AS text, element AS value') + ->from('#__extensions') + ->where('enabled >= 1') + ->where('type =' . $db->quote('component')); + + $items = $db->setQuery($query)->loadObjectList(); + + if (count($items)) { + $lang = Factory::getLanguage(); + + foreach ($items as &$item) { + // Load language + $extension = $item->value; + $source = JPATH_ADMINISTRATOR . '/components/' . $extension; + $lang->load("$extension.sys", JPATH_ADMINISTRATOR) + || $lang->load("$extension.sys", $source); + + // Translate component name + $item->text = Text::_($item->text); + } + + // Sort by component name + $items = ArrayHelper::sortObjects($items, 'text', 1, true, true); + } + + return $items; + } + + /** + * Get a list of the actions for the component or code actions. + * + * @param string $component The name of the component. + * + * @return array + * + * @since 1.6 + */ + public static function getDebugActions($component = null) + { + $actions = array(); + + // Try to get actions for the component + if (!empty($component)) { + $component_actions = Access::getActionsFromFile(JPATH_ADMINISTRATOR . '/components/' . $component . '/access.xml'); + + if (!empty($component_actions)) { + foreach ($component_actions as &$action) { + $descr = (string) $action->title; + + if (!empty($action->description)) { + $descr = (string) $action->description; + } + + $actions[$action->title] = array($action->name, $descr); + } + } + } + + // Use default actions from configuration if no component selected or component doesn't have actions + if (empty($actions)) { + $filename = JPATH_ADMINISTRATOR . '/components/com_config/forms/application.xml'; + + if (is_file($filename)) { + $xml = simplexml_load_file($filename); + + foreach ($xml->children()->fieldset as $fieldset) { + if ('permissions' == (string) $fieldset['name']) { + foreach ($fieldset->children() as $field) { + if ('rules' == (string) $field['name']) { + foreach ($field->children() as $action) { + $descr = (string) $action['title']; + + if (isset($action['description']) && !empty($action['description'])) { + $descr = (string) $action['description']; + } + + $actions[(string) $action['title']] = array( + (string) $action['name'], + $descr + ); + } + + break; + } + } + } + } + + // Load language + $lang = Factory::getLanguage(); + $extension = 'com_config'; + $source = JPATH_ADMINISTRATOR . '/components/' . $extension; + + $lang->load($extension, JPATH_ADMINISTRATOR, null, false, false) + || $lang->load($extension, $source, null, false, false) + || $lang->load($extension, JPATH_ADMINISTRATOR, $lang->getDefault(), false, false) + || $lang->load($extension, $source, $lang->getDefault(), false, false); + } + } + + return $actions; + } + + /** + * Get a list of filter options for the levels. + * + * @return array An array of \JHtmlOption elements. + */ + public static function getLevelsOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '1', Text::sprintf('COM_USERS_OPTION_LEVEL_COMPONENT', 1)); + $options[] = HTMLHelper::_('select.option', '2', Text::sprintf('COM_USERS_OPTION_LEVEL_CATEGORY', 2)); + $options[] = HTMLHelper::_('select.option', '3', Text::sprintf('COM_USERS_OPTION_LEVEL_DEEPER', 3)); + $options[] = HTMLHelper::_('select.option', '4', '4'); + $options[] = HTMLHelper::_('select.option', '5', '5'); + $options[] = HTMLHelper::_('select.option', '6', '6'); + + return $options; + } } diff --git a/administrator/components/com_users/src/Helper/Mfa.php b/administrator/components/com_users/src/Helper/Mfa.php index 34b7ab5c84ae5..1964296ff4785 100644 --- a/administrator/components/com_users/src/Helper/Mfa.php +++ b/administrator/components/com_users/src/Helper/Mfa.php @@ -1,4 +1,5 @@ input->getCmd('option', '') === 'com_users') - { - $app->getLanguage()->load('com_users'); - $app->getDocument() - ->getWebAssetManager() - ->getRegistry() - ->addExtensionRegistryFile('com_users'); - } - - // Get a model - /** @var MVCFactoryInterface $factory */ - $factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory(); - - /** @var MethodsModel $methodsModel */ - $methodsModel = $factory->createModel('Methods', 'Administrator'); - /** @var BackupcodesModel $methodsModel */ - $backupCodesModel = $factory->createModel('Backupcodes', 'Administrator'); - - // Get a view object - $appRoot = $app->isClient('site') ? \JPATH_SITE : \JPATH_ADMINISTRATOR; - $prefix = $app->isClient('site') ? 'Site' : 'Administrator'; - /** @var HtmlView $view */ - $view = $factory->createView('Methods', $prefix, 'Html', - [ - 'base_path' => $appRoot . '/components/com_users', - ] - ); - $view->setModel($methodsModel, true); - /** @noinspection PhpParamsInspection */ - $view->setModel($backupCodesModel); - $view->document = $app->getDocument(); - $view->returnURL = base64_encode(Uri::getInstance()->toString()); - $view->user = $user; - $view->set('forHMVC', true); - - @ob_start(); - - try - { - $view->display(); - } - catch (\Throwable $e) - { - @ob_end_clean(); - - /** - * This is intentional! When you are developing a Multi-factor Authentication plugin you - * will inevitably mess something up and end up with an error. This would cause the - * entire MFA configuration page to disappear. No problem! Set Debug System to Yes in - * Global Configuration and you can see the error exception which will help you solve - * your problem. - */ - if (defined('JDEBUG') && JDEBUG) - { - throw $e; - } - - return null; - } - - return @ob_get_clean(); - } - - /** - * Get a list of all of the MFA Methods - * - * @return MethodDescriptor[] - * @since 4.2.0 - */ - public static function getMfaMethods(): array - { - PluginHelper::importPlugin('multifactorauth'); - - if (is_null(self::$allMFAs)) - { - // Get all the plugin results - $event = new GetMethod; - $temp = Factory::getApplication() - ->getDispatcher() - ->dispatch($event->getName(), $event) - ->getArgument('result', []); - - // Normalize the results - self::$allMFAs = []; - - foreach ($temp as $method) - { - if (!is_array($method) && !($method instanceof MethodDescriptor)) - { - continue; - } - - $method = $method instanceof MethodDescriptor - ? $method : new MethodDescriptor($method); - - if (empty($method['name'])) - { - continue; - } - - self::$allMFAs[$method['name']] = $method; - } - } - - return self::$allMFAs; - } - - /** - * Is the current user allowed to add/edit MFA methods for $user? - * - * This is only allowed if I am adding / editing methods for myself. - * - * If the target user is a member of any group disallowed to use MFA this will return false. - * - * @param User|null $user The user you want to know if we're allowed to edit - * - * @return boolean - * @throws Exception - * @since 4.2.0 - */ - public static function canAddEditMethod(?User $user = null): bool - { - // Cannot do MFA operations on no user or a guest user. - if (is_null($user) || $user->guest) - { - return false; - } - - // If the user is in a user group which disallows MFA we cannot allow adding / editing methods. - $neverMFAGroups = ComponentHelper::getParams('com_users')->get('neverMFAUserGroups', []); - $neverMFAGroups = is_array($neverMFAGroups) ? $neverMFAGroups : []; - - if (count(array_intersect($user->getAuthorisedGroups(), $neverMFAGroups))) - { - return false; - } - - // Check if this is the same as the logged-in user. - $myUser = Factory::getApplication()->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - - return $myUser->id === $user->id; - } - - /** - * Is the current user allowed to delete MFA methods / disable MFA for $user? - * - * This is allowed if: - * - The user being queried is the same as the logged-in user - * - The logged-in user is a Super User AND the queried user is NOT a Super User. - * - * Note that Super Users can be edited by their own user only for security reasons. If a Super - * User gets locked out they must use the Backup Codes to regain access. If that's not possible, - * they will need to delete their records from the `#__user_mfa` table. - * - * @param User|null $user The user being queried. - * - * @return boolean - * @throws Exception - * @since 4.2.0 - */ - public static function canDeleteMethod(?User $user = null): bool - { - // Cannot do MFA operations on no user or a guest user. - if (is_null($user) || $user->guest) - { - return false; - } - - $myUser = Factory::getApplication()->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - - return $myUser->id === $user->id - || ($myUser->authorise('core.admin') && !$user->authorise('core.admin')); - } - - /** - * Return all MFA records for a specific user - * - * @param int|null $userId User ID. NULL for currently logged in user. - * - * @return MfaTable[] - * @throws Exception - * - * @since 4.2.0 - */ - public static function getUserMfaRecords(?int $userId): array - { - if (empty($userId)) - { - $user = Factory::getApplication()->getIdentity() ?: Factory::getUser(); - $userId = $user->id ?: 0; - } - - /** @var DatabaseDriver $db */ - $db = Factory::getContainer()->get('DatabaseDriver'); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__user_mfa')) - ->where($db->quoteName('user_id') . ' = :user_id') - ->bind(':user_id', $userId, ParameterType::INTEGER); - - try - { - $ids = $db->setQuery($query)->loadColumn() ?: []; - } - catch (Exception $e) - { - $ids = []; - } - - if (empty($ids)) - { - return []; - } - - /** @var MVCFactoryInterface $factory */ - $factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory(); - - // Map all results to MFA table objects - $records = array_map( - function ($id) use ($factory) - { - /** @var MfaTable $record */ - $record = $factory->createTable('Mfa', 'Administrator'); - $loaded = $record->load($id); - - return $loaded ? $record : null; - }, - $ids - ); - - // Let's remove Methods we couldn't decrypt when reading from the database. - $hasBackupCodes = false; - - $records = array_filter( - $records, - function ($record) use (&$hasBackupCodes) - { - $isValid = !is_null($record) && (!empty($record->options)); - - if ($isValid && ($record->method === 'backupcodes')) - { - $hasBackupCodes = true; - } - - return $isValid; - } - ); - - // If the only Method is backup codes it's as good as having no records - if ((count($records) === 1) && $hasBackupCodes) - { - return []; - } - - return $records; - } - - /** - * Are the conditions for showing the MFA configuration interface met? - * - * @param User|null $user The user to be configured - * - * @return boolean - * @throws Exception - * @since 4.2.0 - */ - public static function canShowConfigurationInterface(?User $user = null): bool - { - // If I have no user to check against that's all the checking I can do. - if (empty($user)) - { - return false; - } - - // I need at least one MFA method plugin for the setup interface to make any sense. - $plugins = PluginHelper::getPlugin('multifactorauth'); - - if (count($plugins) < 1) - { - return false; - } - - /** @var CMSApplication $app */ - $app = Factory::getApplication(); - - // We can only show a configuration page in the front- or backend application. - if (!$app->isClient('site') && !$app->isClient('administrator')) - { - return false; - } - - // Only show the configuration page if we have an HTML document - if (!($app->getDocument() instanceof HtmlDocument)) - { - return false; - } - - // I must be able to add, edit or delete the user's MFA settings - return self::canAddEditMethod($user) || self::canDeleteMethod($user); - } + /** + * Cache of all currently active MFAs + * + * @var array|null + * @since 4.2.0 + */ + protected static $allMFAs = null; + + /** + * Are we inside the administrator application + * + * @var boolean + * @since 4.2.0 + */ + protected static $isAdmin = null; + + /** + * Get the HTML for the Multi-factor Authentication configuration interface for a user. + * + * This helper method uses a sort of primitive HMVC to display the com_users' Methods page which + * renders the MFA configuration interface. + * + * @param User $user The user we are going to show the configuration UI for. + * + * @return string|null The HTML of the UI; null if we cannot / must not show it. + * @throws Exception + * @since 4.2.0 + */ + public static function getConfigurationInterface(User $user): ?string + { + // Check the conditions + if (!self::canShowConfigurationInterface($user)) { + return null; + } + + /** @var CMSApplication $app */ + $app = Factory::getApplication(); + + if (!$app->input->getCmd('option', '') === 'com_users') { + $app->getLanguage()->load('com_users'); + $app->getDocument() + ->getWebAssetManager() + ->getRegistry() + ->addExtensionRegistryFile('com_users'); + } + + // Get a model + /** @var MVCFactoryInterface $factory */ + $factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory(); + + /** @var MethodsModel $methodsModel */ + $methodsModel = $factory->createModel('Methods', 'Administrator'); + /** @var BackupcodesModel $methodsModel */ + $backupCodesModel = $factory->createModel('Backupcodes', 'Administrator'); + + // Get a view object + $appRoot = $app->isClient('site') ? \JPATH_SITE : \JPATH_ADMINISTRATOR; + $prefix = $app->isClient('site') ? 'Site' : 'Administrator'; + /** @var HtmlView $view */ + $view = $factory->createView( + 'Methods', + $prefix, + 'Html', + [ + 'base_path' => $appRoot . '/components/com_users', + ] + ); + $view->setModel($methodsModel, true); + /** @noinspection PhpParamsInspection */ + $view->setModel($backupCodesModel); + $view->document = $app->getDocument(); + $view->returnURL = base64_encode(Uri::getInstance()->toString()); + $view->user = $user; + $view->set('forHMVC', true); + + @ob_start(); + + try { + $view->display(); + } catch (\Throwable $e) { + @ob_end_clean(); + + /** + * This is intentional! When you are developing a Multi-factor Authentication plugin you + * will inevitably mess something up and end up with an error. This would cause the + * entire MFA configuration page to disappear. No problem! Set Debug System to Yes in + * Global Configuration and you can see the error exception which will help you solve + * your problem. + */ + if (defined('JDEBUG') && JDEBUG) { + throw $e; + } + + return null; + } + + return @ob_get_clean(); + } + + /** + * Get a list of all of the MFA Methods + * + * @return MethodDescriptor[] + * @since 4.2.0 + */ + public static function getMfaMethods(): array + { + PluginHelper::importPlugin('multifactorauth'); + + if (is_null(self::$allMFAs)) { + // Get all the plugin results + $event = new GetMethod(); + $temp = Factory::getApplication() + ->getDispatcher() + ->dispatch($event->getName(), $event) + ->getArgument('result', []); + + // Normalize the results + self::$allMFAs = []; + + foreach ($temp as $method) { + if (!is_array($method) && !($method instanceof MethodDescriptor)) { + continue; + } + + $method = $method instanceof MethodDescriptor + ? $method : new MethodDescriptor($method); + + if (empty($method['name'])) { + continue; + } + + self::$allMFAs[$method['name']] = $method; + } + } + + return self::$allMFAs; + } + + /** + * Is the current user allowed to add/edit MFA methods for $user? + * + * This is only allowed if I am adding / editing methods for myself. + * + * If the target user is a member of any group disallowed to use MFA this will return false. + * + * @param User|null $user The user you want to know if we're allowed to edit + * + * @return boolean + * @throws Exception + * @since 4.2.0 + */ + public static function canAddEditMethod(?User $user = null): bool + { + // Cannot do MFA operations on no user or a guest user. + if (is_null($user) || $user->guest) { + return false; + } + + // If the user is in a user group which disallows MFA we cannot allow adding / editing methods. + $neverMFAGroups = ComponentHelper::getParams('com_users')->get('neverMFAUserGroups', []); + $neverMFAGroups = is_array($neverMFAGroups) ? $neverMFAGroups : []; + + if (count(array_intersect($user->getAuthorisedGroups(), $neverMFAGroups))) { + return false; + } + + // Check if this is the same as the logged-in user. + $myUser = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + return $myUser->id === $user->id; + } + + /** + * Is the current user allowed to delete MFA methods / disable MFA for $user? + * + * This is allowed if: + * - The user being queried is the same as the logged-in user + * - The logged-in user is a Super User AND the queried user is NOT a Super User. + * + * Note that Super Users can be edited by their own user only for security reasons. If a Super + * User gets locked out they must use the Backup Codes to regain access. If that's not possible, + * they will need to delete their records from the `#__user_mfa` table. + * + * @param User|null $user The user being queried. + * + * @return boolean + * @throws Exception + * @since 4.2.0 + */ + public static function canDeleteMethod(?User $user = null): bool + { + // Cannot do MFA operations on no user or a guest user. + if (is_null($user) || $user->guest) { + return false; + } + + $myUser = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + return $myUser->id === $user->id + || ($myUser->authorise('core.admin') && !$user->authorise('core.admin')); + } + + /** + * Return all MFA records for a specific user + * + * @param int|null $userId User ID. NULL for currently logged in user. + * + * @return MfaTable[] + * @throws Exception + * + * @since 4.2.0 + */ + public static function getUserMfaRecords(?int $userId): array + { + if (empty($userId)) { + $user = Factory::getApplication()->getIdentity() ?: Factory::getUser(); + $userId = $user->id ?: 0; + } + + /** @var DatabaseDriver $db */ + $db = Factory::getContainer()->get('DatabaseDriver'); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__user_mfa')) + ->where($db->quoteName('user_id') . ' = :user_id') + ->bind(':user_id', $userId, ParameterType::INTEGER); + + try { + $ids = $db->setQuery($query)->loadColumn() ?: []; + } catch (Exception $e) { + $ids = []; + } + + if (empty($ids)) { + return []; + } + + /** @var MVCFactoryInterface $factory */ + $factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory(); + + // Map all results to MFA table objects + $records = array_map( + function ($id) use ($factory) { + /** @var MfaTable $record */ + $record = $factory->createTable('Mfa', 'Administrator'); + $loaded = $record->load($id); + + return $loaded ? $record : null; + }, + $ids + ); + + // Let's remove Methods we couldn't decrypt when reading from the database. + $hasBackupCodes = false; + + $records = array_filter( + $records, + function ($record) use (&$hasBackupCodes) { + $isValid = !is_null($record) && (!empty($record->options)); + + if ($isValid && ($record->method === 'backupcodes')) { + $hasBackupCodes = true; + } + + return $isValid; + } + ); + + // If the only Method is backup codes it's as good as having no records + if ((count($records) === 1) && $hasBackupCodes) { + return []; + } + + return $records; + } + + /** + * Are the conditions for showing the MFA configuration interface met? + * + * @param User|null $user The user to be configured + * + * @return boolean + * @throws Exception + * @since 4.2.0 + */ + public static function canShowConfigurationInterface(?User $user = null): bool + { + // If I have no user to check against that's all the checking I can do. + if (empty($user)) { + return false; + } + + // I need at least one MFA method plugin for the setup interface to make any sense. + $plugins = PluginHelper::getPlugin('multifactorauth'); + + if (count($plugins) < 1) { + return false; + } + + /** @var CMSApplication $app */ + $app = Factory::getApplication(); + + // We can only show a configuration page in the front- or backend application. + if (!$app->isClient('site') && !$app->isClient('administrator')) { + return false; + } + + // Only show the configuration page if we have an HTML document + if (!($app->getDocument() instanceof HtmlDocument)) { + return false; + } + + // I must be able to add, edit or delete the user's MFA settings + return self::canAddEditMethod($user) || self::canDeleteMethod($user); + } } diff --git a/administrator/components/com_users/src/Helper/UsersHelper.php b/administrator/components/com_users/src/Helper/UsersHelper.php index e8ef7828aead1..41e13cb51662d 100644 --- a/administrator/components/com_users/src/Helper/UsersHelper.php +++ b/administrator/components/com_users/src/Helper/UsersHelper.php @@ -1,4 +1,5 @@ getAll(); - - foreach ($options as &$option) - { - $option->value = $option->id; - $option->text = str_repeat('- ', $option->level) . $option->title; - } - - return $options; - } - - /** - * Creates a list of range options used in filter select list - * used in com_users on users view - * - * @return array - * - * @since 2.5 - */ - public static function getRangeOptions() - { - $options = array( - HTMLHelper::_('select.option', 'today', Text::_('COM_USERS_OPTION_RANGE_TODAY')), - HTMLHelper::_('select.option', 'past_week', Text::_('COM_USERS_OPTION_RANGE_PAST_WEEK')), - HTMLHelper::_('select.option', 'past_1month', Text::_('COM_USERS_OPTION_RANGE_PAST_1MONTH')), - HTMLHelper::_('select.option', 'past_3month', Text::_('COM_USERS_OPTION_RANGE_PAST_3MONTH')), - HTMLHelper::_('select.option', 'past_6month', Text::_('COM_USERS_OPTION_RANGE_PAST_6MONTH')), - HTMLHelper::_('select.option', 'past_year', Text::_('COM_USERS_OPTION_RANGE_PAST_YEAR')), - HTMLHelper::_('select.option', 'post_year', Text::_('COM_USERS_OPTION_RANGE_POST_YEAR')), - ); - - return $options; - } - - /** - * No longer used. - * - * @return array - * - * @since 3.2.0 - * @throws \Exception - * - * @deprecated 4.2.0 Will be removed in 5.0 - */ - public static function getTwoFactorMethods() - { - return []; - } - - /** - * Get a list of the User Groups for Viewing Access Levels - * - * @param string $rules User Groups in JSON format - * - * @return string $groups Comma separated list of User Groups - * - * @since 3.6 - */ - public static function getVisibleByGroups($rules) - { - $rules = json_decode($rules); - - if (!$rules) - { - return false; - } - - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('title', 'text')) - ->from($db->quoteName('#__usergroups')) - ->whereIn($db->quoteName('id'), $rules); - $db->setQuery($query); - - $groups = $db->loadColumn(); - $groups = implode(', ', $groups); - - return $groups; - } - - /** - * Returns a valid section for users. If it is not valid then null - * is returned. - * - * @param string $section The section to get the mapping for - * - * @return string|null The new section - * - * @since 3.7.0 - * @throws \Exception - * @deprecated 5.0 Use \Joomla\Component\Users\Administrator\Extension\UsersComponent::validateSection() instead. - */ - public static function validateSection($section) - { - return Factory::getApplication()->bootComponent('com_users')->validateSection($section, null); - } - - /** - * Returns valid contexts - * - * @return array - * - * @since 3.7.0 - * @deprecated 5.0 Use \Joomla\Component\Users\Administrator\Extension\UsersComponent::getContexts() instead. - */ - public static function getContexts() - { - return Factory::getApplication()->bootComponent('com_users')->getContexts(); - } + /** + * @var CMSObject A cache for the available actions. + * @since 1.6 + */ + protected static $actions; + + /** + * Get a list of filter options for the blocked state of a user. + * + * @return array An array of \JHtmlOption elements. + * + * @since 1.6 + */ + public static function getStateOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '0', Text::_('JENABLED')); + $options[] = HTMLHelper::_('select.option', '1', Text::_('JDISABLED')); + + return $options; + } + + /** + * Get a list of filter options for the activated state of a user. + * + * @return array An array of \JHtmlOption elements. + * + * @since 1.6 + */ + public static function getActiveOptions() + { + // Build the filter options. + $options = array(); + $options[] = HTMLHelper::_('select.option', '0', Text::_('COM_USERS_ACTIVATED')); + $options[] = HTMLHelper::_('select.option', '1', Text::_('COM_USERS_UNACTIVATED')); + + return $options; + } + + /** + * Get a list of the user groups for filtering. + * + * @return array An array of \JHtmlOption elements. + * + * @since 1.6 + */ + public static function getGroups() + { + $options = UserGroupsHelper::getInstance()->getAll(); + + foreach ($options as &$option) { + $option->value = $option->id; + $option->text = str_repeat('- ', $option->level) . $option->title; + } + + return $options; + } + + /** + * Creates a list of range options used in filter select list + * used in com_users on users view + * + * @return array + * + * @since 2.5 + */ + public static function getRangeOptions() + { + $options = array( + HTMLHelper::_('select.option', 'today', Text::_('COM_USERS_OPTION_RANGE_TODAY')), + HTMLHelper::_('select.option', 'past_week', Text::_('COM_USERS_OPTION_RANGE_PAST_WEEK')), + HTMLHelper::_('select.option', 'past_1month', Text::_('COM_USERS_OPTION_RANGE_PAST_1MONTH')), + HTMLHelper::_('select.option', 'past_3month', Text::_('COM_USERS_OPTION_RANGE_PAST_3MONTH')), + HTMLHelper::_('select.option', 'past_6month', Text::_('COM_USERS_OPTION_RANGE_PAST_6MONTH')), + HTMLHelper::_('select.option', 'past_year', Text::_('COM_USERS_OPTION_RANGE_PAST_YEAR')), + HTMLHelper::_('select.option', 'post_year', Text::_('COM_USERS_OPTION_RANGE_POST_YEAR')), + ); + + return $options; + } + + /** + * No longer used. + * + * @return array + * + * @since 3.2.0 + * @throws \Exception + * + * @deprecated 4.2.0 Will be removed in 5.0 + */ + public static function getTwoFactorMethods() + { + return []; + } + + /** + * Get a list of the User Groups for Viewing Access Levels + * + * @param string $rules User Groups in JSON format + * + * @return string $groups Comma separated list of User Groups + * + * @since 3.6 + */ + public static function getVisibleByGroups($rules) + { + $rules = json_decode($rules); + + if (!$rules) { + return false; + } + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('title', 'text')) + ->from($db->quoteName('#__usergroups')) + ->whereIn($db->quoteName('id'), $rules); + $db->setQuery($query); + + $groups = $db->loadColumn(); + $groups = implode(', ', $groups); + + return $groups; + } + + /** + * Returns a valid section for users. If it is not valid then null + * is returned. + * + * @param string $section The section to get the mapping for + * + * @return string|null The new section + * + * @since 3.7.0 + * @throws \Exception + * @deprecated 5.0 Use \Joomla\Component\Users\Administrator\Extension\UsersComponent::validateSection() instead. + */ + public static function validateSection($section) + { + return Factory::getApplication()->bootComponent('com_users')->validateSection($section, null); + } + + /** + * Returns valid contexts + * + * @return array + * + * @since 3.7.0 + * @deprecated 5.0 Use \Joomla\Component\Users\Administrator\Extension\UsersComponent::getContexts() instead. + */ + public static function getContexts() + { + return Factory::getApplication()->bootComponent('com_users')->getContexts(); + } } diff --git a/administrator/components/com_users/src/Model/BackupcodesModel.php b/administrator/components/com_users/src/Model/BackupcodesModel.php index 26d5d2abd5f00..442a4ed224098 100644 --- a/administrator/components/com_users/src/Model/BackupcodesModel.php +++ b/administrator/components/com_users/src/Model/BackupcodesModel.php @@ -1,4 +1,5 @@ getIdentity() ?: - Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - /** @var MfaTable $record */ - $record = $this->getTable('Mfa', 'Administrator'); - $loaded = $record->load( - [ - 'user_id' => $user->id, - 'method' => 'backupcodes', - ] - ); - - if (!$loaded) - { - $record = null; - } - - return $record; - } - - /** - * Generate a new set of backup codes for the specified user. The generated codes are immediately saved to the - * database and the internal cache is updated. - * - * @param User|null $user Which user to generate codes for? - * - * @return void - * @throws \Exception - * @since 4.2.0 - */ - public function regenerateBackupCodes(User $user = null): void - { - // Make sure I have a user - if (empty($user)) - { - $user = Factory::getApplication()->getIdentity() ?: - Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - // Generate backup codes - $backupCodes = []; - - for ($i = 0; $i < 10; $i++) - { - // Each backup code is 2 groups of 4 digits - $backupCodes[$i] = sprintf('%04u%04u', random_int(0, 9999), random_int(0, 9999)); - } - - // Save the backup codes to the database and update the cache - $this->saveBackupCodes($backupCodes, $user); - } - - /** - * Saves the backup codes to the database - * - * @param array $codes An array of exactly 10 elements - * @param User|null $user The user for which to save the backup codes - * - * @return boolean - * @throws \Exception - * @since 4.2.0 - */ - public function saveBackupCodes(array $codes, ?User $user = null): bool - { - // Make sure I have a user - if (empty($user)) - { - $user = Factory::getApplication()->getIdentity() ?: - Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - // Try to load existing backup codes - $existingCodes = $this->getBackupCodes($user); - $jNow = Date::getInstance(); - - /** @var MfaTable $record */ - $record = $this->getTable('Mfa', 'Administrator'); - - if (is_null($existingCodes)) - { - $record->reset(); - - $newData = [ - 'user_id' => $user->id, - 'title' => Text::_('COM_USERS_PROFILE_OTEPS'), - 'method' => 'backupcodes', - 'default' => 0, - 'created_on' => $jNow->toSql(), - 'options' => $codes, - ]; - } - else - { - $record->load( - [ - 'user_id' => $user->id, - 'method' => 'backupcodes', - ] - ); - - $newData = [ - 'options' => $codes, - ]; - } - - $saved = $record->save($newData); - - if (!$saved) - { - return false; - } - - // Finally, update the cache - $this->cache[$user->id] = $codes; - - return true; - } - - /** - * Returns the backup codes for the specified user. Cached values will be preferentially returned, therefore you - * MUST go through this model's Methods ONLY when dealing with backup codes. - * - * @param User|null $user The user for which you want the backup codes - * - * @return array|null The backup codes, or null if they do not exist - * @throws \Exception - * @since 4.2.0 - */ - public function getBackupCodes(User $user = null): ?array - { - // Make sure I have a user - if (empty($user)) - { - $user = Factory::getApplication()->getIdentity() ?: - Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - if (isset($this->cache[$user->id])) - { - return $this->cache[$user->id]; - } - - // If there is no cached record try to load it from the database - $this->cache[$user->id] = null; - - // Try to load the record - /** @var MfaTable $record */ - $record = $this->getTable('Mfa', 'Administrator'); - $loaded = $record->load( - [ - 'user_id' => $user->id, - 'method' => 'backupcodes', - ] - ); - - if ($loaded) - { - $this->cache[$user->id] = $record->options; - } - - return $this->cache[$user->id]; - } - - /** - * Check if the provided string is a backup code. If it is, it will be removed from the list (replaced with an empty - * string) and the codes will be saved to the database. All comparisons are performed in a timing safe manner. - * - * @param string $code The code to check - * @param User|null $user The user to check against - * - * @return boolean - * @throws \Exception - * @since 4.2.0 - */ - public function isBackupCode($code, ?User $user = null): bool - { - // Load the backup codes - $codes = $this->getBackupCodes($user) ?: array_fill(0, 10, ''); - - // Keep only the numbers in the provided $code - $code = filter_var($code, FILTER_SANITIZE_NUMBER_INT); - $code = trim($code); - - // Check if the code is in the array. We always check against ten codes to prevent timing attacks which - // determine the amount of codes. - $result = false; - - // The two arrays let us always add an element to an array, therefore having PHP expend the same amount of time - // for the correct code, the incorrect codes and the fake codes. - $newArray = []; - $dummyArray = []; - - $realLength = count($codes); - $restLength = 10 - $realLength; - - for ($i = 0; $i < $realLength; $i++) - { - if (hash_equals($codes[$i], $code)) - { - // This may seem redundant but makes sure both branches of the if-block are isochronous - $result = $result || true; - $newArray[] = ''; - $dummyArray[] = $codes[$i]; - } - else - { - // This may seem redundant but makes sure both branches of the if-block are isochronous - $result = $result || false; - $dummyArray[] = ''; - $newArray[] = $codes[$i]; - } - } - - /** - * This is an intentional waste of time, symmetrical to the code above, making sure - * evaluating each of the total of ten elements takes the same time. This code should never - * run UNLESS someone messed up with our backup codes array and it no longer contains 10 - * elements. - */ - $otherResult = false; - - $temp1 = ''; - - for ($i = 0; $i < 10; $i++) - { - $temp1[$i] = random_int(0, 99999999); - } - - for ($i = 0; $i < $restLength; $i++) - { - if (Crypt::timingSafeCompare($temp1[$i], $code)) - { - $otherResult = $otherResult || true; - $newArray[] = ''; - $dummyArray[] = $temp1[$i]; - } - else - { - $otherResult = $otherResult || false; - $newArray[] = ''; - $dummyArray[] = $temp1[$i]; - } - } - - // This last check makes sure than an empty code does not validate - $result = $result && !hash_equals('', $code); - - // Save the backup codes - $this->saveBackupCodes($newArray, $user); - - // Finally return the result - return $result; - } + /** + * Caches the backup codes per user ID + * + * @var array + * @since 4.2.0 + */ + protected $cache = []; + + /** + * Get the backup codes record for the specified user + * + * @param User|null $user The user in question. Use null for the currently logged in user. + * + * @return MfaTable|null Record object or null if none is found + * @throws \Exception + * @since 4.2.0 + */ + public function getBackupCodesRecord(User $user = null): ?MfaTable + { + // Make sure I have a user + if (empty($user)) { + $user = Factory::getApplication()->getIdentity() ?: + Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + /** @var MfaTable $record */ + $record = $this->getTable('Mfa', 'Administrator'); + $loaded = $record->load( + [ + 'user_id' => $user->id, + 'method' => 'backupcodes', + ] + ); + + if (!$loaded) { + $record = null; + } + + return $record; + } + + /** + * Generate a new set of backup codes for the specified user. The generated codes are immediately saved to the + * database and the internal cache is updated. + * + * @param User|null $user Which user to generate codes for? + * + * @return void + * @throws \Exception + * @since 4.2.0 + */ + public function regenerateBackupCodes(User $user = null): void + { + // Make sure I have a user + if (empty($user)) { + $user = Factory::getApplication()->getIdentity() ?: + Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + // Generate backup codes + $backupCodes = []; + + for ($i = 0; $i < 10; $i++) { + // Each backup code is 2 groups of 4 digits + $backupCodes[$i] = sprintf('%04u%04u', random_int(0, 9999), random_int(0, 9999)); + } + + // Save the backup codes to the database and update the cache + $this->saveBackupCodes($backupCodes, $user); + } + + /** + * Saves the backup codes to the database + * + * @param array $codes An array of exactly 10 elements + * @param User|null $user The user for which to save the backup codes + * + * @return boolean + * @throws \Exception + * @since 4.2.0 + */ + public function saveBackupCodes(array $codes, ?User $user = null): bool + { + // Make sure I have a user + if (empty($user)) { + $user = Factory::getApplication()->getIdentity() ?: + Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + // Try to load existing backup codes + $existingCodes = $this->getBackupCodes($user); + $jNow = Date::getInstance(); + + /** @var MfaTable $record */ + $record = $this->getTable('Mfa', 'Administrator'); + + if (is_null($existingCodes)) { + $record->reset(); + + $newData = [ + 'user_id' => $user->id, + 'title' => Text::_('COM_USERS_PROFILE_OTEPS'), + 'method' => 'backupcodes', + 'default' => 0, + 'created_on' => $jNow->toSql(), + 'options' => $codes, + ]; + } else { + $record->load( + [ + 'user_id' => $user->id, + 'method' => 'backupcodes', + ] + ); + + $newData = [ + 'options' => $codes, + ]; + } + + $saved = $record->save($newData); + + if (!$saved) { + return false; + } + + // Finally, update the cache + $this->cache[$user->id] = $codes; + + return true; + } + + /** + * Returns the backup codes for the specified user. Cached values will be preferentially returned, therefore you + * MUST go through this model's Methods ONLY when dealing with backup codes. + * + * @param User|null $user The user for which you want the backup codes + * + * @return array|null The backup codes, or null if they do not exist + * @throws \Exception + * @since 4.2.0 + */ + public function getBackupCodes(User $user = null): ?array + { + // Make sure I have a user + if (empty($user)) { + $user = Factory::getApplication()->getIdentity() ?: + Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + if (isset($this->cache[$user->id])) { + return $this->cache[$user->id]; + } + + // If there is no cached record try to load it from the database + $this->cache[$user->id] = null; + + // Try to load the record + /** @var MfaTable $record */ + $record = $this->getTable('Mfa', 'Administrator'); + $loaded = $record->load( + [ + 'user_id' => $user->id, + 'method' => 'backupcodes', + ] + ); + + if ($loaded) { + $this->cache[$user->id] = $record->options; + } + + return $this->cache[$user->id]; + } + + /** + * Check if the provided string is a backup code. If it is, it will be removed from the list (replaced with an empty + * string) and the codes will be saved to the database. All comparisons are performed in a timing safe manner. + * + * @param string $code The code to check + * @param User|null $user The user to check against + * + * @return boolean + * @throws \Exception + * @since 4.2.0 + */ + public function isBackupCode($code, ?User $user = null): bool + { + // Load the backup codes + $codes = $this->getBackupCodes($user) ?: array_fill(0, 10, ''); + + // Keep only the numbers in the provided $code + $code = filter_var($code, FILTER_SANITIZE_NUMBER_INT); + $code = trim($code); + + // Check if the code is in the array. We always check against ten codes to prevent timing attacks which + // determine the amount of codes. + $result = false; + + // The two arrays let us always add an element to an array, therefore having PHP expend the same amount of time + // for the correct code, the incorrect codes and the fake codes. + $newArray = []; + $dummyArray = []; + + $realLength = count($codes); + $restLength = 10 - $realLength; + + for ($i = 0; $i < $realLength; $i++) { + if (hash_equals($codes[$i], $code)) { + // This may seem redundant but makes sure both branches of the if-block are isochronous + $result = $result || true; + $newArray[] = ''; + $dummyArray[] = $codes[$i]; + } else { + // This may seem redundant but makes sure both branches of the if-block are isochronous + $result = $result || false; + $dummyArray[] = ''; + $newArray[] = $codes[$i]; + } + } + + /** + * This is an intentional waste of time, symmetrical to the code above, making sure + * evaluating each of the total of ten elements takes the same time. This code should never + * run UNLESS someone messed up with our backup codes array and it no longer contains 10 + * elements. + */ + $otherResult = false; + + $temp1 = ''; + + for ($i = 0; $i < 10; $i++) { + $temp1[$i] = random_int(0, 99999999); + } + + for ($i = 0; $i < $restLength; $i++) { + if (Crypt::timingSafeCompare($temp1[$i], $code)) { + $otherResult = $otherResult || true; + $newArray[] = ''; + $dummyArray[] = $temp1[$i]; + } else { + $otherResult = $otherResult || false; + $newArray[] = ''; + $dummyArray[] = $temp1[$i]; + } + } + + // This last check makes sure than an empty code does not validate + $result = $result && !hash_equals('', $code); + + // Save the backup codes + $this->saveBackupCodes($newArray, $user); + + // Finally return the result + return $result; + } } diff --git a/administrator/components/com_users/src/Model/CaptiveModel.php b/administrator/components/com_users/src/Model/CaptiveModel.php index f61f47f3aa51b..1b873691e31c5 100644 --- a/administrator/components/com_users/src/Model/CaptiveModel.php +++ b/administrator/components/com_users/src/Model/CaptiveModel.php @@ -1,4 +1,5 @@ registerEvent('onAfterModuleList', [$this, 'onAfterModuleList']); - } - - /** - * Get the MFA records for the user which correspond to active plugins - * - * @param User|null $user The user for which to fetch records. Skip to use the current user. - * @param bool $includeBackupCodes Should I include the backup codes record? - * - * @return array - * @throws Exception - * - * @since 4.2.0 - */ - public function getRecords(User $user = null, bool $includeBackupCodes = false): array - { - if (is_null($user)) - { - $user = Factory::getApplication()->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - // Get the user's MFA records - $records = MfaHelper::getUserMfaRecords($user->id); - - // No MFA Methods? Then we obviously don't need to display a Captive login page. - if (empty($records)) - { - return []; - } - - // Get the enabled MFA Methods' names - $methodNames = $this->getActiveMethodNames(); - - // Filter the records based on currently active MFA Methods - $ret = []; - - $methodNames[] = 'backupcodes'; - $methodNames = array_unique($methodNames); - - if (!$includeBackupCodes) - { - $methodNames = array_filter( - $methodNames, - function ($method) - { - return $method != 'backupcodes'; - } - ); - } - - foreach ($records as $record) - { - // Backup codes must not be included in the list. We add them in the View, at the end of the list. - if (in_array($record->method, $methodNames)) - { - $ret[$record->id] = $record; - } - } - - return $ret; - } - - /** - * Return all the active MFA Methods' names - * - * @return array - * @since 4.2.0 - */ - private function getActiveMethodNames(): ?array - { - if (!is_null($this->activeMFAMethodNames)) - { - return $this->activeMFAMethodNames; - } - - // Let's get a list of all currently active MFA Methods - $mfaMethods = MfaHelper::getMfaMethods(); - - // If no MFA Method is active we can't really display a Captive login page. - if (empty($mfaMethods)) - { - $this->activeMFAMethodNames = []; - - return $this->activeMFAMethodNames; - } - - // Get a list of just the Method names - $this->activeMFAMethodNames = []; - - foreach ($mfaMethods as $mfaMethod) - { - $this->activeMFAMethodNames[] = $mfaMethod['name']; - } - - return $this->activeMFAMethodNames; - } - - /** - * Get the currently selected MFA record for the current user. If the record ID is empty, it does not correspond to - * the currently logged in user or does not correspond to an active plugin null is returned instead. - * - * @param User|null $user The user for which to fetch records. Skip to use the current user. - * - * @return MfaTable|null - * @throws Exception - * - * @since 4.2.0 - */ - public function getRecord(?User $user = null): ?MfaTable - { - $id = (int) $this->getState('record_id', null); - - if ($id <= 0) - { - return null; - } - - if (is_null($user)) - { - $user = Factory::getApplication()->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - /** @var MfaTable $record */ - $record = $this->getTable('Mfa', 'Administrator'); - $loaded = $record->load( - [ - 'user_id' => $user->id, - 'id' => $id, - ] - ); - - if (!$loaded) - { - return null; - } - - $methodNames = $this->getActiveMethodNames(); - - if (!in_array($record->method, $methodNames) && ($record->method != 'backupcodes')) - { - return null; - } - - return $record; - } - - /** - * Load the Captive login page render options for a specific MFA record - * - * @param MfaTable $record The MFA record to process - * - * @return CaptiveRenderOptions The rendering options - * @since 4.2.0 - */ - public function loadCaptiveRenderOptions(?MfaTable $record): CaptiveRenderOptions - { - $renderOptions = new CaptiveRenderOptions; - - if (empty($record)) - { - return $renderOptions; - } - - $event = new Captive($record); - $results = Factory::getApplication() - ->getDispatcher() - ->dispatch($event->getName(), $event) - ->getArgument('result', []); - - if (empty($results)) - { - if ($record->method === 'backupcodes') - { - return $renderOptions->merge( - [ - 'pre_message' => Text::_('COM_USERS_USER_BACKUPCODES_CAPTIVE_PROMPT'), - 'input_type' => 'number', - 'label' => Text::_('COM_USERS_USER_BACKUPCODE'), - ] - ); - } - - return $renderOptions; - } - - foreach ($results as $result) - { - if (empty($result)) - { - continue; - } - - return $renderOptions->merge($result); - } - - return $renderOptions; - } - - /** - * Returns the title to display in the Captive login page, or an empty string if no title is to be displayed. - * - * @return string - * @since 4.2.0 - */ - public function getPageTitle(): string - { - // In the frontend we can choose if we will display a title - $showTitle = (bool) ComponentHelper::getParams('com_users') - ->get('frontend_show_title', 1); - - if (!$showTitle) - { - return ''; - } - - return Text::_('COM_USERS_USER_MULTIFACTOR_AUTH'); - } - - /** - * Translate a MFA Method's name into its human-readable, display name - * - * @param string $name The internal MFA Method name - * - * @return string - * @since 4.2.0 - */ - public function translateMethodName(string $name): string - { - static $map = null; - - if (!is_array($map)) - { - $map = []; - $mfaMethods = MfaHelper::getMfaMethods(); - - if (!empty($mfaMethods)) - { - foreach ($mfaMethods as $mfaMethod) - { - $map[$mfaMethod['name']] = $mfaMethod['display']; - } - } - } - - if ($name == 'backupcodes') - { - return Text::_('COM_USERS_USER_BACKUPCODES'); - } - - return $map[$name] ?? $name; - } - - /** - * Translate a MFA Method's name into the relative URL if its logo image - * - * @param string $name The internal MFA Method name - * - * @return string - * @since 4.2.0 - */ - public function getMethodImage(string $name): string - { - static $map = null; - - if (!is_array($map)) - { - $map = []; - $mfaMethods = MfaHelper::getMfaMethods(); - - if (!empty($mfaMethods)) - { - foreach ($mfaMethods as $mfaMethod) - { - $map[$mfaMethod['name']] = $mfaMethod['image']; - } - } - } - - if ($name == 'backupcodes') - { - return 'media/com_users/images/emergency.svg'; - } - - return $map[$name] ?? $name; - } - - /** - * Process the modules list on Joomla! 4. - * - * Joomla! 4.x is passing an Event object. The first argument of the event object is the array of modules. After - * filtering it we have to overwrite the event argument (NOT just return the new list of modules). If a future - * version of Joomla! uses immutable events we'll have to use Reflection to do that or Joomla! would have to fix - * the way this event is handled, taking its return into account. For now, we just abuse the mutable event - * properties - a feature of the event objects we discussed in the Joomla! 4 Working Group back in August 2015. - * - * @param Event $event The Joomla! event object - * - * @return void - * @throws Exception - * - * @since 4.2.0 - */ - public function onAfterModuleList(Event $event): void - { - $modules = $event->getArgument(0); - - if (empty($modules)) - { - return; - } - - $this->filterModules($modules); - - $event->setArgument(0, $modules); - } - - /** - * This is the Method which actually filters the sites modules based on the allowed module positions specified by - * the user. - * - * @param array $modules The list of the site's modules. Passed by reference. - * - * @return void The by-reference value is modified instead. - * @since 4.2.0 - * @throws Exception - */ - private function filterModules(array &$modules): void - { - $allowedPositions = $this->getAllowedModulePositions(); - - if (empty($allowedPositions)) - { - $modules = []; - - return; - } - - $filtered = []; - - foreach ($modules as $module) - { - if (in_array($module->position, $allowedPositions)) - { - $filtered[] = $module; - } - } - - $modules = $filtered; - } - - /** - * Get a list of module positions we are allowed to display - * - * @return array - * @throws Exception - * - * @since 4.2.0 - */ - private function getAllowedModulePositions(): array - { - $isAdmin = Factory::getApplication()->isClient('administrator'); - - // Load the list of allowed module positions from the component's settings. May be different for front- and back-end - $configKey = 'allowed_positions_' . ($isAdmin ? 'backend' : 'frontend'); - $res = ComponentHelper::getParams('com_users')->get($configKey, []); - - // In the backend we must always add the 'title' module position - if ($isAdmin) - { - $res[] = 'title'; - $res[] = 'toolbar'; - } - - return $res; - } - + /** + * Cache of the names of the currently active MFA Methods + * + * @var array|null + * @since 4.2.0 + */ + protected $activeMFAMethodNames = null; + + /** + * Prevents Joomla from displaying any modules. + * + * This is implemented with a trick. If you use jdoc tags to load modules the JDocumentRendererHtmlModules + * uses JModuleHelper::getModules() to load the list of modules to render. This goes through JModuleHelper::load() + * which triggers the onAfterModuleList event after cleaning up the module list from duplicates. By resetting + * the list to an empty array we force Joomla to not display any modules. + * + * Similar code paths are followed by any canonical code which tries to load modules. So even if your template does + * not use jdoc tags this code will still work as expected. + * + * @param CMSApplication|null $app The CMS application to manipulate + * + * @return void + * @throws Exception + * + * @since 4.2.0 + */ + public function suppressAllModules(CMSApplication $app = null): void + { + if (is_null($app)) { + $app = Factory::getApplication(); + } + + $app->registerEvent('onAfterModuleList', [$this, 'onAfterModuleList']); + } + + /** + * Get the MFA records for the user which correspond to active plugins + * + * @param User|null $user The user for which to fetch records. Skip to use the current user. + * @param bool $includeBackupCodes Should I include the backup codes record? + * + * @return array + * @throws Exception + * + * @since 4.2.0 + */ + public function getRecords(User $user = null, bool $includeBackupCodes = false): array + { + if (is_null($user)) { + $user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + // Get the user's MFA records + $records = MfaHelper::getUserMfaRecords($user->id); + + // No MFA Methods? Then we obviously don't need to display a Captive login page. + if (empty($records)) { + return []; + } + + // Get the enabled MFA Methods' names + $methodNames = $this->getActiveMethodNames(); + + // Filter the records based on currently active MFA Methods + $ret = []; + + $methodNames[] = 'backupcodes'; + $methodNames = array_unique($methodNames); + + if (!$includeBackupCodes) { + $methodNames = array_filter( + $methodNames, + function ($method) { + return $method != 'backupcodes'; + } + ); + } + + foreach ($records as $record) { + // Backup codes must not be included in the list. We add them in the View, at the end of the list. + if (in_array($record->method, $methodNames)) { + $ret[$record->id] = $record; + } + } + + return $ret; + } + + /** + * Return all the active MFA Methods' names + * + * @return array + * @since 4.2.0 + */ + private function getActiveMethodNames(): ?array + { + if (!is_null($this->activeMFAMethodNames)) { + return $this->activeMFAMethodNames; + } + + // Let's get a list of all currently active MFA Methods + $mfaMethods = MfaHelper::getMfaMethods(); + + // If no MFA Method is active we can't really display a Captive login page. + if (empty($mfaMethods)) { + $this->activeMFAMethodNames = []; + + return $this->activeMFAMethodNames; + } + + // Get a list of just the Method names + $this->activeMFAMethodNames = []; + + foreach ($mfaMethods as $mfaMethod) { + $this->activeMFAMethodNames[] = $mfaMethod['name']; + } + + return $this->activeMFAMethodNames; + } + + /** + * Get the currently selected MFA record for the current user. If the record ID is empty, it does not correspond to + * the currently logged in user or does not correspond to an active plugin null is returned instead. + * + * @param User|null $user The user for which to fetch records. Skip to use the current user. + * + * @return MfaTable|null + * @throws Exception + * + * @since 4.2.0 + */ + public function getRecord(?User $user = null): ?MfaTable + { + $id = (int) $this->getState('record_id', null); + + if ($id <= 0) { + return null; + } + + if (is_null($user)) { + $user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + /** @var MfaTable $record */ + $record = $this->getTable('Mfa', 'Administrator'); + $loaded = $record->load( + [ + 'user_id' => $user->id, + 'id' => $id, + ] + ); + + if (!$loaded) { + return null; + } + + $methodNames = $this->getActiveMethodNames(); + + if (!in_array($record->method, $methodNames) && ($record->method != 'backupcodes')) { + return null; + } + + return $record; + } + + /** + * Load the Captive login page render options for a specific MFA record + * + * @param MfaTable $record The MFA record to process + * + * @return CaptiveRenderOptions The rendering options + * @since 4.2.0 + */ + public function loadCaptiveRenderOptions(?MfaTable $record): CaptiveRenderOptions + { + $renderOptions = new CaptiveRenderOptions(); + + if (empty($record)) { + return $renderOptions; + } + + $event = new Captive($record); + $results = Factory::getApplication() + ->getDispatcher() + ->dispatch($event->getName(), $event) + ->getArgument('result', []); + + if (empty($results)) { + if ($record->method === 'backupcodes') { + return $renderOptions->merge( + [ + 'pre_message' => Text::_('COM_USERS_USER_BACKUPCODES_CAPTIVE_PROMPT'), + 'input_type' => 'number', + 'label' => Text::_('COM_USERS_USER_BACKUPCODE'), + ] + ); + } + + return $renderOptions; + } + + foreach ($results as $result) { + if (empty($result)) { + continue; + } + + return $renderOptions->merge($result); + } + + return $renderOptions; + } + + /** + * Returns the title to display in the Captive login page, or an empty string if no title is to be displayed. + * + * @return string + * @since 4.2.0 + */ + public function getPageTitle(): string + { + // In the frontend we can choose if we will display a title + $showTitle = (bool) ComponentHelper::getParams('com_users') + ->get('frontend_show_title', 1); + + if (!$showTitle) { + return ''; + } + + return Text::_('COM_USERS_USER_MULTIFACTOR_AUTH'); + } + + /** + * Translate a MFA Method's name into its human-readable, display name + * + * @param string $name The internal MFA Method name + * + * @return string + * @since 4.2.0 + */ + public function translateMethodName(string $name): string + { + static $map = null; + + if (!is_array($map)) { + $map = []; + $mfaMethods = MfaHelper::getMfaMethods(); + + if (!empty($mfaMethods)) { + foreach ($mfaMethods as $mfaMethod) { + $map[$mfaMethod['name']] = $mfaMethod['display']; + } + } + } + + if ($name == 'backupcodes') { + return Text::_('COM_USERS_USER_BACKUPCODES'); + } + + return $map[$name] ?? $name; + } + + /** + * Translate a MFA Method's name into the relative URL if its logo image + * + * @param string $name The internal MFA Method name + * + * @return string + * @since 4.2.0 + */ + public function getMethodImage(string $name): string + { + static $map = null; + + if (!is_array($map)) { + $map = []; + $mfaMethods = MfaHelper::getMfaMethods(); + + if (!empty($mfaMethods)) { + foreach ($mfaMethods as $mfaMethod) { + $map[$mfaMethod['name']] = $mfaMethod['image']; + } + } + } + + if ($name == 'backupcodes') { + return 'media/com_users/images/emergency.svg'; + } + + return $map[$name] ?? $name; + } + + /** + * Process the modules list on Joomla! 4. + * + * Joomla! 4.x is passing an Event object. The first argument of the event object is the array of modules. After + * filtering it we have to overwrite the event argument (NOT just return the new list of modules). If a future + * version of Joomla! uses immutable events we'll have to use Reflection to do that or Joomla! would have to fix + * the way this event is handled, taking its return into account. For now, we just abuse the mutable event + * properties - a feature of the event objects we discussed in the Joomla! 4 Working Group back in August 2015. + * + * @param Event $event The Joomla! event object + * + * @return void + * @throws Exception + * + * @since 4.2.0 + */ + public function onAfterModuleList(Event $event): void + { + $modules = $event->getArgument(0); + + if (empty($modules)) { + return; + } + + $this->filterModules($modules); + + $event->setArgument(0, $modules); + } + + /** + * This is the Method which actually filters the sites modules based on the allowed module positions specified by + * the user. + * + * @param array $modules The list of the site's modules. Passed by reference. + * + * @return void The by-reference value is modified instead. + * @since 4.2.0 + * @throws Exception + */ + private function filterModules(array &$modules): void + { + $allowedPositions = $this->getAllowedModulePositions(); + + if (empty($allowedPositions)) { + $modules = []; + + return; + } + + $filtered = []; + + foreach ($modules as $module) { + if (in_array($module->position, $allowedPositions)) { + $filtered[] = $module; + } + } + + $modules = $filtered; + } + + /** + * Get a list of module positions we are allowed to display + * + * @return array + * @throws Exception + * + * @since 4.2.0 + */ + private function getAllowedModulePositions(): array + { + $isAdmin = Factory::getApplication()->isClient('administrator'); + + // Load the list of allowed module positions from the component's settings. May be different for front- and back-end + $configKey = 'allowed_positions_' . ($isAdmin ? 'backend' : 'frontend'); + $res = ComponentHelper::getParams('com_users')->get($configKey, []); + + // In the backend we must always add the 'title' module position + if ($isAdmin) { + $res[] = 'title'; + $res[] = 'toolbar'; + } + + return $res; + } } diff --git a/administrator/components/com_users/src/Model/DebuggroupModel.php b/administrator/components/com_users/src/Model/DebuggroupModel.php index bebde5ee18ce7..f3f7df2300b1f 100644 --- a/administrator/components/com_users/src/Model/DebuggroupModel.php +++ b/administrator/components/com_users/src/Model/DebuggroupModel.php @@ -1,4 +1,5 @@ getState('filter.component'); - - return DebugHelper::getDebugActions($component); - } - - /** - * Override getItems method. - * - * @return array - * - * @since 1.6 - */ - public function getItems() - { - $groupId = $this->getState('group_id'); - - if (($assets = parent::getItems()) && $groupId) - { - $actions = $this->getDebugActions(); - - foreach ($assets as &$asset) - { - $asset->checks = array(); - - foreach ($actions as $action) - { - $name = $action[0]; - $asset->checks[$name] = Access::checkGroup($groupId, $name, $asset->name); - } - } - } - - return $assets; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'a.lft', $direction = 'asc') - { - $app = Factory::getApplication(); - - // Adjust the context to support modal layouts. - $layout = $app->input->get('layout', 'default'); - - if ($layout) - { - $this->context .= '.' . $layout; - } - - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('group_id', $this->getUserStateFromRequest($this->context . '.group_id', 'group_id', 0, 'int', false)); - - $levelStart = $this->getUserStateFromRequest($this->context . '.filter.level_start', 'filter_level_start', '', 'cmd'); - $this->setState('filter.level_start', $levelStart); - - $value = $this->getUserStateFromRequest($this->context . '.filter.level_end', 'filter_level_end', '', 'cmd'); - - if ($value > 0 && $value < $levelStart) - { - $value = $levelStart; - } - - $this->setState('filter.level_end', $value); - - $this->setState('filter.component', $this->getUserStateFromRequest($this->context . '.filter.component', 'filter_component', '', 'string')); - - // Load the parameters. - $params = ComponentHelper::getParams('com_users'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('group_id'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.level_start'); - $id .= ':' . $this->getState('filter.level_end'); - $id .= ':' . $this->getState('filter.component'); - - return parent::getStoreId($id); - } - - /** - * Get the group being debugged. - * - * @return CMSObject - * - * @since 1.6 - */ - public function getGroup() - { - $groupId = (int) $this->getState('group_id'); - - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName(['id', 'title'])) - ->from($db->quoteName('#__usergroups')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $groupId, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $group = $db->loadObject(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return $group; - } - - /** - * Build an SQL query to load the list data. - * - * @return DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.id, a.name, a.title, a.level, a.lft, a.rgt' - ) - ); - $query->from($db->quoteName('#__assets', 'a')); - - // Filter the items over the search string if set. - if ($this->getState('filter.search')) - { - $search = '%' . trim($this->getState('filter.search')) . '%'; - - // Add the clauses to the query. - $query->where( - '(' . $db->quoteName('a.name') . ' LIKE :name' - . ' OR ' . $db->quoteName('a.title') . ' LIKE :title)' - ) - ->bind(':name', $search) - ->bind(':title', $search); - } - - // Filter on the start and end levels. - $levelStart = (int) $this->getState('filter.level_start'); - $levelEnd = (int) $this->getState('filter.level_end'); - - if ($levelEnd > 0 && $levelEnd < $levelStart) - { - $levelEnd = $levelStart; - } - - if ($levelStart > 0) - { - $query->where($db->quoteName('a.level') . ' >= :levelStart') - ->bind(':levelStart', $levelStart, ParameterType::INTEGER); - } - - if ($levelEnd > 0) - { - $query->where($db->quoteName('a.level') . ' <= :levelEnd') - ->bind(':levelEnd', $levelEnd, ParameterType::INTEGER); - } - - // Filter the items over the component if set. - if ($this->getState('filter.component')) - { - $component = $this->getState('filter.component'); - $lcomponent = $component . '.%'; - $query->where( - '(' . $db->quoteName('a.name') . ' = :component' - . ' OR ' . $db->quoteName('a.name') . ' LIKE :lcomponent)' - ) - ->bind(':component', $component) - ->bind(':lcomponent', $lcomponent); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'a.title', + 'component', 'a.name', + 'a.lft', + 'a.id', + 'level_start', 'level_end', 'a.level', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Get a list of the actions. + * + * @return array + * + * @since 1.6 + */ + public function getDebugActions() + { + $component = $this->getState('filter.component'); + + return DebugHelper::getDebugActions($component); + } + + /** + * Override getItems method. + * + * @return array + * + * @since 1.6 + */ + public function getItems() + { + $groupId = $this->getState('group_id'); + + if (($assets = parent::getItems()) && $groupId) { + $actions = $this->getDebugActions(); + + foreach ($assets as &$asset) { + $asset->checks = array(); + + foreach ($actions as $action) { + $name = $action[0]; + $asset->checks[$name] = Access::checkGroup($groupId, $name, $asset->name); + } + } + } + + return $assets; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.lft', $direction = 'asc') + { + $app = Factory::getApplication(); + + // Adjust the context to support modal layouts. + $layout = $app->input->get('layout', 'default'); + + if ($layout) { + $this->context .= '.' . $layout; + } + + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('group_id', $this->getUserStateFromRequest($this->context . '.group_id', 'group_id', 0, 'int', false)); + + $levelStart = $this->getUserStateFromRequest($this->context . '.filter.level_start', 'filter_level_start', '', 'cmd'); + $this->setState('filter.level_start', $levelStart); + + $value = $this->getUserStateFromRequest($this->context . '.filter.level_end', 'filter_level_end', '', 'cmd'); + + if ($value > 0 && $value < $levelStart) { + $value = $levelStart; + } + + $this->setState('filter.level_end', $value); + + $this->setState('filter.component', $this->getUserStateFromRequest($this->context . '.filter.component', 'filter_component', '', 'string')); + + // Load the parameters. + $params = ComponentHelper::getParams('com_users'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('group_id'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.level_start'); + $id .= ':' . $this->getState('filter.level_end'); + $id .= ':' . $this->getState('filter.component'); + + return parent::getStoreId($id); + } + + /** + * Get the group being debugged. + * + * @return CMSObject + * + * @since 1.6 + */ + public function getGroup() + { + $groupId = (int) $this->getState('group_id'); + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['id', 'title'])) + ->from($db->quoteName('#__usergroups')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $groupId, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $group = $db->loadObject(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return $group; + } + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.id, a.name, a.title, a.level, a.lft, a.rgt' + ) + ); + $query->from($db->quoteName('#__assets', 'a')); + + // Filter the items over the search string if set. + if ($this->getState('filter.search')) { + $search = '%' . trim($this->getState('filter.search')) . '%'; + + // Add the clauses to the query. + $query->where( + '(' . $db->quoteName('a.name') . ' LIKE :name' + . ' OR ' . $db->quoteName('a.title') . ' LIKE :title)' + ) + ->bind(':name', $search) + ->bind(':title', $search); + } + + // Filter on the start and end levels. + $levelStart = (int) $this->getState('filter.level_start'); + $levelEnd = (int) $this->getState('filter.level_end'); + + if ($levelEnd > 0 && $levelEnd < $levelStart) { + $levelEnd = $levelStart; + } + + if ($levelStart > 0) { + $query->where($db->quoteName('a.level') . ' >= :levelStart') + ->bind(':levelStart', $levelStart, ParameterType::INTEGER); + } + + if ($levelEnd > 0) { + $query->where($db->quoteName('a.level') . ' <= :levelEnd') + ->bind(':levelEnd', $levelEnd, ParameterType::INTEGER); + } + + // Filter the items over the component if set. + if ($this->getState('filter.component')) { + $component = $this->getState('filter.component'); + $lcomponent = $component . '.%'; + $query->where( + '(' . $db->quoteName('a.name') . ' = :component' + . ' OR ' . $db->quoteName('a.name') . ' LIKE :lcomponent)' + ) + ->bind(':component', $component) + ->bind(':lcomponent', $lcomponent); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } } diff --git a/administrator/components/com_users/src/Model/DebuguserModel.php b/administrator/components/com_users/src/Model/DebuguserModel.php index 9c9a23c04d399..97a0f2229c1d6 100644 --- a/administrator/components/com_users/src/Model/DebuguserModel.php +++ b/administrator/components/com_users/src/Model/DebuguserModel.php @@ -1,4 +1,5 @@ getState('filter.component'); - - return DebugHelper::getDebugActions($component); - } - - /** - * Override getItems method. - * - * @return array - * - * @since 1.6 - */ - public function getItems() - { - $userId = $this->getState('user_id'); - $user = Factory::getUser($userId); - - if (($assets = parent::getItems()) && $userId) - { - $actions = $this->getDebugActions(); - - foreach ($assets as &$asset) - { - $asset->checks = array(); - - foreach ($actions as $action) - { - $name = $action[0]; - $asset->checks[$name] = $user->authorise($name, $asset->name); - } - } - } - - return $assets; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function populateState($ordering = 'a.lft', $direction = 'asc') - { - $app = Factory::getApplication(); - - // Adjust the context to support modal layouts. - $layout = $app->input->get('layout', 'default'); - - if ($layout) - { - $this->context .= '.' . $layout; - } - - // Load the filter state. - $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); - $this->setState('user_id', $this->getUserStateFromRequest($this->context . '.user_id', 'user_id', 0, 'int', false)); - - $levelStart = $this->getUserStateFromRequest($this->context . '.filter.level_start', 'filter_level_start', '', 'cmd'); - $this->setState('filter.level_start', $levelStart); - - $value = $this->getUserStateFromRequest($this->context . '.filter.level_end', 'filter_level_end', '', 'cmd'); - - if ($value > 0 && $value < $levelStart) - { - $value = $levelStart; - } - - $this->setState('filter.level_end', $value); - - $this->setState('filter.component', $this->getUserStateFromRequest($this->context . '.filter.component', 'filter_component', '', 'string')); - - // Load the parameters. - $params = ComponentHelper::getParams('com_users'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('user_id'); - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.level_start'); - $id .= ':' . $this->getState('filter.level_end'); - $id .= ':' . $this->getState('filter.component'); - - return parent::getStoreId($id); - } - - /** - * Get the user being debugged. - * - * @return User - * - * @since 1.6 - */ - public function getUser() - { - $userId = $this->getState('user_id'); - - return Factory::getUser($userId); - } - - /** - * Build an SQL query to load the list data. - * - * @return DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.id, a.name, a.title, a.level, a.lft, a.rgt' - ) - ); - $query->from($db->quoteName('#__assets', 'a')); - - // Filter the items over the search string if set. - if ($this->getState('filter.search')) - { - $search = '%' . trim($this->getState('filter.search')) . '%'; - - // Add the clauses to the query. - $query->where( - '(' . $db->quoteName('a.name') . ' LIKE :name' - . ' OR ' . $db->quoteName('a.title') . ' LIKE :title)' - ) - ->bind(':name', $search) - ->bind(':title', $search); - } - - // Filter on the start and end levels. - $levelStart = (int) $this->getState('filter.level_start'); - $levelEnd = (int) $this->getState('filter.level_end'); - - if ($levelEnd > 0 && $levelEnd < $levelStart) - { - $levelEnd = $levelStart; - } - - if ($levelStart > 0) - { - $query->where($db->quoteName('a.level') . ' >= :levelStart') - ->bind(':levelStart', $levelStart, ParameterType::INTEGER); - } - - if ($levelEnd > 0) - { - $query->where($db->quoteName('a.level') . ' <= :levelEnd') - ->bind(':levelEnd', $levelEnd, ParameterType::INTEGER); - } - - // Filter the items over the component if set. - if ($this->getState('filter.component')) - { - $component = $this->getState('filter.component'); - $lcomponent = $component . '.%'; - $query->where( - '(' . $db->quoteName('a.name') . ' = :component' - . ' OR ' . $db->quoteName('a.name') . ' LIKE :lcomponent)' - ) - ->bind(':component', $component) - ->bind(':lcomponent', $lcomponent); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'a.title', + 'component', 'a.name', + 'a.lft', + 'a.id', + 'level_start', 'level_end', 'a.level', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Get a list of the actions. + * + * @return array + * + * @since 1.6 + */ + public function getDebugActions() + { + $component = $this->getState('filter.component'); + + return DebugHelper::getDebugActions($component); + } + + /** + * Override getItems method. + * + * @return array + * + * @since 1.6 + */ + public function getItems() + { + $userId = $this->getState('user_id'); + $user = Factory::getUser($userId); + + if (($assets = parent::getItems()) && $userId) { + $actions = $this->getDebugActions(); + + foreach ($assets as &$asset) { + $asset->checks = array(); + + foreach ($actions as $action) { + $name = $action[0]; + $asset->checks[$name] = $user->authorise($name, $asset->name); + } + } + } + + return $assets; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function populateState($ordering = 'a.lft', $direction = 'asc') + { + $app = Factory::getApplication(); + + // Adjust the context to support modal layouts. + $layout = $app->input->get('layout', 'default'); + + if ($layout) { + $this->context .= '.' . $layout; + } + + // Load the filter state. + $this->setState('filter.search', $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string')); + $this->setState('user_id', $this->getUserStateFromRequest($this->context . '.user_id', 'user_id', 0, 'int', false)); + + $levelStart = $this->getUserStateFromRequest($this->context . '.filter.level_start', 'filter_level_start', '', 'cmd'); + $this->setState('filter.level_start', $levelStart); + + $value = $this->getUserStateFromRequest($this->context . '.filter.level_end', 'filter_level_end', '', 'cmd'); + + if ($value > 0 && $value < $levelStart) { + $value = $levelStart; + } + + $this->setState('filter.level_end', $value); + + $this->setState('filter.component', $this->getUserStateFromRequest($this->context . '.filter.component', 'filter_component', '', 'string')); + + // Load the parameters. + $params = ComponentHelper::getParams('com_users'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('user_id'); + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.level_start'); + $id .= ':' . $this->getState('filter.level_end'); + $id .= ':' . $this->getState('filter.component'); + + return parent::getStoreId($id); + } + + /** + * Get the user being debugged. + * + * @return User + * + * @since 1.6 + */ + public function getUser() + { + $userId = $this->getState('user_id'); + + return Factory::getUser($userId); + } + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.id, a.name, a.title, a.level, a.lft, a.rgt' + ) + ); + $query->from($db->quoteName('#__assets', 'a')); + + // Filter the items over the search string if set. + if ($this->getState('filter.search')) { + $search = '%' . trim($this->getState('filter.search')) . '%'; + + // Add the clauses to the query. + $query->where( + '(' . $db->quoteName('a.name') . ' LIKE :name' + . ' OR ' . $db->quoteName('a.title') . ' LIKE :title)' + ) + ->bind(':name', $search) + ->bind(':title', $search); + } + + // Filter on the start and end levels. + $levelStart = (int) $this->getState('filter.level_start'); + $levelEnd = (int) $this->getState('filter.level_end'); + + if ($levelEnd > 0 && $levelEnd < $levelStart) { + $levelEnd = $levelStart; + } + + if ($levelStart > 0) { + $query->where($db->quoteName('a.level') . ' >= :levelStart') + ->bind(':levelStart', $levelStart, ParameterType::INTEGER); + } + + if ($levelEnd > 0) { + $query->where($db->quoteName('a.level') . ' <= :levelEnd') + ->bind(':levelEnd', $levelEnd, ParameterType::INTEGER); + } + + // Filter the items over the component if set. + if ($this->getState('filter.component')) { + $component = $this->getState('filter.component'); + $lcomponent = $component . '.%'; + $query->where( + '(' . $db->quoteName('a.name') . ' = :component' + . ' OR ' . $db->quoteName('a.name') . ' LIKE :lcomponent)' + ) + ->bind(':component', $component) + ->bind(':lcomponent', $lcomponent); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } } diff --git a/administrator/components/com_users/src/Model/GroupModel.php b/administrator/components/com_users/src/Model/GroupModel.php index 5b770fd105a57..d8ec477a401c0 100644 --- a/administrator/components/com_users/src/Model/GroupModel.php +++ b/administrator/components/com_users/src/Model/GroupModel.php @@ -1,4 +1,5 @@ 'onUserAfterDeleteGroup', - 'event_after_save' => 'onUserAfterSaveGroup', - 'event_before_delete' => 'onUserBeforeDeleteGroup', - 'event_before_save' => 'onUserBeforeSaveGroup', - 'events_map' => array('delete' => 'user', 'save' => 'user') - ), $config - ); - - parent::__construct($config, $factory); - } - - /** - * Returns a reference to the a Table object, always creating it. - * - * @param string $type The table type to instantiate - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A database object - * - * @since 1.6 - */ - public function getTable($type = 'Usergroup', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) - { - $return = Table::getInstance($type, $prefix, $config); - - return $return; - } - - /** - * Method to get the record form. - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_users.group', 'group', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - * @throws \Exception - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_users.edit.group.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_users.group', $data); - - return $data; - } - - /** - * Override preprocessForm to load the user plugin group instead of content. - * - * @param Form $form A form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error loading the form. - */ - protected function preprocessForm(Form $form, $data, $group = '') - { - $obj = is_array($data) ? ArrayHelper::toObject($data, CMSObject::class) : $data; - - if (isset($obj->parent_id) && $obj->parent_id == 0 && $obj->id > 0) - { - $form->setFieldAttribute('parent_id', 'type', 'hidden'); - $form->setFieldAttribute('parent_id', 'hidden', 'true'); - } - - parent::preprocessForm($form, $data, 'user'); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - // Include the user plugins for events. - PluginHelper::importPlugin($this->events_map['save']); - - /** - * Check the super admin permissions for group - * We get the parent group permissions and then check the group permissions manually - * We have to calculate the group permissions manually because we haven't saved the group yet - */ - $parentSuperAdmin = Access::checkGroup($data['parent_id'], 'core.admin'); - - // Get core.admin rules from the root asset - $rules = Access::getAssetRules('root.1')->getData('core.admin'); - - // Get the value for the current group (will be true (allowed), false (denied), or null (inherit) - $groupSuperAdmin = $rules['core.admin']->allow($data['id']); - - // We only need to change the $groupSuperAdmin if the parent is true or false. Otherwise, the value set in the rule takes effect. - if ($parentSuperAdmin === false) - { - // If parent is false (Denied), effective value will always be false - $groupSuperAdmin = false; - } - elseif ($parentSuperAdmin === true) - { - // If parent is true (allowed), group is true unless explicitly set to false - $groupSuperAdmin = ($groupSuperAdmin === false) ? false : true; - } - - // Check for non-super admin trying to save with super admin group - $iAmSuperAdmin = Factory::getUser()->authorise('core.admin'); - - if (!$iAmSuperAdmin && $groupSuperAdmin) - { - $this->setError(Text::_('JLIB_USER_ERROR_NOT_SUPERADMIN')); - - return false; - } - - /** - * Check for super-admin changing self to be non-super-admin - * First, are we a super admin - */ - if ($iAmSuperAdmin) - { - // Next, are we a member of the current group? - $myGroups = Access::getGroupsByUser(Factory::getUser()->get('id'), false); - - if (in_array($data['id'], $myGroups)) - { - // Now, would we have super admin permissions without the current group? - $otherGroups = array_diff($myGroups, array($data['id'])); - $otherSuperAdmin = false; - - foreach ($otherGroups as $otherGroup) - { - $otherSuperAdmin = $otherSuperAdmin ?: Access::checkGroup($otherGroup, 'core.admin'); - } - - /** - * If we would not otherwise have super admin permissions - * and the current group does not have super admin permissions, throw an exception - */ - if ((!$otherSuperAdmin) && (!$groupSuperAdmin)) - { - $this->setError(Text::_('JLIB_USER_ERROR_CANNOT_DEMOTE_SELF')); - - return false; - } - } - } - - if (Factory::getApplication()->input->get('task') == 'save2copy') - { - $data['title'] = $this->generateGroupTitle($data['parent_id'], $data['title']); - } - - // Proceed with the save - return parent::save($data); - } - - /** - * Method to delete rows. - * - * @param array &$pks An array of item ids. - * - * @return boolean Returns true on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function delete(&$pks) - { - // Typecast variable. - $pks = (array) $pks; - $user = Factory::getUser(); - $groups = Access::getGroupsByUser($user->get('id')); - - // Get a row instance. - $table = $this->getTable(); - - // Load plugins. - PluginHelper::importPlugin($this->events_map['delete']); - - // Check if I am a Super Admin - $iAmSuperAdmin = $user->authorise('core.admin'); - - foreach ($pks as $pk) - { - // Do not allow to delete groups to which the current user belongs - if (in_array($pk, $groups)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_DELETE_ERROR_INVALID_GROUP'), 'error'); - - return false; - } - elseif (!$table->load($pk)) - { - // Item is not in the table. - $this->setError($table->getError()); - - return false; - } - } - - // Iterate the items to delete each one. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - // Access checks. - $allow = $user->authorise('core.edit.state', 'com_users'); - - // Don't allow non-super-admin to delete a super admin - $allow = (!$iAmSuperAdmin && Access::checkGroup($pk, 'core.admin')) ? false : $allow; - - if ($allow) - { - // Fire the before delete event. - Factory::getApplication()->triggerEvent($this->event_before_delete, array($table->getProperties())); - - if (!$table->delete($pk)) - { - $this->setError($table->getError()); - - return false; - } - else - { - // Trigger the after delete event. - Factory::getApplication()->triggerEvent($this->event_after_delete, array($table->getProperties(), true, $this->getError())); - } - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); - } - } - } - - return true; - } - - /** - * Method to generate the title of group on Save as Copy action - * - * @param integer $parentId The id of the parent. - * @param string $title The title of group - * - * @return string Contains the modified title. - * - * @since 3.3.7 - */ - protected function generateGroupTitle($parentId, $title) - { - // Alter the title & alias - $table = $this->getTable(); - - while ($table->load(array('title' => $title, 'parent_id' => $parentId))) - { - if ($title == $table->title) - { - $title = StringHelper::increment($title); - } - } - - return $title; - } + /** + * Override parent constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + $config = array_merge( + array( + 'event_after_delete' => 'onUserAfterDeleteGroup', + 'event_after_save' => 'onUserAfterSaveGroup', + 'event_before_delete' => 'onUserBeforeDeleteGroup', + 'event_before_save' => 'onUserBeforeSaveGroup', + 'events_map' => array('delete' => 'user', 'save' => 'user') + ), + $config + ); + + parent::__construct($config, $factory); + } + + /** + * Returns a reference to the a Table object, always creating it. + * + * @param string $type The table type to instantiate + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A database object + * + * @since 1.6 + */ + public function getTable($type = 'Usergroup', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) + { + $return = Table::getInstance($type, $prefix, $config); + + return $return; + } + + /** + * Method to get the record form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.group', 'group', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + * @throws \Exception + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_users.edit.group.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_users.group', $data); + + return $data; + } + + /** + * Override preprocessForm to load the user plugin group instead of content. + * + * @param Form $form A form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error loading the form. + */ + protected function preprocessForm(Form $form, $data, $group = '') + { + $obj = is_array($data) ? ArrayHelper::toObject($data, CMSObject::class) : $data; + + if (isset($obj->parent_id) && $obj->parent_id == 0 && $obj->id > 0) { + $form->setFieldAttribute('parent_id', 'type', 'hidden'); + $form->setFieldAttribute('parent_id', 'hidden', 'true'); + } + + parent::preprocessForm($form, $data, 'user'); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + // Include the user plugins for events. + PluginHelper::importPlugin($this->events_map['save']); + + /** + * Check the super admin permissions for group + * We get the parent group permissions and then check the group permissions manually + * We have to calculate the group permissions manually because we haven't saved the group yet + */ + $parentSuperAdmin = Access::checkGroup($data['parent_id'], 'core.admin'); + + // Get core.admin rules from the root asset + $rules = Access::getAssetRules('root.1')->getData('core.admin'); + + // Get the value for the current group (will be true (allowed), false (denied), or null (inherit) + $groupSuperAdmin = $rules['core.admin']->allow($data['id']); + + // We only need to change the $groupSuperAdmin if the parent is true or false. Otherwise, the value set in the rule takes effect. + if ($parentSuperAdmin === false) { + // If parent is false (Denied), effective value will always be false + $groupSuperAdmin = false; + } elseif ($parentSuperAdmin === true) { + // If parent is true (allowed), group is true unless explicitly set to false + $groupSuperAdmin = ($groupSuperAdmin === false) ? false : true; + } + + // Check for non-super admin trying to save with super admin group + $iAmSuperAdmin = Factory::getUser()->authorise('core.admin'); + + if (!$iAmSuperAdmin && $groupSuperAdmin) { + $this->setError(Text::_('JLIB_USER_ERROR_NOT_SUPERADMIN')); + + return false; + } + + /** + * Check for super-admin changing self to be non-super-admin + * First, are we a super admin + */ + if ($iAmSuperAdmin) { + // Next, are we a member of the current group? + $myGroups = Access::getGroupsByUser(Factory::getUser()->get('id'), false); + + if (in_array($data['id'], $myGroups)) { + // Now, would we have super admin permissions without the current group? + $otherGroups = array_diff($myGroups, array($data['id'])); + $otherSuperAdmin = false; + + foreach ($otherGroups as $otherGroup) { + $otherSuperAdmin = $otherSuperAdmin ?: Access::checkGroup($otherGroup, 'core.admin'); + } + + /** + * If we would not otherwise have super admin permissions + * and the current group does not have super admin permissions, throw an exception + */ + if ((!$otherSuperAdmin) && (!$groupSuperAdmin)) { + $this->setError(Text::_('JLIB_USER_ERROR_CANNOT_DEMOTE_SELF')); + + return false; + } + } + } + + if (Factory::getApplication()->input->get('task') == 'save2copy') { + $data['title'] = $this->generateGroupTitle($data['parent_id'], $data['title']); + } + + // Proceed with the save + return parent::save($data); + } + + /** + * Method to delete rows. + * + * @param array &$pks An array of item ids. + * + * @return boolean Returns true on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function delete(&$pks) + { + // Typecast variable. + $pks = (array) $pks; + $user = Factory::getUser(); + $groups = Access::getGroupsByUser($user->get('id')); + + // Get a row instance. + $table = $this->getTable(); + + // Load plugins. + PluginHelper::importPlugin($this->events_map['delete']); + + // Check if I am a Super Admin + $iAmSuperAdmin = $user->authorise('core.admin'); + + foreach ($pks as $pk) { + // Do not allow to delete groups to which the current user belongs + if (in_array($pk, $groups)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_DELETE_ERROR_INVALID_GROUP'), 'error'); + + return false; + } elseif (!$table->load($pk)) { + // Item is not in the table. + $this->setError($table->getError()); + + return false; + } + } + + // Iterate the items to delete each one. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + // Access checks. + $allow = $user->authorise('core.edit.state', 'com_users'); + + // Don't allow non-super-admin to delete a super admin + $allow = (!$iAmSuperAdmin && Access::checkGroup($pk, 'core.admin')) ? false : $allow; + + if ($allow) { + // Fire the before delete event. + Factory::getApplication()->triggerEvent($this->event_before_delete, array($table->getProperties())); + + if (!$table->delete($pk)) { + $this->setError($table->getError()); + + return false; + } else { + // Trigger the after delete event. + Factory::getApplication()->triggerEvent($this->event_after_delete, array($table->getProperties(), true, $this->getError())); + } + } else { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); + } + } + } + + return true; + } + + /** + * Method to generate the title of group on Save as Copy action + * + * @param integer $parentId The id of the parent. + * @param string $title The title of group + * + * @return string Contains the modified title. + * + * @since 3.3.7 + */ + protected function generateGroupTitle($parentId, $title) + { + // Alter the title & alias + $table = $this->getTable(); + + while ($table->load(array('title' => $title, 'parent_id' => $parentId))) { + if ($title == $table->title) { + $title = StringHelper::increment($title); + } + } + + return $title; + } } diff --git a/administrator/components/com_users/src/Model/GroupsModel.php b/administrator/components/com_users/src/Model/GroupsModel.php index c8f9083a80487..26326d14a3d3e 100644 --- a/administrator/components/com_users/src/Model/GroupsModel.php +++ b/administrator/components/com_users/src/Model/GroupsModel.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Users\Administrator\Model; -\defined('_JEXEC') or die; +namespace Joomla\Component\Users\Administrator\Model; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Helper\UserGroupsHelper; @@ -24,237 +24,219 @@ */ class GroupsModel extends ListModel { - /** - * Override parent constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * - * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel - * @since 3.2 - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null) - { - if (empty($config['filter_fields'])) - { - $config['filter_fields'] = array( - 'id', 'a.id', - 'parent_id', 'a.parent_id', - 'title', 'a.title', - 'lft', 'a.lft', - 'rgt', 'a.rgt', - ); - } - - parent::__construct($config, $factory); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = 'a.lft', $direction = 'asc') - { - // Load the parameters. - $params = ComponentHelper::getParams('com_users'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - - return parent::getStoreId($id); - } - - /** - * Gets the list of groups and adds expensive joins to the result set. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 1.6 - */ - public function getItems() - { - // Get a storage key. - $store = $this->getStoreId(); - - // Try to load the data from internal storage. - if (empty($this->cache[$store])) - { - $items = parent::getItems(); - - // Bail out on an error or empty list. - if (empty($items)) - { - $this->cache[$store] = $items; - - return $items; - } - - try - { - $items = $this->populateExtraData($items); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Add the items to the internal cache. - $this->cache[$store] = $items; - } - - return $this->cache[$store]; - } - - /** - * Build an SQL query to load the list data. - * - * @return DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.*' - ) - ); - $query->from($db->quoteName('#__usergroups') . ' AS a'); - - // Filter the comments over the search string if set. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . trim($search) . '%'; - $query->where($db->quoteName('a.title') . ' LIKE :title'); - $query->bind(':title', $search); - } - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Populate level & path for items. - * - * @param array $items Array of \stdClass objects - * - * @return array - * - * @since 3.6.3 - */ - private function populateExtraData(array $items) - { - // First pass: get list of the group ids and reset the counts. - $groupsByKey = array(); - - foreach ($items as $item) - { - $groupsByKey[(int) $item->id] = $item; - } - - $groupIds = array_keys($groupsByKey); - - $db = $this->getDatabase(); - - // Get total enabled users in group. - $query = $db->getQuery(true); - - // Count the objects in the user group. - $query->select('map.group_id, COUNT(DISTINCT map.user_id) AS user_count') - ->from($db->quoteName('#__user_usergroup_map', 'map')) - ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('map.user_id')) - ->whereIn($db->quoteName('map.group_id'), $groupIds) - ->where($db->quoteName('u.block') . ' = 0') - ->group($db->quoteName('map.group_id')); - $db->setQuery($query); - - try - { - $countEnabled = $db->loadAssocList('group_id', 'count_enabled'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Get total disabled users in group. - $query->clear(); - $query->select('map.group_id, COUNT(DISTINCT map.user_id) AS user_count') - ->from($db->quoteName('#__user_usergroup_map', 'map')) - ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('map.user_id')) - ->whereIn($db->quoteName('map.group_id'), $groupIds) - ->where($db->quoteName('u.block') . ' = 1') - ->group($db->quoteName('map.group_id')); - $db->setQuery($query); - - try - { - $countDisabled = $db->loadAssocList('group_id', 'count_disabled'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Inject the values back into the array. - foreach ($groupsByKey as &$item) - { - $item->count_enabled = isset($countEnabled[$item->id]) ? (int) $countEnabled[$item->id]['user_count'] : 0; - $item->count_disabled = isset($countDisabled[$item->id]) ? (int) $countDisabled[$item->id]['user_count'] : 0; - $item->user_count = $item->count_enabled + $item->count_disabled; - } - - $groups = new UserGroupsHelper($groupsByKey); - - return array_values($groups->getAll()); - } + /** + * Override parent constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'parent_id', 'a.parent_id', + 'title', 'a.title', + 'lft', 'a.lft', + 'rgt', 'a.rgt', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.lft', $direction = 'asc') + { + // Load the parameters. + $params = ComponentHelper::getParams('com_users'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + + return parent::getStoreId($id); + } + + /** + * Gets the list of groups and adds expensive joins to the result set. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 1.6 + */ + public function getItems() + { + // Get a storage key. + $store = $this->getStoreId(); + + // Try to load the data from internal storage. + if (empty($this->cache[$store])) { + $items = parent::getItems(); + + // Bail out on an error or empty list. + if (empty($items)) { + $this->cache[$store] = $items; + + return $items; + } + + try { + $items = $this->populateExtraData($items); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Add the items to the internal cache. + $this->cache[$store] = $items; + } + + return $this->cache[$store]; + } + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.*' + ) + ); + $query->from($db->quoteName('#__usergroups') . ' AS a'); + + // Filter the comments over the search string if set. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . trim($search) . '%'; + $query->where($db->quoteName('a.title') . ' LIKE :title'); + $query->bind(':title', $search); + } + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.lft')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Populate level & path for items. + * + * @param array $items Array of \stdClass objects + * + * @return array + * + * @since 3.6.3 + */ + private function populateExtraData(array $items) + { + // First pass: get list of the group ids and reset the counts. + $groupsByKey = array(); + + foreach ($items as $item) { + $groupsByKey[(int) $item->id] = $item; + } + + $groupIds = array_keys($groupsByKey); + + $db = $this->getDatabase(); + + // Get total enabled users in group. + $query = $db->getQuery(true); + + // Count the objects in the user group. + $query->select('map.group_id, COUNT(DISTINCT map.user_id) AS user_count') + ->from($db->quoteName('#__user_usergroup_map', 'map')) + ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('map.user_id')) + ->whereIn($db->quoteName('map.group_id'), $groupIds) + ->where($db->quoteName('u.block') . ' = 0') + ->group($db->quoteName('map.group_id')); + $db->setQuery($query); + + try { + $countEnabled = $db->loadAssocList('group_id', 'count_enabled'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Get total disabled users in group. + $query->clear(); + $query->select('map.group_id, COUNT(DISTINCT map.user_id) AS user_count') + ->from($db->quoteName('#__user_usergroup_map', 'map')) + ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('map.user_id')) + ->whereIn($db->quoteName('map.group_id'), $groupIds) + ->where($db->quoteName('u.block') . ' = 1') + ->group($db->quoteName('map.group_id')); + $db->setQuery($query); + + try { + $countDisabled = $db->loadAssocList('group_id', 'count_disabled'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Inject the values back into the array. + foreach ($groupsByKey as &$item) { + $item->count_enabled = isset($countEnabled[$item->id]) ? (int) $countEnabled[$item->id]['user_count'] : 0; + $item->count_disabled = isset($countDisabled[$item->id]) ? (int) $countDisabled[$item->id]['user_count'] : 0; + $item->user_count = $item->count_enabled + $item->count_disabled; + } + + $groups = new UserGroupsHelper($groupsByKey); + + return array_values($groups->getAll()); + } } diff --git a/administrator/components/com_users/src/Model/LevelModel.php b/administrator/components/com_users/src/Model/LevelModel.php index 3cd56420196db..8574a135312ad 100644 --- a/administrator/components/com_users/src/Model/LevelModel.php +++ b/administrator/components/com_users/src/Model/LevelModel.php @@ -1,4 +1,5 @@ rules); - - if ($groups === null) - { - throw new \RuntimeException('Invalid rules schema'); - } - - $isAdmin = Factory::getUser()->authorise('core.admin'); - - // Check permissions - foreach ($groups as $group) - { - if (!$isAdmin && Access::checkGroup($group, 'core.admin')) - { - $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); - - return false; - } - } - - // Check if the access level is being used by any content. - if ($this->levelsInUse === null) - { - // Populate the list once. - $this->levelsInUse = array(); - - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('DISTINCT access'); - - // Get all the tables and the prefix - $tables = $db->getTableList(); - $prefix = $db->getPrefix(); - - foreach ($tables as $table) - { - // Get all of the columns in the table - $fields = $db->getTableColumns($table); - - /** - * We are looking for the access field. If custom tables are using something other - * than the 'access' field they are on their own unfortunately. - * Also make sure the table prefix matches the live db prefix (eg, it is not a "bak_" table) - */ - if (strpos($table, $prefix) === 0 && isset($fields['access'])) - { - // Lookup the distinct values of the field. - $query->clear('from') - ->from($db->quoteName($table)); - $db->setQuery($query); - - try - { - $values = $db->loadColumn(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - $this->levelsInUse = array_merge($this->levelsInUse, $values); - - // @todo Could assemble an array of the tables used by each view level list those, - // giving the user a clue in the error where to look. - } - } - - // Get uniques. - $this->levelsInUse = array_unique($this->levelsInUse); - - // Ok, after all that we are ready to check the record :) - } - - if (in_array($record->id, $this->levelsInUse)) - { - $this->setError(Text::sprintf('COM_USERS_ERROR_VIEW_LEVEL_IN_USE', $record->id, $record->title)); - - return false; - } - - return parent::canDelete($record); - } - - /** - * Returns a reference to the a Table object, always creating it. - * - * @param string $type The table type to instantiate - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A database object - * - * @since 1.6 - */ - public function getTable($type = 'ViewLevel', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) - { - $return = Table::getInstance($type, $prefix, $config); - - return $return; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - $result = parent::getItem($pk); - - // Convert the params field to an array. - $result->rules = json_decode($result->rules); - - return $result; - } - - /** - * Method to get the record form. - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_users.level', 'level', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - * @throws \Exception - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_users.edit.level.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_users.level', $data); - - return $data; - } - - /** - * Method to preprocess the form - * - * @param Form $form A form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error loading the form. - */ - protected function preprocessForm(Form $form, $data, $group = '') - { - // TO DO warning! - parent::preprocessForm($form, $data, 'user'); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - */ - public function save($data) - { - if (!isset($data['rules'])) - { - $data['rules'] = array(); - } - - $data['title'] = InputFilter::getInstance()->clean($data['title'], 'TRIM'); - - return parent::save($data); - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @see \Joomla\CMS\Form\FormRule - * @see \JFilterInput - * @since 3.8.8 - */ - public function validate($form, $data, $group = null) - { - $isSuperAdmin = Factory::getUser()->authorise('core.admin'); - - // Non Super user should not be able to change the access levels of super user groups - if (!$isSuperAdmin) - { - if (!isset($data['rules']) || !is_array($data['rules'])) - { - $data['rules'] = array(); - } - - $groups = array_values(UserGroupsHelper::getInstance()->getAll()); - - $rules = array(); - - if (!empty($data['id'])) - { - $table = $this->getTable(); - - $table->load($data['id']); - - $rules = json_decode($table->rules); - } - - $rules = ArrayHelper::toInteger($rules); - - for ($i = 0, $n = count($groups); $i < $n; ++$i) - { - if (Access::checkGroup((int) $groups[$i]->id, 'core.admin')) - { - if (in_array((int) $groups[$i]->id, $rules) && !in_array((int) $groups[$i]->id, $data['rules'])) - { - $data['rules'][] = (int) $groups[$i]->id; - } - elseif (!in_array((int) $groups[$i]->id, $rules) && in_array((int) $groups[$i]->id, $data['rules'])) - { - $this->setError(Text::_('JLIB_USER_ERROR_NOT_SUPERADMIN')); - - return false; - } - } - } - } - - return parent::validate($form, $data, $group); - } + /** + * @var array A list of the access levels in use. + * @since 1.6 + */ + protected $levelsInUse = null; + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission set in the component. + * + * @since 1.6 + */ + protected function canDelete($record) + { + $groups = json_decode($record->rules); + + if ($groups === null) { + throw new \RuntimeException('Invalid rules schema'); + } + + $isAdmin = Factory::getUser()->authorise('core.admin'); + + // Check permissions + foreach ($groups as $group) { + if (!$isAdmin && Access::checkGroup($group, 'core.admin')) { + $this->setError(Text::_('JERROR_ALERTNOAUTHOR')); + + return false; + } + } + + // Check if the access level is being used by any content. + if ($this->levelsInUse === null) { + // Populate the list once. + $this->levelsInUse = array(); + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('DISTINCT access'); + + // Get all the tables and the prefix + $tables = $db->getTableList(); + $prefix = $db->getPrefix(); + + foreach ($tables as $table) { + // Get all of the columns in the table + $fields = $db->getTableColumns($table); + + /** + * We are looking for the access field. If custom tables are using something other + * than the 'access' field they are on their own unfortunately. + * Also make sure the table prefix matches the live db prefix (eg, it is not a "bak_" table) + */ + if (strpos($table, $prefix) === 0 && isset($fields['access'])) { + // Lookup the distinct values of the field. + $query->clear('from') + ->from($db->quoteName($table)); + $db->setQuery($query); + + try { + $values = $db->loadColumn(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + $this->levelsInUse = array_merge($this->levelsInUse, $values); + + // @todo Could assemble an array of the tables used by each view level list those, + // giving the user a clue in the error where to look. + } + } + + // Get uniques. + $this->levelsInUse = array_unique($this->levelsInUse); + + // Ok, after all that we are ready to check the record :) + } + + if (in_array($record->id, $this->levelsInUse)) { + $this->setError(Text::sprintf('COM_USERS_ERROR_VIEW_LEVEL_IN_USE', $record->id, $record->title)); + + return false; + } + + return parent::canDelete($record); + } + + /** + * Returns a reference to the a Table object, always creating it. + * + * @param string $type The table type to instantiate + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A database object + * + * @since 1.6 + */ + public function getTable($type = 'ViewLevel', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) + { + $return = Table::getInstance($type, $prefix, $config); + + return $return; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + $result = parent::getItem($pk); + + // Convert the params field to an array. + $result->rules = json_decode($result->rules); + + return $result; + } + + /** + * Method to get the record form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.level', 'level', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + * @throws \Exception + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_users.edit.level.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_users.level', $data); + + return $data; + } + + /** + * Method to preprocess the form + * + * @param Form $form A form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error loading the form. + */ + protected function preprocessForm(Form $form, $data, $group = '') + { + // TO DO warning! + parent::preprocessForm($form, $data, 'user'); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + */ + public function save($data) + { + if (!isset($data['rules'])) { + $data['rules'] = array(); + } + + $data['title'] = InputFilter::getInstance()->clean($data['title'], 'TRIM'); + + return parent::save($data); + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @see \Joomla\CMS\Form\FormRule + * @see \JFilterInput + * @since 3.8.8 + */ + public function validate($form, $data, $group = null) + { + $isSuperAdmin = Factory::getUser()->authorise('core.admin'); + + // Non Super user should not be able to change the access levels of super user groups + if (!$isSuperAdmin) { + if (!isset($data['rules']) || !is_array($data['rules'])) { + $data['rules'] = array(); + } + + $groups = array_values(UserGroupsHelper::getInstance()->getAll()); + + $rules = array(); + + if (!empty($data['id'])) { + $table = $this->getTable(); + + $table->load($data['id']); + + $rules = json_decode($table->rules); + } + + $rules = ArrayHelper::toInteger($rules); + + for ($i = 0, $n = count($groups); $i < $n; ++$i) { + if (Access::checkGroup((int) $groups[$i]->id, 'core.admin')) { + if (in_array((int) $groups[$i]->id, $rules) && !in_array((int) $groups[$i]->id, $data['rules'])) { + $data['rules'][] = (int) $groups[$i]->id; + } elseif (!in_array((int) $groups[$i]->id, $rules) && in_array((int) $groups[$i]->id, $data['rules'])) { + $this->setError(Text::_('JLIB_USER_ERROR_NOT_SUPERADMIN')); + + return false; + } + } + } + } + + return parent::validate($form, $data, $group); + } } diff --git a/administrator/components/com_users/src/Model/LevelsModel.php b/administrator/components/com_users/src/Model/LevelsModel.php index 7bd91ce32c56e..66c2a924a4a55 100644 --- a/administrator/components/com_users/src/Model/LevelsModel.php +++ b/administrator/components/com_users/src/Model/LevelsModel.php @@ -1,4 +1,5 @@ setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - - return parent::getStoreId($id); - } - - /** - * Build an SQL query to load the list data. - * - * @return DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.*' - ) - ); - $query->from($db->quoteName('#__viewlevels') . ' AS a'); - - // Add the level in the tree. - $query->group('a.id, a.title, a.ordering, a.rules'); - - // Filter the items over the search string if set. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $ids, ParameterType::INTEGER); - } - else - { - $search = '%' . trim($search) . '%'; - $query->where('a.title LIKE :title') - ->bind(':title', $search); - } - } - - $query->group('a.id'); - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Method to adjust the ordering of a row. - * - * @param integer $pk The ID of the primary key to move. - * @param integer $direction Increment, usually +1 or -1 - * - * @return boolean False on failure or error, true otherwise. - */ - public function reorder($pk, $direction = 0) - { - // Sanitize the id and adjustment. - $pk = (!empty($pk)) ? $pk : (int) $this->getState('level.id'); - $user = Factory::getUser(); - - // Get an instance of the record's table. - $table = Table::getInstance('ViewLevel', 'Joomla\\CMS\Table\\'); - - // Load the row. - if (!$table->load($pk)) - { - $this->setError($table->getError()); - - return false; - } - - // Access checks. - $allow = $user->authorise('core.edit.state', 'com_users'); - - if (!$allow) - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); - - return false; - } - - // Move the row. - // @todo: Where clause to restrict category. - $table->move($pk); - - return true; - } - - /** - * Saves the manually set order of records. - * - * @param array $pks An array of primary key ids. - * @param integer $order Order position - * - * @return boolean Boolean true on success, boolean false - * - * @throws \Exception - */ - public function saveorder($pks, $order) - { - $table = Table::getInstance('viewlevel', 'Joomla\\CMS\Table\\'); - $user = Factory::getUser(); - $conditions = array(); - - if (empty($pks)) - { - Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_ERROR_LEVELS_NOLEVELS_SELECTED'), 'error'); - - return false; - } - - // Update ordering values - foreach ($pks as $i => $pk) - { - $table->load((int) $pk); - - // Access checks. - $allow = $user->authorise('core.edit.state', 'com_users'); - - if (!$allow) - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); - } - elseif ($table->ordering != $order[$i]) - { - $table->ordering = $order[$i]; - - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - } - } - - // Execute reorder for each category. - foreach ($conditions as $cond) - { - $table->load($cond[0]); - $table->reorder($cond[1]); - } - - return true; - } + /** + * Override parent constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'ordering', 'a.ordering', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.ordering', $direction = 'asc') + { + // Load the parameters. + $params = ComponentHelper::getParams('com_users'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.*' + ) + ); + $query->from($db->quoteName('#__viewlevels') . ' AS a'); + + // Add the level in the tree. + $query->group('a.id, a.title, a.ordering, a.rules'); + + // Filter the items over the search string if set. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $ids, ParameterType::INTEGER); + } else { + $search = '%' . trim($search) . '%'; + $query->where('a.title LIKE :title') + ->bind(':title', $search); + } + } + + $query->group('a.id'); + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Method to adjust the ordering of a row. + * + * @param integer $pk The ID of the primary key to move. + * @param integer $direction Increment, usually +1 or -1 + * + * @return boolean False on failure or error, true otherwise. + */ + public function reorder($pk, $direction = 0) + { + // Sanitize the id and adjustment. + $pk = (!empty($pk)) ? $pk : (int) $this->getState('level.id'); + $user = Factory::getUser(); + + // Get an instance of the record's table. + $table = Table::getInstance('ViewLevel', 'Joomla\\CMS\Table\\'); + + // Load the row. + if (!$table->load($pk)) { + $this->setError($table->getError()); + + return false; + } + + // Access checks. + $allow = $user->authorise('core.edit.state', 'com_users'); + + if (!$allow) { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED')); + + return false; + } + + // Move the row. + // @todo: Where clause to restrict category. + $table->move($pk); + + return true; + } + + /** + * Saves the manually set order of records. + * + * @param array $pks An array of primary key ids. + * @param integer $order Order position + * + * @return boolean Boolean true on success, boolean false + * + * @throws \Exception + */ + public function saveorder($pks, $order) + { + $table = Table::getInstance('viewlevel', 'Joomla\\CMS\Table\\'); + $user = Factory::getUser(); + $conditions = array(); + + if (empty($pks)) { + Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_ERROR_LEVELS_NOLEVELS_SELECTED'), 'error'); + + return false; + } + + // Update ordering values + foreach ($pks as $i => $pk) { + $table->load((int) $pk); + + // Access checks. + $allow = $user->authorise('core.edit.state', 'com_users'); + + if (!$allow) { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); + } elseif ($table->ordering != $order[$i]) { + $table->ordering = $order[$i]; + + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + } + } + + // Execute reorder for each category. + foreach ($conditions as $cond) { + $table->load($cond[0]); + $table->reorder($cond[1]); + } + + return true; + } } diff --git a/administrator/components/com_users/src/Model/MailModel.php b/administrator/components/com_users/src/Model/MailModel.php index 35b3c5dfc696e..eaa2df60b88ba 100644 --- a/administrator/components/com_users/src/Model/MailModel.php +++ b/administrator/components/com_users/src/Model/MailModel.php @@ -1,4 +1,5 @@ loadForm('com_users.mail', 'mail', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - * @throws \Exception - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_users.display.mail.data', array()); - - $this->preprocessData('com_users.mail', $data); - - return $data; - } - - /** - * Method to preprocess the form - * - * @param Form $form A form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error loading the form. - */ - protected function preprocessForm(Form $form, $data, $group = 'user') - { - parent::preprocessForm($form, $data, $group); - } - - /** - * Send the email - * - * @return boolean - * - * @throws \Exception - */ - public function send() - { - $app = Factory::getApplication(); - $data = $app->input->post->get('jform', array(), 'array'); - $user = Factory::getUser(); - $access = new Access; - $db = $this->getDatabase(); - $language = Factory::getLanguage(); - - $mode = array_key_exists('mode', $data) ? (int) $data['mode'] : 0; - $subject = array_key_exists('subject', $data) ? $data['subject'] : ''; - $grp = array_key_exists('group', $data) ? (int) $data['group'] : 0; - $recurse = array_key_exists('recurse', $data) ? (int) $data['recurse'] : 0; - $bcc = array_key_exists('bcc', $data) ? (int) $data['bcc'] : 0; - $disabled = array_key_exists('disabled', $data) ? (int) $data['disabled'] : 0; - $message_body = array_key_exists('message', $data) ? $data['message'] : ''; - - // Automatically removes html formatting - if (!$mode) - { - $message_body = InputFilter::getInstance()->clean($message_body, 'string'); - } - - // Check for a message body and subject - if (!$message_body || !$subject) - { - $app->setUserState('com_users.display.mail.data', $data); - $this->setError(Text::_('COM_USERS_MAIL_PLEASE_FILL_IN_THE_FORM_CORRECTLY')); - - return false; - } - - // Get users in the group out of the ACL, if group is provided. - $to = $grp !== 0 ? $access->getUsersByGroup($grp, $recurse) : array(); - - // When group is provided but no users are found in the group. - if ($grp !== 0 && !$to) - { - $rows = array(); - } - else - { - // Get all users email and group except for senders - $uid = (int) $user->id; - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('email'), - $db->quoteName('name'), - ] - ) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('id') . ' != :id') - ->bind(':id', $uid, ParameterType::INTEGER); - - if ($grp !== 0) - { - $query->whereIn($db->quoteName('id'), $to); - } - - if ($disabled === 0) - { - $query->where($db->quoteName('block') . ' = 0'); - } - - $db->setQuery($query); - $rows = $db->loadObjectList(); - } - - // Check to see if there are any users in this group before we continue - if (!$rows) - { - $app->setUserState('com_users.display.mail.data', $data); - - if (in_array($user->id, $to)) - { - $this->setError(Text::_('COM_USERS_MAIL_ONLY_YOU_COULD_BE_FOUND_IN_THIS_GROUP')); - } - else - { - $this->setError(Text::_('COM_USERS_MAIL_NO_USERS_COULD_BE_FOUND_IN_THIS_GROUP')); - } - - return false; - } - - // Get the Mailer - $mailer = new MailTemplate('com_users.massmail.mail', $language->getTag()); - $params = ComponentHelper::getParams('com_users'); - - try - { - // Build email message format. - $data = [ - 'subject' => stripslashes($subject), - 'body' => $message_body, - 'subjectprefix' => $params->get('mailSubjectPrefix', ''), - 'bodysuffix' => $params->get('mailBodySuffix', '') - ]; - $mailer->addTemplateData($data); - - $recipientType = $bcc ? 'bcc' : 'to'; - - // Add recipients - foreach ($rows as $row) - { - $mailer->addRecipient($row->email, $row->name, $recipientType); - } - - if ($bcc) - { - $mailer->addRecipient($app->get('mailfrom'), $app->get('fromname')); - } - - // Send the Mail - $rs = $mailer->send(); - } - catch (MailDisabledException | phpMailerException $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $rs = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $rs = false; - } - } - - // Check for an error - if ($rs !== true) - { - $app->setUserState('com_users.display.mail.data', $data); - $this->setError($mailer->ErrorInfo); - - return false; - } - elseif (empty($rs)) - { - $app->setUserState('com_users.display.mail.data', $data); - $this->setError(Text::_('COM_USERS_MAIL_THE_MAIL_COULD_NOT_BE_SENT')); - - return false; - } - else - { - /** - * Fill the data (specially for the 'mode', 'group' and 'bcc': they could not exist in the array - * when the box is not checked and in this case, the default value would be used instead of the '0' - * one) - */ - $data['mode'] = $mode; - $data['subject'] = $subject; - $data['group'] = $grp; - $data['recurse'] = $recurse; - $data['bcc'] = $bcc; - $data['message'] = $message_body; - $app->setUserState('com_users.display.mail.data', array()); - $app->enqueueMessage(Text::plural('COM_USERS_MAIL_EMAIL_SENT_TO_N_USERS', count($rows)), 'message'); - - return true; - } - } + /** + * Method to get the row form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.mail', 'mail', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + * @throws \Exception + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_users.display.mail.data', array()); + + $this->preprocessData('com_users.mail', $data); + + return $data; + } + + /** + * Method to preprocess the form + * + * @param Form $form A form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error loading the form. + */ + protected function preprocessForm(Form $form, $data, $group = 'user') + { + parent::preprocessForm($form, $data, $group); + } + + /** + * Send the email + * + * @return boolean + * + * @throws \Exception + */ + public function send() + { + $app = Factory::getApplication(); + $data = $app->input->post->get('jform', array(), 'array'); + $user = Factory::getUser(); + $access = new Access(); + $db = $this->getDatabase(); + $language = Factory::getLanguage(); + + $mode = array_key_exists('mode', $data) ? (int) $data['mode'] : 0; + $subject = array_key_exists('subject', $data) ? $data['subject'] : ''; + $grp = array_key_exists('group', $data) ? (int) $data['group'] : 0; + $recurse = array_key_exists('recurse', $data) ? (int) $data['recurse'] : 0; + $bcc = array_key_exists('bcc', $data) ? (int) $data['bcc'] : 0; + $disabled = array_key_exists('disabled', $data) ? (int) $data['disabled'] : 0; + $message_body = array_key_exists('message', $data) ? $data['message'] : ''; + + // Automatically removes html formatting + if (!$mode) { + $message_body = InputFilter::getInstance()->clean($message_body, 'string'); + } + + // Check for a message body and subject + if (!$message_body || !$subject) { + $app->setUserState('com_users.display.mail.data', $data); + $this->setError(Text::_('COM_USERS_MAIL_PLEASE_FILL_IN_THE_FORM_CORRECTLY')); + + return false; + } + + // Get users in the group out of the ACL, if group is provided. + $to = $grp !== 0 ? $access->getUsersByGroup($grp, $recurse) : array(); + + // When group is provided but no users are found in the group. + if ($grp !== 0 && !$to) { + $rows = array(); + } else { + // Get all users email and group except for senders + $uid = (int) $user->id; + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('email'), + $db->quoteName('name'), + ] + ) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('id') . ' != :id') + ->bind(':id', $uid, ParameterType::INTEGER); + + if ($grp !== 0) { + $query->whereIn($db->quoteName('id'), $to); + } + + if ($disabled === 0) { + $query->where($db->quoteName('block') . ' = 0'); + } + + $db->setQuery($query); + $rows = $db->loadObjectList(); + } + + // Check to see if there are any users in this group before we continue + if (!$rows) { + $app->setUserState('com_users.display.mail.data', $data); + + if (in_array($user->id, $to)) { + $this->setError(Text::_('COM_USERS_MAIL_ONLY_YOU_COULD_BE_FOUND_IN_THIS_GROUP')); + } else { + $this->setError(Text::_('COM_USERS_MAIL_NO_USERS_COULD_BE_FOUND_IN_THIS_GROUP')); + } + + return false; + } + + // Get the Mailer + $mailer = new MailTemplate('com_users.massmail.mail', $language->getTag()); + $params = ComponentHelper::getParams('com_users'); + + try { + // Build email message format. + $data = [ + 'subject' => stripslashes($subject), + 'body' => $message_body, + 'subjectprefix' => $params->get('mailSubjectPrefix', ''), + 'bodysuffix' => $params->get('mailBodySuffix', '') + ]; + $mailer->addTemplateData($data); + + $recipientType = $bcc ? 'bcc' : 'to'; + + // Add recipients + foreach ($rows as $row) { + $mailer->addRecipient($row->email, $row->name, $recipientType); + } + + if ($bcc) { + $mailer->addRecipient($app->get('mailfrom'), $app->get('fromname')); + } + + // Send the Mail + $rs = $mailer->send(); + } catch (MailDisabledException | phpMailerException $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $rs = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $rs = false; + } + } + + // Check for an error + if ($rs !== true) { + $app->setUserState('com_users.display.mail.data', $data); + $this->setError($mailer->ErrorInfo); + + return false; + } elseif (empty($rs)) { + $app->setUserState('com_users.display.mail.data', $data); + $this->setError(Text::_('COM_USERS_MAIL_THE_MAIL_COULD_NOT_BE_SENT')); + + return false; + } else { + /** + * Fill the data (specially for the 'mode', 'group' and 'bcc': they could not exist in the array + * when the box is not checked and in this case, the default value would be used instead of the '0' + * one) + */ + $data['mode'] = $mode; + $data['subject'] = $subject; + $data['group'] = $grp; + $data['recurse'] = $recurse; + $data['bcc'] = $bcc; + $data['message'] = $message_body; + $app->setUserState('com_users.display.mail.data', array()); + $app->enqueueMessage(Text::plural('COM_USERS_MAIL_EMAIL_SENT_TO_N_USERS', count($rows)), 'message'); + + return true; + } + } } diff --git a/administrator/components/com_users/src/Model/MethodModel.php b/administrator/components/com_users/src/Model/MethodModel.php index c909f6cf8b78f..32df3181299a8 100644 --- a/administrator/components/com_users/src/Model/MethodModel.php +++ b/administrator/components/com_users/src/Model/MethodModel.php @@ -1,4 +1,5 @@ methodExists($method)) - { - return [ - 'name' => $method, - 'display' => '', - 'shortinfo' => '', - 'image' => '', - 'canDisable' => true, - 'allowMultiple' => true, - ]; - } - - return $this->mfaMethods[$method]; - } - - /** - * Is the specified MFA Method available? - * - * @param string $method The Method to check. - * - * @return boolean - * @since 4.2.0 - */ - public function methodExists(string $method): bool - { - if (!is_array($this->mfaMethods)) - { - $this->populateMfaMethods(); - } - - return isset($this->mfaMethods[$method]); - } - - /** - * @param User|null $user The user record. Null to use the currently logged in user. - * - * @return array - * @throws Exception - * - * @since 4.2.0 - */ - public function getRenderOptions(?User $user = null): SetupRenderOptions - { - if (is_null($user)) - { - $user = Factory::getApplication()->getIdentity() ?: Factory::getUser(); - } - - $renderOptions = new SetupRenderOptions; - - $event = new GetSetup($this->getRecord($user)); - $results = Factory::getApplication() - ->getDispatcher() - ->dispatch($event->getName(), $event) - ->getArgument('result', []); - - if (empty($results)) - { - return $renderOptions; - } - - foreach ($results as $result) - { - if (empty($result)) - { - continue; - } - - return $renderOptions->merge($result); - } - - return $renderOptions; - } - - /** - * Get the specified MFA record. It will return a fake default record when no record ID is specified. - * - * @param User|null $user The user record. Null to use the currently logged in user. - * - * @return MfaTable - * @throws Exception - * - * @since 4.2.0 - */ - public function getRecord(User $user = null): MfaTable - { - if (is_null($user)) - { - $user = Factory::getApplication()->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - $defaultRecord = $this->getDefaultRecord($user); - $id = (int) $this->getState('id', 0); - - if ($id <= 0) - { - return $defaultRecord; - } - - /** @var MfaTable $record */ - $record = $this->getTable('Mfa', 'Administrator'); - $loaded = $record->load( - [ - 'user_id' => $user->id, - 'id' => $id, - ] - ); - - if (!$loaded) - { - return $defaultRecord; - } - - if (!$this->methodExists($record->method)) - { - return $defaultRecord; - } - - return $record; - } - - /** - * Return the title to use for the page - * - * @return string - * - * @since 4.2.0 - */ - public function getPageTitle(): string - { - $task = $this->getState('task', 'edit'); - - switch ($task) - { - case 'mfa': - $key = 'COM_USERS_USER_MULTIFACTOR_AUTH'; - break; - - default: - $key = sprintf('COM_USERS_MFA_%s_PAGE_HEAD', $task); - break; - } - - return Text::_($key); - } - - /** - * @param User|null $user The user record. Null to use the current user. - * - * @return MfaTable - * @throws Exception - * - * @since 4.2.0 - */ - protected function getDefaultRecord(?User $user = null): MfaTable - { - if (is_null($user)) - { - $user = Factory::getApplication()->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - $method = $this->getState('method'); - $title = ''; - - if (is_null($this->mfaMethods)) - { - $this->populateMfaMethods(); - } - - if ($method && isset($this->mfaMethods[$method])) - { - $title = $this->mfaMethods[$method]['display']; - } - - /** @var MfaTable $record */ - $record = $this->getTable('Mfa', 'Administrator'); - - $record->bind( - [ - 'id' => null, - 'user_id' => $user->id, - 'title' => $title, - 'method' => $method, - 'default' => 0, - 'options' => [], - ] - ); - - return $record; - } - - /** - * Populate the list of MFA Methods - * - * @return void - * @since 4.2.0 - */ - private function populateMfaMethods(): void - { - $this->mfaMethods = []; - $mfaMethods = MfaHelper::getMfaMethods(); - - if (empty($mfaMethods)) - { - return; - } - - foreach ($mfaMethods as $method) - { - $this->mfaMethods[$method['name']] = $method; - } - - // We also need to add the backup codes Method - $this->mfaMethods['backupcodes'] = [ - 'name' => 'backupcodes', - 'display' => Text::_('COM_USERS_USER_BACKUPCODES'), - 'shortinfo' => Text::_('COM_USERS_USER_BACKUPCODES_DESC'), - 'image' => 'media/com_users/images/emergency.svg', - 'canDisable' => false, - 'allowMultiple' => false, - ]; - } + /** + * List of MFA Methods + * + * @var array + * @since 4.2.0 + */ + protected $mfaMethods = null; + + /** + * Get the specified MFA Method's record + * + * @param string $method The Method to retrieve. + * + * @return array + * @since 4.2.0 + */ + public function getMethod(string $method): array + { + if (!$this->methodExists($method)) { + return [ + 'name' => $method, + 'display' => '', + 'shortinfo' => '', + 'image' => '', + 'canDisable' => true, + 'allowMultiple' => true, + ]; + } + + return $this->mfaMethods[$method]; + } + + /** + * Is the specified MFA Method available? + * + * @param string $method The Method to check. + * + * @return boolean + * @since 4.2.0 + */ + public function methodExists(string $method): bool + { + if (!is_array($this->mfaMethods)) { + $this->populateMfaMethods(); + } + + return isset($this->mfaMethods[$method]); + } + + /** + * @param User|null $user The user record. Null to use the currently logged in user. + * + * @return array + * @throws Exception + * + * @since 4.2.0 + */ + public function getRenderOptions(?User $user = null): SetupRenderOptions + { + if (is_null($user)) { + $user = Factory::getApplication()->getIdentity() ?: Factory::getUser(); + } + + $renderOptions = new SetupRenderOptions(); + + $event = new GetSetup($this->getRecord($user)); + $results = Factory::getApplication() + ->getDispatcher() + ->dispatch($event->getName(), $event) + ->getArgument('result', []); + + if (empty($results)) { + return $renderOptions; + } + + foreach ($results as $result) { + if (empty($result)) { + continue; + } + + return $renderOptions->merge($result); + } + + return $renderOptions; + } + + /** + * Get the specified MFA record. It will return a fake default record when no record ID is specified. + * + * @param User|null $user The user record. Null to use the currently logged in user. + * + * @return MfaTable + * @throws Exception + * + * @since 4.2.0 + */ + public function getRecord(User $user = null): MfaTable + { + if (is_null($user)) { + $user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + $defaultRecord = $this->getDefaultRecord($user); + $id = (int) $this->getState('id', 0); + + if ($id <= 0) { + return $defaultRecord; + } + + /** @var MfaTable $record */ + $record = $this->getTable('Mfa', 'Administrator'); + $loaded = $record->load( + [ + 'user_id' => $user->id, + 'id' => $id, + ] + ); + + if (!$loaded) { + return $defaultRecord; + } + + if (!$this->methodExists($record->method)) { + return $defaultRecord; + } + + return $record; + } + + /** + * Return the title to use for the page + * + * @return string + * + * @since 4.2.0 + */ + public function getPageTitle(): string + { + $task = $this->getState('task', 'edit'); + + switch ($task) { + case 'mfa': + $key = 'COM_USERS_USER_MULTIFACTOR_AUTH'; + break; + + default: + $key = sprintf('COM_USERS_MFA_%s_PAGE_HEAD', $task); + break; + } + + return Text::_($key); + } + + /** + * @param User|null $user The user record. Null to use the current user. + * + * @return MfaTable + * @throws Exception + * + * @since 4.2.0 + */ + protected function getDefaultRecord(?User $user = null): MfaTable + { + if (is_null($user)) { + $user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + $method = $this->getState('method'); + $title = ''; + + if (is_null($this->mfaMethods)) { + $this->populateMfaMethods(); + } + + if ($method && isset($this->mfaMethods[$method])) { + $title = $this->mfaMethods[$method]['display']; + } + + /** @var MfaTable $record */ + $record = $this->getTable('Mfa', 'Administrator'); + + $record->bind( + [ + 'id' => null, + 'user_id' => $user->id, + 'title' => $title, + 'method' => $method, + 'default' => 0, + 'options' => [], + ] + ); + + return $record; + } + + /** + * Populate the list of MFA Methods + * + * @return void + * @since 4.2.0 + */ + private function populateMfaMethods(): void + { + $this->mfaMethods = []; + $mfaMethods = MfaHelper::getMfaMethods(); + + if (empty($mfaMethods)) { + return; + } + + foreach ($mfaMethods as $method) { + $this->mfaMethods[$method['name']] = $method; + } + + // We also need to add the backup codes Method + $this->mfaMethods['backupcodes'] = [ + 'name' => 'backupcodes', + 'display' => Text::_('COM_USERS_USER_BACKUPCODES'), + 'shortinfo' => Text::_('COM_USERS_USER_BACKUPCODES_DESC'), + 'image' => 'media/com_users/images/emergency.svg', + 'canDisable' => false, + 'allowMultiple' => false, + ]; + } } diff --git a/administrator/components/com_users/src/Model/MethodsModel.php b/administrator/components/com_users/src/Model/MethodsModel.php index 3b34a3470f56b..e010a703edef9 100644 --- a/administrator/components/com_users/src/Model/MethodsModel.php +++ b/administrator/components/com_users/src/Model/MethodsModel.php @@ -1,4 +1,5 @@ getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - if ($user->guest) - { - return []; - } - - // Get an associative array of MFA Methods - $rawMethods = MfaHelper::getMfaMethods(); - $methods = []; - - foreach ($rawMethods as $method) - { - $method['active'] = []; - $methods[$method['name']] = $method; - } - - // Put the user MFA records into the Methods array - $userMfaRecords = MfaHelper::getUserMfaRecords($user->id); - - if (!empty($userMfaRecords)) - { - foreach ($userMfaRecords as $record) - { - if (!isset($methods[$record->method])) - { - continue; - } - - $methods[$record->method]->addActiveMethod($record); - } - } - - return $methods; - } - - /** - * Delete all Multi-factor Authentication Methods for the given user. - * - * @param User|null $user The user object to reset MFA for. Null to use the current user. - * - * @return void - * @throws Exception - * - * @since 4.2.0 - */ - public function deleteAll(?User $user = null): void - { - // Make sure we have a user object - if (is_null($user)) - { - $user = Factory::getApplication()->getIdentity() ?: Factory::getUser(); - } - - // If the user object is a guest (who can't have MFA) we abort with an error - if ($user->guest) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__user_mfa')) - ->where($db->quoteName('user_id') . ' = :user_id') - ->bind(':user_id', $user->id, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - } - - /** - * Format a relative timestamp. It deals with timestamps today and yesterday in a special manner. Example returns: - * Yesterday, 13:12 - * Today, 08:33 - * January 1, 2015 - * - * @param string $dateTimeText The database time string to use, e.g. "2017-01-13 13:25:36" - * - * @return string The formatted, human-readable date - * @throws Exception - * - * @since 4.2.0 - */ - public function formatRelative(?string $dateTimeText): string - { - if (empty($dateTimeText)) - { - return Text::_('JNEVER'); - } - - // The timestamp is given in UTC. Make sure Joomla! parses it as such. - $utcTimeZone = new DateTimeZone('UTC'); - $jDate = new Date($dateTimeText, $utcTimeZone); - $unixStamp = $jDate->toUnix(); - - // I'm pretty sure we didn't have MFA in Joomla back in 1970 ;) - if ($unixStamp < 0) - { - return Text::_('JNEVER'); - } - - // I need to display the date in the user's local timezone. That's how you do it. - $user = Factory::getApplication()->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - $userTZ = $user->getParam('timezone', 'UTC'); - $tz = new DateTimeZone($userTZ); - $jDate->setTimezone($tz); - - // Default format string: way in the past, the time of the day is not important - $formatString = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_PAST'); - $containerString = Text::_('COM_USERS_MFA_LBL_PAST'); - - // If the timestamp is within the last 72 hours we may need a special format - if ($unixStamp > (time() - (72 * 3600))) - { - // Is this timestamp today? - $jNow = new Date; - $jNow->setTimezone($tz); - $checkNow = $jNow->format('Ymd', true); - $checkDate = $jDate->format('Ymd', true); - - if ($checkDate == $checkNow) - { - $formatString = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_TODAY'); - $containerString = Text::_('COM_USERS_MFA_LBL_TODAY'); - } - else - { - // Is this timestamp yesterday? - $jYesterday = clone $jNow; - $jYesterday->setTime(0, 0, 0); - $oneSecond = new DateInterval('PT1S'); - $jYesterday->sub($oneSecond); - $checkYesterday = $jYesterday->format('Ymd', true); - - if ($checkDate == $checkYesterday) - { - $formatString = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_YESTERDAY'); - $containerString = Text::_('COM_USERS_MFA_LBL_YESTERDAY'); - } - } - } - - return sprintf($containerString, $jDate->format($formatString, true)); - } - - /** - * Set the user's "don't show this again" flag. - * - * @param User $user The user to check - * @param bool $flag True to set the flag, false to unset it (it will be set to 0, actually) - * - * @return void - * - * @since 4.2.0 - */ - public function setFlag(User $user, bool $flag = true): void - { - $db = $this->getDatabase(); - $profileKey = 'mfa.dontshow'; - $query = $db->getQuery(true) - ->select($db->quoteName('profile_value')) - ->from($db->quoteName('#__user_profiles')) - ->where($db->quoteName('user_id') . ' = :user_id') - ->where($db->quoteName('profile_key') . ' = :profileKey') - ->bind(':user_id', $user->id, ParameterType::INTEGER) - ->bind(':profileKey', $profileKey, ParameterType::STRING); - - try - { - $result = $db->setQuery($query)->loadResult(); - } - catch (Exception $e) - { - return; - } - - $exists = !is_null($result); - - $object = (object) [ - 'user_id' => $user->id, - 'profile_key' => 'mfa.dontshow', - 'profile_value' => ($flag ? 1 : 0), - 'ordering' => 1, - ]; - - if (!$exists) - { - $db->insertObject('#__user_profiles', $object); - } - else - { - $db->updateObject('#__user_profiles', $object, ['user_id', 'profile_key']); - } - } + /** + * Returns a list of all available MFA methods and their currently active records for a given user. + * + * @param User|null $user The user object. Skip to use the current user. + * + * @return array + * @throws Exception + * + * @since 4.2.0 + */ + public function getMethods(?User $user = null): array + { + if (is_null($user)) { + $user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + if ($user->guest) { + return []; + } + + // Get an associative array of MFA Methods + $rawMethods = MfaHelper::getMfaMethods(); + $methods = []; + + foreach ($rawMethods as $method) { + $method['active'] = []; + $methods[$method['name']] = $method; + } + + // Put the user MFA records into the Methods array + $userMfaRecords = MfaHelper::getUserMfaRecords($user->id); + + if (!empty($userMfaRecords)) { + foreach ($userMfaRecords as $record) { + if (!isset($methods[$record->method])) { + continue; + } + + $methods[$record->method]->addActiveMethod($record); + } + } + + return $methods; + } + + /** + * Delete all Multi-factor Authentication Methods for the given user. + * + * @param User|null $user The user object to reset MFA for. Null to use the current user. + * + * @return void + * @throws Exception + * + * @since 4.2.0 + */ + public function deleteAll(?User $user = null): void + { + // Make sure we have a user object + if (is_null($user)) { + $user = Factory::getApplication()->getIdentity() ?: Factory::getUser(); + } + + // If the user object is a guest (who can't have MFA) we abort with an error + if ($user->guest) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__user_mfa')) + ->where($db->quoteName('user_id') . ' = :user_id') + ->bind(':user_id', $user->id, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + } + + /** + * Format a relative timestamp. It deals with timestamps today and yesterday in a special manner. Example returns: + * Yesterday, 13:12 + * Today, 08:33 + * January 1, 2015 + * + * @param string $dateTimeText The database time string to use, e.g. "2017-01-13 13:25:36" + * + * @return string The formatted, human-readable date + * @throws Exception + * + * @since 4.2.0 + */ + public function formatRelative(?string $dateTimeText): string + { + if (empty($dateTimeText)) { + return Text::_('JNEVER'); + } + + // The timestamp is given in UTC. Make sure Joomla! parses it as such. + $utcTimeZone = new DateTimeZone('UTC'); + $jDate = new Date($dateTimeText, $utcTimeZone); + $unixStamp = $jDate->toUnix(); + + // I'm pretty sure we didn't have MFA in Joomla back in 1970 ;) + if ($unixStamp < 0) { + return Text::_('JNEVER'); + } + + // I need to display the date in the user's local timezone. That's how you do it. + $user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + $userTZ = $user->getParam('timezone', 'UTC'); + $tz = new DateTimeZone($userTZ); + $jDate->setTimezone($tz); + + // Default format string: way in the past, the time of the day is not important + $formatString = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_PAST'); + $containerString = Text::_('COM_USERS_MFA_LBL_PAST'); + + // If the timestamp is within the last 72 hours we may need a special format + if ($unixStamp > (time() - (72 * 3600))) { + // Is this timestamp today? + $jNow = new Date(); + $jNow->setTimezone($tz); + $checkNow = $jNow->format('Ymd', true); + $checkDate = $jDate->format('Ymd', true); + + if ($checkDate == $checkNow) { + $formatString = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_TODAY'); + $containerString = Text::_('COM_USERS_MFA_LBL_TODAY'); + } else { + // Is this timestamp yesterday? + $jYesterday = clone $jNow; + $jYesterday->setTime(0, 0, 0); + $oneSecond = new DateInterval('PT1S'); + $jYesterday->sub($oneSecond); + $checkYesterday = $jYesterday->format('Ymd', true); + + if ($checkDate == $checkYesterday) { + $formatString = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_YESTERDAY'); + $containerString = Text::_('COM_USERS_MFA_LBL_YESTERDAY'); + } + } + } + + return sprintf($containerString, $jDate->format($formatString, true)); + } + + /** + * Set the user's "don't show this again" flag. + * + * @param User $user The user to check + * @param bool $flag True to set the flag, false to unset it (it will be set to 0, actually) + * + * @return void + * + * @since 4.2.0 + */ + public function setFlag(User $user, bool $flag = true): void + { + $db = $this->getDatabase(); + $profileKey = 'mfa.dontshow'; + $query = $db->getQuery(true) + ->select($db->quoteName('profile_value')) + ->from($db->quoteName('#__user_profiles')) + ->where($db->quoteName('user_id') . ' = :user_id') + ->where($db->quoteName('profile_key') . ' = :profileKey') + ->bind(':user_id', $user->id, ParameterType::INTEGER) + ->bind(':profileKey', $profileKey, ParameterType::STRING); + + try { + $result = $db->setQuery($query)->loadResult(); + } catch (Exception $e) { + return; + } + + $exists = !is_null($result); + + $object = (object) [ + 'user_id' => $user->id, + 'profile_key' => 'mfa.dontshow', + 'profile_value' => ($flag ? 1 : 0), + 'ordering' => 1, + ]; + + if (!$exists) { + $db->insertObject('#__user_profiles', $object); + } else { + $db->updateObject('#__user_profiles', $object, ['user_id', 'profile_key']); + } + } } diff --git a/administrator/components/com_users/src/Model/NoteModel.php b/administrator/components/com_users/src/Model/NoteModel.php index 974f7a62d16f5..f46e2f1273a04 100644 --- a/administrator/components/com_users/src/Model/NoteModel.php +++ b/administrator/components/com_users/src/Model/NoteModel.php @@ -1,4 +1,5 @@ loadForm('com_users.note', 'note', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 2.5 - * @throws \Exception - */ - public function getItem($pk = null) - { - $result = parent::getItem($pk); - - // Get the dispatcher and load the content plugins. - PluginHelper::importPlugin('content'); - - // Load the user plugins for backward compatibility (v3.3.3 and earlier). - PluginHelper::importPlugin('user'); - - // Trigger the data preparation event. - Factory::getApplication()->triggerEvent('onContentPrepareData', array('com_users.note', $result)); - - return $result; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - * @throws \Exception - */ - protected function loadFormData() - { - // Get the application - $app = Factory::getApplication(); - - // Check the session for previously entered form data. - $data = $app->getUserState('com_users.edit.note.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - - // Prime some default values. - if ($this->getState('note.id') == 0) - { - $data->set('catid', $app->input->get('catid', $app->getUserState('com_users.notes.filter.category_id'), 'int')); - } - - $userId = $app->input->get('u_id', 0, 'int'); - - if ($userId != 0) - { - $data->user_id = $userId; - } - } - - $this->preprocessData('com_users.note', $data); - - return $data; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 2.5 - * @throws \Exception - */ - protected function populateState() - { - parent::populateState(); - - $userId = Factory::getApplication()->input->get('u_id', 0, 'int'); - $this->setState('note.user_id', $userId); - } + use VersionableModelTrait; + + /** + * The type alias for this content type. + * + * @var string + * @since 3.2 + */ + public $typeAlias = 'com_users.note'; + + /** + * Method to get the record form. + * + * @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 \Joomla\CMS\Form\Form|bool A Form object on success, false on failure + * + * @since 2.5 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.note', 'note', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 2.5 + * @throws \Exception + */ + public function getItem($pk = null) + { + $result = parent::getItem($pk); + + // Get the dispatcher and load the content plugins. + PluginHelper::importPlugin('content'); + + // Load the user plugins for backward compatibility (v3.3.3 and earlier). + PluginHelper::importPlugin('user'); + + // Trigger the data preparation event. + Factory::getApplication()->triggerEvent('onContentPrepareData', array('com_users.note', $result)); + + return $result; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + * @throws \Exception + */ + protected function loadFormData() + { + // Get the application + $app = Factory::getApplication(); + + // Check the session for previously entered form data. + $data = $app->getUserState('com_users.edit.note.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + + // Prime some default values. + if ($this->getState('note.id') == 0) { + $data->set('catid', $app->input->get('catid', $app->getUserState('com_users.notes.filter.category_id'), 'int')); + } + + $userId = $app->input->get('u_id', 0, 'int'); + + if ($userId != 0) { + $data->user_id = $userId; + } + } + + $this->preprocessData('com_users.note', $data); + + return $data; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 2.5 + * @throws \Exception + */ + protected function populateState() + { + parent::populateState(); + + $userId = Factory::getApplication()->input->get('u_id', 0, 'int'); + $this->setState('note.user_id', $userId); + } } diff --git a/administrator/components/com_users/src/Model/NotesModel.php b/administrator/components/com_users/src/Model/NotesModel.php index c6778588cfd21..c2d6f736e7242 100644 --- a/administrator/components/com_users/src/Model/NotesModel.php +++ b/administrator/components/com_users/src/Model/NotesModel.php @@ -1,4 +1,5 @@ getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState('list.select', - 'a.id, a.subject, a.checked_out, a.checked_out_time,' . - 'a.catid, a.created_time, a.review_time,' . - 'a.state, a.publish_up, a.publish_down' - ) - ); - $query->from('#__user_notes AS a'); - - // Join over the category - $query->select('c.title AS category_title, c.params AS category_params') - ->join('LEFT', '#__categories AS c ON c.id = a.catid'); - - // Join over the users for the note user. - $query->select('u.name AS user_name') - ->join('LEFT', '#__users AS u ON u.id = a.user_id'); - - // Join over the users for the checked out user. - $query->select('uc.name AS editor') - ->join('LEFT', '#__users AS uc ON uc.id = a.checked_out'); - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $search3 = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $search3, ParameterType::INTEGER); - } - elseif (stripos($search, 'uid:') === 0) - { - $search4 = (int) substr($search, 4); - $query->where($db->quoteName('a.user_id') . ' = :id'); - $query->bind(':id', $search4, ParameterType::INTEGER); - } - else - { - $search = '%' . trim($search) . '%'; - $query->where( - '(' . $db->quoteName('a.subject') . ' LIKE :subject' - . ' OR ' . $db->quoteName('u.name') . ' LIKE :name' - . ' OR ' . $db->quoteName('u.username') . ' LIKE :username)' - ); - $query->bind(':subject', $search); - $query->bind(':name', $search); - $query->bind(':username', $search); - } - } - - // Filter by published state - $published = $this->getState('filter.published'); - - if (is_numeric($published)) - { - $query->where($db->quoteName('a.state') . ' = :state') - ->bind(':state', $published, ParameterType::INTEGER); - } - elseif ($published !== '*') - { - $query->whereIn($db->quoteName('a.state'), [0, 1]); - } - - // Filter by a single category. - $categoryId = (int) $this->getState('filter.category_id'); - - if ($categoryId) - { - $query->where($db->quoteName('a.catid') . ' = :catid') - ->bind(':catid', $categoryId, ParameterType::INTEGER); - } - - // Filter by a single user. - $userId = (int) $this->getState('filter.user_id'); - - if ($userId) - { - // Add the body and where filter. - $query->select('a.body') - ->where($db->quoteName('a.user_id') . ' = :user_id') - ->bind(':user_id', $userId, ParameterType::INTEGER); - } - - // Filter on the level. - if ($level = $this->getState('filter.level')) - { - $level = (int) $level; - $query->where($db->quoteName('c.level') . ' <= :level') - ->bind(':level', $level, ParameterType::INTEGER); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.review_time')) . ' ' . $db->escape($this->getState('list.direction', 'DESC'))); - - return $query; - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 2.5 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.category_id'); - $id .= ':' . $this->getState('filter.user_id'); - $id .= ':' . $this->getState('filter.level'); - - return parent::getStoreId($id); - } - - /** - * Gets a user object if the user filter is set. - * - * @return User The User object - * - * @since 2.5 - */ - public function getUser() - { - $user = new User; - - // Filter by search in title - $search = (int) $this->getState('filter.user_id'); - - if ($search != 0) - { - $user->load((int) $search); - } - - return $user; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function populateState($ordering = 'a.review_time', $direction = 'desc') - { - // Adjust the context to support modal layouts. - if ($layout = Factory::getApplication()->input->get('layout')) - { - $this->context .= '.' . $layout; - } - - parent::populateState($ordering, $direction); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + // Set the list ordering fields. + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'user_id', 'a.user_id', + 'u.name', + 'subject', 'a.subject', + 'catid', 'a.catid', 'category_id', + 'state', 'a.state', 'published', + 'c.title', + 'review_time', 'a.review_time', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'level', 'c.level', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery A DatabaseQuery object to retrieve the data set. + * + * @since 2.5 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.id, a.subject, a.checked_out, a.checked_out_time,' . + 'a.catid, a.created_time, a.review_time,' . + 'a.state, a.publish_up, a.publish_down' + ) + ); + $query->from('#__user_notes AS a'); + + // Join over the category + $query->select('c.title AS category_title, c.params AS category_params') + ->join('LEFT', '#__categories AS c ON c.id = a.catid'); + + // Join over the users for the note user. + $query->select('u.name AS user_name') + ->join('LEFT', '#__users AS u ON u.id = a.user_id'); + + // Join over the users for the checked out user. + $query->select('uc.name AS editor') + ->join('LEFT', '#__users AS uc ON uc.id = a.checked_out'); + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $search3 = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $search3, ParameterType::INTEGER); + } elseif (stripos($search, 'uid:') === 0) { + $search4 = (int) substr($search, 4); + $query->where($db->quoteName('a.user_id') . ' = :id'); + $query->bind(':id', $search4, ParameterType::INTEGER); + } else { + $search = '%' . trim($search) . '%'; + $query->where( + '(' . $db->quoteName('a.subject') . ' LIKE :subject' + . ' OR ' . $db->quoteName('u.name') . ' LIKE :name' + . ' OR ' . $db->quoteName('u.username') . ' LIKE :username)' + ); + $query->bind(':subject', $search); + $query->bind(':name', $search); + $query->bind(':username', $search); + } + } + + // Filter by published state + $published = $this->getState('filter.published'); + + if (is_numeric($published)) { + $query->where($db->quoteName('a.state') . ' = :state') + ->bind(':state', $published, ParameterType::INTEGER); + } elseif ($published !== '*') { + $query->whereIn($db->quoteName('a.state'), [0, 1]); + } + + // Filter by a single category. + $categoryId = (int) $this->getState('filter.category_id'); + + if ($categoryId) { + $query->where($db->quoteName('a.catid') . ' = :catid') + ->bind(':catid', $categoryId, ParameterType::INTEGER); + } + + // Filter by a single user. + $userId = (int) $this->getState('filter.user_id'); + + if ($userId) { + // Add the body and where filter. + $query->select('a.body') + ->where($db->quoteName('a.user_id') . ' = :user_id') + ->bind(':user_id', $userId, ParameterType::INTEGER); + } + + // Filter on the level. + if ($level = $this->getState('filter.level')) { + $level = (int) $level; + $query->where($db->quoteName('c.level') . ' <= :level') + ->bind(':level', $level, ParameterType::INTEGER); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.review_time')) . ' ' . $db->escape($this->getState('list.direction', 'DESC'))); + + return $query; + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 2.5 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.category_id'); + $id .= ':' . $this->getState('filter.user_id'); + $id .= ':' . $this->getState('filter.level'); + + return parent::getStoreId($id); + } + + /** + * Gets a user object if the user filter is set. + * + * @return User The User object + * + * @since 2.5 + */ + public function getUser() + { + $user = new User(); + + // Filter by search in title + $search = (int) $this->getState('filter.user_id'); + + if ($search != 0) { + $user->load((int) $search); + } + + return $user; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function populateState($ordering = 'a.review_time', $direction = 'desc') + { + // Adjust the context to support modal layouts. + if ($layout = Factory::getApplication()->input->get('layout')) { + $this->context .= '.' . $layout; + } + + parent::populateState($ordering, $direction); + } } diff --git a/administrator/components/com_users/src/Model/UserModel.php b/administrator/components/com_users/src/Model/UserModel.php index c2240b21609d0..46eb36baea241 100644 --- a/administrator/components/com_users/src/Model/UserModel.php +++ b/administrator/components/com_users/src/Model/UserModel.php @@ -1,4 +1,5 @@ 'onUserAfterDelete', - 'event_after_save' => 'onUserAfterSave', - 'event_before_delete' => 'onUserBeforeDelete', - 'event_before_save' => 'onUserBeforeSave', - 'events_map' => array('save' => 'user', 'delete' => 'user', 'validate' => 'user') - ), $config - ); - - parent::__construct($config, $factory); - } - - /** - * Returns a reference to the a Table object, always creating it. - * - * @param string $type The table type to instantiate - * @param string $prefix A prefix for the table class name. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return Table A database object - * - * @since 1.6 - */ - public function getTable($type = 'User', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) - { - $table = Table::getInstance($type, $prefix, $config); - - return $table; - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return mixed Object on success, false on failure. - * - * @since 1.6 - */ - public function getItem($pk = null) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('user.id'); - - if ($this->_item === null) - { - $this->_item = array(); - } - - if (!isset($this->_item[$pk])) - { - $this->_item[$pk] = parent::getItem($pk); - } - - return $this->_item[$pk]; - } - - /** - * Method to get the record form. - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_users.user', 'user', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - $user = Factory::getUser(); - - // If the user needs to change their password, mark the password fields as required - if ($user->requireReset) - { - $form->setFieldAttribute('password', 'required', 'true'); - $form->setFieldAttribute('password2', 'required', 'true'); - } - - // When multilanguage is set, a user's default site language should also be a Content Language - if (Multilanguage::isEnabled()) - { - $form->setFieldAttribute('language', 'type', 'frontend_language', 'params'); - } - - $userId = (int) $form->getValue('id'); - - // The user should not be able to set the requireReset value on their own account - if ($userId === (int) $user->id) - { - $form->removeField('requireReset'); - } - - /** - * If users without core.manage permission editing their own account, remove some fields which they should - * not be allowed to change and prevent them to change user name if configured - */ - if (!$user->authorise('core.manage', 'com_users') && (int) $user->id === $userId) - { - if (!ComponentHelper::getParams('com_users')->get('change_login_name')) - { - $form->setFieldAttribute('username', 'required', 'false'); - $form->setFieldAttribute('username', 'readonly', 'true'); - $form->setFieldAttribute('username', 'description', 'COM_USERS_USER_FIELD_NOCHANGE_USERNAME_DESC'); - } - - $form->removeField('lastResetTime'); - $form->removeField('resetCount'); - $form->removeField('sendEmail'); - $form->removeField('block'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - * @throws \Exception - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState('com_users.edit.user.data', array()); - - if (empty($data)) - { - $data = $this->getItem(); - } - - $this->preprocessData('com_users.profile', $data, 'user'); - - return $data; - } - - /** - * Override Joomla\CMS\MVC\Model\AdminModel::preprocessForm to ensure the correct plugin group is loaded. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'user') - { - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 1.6 - * @throws \Exception - */ - public function save($data) - { - $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('user.id'); - $user = User::getInstance($pk); - - $my = Factory::getUser(); - $iAmSuperAdmin = $my->authorise('core.admin'); - - // User cannot modify own user groups - if ((int) $user->id == (int) $my->id && !$iAmSuperAdmin && isset($data['groups'])) - { - // Form was probably tampered with - Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_USERS_ERROR_CANNOT_EDIT_OWN_GROUP'), 'warning'); - - $data['groups'] = null; - } - - if ($data['block'] && $pk == $my->id && !$my->block) - { - $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_BLOCK_SELF')); - - return false; - } - - // Make sure user groups is selected when add/edit an account - if (empty($data['groups']) && ((int) $user->id != (int) $my->id || $iAmSuperAdmin)) - { - $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_SAVE_ACCOUNT_WITHOUT_GROUPS')); - - return false; - } - - // Make sure that we are not removing ourself from Super Admin group - if ($iAmSuperAdmin && $my->get('id') == $pk) - { - // Check that at least one of our new groups is Super Admin - $stillSuperAdmin = false; - $myNewGroups = $data['groups']; - - foreach ($myNewGroups as $group) - { - $stillSuperAdmin = $stillSuperAdmin ?: Access::checkGroup($group, 'core.admin'); - } - - if (!$stillSuperAdmin) - { - $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_DEMOTE_SELF')); - - return false; - } - } - - // Bind the data. - if (!$user->bind($data)) - { - $this->setError($user->getError()); - - return false; - } - - // Store the data. - if (!$user->save()) - { - $this->setError($user->getError()); - - return false; - } - - // Destroy all active sessions for the user after changing the password or blocking him - if ($data['password2'] || $data['block']) - { - UserHelper::destroyUserSessions($user->id, true); - } - - $this->setState('user.id', $user->id); - - return true; - } - - /** - * Method to delete rows. - * - * @param array &$pks An array of item ids. - * - * @return boolean Returns true on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function delete(&$pks) - { - $user = Factory::getUser(); - $table = $this->getTable(); - $pks = (array) $pks; - - // Check if I am a Super Admin - $iAmSuperAdmin = $user->authorise('core.admin'); - - PluginHelper::importPlugin($this->events_map['delete']); - - if (in_array($user->id, $pks)) - { - $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_DELETE_SELF')); - - return false; - } - - // Iterate the items to delete each one. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - // Access checks. - $allow = $user->authorise('core.delete', 'com_users'); - - // Don't allow non-super-admin to delete a super admin - $allow = (!$iAmSuperAdmin && Access::check($pk, 'core.admin')) ? false : $allow; - - if ($allow) - { - // Get users data for the users to delete. - $user_to_delete = Factory::getUser($pk); - - // Fire the before delete event. - Factory::getApplication()->triggerEvent($this->event_before_delete, array($table->getProperties())); - - if (!$table->delete($pk)) - { - $this->setError($table->getError()); - - return false; - } - else - { - // Trigger the after delete event. - Factory::getApplication()->triggerEvent($this->event_after_delete, array($user_to_delete->getProperties(), true, $this->getError())); - } - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); - } - } - else - { - $this->setError($table->getError()); - - return false; - } - } - - return true; - } - - /** - * Method to block user records. - * - * @param array &$pks The ids of the items to publish. - * @param integer $value The value of the published state - * - * @return boolean True on success. - * - * @since 1.6 - * @throws \Exception - */ - public function block(&$pks, $value = 1) - { - $app = Factory::getApplication(); - $user = Factory::getUser(); - - // Check if I am a Super Admin - $iAmSuperAdmin = $user->authorise('core.admin'); - $table = $this->getTable(); - $pks = (array) $pks; - - PluginHelper::importPlugin($this->events_map['save']); - - // Prepare the logout options. - $options = array( - 'clientid' => $app->get('shared_session', '0') ? null : 0, - ); - - // Access checks. - foreach ($pks as $i => $pk) - { - if ($value == 1 && $pk == $user->get('id')) - { - // Cannot block yourself. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_USERS_ERROR_CANNOT_BLOCK_SELF'), 'error'); - } - elseif ($table->load($pk)) - { - $old = $table->getProperties(); - $allow = $user->authorise('core.edit.state', 'com_users'); - - // Don't allow non-super-admin to delete a super admin - $allow = (!$iAmSuperAdmin && Access::check($pk, 'core.admin')) ? false : $allow; - - if ($allow) - { - // Skip changing of same state - if ($table->block == $value) - { - unset($pks[$i]); - continue; - } - - $table->block = (int) $value; - - // If unblocking, also change password reset count to zero to unblock reset - if ($table->block === 0) - { - $table->resetCount = 0; - } - - // Allow an exception to be thrown. - try - { - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($old, false, $table->getProperties())); - - if (in_array(false, $result, true)) - { - // Plugin will have to raise its own error or throw an exception. - return false; - } - - // Store the table. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - if ($table->block) - { - UserHelper::destroyUserSessions($table->id); - } - - // Trigger the after save event - Factory::getApplication()->triggerEvent($this->event_after_save, [$table->getProperties(), false, true, null]); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Log the user out. - if ($value) - { - $app->logout($table->id, $options); - } - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); - } - } - } - - return true; - } - - /** - * Method to activate user records. - * - * @param array &$pks The ids of the items to activate. - * - * @return boolean True on success. - * - * @since 1.6 - * @throws \Exception - */ - public function activate(&$pks) - { - $user = Factory::getUser(); - - // Check if I am a Super Admin - $iAmSuperAdmin = $user->authorise('core.admin'); - $table = $this->getTable(); - $pks = (array) $pks; - - PluginHelper::importPlugin($this->events_map['save']); - - // Access checks. - foreach ($pks as $i => $pk) - { - if ($table->load($pk)) - { - $old = $table->getProperties(); - $allow = $user->authorise('core.edit.state', 'com_users'); - - // Don't allow non-super-admin to delete a super admin - $allow = (!$iAmSuperAdmin && Access::check($pk, 'core.admin')) ? false : $allow; - - if (empty($table->activation)) - { - // Ignore activated accounts. - unset($pks[$i]); - } - elseif ($allow) - { - $table->block = 0; - $table->activation = ''; - - // Allow an exception to be thrown. - try - { - if (!$table->check()) - { - $this->setError($table->getError()); - - return false; - } - - // Trigger the before save event. - $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($old, false, $table->getProperties())); - - if (in_array(false, $result, true)) - { - // Plugin will have to raise it's own error or throw an exception. - return false; - } - - // Store the table. - if (!$table->store()) - { - $this->setError($table->getError()); - - return false; - } - - // Fire the after save event - Factory::getApplication()->triggerEvent($this->event_after_save, [$table->getProperties(), false, true, null]); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - else - { - // Prune items that you can't change. - unset($pks[$i]); - Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); - } - } - } - - return true; - } - - /** - * Method to perform batch operations on an item or a set of items. - * - * @param array $commands An array of commands to perform. - * @param array $pks An array of item ids. - * @param array $contexts An array of item contexts. - * - * @return boolean Returns true on success, false on failure. - * - * @since 2.5 - */ - public function batch($commands, $pks, $contexts) - { - // Sanitize user ids. - $pks = array_unique($pks); - $pks = ArrayHelper::toInteger($pks); - - // Remove any values of zero. - if (array_search(0, $pks, true)) - { - unset($pks[array_search(0, $pks, true)]); - } - - if (empty($pks)) - { - $this->setError(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED')); - - return false; - } - - $done = false; - - if (!empty($commands['group_id'])) - { - $cmd = ArrayHelper::getValue($commands, 'group_action', 'add'); - - if (!$this->batchUser((int) $commands['group_id'], $pks, $cmd)) - { - return false; - } - - $done = true; - } - - if (!empty($commands['reset_id'])) - { - if (!$this->batchReset($pks, $commands['reset_id'])) - { - return false; - } - - $done = true; - } - - if (!$done) - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_INSUFFICIENT_BATCH_INFORMATION')); - - return false; - } - - // Clear the cache - $this->cleanCache(); - - return true; - } - - /** - * Batch flag users as being required to reset their passwords - * - * @param array $userIds An array of user IDs on which to operate - * @param string $action The action to perform - * - * @return boolean True on success, false on failure - * - * @since 3.2 - */ - public function batchReset($userIds, $action) - { - $userIds = ArrayHelper::toInteger($userIds); - - // Check if I am a Super Admin - $iAmSuperAdmin = Factory::getUser()->authorise('core.admin'); - - // Non-super super user cannot work with super-admin user. - if (!$iAmSuperAdmin && UserHelper::checkSuperUserInUsers($userIds)) - { - $this->setError(Text::_('COM_USERS_ERROR_CANNOT_BATCH_SUPERUSER')); - - return false; - } - - // Set the action to perform - if ($action === 'yes') - { - $value = 1; - } - else - { - $value = 0; - } - - // Prune out the current user if they are in the supplied user ID array - $userIds = array_diff($userIds, array(Factory::getUser()->id)); - - if (empty($userIds)) - { - $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_REQUIRERESET_SELF')); - - return false; - } - - // Get the DB object - $db = $this->getDatabase(); - - $userIds = ArrayHelper::toInteger($userIds); - - $query = $db->getQuery(true); - - // Update the reset flag - $query->update($db->quoteName('#__users')) - ->set($db->quoteName('requireReset') . ' = :requireReset') - ->whereIn($db->quoteName('id'), $userIds) - ->bind(':requireReset', $value, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - return true; - } - - /** - * Perform batch operations - * - * @param integer $groupId The group ID which assignments are being edited - * @param array $userIds An array of user IDs on which to operate - * @param string $action The action to perform - * - * @return boolean True on success, false on failure - * - * @since 1.6 - */ - public function batchUser($groupId, $userIds, $action) - { - $userIds = ArrayHelper::toInteger($userIds); - - // Check if I am a Super Admin - $iAmSuperAdmin = Factory::getUser()->authorise('core.admin'); - - // Non-super super user cannot work with super-admin user. - if (!$iAmSuperAdmin && UserHelper::checkSuperUserInUsers($userIds)) - { - $this->setError(Text::_('COM_USERS_ERROR_CANNOT_BATCH_SUPERUSER')); - - return false; - } - - // Non-super admin cannot work with super-admin group. - if ((!$iAmSuperAdmin && Access::checkGroup($groupId, 'core.admin')) || $groupId < 1) - { - $this->setError(Text::_('COM_USERS_ERROR_INVALID_GROUP')); - - return false; - } - - // Get the DB object - $db = $this->getDatabase(); - - switch ($action) - { - // Sets users to a selected group - case 'set': - $doDelete = 'all'; - $doAssign = true; - break; - - // Remove users from a selected group - case 'del': - $doDelete = 'group'; - break; - - // Add users to a selected group - case 'add': - default: - $doAssign = true; - break; - } - - // Remove the users from the group if requested. - if (isset($doDelete)) - { - $query = $db->getQuery(true); - - // Remove users from the group - $query->delete($db->quoteName('#__user_usergroup_map')) - ->whereIn($db->quoteName('user_id'), $userIds); - - // Only remove users from selected group - if ($doDelete == 'group') - { - $query->where($db->quoteName('group_id') . ' = :group_id') - ->bind(':group_id', $groupId, ParameterType::INTEGER); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - // Assign the users to the group if requested. - if (isset($doAssign)) - { - $query = $db->getQuery(true); - - // First, we need to check if the user is already assigned to a group - $query->select($db->quoteName('user_id')) - ->from($db->quoteName('#__user_usergroup_map')) - ->where($db->quoteName('group_id') . ' = :group_id') - ->bind(':group_id', $groupId, ParameterType::INTEGER); - $db->setQuery($query); - $users = $db->loadColumn(); - - // Build the values clause for the assignment query. - $query->clear(); - $groups = false; - - foreach ($userIds as $id) - { - if (!in_array($id, $users)) - { - $query->values($id . ',' . $groupId); - $groups = true; - } - } - - // If we have no users to process, throw an error to notify the user - if (!$groups) - { - $this->setError(Text::_('COM_USERS_ERROR_NO_ADDITIONS')); - - return false; - } - - $query->insert($db->quoteName('#__user_usergroup_map')) - ->columns(array($db->quoteName('user_id'), $db->quoteName('group_id'))); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - - return true; - } - - /** - * Gets the available groups. - * - * @return array An array of groups - * - * @since 1.6 - */ - public function getGroups() - { - $user = Factory::getUser(); - - if ($user->authorise('core.edit', 'com_users') && $user->authorise('core.manage', 'com_users')) - { - $model = $this->bootComponent('com_users') - ->getMVCFactory()->createModel('Groups', 'Administrator', ['ignore_request' => true]); - - return $model->getItems(); - } - else - { - return null; - } - } - - /** - * Gets the groups this object is assigned to - * - * @param integer $userId The user ID to retrieve the groups for - * - * @return array An array of assigned groups - * - * @since 1.6 - */ - public function getAssignedGroups($userId = null) - { - $userId = (!empty($userId)) ? $userId : (int) $this->getState('user.id'); - - if (empty($userId)) - { - $result = array(); - $form = $this->getForm(); - - if ($form) - { - $groupsIDs = $form->getValue('groups'); - } - - if (!empty($groupsIDs)) - { - $result = $groupsIDs; - } - else - { - $params = ComponentHelper::getParams('com_users'); - - if ($groupId = $params->get('new_usertype', $params->get('guest_usergroup', 1))) - { - $result[] = $groupId; - } - } - } - else - { - $result = UserHelper::getUserGroups($userId); - } - - return $result; - } - - /** - * No longer used - * - * @param integer $userId Ignored - * - * @return \stdClass - * - * @since 3.2 - * @deprecated 4.2.0 Will be removed in 5.0 - */ - public function getOtpConfig($userId = null) - { - @trigger_error( - sprintf( - '%s() is deprecated. Use \Joomla\Component\Users\Administrator\Helper\Mfa::getUserMfaRecords() instead.', - __METHOD__ - ), - E_USER_DEPRECATED - ); - - // Return the configuration object - return (object) array( - 'method' => 'none', - 'config' => array(), - 'otep' => array() - ); - } - - /** - * No longer used - * - * @param integer $userId Ignored - * @param \stdClass $otpConfig Ignored - * - * @return boolean True on success - * - * @since 3.2 - * @deprecated 4.2.0 Will be removed in 5.0 - */ - public function setOtpConfig($userId, $otpConfig) - { - @trigger_error( - sprintf( - '%s() is deprecated. Multi-factor Authentication actions are handled by plugins in the multifactorauth folder.', - __METHOD__ - ), - E_USER_DEPRECATED - ); - - return true; - } - - /** - * No longer used - * - * @return string - * - * @since 3.2 - * @deprecated 4.2.0 Will be removed in 5.0 - */ - public function getOtpConfigEncryptionKey() - { - @trigger_error( - sprintf( - '%s() is deprecated. Use \Joomla\CMS\Factory::getApplication()->get(\'secret\') instead', - __METHOD__ - ), - E_USER_DEPRECATED - ); - - return Factory::getApplication()->get('secret'); - } - - /** - * No longer used - * - * @param integer $userId Ignored - * - * @return array Empty array - * - * @since 3.2 - * @throws \Exception - * - * @deprecated 4.2.0 Will be removed in 5.0. - */ - public function getTwofactorform($userId = null) - { - @trigger_error( - sprintf( - '%s() is deprecated. Use \Joomla\Component\Users\Administrator\Helper\Mfa::getConfigurationInterface()', - __METHOD__ - ), - E_USER_DEPRECATED - ); - - return []; - } - - /** - * No longer used - * - * @param integer $userId Ignored - * @param integer $count Ignored - * - * @return array Empty array - * - * @since 3.2 - * @deprecated 4.2.0 Wil be removed in 5.0. - */ - public function generateOteps($userId, $count = 10) - { - @trigger_error( - sprintf( - '%s() is deprecated. See \Joomla\Component\Users\Administrator\Model\BackupcodesModel::saveBackupCodes()', - __METHOD__ - ), - E_USER_DEPRECATED - ); - - return []; - } - - /** - * No longer used. Always returns true. - * - * @param integer $userId Ignored - * @param string $secretKey Ignored - * @param array $options Ignored - * - * @return boolean Always true - * - * @since 3.2 - * @throws \Exception - * - * @deprecated 4.2.0 Will be removed in 5.0. MFA validation is done in the captive login. - */ - public function isValidSecretKey($userId, $secretKey, $options = array()) - { - @trigger_error( - sprintf( - '%s() is deprecated. Multi-factor Authentication actions are handled by plugins in the multifactorauth folder.', - __METHOD__ - ), - E_USER_DEPRECATED - ); - - return true; - } - - /** - * No longer used - * - * @param integer $userId Ignored - * @param string $otep Ignored - * @param object $otpConfig Ignored - * - * @return boolean Always true - * - * @since 3.2 - * @deprecated 4.2.0 Will be removed in 5.0 - */ - public function isValidOtep($userId, $otep, $otpConfig = null) - { - @trigger_error( - sprintf( - '%s() is deprecated. Multi-factor Authentication actions are handled by plugins in the multifactorauth folder.', - __METHOD__ - ), - E_USER_DEPRECATED - ); - - return true; - } + /** + * An item. + * + * @var array + */ + protected $_item = null; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + $config = array_merge( + array( + 'event_after_delete' => 'onUserAfterDelete', + 'event_after_save' => 'onUserAfterSave', + 'event_before_delete' => 'onUserBeforeDelete', + 'event_before_save' => 'onUserBeforeSave', + 'events_map' => array('save' => 'user', 'delete' => 'user', 'validate' => 'user') + ), + $config + ); + + parent::__construct($config, $factory); + } + + /** + * Returns a reference to the a Table object, always creating it. + * + * @param string $type The table type to instantiate + * @param string $prefix A prefix for the table class name. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return Table A database object + * + * @since 1.6 + */ + public function getTable($type = 'User', $prefix = 'Joomla\\CMS\\Table\\', $config = array()) + { + $table = Table::getInstance($type, $prefix, $config); + + return $table; + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return mixed Object on success, false on failure. + * + * @since 1.6 + */ + public function getItem($pk = null) + { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('user.id'); + + if ($this->_item === null) { + $this->_item = array(); + } + + if (!isset($this->_item[$pk])) { + $this->_item[$pk] = parent::getItem($pk); + } + + return $this->_item[$pk]; + } + + /** + * Method to get the record form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.user', 'user', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + $user = Factory::getUser(); + + // If the user needs to change their password, mark the password fields as required + if ($user->requireReset) { + $form->setFieldAttribute('password', 'required', 'true'); + $form->setFieldAttribute('password2', 'required', 'true'); + } + + // When multilanguage is set, a user's default site language should also be a Content Language + if (Multilanguage::isEnabled()) { + $form->setFieldAttribute('language', 'type', 'frontend_language', 'params'); + } + + $userId = (int) $form->getValue('id'); + + // The user should not be able to set the requireReset value on their own account + if ($userId === (int) $user->id) { + $form->removeField('requireReset'); + } + + /** + * If users without core.manage permission editing their own account, remove some fields which they should + * not be allowed to change and prevent them to change user name if configured + */ + if (!$user->authorise('core.manage', 'com_users') && (int) $user->id === $userId) { + if (!ComponentHelper::getParams('com_users')->get('change_login_name')) { + $form->setFieldAttribute('username', 'required', 'false'); + $form->setFieldAttribute('username', 'readonly', 'true'); + $form->setFieldAttribute('username', 'description', 'COM_USERS_USER_FIELD_NOCHANGE_USERNAME_DESC'); + } + + $form->removeField('lastResetTime'); + $form->removeField('resetCount'); + $form->removeField('sendEmail'); + $form->removeField('block'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + * @throws \Exception + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState('com_users.edit.user.data', array()); + + if (empty($data)) { + $data = $this->getItem(); + } + + $this->preprocessData('com_users.profile', $data, 'user'); + + return $data; + } + + /** + * Override Joomla\CMS\MVC\Model\AdminModel::preprocessForm to ensure the correct plugin group is loaded. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'user') + { + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 1.6 + * @throws \Exception + */ + public function save($data) + { + $pk = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('user.id'); + $user = User::getInstance($pk); + + $my = Factory::getUser(); + $iAmSuperAdmin = $my->authorise('core.admin'); + + // User cannot modify own user groups + if ((int) $user->id == (int) $my->id && !$iAmSuperAdmin && isset($data['groups'])) { + // Form was probably tampered with + Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_USERS_ERROR_CANNOT_EDIT_OWN_GROUP'), 'warning'); + + $data['groups'] = null; + } + + if ($data['block'] && $pk == $my->id && !$my->block) { + $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_BLOCK_SELF')); + + return false; + } + + // Make sure user groups is selected when add/edit an account + if (empty($data['groups']) && ((int) $user->id != (int) $my->id || $iAmSuperAdmin)) { + $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_SAVE_ACCOUNT_WITHOUT_GROUPS')); + + return false; + } + + // Make sure that we are not removing ourself from Super Admin group + if ($iAmSuperAdmin && $my->get('id') == $pk) { + // Check that at least one of our new groups is Super Admin + $stillSuperAdmin = false; + $myNewGroups = $data['groups']; + + foreach ($myNewGroups as $group) { + $stillSuperAdmin = $stillSuperAdmin ?: Access::checkGroup($group, 'core.admin'); + } + + if (!$stillSuperAdmin) { + $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_DEMOTE_SELF')); + + return false; + } + } + + // Bind the data. + if (!$user->bind($data)) { + $this->setError($user->getError()); + + return false; + } + + // Store the data. + if (!$user->save()) { + $this->setError($user->getError()); + + return false; + } + + // Destroy all active sessions for the user after changing the password or blocking him + if ($data['password2'] || $data['block']) { + UserHelper::destroyUserSessions($user->id, true); + } + + $this->setState('user.id', $user->id); + + return true; + } + + /** + * Method to delete rows. + * + * @param array &$pks An array of item ids. + * + * @return boolean Returns true on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function delete(&$pks) + { + $user = Factory::getUser(); + $table = $this->getTable(); + $pks = (array) $pks; + + // Check if I am a Super Admin + $iAmSuperAdmin = $user->authorise('core.admin'); + + PluginHelper::importPlugin($this->events_map['delete']); + + if (in_array($user->id, $pks)) { + $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_DELETE_SELF')); + + return false; + } + + // Iterate the items to delete each one. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + // Access checks. + $allow = $user->authorise('core.delete', 'com_users'); + + // Don't allow non-super-admin to delete a super admin + $allow = (!$iAmSuperAdmin && Access::check($pk, 'core.admin')) ? false : $allow; + + if ($allow) { + // Get users data for the users to delete. + $user_to_delete = Factory::getUser($pk); + + // Fire the before delete event. + Factory::getApplication()->triggerEvent($this->event_before_delete, array($table->getProperties())); + + if (!$table->delete($pk)) { + $this->setError($table->getError()); + + return false; + } else { + // Trigger the after delete event. + Factory::getApplication()->triggerEvent($this->event_after_delete, array($user_to_delete->getProperties(), true, $this->getError())); + } + } else { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('JERROR_CORE_DELETE_NOT_PERMITTED'), 'error'); + } + } else { + $this->setError($table->getError()); + + return false; + } + } + + return true; + } + + /** + * Method to block user records. + * + * @param array &$pks The ids of the items to publish. + * @param integer $value The value of the published state + * + * @return boolean True on success. + * + * @since 1.6 + * @throws \Exception + */ + public function block(&$pks, $value = 1) + { + $app = Factory::getApplication(); + $user = Factory::getUser(); + + // Check if I am a Super Admin + $iAmSuperAdmin = $user->authorise('core.admin'); + $table = $this->getTable(); + $pks = (array) $pks; + + PluginHelper::importPlugin($this->events_map['save']); + + // Prepare the logout options. + $options = array( + 'clientid' => $app->get('shared_session', '0') ? null : 0, + ); + + // Access checks. + foreach ($pks as $i => $pk) { + if ($value == 1 && $pk == $user->get('id')) { + // Cannot block yourself. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('COM_USERS_USERS_ERROR_CANNOT_BLOCK_SELF'), 'error'); + } elseif ($table->load($pk)) { + $old = $table->getProperties(); + $allow = $user->authorise('core.edit.state', 'com_users'); + + // Don't allow non-super-admin to delete a super admin + $allow = (!$iAmSuperAdmin && Access::check($pk, 'core.admin')) ? false : $allow; + + if ($allow) { + // Skip changing of same state + if ($table->block == $value) { + unset($pks[$i]); + continue; + } + + $table->block = (int) $value; + + // If unblocking, also change password reset count to zero to unblock reset + if ($table->block === 0) { + $table->resetCount = 0; + } + + // Allow an exception to be thrown. + try { + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($old, false, $table->getProperties())); + + if (in_array(false, $result, true)) { + // Plugin will have to raise its own error or throw an exception. + return false; + } + + // Store the table. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + if ($table->block) { + UserHelper::destroyUserSessions($table->id); + } + + // Trigger the after save event + Factory::getApplication()->triggerEvent($this->event_after_save, [$table->getProperties(), false, true, null]); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Log the user out. + if ($value) { + $app->logout($table->id, $options); + } + } else { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); + } + } + } + + return true; + } + + /** + * Method to activate user records. + * + * @param array &$pks The ids of the items to activate. + * + * @return boolean True on success. + * + * @since 1.6 + * @throws \Exception + */ + public function activate(&$pks) + { + $user = Factory::getUser(); + + // Check if I am a Super Admin + $iAmSuperAdmin = $user->authorise('core.admin'); + $table = $this->getTable(); + $pks = (array) $pks; + + PluginHelper::importPlugin($this->events_map['save']); + + // Access checks. + foreach ($pks as $i => $pk) { + if ($table->load($pk)) { + $old = $table->getProperties(); + $allow = $user->authorise('core.edit.state', 'com_users'); + + // Don't allow non-super-admin to delete a super admin + $allow = (!$iAmSuperAdmin && Access::check($pk, 'core.admin')) ? false : $allow; + + if (empty($table->activation)) { + // Ignore activated accounts. + unset($pks[$i]); + } elseif ($allow) { + $table->block = 0; + $table->activation = ''; + + // Allow an exception to be thrown. + try { + if (!$table->check()) { + $this->setError($table->getError()); + + return false; + } + + // Trigger the before save event. + $result = Factory::getApplication()->triggerEvent($this->event_before_save, array($old, false, $table->getProperties())); + + if (in_array(false, $result, true)) { + // Plugin will have to raise it's own error or throw an exception. + return false; + } + + // Store the table. + if (!$table->store()) { + $this->setError($table->getError()); + + return false; + } + + // Fire the after save event + Factory::getApplication()->triggerEvent($this->event_after_save, [$table->getProperties(), false, true, null]); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + } else { + // Prune items that you can't change. + unset($pks[$i]); + Factory::getApplication()->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'error'); + } + } + } + + return true; + } + + /** + * Method to perform batch operations on an item or a set of items. + * + * @param array $commands An array of commands to perform. + * @param array $pks An array of item ids. + * @param array $contexts An array of item contexts. + * + * @return boolean Returns true on success, false on failure. + * + * @since 2.5 + */ + public function batch($commands, $pks, $contexts) + { + // Sanitize user ids. + $pks = array_unique($pks); + $pks = ArrayHelper::toInteger($pks); + + // Remove any values of zero. + if (array_search(0, $pks, true)) { + unset($pks[array_search(0, $pks, true)]); + } + + if (empty($pks)) { + $this->setError(Text::_('COM_USERS_USERS_NO_ITEM_SELECTED')); + + return false; + } + + $done = false; + + if (!empty($commands['group_id'])) { + $cmd = ArrayHelper::getValue($commands, 'group_action', 'add'); + + if (!$this->batchUser((int) $commands['group_id'], $pks, $cmd)) { + return false; + } + + $done = true; + } + + if (!empty($commands['reset_id'])) { + if (!$this->batchReset($pks, $commands['reset_id'])) { + return false; + } + + $done = true; + } + + if (!$done) { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_INSUFFICIENT_BATCH_INFORMATION')); + + return false; + } + + // Clear the cache + $this->cleanCache(); + + return true; + } + + /** + * Batch flag users as being required to reset their passwords + * + * @param array $userIds An array of user IDs on which to operate + * @param string $action The action to perform + * + * @return boolean True on success, false on failure + * + * @since 3.2 + */ + public function batchReset($userIds, $action) + { + $userIds = ArrayHelper::toInteger($userIds); + + // Check if I am a Super Admin + $iAmSuperAdmin = Factory::getUser()->authorise('core.admin'); + + // Non-super super user cannot work with super-admin user. + if (!$iAmSuperAdmin && UserHelper::checkSuperUserInUsers($userIds)) { + $this->setError(Text::_('COM_USERS_ERROR_CANNOT_BATCH_SUPERUSER')); + + return false; + } + + // Set the action to perform + if ($action === 'yes') { + $value = 1; + } else { + $value = 0; + } + + // Prune out the current user if they are in the supplied user ID array + $userIds = array_diff($userIds, array(Factory::getUser()->id)); + + if (empty($userIds)) { + $this->setError(Text::_('COM_USERS_USERS_ERROR_CANNOT_REQUIRERESET_SELF')); + + return false; + } + + // Get the DB object + $db = $this->getDatabase(); + + $userIds = ArrayHelper::toInteger($userIds); + + $query = $db->getQuery(true); + + // Update the reset flag + $query->update($db->quoteName('#__users')) + ->set($db->quoteName('requireReset') . ' = :requireReset') + ->whereIn($db->quoteName('id'), $userIds) + ->bind(':requireReset', $value, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + return true; + } + + /** + * Perform batch operations + * + * @param integer $groupId The group ID which assignments are being edited + * @param array $userIds An array of user IDs on which to operate + * @param string $action The action to perform + * + * @return boolean True on success, false on failure + * + * @since 1.6 + */ + public function batchUser($groupId, $userIds, $action) + { + $userIds = ArrayHelper::toInteger($userIds); + + // Check if I am a Super Admin + $iAmSuperAdmin = Factory::getUser()->authorise('core.admin'); + + // Non-super super user cannot work with super-admin user. + if (!$iAmSuperAdmin && UserHelper::checkSuperUserInUsers($userIds)) { + $this->setError(Text::_('COM_USERS_ERROR_CANNOT_BATCH_SUPERUSER')); + + return false; + } + + // Non-super admin cannot work with super-admin group. + if ((!$iAmSuperAdmin && Access::checkGroup($groupId, 'core.admin')) || $groupId < 1) { + $this->setError(Text::_('COM_USERS_ERROR_INVALID_GROUP')); + + return false; + } + + // Get the DB object + $db = $this->getDatabase(); + + switch ($action) { + // Sets users to a selected group + case 'set': + $doDelete = 'all'; + $doAssign = true; + break; + + // Remove users from a selected group + case 'del': + $doDelete = 'group'; + break; + + // Add users to a selected group + case 'add': + default: + $doAssign = true; + break; + } + + // Remove the users from the group if requested. + if (isset($doDelete)) { + $query = $db->getQuery(true); + + // Remove users from the group + $query->delete($db->quoteName('#__user_usergroup_map')) + ->whereIn($db->quoteName('user_id'), $userIds); + + // Only remove users from selected group + if ($doDelete == 'group') { + $query->where($db->quoteName('group_id') . ' = :group_id') + ->bind(':group_id', $groupId, ParameterType::INTEGER); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + // Assign the users to the group if requested. + if (isset($doAssign)) { + $query = $db->getQuery(true); + + // First, we need to check if the user is already assigned to a group + $query->select($db->quoteName('user_id')) + ->from($db->quoteName('#__user_usergroup_map')) + ->where($db->quoteName('group_id') . ' = :group_id') + ->bind(':group_id', $groupId, ParameterType::INTEGER); + $db->setQuery($query); + $users = $db->loadColumn(); + + // Build the values clause for the assignment query. + $query->clear(); + $groups = false; + + foreach ($userIds as $id) { + if (!in_array($id, $users)) { + $query->values($id . ',' . $groupId); + $groups = true; + } + } + + // If we have no users to process, throw an error to notify the user + if (!$groups) { + $this->setError(Text::_('COM_USERS_ERROR_NO_ADDITIONS')); + + return false; + } + + $query->insert($db->quoteName('#__user_usergroup_map')) + ->columns(array($db->quoteName('user_id'), $db->quoteName('group_id'))); + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + + return true; + } + + /** + * Gets the available groups. + * + * @return array An array of groups + * + * @since 1.6 + */ + public function getGroups() + { + $user = Factory::getUser(); + + if ($user->authorise('core.edit', 'com_users') && $user->authorise('core.manage', 'com_users')) { + $model = $this->bootComponent('com_users') + ->getMVCFactory()->createModel('Groups', 'Administrator', ['ignore_request' => true]); + + return $model->getItems(); + } else { + return null; + } + } + + /** + * Gets the groups this object is assigned to + * + * @param integer $userId The user ID to retrieve the groups for + * + * @return array An array of assigned groups + * + * @since 1.6 + */ + public function getAssignedGroups($userId = null) + { + $userId = (!empty($userId)) ? $userId : (int) $this->getState('user.id'); + + if (empty($userId)) { + $result = array(); + $form = $this->getForm(); + + if ($form) { + $groupsIDs = $form->getValue('groups'); + } + + if (!empty($groupsIDs)) { + $result = $groupsIDs; + } else { + $params = ComponentHelper::getParams('com_users'); + + if ($groupId = $params->get('new_usertype', $params->get('guest_usergroup', 1))) { + $result[] = $groupId; + } + } + } else { + $result = UserHelper::getUserGroups($userId); + } + + return $result; + } + + /** + * No longer used + * + * @param integer $userId Ignored + * + * @return \stdClass + * + * @since 3.2 + * @deprecated 4.2.0 Will be removed in 5.0 + */ + public function getOtpConfig($userId = null) + { + @trigger_error( + sprintf( + '%s() is deprecated. Use \Joomla\Component\Users\Administrator\Helper\Mfa::getUserMfaRecords() instead.', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + // Return the configuration object + return (object) array( + 'method' => 'none', + 'config' => array(), + 'otep' => array() + ); + } + + /** + * No longer used + * + * @param integer $userId Ignored + * @param \stdClass $otpConfig Ignored + * + * @return boolean True on success + * + * @since 3.2 + * @deprecated 4.2.0 Will be removed in 5.0 + */ + public function setOtpConfig($userId, $otpConfig) + { + @trigger_error( + sprintf( + '%s() is deprecated. Multi-factor Authentication actions are handled by plugins in the multifactorauth folder.', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + return true; + } + + /** + * No longer used + * + * @return string + * + * @since 3.2 + * @deprecated 4.2.0 Will be removed in 5.0 + */ + public function getOtpConfigEncryptionKey() + { + @trigger_error( + sprintf( + '%s() is deprecated. Use \Joomla\CMS\Factory::getApplication()->get(\'secret\') instead', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + return Factory::getApplication()->get('secret'); + } + + /** + * No longer used + * + * @param integer $userId Ignored + * + * @return array Empty array + * + * @since 3.2 + * @throws \Exception + * + * @deprecated 4.2.0 Will be removed in 5.0. + */ + public function getTwofactorform($userId = null) + { + @trigger_error( + sprintf( + '%s() is deprecated. Use \Joomla\Component\Users\Administrator\Helper\Mfa::getConfigurationInterface()', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + return []; + } + + /** + * No longer used + * + * @param integer $userId Ignored + * @param integer $count Ignored + * + * @return array Empty array + * + * @since 3.2 + * @deprecated 4.2.0 Wil be removed in 5.0. + */ + public function generateOteps($userId, $count = 10) + { + @trigger_error( + sprintf( + '%s() is deprecated. See \Joomla\Component\Users\Administrator\Model\BackupcodesModel::saveBackupCodes()', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + return []; + } + + /** + * No longer used. Always returns true. + * + * @param integer $userId Ignored + * @param string $secretKey Ignored + * @param array $options Ignored + * + * @return boolean Always true + * + * @since 3.2 + * @throws \Exception + * + * @deprecated 4.2.0 Will be removed in 5.0. MFA validation is done in the captive login. + */ + public function isValidSecretKey($userId, $secretKey, $options = array()) + { + @trigger_error( + sprintf( + '%s() is deprecated. Multi-factor Authentication actions are handled by plugins in the multifactorauth folder.', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + return true; + } + + /** + * No longer used + * + * @param integer $userId Ignored + * @param string $otep Ignored + * @param object $otpConfig Ignored + * + * @return boolean Always true + * + * @since 3.2 + * @deprecated 4.2.0 Will be removed in 5.0 + */ + public function isValidOtep($userId, $otep, $otpConfig = null) + { + @trigger_error( + sprintf( + '%s() is deprecated. Multi-factor Authentication actions are handled by plugins in the multifactorauth folder.', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + return true; + } } diff --git a/administrator/components/com_users/src/Model/UsersModel.php b/administrator/components/com_users/src/Model/UsersModel.php index 7ec7fd620a985..30de749e7b5da 100644 --- a/administrator/components/com_users/src/Model/UsersModel.php +++ b/administrator/components/com_users/src/Model/UsersModel.php @@ -1,4 +1,5 @@ input->get('layout', 'default', 'cmd')) - { - $this->context .= '.' . $layout; - } - - $groups = json_decode(base64_decode($app->input->get('groups', '', 'BASE64'))); - - if (isset($groups)) - { - $groups = ArrayHelper::toInteger($groups); - } - - $this->setState('filter.groups', $groups); - - $excluded = json_decode(base64_decode($app->input->get('excluded', '', 'BASE64'))); - - if (isset($excluded)) - { - $excluded = ArrayHelper::toInteger($excluded); - } - - $this->setState('filter.excluded', $excluded); - - // Load the parameters. - $params = ComponentHelper::getParams('com_users'); - $this->setState('params', $params); - - // List state information. - parent::populateState($ordering, $direction); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.search'); - $id .= ':' . $this->getState('filter.active'); - $id .= ':' . $this->getState('filter.state'); - $id .= ':' . $this->getState('filter.group_id'); - $id .= ':' . $this->getState('filter.range'); - - if (PluginHelper::isEnabled('multifactorauth')) - { - $id .= ':' . $this->getState('filter.mfa'); - } - - return parent::getStoreId($id); - } - - /** - * Gets the list of users and adds expensive joins to the result set. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 1.6 - */ - public function getItems() - { - // Get a storage key. - $store = $this->getStoreId(); - - // Try to load the data from internal storage. - if (empty($this->cache[$store])) - { - $groups = $this->getState('filter.groups'); - $groupId = $this->getState('filter.group_id'); - - if (isset($groups) && (empty($groups) || $groupId && !in_array($groupId, $groups))) - { - $items = array(); - } - else - { - $items = parent::getItems(); - } - - // Bail out on an error or empty list. - if (empty($items)) - { - $this->cache[$store] = $items; - - return $items; - } - - // Joining the groups with the main query is a performance hog. - // Find the information only on the result set. - - // First pass: get list of the user ids and reset the counts. - $userIds = array(); - - foreach ($items as $item) - { - $userIds[] = (int) $item->id; + /** + * A list of filter variables to not merge into the model's state + * + * @var array + * @since 4.0.0 + */ + protected $filterForbiddenList = array('groups', 'excluded'); + + /** + * Override parent constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'username', 'a.username', + 'email', 'a.email', + 'block', 'a.block', + 'sendEmail', 'a.sendEmail', + 'registerDate', 'a.registerDate', + 'lastvisitDate', 'a.lastvisitDate', + 'activation', 'a.activation', + 'active', + 'group_id', + 'range', + 'lastvisitrange', + 'state', + 'mfa' + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function populateState($ordering = 'a.name', $direction = 'asc') + { + $app = Factory::getApplication(); + + // Adjust the context to support modal layouts. + if ($layout = $app->input->get('layout', 'default', 'cmd')) { + $this->context .= '.' . $layout; + } + + $groups = json_decode(base64_decode($app->input->get('groups', '', 'BASE64'))); + + if (isset($groups)) { + $groups = ArrayHelper::toInteger($groups); + } + + $this->setState('filter.groups', $groups); + + $excluded = json_decode(base64_decode($app->input->get('excluded', '', 'BASE64'))); + + if (isset($excluded)) { + $excluded = ArrayHelper::toInteger($excluded); + } + + $this->setState('filter.excluded', $excluded); + + // Load the parameters. + $params = ComponentHelper::getParams('com_users'); + $this->setState('params', $params); + + // List state information. + parent::populateState($ordering, $direction); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.active'); + $id .= ':' . $this->getState('filter.state'); + $id .= ':' . $this->getState('filter.group_id'); + $id .= ':' . $this->getState('filter.range'); + + if (PluginHelper::isEnabled('multifactorauth')) { + $id .= ':' . $this->getState('filter.mfa'); + } + + return parent::getStoreId($id); + } + + /** + * Gets the list of users and adds expensive joins to the result set. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 1.6 + */ + public function getItems() + { + // Get a storage key. + $store = $this->getStoreId(); + + // Try to load the data from internal storage. + if (empty($this->cache[$store])) { + $groups = $this->getState('filter.groups'); + $groupId = $this->getState('filter.group_id'); + + if (isset($groups) && (empty($groups) || $groupId && !in_array($groupId, $groups))) { + $items = array(); + } else { + $items = parent::getItems(); + } + + // Bail out on an error or empty list. + if (empty($items)) { + $this->cache[$store] = $items; + + return $items; + } + + // Joining the groups with the main query is a performance hog. + // Find the information only on the result set. + + // First pass: get list of the user ids and reset the counts. + $userIds = array(); + + foreach ($items as $item) { + $userIds[] = (int) $item->id; // phpcs:ignore $item->group_count = 0; // phpcs:ignore $item->group_names = ''; // phpcs:ignore $item->note_count = 0; - } - - // Get the counts from the database only for the users in the list. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Join over the group mapping table. - $query->select('map.user_id, COUNT(map.group_id) AS group_count') - ->from('#__user_usergroup_map AS map') - ->whereIn($db->quoteName('map.user_id'), $userIds) - ->group('map.user_id') - // Join over the user groups table. - ->join('LEFT', '#__usergroups AS g2 ON g2.id = map.group_id'); - - $db->setQuery($query); - - // Load the counts into an array indexed on the user id field. - try - { - $userGroups = $db->loadObjectList('user_id'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - $query->clear() - ->select('n.user_id, COUNT(n.id) As note_count') - ->from('#__user_notes AS n') - ->whereIn($db->quoteName('n.user_id'), $userIds) - ->where('n.state >= 0') - ->group('n.user_id'); - - $db->setQuery($query); - - // Load the counts into an array indexed on the aro.value field (the user id). - try - { - $userNotes = $db->loadObjectList('user_id'); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - - // Second pass: collect the group counts into the master items array. - foreach ($items as &$item) - { - if (isset($userGroups[$item->id])) - { + } + + // Get the counts from the database only for the users in the list. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Join over the group mapping table. + $query->select('map.user_id, COUNT(map.group_id) AS group_count') + ->from('#__user_usergroup_map AS map') + ->whereIn($db->quoteName('map.user_id'), $userIds) + ->group('map.user_id') + // Join over the user groups table. + ->join('LEFT', '#__usergroups AS g2 ON g2.id = map.group_id'); + + $db->setQuery($query); + + // Load the counts into an array indexed on the user id field. + try { + $userGroups = $db->loadObjectList('user_id'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + $query->clear() + ->select('n.user_id, COUNT(n.id) As note_count') + ->from('#__user_notes AS n') + ->whereIn($db->quoteName('n.user_id'), $userIds) + ->where('n.state >= 0') + ->group('n.user_id'); + + $db->setQuery($query); + + // Load the counts into an array indexed on the aro.value field (the user id). + try { + $userNotes = $db->loadObjectList('user_id'); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + + // Second pass: collect the group counts into the master items array. + foreach ($items as &$item) { + if (isset($userGroups[$item->id])) { // phpcs:ignore $item->group_count = $userGroups[$item->id]->group_count; - // Group_concat in other databases is not supported + // Group_concat in other databases is not supported // phpcs:ignore $item->group_names = $this->getUserDisplayedGroups($item->id); - } + } - if (isset($userNotes[$item->id])) - { + if (isset($userNotes[$item->id])) { // phpcs:ignore $item->note_count = $userNotes[$item->id]->note_count; - } - } - - // Add the items to the internal cache. - $this->cache[$store] = $items; - } - - return $this->cache[$store]; - } - - /** - * Get the filter form - * - * @param array $data data - * @param boolean $loadData load current data - * - * @return Form|null The \JForm object or null if the form can't be found - * - * @since 4.2.0 - */ - public function getFilterForm($data = [], $loadData = true) - { - $form = parent::getFilterForm($data, $loadData); - - if ($form && !PluginHelper::isEnabled('multifactorauth')) - { - $form->removeField('mfa', 'filter'); - } - - return $form; - } - - - /** - * Build an SQL query to load the list data. - * - * @return DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'a.*' - ) - ); - - $query->from($db->quoteName('#__users') . ' AS a'); - - // Include MFA information - if (PluginHelper::isEnabled('multifactorauth')) - { - $subQuery = $db->getQuery(true) - ->select( - [ - 'MIN(' . $db->quoteName('user_id') . ') AS ' . $db->quoteName('uid'), - 'COUNT(*) AS ' . $db->quoteName('mfaRecords') - ] - ) - ->from($db->quoteName('#__user_mfa')) - ->group($db->quoteName('user_id')); - $query->select($db->quoteName('mfa.mfaRecords')) - ->join( - 'left', - '(' . $subQuery . ') AS ' . $db->quoteName('mfa'), - $db->quoteName('mfa.uid') . ' = ' . $db->quoteName('a.id') - ); - - $mfaState = $this->getState('filter.mfa'); - - if (is_numeric($mfaState)) - { - $mfaState = (int) $mfaState; - - if ($mfaState === 1) - { - $query->where( - '((' . $db->quoteName('mfa.mfaRecords') . ' > 0) OR (' . - $db->quoteName('a.otpKey') . ' IS NOT NULL AND ' . - $db->quoteName('a.otpKey') . ' != ' . $db->quote('') . '))' - ); - } - else - { - $query->where( - '((' . $db->quoteName('mfa.mfaRecords') . ' = 0 OR ' . - $db->quoteName('mfa.mfaRecords') . ' IS NULL) AND (' . - $db->quoteName('a.otpKey') . ' IS NULL OR ' . - $db->quoteName('a.otpKey') . ' = ' . $db->quote('') . '))' - ); - } - } - } - - // If the model is set to check item state, add to the query. - $state = $this->getState('filter.state'); - - if (is_numeric($state)) - { - $query->where($db->quoteName('a.block') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - - // If the model is set to check the activated state, add to the query. - $active = $this->getState('filter.active'); - - if (is_numeric($active)) - { - if ($active == '0') - { - $query->whereIn($db->quoteName('a.activation'), ['', '0']); - } - elseif ($active == '1') - { - $query->where($query->length($db->quoteName('a.activation')) . ' > 1'); - } - } - - // Filter the items over the group id if set. - $groupId = $this->getState('filter.group_id'); - $groups = $this->getState('filter.groups'); - - if ($groupId || isset($groups)) - { - $query->join('LEFT', '#__user_usergroup_map AS map2 ON map2.user_id = a.id') - ->group( - $db->quoteName( - array( - 'a.id', - 'a.name', - 'a.username', - 'a.password', - 'a.block', - 'a.sendEmail', - 'a.registerDate', - 'a.lastvisitDate', - 'a.activation', - 'a.params', - 'a.email', - 'a.lastResetTime', - 'a.resetCount', - 'a.otpKey', - 'a.otep', - 'a.requireReset' - ) - ) - ); - - if ($groupId) - { - $groupId = (int) $groupId; - $query->where($db->quoteName('map2.group_id') . ' = :group_id') - ->bind(':group_id', $groupId, ParameterType::INTEGER); - } - - if (isset($groups)) - { - $query->whereIn($db->quoteName('map2.group_id'), $groups); - } - } - - // Filter the items over the search string if set. - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - if (stripos($search, 'id:') === 0) - { - $ids = (int) substr($search, 3); - $query->where($db->quoteName('a.id') . ' = :id'); - $query->bind(':id', $ids, ParameterType::INTEGER); - } - elseif (stripos($search, 'username:') === 0) - { - $search = '%' . substr($search, 9) . '%'; - $query->where($db->quoteName('a.username') . ' LIKE :username'); - $query->bind(':username', $search); - } - else - { - $search = '%' . trim($search) . '%'; - - // Add the clauses to the query. - $query->where( - '(' . $db->quoteName('a.name') . ' LIKE :name' - . ' OR ' . $db->quoteName('a.username') . ' LIKE :username' - . ' OR ' . $db->quoteName('a.email') . ' LIKE :email)' - ) - ->bind(':name', $search) - ->bind(':username', $search) - ->bind(':email', $search); - } - } - - // Add filter for registration time ranges select list. UI Visitors get a range of predefined - // values. API users can do a full range based on ISO8601 - $range = $this->getState('filter.range'); - $registrationStart = $this->getState('filter.registrationDateStart'); - $registrationEnd = $this->getState('filter.registrationDateEnd'); - - // Apply the range filter. - if ($range || ($registrationStart && $registrationEnd)) - { - if ($range) - { - $dates = $this->buildDateRange($range); - } - else - { - $dates = [ - 'dNow' => $registrationEnd, - 'dStart' => $registrationStart, - ]; - } - - if ($dates['dStart'] !== false) - { - $dStart = $dates['dStart']->format('Y-m-d H:i:s'); - - if ($dates['dNow'] === false) - { - $query->where($db->quoteName('a.registerDate') . ' < :registerDate'); - $query->bind(':registerDate', $dStart); - } - else - { - $dNow = $dates['dNow']->format('Y-m-d H:i:s'); - - $query->where($db->quoteName('a.registerDate') . ' BETWEEN :registerDate1 AND :registerDate2'); - $query->bind(':registerDate1', $dStart); - $query->bind(':registerDate2', $dNow); - } - } - } - - // Add filter for last visit time ranges select list. UI Visitors get a range of predefined - // values. API users can do a full range based on ISO8601 - $lastvisitrange = $this->getState('filter.lastvisitrange'); - $lastVisitStart = $this->getState('filter.lastVisitStart'); - $lastVisitEnd = $this->getState('filter.lastVisitEnd'); - - // Apply the range filter. - if ($lastvisitrange || ($lastVisitStart && $lastVisitEnd)) - { - if ($lastvisitrange) - { - $dates = $this->buildDateRange($lastvisitrange); - } - else - { - $dates = [ - 'dNow' => $lastVisitEnd, - 'dStart' => $lastVisitStart, - ]; - } - - if ($dates['dStart'] === false) - { - $query->where($db->quoteName('a.lastvisitDate') . ' IS NULL'); - } - else - { - $query->where($db->quoteName('a.lastvisitDate') . ' IS NOT NULL'); - - $dStart = $dates['dStart']->format('Y-m-d H:i:s'); - - if ($dates['dNow'] === false) - { - $query->where($db->quoteName('a.lastvisitDate') . ' < :lastvisitDate'); - $query->bind(':lastvisitDate', $dStart); - } - else - { - $dNow = $dates['dNow']->format('Y-m-d H:i:s'); - - $query->where($db->quoteName('a.lastvisitDate') . ' BETWEEN :lastvisitDate1 AND :lastvisitDate2'); - $query->bind(':lastvisitDate1', $dStart); - $query->bind(':lastvisitDate2', $dNow); - } - } - } - - // Filter by excluded users - $excluded = $this->getState('filter.excluded'); - - if (!empty($excluded)) - { - $query->whereNotIn($db->quoteName('id'), $excluded); - } - - // Add the list ordering clause. - $query->order( - $db->quoteName($db->escape($this->getState('list.ordering', 'a.name'))) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) - ); - - return $query; - } - - /** - * Construct the date range to filter on. - * - * @param string $range The textual range to construct the filter for. - * - * @return array The date range to filter on. - * - * @since 3.6.0 - * @throws \Exception - */ - private function buildDateRange($range) - { - // Get UTC for now. - $dNow = new Date; - $dStart = clone $dNow; - - switch ($range) - { - case 'past_week': - $dStart->modify('-7 day'); - break; - - case 'past_1month': - $dStart->modify('-1 month'); - break; - - case 'past_3month': - $dStart->modify('-3 month'); - break; - - case 'past_6month': - $dStart->modify('-6 month'); - $arr = []; - break; - - case 'post_year': - $dNow = false; - case 'past_year': - $dStart->modify('-1 year'); - break; - - case 'today': - // Ranges that need to align with local 'days' need special treatment. - $app = Factory::getApplication(); - $offset = $app->get('offset'); - - // Reset the start time to be the beginning of today, local time. - $dStart = new Date('now', $offset); - $dStart->setTime(0, 0, 0); - - // Now change the timezone back to UTC. - $tz = new \DateTimeZone('GMT'); - $dStart->setTimezone($tz); - break; - case 'never': - $dNow = false; - $dStart = false; - break; - } - - return array('dNow' => $dNow, 'dStart' => $dStart); - } - - /** - * SQL server change - * - * @param integer $userId User identifier - * - * @return string Groups titles imploded :$ - */ - protected function getUserDisplayedGroups($userId) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__usergroups', 'ug')) - ->join('LEFT', $db->quoteName('#__user_usergroup_map', 'map') . ' ON (ug.id = map.group_id)') - ->where($db->quoteName('map.user_id') . ' = :user_id') - ->bind(':user_id', $userId, ParameterType::INTEGER); - - try - { - $result = $db->setQuery($query)->loadColumn(); - } - catch (\RuntimeException $e) - { - $result = array(); - } - - return implode("\n", $result); - } + } + } + + // Add the items to the internal cache. + $this->cache[$store] = $items; + } + + return $this->cache[$store]; + } + + /** + * Get the filter form + * + * @param array $data data + * @param boolean $loadData load current data + * + * @return Form|null The \JForm object or null if the form can't be found + * + * @since 4.2.0 + */ + public function getFilterForm($data = [], $loadData = true) + { + $form = parent::getFilterForm($data, $loadData); + + if ($form && !PluginHelper::isEnabled('multifactorauth')) { + $form->removeField('mfa', 'filter'); + } + + return $form; + } + + + /** + * Build an SQL query to load the list data. + * + * @return DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'a.*' + ) + ); + + $query->from($db->quoteName('#__users') . ' AS a'); + + // Include MFA information + if (PluginHelper::isEnabled('multifactorauth')) { + $subQuery = $db->getQuery(true) + ->select( + [ + 'MIN(' . $db->quoteName('user_id') . ') AS ' . $db->quoteName('uid'), + 'COUNT(*) AS ' . $db->quoteName('mfaRecords') + ] + ) + ->from($db->quoteName('#__user_mfa')) + ->group($db->quoteName('user_id')); + $query->select($db->quoteName('mfa.mfaRecords')) + ->join( + 'left', + '(' . $subQuery . ') AS ' . $db->quoteName('mfa'), + $db->quoteName('mfa.uid') . ' = ' . $db->quoteName('a.id') + ); + + $mfaState = $this->getState('filter.mfa'); + + if (is_numeric($mfaState)) { + $mfaState = (int) $mfaState; + + if ($mfaState === 1) { + $query->where( + '((' . $db->quoteName('mfa.mfaRecords') . ' > 0) OR (' . + $db->quoteName('a.otpKey') . ' IS NOT NULL AND ' . + $db->quoteName('a.otpKey') . ' != ' . $db->quote('') . '))' + ); + } else { + $query->where( + '((' . $db->quoteName('mfa.mfaRecords') . ' = 0 OR ' . + $db->quoteName('mfa.mfaRecords') . ' IS NULL) AND (' . + $db->quoteName('a.otpKey') . ' IS NULL OR ' . + $db->quoteName('a.otpKey') . ' = ' . $db->quote('') . '))' + ); + } + } + } + + // If the model is set to check item state, add to the query. + $state = $this->getState('filter.state'); + + if (is_numeric($state)) { + $query->where($db->quoteName('a.block') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } + + // If the model is set to check the activated state, add to the query. + $active = $this->getState('filter.active'); + + if (is_numeric($active)) { + if ($active == '0') { + $query->whereIn($db->quoteName('a.activation'), ['', '0']); + } elseif ($active == '1') { + $query->where($query->length($db->quoteName('a.activation')) . ' > 1'); + } + } + + // Filter the items over the group id if set. + $groupId = $this->getState('filter.group_id'); + $groups = $this->getState('filter.groups'); + + if ($groupId || isset($groups)) { + $query->join('LEFT', '#__user_usergroup_map AS map2 ON map2.user_id = a.id') + ->group( + $db->quoteName( + array( + 'a.id', + 'a.name', + 'a.username', + 'a.password', + 'a.block', + 'a.sendEmail', + 'a.registerDate', + 'a.lastvisitDate', + 'a.activation', + 'a.params', + 'a.email', + 'a.lastResetTime', + 'a.resetCount', + 'a.otpKey', + 'a.otep', + 'a.requireReset' + ) + ) + ); + + if ($groupId) { + $groupId = (int) $groupId; + $query->where($db->quoteName('map2.group_id') . ' = :group_id') + ->bind(':group_id', $groupId, ParameterType::INTEGER); + } + + if (isset($groups)) { + $query->whereIn($db->quoteName('map2.group_id'), $groups); + } + } + + // Filter the items over the search string if set. + $search = $this->getState('filter.search'); + + if (!empty($search)) { + if (stripos($search, 'id:') === 0) { + $ids = (int) substr($search, 3); + $query->where($db->quoteName('a.id') . ' = :id'); + $query->bind(':id', $ids, ParameterType::INTEGER); + } elseif (stripos($search, 'username:') === 0) { + $search = '%' . substr($search, 9) . '%'; + $query->where($db->quoteName('a.username') . ' LIKE :username'); + $query->bind(':username', $search); + } else { + $search = '%' . trim($search) . '%'; + + // Add the clauses to the query. + $query->where( + '(' . $db->quoteName('a.name') . ' LIKE :name' + . ' OR ' . $db->quoteName('a.username') . ' LIKE :username' + . ' OR ' . $db->quoteName('a.email') . ' LIKE :email)' + ) + ->bind(':name', $search) + ->bind(':username', $search) + ->bind(':email', $search); + } + } + + // Add filter for registration time ranges select list. UI Visitors get a range of predefined + // values. API users can do a full range based on ISO8601 + $range = $this->getState('filter.range'); + $registrationStart = $this->getState('filter.registrationDateStart'); + $registrationEnd = $this->getState('filter.registrationDateEnd'); + + // Apply the range filter. + if ($range || ($registrationStart && $registrationEnd)) { + if ($range) { + $dates = $this->buildDateRange($range); + } else { + $dates = [ + 'dNow' => $registrationEnd, + 'dStart' => $registrationStart, + ]; + } + + if ($dates['dStart'] !== false) { + $dStart = $dates['dStart']->format('Y-m-d H:i:s'); + + if ($dates['dNow'] === false) { + $query->where($db->quoteName('a.registerDate') . ' < :registerDate'); + $query->bind(':registerDate', $dStart); + } else { + $dNow = $dates['dNow']->format('Y-m-d H:i:s'); + + $query->where($db->quoteName('a.registerDate') . ' BETWEEN :registerDate1 AND :registerDate2'); + $query->bind(':registerDate1', $dStart); + $query->bind(':registerDate2', $dNow); + } + } + } + + // Add filter for last visit time ranges select list. UI Visitors get a range of predefined + // values. API users can do a full range based on ISO8601 + $lastvisitrange = $this->getState('filter.lastvisitrange'); + $lastVisitStart = $this->getState('filter.lastVisitStart'); + $lastVisitEnd = $this->getState('filter.lastVisitEnd'); + + // Apply the range filter. + if ($lastvisitrange || ($lastVisitStart && $lastVisitEnd)) { + if ($lastvisitrange) { + $dates = $this->buildDateRange($lastvisitrange); + } else { + $dates = [ + 'dNow' => $lastVisitEnd, + 'dStart' => $lastVisitStart, + ]; + } + + if ($dates['dStart'] === false) { + $query->where($db->quoteName('a.lastvisitDate') . ' IS NULL'); + } else { + $query->where($db->quoteName('a.lastvisitDate') . ' IS NOT NULL'); + + $dStart = $dates['dStart']->format('Y-m-d H:i:s'); + + if ($dates['dNow'] === false) { + $query->where($db->quoteName('a.lastvisitDate') . ' < :lastvisitDate'); + $query->bind(':lastvisitDate', $dStart); + } else { + $dNow = $dates['dNow']->format('Y-m-d H:i:s'); + + $query->where($db->quoteName('a.lastvisitDate') . ' BETWEEN :lastvisitDate1 AND :lastvisitDate2'); + $query->bind(':lastvisitDate1', $dStart); + $query->bind(':lastvisitDate2', $dNow); + } + } + } + + // Filter by excluded users + $excluded = $this->getState('filter.excluded'); + + if (!empty($excluded)) { + $query->whereNotIn($db->quoteName('id'), $excluded); + } + + // Add the list ordering clause. + $query->order( + $db->quoteName($db->escape($this->getState('list.ordering', 'a.name'))) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) + ); + + return $query; + } + + /** + * Construct the date range to filter on. + * + * @param string $range The textual range to construct the filter for. + * + * @return array The date range to filter on. + * + * @since 3.6.0 + * @throws \Exception + */ + private function buildDateRange($range) + { + // Get UTC for now. + $dNow = new Date(); + $dStart = clone $dNow; + + switch ($range) { + case 'past_week': + $dStart->modify('-7 day'); + break; + + case 'past_1month': + $dStart->modify('-1 month'); + break; + + case 'past_3month': + $dStart->modify('-3 month'); + break; + + case 'past_6month': + $dStart->modify('-6 month'); + $arr = []; + break; + + case 'post_year': + $dNow = false; + + // No break + + case 'past_year': + $dStart->modify('-1 year'); + break; + + case 'today': + // Ranges that need to align with local 'days' need special treatment. + $app = Factory::getApplication(); + $offset = $app->get('offset'); + + // Reset the start time to be the beginning of today, local time. + $dStart = new Date('now', $offset); + $dStart->setTime(0, 0, 0); + + // Now change the timezone back to UTC. + $tz = new \DateTimeZone('GMT'); + $dStart->setTimezone($tz); + break; + case 'never': + $dNow = false; + $dStart = false; + break; + } + + return array('dNow' => $dNow, 'dStart' => $dStart); + } + + /** + * SQL server change + * + * @param integer $userId User identifier + * + * @return string Groups titles imploded :$ + */ + protected function getUserDisplayedGroups($userId) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__usergroups', 'ug')) + ->join('LEFT', $db->quoteName('#__user_usergroup_map', 'map') . ' ON (ug.id = map.group_id)') + ->where($db->quoteName('map.user_id') . ' = :user_id') + ->bind(':user_id', $userId, ParameterType::INTEGER); + + try { + $result = $db->setQuery($query)->loadColumn(); + } catch (\RuntimeException $e) { + $result = array(); + } + + return implode("\n", $result); + } } diff --git a/administrator/components/com_users/src/Service/Encrypt.php b/administrator/components/com_users/src/Service/Encrypt.php index 935be2eff6144..eb1ca97d6b0e3 100644 --- a/administrator/components/com_users/src/Service/Encrypt.php +++ b/administrator/components/com_users/src/Service/Encrypt.php @@ -1,4 +1,5 @@ initialize(); - } - - /** - * Encrypt the plaintext $data and return the ciphertext prefixed by ###AES128### - * - * @param string $data The plaintext data - * - * @return string The ciphertext, prefixed by ###AES128### - * - * @since 4.2.0 - */ - public function encrypt(string $data): string - { - if (!is_object($this->aes)) - { - return $data; - } - - $this->aes->setPassword($this->getPassword(), false); - $encrypted = $this->aes->encryptString($data, true); - - return '###AES128###' . $encrypted; - } - - /** - * Decrypt the ciphertext, prefixed by ###AES128###, and return the plaintext. - * - * @param string $data The ciphertext, prefixed by ###AES128### - * @param bool $legacy Use legacy key expansion? Use it to decrypt data encrypted with FOF 3. - * - * @return string The plaintext data - * - * @since 4.2.0 - */ - public function decrypt(string $data, bool $legacy = false): string - { - if (substr($data, 0, 12) != '###AES128###') - { - return $data; - } - - $data = substr($data, 12); - - if (!is_object($this->aes)) - { - return $data; - } - - $this->aes->setPassword($this->getPassword(), $legacy); - $decrypted = $this->aes->decryptString($data, true, $legacy); - - // Decrypted data is null byte padded. We have to remove the padding before proceeding. - return rtrim($decrypted, "\0"); - } - - /** - * Initialize the AES cryptography object - * - * @return void - * @since 4.2.0 - */ - private function initialize(): void - { - if (is_object($this->aes)) - { - return; - } - - $password = $this->getPassword(); - - if (empty($password)) - { - return; - } - - $this->aes = new Aes('cbc'); - $this->aes->setPassword($password); - } - - /** - * Returns the password used to encrypt information in the component - * - * @return string - * - * @since 4.2.0 - */ - private function getPassword(): string - { - try - { - return Factory::getApplication()->get('secret', ''); - } - catch (\Exception $e) - { - return ''; - } - } + /** + * The encryption engine used by this service + * + * @var Aes + * @since 4.2.0 + */ + private $aes; + + /** + * EncryptService constructor. + * + * @since 4.2.0 + */ + public function __construct() + { + $this->initialize(); + } + + /** + * Encrypt the plaintext $data and return the ciphertext prefixed by ###AES128### + * + * @param string $data The plaintext data + * + * @return string The ciphertext, prefixed by ###AES128### + * + * @since 4.2.0 + */ + public function encrypt(string $data): string + { + if (!is_object($this->aes)) { + return $data; + } + + $this->aes->setPassword($this->getPassword(), false); + $encrypted = $this->aes->encryptString($data, true); + + return '###AES128###' . $encrypted; + } + + /** + * Decrypt the ciphertext, prefixed by ###AES128###, and return the plaintext. + * + * @param string $data The ciphertext, prefixed by ###AES128### + * @param bool $legacy Use legacy key expansion? Use it to decrypt data encrypted with FOF 3. + * + * @return string The plaintext data + * + * @since 4.2.0 + */ + public function decrypt(string $data, bool $legacy = false): string + { + if (substr($data, 0, 12) != '###AES128###') { + return $data; + } + + $data = substr($data, 12); + + if (!is_object($this->aes)) { + return $data; + } + + $this->aes->setPassword($this->getPassword(), $legacy); + $decrypted = $this->aes->decryptString($data, true, $legacy); + + // Decrypted data is null byte padded. We have to remove the padding before proceeding. + return rtrim($decrypted, "\0"); + } + + /** + * Initialize the AES cryptography object + * + * @return void + * @since 4.2.0 + */ + private function initialize(): void + { + if (is_object($this->aes)) { + return; + } + + $password = $this->getPassword(); + + if (empty($password)) { + return; + } + + $this->aes = new Aes('cbc'); + $this->aes->setPassword($password); + } + + /** + * Returns the password used to encrypt information in the component + * + * @return string + * + * @since 4.2.0 + */ + private function getPassword(): string + { + try { + return Factory::getApplication()->get('secret', ''); + } catch (\Exception $e) { + return ''; + } + } } diff --git a/administrator/components/com_users/src/Service/HTML/Users.php b/administrator/components/com_users/src/Service/HTML/Users.php index 1f8e9edfe6194..a3c045ccd3f5d 100644 --- a/administrator/components/com_users/src/Service/HTML/Users.php +++ b/administrator/components/com_users/src/Service/HTML/Users.php @@ -1,4 +1,5 @@ element if the specified file exists, otherwise, a null string - * - * @since 2.5 - * @throws \Exception - */ - public function image($src) - { - $src = preg_replace('#[^A-Z0-9\-_\./]#i', '', $src); - $file = JPATH_SITE . '/' . $src; - - Path::check($file); - - if (!file_exists($file)) - { - return ''; - } - - return ''; - } - - /** - * Displays an icon to add a note for this user. - * - * @param integer $userId The user ID - * - * @return string A link to add a note - * - * @since 2.5 - */ - public function addNote($userId) - { - $title = Text::_('COM_USERS_ADD_NOTE'); - - return '' . $title . ''; - } - - /** - * Displays an icon to filter the notes list on this user. - * - * @param integer $count The number of notes for the user - * @param integer $userId The user ID - * - * @return string A link to apply a filter - * - * @since 2.5 - */ - public function filterNotes($count, $userId) - { - if (empty($count)) - { - return ''; - } - - $title = Text::_('COM_USERS_FILTER_NOTES'); - - return '' . $title . ''; - } - - /** - * Displays a note icon. - * - * @param integer $count The number of notes for the user - * @param integer $userId The user ID - * - * @return string A link to a modal window with the user notes - * - * @since 2.5 - */ - public function notes($count, $userId) - { - if (empty($count)) - { - return ''; - } - - $title = Text::plural('COM_USERS_N_USER_NOTES', $count); - - return ''; - } - - /** - * Renders the modal html. - * - * @param integer $count The number of notes for the user - * @param integer $userId The user ID - * - * @return string The html for the rendered modal - * - * @since 3.4.1 - */ - public function notesModal($count, $userId) - { - if (empty($count)) - { - return ''; - } - - $title = Text::plural('COM_USERS_N_USER_NOTES', $count); - $footer = ''; - - return HTMLHelper::_( - 'bootstrap.renderModal', - 'userModal_' . (int) $userId, - array( - 'title' => $title, - 'backdrop' => 'static', - 'keyboard' => true, - 'closeButton' => true, - 'footer' => $footer, - 'url' => Route::_('index.php?option=com_users&view=notes&tmpl=component&layout=modal&filter[user_id]=' . (int) $userId), - 'height' => '300px', - 'width' => '800px', - ) - ); - - } - - /** - * Build an array of block/unblock user states to be used by jgrid.state, - * State options will be different for any user - * and for currently logged in user - * - * @param boolean $self True if state array is for currently logged in user - * - * @return array a list of possible states to display - * - * @since 3.0 - */ - public function blockStates( $self = false) - { - if ($self) - { - $states = array( - 1 => array( - 'task' => 'unblock', - 'text' => '', - 'active_title' => 'COM_USERS_TOOLBAR_BLOCK', - 'inactive_title' => '', - 'tip' => true, - 'active_class' => 'unpublish', - 'inactive_class' => 'unpublish', - ), - 0 => array( - 'task' => 'block', - 'text' => '', - 'active_title' => '', - 'inactive_title' => 'COM_USERS_USERS_ERROR_CANNOT_BLOCK_SELF', - 'tip' => true, - 'active_class' => 'publish', - 'inactive_class' => 'publish', - ) - ); - } - else - { - $states = array( - 1 => array( - 'task' => 'unblock', - 'text' => '', - 'active_title' => 'COM_USERS_TOOLBAR_UNBLOCK', - 'inactive_title' => '', - 'tip' => true, - 'active_class' => 'unpublish', - 'inactive_class' => 'unpublish', - ), - 0 => array( - 'task' => 'block', - 'text' => '', - 'active_title' => 'COM_USERS_TOOLBAR_BLOCK', - 'inactive_title' => '', - 'tip' => true, - 'active_class' => 'publish', - 'inactive_class' => 'publish', - ) - ); - } - - return $states; - } - - /** - * Build an array of activate states to be used by jgrid.state, - * - * @return array a list of possible states to display - * - * @since 3.0 - */ - public function activateStates() - { - $states = array( - 1 => array( - 'task' => 'activate', - 'text' => '', - 'active_title' => 'COM_USERS_TOOLBAR_ACTIVATE', - 'inactive_title' => '', - 'tip' => true, - 'active_class' => 'unpublish', - 'inactive_class' => 'unpublish', - ), - 0 => array( - 'task' => '', - 'text' => '', - 'active_title' => '', - 'inactive_title' => 'COM_USERS_ACTIVATED', - 'tip' => true, - 'active_class' => 'publish', - 'inactive_class' => 'publish', - ) - ); - - return $states; - } - - /** - * Get the sanitized value - * - * @param mixed $value Value of the field - * - * @return mixed String/void - * - * @since 1.6 - */ - public function value($value) - { - if (is_string($value)) - { - $value = trim($value); - } - - if (empty($value)) - { - return Text::_('COM_USERS_PROFILE_VALUE_NOT_FOUND'); - } - - elseif (!is_array($value)) - { - return htmlspecialchars($value, ENT_COMPAT, 'UTF-8'); - } - } - - /** - * Get the space symbol - * - * @param mixed $value Value of the field - * - * @return string - * - * @since 1.6 - */ - public function spacer($value) - { - return ''; - } - - /** - * Get the sanitized template style - * - * @param mixed $value Value of the field - * - * @return mixed String/void - * - * @since 1.6 - */ - public function templatestyle($value) - { - if (empty($value)) - { - return static::value($value); - } - else - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__template_styles')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $value, ParameterType::INTEGER); - $db->setQuery($query); - $title = $db->loadResult(); - - if ($title) - { - return htmlspecialchars($title, ENT_COMPAT, 'UTF-8'); - } - else - { - return static::value(''); - } - } - } - - /** - * Get the sanitized language - * - * @param mixed $value Value of the field - * - * @return mixed String/void - * - * @since 1.6 - */ - public function admin_language($value) - { - if (!$value) - { - return static::value($value); - } - - $path = LanguageHelper::getLanguagePath(JPATH_ADMINISTRATOR, $value); - $file = $path . '/langmetadata.xml'; - - if (!is_file($file)) - { - // For language packs from before 4.0. - $file = $path . '/' . $value . '.xml'; - - if (!is_file($file)) - { - return static::value($value); - } - } - - $result = LanguageHelper::parseXMLLanguageFile($file); - - if ($result) - { - return htmlspecialchars($result['name'], ENT_COMPAT, 'UTF-8'); - } - - return static::value($value); - } - - /** - * Get the sanitized language - * - * @param mixed $value Value of the field - * - * @return mixed String/void - * - * @since 1.6 - */ - public function language($value) - { - if (!$value) - { - return static::value($value); - } - - $path = LanguageHelper::getLanguagePath(JPATH_SITE, $value); - $file = $path . '/langmetadata.xml'; - - if (!is_file($file)) - { - // For language packs from before 4.0. - $file = $path . '/' . $value . '.xml'; - - if (!is_file($file)) - { - return static::value($value); - } - } - - $result = LanguageHelper::parseXMLLanguageFile($file); - - if ($result) - { - return htmlspecialchars($result['name'], ENT_COMPAT, 'UTF-8'); - } - - return static::value($value); - } - - /** - * Get the sanitized editor name - * - * @param mixed $value Value of the field - * - * @return mixed String/void - * - * @since 1.6 - */ - public function editor($value) - { - if (empty($value)) - { - return static::value($value); - } - else - { - $db = Factory::getDbo(); - $lang = Factory::getLanguage(); - $query = $db->getQuery(true) - ->select($db->quoteName('name')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('element') . ' = :element') - ->where($db->quoteName('folder') . ' = ' . $db->quote('editors')) - ->bind(':element', $value); - $db->setQuery($query); - $title = $db->loadResult(); - - if ($title) - { - $lang->load("plg_editors_$value.sys", JPATH_ADMINISTRATOR) - || $lang->load("plg_editors_$value.sys", JPATH_PLUGINS . '/editors/' . $value); - $lang->load($title . '.sys'); - - return Text::_($title); - } - else - { - return static::value(''); - } - } - } + /** + * Display an image. + * + * @param string $src The source of the image + * + * @return string A element if the specified file exists, otherwise, a null string + * + * @since 2.5 + * @throws \Exception + */ + public function image($src) + { + $src = preg_replace('#[^A-Z0-9\-_\./]#i', '', $src); + $file = JPATH_SITE . '/' . $src; + + Path::check($file); + + if (!file_exists($file)) { + return ''; + } + + return ''; + } + + /** + * Displays an icon to add a note for this user. + * + * @param integer $userId The user ID + * + * @return string A link to add a note + * + * @since 2.5 + */ + public function addNote($userId) + { + $title = Text::_('COM_USERS_ADD_NOTE'); + + return '' . $title . ''; + } + + /** + * Displays an icon to filter the notes list on this user. + * + * @param integer $count The number of notes for the user + * @param integer $userId The user ID + * + * @return string A link to apply a filter + * + * @since 2.5 + */ + public function filterNotes($count, $userId) + { + if (empty($count)) { + return ''; + } + + $title = Text::_('COM_USERS_FILTER_NOTES'); + + return '' . $title . ''; + } + + /** + * Displays a note icon. + * + * @param integer $count The number of notes for the user + * @param integer $userId The user ID + * + * @return string A link to a modal window with the user notes + * + * @since 2.5 + */ + public function notes($count, $userId) + { + if (empty($count)) { + return ''; + } + + $title = Text::plural('COM_USERS_N_USER_NOTES', $count); + + return ''; + } + + /** + * Renders the modal html. + * + * @param integer $count The number of notes for the user + * @param integer $userId The user ID + * + * @return string The html for the rendered modal + * + * @since 3.4.1 + */ + public function notesModal($count, $userId) + { + if (empty($count)) { + return ''; + } + + $title = Text::plural('COM_USERS_N_USER_NOTES', $count); + $footer = ''; + + return HTMLHelper::_( + 'bootstrap.renderModal', + 'userModal_' . (int) $userId, + array( + 'title' => $title, + 'backdrop' => 'static', + 'keyboard' => true, + 'closeButton' => true, + 'footer' => $footer, + 'url' => Route::_('index.php?option=com_users&view=notes&tmpl=component&layout=modal&filter[user_id]=' . (int) $userId), + 'height' => '300px', + 'width' => '800px', + ) + ); + } + + /** + * Build an array of block/unblock user states to be used by jgrid.state, + * State options will be different for any user + * and for currently logged in user + * + * @param boolean $self True if state array is for currently logged in user + * + * @return array a list of possible states to display + * + * @since 3.0 + */ + public function blockStates($self = false) + { + if ($self) { + $states = array( + 1 => array( + 'task' => 'unblock', + 'text' => '', + 'active_title' => 'COM_USERS_TOOLBAR_BLOCK', + 'inactive_title' => '', + 'tip' => true, + 'active_class' => 'unpublish', + 'inactive_class' => 'unpublish', + ), + 0 => array( + 'task' => 'block', + 'text' => '', + 'active_title' => '', + 'inactive_title' => 'COM_USERS_USERS_ERROR_CANNOT_BLOCK_SELF', + 'tip' => true, + 'active_class' => 'publish', + 'inactive_class' => 'publish', + ) + ); + } else { + $states = array( + 1 => array( + 'task' => 'unblock', + 'text' => '', + 'active_title' => 'COM_USERS_TOOLBAR_UNBLOCK', + 'inactive_title' => '', + 'tip' => true, + 'active_class' => 'unpublish', + 'inactive_class' => 'unpublish', + ), + 0 => array( + 'task' => 'block', + 'text' => '', + 'active_title' => 'COM_USERS_TOOLBAR_BLOCK', + 'inactive_title' => '', + 'tip' => true, + 'active_class' => 'publish', + 'inactive_class' => 'publish', + ) + ); + } + + return $states; + } + + /** + * Build an array of activate states to be used by jgrid.state, + * + * @return array a list of possible states to display + * + * @since 3.0 + */ + public function activateStates() + { + $states = array( + 1 => array( + 'task' => 'activate', + 'text' => '', + 'active_title' => 'COM_USERS_TOOLBAR_ACTIVATE', + 'inactive_title' => '', + 'tip' => true, + 'active_class' => 'unpublish', + 'inactive_class' => 'unpublish', + ), + 0 => array( + 'task' => '', + 'text' => '', + 'active_title' => '', + 'inactive_title' => 'COM_USERS_ACTIVATED', + 'tip' => true, + 'active_class' => 'publish', + 'inactive_class' => 'publish', + ) + ); + + return $states; + } + + /** + * Get the sanitized value + * + * @param mixed $value Value of the field + * + * @return mixed String/void + * + * @since 1.6 + */ + public function value($value) + { + if (is_string($value)) { + $value = trim($value); + } + + if (empty($value)) { + return Text::_('COM_USERS_PROFILE_VALUE_NOT_FOUND'); + } elseif (!is_array($value)) { + return htmlspecialchars($value, ENT_COMPAT, 'UTF-8'); + } + } + + /** + * Get the space symbol + * + * @param mixed $value Value of the field + * + * @return string + * + * @since 1.6 + */ + public function spacer($value) + { + return ''; + } + + /** + * Get the sanitized template style + * + * @param mixed $value Value of the field + * + * @return mixed String/void + * + * @since 1.6 + */ + public function templatestyle($value) + { + if (empty($value)) { + return static::value($value); + } else { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__template_styles')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $value, ParameterType::INTEGER); + $db->setQuery($query); + $title = $db->loadResult(); + + if ($title) { + return htmlspecialchars($title, ENT_COMPAT, 'UTF-8'); + } else { + return static::value(''); + } + } + } + + /** + * Get the sanitized language + * + * @param mixed $value Value of the field + * + * @return mixed String/void + * + * @since 1.6 + */ + public function admin_language($value) + { + if (!$value) { + return static::value($value); + } + + $path = LanguageHelper::getLanguagePath(JPATH_ADMINISTRATOR, $value); + $file = $path . '/langmetadata.xml'; + + if (!is_file($file)) { + // For language packs from before 4.0. + $file = $path . '/' . $value . '.xml'; + + if (!is_file($file)) { + return static::value($value); + } + } + + $result = LanguageHelper::parseXMLLanguageFile($file); + + if ($result) { + return htmlspecialchars($result['name'], ENT_COMPAT, 'UTF-8'); + } + + return static::value($value); + } + + /** + * Get the sanitized language + * + * @param mixed $value Value of the field + * + * @return mixed String/void + * + * @since 1.6 + */ + public function language($value) + { + if (!$value) { + return static::value($value); + } + + $path = LanguageHelper::getLanguagePath(JPATH_SITE, $value); + $file = $path . '/langmetadata.xml'; + + if (!is_file($file)) { + // For language packs from before 4.0. + $file = $path . '/' . $value . '.xml'; + + if (!is_file($file)) { + return static::value($value); + } + } + + $result = LanguageHelper::parseXMLLanguageFile($file); + + if ($result) { + return htmlspecialchars($result['name'], ENT_COMPAT, 'UTF-8'); + } + + return static::value($value); + } + + /** + * Get the sanitized editor name + * + * @param mixed $value Value of the field + * + * @return mixed String/void + * + * @since 1.6 + */ + public function editor($value) + { + if (empty($value)) { + return static::value($value); + } else { + $db = Factory::getDbo(); + $lang = Factory::getLanguage(); + $query = $db->getQuery(true) + ->select($db->quoteName('name')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' = :element') + ->where($db->quoteName('folder') . ' = ' . $db->quote('editors')) + ->bind(':element', $value); + $db->setQuery($query); + $title = $db->loadResult(); + + if ($title) { + $lang->load("plg_editors_$value.sys", JPATH_ADMINISTRATOR) + || $lang->load("plg_editors_$value.sys", JPATH_PLUGINS . '/editors/' . $value); + $lang->load($title . '.sys'); + + return Text::_($title); + } else { + return static::value(''); + } + } + } } diff --git a/administrator/components/com_users/src/Table/MfaTable.php b/administrator/components/com_users/src/Table/MfaTable.php index 4e201e279c051..324e175f4e093 100644 --- a/administrator/components/com_users/src/Table/MfaTable.php +++ b/administrator/components/com_users/src/Table/MfaTable.php @@ -1,4 +1,5 @@ encryptService = new Encrypt; - } - - /** - * Method to store a row in the database from the Table instance properties. - * - * If a primary key value is set the row with that primary key value will be updated with the instance property values. - * If no primary key value is set a new row will be inserted into the database with the properties from the Table instance. - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return boolean True on success. - * - * @since 4.2.0 - */ - public function store($updateNulls = true) - { - // Encrypt the options before saving them - $this->options = $this->encryptService->encrypt(json_encode($this->options ?: [])); - - // Set last_used date to null if empty or zero date + /** + * Table constructor + * + * @param DatabaseDriver $db Database driver object + * @param DispatcherInterface|null $dispatcher Events dispatcher object + * + * @since 4.2.0 + */ + public function __construct(DatabaseDriver $db, DispatcherInterface $dispatcher = null) + { + parent::__construct('#__user_mfa', 'id', $db, $dispatcher); + + $this->encryptService = new Encrypt(); + } + + /** + * Method to store a row in the database from the Table instance properties. + * + * If a primary key value is set the row with that primary key value will be updated with the instance property values. + * If no primary key value is set a new row will be inserted into the database with the properties from the Table instance. + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return boolean True on success. + * + * @since 4.2.0 + */ + public function store($updateNulls = true) + { + // Encrypt the options before saving them + $this->options = $this->encryptService->encrypt(json_encode($this->options ?: [])); + + // Set last_used date to null if empty or zero date // phpcs:ignore if (!((int) $this->last_used)) - { + { // phpcs:ignore $this->last_used = null; - } + } // phpcs:ignore $records = MfaHelper::getUserMfaRecords($this->user_id); - if ($this->id) - { - // Existing record. Remove it from the list of records. - $records = array_filter( - $records, - function ($rec) { - return $rec->id != $this->id; - } - ); - } - - // Update the dates on a new record - if (empty($this->id)) - { + if ($this->id) { + // Existing record. Remove it from the list of records. + $records = array_filter( + $records, + function ($rec) { + return $rec->id != $this->id; + } + ); + } + + // Update the dates on a new record + if (empty($this->id)) { // phpcs:ignore $this->created_on = Date::getInstance()->toSql(); // phpcs:ignore $this->last_used = null; - } - - // Do I need to mark this record as the default? - if ($this->default == 0) - { - $hasDefaultRecord = array_reduce( - $records, - function ($carry, $record) - { - return $carry || ($record->default == 1); - }, - false - ); - - $this->default = $hasDefaultRecord ? 0 : 1; - } - - // Let's find out if we are saving a new MFA method record without having backup codes yet. - $mustCreateBackupCodes = false; - - if (empty($this->id) && $this->method !== 'backupcodes') - { - // Do I have any backup records? - $hasBackupCodes = array_reduce( - $records, - function (bool $carry, $record) - { - return $carry || $record->method === 'backupcodes'; - }, - false - ); - - $mustCreateBackupCodes = !$hasBackupCodes; - - // If the only other entry is the backup records one I need to make this the default method - if ($hasBackupCodes && count($records) === 1) - { - $this->default = 1; - } - } - - // Store the record - try - { - $result = parent::store($updateNulls); - } - catch (Throwable $e) - { - $this->setError($e->getMessage()); - - $result = false; - } - - // Decrypt the options (they must be decrypted in memory) - $this->decryptOptions(); - - if ($result) - { - // If this record is the default unset the default flag from all other records - $this->switchDefaultRecord(); - - // Do I need to generate backup codes? - if ($mustCreateBackupCodes) - { - $this->generateBackupCodes(); - } - } - - return $result; - } - - /** - * Method to load a row from the database by primary key and bind the fields to the Table instance properties. - * - * @param mixed $keys An optional primary key value to load the row by, or an array of fields to match. - * If not set the instance property value is used. - * @param boolean $reset True to reset the default values before loading the new row. - * - * @return boolean True if successful. False if row not found. - * - * @since 4.2.0 - * @throws \InvalidArgumentException - * @throws RuntimeException - * @throws \UnexpectedValueException - */ - public function load($keys = null, $reset = true) - { - $result = parent::load($keys, $reset); - - if ($result) - { - $this->decryptOptions(); - } - - return $result; - } - - /** - * Method to delete a row from the database table by primary key value. - * - * @param mixed $pk An optional primary key value to delete. If not set the instance property value is used. - * - * @return boolean True on success. - * - * @since 4.2.0 - * @throws \UnexpectedValueException - */ - public function delete($pk = null) - { - $record = $this; - - if ($pk != $this->id) - { - $record = clone $this; - $record->reset(); - $result = $record->load($pk); - - if (!$result) - { - // If the record does not exist I will stomp my feet and deny your request - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - } - - $user = Factory::getApplication()->getIdentity() - ?? Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - - // The user must be a registered user, not a guest - if ($user->guest) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - // Save flags used onAfterDelete - $this->deleteFlags[$record->id] = [ - 'default' => $record->default, + } + + // Do I need to mark this record as the default? + if ($this->default == 0) { + $hasDefaultRecord = array_reduce( + $records, + function ($carry, $record) { + return $carry || ($record->default == 1); + }, + false + ); + + $this->default = $hasDefaultRecord ? 0 : 1; + } + + // Let's find out if we are saving a new MFA method record without having backup codes yet. + $mustCreateBackupCodes = false; + + if (empty($this->id) && $this->method !== 'backupcodes') { + // Do I have any backup records? + $hasBackupCodes = array_reduce( + $records, + function (bool $carry, $record) { + return $carry || $record->method === 'backupcodes'; + }, + false + ); + + $mustCreateBackupCodes = !$hasBackupCodes; + + // If the only other entry is the backup records one I need to make this the default method + if ($hasBackupCodes && count($records) === 1) { + $this->default = 1; + } + } + + // Store the record + try { + $result = parent::store($updateNulls); + } catch (Throwable $e) { + $this->setError($e->getMessage()); + + $result = false; + } + + // Decrypt the options (they must be decrypted in memory) + $this->decryptOptions(); + + if ($result) { + // If this record is the default unset the default flag from all other records + $this->switchDefaultRecord(); + + // Do I need to generate backup codes? + if ($mustCreateBackupCodes) { + $this->generateBackupCodes(); + } + } + + return $result; + } + + /** + * Method to load a row from the database by primary key and bind the fields to the Table instance properties. + * + * @param mixed $keys An optional primary key value to load the row by, or an array of fields to match. + * If not set the instance property value is used. + * @param boolean $reset True to reset the default values before loading the new row. + * + * @return boolean True if successful. False if row not found. + * + * @since 4.2.0 + * @throws \InvalidArgumentException + * @throws RuntimeException + * @throws \UnexpectedValueException + */ + public function load($keys = null, $reset = true) + { + $result = parent::load($keys, $reset); + + if ($result) { + $this->decryptOptions(); + } + + return $result; + } + + /** + * Method to delete a row from the database table by primary key value. + * + * @param mixed $pk An optional primary key value to delete. If not set the instance property value is used. + * + * @return boolean True on success. + * + * @since 4.2.0 + * @throws \UnexpectedValueException + */ + public function delete($pk = null) + { + $record = $this; + + if ($pk != $this->id) { + $record = clone $this; + $record->reset(); + $result = $record->load($pk); + + if (!$result) { + // If the record does not exist I will stomp my feet and deny your request + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + } + + $user = Factory::getApplication()->getIdentity() + ?? Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + // The user must be a registered user, not a guest + if ($user->guest) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + // Save flags used onAfterDelete + $this->deleteFlags[$record->id] = [ + 'default' => $record->default, // phpcs:ignore 'numRecords' => $this->getNumRecords($record->user_id), // phpcs:ignore 'user_id' => $record->user_id, - 'method' => $record->method, - ]; + 'method' => $record->method, + ]; - if (\is_null($pk)) - { + if (\is_null($pk)) { // phpcs:ignore $pk = [$this->_tbl_key => $this->id]; - } - elseif (!\is_array($pk)) - { + } elseif (!\is_array($pk)) { // phpcs:ignore $pk = [$this->_tbl_key => $pk]; - } - - $isDeleted = parent::delete($pk); - - if ($isDeleted) - { - $this->afterDelete($pk); - } - - return $isDeleted; - } - - /** - * Decrypt the possibly encrypted options - * - * @return void - * @since 4.2.0 - */ - private function decryptOptions(): void - { - // Try with modern decryption - $decrypted = @json_decode($this->encryptService->decrypt($this->options ?? ''), true); - - if (is_string($decrypted)) - { - $decrypted = @json_decode($decrypted, true); - } - - // Fall back to legacy decryption - if (!is_array($decrypted)) - { - $decrypted = @json_decode($this->encryptService->decrypt($this->options ?? '', true), true); - - if (is_string($decrypted)) - { - $decrypted = @json_decode($decrypted, true); - } - } - - $this->options = $decrypted ?: []; - } - - /** - * If this record is set to be the default, unset the default flag from the other records for the same user. - * - * @return void - * @since 4.2.0 - */ - private function switchDefaultRecord(): void - { - if (!$this->default) - { - return; - } - - /** - * This record is marked as default, therefore we need to unset the default flag from all other records for this - * user. - */ - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->update($db->quoteName('#__user_mfa')) - ->set($db->quoteName('default') . ' = 0') - ->where($db->quoteName('user_id') . ' = :user_id') - ->where($db->quoteName('id') . ' != :id') + } + + $isDeleted = parent::delete($pk); + + if ($isDeleted) { + $this->afterDelete($pk); + } + + return $isDeleted; + } + + /** + * Decrypt the possibly encrypted options + * + * @return void + * @since 4.2.0 + */ + private function decryptOptions(): void + { + // Try with modern decryption + $decrypted = @json_decode($this->encryptService->decrypt($this->options ?? ''), true); + + if (is_string($decrypted)) { + $decrypted = @json_decode($decrypted, true); + } + + // Fall back to legacy decryption + if (!is_array($decrypted)) { + $decrypted = @json_decode($this->encryptService->decrypt($this->options ?? '', true), true); + + if (is_string($decrypted)) { + $decrypted = @json_decode($decrypted, true); + } + } + + $this->options = $decrypted ?: []; + } + + /** + * If this record is set to be the default, unset the default flag from the other records for the same user. + * + * @return void + * @since 4.2.0 + */ + private function switchDefaultRecord(): void + { + if (!$this->default) { + return; + } + + /** + * This record is marked as default, therefore we need to unset the default flag from all other records for this + * user. + */ + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->update($db->quoteName('#__user_mfa')) + ->set($db->quoteName('default') . ' = 0') + ->where($db->quoteName('user_id') . ' = :user_id') + ->where($db->quoteName('id') . ' != :id') // phpcs:ignore ->bind(':user_id', $this->user_id, ParameterType::INTEGER) - ->bind(':id', $this->id, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - } - - /** - * Regenerate backup code is the flag is set. - * - * @return void - * @throws Exception - * @since 4.2.0 - */ - private function generateBackupCodes(): void - { - /** @var MVCFactoryInterface $factory */ - $factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory(); - - /** @var BackupcodesModel $backupCodes */ - $backupCodes = $factory->createModel('Backupcodes', 'Administrator'); + ->bind(':id', $this->id, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + } + + /** + * Regenerate backup code is the flag is set. + * + * @return void + * @throws Exception + * @since 4.2.0 + */ + private function generateBackupCodes(): void + { + /** @var MVCFactoryInterface $factory */ + $factory = Factory::getApplication()->bootComponent('com_users')->getMVCFactory(); + + /** @var BackupcodesModel $backupCodes */ + $backupCodes = $factory->createModel('Backupcodes', 'Administrator'); // phpcs:ignore $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($this->user_id); - $backupCodes->regenerateBackupCodes($user); - } - - /** - * Runs after successfully deleting a record - * - * @param int|array $pk The promary key of the deleted record - * - * @return void - * @since 4.2.0 - */ - private function afterDelete($pk): void - { - if (is_array($pk)) - { + $backupCodes->regenerateBackupCodes($user); + } + + /** + * Runs after successfully deleting a record + * + * @param int|array $pk The promary key of the deleted record + * + * @return void + * @since 4.2.0 + */ + private function afterDelete($pk): void + { + if (is_array($pk)) { // phpcs:ignore $pk = $pk[$this->_tbl_key] ?? array_shift($pk); - } - - if (!isset($this->deleteFlags[$pk])) - { - return; - } - - if (($this->deleteFlags[$pk]['numRecords'] <= 2) && ($this->deleteFlags[$pk]['method'] != 'backupcodes')) - { - /** - * This was the second to last MFA record in the database (the last one is the `backupcodes`). Therefore, we - * need to delete the remaining entry and go away. We don't trigger this if the Method we are deleting was - * the `backupcodes` because we might just be regenerating the backup codes. - */ - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->delete($db->quoteName('#__user_mfa')) - ->where($db->quoteName('user_id') . ' = :user_id') - ->bind(':user_id', $this->deleteFlags[$pk]['user_id'], ParameterType::INTEGER); - $db->setQuery($query)->execute(); - - unset($this->deleteFlags[$pk]); - - return; - } - - // This was the default record. Promote the next available record to default. - if ($this->deleteFlags[$pk]['default']) - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__user_mfa')) - ->where($db->quoteName('user_id') . ' = :user_id') - ->where($db->quoteName('method') . ' != ' . $db->quote('backupcodes')) - ->bind(':user_id', $this->deleteFlags[$pk]['user_id'], ParameterType::INTEGER); - $ids = $db->setQuery($query)->loadColumn(); - - if (empty($ids)) - { - return; - } - - $id = array_shift($ids); - $query = $db->getQuery(true) - ->update($db->quoteName('#__user_mfa')) - ->set($db->quoteName('default') . ' = 1') - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $db->setQuery($query)->execute(); - } - } - - /** - * Get the number of MFA records for a give user ID - * - * @param int $userId The user ID to check - * - * @return integer - * - * @since 4.2.0 - */ - private function getNumRecords(int $userId): int - { - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select('COUNT(*)') - ->from($db->quoteName('#__user_mfa')) - ->where($db->quoteName('user_id') . ' = :user_id') - ->bind(':user_id', $userId, ParameterType::INTEGER); - $numOldRecords = $db->setQuery($query)->loadResult(); - - return (int) $numOldRecords; - } + } + + if (!isset($this->deleteFlags[$pk])) { + return; + } + + if (($this->deleteFlags[$pk]['numRecords'] <= 2) && ($this->deleteFlags[$pk]['method'] != 'backupcodes')) { + /** + * This was the second to last MFA record in the database (the last one is the `backupcodes`). Therefore, we + * need to delete the remaining entry and go away. We don't trigger this if the Method we are deleting was + * the `backupcodes` because we might just be regenerating the backup codes. + */ + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__user_mfa')) + ->where($db->quoteName('user_id') . ' = :user_id') + ->bind(':user_id', $this->deleteFlags[$pk]['user_id'], ParameterType::INTEGER); + $db->setQuery($query)->execute(); + + unset($this->deleteFlags[$pk]); + + return; + } + + // This was the default record. Promote the next available record to default. + if ($this->deleteFlags[$pk]['default']) { + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__user_mfa')) + ->where($db->quoteName('user_id') . ' = :user_id') + ->where($db->quoteName('method') . ' != ' . $db->quote('backupcodes')) + ->bind(':user_id', $this->deleteFlags[$pk]['user_id'], ParameterType::INTEGER); + $ids = $db->setQuery($query)->loadColumn(); + + if (empty($ids)) { + return; + } + + $id = array_shift($ids); + $query = $db->getQuery(true) + ->update($db->quoteName('#__user_mfa')) + ->set($db->quoteName('default') . ' = 1') + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $db->setQuery($query)->execute(); + } + } + + /** + * Get the number of MFA records for a give user ID + * + * @param int $userId The user ID to check + * + * @return integer + * + * @since 4.2.0 + */ + private function getNumRecords(int $userId): int + { + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__user_mfa')) + ->where($db->quoteName('user_id') . ' = :user_id') + ->bind(':user_id', $userId, ParameterType::INTEGER); + $numOldRecords = $db->setQuery($query)->loadResult(); + + return (int) $numOldRecords; + } } diff --git a/administrator/components/com_users/src/Table/NoteTable.php b/administrator/components/com_users/src/Table/NoteTable.php index b7b22b26a5a87..72744912d5a4e 100644 --- a/administrator/components/com_users/src/Table/NoteTable.php +++ b/administrator/components/com_users/src/Table/NoteTable.php @@ -1,4 +1,5 @@ typeAlias = 'com_users.note'; - parent::__construct('#__user_notes', 'id', $db); - - $this->setColumnAlias('published', 'state'); - } - - /** - * Overloaded store method for the notes table. - * - * @param boolean $updateNulls Toggle whether null values should be updated. - * - * @return boolean True on success, false on failure. - * - * @since 2.5 - */ - public function store($updateNulls = true) - { - $date = Factory::getDate()->toSql(); - $userId = Factory::getUser()->get('id'); - - if (!((int) $this->review_time)) - { - $this->review_time = null; - } - - if ($this->id) - { - // Existing item - $this->modified_time = $date; - $this->modified_user_id = $userId; - } - else - { - // New record. - $this->created_time = $date; - $this->created_user_id = $userId; - $this->modified_time = $date; - $this->modified_user_id = $userId; - } - - // Attempt to store the data. - return parent::store($updateNulls); - } - - /** - * Method to perform sanity checks on the Table instance properties to ensure they are safe to store in the database. - * - * @return boolean True if the instance is sane and able to be stored in the database. - * - * @since 4.0.0 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (empty($this->modified_time)) - { - $this->modified_time = $this->created_time; - } - - if (empty($this->modified_user_id)) - { - $this->modified_user_id = $this->created_user_id; - } - - return true; - } - - /** - * Get the type alias for the history table - * - * @return string The alias as described above - * - * @since 4.0.0 - */ - public function getTypeAlias() - { - return $this->typeAlias; - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * Constructor + * + * @param DatabaseDriver $db Database object + * + * @since 2.5 + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = 'com_users.note'; + parent::__construct('#__user_notes', 'id', $db); + + $this->setColumnAlias('published', 'state'); + } + + /** + * Overloaded store method for the notes table. + * + * @param boolean $updateNulls Toggle whether null values should be updated. + * + * @return boolean True on success, false on failure. + * + * @since 2.5 + */ + public function store($updateNulls = true) + { + $date = Factory::getDate()->toSql(); + $userId = Factory::getUser()->get('id'); + + if (!((int) $this->review_time)) { + $this->review_time = null; + } + + if ($this->id) { + // Existing item + $this->modified_time = $date; + $this->modified_user_id = $userId; + } else { + // New record. + $this->created_time = $date; + $this->created_user_id = $userId; + $this->modified_time = $date; + $this->modified_user_id = $userId; + } + + // Attempt to store the data. + return parent::store($updateNulls); + } + + /** + * Method to perform sanity checks on the Table instance properties to ensure they are safe to store in the database. + * + * @return boolean True if the instance is sane and able to be stored in the database. + * + * @since 4.0.0 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (empty($this->modified_time)) { + $this->modified_time = $this->created_time; + } + + if (empty($this->modified_user_id)) { + $this->modified_user_id = $this->created_user_id; + } + + return true; + } + + /** + * Get the type alias for the history table + * + * @return string The alias as described above + * + * @since 4.0.0 + */ + public function getTypeAlias() + { + return $this->typeAlias; + } } diff --git a/administrator/components/com_users/src/View/Captive/HtmlView.php b/administrator/components/com_users/src/View/Captive/HtmlView.php index 739de5d1e5b7d..a569c5b6ff346 100644 --- a/administrator/components/com_users/src/View/Captive/HtmlView.php +++ b/administrator/components/com_users/src/View/Captive/HtmlView.php @@ -1,4 +1,5 @@ setSiteTemplateStyle(); - - $app = Factory::getApplication(); - $user = Factory::getApplication()->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - - PluginHelper::importPlugin('multifactorauth'); - $event = new BeforeDisplayMethods($user); - $app->getDispatcher()->dispatch($event->getName(), $event); - - /** @var CaptiveModel $model */ - $model = $this->getModel(); - - // Load data from the model - $this->isAdmin = $app->isClient('administrator'); - $this->records = $this->get('records'); - $this->record = $this->get('record'); - $this->mfaMethods = MfaHelper::getMfaMethods(); - - if (!empty($this->records)) - { - /** @var BackupcodesModel $codesModel */ - $codesModel = $this->getModel('Backupcodes'); - $backupCodesRecord = $codesModel->getBackupCodesRecord(); - - if (!is_null($backupCodesRecord)) - { - $backupCodesRecord->title = Text::_('COM_USERS_USER_BACKUPCODES'); - $this->records[] = $backupCodesRecord; - } - } - - // If we only have one record there's no point asking the user to select a MFA Method - if (empty($this->record) && !empty($this->records)) - { - // Default to the first record - $this->record = reset($this->records); - - // If we have multiple records try to make this record the default - if (count($this->records) > 1) - { - foreach ($this->records as $record) - { - if ($record->default) - { - $this->record = $record; - - break; - } - } - } - } - - // Set the correct layout based on the availability of a MFA record - $this->setLayout('default'); - - // If we have no record selected or explicitly asked to run the 'select' task use the correct layout - if (is_null($this->record) || ($model->getState('task') == 'select')) - { - $this->setLayout('select'); - } - - switch ($this->getLayout()) - { - case 'select': - $this->allowEntryBatching = 1; - - $event = new NotifyActionLog('onComUsersCaptiveShowSelect', []); - Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); - break; - - case 'default': - default: - $this->renderOptions = $model->loadCaptiveRenderOptions($this->record); - $this->allowEntryBatching = $this->renderOptions['allowEntryBatching'] ?? 0; - - $event = new NotifyActionLog( - 'onComUsersCaptiveShowCaptive', - [ - $this->escape($this->record->title), - ] - ); - Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); - break; - } - - // Which title should I use for the page? - $this->title = $this->get('PageTitle'); - - // Back-end: always show a title in the 'title' module position, not in the page body - if ($this->isAdmin) - { - ToolbarHelper::title(Text::_('COM_USERS_HEADING_MFA'), 'users user-lock'); - $this->title = ''; - } - - if ($this->isAdmin && $this->getLayout() === 'default') - { - $bar = Toolbar::getInstance(); - $button = (new BasicButton('user-mfa-submit')) - ->text($this->renderOptions['submit_text']) - ->icon($this->renderOptions['submit_icon']); - $bar->appendButton($button); - - $button = (new BasicButton('user-mfa-logout')) - ->text('COM_USERS_MFA_LOGOUT') - ->buttonClass('btn btn-danger') - ->icon('icon icon-lock'); - $bar->appendButton($button); - - if (count($this->records) > 1) - { - $arrow = Factory::getApplication()->getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; - $button = (new BasicButton('user-mfa-choose-another')) - ->text('COM_USERS_MFA_USE_DIFFERENT_METHOD') - ->icon('icon-' . $arrow); - $bar->appendButton($button); - } - } - - // Display the view - parent::display($tpl); - } + use SiteTemplateTrait; + + /** + * The MFA Method records for the current user which correspond to enabled plugins + * + * @var array + * @since 4.2.0 + */ + public $records = []; + + /** + * The currently selected MFA Method record against which we'll be authenticating + * + * @var null|stdClass + * @since 4.2.0 + */ + public $record = null; + + /** + * The Captive MFA page's rendering options + * + * @var array|null + * @since 4.2.0 + */ + public $renderOptions = null; + + /** + * The title to display at the top of the page + * + * @var string + * @since 4.2.0 + */ + public $title = ''; + + /** + * Is this an administrator page? + * + * @var boolean + * @since 4.2.0 + */ + public $isAdmin = false; + + /** + * Does the currently selected Method allow authenticating against all of its records? + * + * @var boolean + * @since 4.2.0 + */ + public $allowEntryBatching = false; + + /** + * All enabled MFA Methods (plugins) + * + * @var array + * @since 4.2.0 + */ + public $mfaMethods; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void A string if successful, otherwise an Error object. + * + * @throws Exception + * @since 4.2.0 + */ + public function display($tpl = null) + { + $this->setSiteTemplateStyle(); + + $app = Factory::getApplication(); + $user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + + PluginHelper::importPlugin('multifactorauth'); + $event = new BeforeDisplayMethods($user); + $app->getDispatcher()->dispatch($event->getName(), $event); + + /** @var CaptiveModel $model */ + $model = $this->getModel(); + + // Load data from the model + $this->isAdmin = $app->isClient('administrator'); + $this->records = $this->get('records'); + $this->record = $this->get('record'); + $this->mfaMethods = MfaHelper::getMfaMethods(); + + if (!empty($this->records)) { + /** @var BackupcodesModel $codesModel */ + $codesModel = $this->getModel('Backupcodes'); + $backupCodesRecord = $codesModel->getBackupCodesRecord(); + + if (!is_null($backupCodesRecord)) { + $backupCodesRecord->title = Text::_('COM_USERS_USER_BACKUPCODES'); + $this->records[] = $backupCodesRecord; + } + } + + // If we only have one record there's no point asking the user to select a MFA Method + if (empty($this->record) && !empty($this->records)) { + // Default to the first record + $this->record = reset($this->records); + + // If we have multiple records try to make this record the default + if (count($this->records) > 1) { + foreach ($this->records as $record) { + if ($record->default) { + $this->record = $record; + + break; + } + } + } + } + + // Set the correct layout based on the availability of a MFA record + $this->setLayout('default'); + + // If we have no record selected or explicitly asked to run the 'select' task use the correct layout + if (is_null($this->record) || ($model->getState('task') == 'select')) { + $this->setLayout('select'); + } + + switch ($this->getLayout()) { + case 'select': + $this->allowEntryBatching = 1; + + $event = new NotifyActionLog('onComUsersCaptiveShowSelect', []); + Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); + break; + + case 'default': + default: + $this->renderOptions = $model->loadCaptiveRenderOptions($this->record); + $this->allowEntryBatching = $this->renderOptions['allowEntryBatching'] ?? 0; + + $event = new NotifyActionLog( + 'onComUsersCaptiveShowCaptive', + [ + $this->escape($this->record->title), + ] + ); + Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); + break; + } + + // Which title should I use for the page? + $this->title = $this->get('PageTitle'); + + // Back-end: always show a title in the 'title' module position, not in the page body + if ($this->isAdmin) { + ToolbarHelper::title(Text::_('COM_USERS_HEADING_MFA'), 'users user-lock'); + $this->title = ''; + } + + if ($this->isAdmin && $this->getLayout() === 'default') { + $bar = Toolbar::getInstance(); + $button = (new BasicButton('user-mfa-submit')) + ->text($this->renderOptions['submit_text']) + ->icon($this->renderOptions['submit_icon']); + $bar->appendButton($button); + + $button = (new BasicButton('user-mfa-logout')) + ->text('COM_USERS_MFA_LOGOUT') + ->buttonClass('btn btn-danger') + ->icon('icon icon-lock'); + $bar->appendButton($button); + + if (count($this->records) > 1) { + $arrow = Factory::getApplication()->getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; + $button = (new BasicButton('user-mfa-choose-another')) + ->text('COM_USERS_MFA_USE_DIFFERENT_METHOD') + ->icon('icon-' . $arrow); + $bar->appendButton($button); + } + } + + // Display the view + parent::display($tpl); + } } diff --git a/administrator/components/com_users/src/View/Debuggroup/HtmlView.php b/administrator/components/com_users/src/View/Debuggroup/HtmlView.php index 77dc0693c26fc..6b4c2d8b43002 100644 --- a/administrator/components/com_users/src/View/Debuggroup/HtmlView.php +++ b/administrator/components/com_users/src/View/Debuggroup/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser()->authorise('core.manage', 'com_users')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - $this->actions = $this->get('DebugActions'); - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->group = $this->get('Group'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_users'); - - ToolbarHelper::title(Text::sprintf('COM_USERS_VIEW_DEBUG_GROUP_TITLE', $this->group->id, $this->escape($this->group->title)), 'users groups'); - ToolbarHelper::cancel('group.cancel', 'JTOOLBAR_CLOSE'); - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_users'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Permissions_for_Group'); - } + /** + * List of component actions + * + * @var array + */ + protected $actions; + + /** + * The item data. + * + * @var object + * @since 1.6 + */ + protected $items; + + /** + * The pagination object. + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state. + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * The id and title for the user group. + * + * @var \stdClass + * @since 4.0.0 + */ + protected $group; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + */ + public $activeFilters; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + // Access check. + if (!$this->getCurrentUser()->authorise('core.manage', 'com_users')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $this->actions = $this->get('DebugActions'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->group = $this->get('Group'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_users'); + + ToolbarHelper::title(Text::sprintf('COM_USERS_VIEW_DEBUG_GROUP_TITLE', $this->group->id, $this->escape($this->group->title)), 'users groups'); + ToolbarHelper::cancel('group.cancel', 'JTOOLBAR_CLOSE'); + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_users'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Permissions_for_Group'); + } } diff --git a/administrator/components/com_users/src/View/Debuguser/HtmlView.php b/administrator/components/com_users/src/View/Debuguser/HtmlView.php index 7d2c463949056..aca918d8a6b5a 100644 --- a/administrator/components/com_users/src/View/Debuguser/HtmlView.php +++ b/administrator/components/com_users/src/View/Debuguser/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser()->authorise('core.manage', 'com_users')) - { - throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - $this->actions = $this->get('DebugActions'); - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->user = $this->get('User'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_users'); - - ToolbarHelper::title(Text::sprintf('COM_USERS_VIEW_DEBUG_USER_TITLE', $this->user->id, $this->escape($this->user->name)), 'users user'); - ToolbarHelper::cancel('user.cancel', 'JTOOLBAR_CLOSE'); - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_users'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Permissions_for_User'); - } + /** + * List of component actions + * + * @var array + */ + protected $actions; + + /** + * The item data. + * + * @var object + * @since 1.6 + */ + protected $items; + + /** + * The pagination object. + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state. + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * The user object of the user being debugged. + * + * @var User + */ + protected $user; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + */ + public $activeFilters; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + // Access check. + if (!$this->getCurrentUser()->authorise('core.manage', 'com_users')) { + throw new NotAllowed(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + $this->actions = $this->get('DebugActions'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->user = $this->get('User'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_users'); + + ToolbarHelper::title(Text::sprintf('COM_USERS_VIEW_DEBUG_USER_TITLE', $this->user->id, $this->escape($this->user->name)), 'users user'); + ToolbarHelper::cancel('user.cancel', 'JTOOLBAR_CLOSE'); + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_users'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Permissions_for_User'); + } } diff --git a/administrator/components/com_users/src/View/Group/HtmlView.php b/administrator/components/com_users/src/View/Group/HtmlView.php index ab9e05db06366..f1454eb75cef4 100644 --- a/administrator/components/com_users/src/View/Group/HtmlView.php +++ b/administrator/components/com_users/src/View/Group/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->item = $this->get('Item'); - $this->form = $this->get('Form'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $isNew = ($this->item->id == 0); - $canDo = ContentHelper::getActions('com_users'); - - ToolbarHelper::title(Text::_($isNew ? 'COM_USERS_VIEW_NEW_GROUP_TITLE' : 'COM_USERS_VIEW_EDIT_GROUP_TITLE'), 'users-cog groups-add'); - - $toolbarButtons = []; - - if ($canDo->get('core.edit') || $canDo->get('core.create')) - { - ToolbarHelper::apply('group.apply'); - $toolbarButtons[] = ['save', 'group.save']; - } - - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'group.save2new']; - } - - // If an existing item, can save to a copy. - if (!$isNew && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'group.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('group.cancel'); - } - else - { - ToolbarHelper::cancel('group.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Users:_New_or_Edit_Group'); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The item data. + * + * @var object + * @since 1.6 + */ + protected $item; + + /** + * The model state. + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->item = $this->get('Item'); + $this->form = $this->get('Form'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $isNew = ($this->item->id == 0); + $canDo = ContentHelper::getActions('com_users'); + + ToolbarHelper::title(Text::_($isNew ? 'COM_USERS_VIEW_NEW_GROUP_TITLE' : 'COM_USERS_VIEW_EDIT_GROUP_TITLE'), 'users-cog groups-add'); + + $toolbarButtons = []; + + if ($canDo->get('core.edit') || $canDo->get('core.create')) { + ToolbarHelper::apply('group.apply'); + $toolbarButtons[] = ['save', 'group.save']; + } + + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'group.save2new']; + } + + // If an existing item, can save to a copy. + if (!$isNew && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'group.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('group.cancel'); + } else { + ToolbarHelper::cancel('group.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Users:_New_or_Edit_Group'); + } } diff --git a/administrator/components/com_users/src/View/Groups/HtmlView.php b/administrator/components/com_users/src/View/Groups/HtmlView.php index fe17b424c0bc9..d670ee1419cfd 100644 --- a/administrator/components/com_users/src/View/Groups/HtmlView.php +++ b/administrator/components/com_users/src/View/Groups/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_users'); - - ToolbarHelper::title(Text::_('COM_USERS_VIEW_GROUPS_TITLE'), 'users-cog groups'); - - if ($canDo->get('core.create')) - { - ToolbarHelper::addNew('group.add'); - } - - if ($canDo->get('core.delete')) - { - ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'groups.delete', 'JTOOLBAR_DELETE'); - ToolbarHelper::divider(); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_users'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Users:_Groups'); - } + /** + * The item data. + * + * @var object + * @since 1.6 + */ + protected $items; + + /** + * The pagination object. + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state. + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_users'); + + ToolbarHelper::title(Text::_('COM_USERS_VIEW_GROUPS_TITLE'), 'users-cog groups'); + + if ($canDo->get('core.create')) { + ToolbarHelper::addNew('group.add'); + } + + if ($canDo->get('core.delete')) { + ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'groups.delete', 'JTOOLBAR_DELETE'); + ToolbarHelper::divider(); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_users'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Users:_Groups'); + } } diff --git a/administrator/components/com_users/src/View/Level/HtmlView.php b/administrator/components/com_users/src/View/Level/HtmlView.php index 8d53c2c7d2ae3..1da02db23346b 100644 --- a/administrator/components/com_users/src/View/Level/HtmlView.php +++ b/administrator/components/com_users/src/View/Level/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->item = $this->get('Item'); - $this->state = $this->get('State'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $isNew = ($this->item->id == 0); - $canDo = ContentHelper::getActions('com_users'); - - ToolbarHelper::title(Text::_($isNew ? 'COM_USERS_VIEW_NEW_LEVEL_TITLE' : 'COM_USERS_VIEW_EDIT_LEVEL_TITLE'), 'user-lock levels-add'); - - $toolbarButtons = []; - - if ($canDo->get('core.edit') || $canDo->get('core.create')) - { - ToolbarHelper::apply('level.apply'); - $toolbarButtons[] = ['save', 'level.save']; - } - - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'level.save2new']; - } - - // If an existing item, can save to a copy. - if (!$isNew && $canDo->get('core.create')) - { - $toolbarButtons[] = ['save2copy', 'level.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('level.cancel'); - } - else - { - ToolbarHelper::cancel('level.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Users:_Edit_Viewing_Access_Level'); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The item data. + * + * @var object + * @since 1.6 + */ + protected $item; + + /** + * The model state. + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $isNew = ($this->item->id == 0); + $canDo = ContentHelper::getActions('com_users'); + + ToolbarHelper::title(Text::_($isNew ? 'COM_USERS_VIEW_NEW_LEVEL_TITLE' : 'COM_USERS_VIEW_EDIT_LEVEL_TITLE'), 'user-lock levels-add'); + + $toolbarButtons = []; + + if ($canDo->get('core.edit') || $canDo->get('core.create')) { + ToolbarHelper::apply('level.apply'); + $toolbarButtons[] = ['save', 'level.save']; + } + + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'level.save2new']; + } + + // If an existing item, can save to a copy. + if (!$isNew && $canDo->get('core.create')) { + $toolbarButtons[] = ['save2copy', 'level.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('level.cancel'); + } else { + ToolbarHelper::cancel('level.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Users:_Edit_Viewing_Access_Level'); + } } diff --git a/administrator/components/com_users/src/View/Levels/HtmlView.php b/administrator/components/com_users/src/View/Levels/HtmlView.php index 6c4429c6c64a7..93427db1bbaa8 100644 --- a/administrator/components/com_users/src/View/Levels/HtmlView.php +++ b/administrator/components/com_users/src/View/Levels/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_users'); - - ToolbarHelper::title(Text::_('COM_USERS_VIEW_LEVELS_TITLE'), 'user-lock levels'); - - if ($canDo->get('core.create')) - { - ToolbarHelper::addNew('level.add'); - } - - if ($canDo->get('core.delete')) - { - ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'level.delete', 'JTOOLBAR_DELETE'); - ToolbarHelper::divider(); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - ToolbarHelper::preferences('com_users'); - ToolbarHelper::divider(); - } - - ToolbarHelper::help('Users:_Viewing_Access_Levels'); - } + /** + * The item data. + * + * @var object + * @since 1.6 + */ + protected $items; + + /** + * The pagination object. + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state. + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_users'); + + ToolbarHelper::title(Text::_('COM_USERS_VIEW_LEVELS_TITLE'), 'user-lock levels'); + + if ($canDo->get('core.create')) { + ToolbarHelper::addNew('level.add'); + } + + if ($canDo->get('core.delete')) { + ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'level.delete', 'JTOOLBAR_DELETE'); + ToolbarHelper::divider(); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + ToolbarHelper::preferences('com_users'); + ToolbarHelper::divider(); + } + + ToolbarHelper::help('Users:_Viewing_Access_Levels'); + } } diff --git a/administrator/components/com_users/src/View/Mail/HtmlView.php b/administrator/components/com_users/src/View/Mail/HtmlView.php index beb275ee904b6..cdb336d463f7c 100644 --- a/administrator/components/com_users/src/View/Mail/HtmlView.php +++ b/administrator/components/com_users/src/View/Mail/HtmlView.php @@ -1,4 +1,5 @@ get('massmailoff', 0) == 1) - { - Factory::getApplication()->redirect(Route::_('index.php', false)); - } + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @throws \Exception + */ + public function display($tpl = null) + { + // Redirect to admin index if mass mailer disabled in conf + if (Factory::getApplication()->get('massmailoff', 0) == 1) { + Factory::getApplication()->redirect(Route::_('index.php', false)); + } - // Get data from the model - $this->form = $this->get('Form'); + // Get data from the model + $this->form = $this->get('Form'); - $this->addToolbar(); - parent::display($tpl); - } + $this->addToolbar(); + parent::display($tpl); + } - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); - ToolbarHelper::title(Text::_('COM_USERS_MASS_MAIL'), 'users massmail'); - ToolbarHelper::custom('mail.send', 'envelope', '', 'COM_USERS_TOOLBAR_MAIL_SEND_MAIL', false); - ToolbarHelper::cancel('mail.cancel'); - ToolbarHelper::divider(); - ToolbarHelper::preferences('com_users'); - ToolbarHelper::divider(); - ToolbarHelper::help('Mass_Mail_Users'); - } + ToolbarHelper::title(Text::_('COM_USERS_MASS_MAIL'), 'users massmail'); + ToolbarHelper::custom('mail.send', 'envelope', '', 'COM_USERS_TOOLBAR_MAIL_SEND_MAIL', false); + ToolbarHelper::cancel('mail.cancel'); + ToolbarHelper::divider(); + ToolbarHelper::preferences('com_users'); + ToolbarHelper::divider(); + ToolbarHelper::help('Mass_Mail_Users'); + } } diff --git a/administrator/components/com_users/src/View/Method/HtmlView.php b/administrator/components/com_users/src/View/Method/HtmlView.php index 450c7f19f1c79..bbdaf82728016 100644 --- a/administrator/components/com_users/src/View/Method/HtmlView.php +++ b/administrator/components/com_users/src/View/Method/HtmlView.php @@ -1,4 +1,5 @@ user)) - { - $this->user = Factory::getApplication()->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - /** @var MethodModel $model */ - $model = $this->getModel(); - $this->setLayout('edit'); - $this->renderOptions = $model->getRenderOptions($this->user); - $this->record = $model->getRecord($this->user); - $this->title = $model->getPageTitle(); - $this->isAdmin = $app->isClient('administrator'); - - // Backup codes are a special case, rendered with a special layout - if ($this->record->method == 'backupcodes') - { - $this->setLayout('backupcodes'); - - $backupCodes = $this->record->options; - - if (!is_array($backupCodes)) - { - $backupCodes = []; - } - - $backupCodes = array_filter( - $backupCodes, - function ($x) { - return !empty($x); - } - ); - - if (count($backupCodes) % 2 != 0) - { - $backupCodes[] = ''; - } - - /** - * The call to array_merge resets the array indices. This is necessary since array_filter kept the indices, - * meaning our elements are completely out of order. - */ - $this->backupCodes = array_merge($backupCodes); - } - - // Set up the isEditExisting property. - $this->isEditExisting = !empty($this->record->id); - - // Back-end: always show a title in the 'title' module position, not in the page body - if ($this->isAdmin) - { - ToolbarHelper::title($this->title, 'users user-lock'); - - $helpUrl = $this->renderOptions['help_url']; - - if (!empty($helpUrl)) - { - ToolbarHelper::help('', false, $helpUrl); - } - - $this->title = ''; - } - - $returnUrl = empty($this->returnURL) ? '' : base64_decode($this->returnURL); - $returnUrl = $returnUrl ?: Route::_('index.php?option=com_users&task=methods.display&user_id=' . $this->user->id); - - if ($this->isAdmin && $this->getLayout() === 'edit') - { - $bar = Toolbar::getInstance(); - $button = (new BasicButton('user-mfa-edit-save')) - ->text($this->renderOptions['submit_text']) - ->icon($this->renderOptions['submit_icon']) - ->onclick('document.getElementById(\'user-mfa-edit-save\').click()'); - - if ($this->renderOptions['show_submit'] || $this->isEditExisting) - { - $bar->appendButton($button); - } - - $button = (new LinkButton('user-mfa-edit-cancel')) - ->text('JCANCEL') - ->buttonClass('btn btn-danger') - ->icon('icon-cancel-2') - ->url($returnUrl); - $bar->appendButton($button); - } - elseif ($this->isAdmin && $this->getLayout() === 'backupcodes') - { - $bar = Toolbar::getInstance(); - - $arrow = Factory::getApplication()->getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; - $button = (new LinkButton('user-mfa-edit-cancel')) - ->text('JTOOLBAR_BACK') - ->icon('icon-' . $arrow) - ->url($returnUrl); - $bar->appendButton($button); - - $button = (new LinkButton('user-mfa-edit-cancel')) - ->text('COM_USERS_MFA_BACKUPCODES_RESET') - ->buttonClass('btn btn-danger') - ->icon('icon-refresh') - ->url( - Route::_( - sprintf( - "index.php?option=com_users&task=method.regenerateBackupCodes&user_id=%s&%s=1&returnurl=%s", - $this->user->id, - Factory::getApplication()->getFormToken(), - base64_encode($returnUrl) - ) - ) - ); - $bar->appendButton($button); - } - - // Display the view - parent::display($tpl); - } + /** + * Is this an administrator page? + * + * @var boolean + * @since 4.2.0 + */ + public $isAdmin = false; + + /** + * The editor page render options + * + * @var array + * @since 4.2.0 + */ + public $renderOptions = []; + + /** + * The MFA Method record being edited + * + * @var object + * @since 4.2.0 + */ + public $record = null; + + /** + * The title text for this page + * + * @var string + * @since 4.2.0 + */ + public $title = ''; + + /** + * The return URL to use for all links and forms + * + * @var string + * @since 4.2.0 + */ + public $returnURL = null; + + /** + * The user object used to display this page + * + * @var User + * @since 4.2.0 + */ + public $user = null; + + /** + * The backup codes for the current user. Only applies when the backup codes record is being "edited" + * + * @var array + * @since 4.2.0 + */ + public $backupCodes = []; + + /** + * Am I editing an existing Method? If it's false then I'm adding a new Method. + * + * @var boolean + * @since 4.2.0 + */ + public $isEditExisting = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @throws \Exception + * @see \JViewLegacy::loadTemplate() + * @since 4.2.0 + */ + public function display($tpl = null): void + { + $app = Factory::getApplication(); + + if (empty($this->user)) { + $this->user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + /** @var MethodModel $model */ + $model = $this->getModel(); + $this->setLayout('edit'); + $this->renderOptions = $model->getRenderOptions($this->user); + $this->record = $model->getRecord($this->user); + $this->title = $model->getPageTitle(); + $this->isAdmin = $app->isClient('administrator'); + + // Backup codes are a special case, rendered with a special layout + if ($this->record->method == 'backupcodes') { + $this->setLayout('backupcodes'); + + $backupCodes = $this->record->options; + + if (!is_array($backupCodes)) { + $backupCodes = []; + } + + $backupCodes = array_filter( + $backupCodes, + function ($x) { + return !empty($x); + } + ); + + if (count($backupCodes) % 2 != 0) { + $backupCodes[] = ''; + } + + /** + * The call to array_merge resets the array indices. This is necessary since array_filter kept the indices, + * meaning our elements are completely out of order. + */ + $this->backupCodes = array_merge($backupCodes); + } + + // Set up the isEditExisting property. + $this->isEditExisting = !empty($this->record->id); + + // Back-end: always show a title in the 'title' module position, not in the page body + if ($this->isAdmin) { + ToolbarHelper::title($this->title, 'users user-lock'); + + $helpUrl = $this->renderOptions['help_url']; + + if (!empty($helpUrl)) { + ToolbarHelper::help('', false, $helpUrl); + } + + $this->title = ''; + } + + $returnUrl = empty($this->returnURL) ? '' : base64_decode($this->returnURL); + $returnUrl = $returnUrl ?: Route::_('index.php?option=com_users&task=methods.display&user_id=' . $this->user->id); + + if ($this->isAdmin && $this->getLayout() === 'edit') { + $bar = Toolbar::getInstance(); + $button = (new BasicButton('user-mfa-edit-save')) + ->text($this->renderOptions['submit_text']) + ->icon($this->renderOptions['submit_icon']) + ->onclick('document.getElementById(\'user-mfa-edit-save\').click()'); + + if ($this->renderOptions['show_submit'] || $this->isEditExisting) { + $bar->appendButton($button); + } + + $button = (new LinkButton('user-mfa-edit-cancel')) + ->text('JCANCEL') + ->buttonClass('btn btn-danger') + ->icon('icon-cancel-2') + ->url($returnUrl); + $bar->appendButton($button); + } elseif ($this->isAdmin && $this->getLayout() === 'backupcodes') { + $bar = Toolbar::getInstance(); + + $arrow = Factory::getApplication()->getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; + $button = (new LinkButton('user-mfa-edit-cancel')) + ->text('JTOOLBAR_BACK') + ->icon('icon-' . $arrow) + ->url($returnUrl); + $bar->appendButton($button); + + $button = (new LinkButton('user-mfa-edit-cancel')) + ->text('COM_USERS_MFA_BACKUPCODES_RESET') + ->buttonClass('btn btn-danger') + ->icon('icon-refresh') + ->url( + Route::_( + sprintf( + "index.php?option=com_users&task=method.regenerateBackupCodes&user_id=%s&%s=1&returnurl=%s", + $this->user->id, + Factory::getApplication()->getFormToken(), + base64_encode($returnUrl) + ) + ) + ); + $bar->appendButton($button); + } + + // Display the view + parent::display($tpl); + } } diff --git a/administrator/components/com_users/src/View/Methods/HtmlView.php b/administrator/components/com_users/src/View/Methods/HtmlView.php index 6a2b19de241f0..e8156398acddd 100644 --- a/administrator/components/com_users/src/View/Methods/HtmlView.php +++ b/administrator/components/com_users/src/View/Methods/HtmlView.php @@ -1,4 +1,5 @@ setSiteTemplateStyle(); - - $app = Factory::getApplication(); - - if (empty($this->user)) - { - $this->user = Factory::getApplication()->getIdentity() - ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); - } - - /** @var MethodsModel $model */ - $model = $this->getModel(); - - if ($this->getLayout() !== 'firsttime') - { - $this->setLayout('default'); - } - - $this->methods = $model->getMethods($this->user); - $this->isAdmin = $app->isClient('administrator'); - $activeRecords = 0; - - foreach ($this->methods as $methodName => $method) - { - $methodActiveRecords = count($method['active']); - - if (!$methodActiveRecords) - { - continue; - } - - $activeRecords += $methodActiveRecords; - $this->mfaActive = true; - - foreach ($method['active'] as $record) - { - if ($record->default) - { - $this->defaultMethod = $methodName; - - break; - } - } - } - - // If there are no backup codes yet we should create new ones - /** @var BackupcodesModel $model */ - $model = $this->getModel('backupcodes'); - $backupCodes = $model->getBackupCodes($this->user); - - if ($activeRecords && empty($backupCodes)) - { - $model->regenerateBackupCodes($this->user); - } - - $backupCodesRecord = $model->getBackupCodesRecord($this->user); - - if (!is_null($backupCodesRecord)) - { - $this->methods = array_merge( - [ - 'backupcodes' => new MethodDescriptor( - [ - 'name' => 'backupcodes', - 'display' => Text::_('COM_USERS_USER_BACKUPCODES'), - 'shortinfo' => Text::_('COM_USERS_USER_BACKUPCODES_DESC'), - 'image' => 'media/com_users/images/emergency.svg', - 'canDisable' => false, - 'active' => [$backupCodesRecord], - ] - ) - ], - $this->methods - ); - } - - $this->isMandatoryMFASetup = $activeRecords === 0 && $app->getSession()->get('com_users.mandatory_mfa_setup', 0) === 1; - - // Back-end: always show a title in the 'title' module position, not in the page body - if ($this->isAdmin) - { - ToolbarHelper::title(Text::_('COM_USERS_MFA_LIST_PAGE_HEAD'), 'users user-lock'); - - if (Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_users')) - { - ToolbarHelper::back('JTOOLBAR_BACK', Route::_('index.php?option=com_users')); - } - } - - // Display the view - parent::display($tpl); - - $event = new NotifyActionLog('onComUsersViewMethodsAfterDisplay', [$this]); - Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); - - Text::script('JGLOBAL_CONFIRM_DELETE'); - } + use SiteTemplateTrait; + + /** + * Is this an administrator page? + * + * @var boolean + * @since 4.2.0 + */ + public $isAdmin = false; + + /** + * The MFA Methods available for this user + * + * @var array + * @since 4.2.0 + */ + public $methods = []; + + /** + * The return URL to use for all links and forms + * + * @var string + * @since 4.2.0 + */ + public $returnURL = null; + + /** + * Are there any active MFA Methods at all? + * + * @var boolean + * @since 4.2.0 + */ + public $mfaActive = false; + + /** + * Which Method has the default record? + * + * @var string + * @since 4.2.0 + */ + public $defaultMethod = ''; + + /** + * The user object used to display this page + * + * @var User + * @since 4.2.0 + */ + public $user = null; + + /** + * Is this page part of the mandatory Multi-factor Authentication setup? + * + * @var boolean + * @since 4.2.0 + */ + public $isMandatoryMFASetup = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @throws \Exception + * @see \JViewLegacy::loadTemplate() + * @since 4.2.0 + */ + public function display($tpl = null): void + { + $this->setSiteTemplateStyle(); + + $app = Factory::getApplication(); + + if (empty($this->user)) { + $this->user = Factory::getApplication()->getIdentity() + ?: Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById(0); + } + + /** @var MethodsModel $model */ + $model = $this->getModel(); + + if ($this->getLayout() !== 'firsttime') { + $this->setLayout('default'); + } + + $this->methods = $model->getMethods($this->user); + $this->isAdmin = $app->isClient('administrator'); + $activeRecords = 0; + + foreach ($this->methods as $methodName => $method) { + $methodActiveRecords = count($method['active']); + + if (!$methodActiveRecords) { + continue; + } + + $activeRecords += $methodActiveRecords; + $this->mfaActive = true; + + foreach ($method['active'] as $record) { + if ($record->default) { + $this->defaultMethod = $methodName; + + break; + } + } + } + + // If there are no backup codes yet we should create new ones + /** @var BackupcodesModel $model */ + $model = $this->getModel('backupcodes'); + $backupCodes = $model->getBackupCodes($this->user); + + if ($activeRecords && empty($backupCodes)) { + $model->regenerateBackupCodes($this->user); + } + + $backupCodesRecord = $model->getBackupCodesRecord($this->user); + + if (!is_null($backupCodesRecord)) { + $this->methods = array_merge( + [ + 'backupcodes' => new MethodDescriptor( + [ + 'name' => 'backupcodes', + 'display' => Text::_('COM_USERS_USER_BACKUPCODES'), + 'shortinfo' => Text::_('COM_USERS_USER_BACKUPCODES_DESC'), + 'image' => 'media/com_users/images/emergency.svg', + 'canDisable' => false, + 'active' => [$backupCodesRecord], + ] + ) + ], + $this->methods + ); + } + + $this->isMandatoryMFASetup = $activeRecords === 0 && $app->getSession()->get('com_users.mandatory_mfa_setup', 0) === 1; + + // Back-end: always show a title in the 'title' module position, not in the page body + if ($this->isAdmin) { + ToolbarHelper::title(Text::_('COM_USERS_MFA_LIST_PAGE_HEAD'), 'users user-lock'); + + if (Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_users')) { + ToolbarHelper::back('JTOOLBAR_BACK', Route::_('index.php?option=com_users')); + } + } + + // Display the view + parent::display($tpl); + + $event = new NotifyActionLog('onComUsersViewMethodsAfterDisplay', [$this]); + Factory::getApplication()->getDispatcher()->dispatch($event->getName(), $event); + + Text::script('JGLOBAL_CONFIRM_DELETE'); + } } diff --git a/administrator/components/com_users/src/View/Note/HtmlView.php b/administrator/components/com_users/src/View/Note/HtmlView.php index 48ac938246924..5181cb441f23f 100644 --- a/administrator/components/com_users/src/View/Note/HtmlView.php +++ b/administrator/components/com_users/src/View/Note/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->item = $this->get('Item'); - $this->form = $this->get('Form'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - parent::display($tpl); - $this->addToolbar(); - } - - /** - * Display the toolbar. - * - * @return void - * - * @since 2.5 - * @throws \Exception - */ - protected function addToolbar() - { - $input = Factory::getApplication()->input; - $input->set('hidemainmenu', 1); - - $user = $this->getCurrentUser(); - $isNew = ($this->item->id == 0); - $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); - - // Since we don't track these assets at the item level, use the category id. - $canDo = ContentHelper::getActions('com_users', 'category', $this->item->catid); - - ToolbarHelper::title(Text::_('COM_USERS_NOTES'), 'users user'); - - $toolbarButtons = []; - - // If not checked out, can save the item. - if (!$checkedOut && ($canDo->get('core.edit') || count($user->getAuthorisedCategories('com_users', 'core.create')))) - { - ToolbarHelper::apply('note.apply'); - $toolbarButtons[] = ['save', 'note.save']; - } - - if (!$checkedOut && count($user->getAuthorisedCategories('com_users', 'core.create'))) - { - $toolbarButtons[] = ['save2new', 'note.save2new']; - } - - // If an existing item, can save to a copy. - if (!$isNew && (count($user->getAuthorisedCategories('com_users', 'core.create')) > 0)) - { - $toolbarButtons[] = ['save2copy', 'note.save2copy']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('note.cancel'); - } - else - { - ToolbarHelper::cancel('note.cancel', 'JTOOLBAR_CLOSE'); - - if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) - { - ToolbarHelper::versions('com_users.note', $this->item->id); - } - } - - ToolbarHelper::divider(); - ToolbarHelper::help('User_Notes:_New_or_Edit'); - } + /** + * The edit form. + * + * @var \Joomla\CMS\Form\Form + * + * @since 2.5 + */ + protected $form; + + /** + * The item data. + * + * @var object + * @since 2.5 + */ + protected $item; + + /** + * The model state. + * + * @var CMSObject + * @since 2.5 + */ + protected $state; + + /** + * Override the display method for the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 2.5 + * @throws \Exception + */ + public function display($tpl = null) + { + // Initialise view variables. + $this->state = $this->get('State'); + $this->item = $this->get('Item'); + $this->form = $this->get('Form'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + parent::display($tpl); + $this->addToolbar(); + } + + /** + * Display the toolbar. + * + * @return void + * + * @since 2.5 + * @throws \Exception + */ + protected function addToolbar() + { + $input = Factory::getApplication()->input; + $input->set('hidemainmenu', 1); + + $user = $this->getCurrentUser(); + $isNew = ($this->item->id == 0); + $checkedOut = !(is_null($this->item->checked_out) || $this->item->checked_out == $user->get('id')); + + // Since we don't track these assets at the item level, use the category id. + $canDo = ContentHelper::getActions('com_users', 'category', $this->item->catid); + + ToolbarHelper::title(Text::_('COM_USERS_NOTES'), 'users user'); + + $toolbarButtons = []; + + // If not checked out, can save the item. + if (!$checkedOut && ($canDo->get('core.edit') || count($user->getAuthorisedCategories('com_users', 'core.create')))) { + ToolbarHelper::apply('note.apply'); + $toolbarButtons[] = ['save', 'note.save']; + } + + if (!$checkedOut && count($user->getAuthorisedCategories('com_users', 'core.create'))) { + $toolbarButtons[] = ['save2new', 'note.save2new']; + } + + // If an existing item, can save to a copy. + if (!$isNew && (count($user->getAuthorisedCategories('com_users', 'core.create')) > 0)) { + $toolbarButtons[] = ['save2copy', 'note.save2copy']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('note.cancel'); + } else { + ToolbarHelper::cancel('note.cancel', 'JTOOLBAR_CLOSE'); + + if (ComponentHelper::isEnabled('com_contenthistory') && $this->state->params->get('save_history', 0) && $canDo->get('core.edit')) { + ToolbarHelper::versions('com_users.note', $this->item->id); + } + } + + ToolbarHelper::divider(); + ToolbarHelper::help('User_Notes:_New_or_Edit'); + } } diff --git a/administrator/components/com_users/src/View/Notes/HtmlView.php b/administrator/components/com_users/src/View/Notes/HtmlView.php index a3c7c490c721d..c0d640864e10a 100644 --- a/administrator/components/com_users/src/View/Notes/HtmlView.php +++ b/administrator/components/com_users/src/View/Notes/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->user = $this->get('User'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) - { - $this->setLayout('emptystate'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Turn parameters into registry objects - foreach ($this->items as $item) - { - $item->cparams = new Registry($item->category_params); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Display the toolbar. - * - * @return void - * - * @since 2.5 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions('com_users', 'category', $this->state->get('filter.category_id')); - - ToolbarHelper::title(Text::_('COM_USERS_VIEW_NOTES_TITLE'), 'users user'); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('note.add'); - } - - if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $canDo->get('core.admin'))) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - if ($canDo->get('core.edit.state')) - { - $childBar->publish('notes.publish')->listCheck(true); - $childBar->unpublish('notes.unpublish')->listCheck(true); - $childBar->archive('notes.archive')->listCheck(true); - $childBar->checkin('notes.checkin')->listCheck(true); - } - - if ($this->state->get('filter.published') != -2 && $canDo->get('core.edit.state')) - { - $childBar->trash('notes.trash'); - } - } - - if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) - { - $toolbar->delete('notes.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences('com_users'); - } - - $toolbar->help('User_Notes'); - } + /** + * A list of user note objects. + * + * @var array + * @since 2.5 + */ + protected $items; + + /** + * The pagination object. + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 2.5 + */ + protected $pagination; + + /** + * The model state. + * + * @var CMSObject + * @since 2.5 + */ + protected $state; + + /** + * The model state. + * + * @var User + * @since 2.5 + */ + protected $user; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Override the display method for the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + // Initialise view variables. + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->user = $this->get('User'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) { + $this->setLayout('emptystate'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Turn parameters into registry objects + foreach ($this->items as $item) { + $item->cparams = new Registry($item->category_params); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Display the toolbar. + * + * @return void + * + * @since 2.5 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_users', 'category', $this->state->get('filter.category_id')); + + ToolbarHelper::title(Text::_('COM_USERS_VIEW_NOTES_TITLE'), 'users user'); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('note.add'); + } + + if (!$this->isEmptyState && ($canDo->get('core.edit.state') || $canDo->get('core.admin'))) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + if ($canDo->get('core.edit.state')) { + $childBar->publish('notes.publish')->listCheck(true); + $childBar->unpublish('notes.unpublish')->listCheck(true); + $childBar->archive('notes.archive')->listCheck(true); + $childBar->checkin('notes.checkin')->listCheck(true); + } + + if ($this->state->get('filter.published') != -2 && $canDo->get('core.edit.state')) { + $childBar->trash('notes.trash'); + } + } + + if (!$this->isEmptyState && $this->state->get('filter.published') == -2 && $canDo->get('core.delete')) { + $toolbar->delete('notes.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences('com_users'); + } + + $toolbar->help('User_Notes'); + } } diff --git a/administrator/components/com_users/src/View/SiteTemplateTrait.php b/administrator/components/com_users/src/View/SiteTemplateTrait.php index 0683a6ec9fede..cd915b2424438 100644 --- a/administrator/components/com_users/src/View/SiteTemplateTrait.php +++ b/administrator/components/com_users/src/View/SiteTemplateTrait.php @@ -1,4 +1,5 @@ get('captive_template', ''); - - if (empty($templateStyle) || !$app->isClient('site')) - { - return; - } + /** + * Set a specific site template style in the frontend application + * + * @return void + * @throws Exception + * @since 4.2.0 + */ + private function setSiteTemplateStyle(): void + { + $app = Factory::getApplication(); + $templateStyle = (int) ComponentHelper::getParams('com_users')->get('captive_template', ''); - $itemId = $app->input->get('Itemid'); + if (empty($templateStyle) || !$app->isClient('site')) { + return; + } - if (!empty($itemId)) - { - return; - } + $itemId = $app->input->get('Itemid'); - $app->input->set('templateStyle', $templateStyle); + if (!empty($itemId)) { + return; + } - try - { - $refApp = new ReflectionObject($app); - $refTemplate = $refApp->getProperty('template'); - $refTemplate->setAccessible(true); - $refTemplate->setValue($app, null); - } - catch (ReflectionException $e) - { - return; - } + $app->input->set('templateStyle', $templateStyle); - $template = $app->getTemplate(true); + try { + $refApp = new ReflectionObject($app); + $refTemplate = $refApp->getProperty('template'); + $refTemplate->setAccessible(true); + $refTemplate->setValue($app, null); + } catch (ReflectionException $e) { + return; + } - $app->set('theme', $template->template); - $app->set('themeParams', $template->params); - } + $template = $app->getTemplate(true); + $app->set('theme', $template->template); + $app->set('themeParams', $template->params); + } } diff --git a/administrator/components/com_users/src/View/User/HtmlView.php b/administrator/components/com_users/src/View/User/HtmlView.php index 38bc006953d2c..b2e57e893fe6c 100644 --- a/administrator/components/com_users/src/View/User/HtmlView.php +++ b/administrator/components/com_users/src/View/User/HtmlView.php @@ -1,4 +1,5 @@ item = $this->get('Item')) - { - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_NOT_EXIST'), 'error'); - $app->redirect('index.php?option=com_users&view=users'); - } - - $this->form = $this->get('Form'); - $this->state = $this->get('State'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Prevent user from modifying own group(s) - $user = Factory::getApplication()->getIdentity(); - - if ((int) $user->id != (int) $this->item->id || $user->authorise('core.admin')) - { - $this->grouplist = $this->get('Groups'); - $this->groups = $this->get('AssignedGroups'); - } - - $this->form->setValue('password', null); - $this->form->setValue('password2', null); - - /** @var User $userBeingEdited */ - $userBeingEdited = Factory::getContainer() - ->get(UserFactoryInterface::class) - ->loadUserById($this->item->id); - - if ($this->item->id > 0 && (int) $userBeingEdited->id == (int) $this->item->id) - { - try - { - $this->mfaConfigurationUI = Mfa::canShowConfigurationInterface($userBeingEdited) - ? Mfa::getConfigurationInterface($userBeingEdited) - : ''; - } - catch (\Exception $e) - { - // In case something goes really wrong with the plugins; prevents hard breaks. - $this->mfaConfigurationUI = null; - } - } - - parent::display($tpl); - - $this->addToolbar(); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = Factory::getApplication()->getIdentity(); - $canDo = ContentHelper::getActions('com_users'); - $isNew = ($this->item->id == 0); - $isProfile = $this->item->id == $user->id; - - ToolbarHelper::title( - Text::_( - $isNew ? 'COM_USERS_VIEW_NEW_USER_TITLE' : ($isProfile ? 'COM_USERS_VIEW_EDIT_PROFILE_TITLE' : 'COM_USERS_VIEW_EDIT_USER_TITLE') - ), - 'user ' . ($isNew ? 'user-add' : ($isProfile ? 'user-profile' : 'user-edit')) - ); - - $toolbarButtons = []; - - if ($canDo->get('core.edit') || $canDo->get('core.create') || $isProfile) - { - ToolbarHelper::apply('user.apply'); - $toolbarButtons[] = ['save', 'user.save']; - } - - if ($canDo->get('core.create') && $canDo->get('core.manage')) - { - $toolbarButtons[] = ['save2new', 'user.save2new']; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - if (empty($this->item->id)) - { - ToolbarHelper::cancel('user.cancel'); - } - else - { - ToolbarHelper::cancel('user.cancel', 'JTOOLBAR_CLOSE'); - } - - ToolbarHelper::divider(); - ToolbarHelper::help('Users:_Edit_Profile'); - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * Gets the available groups + * + * @var array + */ + protected $grouplist; + + /** + * The groups this user is assigned to + * + * @var array + * @since 1.6 + */ + protected $groups; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The Multi-factor Authentication configuration interface for the user. + * + * @var string|null + * @since 4.2.0 + */ + protected $mfaConfigurationUI; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.5 + */ + public function display($tpl = null) + { + // If no item found, dont show the edit screen, redirect with message + if (false === $this->item = $this->get('Item')) { + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_NOT_EXIST'), 'error'); + $app->redirect('index.php?option=com_users&view=users'); + } + + $this->form = $this->get('Form'); + $this->state = $this->get('State'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Prevent user from modifying own group(s) + $user = Factory::getApplication()->getIdentity(); + + if ((int) $user->id != (int) $this->item->id || $user->authorise('core.admin')) { + $this->grouplist = $this->get('Groups'); + $this->groups = $this->get('AssignedGroups'); + } + + $this->form->setValue('password', null); + $this->form->setValue('password2', null); + + /** @var User $userBeingEdited */ + $userBeingEdited = Factory::getContainer() + ->get(UserFactoryInterface::class) + ->loadUserById($this->item->id); + + if ($this->item->id > 0 && (int) $userBeingEdited->id == (int) $this->item->id) { + try { + $this->mfaConfigurationUI = Mfa::canShowConfigurationInterface($userBeingEdited) + ? Mfa::getConfigurationInterface($userBeingEdited) + : ''; + } catch (\Exception $e) { + // In case something goes really wrong with the plugins; prevents hard breaks. + $this->mfaConfigurationUI = null; + } + } + + parent::display($tpl); + + $this->addToolbar(); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = Factory::getApplication()->getIdentity(); + $canDo = ContentHelper::getActions('com_users'); + $isNew = ($this->item->id == 0); + $isProfile = $this->item->id == $user->id; + + ToolbarHelper::title( + Text::_( + $isNew ? 'COM_USERS_VIEW_NEW_USER_TITLE' : ($isProfile ? 'COM_USERS_VIEW_EDIT_PROFILE_TITLE' : 'COM_USERS_VIEW_EDIT_USER_TITLE') + ), + 'user ' . ($isNew ? 'user-add' : ($isProfile ? 'user-profile' : 'user-edit')) + ); + + $toolbarButtons = []; + + if ($canDo->get('core.edit') || $canDo->get('core.create') || $isProfile) { + ToolbarHelper::apply('user.apply'); + $toolbarButtons[] = ['save', 'user.save']; + } + + if ($canDo->get('core.create') && $canDo->get('core.manage')) { + $toolbarButtons[] = ['save2new', 'user.save2new']; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + if (empty($this->item->id)) { + ToolbarHelper::cancel('user.cancel'); + } else { + ToolbarHelper::cancel('user.cancel', 'JTOOLBAR_CLOSE'); + } + + ToolbarHelper::divider(); + ToolbarHelper::help('Users:_Edit_Profile'); + } } diff --git a/administrator/components/com_users/src/View/Users/HtmlView.php b/administrator/components/com_users/src/View/Users/HtmlView.php index ee56bdc8a47c4..3887c808f39d3 100644 --- a/administrator/components/com_users/src/View/Users/HtmlView.php +++ b/administrator/components/com_users/src/View/Users/HtmlView.php @@ -1,4 +1,5 @@ items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->state = $this->get('State'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->canDo = ContentHelper::getActions('com_users'); - $this->db = Factory::getDbo(); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->addToolbar(); - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 1.6 - */ - protected function addToolbar() - { - $canDo = $this->canDo; - $user = $this->getCurrentUser(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_USERS_VIEW_USERS_TITLE'), 'users user'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('user.add'); - } - - if ($canDo->get('core.edit.state') || $canDo->get('core.admin')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('users.activate', 'COM_USERS_TOOLBAR_ACTIVATE', true); - $childBar->unpublish('users.block', 'COM_USERS_TOOLBAR_BLOCK', true); - $childBar->standardButton('unblock') - ->text('COM_USERS_TOOLBAR_UNBLOCK') - ->task('users.unblock') - ->listCheck(true); - - // Add a batch button - if ($user->authorise('core.create', 'com_users') - && $user->authorise('core.edit', 'com_users') - && $user->authorise('core.edit.state', 'com_users')) - { - $childBar->popupButton('batch') - ->text('JTOOLBAR_BATCH') - ->selector('collapseModal') - ->listCheck(true); - } - - if ($canDo->get('core.delete')) - { - $childBar->delete('users.delete') - ->text('JTOOLBAR_DELETE') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences('com_users'); - } - - $toolbar->help('Users'); - } + /** + * The item data. + * + * @var object + * @since 1.6 + */ + protected $items; + + /** + * The pagination object. + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 1.6 + */ + protected $pagination; + + /** + * The model state. + * + * @var CMSObject + * @since 1.6 + */ + protected $state; + + /** + * A Form instance with filter fields. + * + * @var \Joomla\CMS\Form\Form + * + * @since 3.6.3 + */ + public $filterForm; + + /** + * An array with active filters. + * + * @var array + * @since 3.6.3 + */ + public $activeFilters; + + /** + * An ACL object to verify user rights. + * + * @var CMSObject + * @since 3.6.3 + */ + protected $canDo; + + /** + * An instance of DatabaseDriver. + * + * @var DatabaseDriver + * @since 3.6.3 + * + * @deprecated 5.0 Will be removed without replacement + */ + protected $db; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->canDo = ContentHelper::getActions('com_users'); + $this->db = Factory::getDbo(); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->addToolbar(); + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = $this->canDo; + $user = $this->getCurrentUser(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_USERS_VIEW_USERS_TITLE'), 'users user'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('user.add'); + } + + if ($canDo->get('core.edit.state') || $canDo->get('core.admin')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('users.activate', 'COM_USERS_TOOLBAR_ACTIVATE', true); + $childBar->unpublish('users.block', 'COM_USERS_TOOLBAR_BLOCK', true); + $childBar->standardButton('unblock') + ->text('COM_USERS_TOOLBAR_UNBLOCK') + ->task('users.unblock') + ->listCheck(true); + + // Add a batch button + if ( + $user->authorise('core.create', 'com_users') + && $user->authorise('core.edit', 'com_users') + && $user->authorise('core.edit.state', 'com_users') + ) { + $childBar->popupButton('batch') + ->text('JTOOLBAR_BATCH') + ->selector('collapseModal') + ->listCheck(true); + } + + if ($canDo->get('core.delete')) { + $childBar->delete('users.delete') + ->text('JTOOLBAR_DELETE') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences('com_users'); + } + + $toolbar->help('Users'); + } } diff --git a/administrator/components/com_users/tmpl/debuggroup/default.php b/administrator/components/com_users/tmpl/debuggroup/default.php index ac658cdd6c8e4..b76b09280a550 100644 --- a/administrator/components/com_users/tmpl/debuggroup/default.php +++ b/administrator/components/com_users/tmpl/debuggroup/default.php @@ -1,4 +1,5 @@ escape($this->state->get('list.direction')); ?>
-
- $this)); ?> -
- - - - - - - actions as $key => $action) : ?> - - - - - - - - items as $i => $item) : ?> - - - - actions as $action) : ?> - checks[$name]; - if ($check === true) : - $class = 'text-success icon-check'; - $button = 'btn-success'; - $text = Text::_('COM_USERS_DEBUG_EXPLICIT_ALLOW'); - elseif ($check === false) : - $class = 'text-danger icon-times'; - $button = 'btn-danger'; - $text = Text::_('COM_USERS_DEBUG_EXPLICIT_DENY'); - elseif ($check === null) : - $class = 'text-danger icon-minus-circle'; - $button = 'btn-warning'; - $text = Text::_('COM_USERS_DEBUG_IMPLICIT_DENY'); - else : - $class = ''; - $button = ''; - $text = ''; - endif; - ?> - - - - - - - -
- , - , - -
- - - - - - - - - -
- escape(Text::_($item->title)); ?> - - $item->level + 1)) . $this->escape($item->name); ?> - - - - - lft; ?> - - rgt; ?> - - id; ?> -
-
-    -    -   -
+
+ $this)); ?> +
+ + + + + + + actions as $key => $action) : ?> + + + + + + + + items as $i => $item) : ?> + + + + actions as $action) : ?> + checks[$name]; + if ($check === true) : + $class = 'text-success icon-check'; + $button = 'btn-success'; + $text = Text::_('COM_USERS_DEBUG_EXPLICIT_ALLOW'); + elseif ($check === false) : + $class = 'text-danger icon-times'; + $button = 'btn-danger'; + $text = Text::_('COM_USERS_DEBUG_EXPLICIT_DENY'); + elseif ($check === null) : + $class = 'text-danger icon-minus-circle'; + $button = 'btn-warning'; + $text = Text::_('COM_USERS_DEBUG_IMPLICIT_DENY'); + else : + $class = ''; + $button = ''; + $text = ''; + endif; + ?> + + + + + + + +
+ , + , + +
+ + + + + + + + + +
+ escape(Text::_($item->title)); ?> + + $item->level + 1)) . $this->escape($item->name); ?> + + + + + lft; ?> + - rgt; ?> + + id; ?> +
+
+    +    +   +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> -
- - - -
+
+ + + +
diff --git a/administrator/components/com_users/tmpl/debuguser/default.php b/administrator/components/com_users/tmpl/debuguser/default.php index 75b108ef8b93b..159af56b5cbd4 100644 --- a/administrator/components/com_users/tmpl/debuguser/default.php +++ b/administrator/components/com_users/tmpl/debuguser/default.php @@ -1,4 +1,5 @@ actions as $action) : - $name = $action[0]; - if (in_array($name, ['core.login.site', 'core.login.admin', 'core.login.offline', 'core.login.api', 'core.admin'])) : - $loginActions[] = $action; - else : - $actions[] = $action; - endif; + $name = $action[0]; + if (in_array($name, ['core.login.site', 'core.login.admin', 'core.login.offline', 'core.login.api', 'core.admin'])) : + $loginActions[] = $action; + else : + $actions[] = $action; + endif; endforeach; ?>
-
- $this)); ?> -
- items[0]->checks[$name]; - if ($check === true) : - $class = 'text-success icon-check'; - $button = 'btn-success'; - $text = Text::_('COM_USERS_DEBUG_EXPLICIT_ALLOW'); - elseif ($check === false) : - $class = 'text-danger icon-times'; - $button = 'btn-danger'; - $text = Text::_('COM_USERS_DEBUG_EXPLICIT_DENY'); - elseif ($check === null) : - $class = 'text-danger icon-minus-circle'; - $button = 'btn-warning'; - $text = Text::_('COM_USERS_DEBUG_IMPLICIT_DENY'); - else : - $class = ''; - $button = ''; - $text = ''; - endif; - ?> -
- - - -
- -
+
+ $this)); ?> +
+ items[0]->checks[$name]; + if ($check === true) : + $class = 'text-success icon-check'; + $button = 'btn-success'; + $text = Text::_('COM_USERS_DEBUG_EXPLICIT_ALLOW'); + elseif ($check === false) : + $class = 'text-danger icon-times'; + $button = 'btn-danger'; + $text = Text::_('COM_USERS_DEBUG_EXPLICIT_DENY'); + elseif ($check === null) : + $class = 'text-danger icon-minus-circle'; + $button = 'btn-warning'; + $text = Text::_('COM_USERS_DEBUG_IMPLICIT_DENY'); + else : + $class = ''; + $button = ''; + $text = ''; + endif; + ?> +
+ + + +
+ +
- - - - - - - $action) : ?> - - - - - - - - items as $i => $item) :?> - - - - - checks[$name]; - if ($check === true) : - $class = 'text-success icon-check'; - $button = 'btn-success'; - $text = Text::_('COM_USERS_DEBUG_EXPLICIT_ALLOW'); - elseif ($check === false) : - $class = 'text-danger icon-times'; - $button = 'btn-danger'; - $text = Text::_('COM_USERS_DEBUG_EXPLICIT_DENY'); - elseif ($check === null) : - $class = 'text-danger icon-minus-circle'; - $button = 'btn-warning'; - $text = Text::_('COM_USERS_DEBUG_IMPLICIT_DENY'); - else : - $class = ''; - $button = ''; - $text = ''; - endif; - ?> - - - - - - - -
- , - , - -
- - - - - - - - - -
- escape(Text::_($item->title)); ?> - - $item->level + 1)) . $this->escape($item->name); ?> - - - - - lft; ?> - - rgt; ?> - - id; ?> -
+ + + + + + + $action) : ?> + + + + + + + + items as $i => $item) :?> + + + + + checks[$name]; + if ($check === true) : + $class = 'text-success icon-check'; + $button = 'btn-success'; + $text = Text::_('COM_USERS_DEBUG_EXPLICIT_ALLOW'); + elseif ($check === false) : + $class = 'text-danger icon-times'; + $button = 'btn-danger'; + $text = Text::_('COM_USERS_DEBUG_EXPLICIT_DENY'); + elseif ($check === null) : + $class = 'text-danger icon-minus-circle'; + $button = 'btn-warning'; + $text = Text::_('COM_USERS_DEBUG_IMPLICIT_DENY'); + else : + $class = ''; + $button = ''; + $text = ''; + endif; + ?> + + + + + + + +
+ , + , + +
+ + + + + + + + + +
+ escape(Text::_($item->title)); ?> + + $item->level + 1)) . $this->escape($item->name); ?> + + + + + lft; ?> + - rgt; ?> + + id; ?> +
-
-    -    - -
+
+    +    + +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - -
+ + + +
diff --git a/administrator/components/com_users/tmpl/group/edit.php b/administrator/components/com_users/tmpl/group/edit.php index 2bcbb242f6092..d4c95a5542b23 100644 --- a/administrator/components/com_users/tmpl/group/edit.php +++ b/administrator/components/com_users/tmpl/group/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $this->useCoreUI = true; ?>
- 'details', 'recall' => true, 'breakpoint' => 768]); ?> - -
- form->renderField('title'); ?> - form->renderField('parent_id'); ?> -
- - ignore_fieldsets = array('group_details'); ?> - - + 'details', 'recall' => true, 'breakpoint' => 768]); ?> + +
+ form->renderField('title'); ?> + form->renderField('parent_id'); ?> +
+ + ignore_fieldsets = array('group_details'); ?> + + - - + +
diff --git a/administrator/components/com_users/tmpl/groups/default.php b/administrator/components/com_users/tmpl/groups/default.php index def943cd56c4b..36d4d22e6e9cc 100644 --- a/administrator/components/com_users/tmpl/groups/default.php +++ b/administrator/components/com_users/tmpl/groups/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('com_users.admin-users-groups') - ->useScript('multiselect') - ->useScript('table.columns'); + ->useScript('multiselect') + ->useScript('table.columns'); ?>
-
-
-
- $this, 'options' => array('filterButton' => false))); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - items as $i => $item) : - $canCreate = $user->authorise('core.create', 'com_users'); - $canEdit = $user->authorise('core.edit', 'com_users'); +
+
+
+ $this, 'options' => array('filterButton' => false))); ?> + items)) : ?> +
+ + +
+ +
- , - , - -
- - - - - - - - - - - - - -
+ + + + + + + + + + + + + items as $i => $item) : + $canCreate = $user->authorise('core.create', 'com_users'); + $canEdit = $user->authorise('core.edit', 'com_users'); - // If this group is super admin and this user is not super admin, $canEdit is false - if (!$user->authorise('core.admin') && Access::checkGroup($item->id, 'core.admin')) - { - $canEdit = false; - } - $canChange = $user->authorise('core.edit.state', 'com_users'); - ?> - - - - - - - - - - -
+ , + , + +
+ + + + + + + + + + + + + +
- - id, false, 'cid', 'cb', $item->title); ?> - - - $item->level + 1)); ?> - - - escape($item->title); ?> - - escape($item->title); ?> - - - - - - - - - count_enabled; ?> - - - - - count_disabled; ?> - - - - id; ?> -
+ // If this group is super admin and this user is not super admin, $canEdit is false + if (!$user->authorise('core.admin') && Access::checkGroup($item->id, 'core.admin')) { + $canEdit = false; + } + $canChange = $user->authorise('core.edit.state', 'com_users'); + ?> + + + + id, false, 'cid', 'cb', $item->title); ?> + + + + $item->level + 1)); ?> + + + escape($item->title); ?> + + escape($item->title); ?> + + + + + + + + + + + count_enabled; ?> + + + + + + count_disabled; ?> + + + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + - - - -
-
-
+ + + + + +
diff --git a/administrator/components/com_users/tmpl/level/edit.php b/administrator/components/com_users/tmpl/level/edit.php index 389362f311ef2..e4985cc6a9043 100644 --- a/administrator/components/com_users/tmpl/level/edit.php +++ b/administrator/components/com_users/tmpl/level/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?>
- 'details', 'recall' => true, 'breakpoint' => 768]); ?> - - -
- -
-
- form->getLabel('title'); ?> -
-
- form->getInput('title'); ?> -
-
-
- - - -
- -
- item->rules, true); ?> -
-
- - - - - - + 'details', 'recall' => true, 'breakpoint' => 768]); ?> + + +
+ +
+
+ form->getLabel('title'); ?> +
+
+ form->getInput('title'); ?> +
+
+
+ + + +
+ +
+ item->rules, true); ?> +
+
+ + + + + +
diff --git a/administrator/components/com_users/tmpl/levels/default.php b/administrator/components/com_users/tmpl/levels/default.php index cefbc0692e3b2..83e260fb91351 100644 --- a/administrator/components/com_users/tmpl/levels/default.php +++ b/administrator/components/com_users/tmpl/levels/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); $saveOrder = $listOrder == 'a.ordering'; -if ($saveOrder && !empty($this->items)) -{ - $saveOrderingUrl = 'index.php?option=com_users&task=levels.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder && !empty($this->items)) { + $saveOrderingUrl = 'index.php?option=com_users&task=levels.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
-
-
-
- $this, 'options' => array('filterButton' => false))); ?> +
+
+
+ $this, 'options' => array('filterButton' => false))); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - class="js-draggable" data-url="" data-direction=""> - items); ?> - items as $i => $item) : - $ordering = ($listOrder == 'a.ordering'); - $canCreate = $user->authorise('core.create', 'com_users'); - $canEdit = $user->authorise('core.edit', 'com_users'); - $canChange = $user->authorise('core.edit.state', 'com_users'); + items)) : ?> +
+ + +
+ +
- , - , - -
- - - - - - - - - -
+ + + + + + + + + + + class="js-draggable" data-url="" data-direction=""> + items); ?> + items as $i => $item) : + $ordering = ($listOrder == 'a.ordering'); + $canCreate = $user->authorise('core.create', 'com_users'); + $canEdit = $user->authorise('core.edit', 'com_users'); + $canChange = $user->authorise('core.edit.state', 'com_users'); - // Decode level groups - $groups = json_decode($item->rules); + // Decode level groups + $groups = json_decode($item->rules); - // If this group is super admin and this user is not super admin, $canEdit is false - if (!Factory::getUser()->authorise('core.admin') && $groups && Access::checkGroup($groups[0], 'core.admin')) - { - $canEdit = false; - $canChange = false; - } - ?> - - - - - - - - - -
+ , + , + +
+ + + + + + + + + +
- - id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - - - - escape($item->title); ?> - - escape($item->title); ?> - - - rules); ?> - - id; ?> -
+ // If this group is super admin and this user is not super admin, $canEdit is false + if (!Factory::getUser()->authorise('core.admin') && $groups && Access::checkGroup($groups[0], 'core.admin')) { + $canEdit = false; + $canChange = false; + } + ?> + + + + id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + + + + + + escape($item->title); ?> + + escape($item->title); ?> + + + + rules); ?> + + + id; ?> + + + + + - - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - -
-
-
+ + + + +
+
+
diff --git a/administrator/components/com_users/tmpl/mail/default.php b/administrator/components/com_users/tmpl/mail/default.php index 6a5eae8916997..209d5700f76db 100644 --- a/administrator/components/com_users/tmpl/mail/default.php +++ b/administrator/components/com_users/tmpl/mail/default.php @@ -1,4 +1,5 @@
-
-
-
-
- form->getLabel('subject'); ?> - - get('mailSubjectPrefix'))) : ?> - get('mailSubjectPrefix'); ?> - - form->getInput('subject'); ?> - -
-
- form->getLabel('message'); ?> - form->getInput('message'); ?> - get('mailBodySuffix'))) : ?> -
-
- get('mailBodySuffix'); ?> -
-
- -
-
- - -
-
-
- form->getInput('recurse'); ?> - form->getLabel('recurse'); ?> -
-
- form->getInput('mode'); ?> - form->getLabel('mode'); ?> -
-
- form->getInput('disabled'); ?> - form->getLabel('disabled'); ?> -
-
- form->getInput('bcc'); ?> - form->getLabel('bcc'); ?> -
-
- form->getLabel('group'); ?> - form->getInput('group'); ?> -
-
-
+
+
+
+
+ form->getLabel('subject'); ?> + + get('mailSubjectPrefix'))) : ?> + get('mailSubjectPrefix'); ?> + + form->getInput('subject'); ?> + +
+
+ form->getLabel('message'); ?> + form->getInput('message'); ?> + get('mailBodySuffix'))) : ?> +
+
+ get('mailBodySuffix'); ?> +
+
+ +
+
+ + +
+
+
+ form->getInput('recurse'); ?> + form->getLabel('recurse'); ?> +
+
+ form->getInput('mode'); ?> + form->getLabel('mode'); ?> +
+
+ form->getInput('disabled'); ?> + form->getLabel('disabled'); ?> +
+
+ form->getInput('bcc'); ?> + form->getLabel('bcc'); ?> +
+
+ form->getLabel('group'); ?> + form->getInput('group'); ?> +
+
+
diff --git a/administrator/components/com_users/tmpl/note/edit.php b/administrator/components/com_users/tmpl/note/edit.php index ba80a12a1f4d8..8fd2b6d215227 100644 --- a/administrator/components/com_users/tmpl/note/edit.php +++ b/administrator/components/com_users/tmpl/note/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?>
-
-
-
-
-
- form->renderField('subject'); ?> - form->renderField('user_id'); ?> - form->renderField('catid'); ?> - form->renderField('state'); ?> - form->renderField('review_time'); ?> - form->renderField('version_note'); ?> +
+
+
+
+
+ form->renderField('subject'); ?> + form->renderField('user_id'); ?> + form->renderField('catid'); ?> + form->renderField('state'); ?> + form->renderField('review_time'); ?> + form->renderField('version_note'); ?> - - -
-
- form->renderField('body'); ?> -
-
-
-
-
+ + +
+
+ form->renderField('body'); ?> +
+
+
+
+
diff --git a/administrator/components/com_users/tmpl/notes/default.php b/administrator/components/com_users/tmpl/notes/default.php index 07586c547a7d5..b158e7cf1374f 100644 --- a/administrator/components/com_users/tmpl/notes/default.php +++ b/administrator/components/com_users/tmpl/notes/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); @@ -26,103 +27,103 @@ ?>
-
-
-
- $this)); ?> +
+
+
+ $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - items as $i => $item) : - $canEdit = $user->authorise('core.edit', 'com_users.category.' . $item->catid); - $canCheckin = $user->authorise('core.admin', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', 'com_users.category.' . $item->catid) && $canCheckin; - $subject = $item->subject ?: Text::_('COM_USERS_EMPTY_SUBJECT'); - ?> - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - -
- id, false, 'cid', 'cb', $subject); ?> - - state, $i, 'notes.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'notes.', $canCheckin); ?> - - subject ?: Text::_('COM_USERS_EMPTY_SUBJECT'); ?> - - - escape($subject); ?> - - escape($subject); ?> - -
- escape($item->category_title); ?> -
-
- escape($item->user_name); ?> - - review_time !== null) : ?> - review_time, Text::_('DATE_FORMAT_LC4')); ?> - - - - - id; ?> -
+ items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + items as $i => $item) : + $canEdit = $user->authorise('core.edit', 'com_users.category.' . $item->catid); + $canCheckin = $user->authorise('core.admin', 'com_checkin') || $item->checked_out == $user->get('id') || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', 'com_users.category.' . $item->catid) && $canCheckin; + $subject = $item->subject ?: Text::_('COM_USERS_EMPTY_SUBJECT'); + ?> + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + +
+ id, false, 'cid', 'cb', $subject); ?> + + state, $i, 'notes.', $canChange, 'cb', $item->publish_up, $item->publish_down); ?> + + checked_out) : ?> + editor, $item->checked_out_time, 'notes.', $canCheckin); ?> + + subject ?: Text::_('COM_USERS_EMPTY_SUBJECT'); ?> + + + escape($subject); ?> + + escape($subject); ?> + +
+ escape($item->category_title); ?> +
+
+ escape($item->user_name); ?> + + review_time !== null) : ?> + review_time, Text::_('DATE_FORMAT_LC4')); ?> + + + + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - + -
- - - -
-
-
-
+
+ + + +
+
+
+
diff --git a/administrator/components/com_users/tmpl/notes/emptystate.php b/administrator/components/com_users/tmpl/notes/emptystate.php index 2366086b4218b..0b963fa82243e 100644 --- a/administrator/components/com_users/tmpl/notes/emptystate.php +++ b/administrator/components/com_users/tmpl/notes/emptystate.php @@ -1,4 +1,5 @@ 'COM_USERS_NOTES', - 'formURL' => 'index.php?option=com_users&view=notes', - 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:User_Notes', - 'icon' => 'icon-users user', + 'textPrefix' => 'COM_USERS_NOTES', + 'formURL' => 'index.php?option=com_users&view=notes', + 'helpURL' => 'https://docs.joomla.org/Special:MyLanguage/Help40:User_Notes', + 'icon' => 'icon-users user', ]; -if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_users')) -{ - $displayData['createURL'] = 'index.php?option=com_users&task=note.add'; +if (Factory::getApplication()->getIdentity()->authorise('core.create', 'com_users')) { + $displayData['createURL'] = 'index.php?option=com_users&task=note.add'; } echo LayoutHelper::render('joomla.content.emptystate', $displayData); diff --git a/administrator/components/com_users/tmpl/notes/modal.php b/administrator/components/com_users/tmpl/notes/modal.php index deea2d14628d2..19b781d81d4da 100644 --- a/administrator/components/com_users/tmpl/notes/modal.php +++ b/administrator/components/com_users/tmpl/notes/modal.php @@ -1,4 +1,5 @@
-

user->name, $this->user->id); ?>

+

user->name, $this->user->id); ?>

items)) : ?> - + -
    - items as $item) : ?> -
  • -
    - subject) : ?> -

    id, $this->escape($item->subject)); ?>

    - -

    id, Text::_('COM_USERS_EMPTY_SUBJECT')); ?>

    - -
    - -
    - created_time, Text::_('DATE_FORMAT_LC2')); ?> -
    - - cparams->get('image'); ?> - - catid && isset($category_image)) : ?> -
    - -
    - -
    - escape($item->category_title); ?> -
    - - -
    -
    - body) ? HTMLHelper::_('content.prepare', $item->body) : ''); ?> -
    -
  • - -
+
    + items as $item) : ?> +
  • +
    + subject) : ?> +

    id, $this->escape($item->subject)); ?>

    + +

    id, Text::_('COM_USERS_EMPTY_SUBJECT')); ?>

    + +
    + +
    + created_time, Text::_('DATE_FORMAT_LC2')); ?> +
    + + cparams->get('image'); ?> + + catid && isset($category_image)) : ?> +
    + +
    + +
    + escape($item->category_title); ?> +
    + + +
    +
    + body) ? HTMLHelper::_('content.prepare', $item->body) : ''); ?> +
    +
  • + +
diff --git a/administrator/components/com_users/tmpl/user/edit.php b/administrator/components/com_users/tmpl/user/edit.php index 1df46ed313115..d408bbb6906bf 100644 --- a/administrator/components/com_users/tmpl/user/edit.php +++ b/administrator/components/com_users/tmpl/user/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $input = Factory::getApplication()->input; @@ -33,50 +34,50 @@ ?>
-

form->getValue('name', null, Text::_('COM_USERS_USER_NEW_USER_TITLE')); ?>

- -
- 'details', 'recall' => true, 'breakpoint' => 768]); ?> - - -
- -
- form->renderFieldset('user_details'); ?> -
-
- - - - grouplist) : ?> - -
- -
- loadTemplate('groups'); ?> -
-
- - - - ignore_fieldsets = array('user_details'); - echo LayoutHelper::render('joomla.edit.params', $this); - ?> - - mfaConfigurationUI)) : ?> - -
- - mfaConfigurationUI ?> -
- - - - -
- - - - +

form->getValue('name', null, Text::_('COM_USERS_USER_NEW_USER_TITLE')); ?>

+ +
+ 'details', 'recall' => true, 'breakpoint' => 768]); ?> + + +
+ +
+ form->renderFieldset('user_details'); ?> +
+
+ + + + grouplist) : ?> + +
+ +
+ loadTemplate('groups'); ?> +
+
+ + + + ignore_fieldsets = array('user_details'); + echo LayoutHelper::render('joomla.edit.params', $this); + ?> + + mfaConfigurationUI)) : ?> + +
+ + mfaConfigurationUI ?> +
+ + + + +
+ + + +
diff --git a/administrator/components/com_users/tmpl/user/edit_groups.php b/administrator/components/com_users/tmpl/user/edit_groups.php index d8d97b88cd771..fa5d093f04713 100644 --- a/administrator/components/com_users/tmpl/user/edit_groups.php +++ b/administrator/components/com_users/tmpl/user/edit_groups.php @@ -1,4 +1,5 @@ -groups, true); ?> +groups, true); diff --git a/administrator/components/com_users/tmpl/users/default_batch_body.php b/administrator/components/com_users/tmpl/users/default_batch_body.php index b74fd0e867b5b..1c4b4b3ccc19e 100644 --- a/administrator/components/com_users/tmpl/users/default_batch_body.php +++ b/administrator/components/com_users/tmpl/users/default_batch_body.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\HTML\HTMLHelper; @@ -13,16 +15,16 @@ // Create the copy/move options. $options = array( - HTMLHelper::_('select.option', 'add', Text::_('COM_USERS_BATCH_ADD')), - HTMLHelper::_('select.option', 'del', Text::_('COM_USERS_BATCH_DELETE')), - HTMLHelper::_('select.option', 'set', Text::_('COM_USERS_BATCH_SET')) + HTMLHelper::_('select.option', 'add', Text::_('COM_USERS_BATCH_ADD')), + HTMLHelper::_('select.option', 'del', Text::_('COM_USERS_BATCH_DELETE')), + HTMLHelper::_('select.option', 'set', Text::_('COM_USERS_BATCH_SET')) ); // Create the reset password options. $resetOptions = array( - HTMLHelper::_('select.option', '', Text::_('COM_USERS_NO_ACTION')), - HTMLHelper::_('select.option', 'yes', Text::_('JYES')), - HTMLHelper::_('select.option', 'no', Text::_('JNO')) + HTMLHelper::_('select.option', '', Text::_('COM_USERS_NO_ACTION')), + HTMLHelper::_('select.option', 'yes', Text::_('JYES')), + HTMLHelper::_('select.option', 'no', Text::_('JNO')) ); /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ @@ -32,33 +34,33 @@ ?>
-
-
- -
- -
-
-
-
- - - - -
-
-
-
- - - - -
-
-
+
+
+ +
+ +
+
+
+
+ + + + +
+
+
+
+ + + + +
+
+
diff --git a/administrator/components/com_users/tmpl/users/default_batch_footer.php b/administrator/components/com_users/tmpl/users/default_batch_footer.php index 7f35695fde22f..e8db055384e61 100644 --- a/administrator/components/com_users/tmpl/users/default_batch_footer.php +++ b/administrator/components/com_users/tmpl/users/default_batch_footer.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; ?> diff --git a/administrator/components/com_users/tmpl/users/modal.php b/administrator/components/com_users/tmpl/users/modal.php index c1dae97df77ef..787156ab8c865 100644 --- a/administrator/components/com_users/tmpl/users/modal.php +++ b/administrator/components/com_users/tmpl/users/modal.php @@ -1,4 +1,5 @@
-
- -
-   -
- - $this)); ?> - items)) : ?> -
- - -
- - - - - - - - - - - - - - - - items as $item) : ?> - - - - - - - - - - -
- , - , - -
- - - - - - - - - - - -
- - escape($item->name); ?> - - - escape($item->username); ?> - - - - - - - - - - group_names, false); ?> - - id; ?> -
+ + +
+   +
+ + $this)); ?> + items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + items as $item) : ?> + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + +
+ + escape($item->name); ?> + + + escape($item->username); ?> + + + + + + + + + + group_names, false); ?> + + id; ?> +
- - pagination->getListFooter(); ?> + + pagination->getListFooter(); ?> - - - - - - -
+ + + + + + +
diff --git a/administrator/components/com_workflow/services/provider.php b/administrator/components/com_workflow/services/provider.php index dbaf8e74cc1b8..81471b5803650 100644 --- a/administrator/components/com_workflow/services/provider.php +++ b/administrator/components/com_workflow/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Workflow')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Workflow')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Workflow')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Workflow')); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_workflow/src/Controller/DisplayController.php b/administrator/components/com_workflow/src/Controller/DisplayController.php index 7be435b39e32e..d48c96dfc149e 100644 --- a/administrator/components/com_workflow/src/Controller/DisplayController.php +++ b/administrator/components/com_workflow/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ extension)) - { - $extension = $this->input->getCmd('extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (empty($this->extension)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); - } - } - } - - /** - * Method to display a view. - * - * @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()}. - * - * @return BaseController|boolean This object to support chaining. - * - * @since 1.5 - */ - public function display($cachable = false, $urlparams = array()) - { - $view = $this->input->get('view'); - $layout = $this->input->get('layout'); - $id = $this->input->getInt('id'); - - // Check for edit form. - if (in_array($view, ['workflow', 'stage', 'transition']) && $layout == 'edit' && !$this->checkEditId('com_workflow.edit.' . $view, $id)) - { - // Somehow the person just went to the form - we don't allow that. - if (!\count($this->app->getMessageQueue())) - { - $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); - } - - $url = 'index.php?option=com_workflow&view=' . Inflector::pluralize($view) . '&extension=' . $this->input->getCmd('extension'); - - $this->setRedirect(Route::_($url, false)); - - return false; - } - - return parent::display(); - } + /** + * The default view. + * + * @var string + * @since 4.0.0 + */ + protected $default_view = 'workflows'; + + /** + * The extension for which the workflow apply. + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \InvalidArgumentException when no extension is set + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // If extension is not set try to get it from input or throw an exception + if (empty($this->extension)) { + $extension = $this->input->getCmd('extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (empty($this->extension)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); + } + } + } + + /** + * Method to display a view. + * + * @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()}. + * + * @return BaseController|boolean This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = array()) + { + $view = $this->input->get('view'); + $layout = $this->input->get('layout'); + $id = $this->input->getInt('id'); + + // Check for edit form. + if (in_array($view, ['workflow', 'stage', 'transition']) && $layout == 'edit' && !$this->checkEditId('com_workflow.edit.' . $view, $id)) { + // Somehow the person just went to the form - we don't allow that. + if (!\count($this->app->getMessageQueue())) { + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 'error'); + } + + $url = 'index.php?option=com_workflow&view=' . Inflector::pluralize($view) . '&extension=' . $this->input->getCmd('extension'); + + $this->setRedirect(Route::_($url, false)); + + return false; + } + + return parent::display(); + } } diff --git a/administrator/components/com_workflow/src/Controller/StageController.php b/administrator/components/com_workflow/src/Controller/StageController.php index 63be67755ad4d..7cc76ce9b5795 100644 --- a/administrator/components/com_workflow/src/Controller/StageController.php +++ b/administrator/components/com_workflow/src/Controller/StageController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\Controller; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Language\Text; @@ -23,159 +23,151 @@ */ class StageController extends FormController { - /** - * The workflow in where the stage belongs to - * - * @var integer - * @since 4.0.0 - */ - protected $workflowId; - - /** - * The extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * @param CMSApplication $app The Application for the dispatcher - * @param Input $input Input - * - * @since 4.0.0 - * @throws \InvalidArgumentException when no extension or workflow id is set - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - parent::__construct($config, $factory, $app, $input); - - // If workflow id is not set try to get it from input or throw an exception - if (empty($this->workflowId)) - { - $this->workflowId = $this->input->getInt('workflow_id'); - - if (empty($this->workflowId)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); - } - } - - // If extension is not set try to get it from input or throw an exception - if (empty($this->extension)) - { - $extension = $this->input->getCmd('extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (empty($this->extension)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); - } - } - } - - /** - * Method to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowAdd($data = array()) - { - return $this->app->getIdentity()->authorise('core.create', $this->extension . '.workflow.' . (int) $this->workflowId); - } - - /** - * Method to check if you can edit a record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = isset($data[$key]) ? (int) $data[$key] : 0; - $user = $this->app->getIdentity(); - - $record = $this->getModel()->getItem($recordId); - - if (empty($record->id)) - { - return false; - } - - // Check "edit" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit', $this->extension . '.stage.' . $recordId)) - { - return true; - } - - // Check "edit own" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit.own', $this->extension . '.stage.' . $recordId)) - { - return !empty($record) && $record->created_by == $user->id; - } - - return false; - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $append = parent::getRedirectToItemAppend($recordId); - - $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); - - return $append; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); - - return $append; - } + /** + * The workflow in where the stage belongs to + * + * @var integer + * @since 4.0.0 + */ + protected $workflowId; + + /** + * The extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \InvalidArgumentException when no extension or workflow id is set + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // If workflow id is not set try to get it from input or throw an exception + if (empty($this->workflowId)) { + $this->workflowId = $this->input->getInt('workflow_id'); + + if (empty($this->workflowId)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); + } + } + + // If extension is not set try to get it from input or throw an exception + if (empty($this->extension)) { + $extension = $this->input->getCmd('extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (empty($this->extension)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); + } + } + } + + /** + * Method to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowAdd($data = array()) + { + return $this->app->getIdentity()->authorise('core.create', $this->extension . '.workflow.' . (int) $this->workflowId); + } + + /** + * Method to check if you can edit a record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = isset($data[$key]) ? (int) $data[$key] : 0; + $user = $this->app->getIdentity(); + + $record = $this->getModel()->getItem($recordId); + + if (empty($record->id)) { + return false; + } + + // Check "edit" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit', $this->extension . '.stage.' . $recordId)) { + return true; + } + + // Check "edit own" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit.own', $this->extension . '.stage.' . $recordId)) { + return !empty($record) && $record->created_by == $user->id; + } + + return false; + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $append = parent::getRedirectToItemAppend($recordId); + + $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); + + return $append; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); + + return $append; + } } diff --git a/administrator/components/com_workflow/src/Controller/StagesController.php b/administrator/components/com_workflow/src/Controller/StagesController.php index 61be18cba531c..7f64bac3f9e13 100644 --- a/administrator/components/com_workflow/src/Controller/StagesController.php +++ b/administrator/components/com_workflow/src/Controller/StagesController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\Controller; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Language\Text; @@ -25,182 +25,170 @@ */ class StagesController extends AdminController { - /** - * The workflow in where the stage belongs to - * - * @var integer - * @since 4.0.0 - */ - protected $workflowId; - - /** - * The extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * The prefix to use with controller messages. - * - * @var string - * @since 4.0.0 - */ - protected $text_prefix = 'COM_WORKFLOW_STAGES'; - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * @param CMSApplication $app The Application for the dispatcher - * @param Input $input Input - * - * @since 4.0.0 - * @throws \InvalidArgumentException when no extension or workflow id is set - */ - public function __construct(array $config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - parent::__construct($config, $factory, $app, $input); - - // If workflow id is not set try to get it from input or throw an exception - if (empty($this->workflowId)) - { - $this->workflowId = $this->input->getInt('workflow_id'); - - if (empty($this->workflowId)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); - } - } - - // If extension is not set try to get it from input or throw an exception - if (empty($this->extension)) - { - $extension = $this->input->getCmd('extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (empty($this->extension)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); - } - } - - $this->registerTask('unsetDefault', 'setDefault'); - } - - /** - * Proxy for getModel - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config The array of possible config values. Optional. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. - * - * @since 4.0.0 - */ - public function getModel($name = 'Stage', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to set the home property for a list of items - * - * @return void - * - * @since 4.0.0 - */ - public function setDefault() - { - // Check for request forgeries - $this->checkToken(); - - // Get items to publish from the request. - $cid = (array) $this->input->get('cid', array(), 'int'); - $data = array('setDefault' => 1, 'unsetDefault' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($data, $task, 0, 'int'); - - if (!$value) - { - $this->setMessage(Text::_('COM_WORKFLOW_DISABLE_DEFAULT'), 'warning'); - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . '&extension=' . $this->extension, false - ) - ); - - return; - } - - // Remove zero values resulting from input filter - $cid = array_filter($cid); - - if (empty($cid)) - { - $this->setMessage(Text::_('COM_WORKFLOW_NO_ITEM_SELECTED'), 'warning'); - } - elseif (count($cid) > 1) - { - $this->setMessage(Text::_('COM_WORKFLOW_TOO_MANY_STAGES'), 'error'); - } - else - { - // Get the model. - $model = $this->getModel(); - - // Make sure the item ids are integers - $id = reset($cid); - - // Publish the items. - if (!$model->setDefault($id, $value)) - { - $this->setMessage($model->getError(), 'warning'); - } - else - { - $this->setMessage(Text::_('COM_WORKFLOW_STAGE_SET_DEFAULT')); - } - } - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . '&extension=' . $this->extension - . '&workflow_id=' . $this->workflowId, false - ) - ); - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - return '&extension=' . $this->extension . ($this->section ? '.' . $this->section : '') . '&workflow_id=' . $this->workflowId; - } + /** + * The workflow in where the stage belongs to + * + * @var integer + * @since 4.0.0 + */ + protected $workflowId; + + /** + * The extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * The prefix to use with controller messages. + * + * @var string + * @since 4.0.0 + */ + protected $text_prefix = 'COM_WORKFLOW_STAGES'; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \InvalidArgumentException when no extension or workflow id is set + */ + public function __construct(array $config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // If workflow id is not set try to get it from input or throw an exception + if (empty($this->workflowId)) { + $this->workflowId = $this->input->getInt('workflow_id'); + + if (empty($this->workflowId)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); + } + } + + // If extension is not set try to get it from input or throw an exception + if (empty($this->extension)) { + $extension = $this->input->getCmd('extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (empty($this->extension)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); + } + } + + $this->registerTask('unsetDefault', 'setDefault'); + } + + /** + * Proxy for getModel + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config The array of possible config values. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 4.0.0 + */ + public function getModel($name = 'Stage', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to set the home property for a list of items + * + * @return void + * + * @since 4.0.0 + */ + public function setDefault() + { + // Check for request forgeries + $this->checkToken(); + + // Get items to publish from the request. + $cid = (array) $this->input->get('cid', array(), 'int'); + $data = array('setDefault' => 1, 'unsetDefault' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($data, $task, 0, 'int'); + + if (!$value) { + $this->setMessage(Text::_('COM_WORKFLOW_DISABLE_DEFAULT'), 'warning'); + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . '&extension=' . $this->extension, + false + ) + ); + + return; + } + + // Remove zero values resulting from input filter + $cid = array_filter($cid); + + if (empty($cid)) { + $this->setMessage(Text::_('COM_WORKFLOW_NO_ITEM_SELECTED'), 'warning'); + } elseif (count($cid) > 1) { + $this->setMessage(Text::_('COM_WORKFLOW_TOO_MANY_STAGES'), 'error'); + } else { + // Get the model. + $model = $this->getModel(); + + // Make sure the item ids are integers + $id = reset($cid); + + // Publish the items. + if (!$model->setDefault($id, $value)) { + $this->setMessage($model->getError(), 'warning'); + } else { + $this->setMessage(Text::_('COM_WORKFLOW_STAGE_SET_DEFAULT')); + } + } + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . '&extension=' . $this->extension + . '&workflow_id=' . $this->workflowId, + false + ) + ); + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + return '&extension=' . $this->extension . ($this->section ? '.' . $this->section : '') . '&workflow_id=' . $this->workflowId; + } } diff --git a/administrator/components/com_workflow/src/Controller/TransitionController.php b/administrator/components/com_workflow/src/Controller/TransitionController.php index fd7b19e89c586..f9fe617e1dca1 100644 --- a/administrator/components/com_workflow/src/Controller/TransitionController.php +++ b/administrator/components/com_workflow/src/Controller/TransitionController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\Controller; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Language\Text; @@ -23,160 +23,152 @@ */ class TransitionController extends FormController { - /** - * The workflow where the transition takes place - * - * @var integer - * @since 4.0.0 - */ - protected $workflowId; - - /** - * The extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * @param CMSApplication $app The Application for the dispatcher - * @param Input $input Input - * - * @since 4.0.0 - * @throws \InvalidArgumentException when no extension or workflow id is set - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - parent::__construct($config, $factory, $app, $input); - - // If workflow id is not set try to get it from input or throw an exception - if (empty($this->workflowId)) - { - $this->workflowId = $this->input->getInt('workflow_id'); - - if (empty($this->workflowId)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); - } - } - - // If extension is not set try to get it from input or throw an exception - if (empty($this->extension)) - { - $extension = $this->input->getCmd('extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (empty($this->extension)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); - } - } - } - - /** - * Method to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowAdd($data = array()) - { - return $this->app->getIdentity()->authorise('core.create', $this->extension . '.workflow.' . (int) $this->workflowId); - } - - /** - * Method to check if you can edit a record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = isset($data[$key]) ? (int) $data[$key] : 0; - $user = $this->app->getIdentity(); - - $model = $this->getModel(); - - $item = $model->getItem($recordId); - - if (empty($item->id)) - { - return false; - } - - // Check "edit" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit', $this->extension . '.transition.' . $recordId)) - { - return true; - } - - // Check "edit own" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit.own', $this->extension . '.transition.' . $recordId)) - { - return !empty($item) && $item->created_by == $user->id; - } - - return false; - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $append = parent::getRedirectToItemAppend($recordId); - $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension; - - return $append; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension; - - return $append; - } + /** + * The workflow where the transition takes place + * + * @var integer + * @since 4.0.0 + */ + protected $workflowId; + + /** + * The extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \InvalidArgumentException when no extension or workflow id is set + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // If workflow id is not set try to get it from input or throw an exception + if (empty($this->workflowId)) { + $this->workflowId = $this->input->getInt('workflow_id'); + + if (empty($this->workflowId)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); + } + } + + // If extension is not set try to get it from input or throw an exception + if (empty($this->extension)) { + $extension = $this->input->getCmd('extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (empty($this->extension)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); + } + } + } + + /** + * Method to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowAdd($data = array()) + { + return $this->app->getIdentity()->authorise('core.create', $this->extension . '.workflow.' . (int) $this->workflowId); + } + + /** + * Method to check if you can edit a record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = isset($data[$key]) ? (int) $data[$key] : 0; + $user = $this->app->getIdentity(); + + $model = $this->getModel(); + + $item = $model->getItem($recordId); + + if (empty($item->id)) { + return false; + } + + // Check "edit" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit', $this->extension . '.transition.' . $recordId)) { + return true; + } + + // Check "edit own" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit.own', $this->extension . '.transition.' . $recordId)) { + return !empty($item) && $item->created_by == $user->id; + } + + return false; + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $append = parent::getRedirectToItemAppend($recordId); + $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension; + + return $append; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + $append .= '&workflow_id=' . $this->workflowId . '&extension=' . $this->extension; + + return $append; + } } diff --git a/administrator/components/com_workflow/src/Controller/TransitionsController.php b/administrator/components/com_workflow/src/Controller/TransitionsController.php index 31670f514b4e3..c93467e64a289 100644 --- a/administrator/components/com_workflow/src/Controller/TransitionsController.php +++ b/administrator/components/com_workflow/src/Controller/TransitionsController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\Controller; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Language\Text; @@ -23,115 +23,110 @@ */ class TransitionsController extends AdminController { - /** - * The workflow where the transition takes place - * - * @var integer - * @since 4.0.0 - */ - protected $workflowId; - - /** - * The extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * The prefix to use with controller messages. - * - * @var string - * @since 4.0.0 - */ - protected $text_prefix = 'COM_WORKFLOW_TRANSITIONS'; - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * @param CMSApplication $app The Application for the dispatcher - * @param Input $input Input - * - * @since 4.0.0 - * @throws \InvalidArgumentException when no extension or workflow id is set - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - parent::__construct($config, $factory, $app, $input); - - // If workflow id is not set try to get it from input or throw an exception - if (empty($this->workflowId)) - { - $this->workflowId = $this->input->getInt('workflow_id'); - - if (empty($this->workflowId)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); - } - } - - // If extension is not set try to get it from input or throw an exception - if (empty($this->extension)) - { - $extension = $this->input->getCmd('extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (empty($this->extension)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); - } - } - } - - /** - * Proxy for getModel - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config The array of possible config values. Optional. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. - * - * @since 4.0.0 - */ - public function getModel($name = 'Transition', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - - $append .= '&extension=' . $this->extension . ($this->section ? '.' . $this->section : '') - . '&workflow_id=' . $this->workflowId; - - return $append; - } + /** + * The workflow where the transition takes place + * + * @var integer + * @since 4.0.0 + */ + protected $workflowId; + + /** + * The extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * The prefix to use with controller messages. + * + * @var string + * @since 4.0.0 + */ + protected $text_prefix = 'COM_WORKFLOW_TRANSITIONS'; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \InvalidArgumentException when no extension or workflow id is set + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // If workflow id is not set try to get it from input or throw an exception + if (empty($this->workflowId)) { + $this->workflowId = $this->input->getInt('workflow_id'); + + if (empty($this->workflowId)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_WORKFLOW_ID_NOT_SET')); + } + } + + // If extension is not set try to get it from input or throw an exception + if (empty($this->extension)) { + $extension = $this->input->getCmd('extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (empty($this->extension)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); + } + } + } + + /** + * Proxy for getModel + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config The array of possible config values. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 4.0.0 + */ + public function getModel($name = 'Transition', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + + $append .= '&extension=' . $this->extension . ($this->section ? '.' . $this->section : '') + . '&workflow_id=' . $this->workflowId; + + return $append; + } } diff --git a/administrator/components/com_workflow/src/Controller/WorkflowController.php b/administrator/components/com_workflow/src/Controller/WorkflowController.php index 9d4208b24310b..5ecc903ab74dd 100644 --- a/administrator/components/com_workflow/src/Controller/WorkflowController.php +++ b/administrator/components/com_workflow/src/Controller/WorkflowController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\Controller; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Language\Text; @@ -25,223 +25,214 @@ */ class WorkflowController extends FormController { - /** - * The extension for which the workflows apply. - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * @param CMSApplication $app The Application for the dispatcher - * @param Input $input Input - * - * @since 4.0.0 - * @throws \InvalidArgumentException when no extension is set - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - parent::__construct($config, $factory, $app, $input); - - // If extension is not set try to get it from input or throw an exception - if (empty($this->extension)) - { - $extension = $this->input->getCmd('extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (empty($this->extension)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); - } - } - } - - /** - * Method to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowAdd($data = array()) - { - return $this->app->getIdentity()->authorise('core.create', $this->extension); - } - - /** - * Method to check if you can edit a record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = isset($data[$key]) ? (int) $data[$key] : 0; - $user = $this->app->getIdentity(); - - $record = $this->getModel()->getItem($recordId); - - if (empty($record->id)) - { - return false; - } - - // Check "edit" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit', $this->extension . '.workflow.' . $recordId)) - { - return true; - } - - // Check "edit own" permission on record asset (explicit or inherited) - if ($user->authorise('core.edit.own', $this->extension . '.workflow.' . $recordId)) - { - return !empty($record) && $record->created_by == $user->id; - } - - return false; - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') - { - $append = parent::getRedirectToItemAppend($recordId); - $append .= '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); - - return $append; - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - $append = parent::getRedirectToListAppend(); - $append .= '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); - - return $append; - } - - /** - * Function that allows child controller access to model data - * after the data has been saved. - * - * @param BaseDatabaseModel $model The data model object. - * @param array $validData The validated data. - * - * @return void - * - * @since 4.0.0 - */ - public function postSaveHook(BaseDatabaseModel $model, $validData = array()) - { - $task = $this->getTask(); - - // The save2copy task needs to be handled slightly differently. - if ($task === 'save2copy') - { - $table = $model->getTable(); - - $key = $table->getKeyName(); - - $recordId = (int) $this->input->getInt($key); - - // @todo Moves queries out of the controller. - $db = $model->getDbo(); - $query = $db->getQuery(true); - - $query->select('*') - ->from($db->quoteName('#__workflow_stages')) - ->where($db->quoteName('workflow_id') . ' = :id') - ->bind(':id', $recordId, ParameterType::INTEGER); - - $statuses = $db->setQuery($query)->loadAssocList(); - - $smodel = $this->getModel('Stage'); - - $workflowID = (int) $model->getState($model->getName() . '.id'); - - $mapping = []; - - foreach ($statuses as $status) - { - $table = $smodel->getTable(); - - $oldID = $status['id']; - - $status['workflow_id'] = $workflowID; - $status['id'] = 0; - - unset($status['asset_id']); - - $table->save($status); - - $mapping[$oldID] = (int) $table->id; - } - - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__workflow_transitions')) - ->where($db->quoteName('workflow_id') . ' = :id') - ->bind(':id', $recordId, ParameterType::INTEGER); - - $transitions = $db->setQuery($query)->loadAssocList(); - - $tmodel = $this->getModel('Transition'); - - foreach ($transitions as $transition) - { - $table = $tmodel->getTable(); - - $transition['from_stage_id'] = $transition['from_stage_id'] != -1 ? $mapping[$transition['from_stage_id']] : -1; - $transition['to_stage_id'] = $mapping[$transition['to_stage_id']]; + /** + * The extension for which the workflows apply. + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \InvalidArgumentException when no extension is set + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // If extension is not set try to get it from input or throw an exception + if (empty($this->extension)) { + $extension = $this->input->getCmd('extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (empty($this->extension)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); + } + } + } + + /** + * Method to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowAdd($data = array()) + { + return $this->app->getIdentity()->authorise('core.create', $this->extension); + } + + /** + * Method to check if you can edit a record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = isset($data[$key]) ? (int) $data[$key] : 0; + $user = $this->app->getIdentity(); + + $record = $this->getModel()->getItem($recordId); + + if (empty($record->id)) { + return false; + } + + // Check "edit" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit', $this->extension . '.workflow.' . $recordId)) { + return true; + } + + // Check "edit own" permission on record asset (explicit or inherited) + if ($user->authorise('core.edit.own', $this->extension . '.workflow.' . $recordId)) { + return !empty($record) && $record->created_by == $user->id; + } + + return false; + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'id') + { + $append = parent::getRedirectToItemAppend($recordId); + $append .= '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); + + return $append; + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + $append = parent::getRedirectToListAppend(); + $append .= '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); + + return $append; + } + + /** + * Function that allows child controller access to model data + * after the data has been saved. + * + * @param BaseDatabaseModel $model The data model object. + * @param array $validData The validated data. + * + * @return void + * + * @since 4.0.0 + */ + public function postSaveHook(BaseDatabaseModel $model, $validData = array()) + { + $task = $this->getTask(); + + // The save2copy task needs to be handled slightly differently. + if ($task === 'save2copy') { + $table = $model->getTable(); + + $key = $table->getKeyName(); + + $recordId = (int) $this->input->getInt($key); + + // @todo Moves queries out of the controller. + $db = $model->getDbo(); + $query = $db->getQuery(true); + + $query->select('*') + ->from($db->quoteName('#__workflow_stages')) + ->where($db->quoteName('workflow_id') . ' = :id') + ->bind(':id', $recordId, ParameterType::INTEGER); + + $statuses = $db->setQuery($query)->loadAssocList(); + + $smodel = $this->getModel('Stage'); + + $workflowID = (int) $model->getState($model->getName() . '.id'); + + $mapping = []; + + foreach ($statuses as $status) { + $table = $smodel->getTable(); + + $oldID = $status['id']; + + $status['workflow_id'] = $workflowID; + $status['id'] = 0; + + unset($status['asset_id']); + + $table->save($status); + + $mapping[$oldID] = (int) $table->id; + } + + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__workflow_transitions')) + ->where($db->quoteName('workflow_id') . ' = :id') + ->bind(':id', $recordId, ParameterType::INTEGER); + + $transitions = $db->setQuery($query)->loadAssocList(); + + $tmodel = $this->getModel('Transition'); + + foreach ($transitions as $transition) { + $table = $tmodel->getTable(); + + $transition['from_stage_id'] = $transition['from_stage_id'] != -1 ? $mapping[$transition['from_stage_id']] : -1; + $transition['to_stage_id'] = $mapping[$transition['to_stage_id']]; - $transition['workflow_id'] = $workflowID; - $transition['id'] = 0; + $transition['workflow_id'] = $workflowID; + $transition['id'] = 0; - unset($transition['asset_id']); + unset($transition['asset_id']); - $table->save($transition); - } - } - } + $table->save($transition); + } + } + } } diff --git a/administrator/components/com_workflow/src/Controller/WorkflowsController.php b/administrator/components/com_workflow/src/Controller/WorkflowsController.php index 82ea1104490f6..1145466c77020 100644 --- a/administrator/components/com_workflow/src/Controller/WorkflowsController.php +++ b/administrator/components/com_workflow/src/Controller/WorkflowsController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\Controller; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Language\Text; @@ -25,163 +25,150 @@ */ class WorkflowsController extends AdminController { - /** - * The extension for which the workflows apply. - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Constructor. - * - * @param array $config An optional associative array of configuration settings. - * @param MVCFactoryInterface $factory The factory. - * @param CMSApplication $app The Application for the dispatcher - * @param Input $input Input - * - * @since 4.0.0 - * @throws \InvalidArgumentException when no extension is set - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - parent::__construct($config, $factory, $app, $input); - - // If extension is not set try to get it from input or throw an exception - if (empty($this->extension)) - { - $extension = $this->input->getCmd('extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (empty($this->extension)) - { - throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); - } - } - - $this->registerTask('unsetDefault', 'setDefault'); - } - - /** - * Proxy for getModel - * - * @param string $name The model name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config The array of possible config values. Optional. - * - * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. - * - * @since 4.0.0 - */ - public function getModel($name = 'Workflow', $prefix = 'Administrator', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Method to set the home property for a list of items - * - * @return void - * - * @since 4.0.0 - */ - public function setDefault() - { - // Check for request forgeries - $this->checkToken(); - - // Get items to publish from the request. - $cid = (array) $this->input->get('cid', array(), 'int'); - $data = array('setDefault' => 1, 'unsetDefault' => 0); - $task = $this->getTask(); - $value = ArrayHelper::getValue($data, $task, 0, 'int'); - - if (!$value) - { - $this->setMessage(Text::_('COM_WORKFLOW_DISABLE_DEFAULT'), 'warning'); - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''), false - ) - ); - - return; - } - - // Remove zero values resulting from input filter - $cid = array_filter($cid); - - if (empty($cid)) - { - $this->setMessage(Text::_('COM_WORKFLOW_NO_ITEM_SELECTED'), 'warning'); - } - elseif (count($cid) > 1) - { - $this->setMessage(Text::_('COM_WORKFLOW_TOO_MANY_WORKFLOWS'), 'error'); - } - else - { - // Get the model. - $model = $this->getModel(); - - // Make sure the item ids are integers - $id = reset($cid); - - // Publish the items. - if (!$model->setDefault($id, $value)) - { - $this->setMessage($model->getError(), 'warning'); - } - else - { - if ($value === 1) - { - $ntext = 'COM_WORKFLOW_SET_DEFAULT'; - } - else - { - $ntext = 'COM_WORKFLOW_ITEM_UNSET_DEFAULT'; - } - - $this->setMessage(Text::_($ntext, count($cid))); - } - } - - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list - . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''), false - ) - ); - } - - /** - * Gets the URL arguments to append to a list redirect. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToListAppend() - { - return '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); - } + /** + * The extension for which the workflows apply. + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The Application for the dispatcher + * @param Input $input Input + * + * @since 4.0.0 + * @throws \InvalidArgumentException when no extension is set + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // If extension is not set try to get it from input or throw an exception + if (empty($this->extension)) { + $extension = $this->input->getCmd('extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (empty($this->extension)) { + throw new \InvalidArgumentException(Text::_('COM_WORKFLOW_ERROR_EXTENSION_NOT_SET')); + } + } + + $this->registerTask('unsetDefault', 'setDefault'); + } + + /** + * Proxy for getModel + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config The array of possible config values. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 4.0.0 + */ + public function getModel($name = 'Workflow', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to set the home property for a list of items + * + * @return void + * + * @since 4.0.0 + */ + public function setDefault() + { + // Check for request forgeries + $this->checkToken(); + + // Get items to publish from the request. + $cid = (array) $this->input->get('cid', array(), 'int'); + $data = array('setDefault' => 1, 'unsetDefault' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($data, $task, 0, 'int'); + + if (!$value) { + $this->setMessage(Text::_('COM_WORKFLOW_DISABLE_DEFAULT'), 'warning'); + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''), + false + ) + ); + + return; + } + + // Remove zero values resulting from input filter + $cid = array_filter($cid); + + if (empty($cid)) { + $this->setMessage(Text::_('COM_WORKFLOW_NO_ITEM_SELECTED'), 'warning'); + } elseif (count($cid) > 1) { + $this->setMessage(Text::_('COM_WORKFLOW_TOO_MANY_WORKFLOWS'), 'error'); + } else { + // Get the model. + $model = $this->getModel(); + + // Make sure the item ids are integers + $id = reset($cid); + + // Publish the items. + if (!$model->setDefault($id, $value)) { + $this->setMessage($model->getError(), 'warning'); + } else { + if ($value === 1) { + $ntext = 'COM_WORKFLOW_SET_DEFAULT'; + } else { + $ntext = 'COM_WORKFLOW_ITEM_UNSET_DEFAULT'; + } + + $this->setMessage(Text::_($ntext, count($cid))); + } + } + + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_list + . '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''), + false + ) + ); + } + + /** + * Gets the URL arguments to append to a list redirect. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToListAppend() + { + return '&extension=' . $this->extension . ($this->section ? '.' . $this->section : ''); + } } diff --git a/administrator/components/com_workflow/src/Dispatcher/Dispatcher.php b/administrator/components/com_workflow/src/Dispatcher/Dispatcher.php index e34a303e7291e..077aba76c9452 100644 --- a/administrator/components/com_workflow/src/Dispatcher/Dispatcher.php +++ b/administrator/components/com_workflow/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ getApplication()->input->getCmd('extension'); + /** + * Workflows have to check for extension permission + * + * @return void + */ + protected function checkAccess() + { + $extension = $this->getApplication()->input->getCmd('extension'); - $parts = explode('.', $extension); + $parts = explode('.', $extension); - // Check the user has permission to access this component if in the backend - if ($this->app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.manage.workflow', $parts[0])) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + // Check the user has permission to access this component if in the backend + if ($this->app->isClient('administrator') && !$this->app->getIdentity()->authorise('core.manage.workflow', $parts[0])) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/administrator/components/com_workflow/src/Field/ComponentsWorkflowField.php b/administrator/components/com_workflow/src/Field/ComponentsWorkflowField.php index 681abc8f7b090..9d49792b07517 100644 --- a/administrator/components/com_workflow/src/Field/ComponentsWorkflowField.php +++ b/administrator/components/com_workflow/src/Field/ComponentsWorkflowField.php @@ -1,4 +1,5 @@ getDatabase(); - - $query = $db->getQuery(true) - ->select('DISTINCT a.name AS text, a.element AS value') - ->from('#__extensions as a') - ->where('a.enabled >= 1') - ->where('a.type =' . $db->quote('component')); - - $items = $db->setQuery($query)->loadObjectList(); - - $options = []; - - if (count($items)) - { - $lang = Factory::getLanguage(); - - $components = []; - - // Search for components supporting Fieldgroups - suppose that these components support fields as well - foreach ($items as &$item) - { - $availableActions = Access::getActionsFromFile( - JPATH_ADMINISTRATOR . '/components/' . $item->value . '/access.xml', - "/access/section[@name='workflow']/" - ); - - if (!empty($availableActions)) - { - // Load language - $source = JPATH_ADMINISTRATOR . '/components/' . $item->value; - $lang->load($item->value . 'sys', JPATH_ADMINISTRATOR) - || $lang->load($item->value . 'sys', $source); - - // Translate component name - $item->text = Text::_($item->text); - - $components[] = $item; - } - } - - if (empty($components)) - { - return []; - } - - foreach ($components as $component) - { - // Search for different contexts - $c = Factory::getApplication()->bootComponent($component->value); - - if ($c instanceof WorkflowServiceInterface) - { - $contexts = $c->getContexts(); - - foreach ($contexts as $context) - { - $newOption = new \stdClass; - $newOption->value = strtolower($component->value . '.' . $context); - $newOption->text = $component->text . ' - ' . Text::_($context); - $options[] = $newOption; - } - } - else - { - $options[] = $component; - } - } - - // Sort by name - $items = ArrayHelper::sortObjects($options, 'text', 1, true, true); - } - - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $items); - - return $options; - } + /** + * The form field type. + * + * @var string + * @since 3.7.0 + */ + protected $type = 'ComponentsWorkflow'; + + /** + * Method to get a list of options for a list input. + * + * @return array An array of JHtml options. + * + * @since 3.7.0 + */ + protected function getOptions() + { + // Initialise variable. + $db = $this->getDatabase(); + + $query = $db->getQuery(true) + ->select('DISTINCT a.name AS text, a.element AS value') + ->from('#__extensions as a') + ->where('a.enabled >= 1') + ->where('a.type =' . $db->quote('component')); + + $items = $db->setQuery($query)->loadObjectList(); + + $options = []; + + if (count($items)) { + $lang = Factory::getLanguage(); + + $components = []; + + // Search for components supporting Fieldgroups - suppose that these components support fields as well + foreach ($items as &$item) { + $availableActions = Access::getActionsFromFile( + JPATH_ADMINISTRATOR . '/components/' . $item->value . '/access.xml', + "/access/section[@name='workflow']/" + ); + + if (!empty($availableActions)) { + // Load language + $source = JPATH_ADMINISTRATOR . '/components/' . $item->value; + $lang->load($item->value . 'sys', JPATH_ADMINISTRATOR) + || $lang->load($item->value . 'sys', $source); + + // Translate component name + $item->text = Text::_($item->text); + + $components[] = $item; + } + } + + if (empty($components)) { + return []; + } + + foreach ($components as $component) { + // Search for different contexts + $c = Factory::getApplication()->bootComponent($component->value); + + if ($c instanceof WorkflowServiceInterface) { + $contexts = $c->getContexts(); + + foreach ($contexts as $context) { + $newOption = new \stdClass(); + $newOption->value = strtolower($component->value . '.' . $context); + $newOption->text = $component->text . ' - ' . Text::_($context); + $options[] = $newOption; + } + } else { + $options[] = $component; + } + } + + // Sort by name + $items = ArrayHelper::sortObjects($options, 'text', 1, true, true); + } + + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $items); + + return $options; + } } diff --git a/administrator/components/com_workflow/src/Field/WorkflowcontextsField.php b/administrator/components/com_workflow/src/Field/WorkflowcontextsField.php index 08752825cdfd0..e4407da0c6617 100644 --- a/administrator/components/com_workflow/src/Field/WorkflowcontextsField.php +++ b/administrator/components/com_workflow/src/Field/WorkflowcontextsField.php @@ -1,4 +1,5 @@ getOptions()) < 2) - { - $this->layout = 'joomla.form.field.hidden'; - } + /** + * Method to get the field input markup for a generic list. + * Use the multiple attribute to enable multiselect. + * + * @return string The field input markup. + * + * @since 4.0.0 + */ + protected function getInput() + { + if (count($this->getOptions()) < 2) { + $this->layout = 'joomla.form.field.hidden'; + } - return parent::getInput(); - } + return parent::getInput(); + } - /** - * Method to get the field options. - * - * @return array The field option objects. - * - * @since 4.0.0 - */ - protected function getOptions() - { - $parts = explode('.', $this->value); + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 4.0.0 + */ + protected function getOptions() + { + $parts = explode('.', $this->value); - $component = Factory::getApplication()->bootComponent($parts[0]); + $component = Factory::getApplication()->bootComponent($parts[0]); - if ($component instanceof WorkflowServiceInterface) - { - return $component->getWorkflowContexts(); - } + if ($component instanceof WorkflowServiceInterface) { + return $component->getWorkflowContexts(); + } - return []; - } + return []; + } } diff --git a/administrator/components/com_workflow/src/Helper/StageHelper.php b/administrator/components/com_workflow/src/Helper/StageHelper.php index 3348c32aa4ff9..a0ed5ed266170 100644 --- a/administrator/components/com_workflow/src/Helper/StageHelper.php +++ b/administrator/components/com_workflow/src/Helper/StageHelper.php @@ -1,4 +1,5 @@ option . '.' . $this->name; - $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); - - $this->setState('filter.extension', $extension); - } - - /** - * Method to change the title - * - * @param integer $categoryId The id of the category. - * @param string $alias The alias. - * @param string $title The title. - * - * @return array Contains the modified title and alias. - * - * @since 4.0.0 - */ - protected function generateNewTitle($categoryId, $alias, $title) - { - // Alter the title & alias - $table = $this->getTable(); - - while ($table->load(array('title' => $title))) - { - $title = StringHelper::increment($title); - } - - return array($title, $alias); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function save($data) - { - $table = $this->getTable(); - $context = $this->option . '.' . $this->name; - $app = Factory::getApplication(); - $user = $app->getIdentity(); - $input = $app->input; - $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); - - if (empty($data['workflow_id'])) - { - $data['workflow_id'] = $workflowID; - } - - $workflow = $this->getTable('Workflow'); - - $workflow->load($data['workflow_id']); - - $parts = explode('.', $workflow->extension); - - if (isset($data['rules']) && !$user->authorise('core.admin', $parts[0])) - { - unset($data['rules']); - } - - // Make sure we use the correct extension when editing an existing workflow - $key = $table->getKeyName(); - $pk = (isset($data[$key])) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); - - if ($pk > 0) - { - $table->load($pk); - - if ((int) $table->workflow_id) - { - $data['workflow_id'] = (int) $table->workflow_id; - } - } - - if ($input->get('task') == 'save2copy') - { - $origTable = clone $this->getTable(); - - // Alter the title for save as copy - if ($origTable->load(['title' => $data['title']])) - { - list($title) = $this->generateNewTitle(0, '', $data['title']); - $data['title'] = $title; - } - - $data['published'] = 0; - $data['default'] = 0; - } - - return parent::save($data); - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission for the component. - * - * @since 4.0.0 - */ - protected function canDelete($record) - { - $table = $this->getTable('Workflow', 'Administrator'); - - $table->load($record->workflow_id); - - if (empty($record->id) || $record->published != -2) - { - return false; - } - - $app = Factory::getApplication(); - $extension = $app->getUserStateFromRequest('com_workflow.stage.filter.extension', 'extension', null, 'cmd'); - - $parts = explode('.', $extension); - - $component = reset($parts); - - if (!Factory::getUser()->authorise('core.delete', $component . '.state.' . (int) $record->id) || $record->default) - { - $this->setError(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED')); - - return false; - } - - return true; - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 4.0.0 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - $app = Factory::getApplication(); - $context = $this->option . '.' . $this->name; - $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); - - if (!\property_exists($record, 'workflow_id')) - { - $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); - $record->workflow_id = $workflowID; - } - - // Check for existing workflow. - if (!empty($record->id)) - { - return $user->authorise('core.edit.state', $extension . '.state.' . (int) $record->id); - } - - // Default to component settings if workflow isn't known. - return $user->authorise('core.edit.state', $extension); - } - - /** - * Abstract method for getting the form from the model. - * - * @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 Form|boolean A Form object on success, false on failure - * - * @since 4.0.0 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm( - 'com_workflow.state', - 'stage', - array( - 'control' => 'jform', - 'load_data' => $loadData - ) - ); - - if (empty($form)) - { - return false; - } - - $id = $data['id'] ?? $form->getValue('id'); - - $item = $this->getItem($id); - - $canEditState = $this->canEditState((object) $item); - - // Modify the form based on access controls. - if (!$canEditState || !empty($item->default)) - { - if (!$canEditState) - { - $form->setFieldAttribute('published', 'disabled', 'true'); - $form->setFieldAttribute('published', 'required', 'false'); - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - $form->setFieldAttribute('default', 'disabled', 'true'); - $form->setFieldAttribute('default', 'required', 'false'); - $form->setFieldAttribute('default', 'filter', 'unset'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 4.0.0 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState( - 'com_workflow.edit.state.data', - array() - ); - - if (empty($data)) - { - $data = $this->getItem(); - } - - return $data; - } - - /** - * Method to change the home state of one or more items. - * - * @param array $pk A list of the primary keys to change. - * @param integer $value The value of the home state. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function setDefault($pk, $value = 1) - { - $table = $this->getTable(); - - if ($table->load($pk)) - { - if (!$table->published) - { - $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); - - return false; - } - } - - if (empty($table->id) || !$this->canEditState($table)) - { - Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); - - return false; - } - - if ($value) - { - // Verify that the home page for this language is unique per client id - if ($table->load(array('default' => '1', 'workflow_id' => $table->workflow_id))) - { - $table->default = 0; - $table->store(); - } - } - - if ($table->load($pk)) - { - $table->default = $value; - $table->store(); - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to change the published state of one or more records. - * - * @param array &$pks A list of the primary keys to change. - * @param integer $value The value of the published state. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function publish(&$pks, $value = 1) - { - $table = $this->getTable(); - $pks = (array) $pks; - $app = Factory::getApplication(); - $extension = $app->getUserStateFromRequest('com_workflow.state.filter.extension', 'extension', null, 'cmd'); - - // Default item existence checks. - if ($value != 1) - { - foreach ($pks as $i => $pk) - { - if ($table->load($pk) && $table->default) - { - // Prune items that you can't change. - $app->enqueueMessage(Text::_('COM_WORKFLOW_MSG_DISABLE_DEFAULT'), 'error'); - - unset($pks[$i]); - } - } - } - - return parent::publish($pks, $value); - } - - /** - * Method to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 4.0.0 - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $extension = Factory::getApplication()->input->get('extension'); - - $parts = explode('.', $extension); - - $extension = array_shift($parts); - - // Set the access control rules field component value. - $form->setFieldAttribute('rules', 'component', $extension); - - parent::preprocessForm($form, $data, $group); - } + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 4.0.0 + */ + public function populateState() + { + parent::populateState(); + + $app = Factory::getApplication(); + $context = $this->option . '.' . $this->name; + $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); + + $this->setState('filter.extension', $extension); + } + + /** + * Method to change the title + * + * @param integer $categoryId The id of the category. + * @param string $alias The alias. + * @param string $title The title. + * + * @return array Contains the modified title and alias. + * + * @since 4.0.0 + */ + protected function generateNewTitle($categoryId, $alias, $title) + { + // Alter the title & alias + $table = $this->getTable(); + + while ($table->load(array('title' => $title))) { + $title = StringHelper::increment($title); + } + + return array($title, $alias); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function save($data) + { + $table = $this->getTable(); + $context = $this->option . '.' . $this->name; + $app = Factory::getApplication(); + $user = $app->getIdentity(); + $input = $app->input; + $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); + + if (empty($data['workflow_id'])) { + $data['workflow_id'] = $workflowID; + } + + $workflow = $this->getTable('Workflow'); + + $workflow->load($data['workflow_id']); + + $parts = explode('.', $workflow->extension); + + if (isset($data['rules']) && !$user->authorise('core.admin', $parts[0])) { + unset($data['rules']); + } + + // Make sure we use the correct extension when editing an existing workflow + $key = $table->getKeyName(); + $pk = (isset($data[$key])) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); + + if ($pk > 0) { + $table->load($pk); + + if ((int) $table->workflow_id) { + $data['workflow_id'] = (int) $table->workflow_id; + } + } + + if ($input->get('task') == 'save2copy') { + $origTable = clone $this->getTable(); + + // Alter the title for save as copy + if ($origTable->load(['title' => $data['title']])) { + list($title) = $this->generateNewTitle(0, '', $data['title']); + $data['title'] = $title; + } + + $data['published'] = 0; + $data['default'] = 0; + } + + return parent::save($data); + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission for the component. + * + * @since 4.0.0 + */ + protected function canDelete($record) + { + $table = $this->getTable('Workflow', 'Administrator'); + + $table->load($record->workflow_id); + + if (empty($record->id) || $record->published != -2) { + return false; + } + + $app = Factory::getApplication(); + $extension = $app->getUserStateFromRequest('com_workflow.stage.filter.extension', 'extension', null, 'cmd'); + + $parts = explode('.', $extension); + + $component = reset($parts); + + if (!Factory::getUser()->authorise('core.delete', $component . '.state.' . (int) $record->id) || $record->default) { + $this->setError(Text::_('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED')); + + return false; + } + + return true; + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 4.0.0 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + $app = Factory::getApplication(); + $context = $this->option . '.' . $this->name; + $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); + + if (!\property_exists($record, 'workflow_id')) { + $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); + $record->workflow_id = $workflowID; + } + + // Check for existing workflow. + if (!empty($record->id)) { + return $user->authorise('core.edit.state', $extension . '.state.' . (int) $record->id); + } + + // Default to component settings if workflow isn't known. + return $user->authorise('core.edit.state', $extension); + } + + /** + * Abstract method for getting the form from the model. + * + * @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 Form|boolean A Form object on success, false on failure + * + * @since 4.0.0 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm( + 'com_workflow.state', + 'stage', + array( + 'control' => 'jform', + 'load_data' => $loadData + ) + ); + + if (empty($form)) { + return false; + } + + $id = $data['id'] ?? $form->getValue('id'); + + $item = $this->getItem($id); + + $canEditState = $this->canEditState((object) $item); + + // Modify the form based on access controls. + if (!$canEditState || !empty($item->default)) { + if (!$canEditState) { + $form->setFieldAttribute('published', 'disabled', 'true'); + $form->setFieldAttribute('published', 'required', 'false'); + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + $form->setFieldAttribute('default', 'disabled', 'true'); + $form->setFieldAttribute('default', 'required', 'false'); + $form->setFieldAttribute('default', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 4.0.0 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState( + 'com_workflow.edit.state.data', + array() + ); + + if (empty($data)) { + $data = $this->getItem(); + } + + return $data; + } + + /** + * Method to change the home state of one or more items. + * + * @param array $pk A list of the primary keys to change. + * @param integer $value The value of the home state. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function setDefault($pk, $value = 1) + { + $table = $this->getTable(); + + if ($table->load($pk)) { + if (!$table->published) { + $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); + + return false; + } + } + + if (empty($table->id) || !$this->canEditState($table)) { + Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); + + return false; + } + + if ($value) { + // Verify that the home page for this language is unique per client id + if ($table->load(array('default' => '1', 'workflow_id' => $table->workflow_id))) { + $table->default = 0; + $table->store(); + } + } + + if ($table->load($pk)) { + $table->default = $value; + $table->store(); + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to change the published state of one or more records. + * + * @param array &$pks A list of the primary keys to change. + * @param integer $value The value of the published state. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function publish(&$pks, $value = 1) + { + $table = $this->getTable(); + $pks = (array) $pks; + $app = Factory::getApplication(); + $extension = $app->getUserStateFromRequest('com_workflow.state.filter.extension', 'extension', null, 'cmd'); + + // Default item existence checks. + if ($value != 1) { + foreach ($pks as $i => $pk) { + if ($table->load($pk) && $table->default) { + // Prune items that you can't change. + $app->enqueueMessage(Text::_('COM_WORKFLOW_MSG_DISABLE_DEFAULT'), 'error'); + + unset($pks[$i]); + } + } + } + + return parent::publish($pks, $value); + } + + /** + * Method to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 4.0.0 + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $extension = Factory::getApplication()->input->get('extension'); + + $parts = explode('.', $extension); + + $extension = array_shift($parts); + + // Set the access control rules field component value. + $form->setFieldAttribute('rules', 'component', $extension); + + parent::preprocessForm($form, $data, $group); + } } diff --git a/administrator/components/com_workflow/src/Model/StagesModel.php b/administrator/components/com_workflow/src/Model/StagesModel.php index 53fb31fd4789b..74a2fa51379db 100644 --- a/administrator/components/com_workflow/src/Model/StagesModel.php +++ b/administrator/components/com_workflow/src/Model/StagesModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest($this->context . '.filter.workflow_id', 'workflow_id', 1, 'int'); - $extension = $app->getUserStateFromRequest($this->context . '.filter.extension', 'extension', null, 'cmd'); - - if ($workflowID) - { - $table = $this->getTable('Workflow', 'Administrator'); - - if ($table->load($workflowID)) - { - $this->setState('active_workflow', $table->title); - } - } - - $this->setState('filter.workflow_id', $workflowID); - $this->setState('filter.extension', $extension); - - parent::populateState($ordering, $direction); - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param object $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 4.0.0 - */ - protected function getReorderConditions($table) - { - return [ - $this->getDatabase()->quoteName('workflow_id') . ' = ' . (int) $table->workflow_id, - ]; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $type The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return \Joomla\CMS\Table\Table A Table object - * - * @since 4.0.0 - */ - public function getTable($type = 'Stage', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } - - /** - * Method to get the data that should be injected in the form. - * - * @return string The query to database. - * - * @since 4.0.0 - */ - public function getListQuery() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query - ->select( - [ - $db->quoteName('s.id'), - $db->quoteName('s.title'), - $db->quoteName('s.ordering'), - $db->quoteName('s.default'), - $db->quoteName('s.published'), - $db->quoteName('s.checked_out'), - $db->quoteName('s.checked_out_time'), - $db->quoteName('s.description'), - $db->quoteName('uc.name', 'editor'), - ] - ) - ->from($db->quoteName('#__workflow_stages', 's')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('s.checked_out')); - - // Filter by extension - if ($workflowID = (int) $this->getState('filter.workflow_id')) - { - $query->where($db->quoteName('s.workflow_id') . ' = :id') - ->bind(':id', $workflowID, ParameterType::INTEGER); - } - - $status = (string) $this->getState('filter.published'); - - // Filter by publish state - if (is_numeric($status)) - { - $status = (int) $status; - $query->where($db->quoteName('s.published') . ' = :status') - ->bind(':status', $status, ParameterType::INTEGER); - } - elseif ($status === '') - { - $query->where($db->quoteName('s.published') . ' IN (0, 1)'); - } - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where('(' . $db->quoteName('s.title') . ' LIKE :search1 OR ' . $db->quoteName('s.description') . ' LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 's.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Returns a workflow object - * - * @return object The workflow - * - * @since 4.0.0 - */ - public function getWorkflow() - { - $table = $this->getTable('Workflow', 'Administrator'); - - $workflowId = (int) $this->getState('filter.workflow_id'); - - if ($workflowId > 0) - { - $table->load($workflowId); - } - - return (object) $table->getProperties(); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @see JController + * @since 4.0.0 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 's.id', + 'title', 's.title', + 'ordering','s.ordering', + 'published', 's.published' + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 4.0.0 + */ + protected function populateState($ordering = 's.ordering', $direction = 'ASC') + { + $app = Factory::getApplication(); + + $workflowID = $app->getUserStateFromRequest($this->context . '.filter.workflow_id', 'workflow_id', 1, 'int'); + $extension = $app->getUserStateFromRequest($this->context . '.filter.extension', 'extension', null, 'cmd'); + + if ($workflowID) { + $table = $this->getTable('Workflow', 'Administrator'); + + if ($table->load($workflowID)) { + $this->setState('active_workflow', $table->title); + } + } + + $this->setState('filter.workflow_id', $workflowID); + $this->setState('filter.extension', $extension); + + parent::populateState($ordering, $direction); + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param object $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 4.0.0 + */ + protected function getReorderConditions($table) + { + return [ + $this->getDatabase()->quoteName('workflow_id') . ' = ' . (int) $table->workflow_id, + ]; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $type The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\Table\Table A Table object + * + * @since 4.0.0 + */ + public function getTable($type = 'Stage', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } + + /** + * Method to get the data that should be injected in the form. + * + * @return string The query to database. + * + * @since 4.0.0 + */ + public function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query + ->select( + [ + $db->quoteName('s.id'), + $db->quoteName('s.title'), + $db->quoteName('s.ordering'), + $db->quoteName('s.default'), + $db->quoteName('s.published'), + $db->quoteName('s.checked_out'), + $db->quoteName('s.checked_out_time'), + $db->quoteName('s.description'), + $db->quoteName('uc.name', 'editor'), + ] + ) + ->from($db->quoteName('#__workflow_stages', 's')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('s.checked_out')); + + // Filter by extension + if ($workflowID = (int) $this->getState('filter.workflow_id')) { + $query->where($db->quoteName('s.workflow_id') . ' = :id') + ->bind(':id', $workflowID, ParameterType::INTEGER); + } + + $status = (string) $this->getState('filter.published'); + + // Filter by publish state + if (is_numeric($status)) { + $status = (int) $status; + $query->where($db->quoteName('s.published') . ' = :status') + ->bind(':status', $status, ParameterType::INTEGER); + } elseif ($status === '') { + $query->where($db->quoteName('s.published') . ' IN (0, 1)'); + } + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where('(' . $db->quoteName('s.title') . ' LIKE :search1 OR ' . $db->quoteName('s.description') . ' LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 's.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Returns a workflow object + * + * @return object The workflow + * + * @since 4.0.0 + */ + public function getWorkflow() + { + $table = $this->getTable('Workflow', 'Administrator'); + + $workflowId = (int) $this->getState('filter.workflow_id'); + + if ($workflowId > 0) { + $table->load($workflowId); + } + + return (object) $table->getProperties(); + } } diff --git a/administrator/components/com_workflow/src/Model/TransitionModel.php b/administrator/components/com_workflow/src/Model/TransitionModel.php index 1e9708ccd5cc2..58df72b2879e3 100644 --- a/administrator/components/com_workflow/src/Model/TransitionModel.php +++ b/administrator/components/com_workflow/src/Model/TransitionModel.php @@ -1,4 +1,5 @@ option . '.' . $this->name; - $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); - - $this->setState('filter.extension', $extension); - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission for the component. - * - * @since 4.0.0 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->published != -2) - { - return false; - } - - $app = Factory::getApplication(); - $extension = $app->getUserStateFromRequest('com_workflow.transition.filter.extension', 'extension', null, 'cmd'); - - return Factory::getUser()->authorise('core.delete', $extension . '.transition.' . (int) $record->id); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 4.0.0 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - $app = Factory::getApplication(); - $context = $this->option . '.' . $this->name; - $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); - - if (!\property_exists($record, 'workflow_id')) - { - $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); - $record->workflow_id = $workflowID; - } - - // Check for existing workflow. - if (!empty($record->id)) - { - return $user->authorise('core.edit.state', $extension . '.transition.' . (int) $record->id); - } - - // Default to component settings if workflow isn't known. - return $user->authorise('core.edit.state', $extension); - } - - /** - * Method to get a single record. - * - * @param integer $pk The id of the primary key. - * - * @return \Joomla\CMS\Object\CMSObject|boolean Object on success, false on failure. - * - * @since 4.0.0 - */ - public function getItem($pk = null) - { - $item = parent::getItem($pk); - - if (property_exists($item, 'options')) - { - $registry = new Registry($item->options); - $item->options = $registry->toArray(); - } - - return $item; - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function save($data) - { - $table = $this->getTable(); - $context = $this->option . '.' . $this->name; - $app = Factory::getApplication(); - $user = $app->getIdentity(); - $input = $app->input; - - $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); - - if (empty($data['workflow_id'])) - { - $data['workflow_id'] = $workflowID; - } - - $workflow = $this->getTable('Workflow'); - - $workflow->load($data['workflow_id']); - - $parts = explode('.', $workflow->extension); - - if (isset($data['rules']) && !$user->authorise('core.admin', $parts[0])) - { - unset($data['rules']); - } - - // Make sure we use the correct workflow_id when editing an existing transition - $key = $table->getKeyName(); - $pk = (isset($data[$key])) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); - - if ($pk > 0) - { - $table->load($pk); - - if ((int) $table->workflow_id) - { - $data['workflow_id'] = (int) $table->workflow_id; - } - } - - if ($input->get('task') == 'save2copy') - { - $origTable = clone $this->getTable(); - - // Alter the title for save as copy - if ($origTable->load(['title' => $data['title']])) - { - list($title) = $this->generateNewTitle(0, '', $data['title']); - $data['title'] = $title; - } - - $data['published'] = 0; - } - - return parent::save($data); - } - - /** - * Method to change the title - * - * @param integer $categoryId The id of the category. - * @param string $alias The alias. - * @param string $title The title. - * - * @return array Contains the modified title and alias. - * - * @since 4.0.0 - */ - protected function generateNewTitle($categoryId, $alias, $title) - { - // Alter the title & alias - $table = $this->getTable(); - - while ($table->load(array('title' => $title))) - { - $title = StringHelper::increment($title); - } - - return array($title, $alias); - } - - /** - * Abstract method for getting the form from the model. - * - * @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 \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure - * - * @since 4.0.0 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm( - 'com_workflow.transition', - 'transition', - array( - 'control' => 'jform', - 'load_data' => $loadData - ) - ); - - if (empty($form)) - { - return false; - } - - $id = $data['id'] ?? $form->getValue('id'); - - $item = $this->getItem($id); - - $canEditState = $this->canEditState((object) $item); - - // Modify the form based on access controls. - if (!$canEditState) - { - $form->setFieldAttribute('published', 'disabled', 'true'); - $form->setFieldAttribute('published', 'required', 'false'); - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - if (!empty($item->workflow_id)) - { - $data['workflow_id'] = (int) $item->workflow_id; - } - - if (empty($data['workflow_id'])) - { - $context = $this->option . '.' . $this->name; - - $data['workflow_id'] = (int) Factory::getApplication()->getUserStateFromRequest( - $context . '.filter.workflow_id', 'workflow_id', - 0, - 'int' - ); - } - - $where = $this->getDatabase()->quoteName('workflow_id') . ' = ' . (int) $data['workflow_id']; - $where .= ' AND ' . $this->getDatabase()->quoteName('published') . ' = 1'; - - $form->setFieldAttribute('from_stage_id', 'sql_where', $where); - $form->setFieldAttribute('to_stage_id', 'sql_where', $where); - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 4.0.0 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState( - 'com_workflow.edit.transition.data', - array() - ); - - if (empty($data)) - { - $data = $this->getItem(); - } - - return $data; - } - - public function getWorkflow() - { - $app = Factory::getApplication(); - - $context = $this->option . '.' . $this->name; - - $workflow_id = (int) $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); - - $workflow = $this->getTable('Workflow'); - - $workflow->load($workflow_id); - - return (object) $workflow->getProperties(); - } - - /** - * Trigger the form preparation for the workflow group - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @see FormField - * @since 4.0.0 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $extension = Factory::getApplication()->input->get('extension'); - - $parts = explode('.', $extension); - - $extension = array_shift($parts); - - // Set the access control rules field component value. - $form->setFieldAttribute('rules', 'component', $extension); - - // Import the appropriate plugin group. - PluginHelper::importPlugin('workflow'); - - parent::preprocessForm($form, $data, $group); - } + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 4.0.0 + */ + public function populateState() + { + parent::populateState(); + + $app = Factory::getApplication(); + $context = $this->option . '.' . $this->name; + $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); + + $this->setState('filter.extension', $extension); + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission for the component. + * + * @since 4.0.0 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->published != -2) { + return false; + } + + $app = Factory::getApplication(); + $extension = $app->getUserStateFromRequest('com_workflow.transition.filter.extension', 'extension', null, 'cmd'); + + return Factory::getUser()->authorise('core.delete', $extension . '.transition.' . (int) $record->id); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 4.0.0 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + $app = Factory::getApplication(); + $context = $this->option . '.' . $this->name; + $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); + + if (!\property_exists($record, 'workflow_id')) { + $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); + $record->workflow_id = $workflowID; + } + + // Check for existing workflow. + if (!empty($record->id)) { + return $user->authorise('core.edit.state', $extension . '.transition.' . (int) $record->id); + } + + // Default to component settings if workflow isn't known. + return $user->authorise('core.edit.state', $extension); + } + + /** + * Method to get a single record. + * + * @param integer $pk The id of the primary key. + * + * @return \Joomla\CMS\Object\CMSObject|boolean Object on success, false on failure. + * + * @since 4.0.0 + */ + public function getItem($pk = null) + { + $item = parent::getItem($pk); + + if (property_exists($item, 'options')) { + $registry = new Registry($item->options); + $item->options = $registry->toArray(); + } + + return $item; + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function save($data) + { + $table = $this->getTable(); + $context = $this->option . '.' . $this->name; + $app = Factory::getApplication(); + $user = $app->getIdentity(); + $input = $app->input; + + $workflowID = $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); + + if (empty($data['workflow_id'])) { + $data['workflow_id'] = $workflowID; + } + + $workflow = $this->getTable('Workflow'); + + $workflow->load($data['workflow_id']); + + $parts = explode('.', $workflow->extension); + + if (isset($data['rules']) && !$user->authorise('core.admin', $parts[0])) { + unset($data['rules']); + } + + // Make sure we use the correct workflow_id when editing an existing transition + $key = $table->getKeyName(); + $pk = (isset($data[$key])) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); + + if ($pk > 0) { + $table->load($pk); + + if ((int) $table->workflow_id) { + $data['workflow_id'] = (int) $table->workflow_id; + } + } + + if ($input->get('task') == 'save2copy') { + $origTable = clone $this->getTable(); + + // Alter the title for save as copy + if ($origTable->load(['title' => $data['title']])) { + list($title) = $this->generateNewTitle(0, '', $data['title']); + $data['title'] = $title; + } + + $data['published'] = 0; + } + + return parent::save($data); + } + + /** + * Method to change the title + * + * @param integer $categoryId The id of the category. + * @param string $alias The alias. + * @param string $title The title. + * + * @return array Contains the modified title and alias. + * + * @since 4.0.0 + */ + protected function generateNewTitle($categoryId, $alias, $title) + { + // Alter the title & alias + $table = $this->getTable(); + + while ($table->load(array('title' => $title))) { + $title = StringHelper::increment($title); + } + + return array($title, $alias); + } + + /** + * Abstract method for getting the form from the model. + * + * @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 \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure + * + * @since 4.0.0 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm( + 'com_workflow.transition', + 'transition', + array( + 'control' => 'jform', + 'load_data' => $loadData + ) + ); + + if (empty($form)) { + return false; + } + + $id = $data['id'] ?? $form->getValue('id'); + + $item = $this->getItem($id); + + $canEditState = $this->canEditState((object) $item); + + // Modify the form based on access controls. + if (!$canEditState) { + $form->setFieldAttribute('published', 'disabled', 'true'); + $form->setFieldAttribute('published', 'required', 'false'); + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + if (!empty($item->workflow_id)) { + $data['workflow_id'] = (int) $item->workflow_id; + } + + if (empty($data['workflow_id'])) { + $context = $this->option . '.' . $this->name; + + $data['workflow_id'] = (int) Factory::getApplication()->getUserStateFromRequest( + $context . '.filter.workflow_id', + 'workflow_id', + 0, + 'int' + ); + } + + $where = $this->getDatabase()->quoteName('workflow_id') . ' = ' . (int) $data['workflow_id']; + $where .= ' AND ' . $this->getDatabase()->quoteName('published') . ' = 1'; + + $form->setFieldAttribute('from_stage_id', 'sql_where', $where); + $form->setFieldAttribute('to_stage_id', 'sql_where', $where); + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 4.0.0 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState( + 'com_workflow.edit.transition.data', + array() + ); + + if (empty($data)) { + $data = $this->getItem(); + } + + return $data; + } + + public function getWorkflow() + { + $app = Factory::getApplication(); + + $context = $this->option . '.' . $this->name; + + $workflow_id = (int) $app->getUserStateFromRequest($context . '.filter.workflow_id', 'workflow_id', 0, 'int'); + + $workflow = $this->getTable('Workflow'); + + $workflow->load($workflow_id); + + return (object) $workflow->getProperties(); + } + + /** + * Trigger the form preparation for the workflow group + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @see FormField + * @since 4.0.0 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $extension = Factory::getApplication()->input->get('extension'); + + $parts = explode('.', $extension); + + $extension = array_shift($parts); + + // Set the access control rules field component value. + $form->setFieldAttribute('rules', 'component', $extension); + + // Import the appropriate plugin group. + PluginHelper::importPlugin('workflow'); + + parent::preprocessForm($form, $data, $group); + } } diff --git a/administrator/components/com_workflow/src/Model/TransitionsModel.php b/administrator/components/com_workflow/src/Model/TransitionsModel.php index 8ecf6afd2ea3e..53b178bc67dd6 100644 --- a/administrator/components/com_workflow/src/Model/TransitionsModel.php +++ b/administrator/components/com_workflow/src/Model/TransitionsModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest($this->context . '.filter.workflow_id', 'workflow_id', 1, 'int'); - $extension = $app->getUserStateFromRequest($this->context . '.filter.extension', 'extension', null, 'cmd'); - - if ($workflowID) - { - $table = $this->getTable('Workflow', 'Administrator'); - - if ($table->load($workflowID)) - { - $this->setState('active_workflow', $table->title); - } - } - - $this->setState('filter.workflow_id', $workflowID); - $this->setState('filter.extension', $extension); - - parent::populateState($ordering, $direction); - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $type The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return \Joomla\CMS\Table\Table A Table object - * - * @since 4.0.0 - */ - public function getTable($type = 'Transition', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param object $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 4.0.0 - */ - protected function getReorderConditions($table) - { - return [ - $this->getDatabase()->quoteName('workflow_id') . ' = ' . (int) $table->workflow_id, - ]; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return string The query to database. - * - * @since 4.0.0 - */ - public function getListQuery() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query - ->select( - [ - $db->quoteName('t.id'), - $db->quoteName('t.title'), - $db->quoteName('t.from_stage_id'), - $db->quoteName('t.to_stage_id'), - $db->quoteName('t.published'), - $db->quoteName('t.checked_out'), - $db->quoteName('t.checked_out_time'), - $db->quoteName('t.ordering'), - $db->quoteName('t.description'), - $db->quoteName('f_stage.title', 'from_stage'), - $db->quoteName('t_stage.title', 'to_stage'), - $db->quoteName('uc.name', 'editor'), - ] - ) - ->from($db->quoteName('#__workflow_transitions', 't')) - ->join('LEFT', $db->quoteName('#__workflow_stages', 'f_stage'), $db->quoteName('f_stage.id') . ' = ' . $db->quoteName('t.from_stage_id')) - ->join('LEFT', $db->quoteName('#__workflow_stages', 't_stage'), $db->quoteName('t_stage.id') . ' = ' . $db->quoteName('t.to_stage_id')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('t.checked_out')); - - // Filter by extension - if ($workflowID = (int) $this->getState('filter.workflow_id')) - { - $query->where($db->quoteName('t.workflow_id') . ' = :id') - ->bind(':id', $workflowID, ParameterType::INTEGER); - } - - $status = (string) $this->getState('filter.published'); - - // Filter by status - if (is_numeric($status)) - { - $status = (int) $status; - $query->where($db->quoteName('t.published') . ' = :status') - ->bind(':status', $status, ParameterType::INTEGER); - } - elseif ($status === '') - { - $query->where($db->quoteName('t.published') . ' IN (0, 1)'); - } - - // Filter by column from_stage_id - if ($fromStage = (int) $this->getState('filter.from_stage')) - { - $query->where($db->quoteName('from_stage_id') . ' = :fromStage') - ->bind(':fromStage', $fromStage, ParameterType::INTEGER); - } - - // Filter by column to_stage_id - if ($toStage = (int) $this->getState('filter.to_stage')) - { - $query->where($db->quoteName('to_stage_id') . ' = :toStage') - ->bind(':toStage', $toStage, ParameterType::INTEGER); - } - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where('(' . $db->quoteName('t.title') . ' LIKE :search1 OR ' . $db->quoteName('t.description') . ' LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - - // Add the list ordering clause. - $orderCol = $this->state->get('list.ordering', 't.id'); - $orderDirn = strtoupper($this->state->get('list.direction', 'ASC')); - - $query->order($db->escape($orderCol) . ' ' . ($orderDirn === 'DESC' ? 'DESC' : 'ASC')); - - return $query; - } - - /** - * Get the filter form - * - * @param array $data data - * @param boolean $loadData load current data - * - * @return \Joomla\CMS\Form\Form|boolean The Form object or false on error - * - * @since 4.0.0 - */ - public function getFilterForm($data = array(), $loadData = true) - { - $form = parent::getFilterForm($data, $loadData); - - $id = (int) $this->getState('filter.workflow_id'); - - if ($form) - { - $where = $this->getDatabase()->quoteName('workflow_id') . ' = ' . $id . ' AND ' . $this->getDatabase()->quoteName('published') . ' = 1'; - - $form->setFieldAttribute('from_stage', 'sql_where', $where, 'filter'); - $form->setFieldAttribute('to_stage', 'sql_where', $where, 'filter'); - } - - return $form; - } - - /** - * Returns a workflow object - * - * @return object The workflow - * - * @since 4.0.0 - */ - public function getWorkflow() - { - $table = $this->getTable('Workflow', 'Administrator'); - - $workflowId = (int) $this->getState('filter.workflow_id'); - - if ($workflowId > 0) - { - $table->load($workflowId); - } - - return (object) $table->getProperties(); - } - + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @see JController + * @since 4.0.0 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 't.id', + 'published', 't.published', + 'ordering', 't.ordering', + 'title', 't.title', + 'from_stage', 't.from_stage_id', + 'to_stage', 't.to_stage_id' + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 4.0.0 + */ + protected function populateState($ordering = 't.ordering', $direction = 'ASC') + { + $app = Factory::getApplication(); + $workflowID = $app->getUserStateFromRequest($this->context . '.filter.workflow_id', 'workflow_id', 1, 'int'); + $extension = $app->getUserStateFromRequest($this->context . '.filter.extension', 'extension', null, 'cmd'); + + if ($workflowID) { + $table = $this->getTable('Workflow', 'Administrator'); + + if ($table->load($workflowID)) { + $this->setState('active_workflow', $table->title); + } + } + + $this->setState('filter.workflow_id', $workflowID); + $this->setState('filter.extension', $extension); + + parent::populateState($ordering, $direction); + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $type The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\Table\Table A Table object + * + * @since 4.0.0 + */ + public function getTable($type = 'Transition', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param object $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 4.0.0 + */ + protected function getReorderConditions($table) + { + return [ + $this->getDatabase()->quoteName('workflow_id') . ' = ' . (int) $table->workflow_id, + ]; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return string The query to database. + * + * @since 4.0.0 + */ + public function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query + ->select( + [ + $db->quoteName('t.id'), + $db->quoteName('t.title'), + $db->quoteName('t.from_stage_id'), + $db->quoteName('t.to_stage_id'), + $db->quoteName('t.published'), + $db->quoteName('t.checked_out'), + $db->quoteName('t.checked_out_time'), + $db->quoteName('t.ordering'), + $db->quoteName('t.description'), + $db->quoteName('f_stage.title', 'from_stage'), + $db->quoteName('t_stage.title', 'to_stage'), + $db->quoteName('uc.name', 'editor'), + ] + ) + ->from($db->quoteName('#__workflow_transitions', 't')) + ->join('LEFT', $db->quoteName('#__workflow_stages', 'f_stage'), $db->quoteName('f_stage.id') . ' = ' . $db->quoteName('t.from_stage_id')) + ->join('LEFT', $db->quoteName('#__workflow_stages', 't_stage'), $db->quoteName('t_stage.id') . ' = ' . $db->quoteName('t.to_stage_id')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('t.checked_out')); + + // Filter by extension + if ($workflowID = (int) $this->getState('filter.workflow_id')) { + $query->where($db->quoteName('t.workflow_id') . ' = :id') + ->bind(':id', $workflowID, ParameterType::INTEGER); + } + + $status = (string) $this->getState('filter.published'); + + // Filter by status + if (is_numeric($status)) { + $status = (int) $status; + $query->where($db->quoteName('t.published') . ' = :status') + ->bind(':status', $status, ParameterType::INTEGER); + } elseif ($status === '') { + $query->where($db->quoteName('t.published') . ' IN (0, 1)'); + } + + // Filter by column from_stage_id + if ($fromStage = (int) $this->getState('filter.from_stage')) { + $query->where($db->quoteName('from_stage_id') . ' = :fromStage') + ->bind(':fromStage', $fromStage, ParameterType::INTEGER); + } + + // Filter by column to_stage_id + if ($toStage = (int) $this->getState('filter.to_stage')) { + $query->where($db->quoteName('to_stage_id') . ' = :toStage') + ->bind(':toStage', $toStage, ParameterType::INTEGER); + } + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where('(' . $db->quoteName('t.title') . ' LIKE :search1 OR ' . $db->quoteName('t.description') . ' LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } + + // Add the list ordering clause. + $orderCol = $this->state->get('list.ordering', 't.id'); + $orderDirn = strtoupper($this->state->get('list.direction', 'ASC')); + + $query->order($db->escape($orderCol) . ' ' . ($orderDirn === 'DESC' ? 'DESC' : 'ASC')); + + return $query; + } + + /** + * Get the filter form + * + * @param array $data data + * @param boolean $loadData load current data + * + * @return \Joomla\CMS\Form\Form|boolean The Form object or false on error + * + * @since 4.0.0 + */ + public function getFilterForm($data = array(), $loadData = true) + { + $form = parent::getFilterForm($data, $loadData); + + $id = (int) $this->getState('filter.workflow_id'); + + if ($form) { + $where = $this->getDatabase()->quoteName('workflow_id') . ' = ' . $id . ' AND ' . $this->getDatabase()->quoteName('published') . ' = 1'; + + $form->setFieldAttribute('from_stage', 'sql_where', $where, 'filter'); + $form->setFieldAttribute('to_stage', 'sql_where', $where, 'filter'); + } + + return $form; + } + + /** + * Returns a workflow object + * + * @return object The workflow + * + * @since 4.0.0 + */ + public function getWorkflow() + { + $table = $this->getTable('Workflow', 'Administrator'); + + $workflowId = (int) $this->getState('filter.workflow_id'); + + if ($workflowId > 0) { + $table->load($workflowId); + } + + return (object) $table->getProperties(); + } } diff --git a/administrator/components/com_workflow/src/Model/WorkflowModel.php b/administrator/components/com_workflow/src/Model/WorkflowModel.php index 6e232db185091..e361ef837b576 100644 --- a/administrator/components/com_workflow/src/Model/WorkflowModel.php +++ b/administrator/components/com_workflow/src/Model/WorkflowModel.php @@ -1,4 +1,5 @@ option . '.' . $this->name; - $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); - - $this->setState('filter.extension', $extension); - } - - /** - * Method to change the title - * - * @param integer $categoryId The id of the category. - * @param string $alias The alias. - * @param string $title The title. - * - * @return array Contains the modified title and alias. - * - * @since 4.0.0 - */ - protected function generateNewTitle($categoryId, $alias, $title) - { - // Alter the title & alias - $table = $this->getTable(); - - while ($table->load(array('title' => $title))) - { - $title = StringHelper::increment($title); - } - - return array($title, $alias); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function save($data) - { - $table = $this->getTable(); - $app = Factory::getApplication(); - $user = $app->getIdentity(); - $input = $app->input; - $context = $this->option . '.' . $this->name; - $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); - $data['extension'] = !empty($data['extension']) ? $data['extension'] : $extension; - - // Make sure we use the correct extension when editing an existing workflow - $key = $table->getKeyName(); - $pk = (isset($data[$key])) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); - - if ($pk > 0) - { - $table->load($pk); - - $data['extension'] = $table->extension; - } - - if (isset($data['rules']) && !$user->authorise('core.admin', $data['extension'])) - { - unset($data['rules']); - } - - if ($input->get('task') == 'save2copy') - { - $origTable = clone $this->getTable(); - - // Alter the title for save as copy - if ($origTable->load(['title' => $data['title']])) - { - list($title) = $this->generateNewTitle(0, '', $data['title']); - $data['title'] = $title; - } - - // Unpublish new copy - $data['published'] = 0; - $data['default'] = 0; - } - - $result = parent::save($data); - - // Create default stage for new workflow - if ($result && $input->getCmd('task') !== 'save2copy' && $this->getState($this->getName() . '.new')) - { - $workflow_id = (int) $this->getState($this->getName() . '.id'); - - $table = $this->getTable('Stage'); - - $table->id = 0; - $table->title = 'COM_WORKFLOW_BASIC_STAGE'; - $table->description = ''; - $table->workflow_id = $workflow_id; - $table->published = 1; - $table->default = 1; - - $table->store(); - } - - return $result; - } - - /** - * Abstract method for getting the form from the model. - * - * @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 \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure - * - * @since 4.0.0 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm( - 'com_workflow.workflow', - 'workflow', - array( - 'control' => 'jform', - 'load_data' => $loadData - ) - ); - - if (empty($form)) - { - return false; - } - - $id = $data['id'] ?? $form->getValue('id'); - - $item = $this->getItem($id); - - $canEditState = $this->canEditState((object) $item); - - // Modify the form based on access controls. - if (!$canEditState || !empty($item->default)) - { - if (!$canEditState) - { - $form->setFieldAttribute('published', 'disabled', 'true'); - $form->setFieldAttribute('published', 'required', 'false'); - $form->setFieldAttribute('published', 'filter', 'unset'); - } - - $form->setFieldAttribute('default', 'disabled', 'true'); - $form->setFieldAttribute('default', 'required', 'false'); - $form->setFieldAttribute('default', 'filter', 'unset'); - } - - $form->setFieldAttribute('created', 'default', Factory::getDate()->format('Y-m-d H:i:s')); - $form->setFieldAttribute('modified', 'default', Factory::getDate()->format('Y-m-d H:i:s')); - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 4.0.0 - */ - protected function loadFormData() - { - // Check the session for previously entered form data. - $data = Factory::getApplication()->getUserState( - 'com_workflow.edit.workflow.data', - array() - ); - - if (empty($data)) - { - $data = $this->getItem(); - } - - return $data; - } - - /** - * Method to preprocess the form. - * - * @param Form $form Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 4.0.0 - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $extension = Factory::getApplication()->input->get('extension'); - - $parts = explode('.', $extension); - - $extension = array_shift($parts); - - // Set the access control rules field component value. - $form->setFieldAttribute('rules', 'component', $extension); - - parent::preprocessForm($form, $data, $group); - } - - /** - * A protected method to get a set of ordering conditions. - * - * @param object $table A record object. - * - * @return array An array of conditions to add to ordering queries. - * - * @since 4.0.0 - */ - protected function getReorderConditions($table) - { - $db = $this->getDatabase(); - - return [ - $db->quoteName('extension') . ' = ' . $db->quote($table->extension), - ]; - } - - /** - * Method to change the default state of one item. - * - * @param array $pk A list of the primary keys to change. - * @param integer $value The value of the home state. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function setDefault($pk, $value = 1) - { - $table = $this->getTable(); - - if ($table->load($pk)) - { - if ($table->published !== 1) - { - $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); - - return false; - } - } - - if (empty($table->id) || !$this->canEditState($table)) - { - Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); - - return false; - } - - $date = Factory::getDate()->toSql(); - - if ($value) - { - // Unset other default item - if ($table->load( - [ - 'default' => '1', - 'extension' => $table->get('extension') - ] - )) - { - $table->default = 0; - $table->modified = $date; - $table->store(); - } - } - - if ($table->load($pk)) - { - $table->modified = $date; - $table->default = $value; - $table->store(); - } - - // Clean the cache - $this->cleanCache(); - - return true; - } - - /** - * Method to test whether a record can be deleted. - * - * @param object $record A record object. - * - * @return boolean True if allowed to delete the record. Defaults to the permission for the component. - * - * @since 4.0.0 - */ - protected function canDelete($record) - { - if (empty($record->id) || $record->published != -2) - { - return false; - } - - return Factory::getUser()->authorise('core.delete', $record->extension . '.workflow.' . (int) $record->id); - } - - /** - * Method to test whether a record can have its state changed. - * - * @param object $record A record object. - * - * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. - * - * @since 4.0.0 - */ - protected function canEditState($record) - { - $user = Factory::getUser(); - - // Check for existing workflow. - if (!empty($record->id)) - { - return $user->authorise('core.edit.state', $record->extension . '.workflow.' . (int) $record->id); - } - - // Default to component settings if workflow isn't known. - return $user->authorise('core.edit.state', $record->extension); - } - - /** - * Method to change the published state of one or more records. - * - * @param array &$pks A list of the primary keys to change. - * @param integer $value The value of the published state. - * - * @return boolean True on success. - * - * @since 4.0.0 - */ - public function publish(&$pks, $value = 1) - { - $table = $this->getTable(); - $pks = (array) $pks; - - $date = Factory::getDate()->toSql(); - - // Default workflow item check. - foreach ($pks as $i => $pk) - { - if ($table->load($pk) && $value != 1 && $table->default) - { - // Prune items that you can't change. - Factory::getApplication()->enqueueMessage(Text::_('COM_WORKFLOW_UNPUBLISH_DEFAULT_ERROR'), 'error'); - unset($pks[$i]); - break; - } - } - - // Clean the cache. - $this->cleanCache(); - - // Ensure that previous checks don't empty the array. - if (empty($pks)) - { - return true; - } - - $table->load($pk); - $table->modified = $date; - $table->store(); - - return parent::publish($pks, $value); - } + /** + * Auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 4.0.0 + */ + public function populateState() + { + parent::populateState(); + + $app = Factory::getApplication(); + $context = $this->option . '.' . $this->name; + $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); + + $this->setState('filter.extension', $extension); + } + + /** + * Method to change the title + * + * @param integer $categoryId The id of the category. + * @param string $alias The alias. + * @param string $title The title. + * + * @return array Contains the modified title and alias. + * + * @since 4.0.0 + */ + protected function generateNewTitle($categoryId, $alias, $title) + { + // Alter the title & alias + $table = $this->getTable(); + + while ($table->load(array('title' => $title))) { + $title = StringHelper::increment($title); + } + + return array($title, $alias); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function save($data) + { + $table = $this->getTable(); + $app = Factory::getApplication(); + $user = $app->getIdentity(); + $input = $app->input; + $context = $this->option . '.' . $this->name; + $extension = $app->getUserStateFromRequest($context . '.filter.extension', 'extension', null, 'cmd'); + $data['extension'] = !empty($data['extension']) ? $data['extension'] : $extension; + + // Make sure we use the correct extension when editing an existing workflow + $key = $table->getKeyName(); + $pk = (isset($data[$key])) ? $data[$key] : (int) $this->getState($this->getName() . '.id'); + + if ($pk > 0) { + $table->load($pk); + + $data['extension'] = $table->extension; + } + + if (isset($data['rules']) && !$user->authorise('core.admin', $data['extension'])) { + unset($data['rules']); + } + + if ($input->get('task') == 'save2copy') { + $origTable = clone $this->getTable(); + + // Alter the title for save as copy + if ($origTable->load(['title' => $data['title']])) { + list($title) = $this->generateNewTitle(0, '', $data['title']); + $data['title'] = $title; + } + + // Unpublish new copy + $data['published'] = 0; + $data['default'] = 0; + } + + $result = parent::save($data); + + // Create default stage for new workflow + if ($result && $input->getCmd('task') !== 'save2copy' && $this->getState($this->getName() . '.new')) { + $workflow_id = (int) $this->getState($this->getName() . '.id'); + + $table = $this->getTable('Stage'); + + $table->id = 0; + $table->title = 'COM_WORKFLOW_BASIC_STAGE'; + $table->description = ''; + $table->workflow_id = $workflow_id; + $table->published = 1; + $table->default = 1; + + $table->store(); + } + + return $result; + } + + /** + * Abstract method for getting the form from the model. + * + * @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 \Joomla\CMS\Form\Form|boolean A Form object on success, false on failure + * + * @since 4.0.0 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm( + 'com_workflow.workflow', + 'workflow', + array( + 'control' => 'jform', + 'load_data' => $loadData + ) + ); + + if (empty($form)) { + return false; + } + + $id = $data['id'] ?? $form->getValue('id'); + + $item = $this->getItem($id); + + $canEditState = $this->canEditState((object) $item); + + // Modify the form based on access controls. + if (!$canEditState || !empty($item->default)) { + if (!$canEditState) { + $form->setFieldAttribute('published', 'disabled', 'true'); + $form->setFieldAttribute('published', 'required', 'false'); + $form->setFieldAttribute('published', 'filter', 'unset'); + } + + $form->setFieldAttribute('default', 'disabled', 'true'); + $form->setFieldAttribute('default', 'required', 'false'); + $form->setFieldAttribute('default', 'filter', 'unset'); + } + + $form->setFieldAttribute('created', 'default', Factory::getDate()->format('Y-m-d H:i:s')); + $form->setFieldAttribute('modified', 'default', Factory::getDate()->format('Y-m-d H:i:s')); + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 4.0.0 + */ + protected function loadFormData() + { + // Check the session for previously entered form data. + $data = Factory::getApplication()->getUserState( + 'com_workflow.edit.workflow.data', + array() + ); + + if (empty($data)) { + $data = $this->getItem(); + } + + return $data; + } + + /** + * Method to preprocess the form. + * + * @param Form $form Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 4.0.0 + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $extension = Factory::getApplication()->input->get('extension'); + + $parts = explode('.', $extension); + + $extension = array_shift($parts); + + // Set the access control rules field component value. + $form->setFieldAttribute('rules', 'component', $extension); + + parent::preprocessForm($form, $data, $group); + } + + /** + * A protected method to get a set of ordering conditions. + * + * @param object $table A record object. + * + * @return array An array of conditions to add to ordering queries. + * + * @since 4.0.0 + */ + protected function getReorderConditions($table) + { + $db = $this->getDatabase(); + + return [ + $db->quoteName('extension') . ' = ' . $db->quote($table->extension), + ]; + } + + /** + * Method to change the default state of one item. + * + * @param array $pk A list of the primary keys to change. + * @param integer $value The value of the home state. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function setDefault($pk, $value = 1) + { + $table = $this->getTable(); + + if ($table->load($pk)) { + if ($table->published !== 1) { + $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); + + return false; + } + } + + if (empty($table->id) || !$this->canEditState($table)) { + Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror'); + + return false; + } + + $date = Factory::getDate()->toSql(); + + if ($value) { + // Unset other default item + if ( + $table->load( + [ + 'default' => '1', + 'extension' => $table->get('extension') + ] + ) + ) { + $table->default = 0; + $table->modified = $date; + $table->store(); + } + } + + if ($table->load($pk)) { + $table->modified = $date; + $table->default = $value; + $table->store(); + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + + /** + * Method to test whether a record can be deleted. + * + * @param object $record A record object. + * + * @return boolean True if allowed to delete the record. Defaults to the permission for the component. + * + * @since 4.0.0 + */ + protected function canDelete($record) + { + if (empty($record->id) || $record->published != -2) { + return false; + } + + return Factory::getUser()->authorise('core.delete', $record->extension . '.workflow.' . (int) $record->id); + } + + /** + * Method to test whether a record can have its state changed. + * + * @param object $record A record object. + * + * @return boolean True if allowed to change the state of the record. Defaults to the permission set in the component. + * + * @since 4.0.0 + */ + protected function canEditState($record) + { + $user = Factory::getUser(); + + // Check for existing workflow. + if (!empty($record->id)) { + return $user->authorise('core.edit.state', $record->extension . '.workflow.' . (int) $record->id); + } + + // Default to component settings if workflow isn't known. + return $user->authorise('core.edit.state', $record->extension); + } + + /** + * Method to change the published state of one or more records. + * + * @param array &$pks A list of the primary keys to change. + * @param integer $value The value of the published state. + * + * @return boolean True on success. + * + * @since 4.0.0 + */ + public function publish(&$pks, $value = 1) + { + $table = $this->getTable(); + $pks = (array) $pks; + + $date = Factory::getDate()->toSql(); + + // Default workflow item check. + foreach ($pks as $i => $pk) { + if ($table->load($pk) && $value != 1 && $table->default) { + // Prune items that you can't change. + Factory::getApplication()->enqueueMessage(Text::_('COM_WORKFLOW_UNPUBLISH_DEFAULT_ERROR'), 'error'); + unset($pks[$i]); + break; + } + } + + // Clean the cache. + $this->cleanCache(); + + // Ensure that previous checks don't empty the array. + if (empty($pks)) { + return true; + } + + $table->load($pk); + $table->modified = $date; + $table->store(); + + return parent::publish($pks, $value); + } } diff --git a/administrator/components/com_workflow/src/Model/WorkflowsModel.php b/administrator/components/com_workflow/src/Model/WorkflowsModel.php index 9fcd14fbc68af..b0f460e510e44 100644 --- a/administrator/components/com_workflow/src/Model/WorkflowsModel.php +++ b/administrator/components/com_workflow/src/Model/WorkflowsModel.php @@ -1,4 +1,5 @@ getUserStateFromRequest($this->context . '.filter.extension', 'extension', null, 'cmd'); - - $this->setState('filter.extension', $extension); - $parts = explode('.', $extension); - - // Extract the component name - $this->setState('filter.component', $parts[0]); - - // Extract the optional section name - $this->setState('filter.section', (count($parts) > 1) ? $parts[1] : null); - - parent::populateState($ordering, $direction); - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $type The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for model. Optional. - * - * @return \Joomla\CMS\Table\Table A Table object - * - * @since 4.0.0 - */ - public function getTable($type = 'Workflow', $prefix = 'Administrator', $config = array()) - { - return parent::getTable($type, $prefix, $config); - } - - /** - * Method to get an array of data items. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 4.0.0 - */ - public function getItems() - { - $items = parent::getItems(); - - if ($items) - { - $this->countItems($items); - } - - return $items; - } - - /** - * Get the filter form - * - * @param array $data data - * @param boolean $loadData load current data - * - * @return \Joomla\CMS\Form\Form|bool the Form object or false - * - * @since 4.0.0 - */ - public function getFilterForm($data = array(), $loadData = true) - { - $form = parent::getFilterForm($data, $loadData); - - if ($form) - { - $form->setValue('extension', null, $this->getState('filter.extension')); - } - - return $form; - } - - /** - * Add the number of transitions and states to all workflow items - * - * @param array $items The workflow items - * - * @return mixed An array of data items on success, false on failure. - * - * @since 4.0.0 - */ - protected function countItems($items) - { - $db = $this->getDatabase(); - - $ids = [0]; - - foreach ($items as $item) - { - $ids[] = (int) $item->id; - - $item->count_states = 0; - $item->count_transitions = 0; - } - - $query = $db->getQuery(true); - - $query->select( - [ - $db->quoteName('workflow_id'), - 'COUNT(*) AS ' . $db->quoteName('count'), - ] - ) - ->from($db->quoteName('#__workflow_stages')) - ->whereIn($db->quoteName('workflow_id'), $ids) - ->where($db->quoteName('published') . ' >= 0') - ->group($db->quoteName('workflow_id')); - - $status = $db->setQuery($query)->loadObjectList('workflow_id'); - - $query = $db->getQuery(true); - - $query->select( - [ - $db->quoteName('workflow_id'), - 'COUNT(*) AS ' . $db->quoteName('count'), - ] - ) - ->from($db->quoteName('#__workflow_transitions')) - ->whereIn($db->quoteName('workflow_id'), $ids) - ->where($db->quoteName('published') . ' >= 0') - ->group($db->quoteName('workflow_id')); - - $transitions = $db->setQuery($query)->loadObjectList('workflow_id'); - - foreach ($items as $item) - { - if (isset($status[$item->id])) - { - $item->count_states = (int) $status[$item->id]->count; - } - - if (isset($transitions[$item->id])) - { - $item->count_transitions = (int) $transitions[$item->id]->count; - } - } - } - - /** - * Method to get the data that should be injected in the form. - * - * @return string The query to database. - * - * @since 4.0.0 - */ - public function getListQuery() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->select( - [ - $db->quoteName('w.id'), - $db->quoteName('w.title'), - $db->quoteName('w.created'), - $db->quoteName('w.modified'), - $db->quoteName('w.published'), - $db->quoteName('w.checked_out'), - $db->quoteName('w.checked_out_time'), - $db->quoteName('w.ordering'), - $db->quoteName('w.default'), - $db->quoteName('w.created_by'), - $db->quoteName('w.description'), - $db->quoteName('u.name'), - $db->quoteName('uc.name', 'editor'), - ] - ) - ->from($db->quoteName('#__workflows', 'w')) - ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('w.created_by')) - ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('w.checked_out')); - - // Filter by extension - if ($extension = $this->getState('filter.extension')) - { - $query->where($db->quoteName('extension') . ' = :extension') - ->bind(':extension', $extension); - } - - $status = (string) $this->getState('filter.published'); - - // Filter by status - if (is_numeric($status)) - { - $status = (int) $status; - $query->where($db->quoteName('w.published') . ' = :published') - ->bind(':published', $status, ParameterType::INTEGER); - } - elseif ($status === '') - { - $query->where($db->quoteName('w.published') . ' IN (0, 1)'); - } - - // Filter by search in title - $search = $this->getState('filter.search'); - - if (!empty($search)) - { - $search = '%' . str_replace(' ', '%', trim($search)) . '%'; - $query->where('(' . $db->quoteName('w.title') . ' LIKE :search1 OR ' . $db->quoteName('w.description') . ' LIKE :search2)') - ->bind([':search1', ':search2'], $search); - } - - // Add the list ordering clause. - $orderCol = $this->state->get('list.ordering', 'w.ordering'); - $orderDirn = strtoupper($this->state->get('list.direction', 'ASC')); - - $query->order($db->escape($orderCol) . ' ' . ($orderDirn === 'DESC' ? 'DESC' : 'ASC')); - - return $query; - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @see JController + * @since 4.0.0 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'w.id', + 'title', 'w.title', + 'published', 'w.published', + 'created_by', 'w.created_by', + 'created', 'w.created', + 'ordering', 'w.ordering', + 'modified', 'w.modified', + 'description', 'w.description' + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 4.0.0 + */ + protected function populateState($ordering = 'w.ordering', $direction = 'asc') + { + $app = Factory::getApplication(); + $extension = $app->getUserStateFromRequest($this->context . '.filter.extension', 'extension', null, 'cmd'); + + $this->setState('filter.extension', $extension); + $parts = explode('.', $extension); + + // Extract the component name + $this->setState('filter.component', $parts[0]); + + // Extract the optional section name + $this->setState('filter.section', (count($parts) > 1) ? $parts[1] : null); + + parent::populateState($ordering, $direction); + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $type The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for model. Optional. + * + * @return \Joomla\CMS\Table\Table A Table object + * + * @since 4.0.0 + */ + public function getTable($type = 'Workflow', $prefix = 'Administrator', $config = array()) + { + return parent::getTable($type, $prefix, $config); + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 4.0.0 + */ + public function getItems() + { + $items = parent::getItems(); + + if ($items) { + $this->countItems($items); + } + + return $items; + } + + /** + * Get the filter form + * + * @param array $data data + * @param boolean $loadData load current data + * + * @return \Joomla\CMS\Form\Form|bool the Form object or false + * + * @since 4.0.0 + */ + public function getFilterForm($data = array(), $loadData = true) + { + $form = parent::getFilterForm($data, $loadData); + + if ($form) { + $form->setValue('extension', null, $this->getState('filter.extension')); + } + + return $form; + } + + /** + * Add the number of transitions and states to all workflow items + * + * @param array $items The workflow items + * + * @return mixed An array of data items on success, false on failure. + * + * @since 4.0.0 + */ + protected function countItems($items) + { + $db = $this->getDatabase(); + + $ids = [0]; + + foreach ($items as $item) { + $ids[] = (int) $item->id; + + $item->count_states = 0; + $item->count_transitions = 0; + } + + $query = $db->getQuery(true); + + $query->select( + [ + $db->quoteName('workflow_id'), + 'COUNT(*) AS ' . $db->quoteName('count'), + ] + ) + ->from($db->quoteName('#__workflow_stages')) + ->whereIn($db->quoteName('workflow_id'), $ids) + ->where($db->quoteName('published') . ' >= 0') + ->group($db->quoteName('workflow_id')); + + $status = $db->setQuery($query)->loadObjectList('workflow_id'); + + $query = $db->getQuery(true); + + $query->select( + [ + $db->quoteName('workflow_id'), + 'COUNT(*) AS ' . $db->quoteName('count'), + ] + ) + ->from($db->quoteName('#__workflow_transitions')) + ->whereIn($db->quoteName('workflow_id'), $ids) + ->where($db->quoteName('published') . ' >= 0') + ->group($db->quoteName('workflow_id')); + + $transitions = $db->setQuery($query)->loadObjectList('workflow_id'); + + foreach ($items as $item) { + if (isset($status[$item->id])) { + $item->count_states = (int) $status[$item->id]->count; + } + + if (isset($transitions[$item->id])) { + $item->count_transitions = (int) $transitions[$item->id]->count; + } + } + } + + /** + * Method to get the data that should be injected in the form. + * + * @return string The query to database. + * + * @since 4.0.0 + */ + public function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select( + [ + $db->quoteName('w.id'), + $db->quoteName('w.title'), + $db->quoteName('w.created'), + $db->quoteName('w.modified'), + $db->quoteName('w.published'), + $db->quoteName('w.checked_out'), + $db->quoteName('w.checked_out_time'), + $db->quoteName('w.ordering'), + $db->quoteName('w.default'), + $db->quoteName('w.created_by'), + $db->quoteName('w.description'), + $db->quoteName('u.name'), + $db->quoteName('uc.name', 'editor'), + ] + ) + ->from($db->quoteName('#__workflows', 'w')) + ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('w.created_by')) + ->join('LEFT', $db->quoteName('#__users', 'uc'), $db->quoteName('uc.id') . ' = ' . $db->quoteName('w.checked_out')); + + // Filter by extension + if ($extension = $this->getState('filter.extension')) { + $query->where($db->quoteName('extension') . ' = :extension') + ->bind(':extension', $extension); + } + + $status = (string) $this->getState('filter.published'); + + // Filter by status + if (is_numeric($status)) { + $status = (int) $status; + $query->where($db->quoteName('w.published') . ' = :published') + ->bind(':published', $status, ParameterType::INTEGER); + } elseif ($status === '') { + $query->where($db->quoteName('w.published') . ' IN (0, 1)'); + } + + // Filter by search in title + $search = $this->getState('filter.search'); + + if (!empty($search)) { + $search = '%' . str_replace(' ', '%', trim($search)) . '%'; + $query->where('(' . $db->quoteName('w.title') . ' LIKE :search1 OR ' . $db->quoteName('w.description') . ' LIKE :search2)') + ->bind([':search1', ':search2'], $search); + } + + // Add the list ordering clause. + $orderCol = $this->state->get('list.ordering', 'w.ordering'); + $orderDirn = strtoupper($this->state->get('list.direction', 'ASC')); + + $query->order($db->escape($orderCol) . ' ' . ($orderDirn === 'DESC' ? 'DESC' : 'ASC')); + + return $query; + } } diff --git a/administrator/components/com_workflow/src/Table/StageTable.php b/administrator/components/com_workflow/src/Table/StageTable.php index 382e0422a0803..99a3fd5d3e6a1 100644 --- a/administrator/components/com_workflow/src/Table/StageTable.php +++ b/administrator/components/com_workflow/src/Table/StageTable.php @@ -1,4 +1,5 @@ getDbo(); - $app = Factory::getApplication(); - $pk = (int) $pk; - - $query = $db->getQuery(true) - ->select($db->quoteName('default')) - ->from($db->quoteName('#__workflow_stages')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $pk, ParameterType::INTEGER); - - $isDefault = $db->setQuery($query)->loadResult(); - - if ($isDefault) - { - $app->enqueueMessage(Text::_('COM_WORKFLOW_MSG_DELETE_IS_DEFAULT'), 'error'); - - return false; - } - - try - { - $query = $db->getQuery(true) - ->delete($db->quoteName('#__workflow_transitions')) - ->where( - [ - $db->quoteName('to_stage_id') . ' = :idTo', - $db->quoteName('from_stage_id') . ' = :idFrom', - ], - 'OR' - ) - ->bind([':idTo', ':idFrom'], $pk, ParameterType::INTEGER); - - $db->setQuery($query)->execute(); - - return parent::delete($pk); - } - catch (\RuntimeException $e) - { - $app->enqueueMessage(Text::sprintf('COM_WORKFLOW_MSG_WORKFLOWS_DELETE_ERROR', $e->getMessage()), 'error'); - } - - return false; - } - - /** - * Overloaded check function - * - * @return boolean True on success - * - * @see Table::check() - * @since 4.0.0 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (trim($this->title) === '') - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_STATE')); - - return false; - } - - if (!empty($this->default)) - { - if ((int) $this->published !== 1) - { - $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); - - return false; - } - } - else - { - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $query - ->select($db->quoteName('id')) - ->from($db->quoteName('#__workflow_stages')) - ->where( - [ - $db->quoteName('workflow_id') . ' = :id', - $db->quoteName('default') . ' = 1', - ] - ) - ->bind(':id', $this->workflow_id, ParameterType::INTEGER); - - $id = $db->setQuery($query)->loadResult(); - - // If there is no default stage => set the current to default to recover - if (empty($id)) - { - $this->default = '1'; - } - // This stage is the default, but someone has tried to disable it => not allowed - elseif ($id === $this->id) - { - $this->setError(Text::_('COM_WORKFLOW_DISABLE_DEFAULT')); - - return false; - } - } - - return true; - } - - /** - * Overloaded store function - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return mixed False on failure, positive integer on success. - * - * @see Table::store() - * @since 4.0.0 - */ - public function store($updateNulls = true) - { - $table = new StageTable($this->getDbo()); - - if ($this->default == '1') - { - // Verify that the default is unique for this workflow - if ($table->load(array('default' => '1', 'workflow_id' => (int) $this->workflow_id))) - { - $table->default = 0; - $table->store(); - } - } - - return parent::store($updateNulls); - } - - /** - * Method to bind an associative array or object to the Table instance. - * This method only binds properties that are publicly accessible and optionally - * takes an array of properties to ignore when binding. - * - * @param array|object $src An associative array or object to bind to the Table instance. - * @param array|string $ignore An optional array or space separated list of properties to ignore while binding. - * - * @return boolean True on success. - * - * @since 4.0.0 - * @throws \InvalidArgumentException - */ - public function bind($src, $ignore = array()) - { - // Bind the rules. - if (isset($src['rules']) && \is_array($src['rules'])) - { - $rules = new Rules($src['rules']); - $this->setRules($rules); - } - - return parent::bind($src, $ignore); - } - - /** - * Method to compute the default name of the asset. - * The default name is in the form table_name.id - * where id is the value of the primary key of the table. - * - * @return string - * - * @since 4.0.0 - */ - protected function _getAssetName() - { - $k = $this->_tbl_key; - $workflow = new WorkflowTable($this->getDbo()); - $workflow->load($this->workflow_id); - - $parts = explode('.', $workflow->extension); - - $extension = array_shift($parts); - - return $extension . '.stage.' . (int) $this->$k; - } - - /** - * Method to return the title to use for the asset table. - * - * @return string - * - * @since 4.0.0 - */ - protected function _getAssetTitle() - { - return $this->title; - } - - /** - * Get the parent asset id for the record - * - * @param Table|null $table A Table object for the asset parent. - * @param integer|null $id The id for the asset - * - * @return integer The id of the asset's parent - * - * @since 4.0.0 - */ - protected function _getAssetParentId(Table $table = null, $id = null) - { - $asset = self::getInstance('Asset', 'JTable', array('dbo' => $this->getDbo())); - - $workflow = new WorkflowTable($this->getDbo()); - $workflow->load($this->workflow_id); - - $parts = explode('.', $workflow->extension); - - $extension = array_shift($parts); - - $name = $extension . '.workflow.' . (int) $workflow->id; - - $asset->loadByName($name); - $assetId = $asset->id; - - return !empty($assetId) ? $assetId : parent::_getAssetParentId($table, $id); - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * @param DatabaseDriver $db Database connector object + * + * @since 4.0.0 + */ + public function __construct(DatabaseDriver $db) + { + parent::__construct('#__workflow_stages', 'id', $db); + } + + /** + * Deletes workflow with transition and stages. + * + * @param int $pk Extension ids to delete. + * + * @return boolean True on success. + * + * @since 4.0.0 + * + * @throws \UnexpectedValueException + */ + public function delete($pk = null) + { + $db = $this->getDbo(); + $app = Factory::getApplication(); + $pk = (int) $pk; + + $query = $db->getQuery(true) + ->select($db->quoteName('default')) + ->from($db->quoteName('#__workflow_stages')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $pk, ParameterType::INTEGER); + + $isDefault = $db->setQuery($query)->loadResult(); + + if ($isDefault) { + $app->enqueueMessage(Text::_('COM_WORKFLOW_MSG_DELETE_IS_DEFAULT'), 'error'); + + return false; + } + + try { + $query = $db->getQuery(true) + ->delete($db->quoteName('#__workflow_transitions')) + ->where( + [ + $db->quoteName('to_stage_id') . ' = :idTo', + $db->quoteName('from_stage_id') . ' = :idFrom', + ], + 'OR' + ) + ->bind([':idTo', ':idFrom'], $pk, ParameterType::INTEGER); + + $db->setQuery($query)->execute(); + + return parent::delete($pk); + } catch (\RuntimeException $e) { + $app->enqueueMessage(Text::sprintf('COM_WORKFLOW_MSG_WORKFLOWS_DELETE_ERROR', $e->getMessage()), 'error'); + } + + return false; + } + + /** + * Overloaded check function + * + * @return boolean True on success + * + * @see Table::check() + * @since 4.0.0 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (trim($this->title) === '') { + $this->setError(Text::_('JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_STATE')); + + return false; + } + + if (!empty($this->default)) { + if ((int) $this->published !== 1) { + $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); + + return false; + } + } else { + $db = $this->getDbo(); + $query = $db->getQuery(true); + + $query + ->select($db->quoteName('id')) + ->from($db->quoteName('#__workflow_stages')) + ->where( + [ + $db->quoteName('workflow_id') . ' = :id', + $db->quoteName('default') . ' = 1', + ] + ) + ->bind(':id', $this->workflow_id, ParameterType::INTEGER); + + $id = $db->setQuery($query)->loadResult(); + + // If there is no default stage => set the current to default to recover + if (empty($id)) { + $this->default = '1'; + } elseif ($id === $this->id) { + // This stage is the default, but someone has tried to disable it => not allowed + $this->setError(Text::_('COM_WORKFLOW_DISABLE_DEFAULT')); + + return false; + } + } + + return true; + } + + /** + * Overloaded store function + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return mixed False on failure, positive integer on success. + * + * @see Table::store() + * @since 4.0.0 + */ + public function store($updateNulls = true) + { + $table = new StageTable($this->getDbo()); + + if ($this->default == '1') { + // Verify that the default is unique for this workflow + if ($table->load(array('default' => '1', 'workflow_id' => (int) $this->workflow_id))) { + $table->default = 0; + $table->store(); + } + } + + return parent::store($updateNulls); + } + + /** + * Method to bind an associative array or object to the Table instance. + * This method only binds properties that are publicly accessible and optionally + * takes an array of properties to ignore when binding. + * + * @param array|object $src An associative array or object to bind to the Table instance. + * @param array|string $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return boolean True on success. + * + * @since 4.0.0 + * @throws \InvalidArgumentException + */ + public function bind($src, $ignore = array()) + { + // Bind the rules. + if (isset($src['rules']) && \is_array($src['rules'])) { + $rules = new Rules($src['rules']); + $this->setRules($rules); + } + + return parent::bind($src, $ignore); + } + + /** + * Method to compute the default name of the asset. + * The default name is in the form table_name.id + * where id is the value of the primary key of the table. + * + * @return string + * + * @since 4.0.0 + */ + protected function _getAssetName() + { + $k = $this->_tbl_key; + $workflow = new WorkflowTable($this->getDbo()); + $workflow->load($this->workflow_id); + + $parts = explode('.', $workflow->extension); + + $extension = array_shift($parts); + + return $extension . '.stage.' . (int) $this->$k; + } + + /** + * Method to return the title to use for the asset table. + * + * @return string + * + * @since 4.0.0 + */ + protected function _getAssetTitle() + { + return $this->title; + } + + /** + * Get the parent asset id for the record + * + * @param Table|null $table A Table object for the asset parent. + * @param integer|null $id The id for the asset + * + * @return integer The id of the asset's parent + * + * @since 4.0.0 + */ + protected function _getAssetParentId(Table $table = null, $id = null) + { + $asset = self::getInstance('Asset', 'JTable', array('dbo' => $this->getDbo())); + + $workflow = new WorkflowTable($this->getDbo()); + $workflow->load($this->workflow_id); + + $parts = explode('.', $workflow->extension); + + $extension = array_shift($parts); + + $name = $extension . '.workflow.' . (int) $workflow->id; + + $asset->loadByName($name); + $assetId = $asset->id; + + return !empty($assetId) ? $assetId : parent::_getAssetParentId($table, $id); + } } diff --git a/administrator/components/com_workflow/src/Table/TransitionTable.php b/administrator/components/com_workflow/src/Table/TransitionTable.php index 95b5b2de1ae8e..f78051957d928 100644 --- a/administrator/components/com_workflow/src/Table/TransitionTable.php +++ b/administrator/components/com_workflow/src/Table/TransitionTable.php @@ -1,4 +1,5 @@ setRules($rules); - } - - return parent::bind($src, $ignore); - } - - /** - * Method to compute the default name of the asset. - * The default name is in the form table_name.id - * where id is the value of the primary key of the table. - * - * @return string - * - * @since 4.0.0 - */ - protected function _getAssetName() - { - $k = $this->_tbl_key; - $workflow = new WorkflowTable($this->getDbo()); - $workflow->load($this->workflow_id); - - $parts = explode('.', $workflow->extension); - - $extension = array_shift($parts); - - return $extension . '.transition.' . (int) $this->$k; - } - - /** - * Method to return the title to use for the asset table. - * - * @return string - * - * @since 4.0.0 - */ - protected function _getAssetTitle() - { - return $this->title; - } - - /** - * Get the parent asset id for the record - * - * @param Table $table A Table object for the asset parent. - * @param integer $id The id for the asset - * - * @return integer The id of the asset's parent - * - * @since 4.0.0 - */ - protected function _getAssetParentId(Table $table = null, $id = null) - { - $asset = self::getInstance('Asset', 'JTable', array('dbo' => $this->getDbo())); - - $workflow = new WorkflowTable($this->getDbo()); - $workflow->load($this->workflow_id); - - $parts = explode('.', $workflow->extension); - - $extension = array_shift($parts); - - $name = $extension . '.workflow.' . (int) $workflow->id; - - $asset->loadByName($name); - $assetId = $asset->id; - - return !empty($assetId) ? $assetId : parent::_getAssetParentId($table, $id); - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * An array of key names to be json encoded in the bind function + * + * @var array + * + * @since 4.0.0 + */ + protected $_jsonEncode = [ + 'options' + ]; + + /** + * @param DatabaseDriver $db Database connector object + * + * @since 4.0.0 + */ + public function __construct(DatabaseDriver $db) + { + parent::__construct('#__workflow_transitions', 'id', $db); + } + + /** + * Method to bind an associative array or object to the Table instance. + * This method only binds properties that are publicly accessible and optionally + * takes an array of properties to ignore when binding. + * + * @param array|object $src An associative array or object to bind to the Table instance. + * @param array|string $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return boolean True on success. + * + * @since 4.0.0 + * @throws \InvalidArgumentException + */ + public function bind($src, $ignore = array()) + { + // Bind the rules. + if (isset($src['rules']) && \is_array($src['rules'])) { + $rules = new Rules($src['rules']); + $this->setRules($rules); + } + + return parent::bind($src, $ignore); + } + + /** + * Method to compute the default name of the asset. + * The default name is in the form table_name.id + * where id is the value of the primary key of the table. + * + * @return string + * + * @since 4.0.0 + */ + protected function _getAssetName() + { + $k = $this->_tbl_key; + $workflow = new WorkflowTable($this->getDbo()); + $workflow->load($this->workflow_id); + + $parts = explode('.', $workflow->extension); + + $extension = array_shift($parts); + + return $extension . '.transition.' . (int) $this->$k; + } + + /** + * Method to return the title to use for the asset table. + * + * @return string + * + * @since 4.0.0 + */ + protected function _getAssetTitle() + { + return $this->title; + } + + /** + * Get the parent asset id for the record + * + * @param Table $table A Table object for the asset parent. + * @param integer $id The id for the asset + * + * @return integer The id of the asset's parent + * + * @since 4.0.0 + */ + protected function _getAssetParentId(Table $table = null, $id = null) + { + $asset = self::getInstance('Asset', 'JTable', array('dbo' => $this->getDbo())); + + $workflow = new WorkflowTable($this->getDbo()); + $workflow->load($this->workflow_id); + + $parts = explode('.', $workflow->extension); + + $extension = array_shift($parts); + + $name = $extension . '.workflow.' . (int) $workflow->id; + + $asset->loadByName($name); + $assetId = $asset->id; + + return !empty($assetId) ? $assetId : parent::_getAssetParentId($table, $id); + } } diff --git a/administrator/components/com_workflow/src/Table/WorkflowTable.php b/administrator/components/com_workflow/src/Table/WorkflowTable.php index a9f7267bb39b7..d0c444e278ad5 100644 --- a/administrator/components/com_workflow/src/Table/WorkflowTable.php +++ b/administrator/components/com_workflow/src/Table/WorkflowTable.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\Table; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\Table; use Joomla\CMS\Access\Rules; use Joomla\CMS\Factory; @@ -24,317 +24,290 @@ */ class WorkflowTable extends Table { - /** - * Indicates that columns fully support the NULL value in the database - * - * @var boolean - * - * @since 4.0.0 - */ - protected $_supportNullValue = true; - - /** - * @param DatabaseDriver $db Database connector object - * - * @since 4.0.0 - */ - public function __construct(DatabaseDriver $db) - { - $this->typeAlias = '{extension}.workflow'; - - parent::__construct('#__workflows', 'id', $db); - } - - /** - * Deletes workflow with transition and states. - * - * @param int $pk Extension ids to delete. - * - * @return boolean - * - * @since 4.0.0 - * - * @throws \Exception on ACL error - */ - public function delete($pk = null) - { - $db = $this->getDbo(); - $app = Factory::getApplication(); - $pk = (int) $pk; - - // Gets the workflow information that is going to be deleted. - $query = $db->getQuery(true) - ->select($db->quoteName('default')) - ->from($db->quoteName('#__workflows')) - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $pk, ParameterType::INTEGER); - - $isDefault = $db->setQuery($query)->loadResult(); - - if ($isDefault) - { - $app->enqueueMessage(Text::_('COM_WORKFLOW_MSG_DELETE_DEFAULT'), 'error'); - - return false; - } - - // Delete the workflow states, then transitions from all tables. - try - { - $query = $db->getQuery(true) - ->delete($db->quoteName('#__workflow_stages')) - ->where($db->quoteName('workflow_id') . ' = :id') - ->bind(':id', $pk, ParameterType::INTEGER); - - $db->setQuery($query)->execute(); - - $query = $db->getQuery(true) - ->delete($db->quoteName('#__workflow_transitions')) - ->where($db->quoteName('workflow_id') . ' = :id') - ->bind(':id', $pk, ParameterType::INTEGER); - - $db->setQuery($query)->execute(); - - return parent::delete($pk); - } - catch (\RuntimeException $e) - { - $app->enqueueMessage(Text::sprintf('COM_WORKFLOW_MSG_WORKFLOWS_DELETE_ERROR', $e->getMessage()), 'error'); - - return false; - } - } - - /** - * Overloaded check function - * - * @return boolean True on success - * - * @see Table::check() - * @since 4.0.0 - */ - public function check() - { - try - { - parent::check(); - } - catch (\Exception $e) - { - $this->setError($e->getMessage()); - - return false; - } - - if (trim($this->title) === '') - { - $this->setError(Text::_('JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_WORKFLOW')); - - return false; - } - - if (!empty($this->default)) - { - if ((int) $this->published !== 1) - { - $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); - - return false; - } - } - else - { - $db = $this->getDbo(); - $query = $db->getQuery(true); - - $query - ->select($db->quoteName('id')) - ->from($db->quoteName('#__workflows')) - ->where($db->quoteName('default') . ' = 1'); - - $id = $db->setQuery($query)->loadResult(); - - // If there is no default workflow => set the current to default to recover - if (empty($id)) - { - $this->default = '1'; - } - // This workflow is the default, but someone has tried to disable it => not allowed - elseif ($id === $this->id) - { - $this->setError(Text::_('COM_WORKFLOW_DISABLE_DEFAULT')); - - return false; - } - } - - return true; - } - - /** - * Overloaded store function - * - * @param boolean $updateNulls True to update fields even if they are null. - * - * @return mixed False on failure, positive integer on success. - * - * @see Table::store() - * @since 4.0.0 - */ - public function store($updateNulls = true) - { - $date = Factory::getDate(); - $user = Factory::getUser(); - - $table = new WorkflowTable($this->getDbo()); - - if ($this->id) - { - // Existing item - $this->modified_by = $user->id; - $this->modified = $date->toSql(); - } - else - { - $this->modified_by = 0; - } - - if (!(int) $this->created) - { - $this->created = $date->toSql(); - } - - if (empty($this->created_by)) - { - $this->created_by = $user->id; - } - - if (!(int) $this->modified) - { - $this->modified = $this->created; - } - - if (empty($this->modified_by)) - { - $this->modified_by = $this->created_by; - } - - if ((int) $this->default === 1) - { - // Verify that the default is unique for this workflow - if ($table->load( - [ - 'default' => '1', - 'extension' => $this->extension - ] - )) - { - $table->default = 0; - $table->store(); - } - } - - return parent::store($updateNulls); - } - - /** - * Method to bind an associative array or object to the Table instance. - * This method only binds properties that are publicly accessible and optionally - * takes an array of properties to ignore when binding. - * - * @param array|object $src An associative array or object to bind to the Table instance. - * @param array|string $ignore An optional array or space separated list of properties to ignore while binding. - * - * @return boolean True on success. - * - * @since 4.0.0 - * @throws \InvalidArgumentException - */ - public function bind($src, $ignore = array()) - { - // Bind the rules. - if (isset($src['rules']) && \is_array($src['rules'])) - { - $rules = new Rules($src['rules']); - $this->setRules($rules); - } - - return parent::bind($src, $ignore); - } - - /** - * Method to compute the default name of the asset. - * The default name is in the form table_name.id - * where id is the value of the primary key of the table. - * - * @return string - * - * @since 4.0.0 - */ - protected function _getAssetName() - { - $k = $this->_tbl_key; - - $parts = explode('.', $this->extension); - - $extension = array_shift($parts); - - return $extension . '.workflow.' . (int) $this->$k; - } - - /** - * Method to return the title to use for the asset table. - * - * @return string - * - * @since 4.0.0 - */ - protected function _getAssetTitle() - { - return $this->title; - } - - /** - * Get the parent asset id for the record - * - * @param Table $table A Table object for the asset parent. - * @param integer $id The id for the asset - * - * @return integer The id of the asset's parent - * - * @since 4.0.0 - */ - protected function _getAssetParentId(Table $table = null, $id = null) - { - $assetId = null; - - $parts = explode('.', $this->extension); - - $extension = array_shift($parts); - - // Build the query to get the asset id for the parent category. - $query = $this->getDbo()->getQuery(true) - ->select($this->getDbo()->quoteName('id')) - ->from($this->getDbo()->quoteName('#__assets')) - ->where($this->getDbo()->quoteName('name') . ' = :extension') - ->bind(':extension', $extension); - - // Get the asset id from the database. - $this->getDbo()->setQuery($query); - - if ($result = $this->getDbo()->loadResult()) - { - $assetId = (int) $result; - } - - // Return the asset id. - if ($assetId) - { - return $assetId; - } - else - { - return parent::_getAssetParentId($table, $id); - } - } + /** + * Indicates that columns fully support the NULL value in the database + * + * @var boolean + * + * @since 4.0.0 + */ + protected $_supportNullValue = true; + + /** + * @param DatabaseDriver $db Database connector object + * + * @since 4.0.0 + */ + public function __construct(DatabaseDriver $db) + { + $this->typeAlias = '{extension}.workflow'; + + parent::__construct('#__workflows', 'id', $db); + } + + /** + * Deletes workflow with transition and states. + * + * @param int $pk Extension ids to delete. + * + * @return boolean + * + * @since 4.0.0 + * + * @throws \Exception on ACL error + */ + public function delete($pk = null) + { + $db = $this->getDbo(); + $app = Factory::getApplication(); + $pk = (int) $pk; + + // Gets the workflow information that is going to be deleted. + $query = $db->getQuery(true) + ->select($db->quoteName('default')) + ->from($db->quoteName('#__workflows')) + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $pk, ParameterType::INTEGER); + + $isDefault = $db->setQuery($query)->loadResult(); + + if ($isDefault) { + $app->enqueueMessage(Text::_('COM_WORKFLOW_MSG_DELETE_DEFAULT'), 'error'); + + return false; + } + + // Delete the workflow states, then transitions from all tables. + try { + $query = $db->getQuery(true) + ->delete($db->quoteName('#__workflow_stages')) + ->where($db->quoteName('workflow_id') . ' = :id') + ->bind(':id', $pk, ParameterType::INTEGER); + + $db->setQuery($query)->execute(); + + $query = $db->getQuery(true) + ->delete($db->quoteName('#__workflow_transitions')) + ->where($db->quoteName('workflow_id') . ' = :id') + ->bind(':id', $pk, ParameterType::INTEGER); + + $db->setQuery($query)->execute(); + + return parent::delete($pk); + } catch (\RuntimeException $e) { + $app->enqueueMessage(Text::sprintf('COM_WORKFLOW_MSG_WORKFLOWS_DELETE_ERROR', $e->getMessage()), 'error'); + + return false; + } + } + + /** + * Overloaded check function + * + * @return boolean True on success + * + * @see Table::check() + * @since 4.0.0 + */ + public function check() + { + try { + parent::check(); + } catch (\Exception $e) { + $this->setError($e->getMessage()); + + return false; + } + + if (trim($this->title) === '') { + $this->setError(Text::_('JLIB_DATABASE_ERROR_MUSTCONTAIN_A_TITLE_WORKFLOW')); + + return false; + } + + if (!empty($this->default)) { + if ((int) $this->published !== 1) { + $this->setError(Text::_('COM_WORKFLOW_ITEM_MUST_PUBLISHED')); + + return false; + } + } else { + $db = $this->getDbo(); + $query = $db->getQuery(true); + + $query + ->select($db->quoteName('id')) + ->from($db->quoteName('#__workflows')) + ->where($db->quoteName('default') . ' = 1'); + + $id = $db->setQuery($query)->loadResult(); + + // If there is no default workflow => set the current to default to recover + if (empty($id)) { + $this->default = '1'; + } elseif ($id === $this->id) { + // This workflow is the default, but someone has tried to disable it => not allowed + $this->setError(Text::_('COM_WORKFLOW_DISABLE_DEFAULT')); + + return false; + } + } + + return true; + } + + /** + * Overloaded store function + * + * @param boolean $updateNulls True to update fields even if they are null. + * + * @return mixed False on failure, positive integer on success. + * + * @see Table::store() + * @since 4.0.0 + */ + public function store($updateNulls = true) + { + $date = Factory::getDate(); + $user = Factory::getUser(); + + $table = new WorkflowTable($this->getDbo()); + + if ($this->id) { + // Existing item + $this->modified_by = $user->id; + $this->modified = $date->toSql(); + } else { + $this->modified_by = 0; + } + + if (!(int) $this->created) { + $this->created = $date->toSql(); + } + + if (empty($this->created_by)) { + $this->created_by = $user->id; + } + + if (!(int) $this->modified) { + $this->modified = $this->created; + } + + if (empty($this->modified_by)) { + $this->modified_by = $this->created_by; + } + + if ((int) $this->default === 1) { + // Verify that the default is unique for this workflow + if ( + $table->load( + [ + 'default' => '1', + 'extension' => $this->extension + ] + ) + ) { + $table->default = 0; + $table->store(); + } + } + + return parent::store($updateNulls); + } + + /** + * Method to bind an associative array or object to the Table instance. + * This method only binds properties that are publicly accessible and optionally + * takes an array of properties to ignore when binding. + * + * @param array|object $src An associative array or object to bind to the Table instance. + * @param array|string $ignore An optional array or space separated list of properties to ignore while binding. + * + * @return boolean True on success. + * + * @since 4.0.0 + * @throws \InvalidArgumentException + */ + public function bind($src, $ignore = array()) + { + // Bind the rules. + if (isset($src['rules']) && \is_array($src['rules'])) { + $rules = new Rules($src['rules']); + $this->setRules($rules); + } + + return parent::bind($src, $ignore); + } + + /** + * Method to compute the default name of the asset. + * The default name is in the form table_name.id + * where id is the value of the primary key of the table. + * + * @return string + * + * @since 4.0.0 + */ + protected function _getAssetName() + { + $k = $this->_tbl_key; + + $parts = explode('.', $this->extension); + + $extension = array_shift($parts); + + return $extension . '.workflow.' . (int) $this->$k; + } + + /** + * Method to return the title to use for the asset table. + * + * @return string + * + * @since 4.0.0 + */ + protected function _getAssetTitle() + { + return $this->title; + } + + /** + * Get the parent asset id for the record + * + * @param Table $table A Table object for the asset parent. + * @param integer $id The id for the asset + * + * @return integer The id of the asset's parent + * + * @since 4.0.0 + */ + protected function _getAssetParentId(Table $table = null, $id = null) + { + $assetId = null; + + $parts = explode('.', $this->extension); + + $extension = array_shift($parts); + + // Build the query to get the asset id for the parent category. + $query = $this->getDbo()->getQuery(true) + ->select($this->getDbo()->quoteName('id')) + ->from($this->getDbo()->quoteName('#__assets')) + ->where($this->getDbo()->quoteName('name') . ' = :extension') + ->bind(':extension', $extension); + + // Get the asset id from the database. + $this->getDbo()->setQuery($query); + + if ($result = $this->getDbo()->loadResult()) { + $assetId = (int) $result; + } + + // Return the asset id. + if ($assetId) { + return $assetId; + } else { + return parent::_getAssetParentId($table, $id); + } + } } diff --git a/administrator/components/com_workflow/src/View/Stage/HtmlView.php b/administrator/components/com_workflow/src/View/Stage/HtmlView.php index 1c24c2cd7a3f1..0fabb6b6a8955 100644 --- a/administrator/components/com_workflow/src/View/Stage/HtmlView.php +++ b/administrator/components/com_workflow/src/View/Stage/HtmlView.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\View\Stage; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\View\Stage; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; @@ -24,155 +24,147 @@ */ class HtmlView extends BaseHtmlView { - /** - * The model state - * - * @var object - * @since 4.0.0 - */ - protected $state; - - /** - * From object to generate fields - * - * @var \Joomla\CMS\Form\Form - * - * @since 4.0.0 - */ - protected $form; - - /** - * Items array - * - * @var object - * @since 4.0.0 - */ - protected $item; - - /** - * The name of current extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Display item view - * - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.0.0 - */ - public function display($tpl = null) - { - // Get the Data - $this->state = $this->get('State'); - $this->form = $this->get('Form'); - $this->item = $this->get('Item'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $extension = $this->state->get('filter.extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - // Set the toolbar - $this->addToolbar(); - - // Display the template - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = $this->getCurrentUser(); - $userId = $user->id; - $isNew = empty($this->item->id); - - $canDo = StageHelper::getActions($this->extension, 'stage', $this->item->id); - - ToolbarHelper::title(empty($this->item->id) ? Text::_('COM_WORKFLOW_STAGE_ADD') : Text::_('COM_WORKFLOW_STAGE_EDIT'), 'address'); - - $toolbarButtons = []; - - if ($isNew) - { - // For new records, check the create permission. - if ($canDo->get('core.create')) - { - ToolbarHelper::apply('stage.apply'); - $toolbarButtons = [['save', 'stage.save'], ['save2new', 'stage.save2new']]; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel( - 'stage.cancel' - ); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); - - if ($itemEditable) - { - ToolbarHelper::apply('stage.apply'); - $toolbarButtons = [['save', 'stage.save']]; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'stage.save2new']; - $toolbarButtons[] = ['save2copy', 'stage.save2copy']; - } - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel( - 'stage.cancel', - 'JTOOLBAR_CLOSE' - ); - } - - ToolbarHelper::divider(); - } + /** + * The model state + * + * @var object + * @since 4.0.0 + */ + protected $state; + + /** + * From object to generate fields + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + protected $form; + + /** + * Items array + * + * @var object + * @since 4.0.0 + */ + protected $item; + + /** + * The name of current extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Display item view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + // Get the Data + $this->state = $this->get('State'); + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $extension = $this->state->get('filter.extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + // Set the toolbar + $this->addToolbar(); + + // Display the template + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $userId = $user->id; + $isNew = empty($this->item->id); + + $canDo = StageHelper::getActions($this->extension, 'stage', $this->item->id); + + ToolbarHelper::title(empty($this->item->id) ? Text::_('COM_WORKFLOW_STAGE_ADD') : Text::_('COM_WORKFLOW_STAGE_EDIT'), 'address'); + + $toolbarButtons = []; + + if ($isNew) { + // For new records, check the create permission. + if ($canDo->get('core.create')) { + ToolbarHelper::apply('stage.apply'); + $toolbarButtons = [['save', 'stage.save'], ['save2new', 'stage.save2new']]; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel( + 'stage.cancel' + ); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); + + if ($itemEditable) { + ToolbarHelper::apply('stage.apply'); + $toolbarButtons = [['save', 'stage.save']]; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'stage.save2new']; + $toolbarButtons[] = ['save2copy', 'stage.save2copy']; + } + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel( + 'stage.cancel', + 'JTOOLBAR_CLOSE' + ); + } + + ToolbarHelper::divider(); + } } diff --git a/administrator/components/com_workflow/src/View/Stages/HtmlView.php b/administrator/components/com_workflow/src/View/Stages/HtmlView.php index 294f44037113b..8919cf7958562 100644 --- a/administrator/components/com_workflow/src/View/Stages/HtmlView.php +++ b/administrator/components/com_workflow/src/View/Stages/HtmlView.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\View\Stages; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\View\Stages; use Joomla\CMS\Factory; use Joomla\CMS\Helper\ContentHelper; @@ -27,198 +27,190 @@ */ class HtmlView extends BaseHtmlView { - /** - * An array of stages - * - * @var array - * @since 4.0.0 - */ - protected $stages; - - /** - * The model stage - * - * @var object - * @since 4.0.0 - */ - protected $stage; - - /** - * The HTML for displaying sidebar - * - * @var string - * @since 4.0.0 - */ - protected $sidebar; - - /** - * The pagination object - * - * @var \Joomla\CMS\Pagination\Pagination - * - * @since 4.0.0 - */ - protected $pagination; - - /** - * Form object for search filters - * - * @var \Joomla\CMS\Form\Form - * - * @since 4.0.0 - */ - public $filterForm; - - /** - * The active search filters - * - * @var array - * @since 4.0.0 - */ - public $activeFilters; - - /** - * The current workflow - * - * @var object - * @since 4.0.0 - */ - protected $workflow; - - /** - * The ID of current workflow - * - * @var integer - * @since 4.0.0 - */ - protected $workflowID; - - /** - * The name of current extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Display the view - * - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.0.0 - */ - public function display($tpl = null) - { - $this->state = $this->get('State'); - $this->stages = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->workflow = $this->get('Workflow'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->workflowID = $this->workflow->id; - - $parts = explode('.', $this->workflow->extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - if (!empty($this->stages)) - { - $extension = Factory::getApplication()->input->getCmd('extension'); - $workflow = new Workflow($extension); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions($this->extension, 'workflow', $this->workflowID); - - $user = $this->getCurrentUser(); - - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::sprintf('COM_WORKFLOW_STAGES_LIST', Text::_($this->state->get('active_workflow', ''))), 'address contact'); - - $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; - - ToolbarHelper::link( - Route::_('index.php?option=com_workflow&view=workflows&extension=' . $this->escape($this->workflow->extension)), - 'JTOOLBAR_BACK', - $arrow - ); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('stage.add'); - } - - if ($canDo->get('core.edit.state') || $user->authorise('core.admin')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('stages.publish', 'JTOOLBAR_ENABLE')->listCheck(true); - $childBar->unpublish('stages.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); - $childBar->makeDefault('stages.setDefault', 'COM_WORKFLOW_TOOLBAR_DEFAULT'); - - if ($canDo->get('core.admin')) - { - $childBar->checkin('stages.checkin')->listCheck(true); - } - - if ($this->state->get('filter.published') !== '-2') - { - $childBar->trash('stages.trash'); - } - } - - if ($this->state->get('filter.published') === '-2' && $canDo->get('core.delete')) - { - $toolbar->delete('stages.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - $toolbar->help('Stages_List:_Basic_Workflow'); - } + /** + * An array of stages + * + * @var array + * @since 4.0.0 + */ + protected $stages; + + /** + * The model stage + * + * @var object + * @since 4.0.0 + */ + protected $stage; + + /** + * The HTML for displaying sidebar + * + * @var string + * @since 4.0.0 + */ + protected $sidebar; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 4.0.0 + */ + protected $pagination; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * The current workflow + * + * @var object + * @since 4.0.0 + */ + protected $workflow; + + /** + * The ID of current workflow + * + * @var integer + * @since 4.0.0 + */ + protected $workflowID; + + /** + * The name of current extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->stages = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->workflow = $this->get('Workflow'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->workflowID = $this->workflow->id; + + $parts = explode('.', $this->workflow->extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + if (!empty($this->stages)) { + $extension = Factory::getApplication()->input->getCmd('extension'); + $workflow = new Workflow($extension); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions($this->extension, 'workflow', $this->workflowID); + + $user = $this->getCurrentUser(); + + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::sprintf('COM_WORKFLOW_STAGES_LIST', Text::_($this->state->get('active_workflow', ''))), 'address contact'); + + $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; + + ToolbarHelper::link( + Route::_('index.php?option=com_workflow&view=workflows&extension=' . $this->escape($this->workflow->extension)), + 'JTOOLBAR_BACK', + $arrow + ); + + if ($canDo->get('core.create')) { + $toolbar->addNew('stage.add'); + } + + if ($canDo->get('core.edit.state') || $user->authorise('core.admin')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('stages.publish', 'JTOOLBAR_ENABLE')->listCheck(true); + $childBar->unpublish('stages.unpublish', 'JTOOLBAR_DISABLE')->listCheck(true); + $childBar->makeDefault('stages.setDefault', 'COM_WORKFLOW_TOOLBAR_DEFAULT'); + + if ($canDo->get('core.admin')) { + $childBar->checkin('stages.checkin')->listCheck(true); + } + + if ($this->state->get('filter.published') !== '-2') { + $childBar->trash('stages.trash'); + } + } + + if ($this->state->get('filter.published') === '-2' && $canDo->get('core.delete')) { + $toolbar->delete('stages.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + $toolbar->help('Stages_List:_Basic_Workflow'); + } } diff --git a/administrator/components/com_workflow/src/View/Transition/HtmlView.php b/administrator/components/com_workflow/src/View/Transition/HtmlView.php index 52d9112090361..dc3f2f7556a77 100644 --- a/administrator/components/com_workflow/src/View/Transition/HtmlView.php +++ b/administrator/components/com_workflow/src/View/Transition/HtmlView.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\View\Transition; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\View\Transition; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; @@ -24,194 +24,183 @@ */ class HtmlView extends BaseHtmlView { - /** - * The model state - * - * @var object - * @since 4.0.0 - */ - protected $state; - - /** - * Form object to generate fields - * - * @var \Joomla\CMS\Form\Form - * - * @since 4.0.0 - */ - protected $form; - - /** - * Items array - * - * @var object - * @since 4.0.0 - */ - protected $item; - - /** - * That is object of Application - * - * @var \Joomla\CMS\Application\CMSApplication - * @since 4.0.0 - */ - protected $app; - - /** - * The application input object. - * - * @var \Joomla\CMS\Input\Input - * @since 4.0.0 - */ - protected $input; - - /** - * The ID of current workflow - * - * @var integer - * @since 4.0.0 - */ - protected $workflowID; - - /** - * The name of current extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Display item view - * - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.0.0 - */ - public function display($tpl = null) - { - $this->app = Factory::getApplication(); - $this->input = $this->app->input; - - // Get the Data - $this->state = $this->get('State'); - $this->form = $this->get('Form'); - $this->item = $this->get('Item'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $extension = $this->state->get('filter.extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - // Get the ID of workflow - $this->workflowID = $this->input->getCmd("workflow_id"); - - // Set the toolbar - $this->addToolbar(); - - // Display the template - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = $this->getCurrentUser(); - $userId = $user->id; - $isNew = empty($this->item->id); - - $canDo = StageHelper::getActions($this->extension, 'transition', $this->item->id); - - ToolbarHelper::title(empty($this->item->id) ? Text::_('COM_WORKFLOW_TRANSITION_ADD') : Text::_('COM_WORKFLOW_TRANSITION_EDIT'), 'address'); - - $toolbarButtons = []; - - $canCreate = $canDo->get('core.create'); - - if ($isNew) - { - // For new records, check the create permission. - if ($canCreate) - { - ToolbarHelper::apply('transition.apply'); - $toolbarButtons = [['save', 'transition.save'], ['save2new', 'transition.save2new']]; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel( - 'transition.cancel' - ); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); - - if ($itemEditable) - { - ToolbarHelper::apply('transition.apply'); - $toolbarButtons[] = ['save', 'transition.save']; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canCreate) - { - $toolbarButtons[] = ['save2new', 'transition.save2new']; - $toolbarButtons[] = ['save2copy', 'transition.save2copy']; - } - } - - if (count($toolbarButtons) > 1) - { - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - } - else - { - ToolbarHelper::save('transition.save'); - } - - ToolbarHelper::cancel( - 'transition.cancel', - 'JTOOLBAR_CLOSE' - ); - } - - ToolbarHelper::divider(); - } + /** + * The model state + * + * @var object + * @since 4.0.0 + */ + protected $state; + + /** + * Form object to generate fields + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + protected $form; + + /** + * Items array + * + * @var object + * @since 4.0.0 + */ + protected $item; + + /** + * That is object of Application + * + * @var \Joomla\CMS\Application\CMSApplication + * @since 4.0.0 + */ + protected $app; + + /** + * The application input object. + * + * @var \Joomla\CMS\Input\Input + * @since 4.0.0 + */ + protected $input; + + /** + * The ID of current workflow + * + * @var integer + * @since 4.0.0 + */ + protected $workflowID; + + /** + * The name of current extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Display item view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->app = Factory::getApplication(); + $this->input = $this->app->input; + + // Get the Data + $this->state = $this->get('State'); + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $extension = $this->state->get('filter.extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + // Get the ID of workflow + $this->workflowID = $this->input->getCmd("workflow_id"); + + // Set the toolbar + $this->addToolbar(); + + // Display the template + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $userId = $user->id; + $isNew = empty($this->item->id); + + $canDo = StageHelper::getActions($this->extension, 'transition', $this->item->id); + + ToolbarHelper::title(empty($this->item->id) ? Text::_('COM_WORKFLOW_TRANSITION_ADD') : Text::_('COM_WORKFLOW_TRANSITION_EDIT'), 'address'); + + $toolbarButtons = []; + + $canCreate = $canDo->get('core.create'); + + if ($isNew) { + // For new records, check the create permission. + if ($canCreate) { + ToolbarHelper::apply('transition.apply'); + $toolbarButtons = [['save', 'transition.save'], ['save2new', 'transition.save2new']]; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel( + 'transition.cancel' + ); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); + + if ($itemEditable) { + ToolbarHelper::apply('transition.apply'); + $toolbarButtons[] = ['save', 'transition.save']; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canCreate) { + $toolbarButtons[] = ['save2new', 'transition.save2new']; + $toolbarButtons[] = ['save2copy', 'transition.save2copy']; + } + } + + if (count($toolbarButtons) > 1) { + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + } else { + ToolbarHelper::save('transition.save'); + } + + ToolbarHelper::cancel( + 'transition.cancel', + 'JTOOLBAR_CLOSE' + ); + } + + ToolbarHelper::divider(); + } } diff --git a/administrator/components/com_workflow/src/View/Transitions/HtmlView.php b/administrator/components/com_workflow/src/View/Transitions/HtmlView.php index 50653c395630e..01562949cc892 100644 --- a/administrator/components/com_workflow/src/View/Transitions/HtmlView.php +++ b/administrator/components/com_workflow/src/View/Transitions/HtmlView.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\View\Transitions; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\View\Transitions; use Joomla\CMS\Factory; use Joomla\CMS\Helper\ContentHelper; @@ -26,191 +26,184 @@ */ class HtmlView extends BaseHtmlView { - /** - * An array of transitions - * - * @var array - * @since 4.0.0 - */ - protected $transitions; - - /** - * The model state - * - * @var object - * @since 4.0.0 - */ - protected $state; - - /** - * The HTML for displaying sidebar - * - * @var string - * @since 4.0.0 - */ - protected $sidebar; - - /** - * The pagination object - * - * @var \Joomla\CMS\Pagination\Pagination - * - * @since 4.0.0 - */ - protected $pagination; - - /** - * Form object for search filters - * - * @var \Joomla\CMS\Form\Form - * - * @since 4.0.0 - */ - public $filterForm; - - /** - * The active search filters - * - * @var array - * @since 4.0.0 - */ - public $activeFilters; - - /** - * The current workflow - * - * @var object - * @since 4.0.0 - */ - protected $workflow; - - /** - * The ID of current workflow - * - * @var integer - * @since 4.0.0 - */ - protected $workflowID; - - /** - * The name of current extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Display the view - * - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.0.0 - */ - public function display($tpl = null) - { - $this->state = $this->get('State'); - $this->transitions = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - $this->workflow = $this->get('Workflow'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->workflowID = $this->workflow->id; - - $parts = explode('.', $this->workflow->extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions($this->extension, 'workflow', $this->workflowID); - - $user = $this->getCurrentUser(); - - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::sprintf('COM_WORKFLOW_TRANSITIONS_LIST', Text::_($this->state->get('active_workflow'))), 'address contact'); - - $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; - - ToolbarHelper::link( - Route::_('index.php?option=com_workflow&view=workflows&extension=' . $this->escape($this->workflow->extension)), - 'JTOOLBAR_BACK', - $arrow - ); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('transition.add'); - } - - if ($canDo->get('core.edit.state') || $user->authorise('core.admin')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('transitions.publish', 'JTOOLBAR_ENABLE'); - $childBar->unpublish('transitions.unpublish', 'JTOOLBAR_DISABLE'); - - if ($canDo->get('core.admin')) - { - $childBar->checkin('transitions.checkin')->listCheck(true); - } - - if ($this->state->get('filter.published') !== '-2') - { - $childBar->trash('transitions.trash'); - } - } - - if ($this->state->get('filter.published') === '-2' && $canDo->get('core.delete')) - { - $toolbar->delete('transitions.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - $toolbar->help('Transitions_List:_Basic_Workflow'); - } + /** + * An array of transitions + * + * @var array + * @since 4.0.0 + */ + protected $transitions; + + /** + * The model state + * + * @var object + * @since 4.0.0 + */ + protected $state; + + /** + * The HTML for displaying sidebar + * + * @var string + * @since 4.0.0 + */ + protected $sidebar; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 4.0.0 + */ + protected $pagination; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * The current workflow + * + * @var object + * @since 4.0.0 + */ + protected $workflow; + + /** + * The ID of current workflow + * + * @var integer + * @since 4.0.0 + */ + protected $workflowID; + + /** + * The name of current extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->transitions = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + $this->workflow = $this->get('Workflow'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->workflowID = $this->workflow->id; + + $parts = explode('.', $this->workflow->extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions($this->extension, 'workflow', $this->workflowID); + + $user = $this->getCurrentUser(); + + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::sprintf('COM_WORKFLOW_TRANSITIONS_LIST', Text::_($this->state->get('active_workflow'))), 'address contact'); + + $arrow = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left'; + + ToolbarHelper::link( + Route::_('index.php?option=com_workflow&view=workflows&extension=' . $this->escape($this->workflow->extension)), + 'JTOOLBAR_BACK', + $arrow + ); + + if ($canDo->get('core.create')) { + $toolbar->addNew('transition.add'); + } + + if ($canDo->get('core.edit.state') || $user->authorise('core.admin')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('transitions.publish', 'JTOOLBAR_ENABLE'); + $childBar->unpublish('transitions.unpublish', 'JTOOLBAR_DISABLE'); + + if ($canDo->get('core.admin')) { + $childBar->checkin('transitions.checkin')->listCheck(true); + } + + if ($this->state->get('filter.published') !== '-2') { + $childBar->trash('transitions.trash'); + } + } + + if ($this->state->get('filter.published') === '-2' && $canDo->get('core.delete')) { + $toolbar->delete('transitions.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + $toolbar->help('Transitions_List:_Basic_Workflow'); + } } diff --git a/administrator/components/com_workflow/src/View/Workflow/HtmlView.php b/administrator/components/com_workflow/src/View/Workflow/HtmlView.php index 0bc91d5cfe0cb..3a531ade83906 100644 --- a/administrator/components/com_workflow/src/View/Workflow/HtmlView.php +++ b/administrator/components/com_workflow/src/View/Workflow/HtmlView.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\View\Workflow; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\View\Workflow; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; @@ -24,158 +24,150 @@ */ class HtmlView extends BaseHtmlView { - /** - * The model state - * - * @var object - * @since 4.0.0 - */ - protected $state; - - /** - * The Form object - * - * @var \Joomla\CMS\Form\Form - */ - protected $form; - - /** - * The active item - * - * @var object - */ - protected $item; - - /** - * The ID of current workflow - * - * @var integer - * @since 4.0.0 - */ - protected $workflowID; - - /** - * The name of current extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Display item view - * - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.0.0 - */ - public function display($tpl = null) - { - // Get the Data - $this->state = $this->get('State'); - $this->form = $this->get('Form'); - $this->item = $this->get('Item'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $extension = $this->state->get('filter.extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - // Set the toolbar - $this->addToolbar(); - - // Display the template - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - Factory::getApplication()->input->set('hidemainmenu', true); - - $user = $this->getCurrentUser(); - $userId = $user->id; - $isNew = empty($this->item->id); - - $canDo = WorkflowHelper::getActions($this->extension, 'workflow', $this->item->id); - - ToolbarHelper::title(empty($this->item->id) ? Text::_('COM_WORKFLOW_WORKFLOWS_ADD') : Text::_('COM_WORKFLOW_WORKFLOWS_EDIT'), 'address'); - - $toolbarButtons = []; - - if ($isNew) - { - // For new records, check the create permission. - if ($canDo->get('core.create')) - { - ToolbarHelper::apply('workflow.apply'); - $toolbarButtons = [['save', 'workflow.save'], ['save2new', 'workflow.save2new']]; - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel( - 'workflow.cancel' - ); - } - else - { - // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. - $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); - - if ($itemEditable) - { - ToolbarHelper::apply('workflow.apply'); - $toolbarButtons = [['save', 'workflow.save']]; - - // We can save this record, but check the create permission to see if we can return to make a new one. - if ($canDo->get('core.create')) - { - $toolbarButtons[] = ['save2new', 'workflow.save2new']; - $toolbarButtons[] = ['save2copy', 'workflow.save2copy']; - } - } - - ToolbarHelper::saveGroup( - $toolbarButtons, - 'btn-success' - ); - - ToolbarHelper::cancel( - 'workflow.cancel', - 'JTOOLBAR_CLOSE' - ); - } - } + /** + * The model state + * + * @var object + * @since 4.0.0 + */ + protected $state; + + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The ID of current workflow + * + * @var integer + * @since 4.0.0 + */ + protected $workflowID; + + /** + * The name of current extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Display item view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + // Get the Data + $this->state = $this->get('State'); + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $extension = $this->state->get('filter.extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + // Set the toolbar + $this->addToolbar(); + + // Display the template + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + Factory::getApplication()->input->set('hidemainmenu', true); + + $user = $this->getCurrentUser(); + $userId = $user->id; + $isNew = empty($this->item->id); + + $canDo = WorkflowHelper::getActions($this->extension, 'workflow', $this->item->id); + + ToolbarHelper::title(empty($this->item->id) ? Text::_('COM_WORKFLOW_WORKFLOWS_ADD') : Text::_('COM_WORKFLOW_WORKFLOWS_EDIT'), 'address'); + + $toolbarButtons = []; + + if ($isNew) { + // For new records, check the create permission. + if ($canDo->get('core.create')) { + ToolbarHelper::apply('workflow.apply'); + $toolbarButtons = [['save', 'workflow.save'], ['save2new', 'workflow.save2new']]; + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel( + 'workflow.cancel' + ); + } else { + // Since it's an existing record, check the edit permission, or fall back to edit own if the owner. + $itemEditable = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by == $userId); + + if ($itemEditable) { + ToolbarHelper::apply('workflow.apply'); + $toolbarButtons = [['save', 'workflow.save']]; + + // We can save this record, but check the create permission to see if we can return to make a new one. + if ($canDo->get('core.create')) { + $toolbarButtons[] = ['save2new', 'workflow.save2new']; + $toolbarButtons[] = ['save2copy', 'workflow.save2copy']; + } + } + + ToolbarHelper::saveGroup( + $toolbarButtons, + 'btn-success' + ); + + ToolbarHelper::cancel( + 'workflow.cancel', + 'JTOOLBAR_CLOSE' + ); + } + } } diff --git a/administrator/components/com_workflow/src/View/Workflows/HtmlView.php b/administrator/components/com_workflow/src/View/Workflows/HtmlView.php index c7732cce4f66b..6202c3a1cff17 100644 --- a/administrator/components/com_workflow/src/View/Workflows/HtmlView.php +++ b/administrator/components/com_workflow/src/View/Workflows/HtmlView.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Workflow\Administrator\View\Workflows; -\defined('_JEXEC') or die; +namespace Joomla\Component\Workflow\Administrator\View\Workflows; use Joomla\CMS\Factory; use Joomla\CMS\Helper\ContentHelper; @@ -25,171 +25,163 @@ */ class HtmlView extends BaseHtmlView { - /** - * An array of workflows - * - * @var array - * @since 4.0.0 - */ - protected $workflows; - - /** - * The model state - * - * @var object - * @since 4.0.0 - */ - protected $state; - - /** - * The pagination object - * - * @var \Joomla\CMS\Pagination\Pagination - * @since 4.0.0 - */ - protected $pagination; - - /** - * The HTML for displaying sidebar - * - * @var string - * @since 4.0.0 - */ - protected $sidebar; - - /** - * Form object for search filters - * - * @var \Joomla\CMS\Form\Form - * @since 4.0.0 - */ - public $filterForm; - - /** - * The active search filters - * - * @var array - * @since 4.0.0 - */ - public $activeFilters; - - /** - * The name of current extension - * - * @var string - * @since 4.0.0 - */ - protected $extension; - - /** - * The section of the current extension - * - * @var string - * @since 4.0.0 - */ - protected $section; - - /** - * Display the view - * - * @param string $tpl The name of the template file to parse; automatically searches through the template paths. - * - * @return void - * - * @since 4.0.0 - */ - public function display($tpl = null) - { - $this->state = $this->get('State'); - $this->workflows = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->filterForm = $this->get('FilterForm'); - $this->activeFilters = $this->get('ActiveFilters'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $extension = $this->state->get('filter.extension'); - - $parts = explode('.', $extension); - - $this->extension = array_shift($parts); - - if (!empty($parts)) - { - $this->section = array_shift($parts); - } - - $this->addToolbar(); - - parent::display($tpl); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 4.0.0 - */ - protected function addToolbar() - { - $canDo = ContentHelper::getActions($this->extension, $this->section); - - $user = Factory::getApplication()->getIdentity(); - - // Get the toolbar object instance - $toolbar = Toolbar::getInstance('toolbar'); - - ToolbarHelper::title(Text::_('COM_WORKFLOW_WORKFLOWS_LIST'), 'file-alt contact'); - - if ($canDo->get('core.create')) - { - $toolbar->addNew('workflow.add'); - } - - if ($canDo->get('core.edit.state') || $user->authorise('core.admin')) - { - $dropdown = $toolbar->dropdownButton('status-group') - ->text('JTOOLBAR_CHANGE_STATUS') - ->toggleSplit(false) - ->icon('icon-ellipsis-h') - ->buttonClass('btn btn-action') - ->listCheck(true); - - $childBar = $dropdown->getChildToolbar(); - - $childBar->publish('workflows.publish', 'JTOOLBAR_ENABLE'); - $childBar->unpublish('workflows.unpublish', 'JTOOLBAR_DISABLE'); - $childBar->makeDefault('workflows.setDefault', 'COM_WORKFLOW_TOOLBAR_DEFAULT'); - - if ($canDo->get('core.admin')) - { - $childBar->checkin('workflows.checkin')->listCheck(true); - } - - if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) - { - $childBar->trash('workflows.trash'); - } - } - - if ($this->state->get('filter.published') === '-2' && $canDo->get('core.delete')) - { - $toolbar->delete('workflows.delete') - ->text('JTOOLBAR_EMPTY_TRASH') - ->message('JGLOBAL_CONFIRM_DELETE') - ->listCheck(true); - } - - if ($canDo->get('core.admin') || $canDo->get('core.options')) - { - $toolbar->preferences($this->extension); - } - - $toolbar->help('Workflows_List'); - } + /** + * An array of workflows + * + * @var array + * @since 4.0.0 + */ + protected $workflows; + + /** + * The model state + * + * @var object + * @since 4.0.0 + */ + protected $state; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 4.0.0 + */ + protected $pagination; + + /** + * The HTML for displaying sidebar + * + * @var string + * @since 4.0.0 + */ + protected $sidebar; + + /** + * Form object for search filters + * + * @var \Joomla\CMS\Form\Form + * @since 4.0.0 + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + * @since 4.0.0 + */ + public $activeFilters; + + /** + * The name of current extension + * + * @var string + * @since 4.0.0 + */ + protected $extension; + + /** + * The section of the current extension + * + * @var string + * @since 4.0.0 + */ + protected $section; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->state = $this->get('State'); + $this->workflows = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $extension = $this->state->get('filter.extension'); + + $parts = explode('.', $extension); + + $this->extension = array_shift($parts); + + if (!empty($parts)) { + $this->section = array_shift($parts); + } + + $this->addToolbar(); + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 4.0.0 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions($this->extension, $this->section); + + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_WORKFLOW_WORKFLOWS_LIST'), 'file-alt contact'); + + if ($canDo->get('core.create')) { + $toolbar->addNew('workflow.add'); + } + + if ($canDo->get('core.edit.state') || $user->authorise('core.admin')) { + $dropdown = $toolbar->dropdownButton('status-group') + ->text('JTOOLBAR_CHANGE_STATUS') + ->toggleSplit(false) + ->icon('icon-ellipsis-h') + ->buttonClass('btn btn-action') + ->listCheck(true); + + $childBar = $dropdown->getChildToolbar(); + + $childBar->publish('workflows.publish', 'JTOOLBAR_ENABLE'); + $childBar->unpublish('workflows.unpublish', 'JTOOLBAR_DISABLE'); + $childBar->makeDefault('workflows.setDefault', 'COM_WORKFLOW_TOOLBAR_DEFAULT'); + + if ($canDo->get('core.admin')) { + $childBar->checkin('workflows.checkin')->listCheck(true); + } + + if ($canDo->get('core.edit.state') && $this->state->get('filter.published') != -2) { + $childBar->trash('workflows.trash'); + } + } + + if ($this->state->get('filter.published') === '-2' && $canDo->get('core.delete')) { + $toolbar->delete('workflows.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($canDo->get('core.admin') || $canDo->get('core.options')) { + $toolbar->preferences($this->extension); + } + + $toolbar->help('Workflows_List'); + } } diff --git a/administrator/components/com_workflow/tmpl/stage/edit.php b/administrator/components/com_workflow/tmpl/stage/edit.php index b6f53c27fbb9d..24a89fecbb458 100644 --- a/administrator/components/com_workflow/tmpl/stage/edit.php +++ b/administrator/components/com_workflow/tmpl/stage/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $app = Factory::getApplication(); $user = $app->getIdentity(); @@ -35,54 +36,54 @@
- + - - item->id != 0) : ?> -
-
-
-
- -
-
- -
-
-
-
- + + item->id != 0) : ?> +
+
+
+
+ +
+
+ +
+
+
+
+ -
- 'details', 'recall' => true, 'breakpoint' => 768]); ?> +
+ 'details', 'recall' => true, 'breakpoint' => 768]); ?> - -
-
- form->renderField('description'); ?> -
-
-
- form->renderField('published'); ?> - form->renderField('default'); ?> -
-
-
- + +
+
+ form->renderField('description'); ?> +
+
+
+ form->renderField('published'); ?> + form->renderField('default'); ?> +
+
+
+ - authorise('core.admin', $this->extension)) : ?> - -
- - form->getInput('rules'); ?> -
- - + authorise('core.admin', $this->extension)) : ?> + +
+ + form->getInput('rules'); ?> +
+ + - + - form->getInput('workflow_id'); ?> - - -
+ form->getInput('workflow_id'); ?> + + +
diff --git a/administrator/components/com_workflow/tmpl/stages/default.php b/administrator/components/com_workflow/tmpl/stages/default.php index 48c4a7969a085..d7de9de7c323b 100644 --- a/administrator/components/com_workflow/tmpl/stages/default.php +++ b/administrator/components/com_workflow/tmpl/stages/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $user = Factory::getUser(); $userId = $user->id; @@ -30,128 +32,128 @@ $saveOrder = ($listOrder == 's.ordering'); -if ($saveOrder) -{ - $saveOrderingUrl = 'index.php?option=com_workflow&task=stages.saveOrderAjax&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->escape($this->extension) . '&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder) { + $saveOrderingUrl = 'index.php?option=com_workflow&task=stages.saveOrderAjax&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->escape($this->extension) . '&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
-
- sidebar)) : ?> -
- sidebar; ?> -
- -
-
- $this)); - ?> - stages)) : ?> -
- - -
- - - - - - - - - - - - - - - stages as $i => $item): - $edit = Route::_('index.php?option=com_workflow&task=stage.edit&id=' . $item->id . '&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->extension); +
+ sidebar)) : ?> +
+ sidebar; ?> +
+ +
+
+ $this)); + ?> + stages)) : ?> +
+ + +
+ +
- , - , - -
- - - - - - - - - - - -
+ + + + + + + + + + + + + stages as $i => $item) : + $edit = Route::_('index.php?option=com_workflow&task=stage.edit&id=' . $item->id . '&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->extension); - $canEdit = $user->authorise('core.edit', $this->extension . '.stage.' . $item->id); - $canCheckin = $user->authorise('core.admin', 'com_workflow') || $item->checked_out == $userId || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', $this->extension . '.stage.' . $item->id) && $canCheckin; + $canEdit = $user->authorise('core.edit', $this->extension . '.stage.' . $item->id); + $canCheckin = $user->authorise('core.admin', 'com_workflow') || $item->checked_out == $userId || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', $this->extension . '.stage.' . $item->id) && $canCheckin; - ?> - - - - - - - - - - -
+ , + , + +
+ + + + + + + + + + + +
- id, false, 'cid', 'cb', Text::_($item->title)); ?> - - - - - - - - - - published, $i, 'stages.', $canChange); ?> - - default, $i, 'stages.', $canChange); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'stages.', $canCheckin); ?> - - - - escape(Text::_($item->title)); ?> - -
escape(Text::_($item->description)); ?>
- - escape(Text::_($item->title)); ?> -
escape(Text::_($item->description)); ?>
- -
- id; ?> -
- - pagination->getListFooter(); ?> + ?> + + + id, false, 'cid', 'cb', Text::_($item->title)); ?> + + + + + + + + + + + + published, $i, 'stages.', $canChange); ?> + + + default, $i, 'stages.', $canChange); ?> + + + checked_out) : ?> + editor, $item->checked_out_time, 'stages.', $canCheckin); ?> + + + + escape(Text::_($item->title)); ?> + +
escape(Text::_($item->description)); ?>
+ + escape(Text::_($item->title)); ?> +
escape(Text::_($item->description)); ?>
+ + + + id; ?> + + + + + + + pagination->getListFooter(); ?> - - - - - - -
-
-
+ + + + + + + + +
diff --git a/administrator/components/com_workflow/tmpl/transition/edit.php b/administrator/components/com_workflow/tmpl/transition/edit.php index aaefb2ff14182..ee1f49c0be8fe 100644 --- a/administrator/components/com_workflow/tmpl/transition/edit.php +++ b/administrator/components/com_workflow/tmpl/transition/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $app = Factory::getApplication(); $user = $app->getIdentity(); @@ -34,38 +35,37 @@ ?>
- -
- 'details', 'recall' => true, 'breakpoint' => 768]); ?> - - -
-
- form->renderField('from_stage_id'); ?> - form->renderField('to_stage_id'); ?> - form->renderField('description'); ?> -
-
- -
-
- + +
+ 'details', 'recall' => true, 'breakpoint' => 768]); ?> - + +
+
+ form->renderField('from_stage_id'); ?> + form->renderField('to_stage_id'); ?> + form->renderField('description'); ?> +
+
+ +
+
+ - authorise('core.admin', $this->extension)) : ?> + - -
- - form->getInput('rules'); ?> -
- - + authorise('core.admin', $this->extension)) : ?> + +
+ + form->getInput('rules'); ?> +
+ + - -
- form->getInput('workflow_id'); ?> - - + +
+ form->getInput('workflow_id'); ?> + +
diff --git a/administrator/components/com_workflow/tmpl/transitions/default.php b/administrator/components/com_workflow/tmpl/transitions/default.php index 6cc6eee815d58..97dee3f5c4a84 100644 --- a/administrator/components/com_workflow/tmpl/transitions/default.php +++ b/administrator/components/com_workflow/tmpl/transitions/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); -$user = Factory::getUser(); +$user = Factory::getUser(); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); @@ -29,136 +31,136 @@ $saveOrder = ($listOrder == 't.ordering'); -if ($saveOrder) -{ - $saveOrderingUrl = 'index.php?option=com_workflow&task=transitions.saveOrderAjax&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->escape($this->workflow->extension) . '&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder) { + $saveOrderingUrl = 'index.php?option=com_workflow&task=transitions.saveOrderAjax&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->escape($this->workflow->extension) . '&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } ?>
-
- sidebar)) : ?> -
- sidebar; ?> -
- -
-
- $this)); - ?> - transitions)) : ?> -
- - -
- - - - - - - - - - - - - - - - transitions as $i => $item): - $edit = Route::_('index.php?option=com_workflow&task=transition.edit&id=' . $item->id . '&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->escape($this->workflow->extension)); +
+ sidebar)) : ?> +
+ sidebar; ?> +
+ +
+
+ $this)); + ?> + transitions)) : ?> +
+ + +
+ +
- , - , - -
- - - - - - - - - - - - - -
+ + + + + + + + + + + + + + transitions as $i => $item) : + $edit = Route::_('index.php?option=com_workflow&task=transition.edit&id=' . $item->id . '&workflow_id=' . (int) $this->workflowID . '&extension=' . $this->escape($this->workflow->extension)); - $canEdit = $user->authorise('core.edit', $this->extension . '.transition.' . $item->id); - $canCheckin = $user->authorise('core.admin', 'com_workflow') || $item->checked_out == $user->id || is_null($item->checked_out); - $canChange = $user->authorise('core.edit.state', $this->extension . '.transition.' . $item->id) && $canCheckin; - ?> - - - - - - - - - - - -
+ , + , + +
+ + + + + + + + + + + + + +
- id, false, 'cid', 'cb', Text::_($item->title)); ?> - - - - - - - - - - published, $i, 'transitions.', $canChange); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'transitions.', $canCheckin); ?> - - - - escape(Text::_($item->title)); ?> - -
escape(Text::_($item->description)); ?>
- - escape(Text::_($item->title)); ?> -
escape(Text::_($item->description)); ?>
- -
- from_stage_id < 0): ?> - - - escape(Text::_($item->from_stage)); ?> - - - escape(Text::_($item->to_stage)); ?> - - id; ?> -
- - pagination->getListFooter(); ?> - - - - - - -
-
-
+ $canEdit = $user->authorise('core.edit', $this->extension . '.transition.' . $item->id); + $canCheckin = $user->authorise('core.admin', 'com_workflow') || $item->checked_out == $user->id || is_null($item->checked_out); + $canChange = $user->authorise('core.edit.state', $this->extension . '.transition.' . $item->id) && $canCheckin; + ?> + + + id, false, 'cid', 'cb', Text::_($item->title)); ?> + + + + + + + + + + + + published, $i, 'transitions.', $canChange); ?> + + + checked_out) : ?> + editor, $item->checked_out_time, 'transitions.', $canCheckin); ?> + + + + escape(Text::_($item->title)); ?> + +
escape(Text::_($item->description)); ?>
+ + escape(Text::_($item->title)); ?> +
escape(Text::_($item->description)); ?>
+ + + + from_stage_id < 0) : ?> + + + escape(Text::_($item->from_stage)); ?> + + + + escape(Text::_($item->to_stage)); ?> + + + id; ?> + + + + + + + pagination->getListFooter(); ?> + + + + + + + + +
diff --git a/administrator/components/com_workflow/tmpl/workflow/edit.php b/administrator/components/com_workflow/tmpl/workflow/edit.php index 32725ab18acd8..055056043752a 100644 --- a/administrator/components/com_workflow/tmpl/workflow/edit.php +++ b/administrator/components/com_workflow/tmpl/workflow/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); $app = Factory::getApplication(); $user = $app->getIdentity(); @@ -34,57 +35,56 @@
- - - - item->id != 0) : ?> -
-
-
-
- -
-
- -
-
-
-
- + -
- 'general', 'recall' => true, 'breakpoint' => 768]); ?> + + item->id != 0) : ?> +
+
+
+
+ +
+
+ +
+
+
+
+ - -
-
-
- form->renderField('description'); ?> -
-
-
-
- form->renderField('published'); ?> - form->renderField('default'); ?> -
-
-
- +
+ 'general', 'recall' => true, 'breakpoint' => 768]); ?> - authorise('core.admin', $this->extension)) : ?> + +
+
+
+ form->renderField('description'); ?> +
+
+
+
+ form->renderField('published'); ?> + form->renderField('default'); ?> +
+
+
+ - -
- - form->getInput('rules'); ?> -
- + authorise('core.admin', $this->extension)) : ?> + +
+ + form->getInput('rules'); ?> +
+ - + - -
- form->getInput('extension'); ?> - - + +
+ form->getInput('extension'); ?> + +
diff --git a/administrator/components/com_workflow/tmpl/workflows/default.php b/administrator/components/com_workflow/tmpl/workflows/default.php index 449445fae312d..5da642bbe09a3 100644 --- a/administrator/components/com_workflow/tmpl/workflows/default.php +++ b/administrator/components/com_workflow/tmpl/workflows/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('table.columns') - ->useScript('multiselect'); + ->useScript('multiselect'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); @@ -29,15 +31,13 @@ $orderingColumn = 'created'; $saveOrderingUrl = ''; -if (strpos($listOrder, 'modified') !== false) -{ - $orderingColumn = 'modified'; +if (strpos($listOrder, 'modified') !== false) { + $orderingColumn = 'modified'; } -if ($saveOrder) -{ - $saveOrderingUrl = 'index.php?option=com_workflow&task=workflows.saveOrderAjax&tmpl=component&extension=' . $this->escape($this->extension) . '&' . Session::getFormToken() . '=1'; - HTMLHelper::_('draggablelist.draggable'); +if ($saveOrder) { + $saveOrderingUrl = 'index.php?option=com_workflow&task=workflows.saveOrderAjax&tmpl=component&extension=' . $this->escape($this->extension) . '&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); } $extension = $this->escape($this->state->get('filter.extension')); @@ -46,144 +46,147 @@ $userId = $user->id; ?>
-
- sidebar)) : ?> -
- sidebar; ?> -
- -
-
- $this, 'options' => array('selectorFieldName' => 'extension'))); - ?> - workflows)) : ?> -
- - -
- - - - - - - - - - - - - - - - class="js-draggable" data-url="" data-direction="" data-nested="false"> - workflows as $i => $item): - $states = Route::_('index.php?option=com_workflow&view=stages&workflow_id=' . $item->id . '&extension=' . $extension); - $transitions = Route::_('index.php?option=com_workflow&view=transitions&workflow_id=' . $item->id . '&extension=' . $extension); - $edit = Route::_('index.php?option=com_workflow&task=workflow.edit&id=' . $item->id . '&extension=' . $extension); +
+ sidebar)) : ?> +
+ sidebar; ?> +
+ +
+
+ $this, 'options' => array('selectorFieldName' => 'extension'))); + ?> + workflows)) : ?> +
+ + +
+ +
- , - , - -
- - - - - - - - - - - - - - - -
+ + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="false"> + workflows as $i => $item) : + $states = Route::_('index.php?option=com_workflow&view=stages&workflow_id=' . $item->id . '&extension=' . $extension); + $transitions = Route::_('index.php?option=com_workflow&view=transitions&workflow_id=' . $item->id . '&extension=' . $extension); + $edit = Route::_('index.php?option=com_workflow&task=workflow.edit&id=' . $item->id . '&extension=' . $extension); - $canEdit = $user->authorise('core.edit', $extension . '.workflow.' . $item->id); - $canCheckin = $user->authorise('core.admin', 'com_workflow') || $item->checked_out == $userId || is_null($item->checked_out); - $canEditOwn = $user->authorise('core.edit.own', $extension . '.workflow.' . $item->id) && $item->created_by == $userId; - $canChange = $user->authorise('core.edit.state', $extension . '.workflow.' . $item->id) && $canCheckin; - ?> - - - - - - - - - - - -
+ , + , + +
+ + + + + + + + + + + + + + + +
- id, false, 'cid', 'cb', Text::_($item->title)); ?> - - - - - - - - - - published, $i, 'workflows.', $canChange); ?> - - checked_out) : ?> - editor, $item->checked_out_time, 'workflows.', $canCheckin); ?> - - - - escape(Text::_($item->title)); ?> - -
description; ?>
- - escape(Text::_($item->title)); ?> -
description; ?>
- -
- default, $i, 'workflows.', $canChange); ?> - - - count_states; ?> - - - - - count_transitions; ?> - - - - id; ?> -
- - pagination->getListFooter(); ?> + $canEdit = $user->authorise('core.edit', $extension . '.workflow.' . $item->id); + $canCheckin = $user->authorise('core.admin', 'com_workflow') || $item->checked_out == $userId || is_null($item->checked_out); + $canEditOwn = $user->authorise('core.edit.own', $extension . '.workflow.' . $item->id) && $item->created_by == $userId; + $canChange = $user->authorise('core.edit.state', $extension . '.workflow.' . $item->id) && $canCheckin; + ?> + + + id, false, 'cid', 'cb', Text::_($item->title)); ?> + + + + + + + + + + + + published, $i, 'workflows.', $canChange); ?> + + + checked_out) : ?> + editor, $item->checked_out_time, 'workflows.', $canCheckin); ?> + + + + escape(Text::_($item->title)); ?> + +
description; ?>
+ + escape(Text::_($item->title)); ?> +
description; ?>
+ + + + default, $i, 'workflows.', $canChange); ?> + + + + count_states; ?> + + + + + + count_transitions; ?> + + + + + id; ?> + + + + + + pagination->getListFooter(); ?> - - - - -
-
-
+ + + + + + +
diff --git a/administrator/components/com_wrapper/services/provider.php b/administrator/components/com_wrapper/services/provider.php index 0b52fecb1e1dc..cf2b51358aa72 100644 --- a/administrator/components/com_wrapper/services/provider.php +++ b/administrator/components/com_wrapper/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Wrapper')); - $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Wrapper')); - $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Wrapper')); - $container->set( - ComponentInterface::class, - function (Container $container) - { - $component = new WrapperComponent($container->get(ComponentDispatcherFactoryInterface::class)); - $component->setMVCFactory($container->get(MVCFactoryInterface::class)); - $component->setRouterFactory($container->get(RouterFactoryInterface::class)); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new MVCFactory('\\Joomla\\Component\\Wrapper')); + $container->registerServiceProvider(new ComponentDispatcherFactory('\\Joomla\\Component\\Wrapper')); + $container->registerServiceProvider(new RouterFactory('\\Joomla\\Component\\Wrapper')); + $container->set( + ComponentInterface::class, + function (Container $container) { + $component = new WrapperComponent($container->get(ComponentDispatcherFactoryInterface::class)); + $component->setMVCFactory($container->get(MVCFactoryInterface::class)); + $component->setRouterFactory($container->get(RouterFactoryInterface::class)); - return $component; - } - ); - } + return $component; + } + ); + } }; diff --git a/administrator/components/com_wrapper/src/Extension/WrapperComponent.php b/administrator/components/com_wrapper/src/Extension/WrapperComponent.php index 5479cbbf9daf2..a964a4ab6c8c6 100644 --- a/administrator/components/com_wrapper/src/Extension/WrapperComponent.php +++ b/administrator/components/com_wrapper/src/Extension/WrapperComponent.php @@ -1,4 +1,5 @@ alias('session.web', 'session.web.administrator') - ->alias('session', 'session.web.administrator') - ->alias('JSession', 'session.web.administrator') - ->alias(\Joomla\CMS\Session\Session::class, 'session.web.administrator') - ->alias(\Joomla\Session\Session::class, 'session.web.administrator') - ->alias(\Joomla\Session\SessionInterface::class, 'session.web.administrator'); + ->alias('session', 'session.web.administrator') + ->alias('JSession', 'session.web.administrator') + ->alias(\Joomla\CMS\Session\Session::class, 'session.web.administrator') + ->alias(\Joomla\Session\Session::class, 'session.web.administrator') + ->alias(\Joomla\Session\SessionInterface::class, 'session.web.administrator'); // Instantiate the application. $app = $container->get(\Joomla\CMS\Application\AdministratorApplication::class); diff --git a/administrator/includes/defines.php b/administrator/includes/defines.php index e43728b056177..3e8a3fe5fb526 100644 --- a/administrator/includes/defines.php +++ b/administrator/includes/defines.php @@ -1,4 +1,5 @@ isInDevelopmentState()))) -{ - if (file_exists(JPATH_INSTALLATION . '/index.php')) - { - header('Location: ../installation/index.php'); - - exit(); - } - else - { - echo 'No configuration file found and no installation code available. Exiting...'; - - exit; - } +if ( + !file_exists(JPATH_CONFIGURATION . '/configuration.php') + || (filesize(JPATH_CONFIGURATION . '/configuration.php') < 10) + || (file_exists(JPATH_INSTALLATION . '/index.php') && (false === (new Version())->isInDevelopmentState())) +) { + if (file_exists(JPATH_INSTALLATION . '/index.php')) { + header('Location: ../installation/index.php'); + + exit(); + } else { + echo 'No configuration file found and no installation code available. Exiting...'; + + exit; + } } // Pre-Load configuration. Don't remove the Output Buffering due to BOM issues, see JCode 26026 @@ -39,65 +38,59 @@ ob_end_clean(); // System configuration. -$config = new JConfig; +$config = new JConfig(); // Set the error_reporting, and adjust a global Error Handler -switch ($config->error_reporting) -{ - case 'default': - case '-1': - - break; +switch ($config->error_reporting) { + case 'default': + case '-1': + break; - case 'none': - case '0': - error_reporting(0); + case 'none': + case '0': + error_reporting(0); - break; + break; - case 'simple': - error_reporting(E_ERROR | E_WARNING | E_PARSE); - ini_set('display_errors', 1); + case 'simple': + error_reporting(E_ERROR | E_WARNING | E_PARSE); + ini_set('display_errors', 1); - break; + break; - case 'maximum': - case 'development': // <= Stays for backward compatibility, @TODO: can be removed in 5.0 - error_reporting(E_ALL); - ini_set('display_errors', 1); + case 'maximum': + case 'development': // <= Stays for backward compatibility, @TODO: can be removed in 5.0 + error_reporting(E_ALL); + ini_set('display_errors', 1); - break; + break; - default: - error_reporting($config->error_reporting); - ini_set('display_errors', 1); + default: + error_reporting($config->error_reporting); + ini_set('display_errors', 1); - break; + break; } define('JDEBUG', $config->debug); // Check deprecation logging -if (empty($config->log_deprecated)) -{ - // Reset handler for E_USER_DEPRECATED - set_error_handler(null, E_USER_DEPRECATED); -} -else -{ - // Make sure handler for E_USER_DEPRECATED is registered - set_error_handler(['Joomla\CMS\Exception\ExceptionHandler', 'handleUserDeprecatedErrors'], E_USER_DEPRECATED); +if (empty($config->log_deprecated)) { + // Reset handler for E_USER_DEPRECATED + set_error_handler(null, E_USER_DEPRECATED); +} else { + // Make sure handler for E_USER_DEPRECATED is registered + set_error_handler(['Joomla\CMS\Exception\ExceptionHandler', 'handleUserDeprecatedErrors'], E_USER_DEPRECATED); } -if (JDEBUG || $config->error_reporting === 'maximum') -{ - // Set new Exception handler with debug enabled - $errorHandler->setExceptionHandler( - [ - new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), - 'renderException' - ] - ); +if (JDEBUG || $config->error_reporting === 'maximum') { + // Set new Exception handler with debug enabled + $errorHandler->setExceptionHandler( + [ + new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), + 'renderException' + ] + ); } /** @@ -106,15 +99,12 @@ * We need to do this as high up the stack as we can, as the default in \Joomla\Utilities\IpHelper is to * $allowIpOverride = true which is the wrong default for a generic site NOT behind a trusted proxy/load balancer. */ -if (property_exists($config, 'behind_loadbalancer') && $config->behind_loadbalancer == 1) -{ - // If Joomla is configured to be behind a trusted proxy/load balancer, allow HTTP Headers to override the REMOTE_ADDR - IpHelper::setAllowIpOverrides(true); -} -else -{ - // We disable the allowing of IP overriding using headers by default. - IpHelper::setAllowIpOverrides(false); +if (property_exists($config, 'behind_loadbalancer') && $config->behind_loadbalancer == 1) { + // If Joomla is configured to be behind a trusted proxy/load balancer, allow HTTP Headers to override the REMOTE_ADDR + IpHelper::setAllowIpOverrides(true); +} else { + // We disable the allowing of IP overriding using headers by default. + IpHelper::setAllowIpOverrides(false); } unset($config); diff --git a/administrator/index.php b/administrator/index.php index cfec264f131a8..aba7c599d4637 100644 --- a/administrator/index.php +++ b/administrator/index.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; + /** * en-GB localise class. @@ -15,76 +20,71 @@ */ abstract class En_GBLocalise { - /** - * Returns the potential suffixes for a specific number of items - * - * @param integer $count The number of items. - * - * @return array An array of potential suffixes. - * - * @since 1.6 - */ - public static function getPluralSuffixes($count) - { - if ($count == 0) - { - return array('0'); - } - elseif ($count == 1) - { - return array('ONE', '1'); - } - else - { - return array('OTHER', 'MORE'); - } - } + /** + * Returns the potential suffixes for a specific number of items + * + * @param integer $count The number of items. + * + * @return array An array of potential suffixes. + * + * @since 1.6 + */ + public static function getPluralSuffixes($count) + { + if ($count == 0) { + return array('0'); + } elseif ($count == 1) { + return array('ONE', '1'); + } else { + return array('OTHER', 'MORE'); + } + } - /** - * Returns the ignored search words - * - * @return array An array of ignored search words. - * - * @since 1.6 - */ - public static function getIgnoredSearchWords() - { - return array('and', 'in', 'on'); - } + /** + * Returns the ignored search words + * + * @return array An array of ignored search words. + * + * @since 1.6 + */ + public static function getIgnoredSearchWords() + { + return array('and', 'in', 'on'); + } - /** - * Returns the lower length limit of search words - * - * @return integer The lower length limit of search words. - * - * @since 1.6 - */ - public static function getLowerLimitSearchWord() - { - return 3; - } + /** + * Returns the lower length limit of search words + * + * @return integer The lower length limit of search words. + * + * @since 1.6 + */ + public static function getLowerLimitSearchWord() + { + return 3; + } - /** - * Returns the upper length limit of search words - * - * @return integer The upper length limit of search words. - * - * @since 1.6 - */ - public static function getUpperLimitSearchWord() - { - return 20; - } + /** + * Returns the upper length limit of search words + * + * @return integer The upper length limit of search words. + * + * @since 1.6 + */ + public static function getUpperLimitSearchWord() + { + return 20; + } - /** - * Returns the number of chars to display when searching - * - * @return integer The number of chars to display when searching. - * - * @since 1.6 - */ - public static function getSearchDisplayedCharactersNumber() - { - return 200; - } + /** + * Returns the number of chars to display when searching + * + * @return integer The number of chars to display when searching. + * + * @since 1.6 + */ + public static function getSearchDisplayedCharactersNumber() + { + return 200; + } } diff --git a/administrator/modules/mod_custom/mod_custom.php b/administrator/modules/mod_custom/mod_custom.php index 190a310df45e1..05476b1e6f05f 100644 --- a/administrator/modules/mod_custom/mod_custom.php +++ b/administrator/modules/mod_custom/mod_custom.php @@ -1,4 +1,5 @@ def('prepare_content', 1)) -{ - PluginHelper::importPlugin('content'); - $module->content = HTMLHelper::_('content.prepare', $module->content, '', 'mod_custom.content'); +if ($params->def('prepare_content', 1)) { + PluginHelper::importPlugin('content'); + $module->content = HTMLHelper::_('content.prepare', $module->content, '', 'mod_custom.content'); } // Replace 'images/' to '../images/' when using an image from /images in backend. diff --git a/administrator/modules/mod_custom/tmpl/default.php b/administrator/modules/mod_custom/tmpl/default.php index dea6f8526b9df..c9262884b21d2 100644 --- a/administrator/modules/mod_custom/tmpl/default.php +++ b/administrator/modules/mod_custom/tmpl/default.php @@ -1,4 +1,5 @@
- content; ?> + content; ?>
diff --git a/administrator/modules/mod_feed/mod_feed.php b/administrator/modules/mod_feed/mod_feed.php index 7c5175edb75d3..3a3c9d760d88b 100644 --- a/administrator/modules/mod_feed/mod_feed.php +++ b/administrator/modules/mod_feed/mod_feed.php @@ -1,4 +1,5 @@ get('rssurl', ''); + /** + * Method to load a feed. + * + * @param \Joomla\Registry\Registry $params The parameters object. + * + * @return \Joomla\CMS\Feed\Feed|string Return a JFeedReader object or a string message if error. + * + * @since 1.5 + */ + public static function getFeed($params) + { + // Module params + $rssurl = $params->get('rssurl', ''); - // Get RSS parsed object - try - { - $feed = new FeedFactory; - $rssDoc = $feed->getFeed($rssurl); - } - catch (\Exception $e) - { - return Text::_('MOD_FEED_ERR_FEED_NOT_RETRIEVED'); - } + // Get RSS parsed object + try { + $feed = new FeedFactory(); + $rssDoc = $feed->getFeed($rssurl); + } catch (\Exception $e) { + return Text::_('MOD_FEED_ERR_FEED_NOT_RETRIEVED'); + } - if (empty($rssDoc)) - { - return Text::_('MOD_FEED_ERR_FEED_NOT_RETRIEVED'); - } + if (empty($rssDoc)) { + return Text::_('MOD_FEED_ERR_FEED_NOT_RETRIEVED'); + } - return $rssDoc; - } + return $rssDoc; + } } diff --git a/administrator/modules/mod_feed/tmpl/default.php b/administrator/modules/mod_feed/tmpl/default.php index 6adc4e1c62098..4846fceb8fef6 100644 --- a/administrator/modules/mod_feed/tmpl/default.php +++ b/administrator/modules/mod_feed/tmpl/default.php @@ -1,4 +1,5 @@ ' . Text::_('MOD_FEED_ERR_NO_URL') . ''; - - return; -} +if (empty($rssurl)) { + echo '
' . Text::_('MOD_FEED_ERR_NO_URL') . '
'; -if (!empty($feed) && is_string($feed)) -{ - echo $feed; + return; } -else -{ - $lang = $app->getLanguage(); - $myrtl = $params->get('rssrtl', 0); - $direction = ' '; - if ($lang->isRtl() && $myrtl == 0) - { - $direction = ' redirect-rtl'; - } - // Feed description - elseif ($lang->isRtl() && $myrtl == 1) - { - $direction = ' redirect-ltr'; - } - elseif ($lang->isRtl() && $myrtl == 2) - { - $direction = ' redirect-rtl'; - } - elseif ($myrtl == 0) - { - $direction = ' redirect-ltr'; - } - elseif ($myrtl == 1) - { - $direction = ' redirect-ltr'; - } - elseif ($myrtl == 2) - { - $direction = ' redirect-rtl'; - } +if (!empty($feed) && is_string($feed)) { + echo $feed; +} else { + $lang = $app->getLanguage(); + $myrtl = $params->get('rssrtl', 0); + $direction = ' '; - if ($feed != false) : - ?> -
- isRtl() && $myrtl == 0) { + $direction = ' redirect-rtl'; + } elseif ($lang->isRtl() && $myrtl == 1) { + // Feed description + $direction = ' redirect-ltr'; + } elseif ($lang->isRtl() && $myrtl == 2) { + $direction = ' redirect-rtl'; + } elseif ($myrtl == 0) { + $direction = ' redirect-ltr'; + } elseif ($myrtl == 1) { + $direction = ' redirect-ltr'; + } elseif ($myrtl == 2) { + $direction = ' redirect-rtl'; + } - // Feed title - if (!is_null($feed->title) && $params->get('rsstitle', 1)) : ?> -

- - title; ?> -

- get('rssdate', 1)) : ?> -

- publishedDate, Text::_('DATE_FORMAT_LC3')); ?> -

- + if ($feed != false) : + ?> +
+ - get('rssdesc', 1)) : ?> - description; ?> - + // Feed title + if (!is_null($feed->title) && $params->get('rsstitle', 1)) : ?> +

+ + title; ?> +

+ get('rssdate', 1)) : ?> +

+ publishedDate, Text::_('DATE_FORMAT_LC3')); ?> +

+ - - get('rssimage', 1) && $feed->image) : ?> - <?php echo $feed->image->title; ?> - + + get('rssdesc', 1)) : ?> + description; ?> + + + get('rssimage', 1) && $feed->image) : ?> + <?php echo $feed->image->title; ?> + - - -
    - get('rssitems', 3); $i++) : - if (!$feed->offsetExists($i)) : - break; - endif; - $uri = $feed[$i]->uri || !$feed[$i]->isPermaLink ? trim($feed[$i]->uri) : trim($feed[$i]->guid); - $uri = !$uri || stripos($uri, 'http') !== 0 ? $rssurl : $uri; - $text = $feed[$i]->content !== '' ? trim($feed[$i]->content) : ''; - ?> -
  • - - - - - + + +
      + get('rssitems', 3); $i++) : + if (!$feed->offsetExists($i)) : + break; + endif; + $uri = $feed[$i]->uri || !$feed[$i]->isPermaLink ? trim($feed[$i]->uri) : trim($feed[$i]->guid); + $uri = !$uri || stripos($uri, 'http') !== 0 ? $rssurl : $uri; + $text = $feed[$i]->content !== '' ? trim($feed[$i]->content) : ''; + ?> +
    • + + + + + - get('rssitemdate', 0)) : ?> -
      - publishedDate, Text::_('DATE_FORMAT_LC3')); ?> -
      - + get('rssitemdate', 0)) : ?> +
      + publishedDate, Text::_('DATE_FORMAT_LC3')); ?> +
      + - get('rssitemdesc', 1) && $text !== '') : ?> -
      - get('word_count', 0), true, false); - echo str_replace(''', "'", $text); - ?> -
      - -
    • - -
    - -
- get('rssitemdesc', 1) && $text !== '') : ?> +
+ get('word_count', 0), true, false); + echo str_replace(''', "'", $text); + ?> +
+ + + + + +
+ -
- -
-
- -
+ title="" + target="_blank"> +
+ +
+
+ +
diff --git a/administrator/modules/mod_latest/mod_latest.php b/administrator/modules/mod_latest/mod_latest.php index 28ed0c2486aa2..287f2e7bee024 100644 --- a/administrator/modules/mod_latest/mod_latest.php +++ b/administrator/modules/mod_latest/mod_latest.php @@ -1,4 +1,5 @@ get('workflow_enabled'); -if ($workflow_enabled) -{ - $app->getLanguage()->load('com_workflow'); +if ($workflow_enabled) { + $app->getLanguage()->load('com_workflow'); } -if ($params->get('automatic_title', 0)) -{ - $module->title = LatestHelper::getTitle($params); +if ($params->get('automatic_title', 0)) { + $module->title = LatestHelper::getTitle($params); } -if (count($list)) -{ - require ModuleHelper::getLayoutPath('mod_latest', $params->get('layout', 'default')); -} -else -{ - $app->getLanguage()->load('com_content'); - - echo LayoutHelper::render('joomla.content.emptystate_module', [ - 'textPrefix' => 'COM_CONTENT', - 'icon' => 'icon-copy', - ] - ); +if (count($list)) { + require ModuleHelper::getLayoutPath('mod_latest', $params->get('layout', 'default')); +} else { + $app->getLanguage()->load('com_content'); + + echo LayoutHelper::render('joomla.content.emptystate_module', [ + 'textPrefix' => 'COM_CONTENT', + 'icon' => 'icon-copy', + ]); } diff --git a/administrator/modules/mod_latest/src/Helper/LatestHelper.php b/administrator/modules/mod_latest/src/Helper/LatestHelper.php index 5353913a538db..18aefeb615097 100644 --- a/administrator/modules/mod_latest/src/Helper/LatestHelper.php +++ b/administrator/modules/mod_latest/src/Helper/LatestHelper.php @@ -1,4 +1,5 @@ setState('list.select', 'a.id, a.title, a.checked_out, a.checked_out_time, ' . - ' a.access, a.created, a.created_by, a.created_by_alias, a.featured, a.state, a.publish_up, a.publish_down' - ); - - // Set Ordering filter - switch ($params->get('ordering', 'c_dsc')) - { - case 'm_dsc': - $model->setState('list.ordering', 'a.modified DESC, a.created'); - $model->setState('list.direction', 'DESC'); - break; - - case 'c_dsc': - default: - $model->setState('list.ordering', 'a.created'); - $model->setState('list.direction', 'DESC'); - break; - } - - // Set Category Filter - $categoryId = $params->get('catid', null); - - if (is_numeric($categoryId)) - { - $model->setState('filter.category_id', $categoryId); - } - - // Set User Filter. - $userId = $user->get('id'); - - switch ($params->get('user_id', '0')) - { - case 'by_me': - $model->setState('filter.author_id', $userId); - break; - - case 'not_me': - $model->setState('filter.author_id', $userId); - $model->setState('filter.author_id.include', false); - break; - } - - // Set the Start and Limit - $model->setState('list.start', 0); - $model->setState('list.limit', $params->get('count', 5)); - - $items = $model->getItems(); - - if ($error = $model->getError()) - { - throw new \Exception($error, 500); - } - - // Set the links - foreach ($items as &$item) - { - $item->link = ''; - - if ($user->authorise('core.edit', 'com_content.article.' . $item->id) - || ($user->authorise('core.edit.own', 'com_content.article.' . $item->id) && ($userId === $item->created_by))) - { - $item->link = Route::_('index.php?option=com_content&task=article.edit&id=' . $item->id); - } - } - - return $items; - } - - /** - * Get the alternate title for the module. - * - * @param \Joomla\Registry\Registry $params The module parameters. - * - * @return string The alternate title for the module. - */ - public static function getTitle($params) - { - $who = $params->get('user_id', 0); - $catid = (int) $params->get('catid', null); - $type = $params->get('ordering') === 'c_dsc' ? '_CREATED' : '_MODIFIED'; - $title = ''; - - if ($catid) - { - $category = Categories::getInstance('Content')->get($catid); - $title = Text::_('MOD_POPULAR_UNEXISTING'); - - if ($category) - { - $title = $category->title; - } - } - - return Text::plural( - 'MOD_LATEST_TITLE' . $type . ($catid ? '_CATEGORY' : '') . ($who != '0' ? "_$who" : ''), - (int) $params->get('count', 5), - $title - ); - } + /** + * Get a list of articles. + * + * @param Registry &$params The module parameters. + * @param ArticlesModel $model The model. + * + * @return mixed An array of articles, or false on error. + */ + public static function getList(Registry &$params, ArticlesModel $model) + { + $user = Factory::getUser(); + + // Set List SELECT + $model->setState('list.select', 'a.id, a.title, a.checked_out, a.checked_out_time, ' . + ' a.access, a.created, a.created_by, a.created_by_alias, a.featured, a.state, a.publish_up, a.publish_down'); + + // Set Ordering filter + switch ($params->get('ordering', 'c_dsc')) { + case 'm_dsc': + $model->setState('list.ordering', 'a.modified DESC, a.created'); + $model->setState('list.direction', 'DESC'); + break; + + case 'c_dsc': + default: + $model->setState('list.ordering', 'a.created'); + $model->setState('list.direction', 'DESC'); + break; + } + + // Set Category Filter + $categoryId = $params->get('catid', null); + + if (is_numeric($categoryId)) { + $model->setState('filter.category_id', $categoryId); + } + + // Set User Filter. + $userId = $user->get('id'); + + switch ($params->get('user_id', '0')) { + case 'by_me': + $model->setState('filter.author_id', $userId); + break; + + case 'not_me': + $model->setState('filter.author_id', $userId); + $model->setState('filter.author_id.include', false); + break; + } + + // Set the Start and Limit + $model->setState('list.start', 0); + $model->setState('list.limit', $params->get('count', 5)); + + $items = $model->getItems(); + + if ($error = $model->getError()) { + throw new \Exception($error, 500); + } + + // Set the links + foreach ($items as &$item) { + $item->link = ''; + + if ( + $user->authorise('core.edit', 'com_content.article.' . $item->id) + || ($user->authorise('core.edit.own', 'com_content.article.' . $item->id) && ($userId === $item->created_by)) + ) { + $item->link = Route::_('index.php?option=com_content&task=article.edit&id=' . $item->id); + } + } + + return $items; + } + + /** + * Get the alternate title for the module. + * + * @param \Joomla\Registry\Registry $params The module parameters. + * + * @return string The alternate title for the module. + */ + public static function getTitle($params) + { + $who = $params->get('user_id', 0); + $catid = (int) $params->get('catid', null); + $type = $params->get('ordering') === 'c_dsc' ? '_CREATED' : '_MODIFIED'; + $title = ''; + + if ($catid) { + $category = Categories::getInstance('Content')->get($catid); + $title = Text::_('MOD_POPULAR_UNEXISTING'); + + if ($category) { + $title = $category->title; + } + } + + return Text::plural( + 'MOD_LATEST_TITLE' . $type . ($catid ? '_CATEGORY' : '') . ($who != '0' ? "_$who" : ''), + (int) $params->get('count', 5), + $title + ); + } } diff --git a/administrator/modules/mod_latest/tmpl/default.php b/administrator/modules/mod_latest/tmpl/default.php index 0a317f748fb48..f77973ade2cd1 100644 --- a/administrator/modules/mod_latest/tmpl/default.php +++ b/administrator/modules/mod_latest/tmpl/default.php @@ -1,4 +1,5 @@ - - - - - - - - - - - - - - $item) : ?> - - - - - - - - - - - - - - - + + + + + + + + + + + + + + $item) : ?> + + + + + + + + + + + + + + +
title; ?>
- checked_out) : ?> - editor, $item->checked_out_time, $module->id); ?> - - link) : ?> - - title, ENT_QUOTES, 'UTF-8'); ?> - - - title, ENT_QUOTES, 'UTF-8'); ?> - - - stage_title); ?> - - author_name; ?> - - created, Text::_('DATE_FORMAT_LC4')); ?> -
- -
title; ?>
+ checked_out) : ?> + editor, $item->checked_out_time, $module->id); ?> + + link) : ?> + + title, ENT_QUOTES, 'UTF-8'); ?> + + + title, ENT_QUOTES, 'UTF-8'); ?> + + + stage_title); ?> + + author_name; ?> + + created, Text::_('DATE_FORMAT_LC4')); ?> +
+ +
diff --git a/administrator/modules/mod_latestactions/mod_latestactions.php b/administrator/modules/mod_latestactions/mod_latestactions.php index 3b60fff23fb17..91e0a7f37427a 100644 --- a/administrator/modules/mod_latestactions/mod_latestactions.php +++ b/administrator/modules/mod_latestactions/mod_latestactions.php @@ -1,4 +1,5 @@ getIdentity()->authorise('core.admin')) -{ - return; +if (!$app->getIdentity()->authorise('core.admin')) { + return; } $list = LatestActionsHelper::getList($params); -if ($params->get('automatic_title', 0)) -{ - $module->title = LatestActionsHelper::getTitle($params); +if ($params->get('automatic_title', 0)) { + $module->title = LatestActionsHelper::getTitle($params); } require ModuleHelper::getLayoutPath('mod_latestactions', $params->get('layout', 'default')); diff --git a/administrator/modules/mod_latestactions/src/Helper/LatestActionsHelper.php b/administrator/modules/mod_latestactions/src/Helper/LatestActionsHelper.php index 61aae97beef10..98cfc0dccce9d 100644 --- a/administrator/modules/mod_latestactions/src/Helper/LatestActionsHelper.php +++ b/administrator/modules/mod_latestactions/src/Helper/LatestActionsHelper.php @@ -1,4 +1,5 @@ bootComponent('com_actionlogs')->getMVCFactory() - ->createModel('Actionlogs', 'Administrator', ['ignore_request' => true]); + /** + * Get a list of articles. + * + * @param Registry &$params The module parameters. + * + * @return mixed An array of action logs, or false on error. + * + * @since 3.9.1 + * + * @throws \Exception + */ + public static function getList(&$params) + { + /** @var \Joomla\Component\Actionlogs\Administrator\Model\ActionlogsModel $model */ + $model = Factory::getApplication()->bootComponent('com_actionlogs')->getMVCFactory() + ->createModel('Actionlogs', 'Administrator', ['ignore_request' => true]); - // Set the Start and Limit - $model->setState('list.start', 0); - $model->setState('list.limit', $params->get('count', 5)); - $model->setState('list.ordering', 'a.id'); - $model->setState('list.direction', 'DESC'); + // Set the Start and Limit + $model->setState('list.start', 0); + $model->setState('list.limit', $params->get('count', 5)); + $model->setState('list.ordering', 'a.id'); + $model->setState('list.direction', 'DESC'); - $rows = $model->getItems(); + $rows = $model->getItems(); - // Load all actionlog plugins language files - ActionlogsHelper::loadActionLogPluginsLanguage(); + // Load all actionlog plugins language files + ActionlogsHelper::loadActionLogPluginsLanguage(); - foreach ($rows as $row) - { - $row->message = ActionlogsHelper::getHumanReadableLogMessage($row); - } + foreach ($rows as $row) { + $row->message = ActionlogsHelper::getHumanReadableLogMessage($row); + } - return $rows; - } + return $rows; + } - /** - * Get the alternate title for the module - * - * @param Registry $params The module parameters. - * - * @return string The alternate title for the module. - * - * @since 3.9.1 - */ - public static function getTitle($params) - { - return Text::plural('MOD_LATESTACTIONS_TITLE', $params->get('count', 5)); - } + /** + * Get the alternate title for the module + * + * @param Registry $params The module parameters. + * + * @return string The alternate title for the module. + * + * @since 3.9.1 + */ + public static function getTitle($params) + { + return Text::plural('MOD_LATESTACTIONS_TITLE', $params->get('count', 5)); + } } diff --git a/administrator/modules/mod_latestactions/tmpl/default.php b/administrator/modules/mod_latestactions/tmpl/default.php index 0d27c28500318..a36b6fc510125 100644 --- a/administrator/modules/mod_latestactions/tmpl/default.php +++ b/administrator/modules/mod_latestactions/tmpl/default.php @@ -1,4 +1,5 @@ - - - - - - - - - - $item) : ?> - - - - - - - - - - - + + + + + + + + + + $item) : ?> + + + + + + + + + + +
title; ?>
- message; ?> - - log_date); ?> -
- -
title; ?>
+ message; ?> + + log_date); ?> +
+ +
diff --git a/administrator/modules/mod_logged/mod_logged.php b/administrator/modules/mod_logged/mod_logged.php index e646784599a89..27a210d8570f3 100644 --- a/administrator/modules/mod_logged/mod_logged.php +++ b/administrator/modules/mod_logged/mod_logged.php @@ -1,4 +1,5 @@ get('automatic_title', 0)) -{ - $module->title = LoggedHelper::getTitle($params); +if ($params->get('automatic_title', 0)) { + $module->title = LoggedHelper::getTitle($params); } // Check if session metadata tracking is enabled -if ($app->get('session_metadata', true)) -{ - $users = LoggedHelper::getList($params, $app, Factory::getContainer()->get(DatabaseInterface::class)); +if ($app->get('session_metadata', true)) { + $users = LoggedHelper::getList($params, $app, Factory::getContainer()->get(DatabaseInterface::class)); - require ModuleHelper::getLayoutPath('mod_logged', $params->get('layout', 'default')); -} -else -{ - require ModuleHelper::getLayoutPath('mod_logged', 'disabled'); + require ModuleHelper::getLayoutPath('mod_logged', $params->get('layout', 'default')); +} else { + require ModuleHelper::getLayoutPath('mod_logged', 'disabled'); } diff --git a/administrator/modules/mod_logged/src/Helper/LoggedHelper.php b/administrator/modules/mod_logged/src/Helper/LoggedHelper.php index 490f2b5ea2bde..d7ba31fcfc73f 100644 --- a/administrator/modules/mod_logged/src/Helper/LoggedHelper.php +++ b/administrator/modules/mod_logged/src/Helper/LoggedHelper.php @@ -1,4 +1,5 @@ getIdentity(); - $query = $db->getQuery(true) - ->select('s.time, s.client_id, u.id, u.name, u.username') - ->from('#__session AS s') - ->join('LEFT', '#__users AS u ON s.userid = u.id') - ->where('s.guest = 0') - ->setLimit($params->get('count', 5), 0); + /** + * Get a list of logged users. + * + * @param Registry $params The module parameters + * @param CMSApplication $app The application + * @param DatabaseInterface $db The database + * + * @return mixed An array of users, or false on error. + * + * @throws \RuntimeException + */ + public static function getList(Registry $params, CMSApplication $app, DatabaseInterface $db) + { + $user = $app->getIdentity(); + $query = $db->getQuery(true) + ->select('s.time, s.client_id, u.id, u.name, u.username') + ->from('#__session AS s') + ->join('LEFT', '#__users AS u ON s.userid = u.id') + ->where('s.guest = 0') + ->setLimit($params->get('count', 5), 0); - $db->setQuery($query); + $db->setQuery($query); - try - { - $results = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - throw $e; - } + try { + $results = $db->loadObjectList(); + } catch (\RuntimeException $e) { + throw $e; + } - foreach ($results as $k => $result) - { - $results[$k]->logoutLink = ''; + foreach ($results as $k => $result) { + $results[$k]->logoutLink = ''; - if ($user->authorise('core.manage', 'com_users')) - { - $results[$k]->editLink = Route::_('index.php?option=com_users&task=user.edit&id=' . $result->id); - $results[$k]->logoutLink = Route::_( - 'index.php?option=com_login&task=logout&uid=' . $result->id . '&' . Session::getFormToken() . '=1' - ); - } + if ($user->authorise('core.manage', 'com_users')) { + $results[$k]->editLink = Route::_('index.php?option=com_users&task=user.edit&id=' . $result->id); + $results[$k]->logoutLink = Route::_( + 'index.php?option=com_login&task=logout&uid=' . $result->id . '&' . Session::getFormToken() . '=1' + ); + } - if ($params->get('name', 1) == 0) - { - $results[$k]->name = $results[$k]->username; - } - } + if ($params->get('name', 1) == 0) { + $results[$k]->name = $results[$k]->username; + } + } - return $results; - } + return $results; + } - /** - * Get the alternate title for the module - * - * @param \Joomla\Registry\Registry $params The module parameters. - * - * @return string The alternate title for the module. - */ - public static function getTitle($params) - { - return Text::plural('MOD_LOGGED_TITLE', $params->get('count', 5)); - } + /** + * Get the alternate title for the module + * + * @param \Joomla\Registry\Registry $params The module parameters. + * + * @return string The alternate title for the module. + */ + public static function getTitle($params) + { + return Text::plural('MOD_LOGGED_TITLE', $params->get('count', 5)); + } } diff --git a/administrator/modules/mod_logged/tmpl/default.php b/administrator/modules/mod_logged/tmpl/default.php index 27726f16d8149..9e878efa45481 100644 --- a/administrator/modules/mod_logged/tmpl/default.php +++ b/administrator/modules/mod_logged/tmpl/default.php @@ -1,4 +1,5 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + +
title; ?>
- get('name', 1) == 0) : ?> - - - - -
- editLink)) : ?> - - name, ENT_QUOTES, 'UTF-8'); ?> - - - name, ENT_QUOTES, 'UTF-8'); ?> - - - client_id === null) : ?> - - - client_id) : ?> - - -
- - -
- -
- time, Text::_('DATE_FORMAT_LC5')); ?> -
title; ?>
+ get('name', 1) == 0) : ?> + + + + +
+ editLink)) : ?> + + name, ENT_QUOTES, 'UTF-8'); ?> + + + name, ENT_QUOTES, 'UTF-8'); ?> + + + client_id === null) : ?> + + + client_id) : ?> + + +
+ + +
+ +
+ time, Text::_('DATE_FORMAT_LC5')); ?> +
diff --git a/administrator/modules/mod_logged/tmpl/disabled.php b/administrator/modules/mod_logged/tmpl/disabled.php index fade4218a5158..676696d68d817 100644 --- a/administrator/modules/mod_logged/tmpl/disabled.php +++ b/administrator/modules/mod_logged/tmpl/disabled.php @@ -1,4 +1,5 @@
-

- - -

+

+ + +

diff --git a/administrator/modules/mod_login/mod_login.php b/administrator/modules/mod_login/mod_login.php index 39759064dd82c..25e009c372ce2 100644 --- a/administrator/modules/mod_login/mod_login.php +++ b/administrator/modules/mod_login/mod_login.php @@ -1,4 +1,5 @@ getLanguage()->isRtl()) - { - foreach ($languages as &$language) - { - $language['text'] = $language['text'] . '‎'; - } - } + // Fix wrongly set parentheses in RTL languages + if (Factory::getApplication()->getLanguage()->isRtl()) { + foreach ($languages as &$language) { + $language['text'] = $language['text'] . '‎'; + } + } - array_unshift($languages, HTMLHelper::_('select.option', '', Text::_('JDEFAULTLANGUAGE'))); + array_unshift($languages, HTMLHelper::_('select.option', '', Text::_('JDEFAULTLANGUAGE'))); - return HTMLHelper::_('select.genericlist', $languages, 'lang', 'class="form-select"', 'value', 'text', null); - } + return HTMLHelper::_('select.genericlist', $languages, 'lang', 'class="form-select"', 'value', 'text', null); + } - /** - * Get the redirect URI after login. - * - * @return string - */ - public static function getReturnUri() - { - $uri = Uri::getInstance(); - $return = 'index.php' . $uri->toString(array('query')); + /** + * Get the redirect URI after login. + * + * @return string + */ + public static function getReturnUri() + { + $uri = Uri::getInstance(); + $return = 'index.php' . $uri->toString(array('query')); - if ($return != 'index.php?option=com_login') - { - return base64_encode($return); - } - else - { - return base64_encode('index.php'); - } - } + if ($return != 'index.php?option=com_login') { + return base64_encode($return); + } else { + return base64_encode('index.php'); + } + } } diff --git a/administrator/modules/mod_login/tmpl/default.php b/administrator/modules/mod_login/tmpl/default.php index 9249ecbaf6439..7a9234d9736fc 100644 --- a/administrator/modules/mod_login/tmpl/default.php +++ b/administrator/modules/mod_login/tmpl/default.php @@ -1,4 +1,5 @@ getDocument()->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('field.passwordview') - ->registerAndUseScript('mod_login.admin', 'mod_login/admin-login.min.js', [], ['defer' => true], ['core', 'form.validate']); + ->useScript('field.passwordview') + ->registerAndUseScript('mod_login.admin', 'mod_login/admin-login.min.js', [], ['defer' => true], ['core', 'form.validate']); Text::script('JSHOWPASSWORD'); Text::script('JHIDEPASSWORD'); ?>
-
- -
- -
+
+ +
+ +
- -
-
-
- -
+ +
+
+
+ +
- - + + -
-
+
+
-
- -
- - -
- - -
- -
- -
- -
- - - - -
-
+
+ +
+ + +
+ + +
+ +
+ +
+ +
+ + + + +
+
-
- '_blank', - 'rel' => 'noopener nofollow', - 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGIN_CREDENTIALS')) - ] - ); ?> -
+
+ '_blank', + 'rel' => 'noopener nofollow', + 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGIN_CREDENTIALS')) + ] + ); ?> +
diff --git a/administrator/modules/mod_loginsupport/mod_loginsupport.php b/administrator/modules/mod_loginsupport/mod_loginsupport.php index 8bd2a0b2f5b73..77be815970ec0 100644 --- a/administrator/modules/mod_loginsupport/mod_loginsupport.php +++ b/administrator/modules/mod_loginsupport/mod_loginsupport.php @@ -1,4 +1,5 @@ get('automatic_title')) -{ - $module->title = Text::_('MOD_LOGINSUPPORT_TITLE'); +if ($params->get('automatic_title')) { + $module->title = Text::_('MOD_LOGINSUPPORT_TITLE'); } require ModuleHelper::getLayoutPath('mod_loginsupport', $params->get('layout', 'default')); diff --git a/administrator/modules/mod_loginsupport/tmpl/default.php b/administrator/modules/mod_loginsupport/tmpl/default.php index a01598dba85b9..78443fedee070 100644 --- a/administrator/modules/mod_loginsupport/tmpl/default.php +++ b/administrator/modules/mod_loginsupport/tmpl/default.php @@ -1,4 +1,5 @@
-

-
    -
  • - get('forum_url'), - Text::_('MOD_LOGINSUPPORT_FORUM'), - [ - 'target' => '_blank', - 'rel' => 'nofollow noopener', - 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGINSUPPORT_FORUM')) - ] - ); ?> -
  • -
  • - get('documentation_url'), - Text::_('MOD_LOGINSUPPORT_DOCUMENTATION'), - [ - 'target' => '_blank', - 'rel' => 'nofollow noopener', - 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGINSUPPORT_DOCUMENTATION')) - ] - ); ?> -
  • -
  • - get('news_url'), - Text::_('MOD_LOGINSUPPORT_NEWS'), - [ - 'target' => '_blank', - 'rel' => 'nofollow noopener', - 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGINSUPPORT_NEWS')) - ] - ); ?> -
  • -
+

+
    +
  • + get('forum_url'), + Text::_('MOD_LOGINSUPPORT_FORUM'), + [ + 'target' => '_blank', + 'rel' => 'nofollow noopener', + 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGINSUPPORT_FORUM')) + ] + ); ?> +
  • +
  • + get('documentation_url'), + Text::_('MOD_LOGINSUPPORT_DOCUMENTATION'), + [ + 'target' => '_blank', + 'rel' => 'nofollow noopener', + 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGINSUPPORT_DOCUMENTATION')) + ] + ); ?> +
  • +
  • + get('news_url'), + Text::_('MOD_LOGINSUPPORT_NEWS'), + [ + 'target' => '_blank', + 'rel' => 'nofollow noopener', + 'title' => Text::sprintf('JBROWSERTARGET_NEW_TITLE', Text::_('MOD_LOGINSUPPORT_NEWS')) + ] + ); ?> +
  • +
diff --git a/administrator/modules/mod_menu/mod_menu.php b/administrator/modules/mod_menu/mod_menu.php index 5c5dbbf18bb32..484dd7e24687e 100644 --- a/administrator/modules/mod_menu/mod_menu.php +++ b/administrator/modules/mod_menu/mod_menu.php @@ -1,4 +1,5 @@ application = $application; - $this->root = new AdministratorMenuItem; - } - - /** - * Populate the menu items in the menu tree object - * - * @param Registry $params Menu configuration parameters - * @param bool $enabled Whether the menu should be enabled or disabled - * - * @return AdministratorMenuItem Root node of the menu tree - * - * @since 3.7.0 - */ - public function load($params, $enabled) - { - $this->params = $params; - $this->enabled = $enabled; - $menutype = $this->params->get('menutype', '*'); - - if ($menutype === '*') - { - $name = $this->params->get('preset', 'default'); - $this->root = MenusHelper::loadPreset($name); - } - else - { - $this->root = MenusHelper::getMenuItems($menutype, true); - - // Can we access everything important with this menu? Create a recovery menu! - if ($this->enabled - && $this->params->get('check', 1) - && $this->check($this->root, $this->params)) - { - $this->params->set('recovery', true); - - // In recovery mode, load the preset inside a special root node. - $this->root = new AdministratorMenuItem(['level' => 0]); - $heading = new AdministratorMenuItem(['title' => 'MOD_MENU_RECOVERY_MENU_ROOT', 'type' => 'heading']); - $this->root->addChild($heading); - - MenusHelper::loadPreset('default', true, $heading); - - $this->preprocess($this->root); - - $this->root->addChild(new AdministratorMenuItem(['type' => 'separator'])); - - // Add link to exit recovery mode - $uri = clone Uri::getInstance(); - $uri->setVar('recover_menu', 0); - - $this->root->addChild(new AdministratorMenuItem(['title' => 'MOD_MENU_RECOVERY_EXIT', 'type' => 'url', 'link' => $uri->toString()])); - - return $this->root; - } - } - - $this->preprocess($this->root); - - return $this->root; - } - - /** - * Method to render a given level of a menu using provided layout file - * - * @param string $layoutFile The layout file to be used to render - * @param AdministratorMenuItem $node Node to render the children of - * - * @return void - * - * @since 3.8.0 - */ - public function renderSubmenu($layoutFile, $node) - { - if (is_file($layoutFile)) - { - $children = $node->getChildren(); - - foreach ($children as $current) - { - $current->level = $node->level + 1; - - // This sets the scope to this object for the layout file and also isolates other `include`s - require $layoutFile; - } - } - } - - /** - * Check the flat list of menu items for important links - * - * @param AdministratorMenuItem $node The menu items array - * @param Registry $params Module options - * - * @return boolean Whether to show recovery menu - * - * @since 3.8.0 - */ - protected function check($node, Registry $params) - { - $me = $this->application->getIdentity(); - $authMenus = $me->authorise('core.manage', 'com_menus'); - $authModules = $me->authorise('core.manage', 'com_modules'); - - if (!$authMenus && !$authModules) - { - return false; - } - - $items = $node->getChildren(true); - $types = array_column($items, 'type'); - $elements = array_column($items, 'element'); - $rMenu = $authMenus && !\in_array('com_menus', $elements); - $rModule = $authModules && !\in_array('com_modules', $elements); - $rContainer = !\in_array('container', $types); - - if ($rMenu || $rModule || $rContainer) - { - $recovery = $this->application->getUserStateFromRequest('mod_menu.recovery', 'recover_menu', 0, 'int'); - - if ($recovery) - { - return true; - } - - $missing = array(); - - if ($rMenu) - { - $missing[] = Text::_('MOD_MENU_IMPORTANT_ITEM_MENU_MANAGER'); - } - - if ($rModule) - { - $missing[] = Text::_('MOD_MENU_IMPORTANT_ITEM_MODULE_MANAGER'); - } - - if ($rContainer) - { - $missing[] = Text::_('MOD_MENU_IMPORTANT_ITEM_COMPONENTS_CONTAINER'); - } - - $uri = clone Uri::getInstance(); - $uri->setVar('recover_menu', 1); - - $table = Table::getInstance('MenuType'); - $menutype = $params->get('menutype'); - - $table->load(array('menutype' => $menutype)); - - $menutype = $table->get('title', $menutype); - $message = Text::sprintf('MOD_MENU_IMPORTANT_ITEMS_INACCESSIBLE_LIST_WARNING', $menutype, implode(', ', $missing), $uri); - - $this->application->enqueueMessage($message, 'warning'); - } - - return false; - } - - /** - * Filter and perform other preparatory tasks for loaded menu items based on access rights and module configurations for display - * - * @param AdministratorMenuItem $parent A menu item to process - * - * @return array - * - * @since 3.8.0 - */ - protected function preprocess($parent) - { - $user = $this->application->getIdentity(); - $language = $this->application->getLanguage(); - - $noSeparator = true; - $children = $parent->getChildren(); - - /** - * Trigger onPreprocessMenuItems for the current level of backend menu items. - * $children is an array of AdministratorMenuItem objects. A plugin can traverse the whole tree, - * but new nodes will only be run through this method if their parents have not been processed yet. - */ - $this->application->triggerEvent('onPreprocessMenuItems', array('com_menus.administrator.module', $children, $this->params, $this->enabled)); - - foreach ($children as $item) - { - $itemParams = $item->getParams(); - - // Exclude item with menu item option set to exclude from menu modules - if ($itemParams->get('menu_show', 1) == 0) - { - $parent->removeChild($item); - continue; - } - - $item->scope = $item->scope ?? 'default'; - $item->icon = $item->icon ?? ''; - - // Whether this scope can be displayed. Applies only to preset items. Db driven items should use un/published state. - if (($item->scope === 'help' && $this->params->get('showhelp', 1) == 0) || ($item->scope === 'edit' && !$this->params->get('shownew', 1))) - { - $parent->removeChild($item); - continue; - } - - if (substr($item->link, 0, 8) === 'special:') - { - $special = substr($item->link, 8); - - if ($special === 'language-forum') - { - $item->link = 'index.php?option=com_admin&view=help&layout=langforum'; - } - elseif ($special === 'custom-forum') - { - $item->link = $this->params->get('forum_url'); - } - } - - $uri = new Uri($item->link); - $query = $uri->getQuery(true); - - /** - * If component is passed in the link via option variable, we set $item->element to this value for further - * processing. It is needed for links from menu items of third party extensions link to Joomla! core - * components like com_categories, com_fields... - */ - if ($option = $uri->getVar('option')) - { - $item->element = $option; - } - - // Exclude item if is not enabled - if ($item->element && !ComponentHelper::isEnabled($item->element)) - { - $parent->removeChild($item); - continue; - } - - /* - * Multilingual Associations if the site is not set as multilingual and/or Associations is not enabled in - * the Language Filter plugin - */ - - if ($item->element === 'com_associations' && !Associations::isEnabled()) - { - $parent->removeChild($item); - continue; - } - - // Exclude Mass Mail if disabled in global configuration - if ($item->scope === 'massmail' && ($this->application->get('mailonline', 1) == 0 || $this->application->get('massmailoff', 0) == 1)) - { - $parent->removeChild($item); - continue; - } - - // Exclude item if the component is not authorised - $assetName = $item->element; - - if ($item->element === 'com_categories') - { - $assetName = $query['extension'] ?? 'com_content'; - } - elseif ($item->element === 'com_fields') - { - // Only display Fields menus when enabled in the component - $createFields = null; - - if (isset($query['context'])) - { - $createFields = ComponentHelper::getParams(strstr($query['context'], '.', true))->get('custom_fields_enable', 1); - } - - if (!$createFields) - { - $parent->removeChild($item); - continue; - } - - list($assetName) = isset($query['context']) ? explode('.', $query['context'], 2) : array('com_fields'); - } - elseif ($item->element === 'com_cpanel' && $item->link === 'index.php') - { - continue; - } - elseif ($item->link === 'index.php?option=com_cpanel&view=help' - || $item->link === 'index.php?option=com_cpanel&view=cpanel&dashboard=help') - { - if ($this->params->get('showhelp', 1)) - { - continue; - } - - // Exclude help menu item if set such in mod_menu - $parent->removeChild($item); - continue; - } - elseif ($item->element === 'com_workflow') - { - // Only display Workflow menus when enabled in the component - $workflow = null; - - if (isset($query['extension'])) - { - $parts = explode('.', $query['extension']); - - $workflow = ComponentHelper::getParams($parts[0])->get('workflow_enabled') && $user->authorise('core.manage.workflow', $parts[0]); - } - - if (!$workflow) - { - $parent->removeChild($item); - continue; - } - - list($assetName) = isset($query['extension']) ? explode('.', $query['extension'], 2) : array('com_workflow'); - } - // Special case for components which only allow super user access - elseif (\in_array($item->element, array('com_config', 'com_privacy', 'com_actionlogs'), true) && !$user->authorise('core.admin')) - { - $parent->removeChild($item); - continue; - } - elseif ($item->element === 'com_joomlaupdate' && !$user->authorise('core.admin')) - { - $parent->removeChild($item); - continue; - } - elseif (($item->link === 'index.php?option=com_installer&view=install' || $item->link === 'index.php?option=com_installer&view=languages') - && !$user->authorise('core.admin')) - { - continue; - } - elseif ($item->element === 'com_admin') - { - if (isset($query['view']) && $query['view'] === 'sysinfo' && !$user->authorise('core.admin')) - { - $parent->removeChild($item); - continue; - } - } - elseif ($item->link === 'index.php?option=com_messages&view=messages' && !$user->authorise('core.manage', 'com_users')) - { - $parent->removeChild($item); - continue; - } - - if ($assetName && !$user->authorise(($item->scope === 'edit') ? 'core.create' : 'core.manage', $assetName)) - { - $parent->removeChild($item); - continue; - } - - // Exclude if link is invalid - if (is_null($item->link) || !\in_array($item->type, array('separator', 'heading', 'container')) && trim($item->link) === '') - { - $parent->removeChild($item); - continue; - } - - // Process any children if exists - if ($item->hasChildren()) - { - $this->preprocess($item); - } - - // Populate automatic children for container items - if ($item->type === 'container') - { - $exclude = (array) $itemParams->get('hideitems') ?: array(); - $components = MenusHelper::getMenuItems('main', false, $exclude); - - // We are adding the nodes first to preprocess them, then sort them and add them again. - foreach ($components->getChildren() as $c) - { - $item->addChild($c); - } - - $this->preprocess($item); - $children = ArrayHelper::sortObjects($item->getChildren(), 'text', 1, false, true); - - foreach ($children as $c) - { - $item->addChild($c); - } - } - - // Exclude if there are no child items under heading or container - if (\in_array($item->type, array('heading', 'container')) && !$item->hasChildren() && empty($item->components)) - { - $parent->removeChild($item); - continue; - } - - // Remove repeated and edge positioned separators, It is important to put this check at the end of any logical filtering. - if ($item->type === 'separator') - { - if ($noSeparator) - { - $parent->removeChild($item); - continue; - } - - $noSeparator = true; - } - else - { - $noSeparator = false; - } - - // Ok we passed everything, load language at last only - if ($item->element) - { - $language->load($item->element . '.sys', JPATH_ADMINISTRATOR) || - $language->load($item->element . '.sys', JPATH_ADMINISTRATOR . '/components/' . $item->element); - } - - if ($item->type === 'separator' && $itemParams->get('text_separator') == 0) - { - $item->title = ''; - } - - $item->text = Text::_($item->title); - } - - // If last one was a separator remove it too. - $last = end($parent->getChildren()); - - if ($last && $last->type === 'separator' && $last->getSibling(false) && $last->getSibling(false)->type === 'separator') - { - $parent->removeChild($last); - } - } - - /** - * Method to get the CSS class name for an icon identifier or create one if - * a custom image path is passed as the identifier - * - * @param AdministratorMenuItem $node Node to get icon data from - * - * @return string CSS class name - * - * @since 3.8.0 - */ - public function getIconClass($node) - { - $identifier = $node->class; - - // Top level is special - if (trim($identifier) == '') - { - return null; - } - - // We were passed a class name - if (substr($identifier, 0, 6) == 'class:') - { - $class = substr($identifier, 6); - } - // We were passed background icon url. Build the CSS class for the icon - else - { - if ($identifier == null) - { - return null; - } - - $class = preg_replace('#\.[^.]*$#', '', basename($identifier)); - $class = preg_replace('#\.\.[^A-Za-z0-9\.\_\- ]#', '', $class); - } - - $html = 'icon-' . $class . ' icon-fw'; - - return $html; - } - - /** - * Create unique identifier - * - * @return string - * - * @since 4.0.0 - */ - public function getCounter() - { - $this->counter++; - - return $this->counter; - } + /** + * The root of the menu + * + * @var AdministratorMenuItem + * + * @since 4.0.0 + */ + protected $root; + + /** + * An array of AdministratorMenuItem nodes + * + * @var AdministratorMenuItem[] + * + * @since 4.0.0 + */ + protected $nodes = []; + + /** + * The module options + * + * @var Registry + * + * @since 3.8.0 + */ + protected $params; + + /** + * The menu bar state + * + * @var boolean + * + * @since 3.8.0 + */ + protected $enabled; + + /** + * The application + * + * @var boolean + * + * @since 4.0.0 + */ + protected $application; + + /** + * A counter for unique IDs + * + * @var integer + * + * @since 4.0.0 + */ + protected $counter = 0; + + /** + * CssMenu constructor. + * + * @param CMSApplication $application The application + * + * @since 4.0.0 + */ + public function __construct(CMSApplication $application) + { + $this->application = $application; + $this->root = new AdministratorMenuItem(); + } + + /** + * Populate the menu items in the menu tree object + * + * @param Registry $params Menu configuration parameters + * @param bool $enabled Whether the menu should be enabled or disabled + * + * @return AdministratorMenuItem Root node of the menu tree + * + * @since 3.7.0 + */ + public function load($params, $enabled) + { + $this->params = $params; + $this->enabled = $enabled; + $menutype = $this->params->get('menutype', '*'); + + if ($menutype === '*') { + $name = $this->params->get('preset', 'default'); + $this->root = MenusHelper::loadPreset($name); + } else { + $this->root = MenusHelper::getMenuItems($menutype, true); + + // Can we access everything important with this menu? Create a recovery menu! + if ( + $this->enabled + && $this->params->get('check', 1) + && $this->check($this->root, $this->params) + ) { + $this->params->set('recovery', true); + + // In recovery mode, load the preset inside a special root node. + $this->root = new AdministratorMenuItem(['level' => 0]); + $heading = new AdministratorMenuItem(['title' => 'MOD_MENU_RECOVERY_MENU_ROOT', 'type' => 'heading']); + $this->root->addChild($heading); + + MenusHelper::loadPreset('default', true, $heading); + + $this->preprocess($this->root); + + $this->root->addChild(new AdministratorMenuItem(['type' => 'separator'])); + + // Add link to exit recovery mode + $uri = clone Uri::getInstance(); + $uri->setVar('recover_menu', 0); + + $this->root->addChild(new AdministratorMenuItem(['title' => 'MOD_MENU_RECOVERY_EXIT', 'type' => 'url', 'link' => $uri->toString()])); + + return $this->root; + } + } + + $this->preprocess($this->root); + + return $this->root; + } + + /** + * Method to render a given level of a menu using provided layout file + * + * @param string $layoutFile The layout file to be used to render + * @param AdministratorMenuItem $node Node to render the children of + * + * @return void + * + * @since 3.8.0 + */ + public function renderSubmenu($layoutFile, $node) + { + if (is_file($layoutFile)) { + $children = $node->getChildren(); + + foreach ($children as $current) { + $current->level = $node->level + 1; + + // This sets the scope to this object for the layout file and also isolates other `include`s + require $layoutFile; + } + } + } + + /** + * Check the flat list of menu items for important links + * + * @param AdministratorMenuItem $node The menu items array + * @param Registry $params Module options + * + * @return boolean Whether to show recovery menu + * + * @since 3.8.0 + */ + protected function check($node, Registry $params) + { + $me = $this->application->getIdentity(); + $authMenus = $me->authorise('core.manage', 'com_menus'); + $authModules = $me->authorise('core.manage', 'com_modules'); + + if (!$authMenus && !$authModules) { + return false; + } + + $items = $node->getChildren(true); + $types = array_column($items, 'type'); + $elements = array_column($items, 'element'); + $rMenu = $authMenus && !\in_array('com_menus', $elements); + $rModule = $authModules && !\in_array('com_modules', $elements); + $rContainer = !\in_array('container', $types); + + if ($rMenu || $rModule || $rContainer) { + $recovery = $this->application->getUserStateFromRequest('mod_menu.recovery', 'recover_menu', 0, 'int'); + + if ($recovery) { + return true; + } + + $missing = array(); + + if ($rMenu) { + $missing[] = Text::_('MOD_MENU_IMPORTANT_ITEM_MENU_MANAGER'); + } + + if ($rModule) { + $missing[] = Text::_('MOD_MENU_IMPORTANT_ITEM_MODULE_MANAGER'); + } + + if ($rContainer) { + $missing[] = Text::_('MOD_MENU_IMPORTANT_ITEM_COMPONENTS_CONTAINER'); + } + + $uri = clone Uri::getInstance(); + $uri->setVar('recover_menu', 1); + + $table = Table::getInstance('MenuType'); + $menutype = $params->get('menutype'); + + $table->load(array('menutype' => $menutype)); + + $menutype = $table->get('title', $menutype); + $message = Text::sprintf('MOD_MENU_IMPORTANT_ITEMS_INACCESSIBLE_LIST_WARNING', $menutype, implode(', ', $missing), $uri); + + $this->application->enqueueMessage($message, 'warning'); + } + + return false; + } + + /** + * Filter and perform other preparatory tasks for loaded menu items based on access rights and module configurations for display + * + * @param AdministratorMenuItem $parent A menu item to process + * + * @return array + * + * @since 3.8.0 + */ + protected function preprocess($parent) + { + $user = $this->application->getIdentity(); + $language = $this->application->getLanguage(); + + $noSeparator = true; + $children = $parent->getChildren(); + + /** + * Trigger onPreprocessMenuItems for the current level of backend menu items. + * $children is an array of AdministratorMenuItem objects. A plugin can traverse the whole tree, + * but new nodes will only be run through this method if their parents have not been processed yet. + */ + $this->application->triggerEvent('onPreprocessMenuItems', array('com_menus.administrator.module', $children, $this->params, $this->enabled)); + + foreach ($children as $item) { + $itemParams = $item->getParams(); + + // Exclude item with menu item option set to exclude from menu modules + if ($itemParams->get('menu_show', 1) == 0) { + $parent->removeChild($item); + continue; + } + + $item->scope = $item->scope ?? 'default'; + $item->icon = $item->icon ?? ''; + + // Whether this scope can be displayed. Applies only to preset items. Db driven items should use un/published state. + if (($item->scope === 'help' && $this->params->get('showhelp', 1) == 0) || ($item->scope === 'edit' && !$this->params->get('shownew', 1))) { + $parent->removeChild($item); + continue; + } + + if (substr($item->link, 0, 8) === 'special:') { + $special = substr($item->link, 8); + + if ($special === 'language-forum') { + $item->link = 'index.php?option=com_admin&view=help&layout=langforum'; + } elseif ($special === 'custom-forum') { + $item->link = $this->params->get('forum_url'); + } + } + + $uri = new Uri($item->link); + $query = $uri->getQuery(true); + + /** + * If component is passed in the link via option variable, we set $item->element to this value for further + * processing. It is needed for links from menu items of third party extensions link to Joomla! core + * components like com_categories, com_fields... + */ + if ($option = $uri->getVar('option')) { + $item->element = $option; + } + + // Exclude item if is not enabled + if ($item->element && !ComponentHelper::isEnabled($item->element)) { + $parent->removeChild($item); + continue; + } + + /* + * Multilingual Associations if the site is not set as multilingual and/or Associations is not enabled in + * the Language Filter plugin + */ + + if ($item->element === 'com_associations' && !Associations::isEnabled()) { + $parent->removeChild($item); + continue; + } + + // Exclude Mass Mail if disabled in global configuration + if ($item->scope === 'massmail' && ($this->application->get('mailonline', 1) == 0 || $this->application->get('massmailoff', 0) == 1)) { + $parent->removeChild($item); + continue; + } + + // Exclude item if the component is not authorised + $assetName = $item->element; + + if ($item->element === 'com_categories') { + $assetName = $query['extension'] ?? 'com_content'; + } elseif ($item->element === 'com_fields') { + // Only display Fields menus when enabled in the component + $createFields = null; + + if (isset($query['context'])) { + $createFields = ComponentHelper::getParams(strstr($query['context'], '.', true))->get('custom_fields_enable', 1); + } + + if (!$createFields) { + $parent->removeChild($item); + continue; + } + + list($assetName) = isset($query['context']) ? explode('.', $query['context'], 2) : array('com_fields'); + } elseif ($item->element === 'com_cpanel' && $item->link === 'index.php') { + continue; + } elseif ( + $item->link === 'index.php?option=com_cpanel&view=help' + || $item->link === 'index.php?option=com_cpanel&view=cpanel&dashboard=help' + ) { + if ($this->params->get('showhelp', 1)) { + continue; + } + + // Exclude help menu item if set such in mod_menu + $parent->removeChild($item); + continue; + } elseif ($item->element === 'com_workflow') { + // Only display Workflow menus when enabled in the component + $workflow = null; + + if (isset($query['extension'])) { + $parts = explode('.', $query['extension']); + + $workflow = ComponentHelper::getParams($parts[0])->get('workflow_enabled') && $user->authorise('core.manage.workflow', $parts[0]); + } + + if (!$workflow) { + $parent->removeChild($item); + continue; + } + + list($assetName) = isset($query['extension']) ? explode('.', $query['extension'], 2) : array('com_workflow'); + } elseif (\in_array($item->element, array('com_config', 'com_privacy', 'com_actionlogs'), true) && !$user->authorise('core.admin')) { + // Special case for components which only allow super user access + $parent->removeChild($item); + continue; + } elseif ($item->element === 'com_joomlaupdate' && !$user->authorise('core.admin')) { + $parent->removeChild($item); + continue; + } elseif ( + ($item->link === 'index.php?option=com_installer&view=install' || $item->link === 'index.php?option=com_installer&view=languages') + && !$user->authorise('core.admin') + ) { + continue; + } elseif ($item->element === 'com_admin') { + if (isset($query['view']) && $query['view'] === 'sysinfo' && !$user->authorise('core.admin')) { + $parent->removeChild($item); + continue; + } + } elseif ($item->link === 'index.php?option=com_messages&view=messages' && !$user->authorise('core.manage', 'com_users')) { + $parent->removeChild($item); + continue; + } + + if ($assetName && !$user->authorise(($item->scope === 'edit') ? 'core.create' : 'core.manage', $assetName)) { + $parent->removeChild($item); + continue; + } + + // Exclude if link is invalid + if (is_null($item->link) || !\in_array($item->type, array('separator', 'heading', 'container')) && trim($item->link) === '') { + $parent->removeChild($item); + continue; + } + + // Process any children if exists + if ($item->hasChildren()) { + $this->preprocess($item); + } + + // Populate automatic children for container items + if ($item->type === 'container') { + $exclude = (array) $itemParams->get('hideitems') ?: array(); + $components = MenusHelper::getMenuItems('main', false, $exclude); + + // We are adding the nodes first to preprocess them, then sort them and add them again. + foreach ($components->getChildren() as $c) { + $item->addChild($c); + } + + $this->preprocess($item); + $children = ArrayHelper::sortObjects($item->getChildren(), 'text', 1, false, true); + + foreach ($children as $c) { + $item->addChild($c); + } + } + + // Exclude if there are no child items under heading or container + if (\in_array($item->type, array('heading', 'container')) && !$item->hasChildren() && empty($item->components)) { + $parent->removeChild($item); + continue; + } + + // Remove repeated and edge positioned separators, It is important to put this check at the end of any logical filtering. + if ($item->type === 'separator') { + if ($noSeparator) { + $parent->removeChild($item); + continue; + } + + $noSeparator = true; + } else { + $noSeparator = false; + } + + // Ok we passed everything, load language at last only + if ($item->element) { + $language->load($item->element . '.sys', JPATH_ADMINISTRATOR) || + $language->load($item->element . '.sys', JPATH_ADMINISTRATOR . '/components/' . $item->element); + } + + if ($item->type === 'separator' && $itemParams->get('text_separator') == 0) { + $item->title = ''; + } + + $item->text = Text::_($item->title); + } + + // If last one was a separator remove it too. + $last = end($parent->getChildren()); + + if ($last && $last->type === 'separator' && $last->getSibling(false) && $last->getSibling(false)->type === 'separator') { + $parent->removeChild($last); + } + } + + /** + * Method to get the CSS class name for an icon identifier or create one if + * a custom image path is passed as the identifier + * + * @param AdministratorMenuItem $node Node to get icon data from + * + * @return string CSS class name + * + * @since 3.8.0 + */ + public function getIconClass($node) + { + $identifier = $node->class; + + // Top level is special + if (trim($identifier) == '') { + return null; + } + + // We were passed a class name + if (substr($identifier, 0, 6) == 'class:') { + $class = substr($identifier, 6); + } else { + // We were passed background icon url. Build the CSS class for the icon + if ($identifier == null) { + return null; + } + + $class = preg_replace('#\.[^.]*$#', '', basename($identifier)); + $class = preg_replace('#\.\.[^A-Za-z0-9\.\_\- ]#', '', $class); + } + + $html = 'icon-' . $class . ' icon-fw'; + + return $html; + } + + /** + * Create unique identifier + * + * @return string + * + * @since 4.0.0 + */ + public function getCounter() + { + $this->counter++; + + return $this->counter; + } } diff --git a/administrator/modules/mod_menu/tmpl/default.php b/administrator/modules/mod_menu/tmpl/default.php index 01f57febbe85b..581f4e9fb9e67 100644 --- a/administrator/modules/mod_menu/tmpl/default.php +++ b/administrator/modules/mod_menu/tmpl/default.php @@ -1,4 +1,5 @@ getWebAssetManager(); $wa->getRegistry()->addExtensionRegistryFile('com_cpanel'); $wa->useScript('metismenujs') - ->registerAndUseScript('mod_menu.admin-menu', 'mod_menu/admin-menu.min.js', [], ['defer' => true], ['metismenujs']) - ->useScript('com_cpanel.admin-system-loader'); + ->registerAndUseScript('mod_menu.admin-menu', 'mod_menu/admin-menu.min.js', [], ['defer' => true], ['metismenujs']) + ->useScript('com_cpanel.admin-system-loader'); // Recurse through children of root node if they exist -if ($root->hasChildren()) -{ - echo '\n"; } diff --git a/administrator/modules/mod_menu/tmpl/default_submenu.php b/administrator/modules/mod_menu/tmpl/default_submenu.php index 566800a96aa78..13230279c5846 100644 --- a/administrator/modules/mod_menu/tmpl/default_submenu.php +++ b/administrator/modules/mod_menu/tmpl/default_submenu.php @@ -1,4 +1,5 @@ getParams(); // Build the CSS class suffix -if (!$this->enabled) -{ - $class .= ' disabled'; -} -elseif ($current->type == 'separator') -{ - $class = $current->title ? 'menuitem-group' : 'divider'; -} -elseif ($current->hasChildren()) -{ - $class .= ' parent'; +if (!$this->enabled) { + $class .= ' disabled'; +} elseif ($current->type == 'separator') { + $class = $current->title ? 'menuitem-group' : 'divider'; +} elseif ($current->hasChildren()) { + $class .= ' parent'; } -if ($current->level == 1) -{ - $class .= ' item-level-1'; -} -elseif ($current->level == 2) -{ - $class .= ' item-level-2'; -} -elseif ($current->level == 3) -{ - $class .= ' item-level-3'; +if ($current->level == 1) { + $class .= ' item-level-1'; +} elseif ($current->level == 2) { + $class .= ' item-level-2'; +} elseif ($current->level == 3) { + $class .= ' item-level-3'; } // Set the correct aria role and print the item -if ($current->type == 'separator') -{ - echo '
  • '; +if ($current->type == 'separator') { + echo '
  • '; } // Print a link if it exists @@ -67,18 +55,14 @@ $itemIconClass = ''; $itemImage = ''; -if ($current->hasChildren()) -{ - $linkClass[] = 'has-arrow'; +if ($current->hasChildren()) { + $linkClass[] = 'has-arrow'; - if ($current->level > 2) - { - $dataToggle = ' data-bs-toggle="dropdown"'; - } -} -else -{ - $linkClass[] = 'no-dropdown'; + if ($current->level > 2) { + $dataToggle = ' data-bs-toggle="dropdown"'; + } +} else { + $linkClass[] = 'no-dropdown'; } // Implode out $linkClass for rendering @@ -100,110 +84,86 @@ $iconImage = $current->icon; $homeImage = ''; -if ($iconClass === '' && $itemIconClass) -{ - $iconClass = ''; +if ($iconClass === '' && $itemIconClass) { + $iconClass = ''; } -if ($iconImage) -{ - if (substr($iconImage, 0, 6) == 'class:' && substr($iconImage, 6) == 'icon-home') - { - $iconImage = ''; - $iconImage .= '' . Text::_('JDEFAULT') . ''; - } - elseif (substr($iconImage, 0, 6) == 'image:') - { - $iconImage = ' ' . substr($iconImage, 6) . ''; - } - else - { - $iconImage = ''; - } +if ($iconImage) { + if (substr($iconImage, 0, 6) == 'class:' && substr($iconImage, 6) == 'icon-home') { + $iconImage = ''; + $iconImage .= '' . Text::_('JDEFAULT') . ''; + } elseif (substr($iconImage, 0, 6) == 'image:') { + $iconImage = ' ' . substr($iconImage, 6) . ''; + } else { + $iconImage = ''; + } } $itemImage = (empty($itemIconClass) && $itemImage) ? '  ' : ''; // If the item image is not set, the item title would not have margin. Here we add it. -if ($icon == '' && $iconClass == '' && $current->level == 1 && $current->target == '') -{ - $iconClass = ''; +if ($icon == '' && $iconClass == '' && $current->level == 1 && $current->target == '') { + $iconClass = ''; +} + +if ($link != '' && $current->target != '') { + echo '' + . $iconClass + . '' . $itemImage . Text::_($current->title) . '' . $ajax . ''; +} elseif ($link != '' && $current->type !== 'separator') { + echo '' + . $iconClass + . '' . $itemImage . Text::_($current->title) . '' . $iconImage . ''; +} elseif ($current->title != '' && $current->type !== 'separator') { + echo '' + . $iconClass + . '' . $itemImage . Text::_($current->title) . '' . $ajax . ''; +} elseif ($current->title != '' && $current->type === 'separator') { + echo '' . Text::_($current->title) . '' . $ajax; +} else { + echo '' . Text::_($current->title) . '' . $ajax; +} + +if ($currentParams->get('menu-quicktask') && (int) $this->params->get('shownew', 1) === 1) { + $params = $current->getParams(); + $user = $this->application->getIdentity(); + $link = $params->get('menu-quicktask'); + $icon = $params->get('menu-quicktask-icon', 'plus'); + $title = $params->get('menu-quicktask-title', 'MOD_MENU_QUICKTASK_NEW'); + $permission = $params->get('menu-quicktask-permission'); + $scope = $current->scope !== 'default' ? $current->scope : null; + + if (!$permission || $user->authorise($permission, $scope)) { + echo ''; + echo ''; + echo '' . Text::_($title) . ''; + echo ''; + } +} + +if (!empty($current->dashboard)) { + $titleDashboard = Text::sprintf('MOD_MENU_DASHBOARD_LINK', Text::_($current->title)); + echo '' + . '' + . '' . $titleDashboard . '' + . ''; } -if ($link != '' && $current->target != '') -{ - echo '' - . $iconClass - . '' . $itemImage . Text::_($current->title) . '' . $ajax . ''; -} -elseif ($link != '' && $current->type !== 'separator') -{ - echo '' - . $iconClass - . '' . $itemImage . Text::_($current->title) . '' . $iconImage . ''; -} -elseif ($current->title != '' && $current->type !== 'separator') -{ - echo '' - . $iconClass - . ''. $itemImage . Text::_($current->title) . '' . $ajax . ''; -} -elseif ($current->title != '' && $current->type === 'separator') -{ - echo '' . Text::_($current->title) . '' . $ajax; -} -else -{ - echo '' . Text::_($current->title) . '' . $ajax; -} +// Recurse through children if they exist +if ($this->enabled && $current->hasChildren()) { + if ($current->level > 1) { + $id = $current->id ? ' id="menu-' . strtolower($current->id) . '"' : ''; -if ($currentParams->get('menu-quicktask') && (int) $this->params->get('shownew', 1) === 1) -{ - $params = $current->getParams(); - $user = $this->application->getIdentity(); - $link = $params->get('menu-quicktask'); - $icon = $params->get('menu-quicktask-icon', 'plus'); - $title = $params->get('menu-quicktask-title', 'MOD_MENU_QUICKTASK_NEW'); - $permission = $params->get('menu-quicktask-permission'); - $scope = $current->scope !== 'default' ? $current->scope : null; - - if (!$permission || $user->authorise($permission, $scope)) - { - echo ''; - echo ''; - echo '' . Text::_($title) . ''; - echo ''; - } -} + echo '' . "\n"; + } else { + echo '
      ' . "\n"; + } -if (!empty($current->dashboard)) -{ - $titleDashboard = Text::sprintf('MOD_MENU_DASHBOARD_LINK', Text::_($current->title)); - echo '' - . '' - . '' . $titleDashboard . '' - . ''; -} + // WARNING: Do not use direct 'include' or 'require' as it is important to isolate the scope for each call + $this->renderSubmenu(__FILE__, $current); -// Recurse through children if they exist -if ($this->enabled && $current->hasChildren()) -{ - if ($current->level > 1) - { - $id = $current->id ? ' id="menu-' . strtolower($current->id) . '"' : ''; - - echo '' . "\n"; - } - else - { - echo '
        ' . "\n"; - } - - // WARNING: Do not use direct 'include' or 'require' as it is important to isolate the scope for each call - $this->renderSubmenu(__FILE__, $current); - - echo "
      \n"; + echo "
    \n"; } echo "
  • \n"; diff --git a/administrator/modules/mod_messages/mod_messages.php b/administrator/modules/mod_messages/mod_messages.php index 296f4e1109a59..36ce8130708c9 100644 --- a/administrator/modules/mod_messages/mod_messages.php +++ b/administrator/modules/mod_messages/mod_messages.php @@ -1,4 +1,5 @@ getIdentity()->authorise('core.login.admin') || !$app->getIdentity()->authorise('core.manage', 'com_messages')) -{ - return; +if (!$app->getIdentity()->authorise('core.login.admin') || !$app->getIdentity()->authorise('core.manage', 'com_messages')) { + return; } // Try to get the items from the messages model -try -{ - /** @var \Joomla\Component\Messages\Administrator\Model\MessagesModel $messagesModel */ - $messagesModel = $app->bootComponent('com_messages')->getMVCFactory() - ->createModel('Messages', 'Administrator', ['ignore_request' => true]); - $messagesModel->setState('filter.state', 0); - $messages = $messagesModel->getItems(); -} -catch (RuntimeException $e) -{ - $messages = []; +try { + /** @var \Joomla\Component\Messages\Administrator\Model\MessagesModel $messagesModel */ + $messagesModel = $app->bootComponent('com_messages')->getMVCFactory() + ->createModel('Messages', 'Administrator', ['ignore_request' => true]); + $messagesModel->setState('filter.state', 0); + $messages = $messagesModel->getItems(); +} catch (RuntimeException $e) { + $messages = []; - // Still render the error message from the Exception object - $app->enqueueMessage($e->getMessage(), 'error'); + // Still render the error message from the Exception object + $app->enqueueMessage($e->getMessage(), 'error'); } $countUnread = count($messages); diff --git a/administrator/modules/mod_messages/tmpl/default.php b/administrator/modules/mod_messages/tmpl/default.php index 43da51c9c8d8d..77e03cef5e79e 100644 --- a/administrator/modules/mod_messages/tmpl/default.php +++ b/administrator/modules/mod_messages/tmpl/default.php @@ -1,4 +1,5 @@ input->getBool('hidemainmenu'); -if ($hideLinks || $countUnread < 1) -{ - return; +if ($hideLinks || $countUnread < 1) { + return; } $route = 'index.php?option=com_messages&view=messages'; ?> -
    -
    - - -
    -
    -
    - -
    +
    +
    + + +
    +
    +
    + +
    diff --git a/administrator/modules/mod_multilangstatus/mod_multilangstatus.php b/administrator/modules/mod_multilangstatus/mod_multilangstatus.php index 463f03f1b4774..959e1fb4fefe9 100644 --- a/administrator/modules/mod_multilangstatus/mod_multilangstatus.php +++ b/administrator/modules/mod_multilangstatus/mod_multilangstatus.php @@ -1,4 +1,5 @@ input->getBool('hidemainmenu'); -if (!$multilanguageEnabled || $hideLinks) -{ - return; +if (!$multilanguageEnabled || $hideLinks) { + return; } $modalHTML = HTMLHelper::_( - 'bootstrap.renderModal', - 'multiLangModal', - array( - 'title' => Text::_('MOD_MULTILANGSTATUS'), - 'url' => Route::_('index.php?option=com_languages&view=multilangstatus&tmpl=component'), - 'height' => '400px', - 'width' => '800px', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ) + 'bootstrap.renderModal', + 'multiLangModal', + array( + 'title' => Text::_('MOD_MULTILANGSTATUS'), + 'url' => Route::_('index.php?option=com_languages&view=multilangstatus&tmpl=component'), + 'height' => '400px', + 'width' => '800px', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ) ); $app->getDocument()->getWebAssetManager() - ->registerAndUseScript('mod_multilangstatus.admin', 'mod_multilangstatus/admin-multilangstatus.min.js', [], ['type' => 'module', 'defer' => true]); + ->registerAndUseScript('mod_multilangstatus.admin', 'mod_multilangstatus/admin-multilangstatus.min.js', [], ['type' => 'module', 'defer' => true]); ?> -
    - -
    -
    - -
    +
    + +
    +
    + +
    diff --git a/administrator/modules/mod_popular/mod_popular.php b/administrator/modules/mod_popular/mod_popular.php index 9b6d859456671..6e283cfd5864c 100644 --- a/administrator/modules/mod_popular/mod_popular.php +++ b/administrator/modules/mod_popular/mod_popular.php @@ -1,4 +1,5 @@ get('automatic_title', 0)) -{ - $module->title = PopularHelper::getTitle($params); +if ($params->get('automatic_title', 0)) { + $module->title = PopularHelper::getTitle($params); } // If recording of hits is disabled. -if (!ComponentHelper::getParams('com_content')->get('record_hits', 1)) -{ - echo LayoutHelper::render('joomla.content.emptystate_module', [ - 'title' => 'JGLOBAL_RECORD_HITS_DISABLED', - 'icon' => 'icon-minus-circle', - ] - ); - - return; +if (!ComponentHelper::getParams('com_content')->get('record_hits', 1)) { + echo LayoutHelper::render('joomla.content.emptystate_module', [ + 'title' => 'JGLOBAL_RECORD_HITS_DISABLED', + 'icon' => 'icon-minus-circle', + ]); + + return; } // If there are some articles to display. -if (count($list)) -{ - require ModuleHelper::getLayoutPath('mod_popular', $params->get('layout', 'default')); +if (count($list)) { + require ModuleHelper::getLayoutPath('mod_popular', $params->get('layout', 'default')); - return; + return; } // If there are no articles to display, show empty state. $app->getLanguage()->load('com_content'); echo LayoutHelper::render('joomla.content.emptystate_module', [ - 'textPrefix' => 'COM_CONTENT', - 'icon' => 'icon-copy', - ] -); + 'textPrefix' => 'COM_CONTENT', + 'icon' => 'icon-copy', + ]); diff --git a/administrator/modules/mod_popular/src/Helper/PopularHelper.php b/administrator/modules/mod_popular/src/Helper/PopularHelper.php index 1572b9b821013..723ee72a9d8e6 100644 --- a/administrator/modules/mod_popular/src/Helper/PopularHelper.php +++ b/administrator/modules/mod_popular/src/Helper/PopularHelper.php @@ -1,4 +1,5 @@ setState('list.select', 'a.id, a.title, a.checked_out, a.checked_out_time, ' . - ' a.publish_up, a.hits' - ); - - // Set Ordering filter - $model->setState('list.ordering', 'a.hits'); - $model->setState('list.direction', 'DESC'); - - // Set Category Filter - $categoryId = $params->get('catid', null); - - if (is_numeric($categoryId)) - { - $model->setState('filter.category_id', $categoryId); - } - - // Set User Filter. - $userId = $user->get('id'); - - switch ($params->get('user_id', '0')) - { - case 'by_me': - $model->setState('filter.author_id', $userId); - break; - - case 'not_me': - $model->setState('filter.author_id', $userId); - $model->setState('filter.author_id.include', false); - break; - } - - // Set the Start and Limit - $model->setState('list.start', 0); - $model->setState('list.limit', $params->get('count', 5)); - - $items = $model->getItems(); - - if ($error = $model->getError()) - { - throw new \Exception($error, 500); - } - - // Set the links - foreach ($items as &$item) - { - $item->link = ''; - - if ($user->authorise('core.edit', 'com_content.article.' . $item->id) - || ($user->authorise('core.edit.own', 'com_content.article.' . $item->id) && ($userId === $item->created_by))) - { - $item->link = Route::_('index.php?option=com_content&task=article.edit&id=' . $item->id); - } - } - - return $items; - } - - /** - * Get the alternate title for the module - * - * @param Registry $params The module parameters. - * - * @return string The alternate title for the module. - */ - public static function getTitle($params) - { - $who = $params->get('user_id', 0); - $catid = (int) $params->get('catid', null); - $title = ''; - - if ($catid) - { - $category = Categories::getInstance('Content')->get($catid); - $title = Text::_('MOD_POPULAR_UNEXISTING'); - - if ($category) - { - $title = $category->title; - } - } - - return Text::plural( - 'MOD_POPULAR_TITLE' . ($catid ? '_CATEGORY' : '') . ($who != '0' ? "_$who" : ''), - (int) $params->get('count', 5), - $title - ); - } + /** + * Get a list of the most popular articles. + * + * @param Registry &$params The module parameters. + * @param ArticlesModel $model The model. + * + * @return mixed An array of articles, or false on error. + * + * @throws \Exception + */ + public static function getList(Registry &$params, ArticlesModel $model) + { + $user = Factory::getUser(); + + // Set List SELECT + $model->setState('list.select', 'a.id, a.title, a.checked_out, a.checked_out_time, ' . + ' a.publish_up, a.hits'); + + // Set Ordering filter + $model->setState('list.ordering', 'a.hits'); + $model->setState('list.direction', 'DESC'); + + // Set Category Filter + $categoryId = $params->get('catid', null); + + if (is_numeric($categoryId)) { + $model->setState('filter.category_id', $categoryId); + } + + // Set User Filter. + $userId = $user->get('id'); + + switch ($params->get('user_id', '0')) { + case 'by_me': + $model->setState('filter.author_id', $userId); + break; + + case 'not_me': + $model->setState('filter.author_id', $userId); + $model->setState('filter.author_id.include', false); + break; + } + + // Set the Start and Limit + $model->setState('list.start', 0); + $model->setState('list.limit', $params->get('count', 5)); + + $items = $model->getItems(); + + if ($error = $model->getError()) { + throw new \Exception($error, 500); + } + + // Set the links + foreach ($items as &$item) { + $item->link = ''; + + if ( + $user->authorise('core.edit', 'com_content.article.' . $item->id) + || ($user->authorise('core.edit.own', 'com_content.article.' . $item->id) && ($userId === $item->created_by)) + ) { + $item->link = Route::_('index.php?option=com_content&task=article.edit&id=' . $item->id); + } + } + + return $items; + } + + /** + * Get the alternate title for the module + * + * @param Registry $params The module parameters. + * + * @return string The alternate title for the module. + */ + public static function getTitle($params) + { + $who = $params->get('user_id', 0); + $catid = (int) $params->get('catid', null); + $title = ''; + + if ($catid) { + $category = Categories::getInstance('Content')->get($catid); + $title = Text::_('MOD_POPULAR_UNEXISTING'); + + if ($category) { + $title = $category->title; + } + } + + return Text::plural( + 'MOD_POPULAR_TITLE' . ($catid ? '_CATEGORY' : '') . ($who != '0' ? "_$who" : ''), + (int) $params->get('count', 5), + $title + ); + } } diff --git a/administrator/modules/mod_popular/tmpl/default.php b/administrator/modules/mod_popular/tmpl/default.php index 6992abc8befe8..4949093a8177e 100644 --- a/administrator/modules/mod_popular/tmpl/default.php +++ b/administrator/modules/mod_popular/tmpl/default.php @@ -1,4 +1,5 @@ - - - - - - - - - - - $item) : ?> - - hits; ?> - = 10000 ? 'danger' : ($hits >= 1000 ? 'warning' : ($hits >= 100 ? 'info' : 'secondary'))); ?> - - - - - - - - - - - - + + + + + + + + + + + $item) : ?> + + hits; ?> + = 10000 ? 'danger' : ($hits >= 1000 ? 'warning' : ($hits >= 100 ? 'info' : 'secondary'))); ?> + + + + + + + + + + + +
    title; ?>
    - checked_out) : ?> - editor, $item->checked_out_time); ?> - - link) : ?> - - title, ENT_QUOTES, 'UTF-8'); ?> - - - title, ENT_QUOTES, 'UTF-8'); ?> - - - hits; ?> - - publish_up, Text::_('DATE_FORMAT_LC4')); ?> -
    - -
    title; ?>
    + checked_out) : ?> + editor, $item->checked_out_time); ?> + + link) : ?> + + title, ENT_QUOTES, 'UTF-8'); ?> + + + title, ENT_QUOTES, 'UTF-8'); ?> + + + hits; ?> + + publish_up, Text::_('DATE_FORMAT_LC4')); ?> +
    + +
    diff --git a/administrator/modules/mod_post_installation_messages/mod_post_installation_messages.php b/administrator/modules/mod_post_installation_messages/mod_post_installation_messages.php index 1812161ddf736..9f9268c4ccf59 100644 --- a/administrator/modules/mod_post_installation_messages/mod_post_installation_messages.php +++ b/administrator/modules/mod_post_installation_messages/mod_post_installation_messages.php @@ -1,4 +1,5 @@ bootComponent('com_postinstall')->getMVCFactory() - ->createModel('Messages', 'Administrator', ['ignore_request' => true]); - $messagesCount = $messagesModel->getItemsCount(); -} -catch (RuntimeException $e) -{ - $messagesCount = 0; +try { + /** @var \Joomla\Component\Postinstall\Administrator\Model\MessagesModel $messagesModel */ + $messagesModel = $app->bootComponent('com_postinstall')->getMVCFactory() + ->createModel('Messages', 'Administrator', ['ignore_request' => true]); + $messagesCount = $messagesModel->getItemsCount(); +} catch (RuntimeException $e) { + $messagesCount = 0; - // Still render the error message from the Exception object - $app->enqueueMessage($e->getMessage(), 'error'); + // Still render the error message from the Exception object + $app->enqueueMessage($e->getMessage(), 'error'); } $joomlaFilesExtensionId = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; diff --git a/administrator/modules/mod_post_installation_messages/tmpl/default.php b/administrator/modules/mod_post_installation_messages/tmpl/default.php index 8efb6503ef488..f2b9cef548449 100644 --- a/administrator/modules/mod_post_installation_messages/tmpl/default.php +++ b/administrator/modules/mod_post_installation_messages/tmpl/default.php @@ -1,4 +1,5 @@ input->getBool('hidemainmenu'); -if ($hideLinks || $messagesCount < 1) -{ - return; +if ($hideLinks || $messagesCount < 1) { + return; } ?> getIdentity()->authorise('core.manage', 'com_postinstall')) : ?> - -
    -
    - - -
    -
    -
    - -
    -
    + +
    +
    + + +
    +
    +
    + +
    +
    diff --git a/administrator/modules/mod_privacy_dashboard/mod_privacy_dashboard.php b/administrator/modules/mod_privacy_dashboard/mod_privacy_dashboard.php index a58da39b7ee66..28c4bb5e75f88 100644 --- a/administrator/modules/mod_privacy_dashboard/mod_privacy_dashboard.php +++ b/administrator/modules/mod_privacy_dashboard/mod_privacy_dashboard.php @@ -1,4 +1,5 @@ getIdentity()->authorise('core.admin')) -{ - return; +if (!$app->getIdentity()->authorise('core.admin')) { + return; } // Boot component to ensure HTML helpers are loaded @@ -25,19 +25,15 @@ // Load the privacy component language file. $lang = $app->getLanguage(); $lang->load('com_privacy', JPATH_ADMINISTRATOR) - || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); + || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); $list = PrivacyDashboardHelper::getData(); -if (count($list)) -{ - require ModuleHelper::getLayoutPath('mod_privacy_dashboard', $params->get('layout', 'default')); -} -else -{ - echo LayoutHelper::render('joomla.content.emptystate_module', [ - 'textPrefix' => 'COM_PRIVACY_REQUESTS', - 'icon' => 'icon-lock', - ] - ); +if (count($list)) { + require ModuleHelper::getLayoutPath('mod_privacy_dashboard', $params->get('layout', 'default')); +} else { + echo LayoutHelper::render('joomla.content.emptystate_module', [ + 'textPrefix' => 'COM_PRIVACY_REQUESTS', + 'icon' => 'icon-lock', + ]); } diff --git a/administrator/modules/mod_privacy_dashboard/src/Helper/PrivacyDashboardHelper.php b/administrator/modules/mod_privacy_dashboard/src/Helper/PrivacyDashboardHelper.php index 7a7088d69cc8e..06f358bdbd496 100644 --- a/administrator/modules/mod_privacy_dashboard/src/Helper/PrivacyDashboardHelper.php +++ b/administrator/modules/mod_privacy_dashboard/src/Helper/PrivacyDashboardHelper.php @@ -1,4 +1,5 @@ getQuery(true) - ->select( - [ - 'COUNT(*) AS count', - $db->quoteName('status'), - $db->quoteName('request_type'), - ] - ) - ->from($db->quoteName('#__privacy_requests')) - ->group($db->quoteName('status')) - ->group($db->quoteName('request_type')); + /** + * Method to retrieve information about the site privacy requests + * + * @return array Array containing site privacy requests + * + * @since 3.9.0 + */ + public static function getData() + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + 'COUNT(*) AS count', + $db->quoteName('status'), + $db->quoteName('request_type'), + ] + ) + ->from($db->quoteName('#__privacy_requests')) + ->group($db->quoteName('status')) + ->group($db->quoteName('request_type')); - $db->setQuery($query); + $db->setQuery($query); - try - { - return $db->loadObjectList(); - } - catch (ExecutionFailureException $e) - { - return []; - } - } + try { + return $db->loadObjectList(); + } catch (ExecutionFailureException $e) { + return []; + } + } } diff --git a/administrator/modules/mod_privacy_dashboard/tmpl/default.php b/administrator/modules/mod_privacy_dashboard/tmpl/default.php index 0450c0f4746ba..c82f8205ede0a 100644 --- a/administrator/modules/mod_privacy_dashboard/tmpl/default.php +++ b/administrator/modules/mod_privacy_dashboard/tmpl/default.php @@ -1,4 +1,5 @@ - - - - - - - - - - - $item) : ?> - status, array(0, 1))) : ?> - count; ?> - - count; ?> - - - - - - - - - - - - + + + + + + + + + + + $item) : ?> + status, array(0, 1))) : ?> + count; ?> + + count; ?> + + + + + + + + + + + +
    title; ?>
    - - request_type); ?> - - - status); ?> - - count; ?> -
    - -
    title; ?>
    + + request_type); ?> + + + status); ?> + + count; ?> +
    + +
    -
    -
    -
    -
    +
    +
    +
    +
    diff --git a/administrator/modules/mod_privacy_status/mod_privacy_status.php b/administrator/modules/mod_privacy_status/mod_privacy_status.php index 964b15118d67d..afa43af91bbf1 100644 --- a/administrator/modules/mod_privacy_status/mod_privacy_status.php +++ b/administrator/modules/mod_privacy_status/mod_privacy_status.php @@ -1,4 +1,5 @@ getIdentity()->authorise('core.admin')) -{ - return; +if (!$app->getIdentity()->authorise('core.admin')) { + return; } // Boot component to ensure HTML helpers are loaded @@ -27,7 +27,7 @@ // Load the privacy component language file. $lang = $app->getLanguage(); $lang->load('com_privacy', JPATH_ADMINISTRATOR) - || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); + || $lang->load('com_privacy', JPATH_ADMINISTRATOR . '/components/com_privacy'); $privacyPolicyInfo = PrivacyStatusHelper::getPrivacyPolicyInfo(); $requestFormPublished = PrivacyStatusHelper::getRequestFormPublished(); diff --git a/administrator/modules/mod_privacy_status/src/Helper/PrivacyStatusHelper.php b/administrator/modules/mod_privacy_status/src/Helper/PrivacyStatusHelper.php index 2bbeab6647f34..2d7bb9ea2a17a 100644 --- a/administrator/modules/mod_privacy_status/src/Helper/PrivacyStatusHelper.php +++ b/administrator/modules/mod_privacy_status/src/Helper/PrivacyStatusHelper.php @@ -1,4 +1,5 @@ false, - 'articlePublished' => false, - 'editLink' => '', - ]; - - /* - * Prior to 3.9.0 it was common for a plugin such as the User - Profile plugin to define a privacy policy or - * terms of service article, therefore we will also import the user plugin group to process this event. - */ - PluginHelper::importPlugin('privacy'); - PluginHelper::importPlugin('user'); - - Factory::getApplication()->triggerEvent('onPrivacyCheckPrivacyPolicyPublished', [&$policy]); - - return $policy; - } - - /** - * Check whether there is a menu item for the request form - * - * @return array Array containing a status of whether a menu is published for the request form and its current link - * - * @since 4.0.0 - */ - public static function getRequestFormPublished() - { - $status = [ - 'exists' => false, - 'published' => false, - 'link' => '', - ]; - - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('id'), - $db->quoteName('published'), - $db->quoteName('language'), - ] - ) - ->from($db->quoteName('#__menu')) - ->where( - [ - $db->quoteName('client_id') . ' = 0', - $db->quoteName('link') . ' = ' . $db->quote('index.php?option=com_privacy&view=request'), - ] - ) - ->setLimit(1); - $db->setQuery($query); - - $menuItem = $db->loadObject(); - - // Check if the menu item exists in database - if ($menuItem) - { - $status['exists'] = true; - - // Check if the menu item is published - if ($menuItem->published == 1) - { - $status['published'] = true; - } - - // Add language to the url if the site is multilingual - if (Multilanguage::isEnabled() && $menuItem->language && $menuItem->language !== '*') - { - $lang = '&lang=' . $menuItem->language; - } - else - { - $lang = ''; - } - } - - $linkMode = Factory::getApplication()->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; - - if (!$menuItem) - { - if (Multilanguage::isEnabled()) - { - // Find the Itemid of the home menu item tagged to the site default language - $params = ComponentHelper::getParams('com_languages'); - $defaultSiteLanguage = $params->get('site'); - - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__menu')) - ->where( - [ - $db->quoteName('client_id') . ' = 0', - $db->quoteName('home') . ' = 1', - $db->quoteName('language') . ' = :language', - ] - ) - ->bind(':language', $defaultSiteLanguage) - ->setLimit(1); - $db->setQuery($query); - - $homeId = (int) $db->loadResult(); - $itemId = $homeId ? '&Itemid=' . $homeId : ''; - } - else - { - $itemId = ''; - } - - $status['link'] = Route::link('site', 'index.php?option=com_privacy&view=request' . $itemId, true, $linkMode); - } - else - { - $status['link'] = Route::link('site', 'index.php?Itemid=' . $menuItem->id . $lang, true, $linkMode); - } - - return $status; - } - - /** - * Method to return number privacy requests older than X days. - * - * @return integer - * - * @since 4.0.0 - */ - public static function getNumberUrgentRequests() - { - // Load the parameters. - $params = ComponentHelper::getComponent('com_privacy')->getParams(); - $notify = (int) $params->get('notify', 14); - $now = Factory::getDate()->toSql(); - $period = '-' . $notify; - - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $query->select('COUNT(*)') - ->from($db->quoteName('#__privacy_requests')) - ->where( - [ - $db->quoteName('status') . ' = 1', - $query->dateAdd($db->quote($now), $period, 'DAY') . ' > ' . $db->quoteName('requested_at'), - ] - ); - $db->setQuery($query); - - return (int) $db->loadResult(); - } + /** + * Get the information about the published privacy policy + * + * @return array Array containing a status of whether a privacy policy is set and a link to the policy document for editing + * + * @since 4.0.0 + */ + public static function getPrivacyPolicyInfo() + { + $policy = [ + 'published' => false, + 'articlePublished' => false, + 'editLink' => '', + ]; + + /* + * Prior to 3.9.0 it was common for a plugin such as the User - Profile plugin to define a privacy policy or + * terms of service article, therefore we will also import the user plugin group to process this event. + */ + PluginHelper::importPlugin('privacy'); + PluginHelper::importPlugin('user'); + + Factory::getApplication()->triggerEvent('onPrivacyCheckPrivacyPolicyPublished', [&$policy]); + + return $policy; + } + + /** + * Check whether there is a menu item for the request form + * + * @return array Array containing a status of whether a menu is published for the request form and its current link + * + * @since 4.0.0 + */ + public static function getRequestFormPublished() + { + $status = [ + 'exists' => false, + 'published' => false, + 'link' => '', + ]; + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('id'), + $db->quoteName('published'), + $db->quoteName('language'), + ] + ) + ->from($db->quoteName('#__menu')) + ->where( + [ + $db->quoteName('client_id') . ' = 0', + $db->quoteName('link') . ' = ' . $db->quote('index.php?option=com_privacy&view=request'), + ] + ) + ->setLimit(1); + $db->setQuery($query); + + $menuItem = $db->loadObject(); + + // Check if the menu item exists in database + if ($menuItem) { + $status['exists'] = true; + + // Check if the menu item is published + if ($menuItem->published == 1) { + $status['published'] = true; + } + + // Add language to the url if the site is multilingual + if (Multilanguage::isEnabled() && $menuItem->language && $menuItem->language !== '*') { + $lang = '&lang=' . $menuItem->language; + } else { + $lang = ''; + } + } + + $linkMode = Factory::getApplication()->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; + + if (!$menuItem) { + if (Multilanguage::isEnabled()) { + // Find the Itemid of the home menu item tagged to the site default language + $params = ComponentHelper::getParams('com_languages'); + $defaultSiteLanguage = $params->get('site'); + + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__menu')) + ->where( + [ + $db->quoteName('client_id') . ' = 0', + $db->quoteName('home') . ' = 1', + $db->quoteName('language') . ' = :language', + ] + ) + ->bind(':language', $defaultSiteLanguage) + ->setLimit(1); + $db->setQuery($query); + + $homeId = (int) $db->loadResult(); + $itemId = $homeId ? '&Itemid=' . $homeId : ''; + } else { + $itemId = ''; + } + + $status['link'] = Route::link('site', 'index.php?option=com_privacy&view=request' . $itemId, true, $linkMode); + } else { + $status['link'] = Route::link('site', 'index.php?Itemid=' . $menuItem->id . $lang, true, $linkMode); + } + + return $status; + } + + /** + * Method to return number privacy requests older than X days. + * + * @return integer + * + * @since 4.0.0 + */ + public static function getNumberUrgentRequests() + { + // Load the parameters. + $params = ComponentHelper::getComponent('com_privacy')->getParams(); + $notify = (int) $params->get('notify', 14); + $now = Factory::getDate()->toSql(); + $period = '-' . $notify; + + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $query->select('COUNT(*)') + ->from($db->quoteName('#__privacy_requests')) + ->where( + [ + $db->quoteName('status') . ' = 1', + $query->dateAdd($db->quote($now), $period, 'DAY') . ' > ' . $db->quoteName('requested_at'), + ] + ); + $db->setQuery($query); + + return (int) $db->loadResult(); + } } diff --git a/administrator/modules/mod_privacy_status/tmpl/default.php b/administrator/modules/mod_privacy_status/tmpl/default.php index 35bc270b8ccc3..5b1c3663b872e 100644 --- a/administrator/modules/mod_privacy_status/tmpl/default.php +++ b/administrator/modules/mod_privacy_status/tmpl/default.php @@ -1,4 +1,5 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    - - - - - - - - - - - - - - - - - -
    - - - - - - -
    - - - - - - - - - - - - - - - - - -
    - - - -
    - - - - - - - - - - - - -
    - - 0) : ?> - - -
    - - - - - - - - - - - - - -
    - - -
    - -
    - - - - - - - - - - - - - - - - - -
    + + + + + + + + + + + + + + + + + +
    + + + + + + +
    + + + + + + + + + + + + + + + + + +
    + + + +
    + + + + + + + + + + + + +
    + + 0) : ?> + + +
    + + + + + + + + + + + + + +
    + + +
    + +
    + + + + + + + + + + + + + + + + + +
    diff --git a/administrator/modules/mod_quickicon/services/provider.php b/administrator/modules/mod_quickicon/services/provider.php index 86c396968362b..34f4e43c5ac33 100644 --- a/administrator/modules/mod_quickicon/services/provider.php +++ b/administrator/modules/mod_quickicon/services/provider.php @@ -1,4 +1,5 @@ registerServiceProvider(new ModuleDispatcherFactory('\\Joomla\\Module\\Quickicon')); - $container->registerServiceProvider(new HelperFactory('\\Joomla\\Module\\Quickicon\\Administrator\\Helper')); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->registerServiceProvider(new ModuleDispatcherFactory('\\Joomla\\Module\\Quickicon')); + $container->registerServiceProvider(new HelperFactory('\\Joomla\\Module\\Quickicon\\Administrator\\Helper')); - $container->registerServiceProvider(new Module); - } + $container->registerServiceProvider(new Module()); + } }; diff --git a/administrator/modules/mod_quickicon/src/Dispatcher/Dispatcher.php b/administrator/modules/mod_quickicon/src/Dispatcher/Dispatcher.php index f047562713c66..6db1a597d1818 100644 --- a/administrator/modules/mod_quickicon/src/Dispatcher/Dispatcher.php +++ b/administrator/modules/mod_quickicon/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->bootModule('mod_quickicon', 'administrator')->getHelper('QuickIconHelper'); - $data['buttons'] = $helper->getButtons($data['params'], $this->getApplication()); + $helper = $this->app->bootModule('mod_quickicon', 'administrator')->getHelper('QuickIconHelper'); + $data['buttons'] = $helper->getButtons($data['params'], $this->getApplication()); - return $data; - } + return $data; + } } diff --git a/administrator/modules/mod_quickicon/src/Event/QuickIconsEvent.php b/administrator/modules/mod_quickicon/src/Event/QuickIconsEvent.php index 3e0da89908299..c0cdac0ad37fa 100644 --- a/administrator/modules/mod_quickicon/src/Event/QuickIconsEvent.php +++ b/administrator/modules/mod_quickicon/src/Event/QuickIconsEvent.php @@ -1,4 +1,5 @@ context; - } + /** + * Get the event context + * + * @return string + * + * @since 4.0.0 + */ + public function getContext() + { + return $this->context; + } - /** - * Set the event context - * - * @param string $context The event context - * - * @return string - * - * @since 4.0.0 - */ - public function setContext($context) - { - $this->context = $context; + /** + * Set the event context + * + * @param string $context The event context + * + * @return string + * + * @since 4.0.0 + */ + public function setContext($context) + { + $this->context = $context; - return $context; - } + return $context; + } } diff --git a/administrator/modules/mod_quickicon/src/Helper/QuickIconHelper.php b/administrator/modules/mod_quickicon/src/Helper/QuickIconHelper.php index e6496e281e254..b70ead7d4f402 100644 --- a/administrator/modules/mod_quickicon/src/Helper/QuickIconHelper.php +++ b/administrator/modules/mod_quickicon/src/Helper/QuickIconHelper.php @@ -1,4 +1,5 @@ get('context', 'mod_quickicon'); - - if (!isset($this->buttons[$key])) - { - // Load mod_quickicon language file in case this method is called before rendering the module - $application->getLanguage()->load('mod_quickicon'); - - $this->buttons[$key] = []; - - if ($params->get('show_users')) - { - $tmp = [ - 'image' => 'icon-users', - 'link' => Route::_('index.php?option=com_users&view=users'), - 'linkadd' => Route::_('index.php?option=com_users&task=user.add'), - 'name' => 'MOD_QUICKICON_USER_MANAGER', - 'access' => array('core.manage', 'com_users', 'core.create', 'com_users'), - 'group' => 'MOD_QUICKICON_SITE', - ]; - - if ($params->get('show_users') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_users&task=users.getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_menuitems')) - { - $tmp = [ - 'image' => 'icon-list', - 'link' => Route::_('index.php?option=com_menus&view=items&menutype='), - 'linkadd' => Route::_('index.php?option=com_menus&task=item.add'), - 'name' => 'MOD_QUICKICON_MENUITEMS_MANAGER', - 'access' => array('core.manage', 'com_menus', 'core.create', 'com_menus'), - 'group' => 'MOD_QUICKICON_STRUCTURE', - ]; - - if ($params->get('show_menuitems') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_menus&task=items.getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_articles')) - { - $tmp = [ - 'image' => 'icon-file-alt', - 'link' => Route::_('index.php?option=com_content&view=articles'), - 'linkadd' => Route::_('index.php?option=com_content&task=article.add'), - 'name' => 'MOD_QUICKICON_ARTICLE_MANAGER', - 'access' => array('core.manage', 'com_content', 'core.create', 'com_content'), - 'group' => 'MOD_QUICKICON_SITE', - ]; - - if ($params->get('show_articles') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_content&task=articles.getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_tags')) - { - $tmp = [ - 'image' => 'icon-tag', - 'link' => Route::_('index.php?option=com_tags&view=tags'), - 'linkadd' => Route::_('index.php?option=com_tags&task=tag.edit'), - 'name' => 'MOD_QUICKICON_TAGS_MANAGER', - 'access' => array('core.manage', 'com_tags', 'core.create', 'com_tags'), - 'group' => 'MOD_QUICKICON_SITE', - ]; - - if ($params->get('show_tags') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_tags&task=tags.getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_categories')) - { - $tmp = [ - 'image' => 'icon-folder-open', - 'link' => Route::_('index.php?option=com_categories&view=categories&extension=com_content'), - 'linkadd' => Route::_('index.php?option=com_categories&task=category.add'), - 'name' => 'MOD_QUICKICON_CATEGORY_MANAGER', - 'access' => array('core.manage', 'com_categories', 'core.create', 'com_categories'), - 'group' => 'MOD_QUICKICON_SITE', - ]; - - if ($params->get('show_categories') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_categories&task=categories.getQuickiconContent&extension=content&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_media')) - { - $this->buttons[$key][] = [ - 'image' => 'icon-images', - 'link' => Route::_('index.php?option=com_media'), - 'name' => 'MOD_QUICKICON_MEDIA_MANAGER', - 'access' => array('core.manage', 'com_media'), - 'group' => 'MOD_QUICKICON_SITE', - ]; - } - - if ($params->get('show_modules')) - { - $tmp = [ - 'image' => 'icon-cube', - 'link' => Route::_('index.php?option=com_modules&view=modules&client_id=0'), - 'linkadd' => Route::_('index.php?option=com_modules&view=select&client_id=0'), - 'name' => 'MOD_QUICKICON_MODULE_MANAGER', - 'access' => array('core.manage', 'com_modules'), - 'group' => 'MOD_QUICKICON_SITE' - ]; - - if ($params->get('show_modules') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_modules&task=modules.getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_plugins')) - { - $tmp = [ - 'image' => 'icon-plug', - 'link' => Route::_('index.php?option=com_plugins'), - 'name' => 'MOD_QUICKICON_PLUGIN_MANAGER', - 'access' => array('core.manage', 'com_plugins'), - 'group' => 'MOD_QUICKICON_SITE' - ]; - - if ($params->get('show_plugins') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_plugins&task=plugins.getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_template_styles')) - { - $this->buttons[$key][] = [ - 'image' => 'icon-paint-brush', - 'link' => Route::_('index.php?option=com_templates&view=styles&client_id=0'), - 'name' => 'MOD_QUICKICON_TEMPLATE_STYLES', - 'access' => array('core.admin', 'com_templates'), - 'group' => 'MOD_QUICKICON_SITE' - ]; - } - - if ($params->get('show_template_code')) - { - $this->buttons[$key][] = [ - 'image' => 'icon-code', - 'link' => Route::_('index.php?option=com_templates&view=templates&client_id=0'), - 'name' => 'MOD_QUICKICON_TEMPLATE_CODE', - 'access' => array('core.admin', 'com_templates'), - 'group' => 'MOD_QUICKICON_SITE' - ]; - } - - if ($params->get('show_checkin')) - { - $tmp = [ - 'image' => 'icon-unlock-alt', - 'link' => Route::_('index.php?option=com_checkin'), - 'name' => 'MOD_QUICKICON_CHECKINS', - 'access' => array('core.admin', 'com_checkin'), - 'group' => 'MOD_QUICKICON_SYSTEM' - ]; - - if ($params->get('show_checkin') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_checkin&task=getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_cache')) - { - $tmp = [ - 'image' => 'icon-cloud', - 'link' => Route::_('index.php?option=com_cache'), - 'name' => 'MOD_QUICKICON_CACHE', - 'access' => array('core.admin', 'com_cache'), - 'group' => 'MOD_QUICKICON_SYSTEM' - ]; - - if ($params->get('show_cache') == 2) - { - $tmp['ajaxurl'] = 'index.php?option=com_cache&task=display.getQuickiconContent&format=json'; - } - - $this->buttons[$key][] = $tmp; - } - - if ($params->get('show_global')) - { - $this->buttons[$key][] = [ - 'image' => 'icon-cog', - 'link' => Route::_('index.php?option=com_config'), - 'name' => 'MOD_QUICKICON_GLOBAL_CONFIGURATION', - 'access' => array('core.manage', 'com_config', 'core.admin', 'com_config'), - 'group' => 'MOD_QUICKICON_SYSTEM', - ]; - } - - PluginHelper::importPlugin('quickicon'); - - $arrays = (array) $application->triggerEvent( - 'onGetIcons', - new QuickIconsEvent('onGetIcons', ['context' => $context]) - ); - - foreach ($arrays as $response) - { - if (!\is_array($response)) - { - continue; - } - - foreach ($response as $icon) - { - $default = array( - 'link' => null, - 'image' => null, - 'text' => null, - 'name' => null, - 'linkadd' => null, - 'access' => true, - 'class' => null, - 'group' => 'MOD_QUICKICON', - ); - - $icon = array_merge($default, $icon); - - if (!\is_null($icon['link']) && (!\is_null($icon['text']) || !\is_null($icon['name']))) - { - $this->buttons[$key][] = $icon; - } - } - } - } - - return $this->buttons[$key]; - } + /** + * Stack to hold buttons + * + * @var array[] + * @since 1.6 + */ + protected $buttons = array(); + + /** + * Helper method to return button list. + * + * This method returns the array by reference so it can be + * used to add custom buttons or remove default ones. + * + * @param Registry $params The module parameters + * @param CMSApplication $application The application + * + * @return array An array of buttons + * + * @since 1.6 + */ + public function getButtons(Registry $params, CMSApplication $application = null) + { + if ($application == null) { + $application = Factory::getApplication(); + } + + $key = (string) $params; + $context = (string) $params->get('context', 'mod_quickicon'); + + if (!isset($this->buttons[$key])) { + // Load mod_quickicon language file in case this method is called before rendering the module + $application->getLanguage()->load('mod_quickicon'); + + $this->buttons[$key] = []; + + if ($params->get('show_users')) { + $tmp = [ + 'image' => 'icon-users', + 'link' => Route::_('index.php?option=com_users&view=users'), + 'linkadd' => Route::_('index.php?option=com_users&task=user.add'), + 'name' => 'MOD_QUICKICON_USER_MANAGER', + 'access' => array('core.manage', 'com_users', 'core.create', 'com_users'), + 'group' => 'MOD_QUICKICON_SITE', + ]; + + if ($params->get('show_users') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_users&task=users.getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_menuitems')) { + $tmp = [ + 'image' => 'icon-list', + 'link' => Route::_('index.php?option=com_menus&view=items&menutype='), + 'linkadd' => Route::_('index.php?option=com_menus&task=item.add'), + 'name' => 'MOD_QUICKICON_MENUITEMS_MANAGER', + 'access' => array('core.manage', 'com_menus', 'core.create', 'com_menus'), + 'group' => 'MOD_QUICKICON_STRUCTURE', + ]; + + if ($params->get('show_menuitems') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_menus&task=items.getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_articles')) { + $tmp = [ + 'image' => 'icon-file-alt', + 'link' => Route::_('index.php?option=com_content&view=articles'), + 'linkadd' => Route::_('index.php?option=com_content&task=article.add'), + 'name' => 'MOD_QUICKICON_ARTICLE_MANAGER', + 'access' => array('core.manage', 'com_content', 'core.create', 'com_content'), + 'group' => 'MOD_QUICKICON_SITE', + ]; + + if ($params->get('show_articles') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_content&task=articles.getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_tags')) { + $tmp = [ + 'image' => 'icon-tag', + 'link' => Route::_('index.php?option=com_tags&view=tags'), + 'linkadd' => Route::_('index.php?option=com_tags&task=tag.edit'), + 'name' => 'MOD_QUICKICON_TAGS_MANAGER', + 'access' => array('core.manage', 'com_tags', 'core.create', 'com_tags'), + 'group' => 'MOD_QUICKICON_SITE', + ]; + + if ($params->get('show_tags') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_tags&task=tags.getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_categories')) { + $tmp = [ + 'image' => 'icon-folder-open', + 'link' => Route::_('index.php?option=com_categories&view=categories&extension=com_content'), + 'linkadd' => Route::_('index.php?option=com_categories&task=category.add'), + 'name' => 'MOD_QUICKICON_CATEGORY_MANAGER', + 'access' => array('core.manage', 'com_categories', 'core.create', 'com_categories'), + 'group' => 'MOD_QUICKICON_SITE', + ]; + + if ($params->get('show_categories') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_categories&task=categories.getQuickiconContent&extension=content&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_media')) { + $this->buttons[$key][] = [ + 'image' => 'icon-images', + 'link' => Route::_('index.php?option=com_media'), + 'name' => 'MOD_QUICKICON_MEDIA_MANAGER', + 'access' => array('core.manage', 'com_media'), + 'group' => 'MOD_QUICKICON_SITE', + ]; + } + + if ($params->get('show_modules')) { + $tmp = [ + 'image' => 'icon-cube', + 'link' => Route::_('index.php?option=com_modules&view=modules&client_id=0'), + 'linkadd' => Route::_('index.php?option=com_modules&view=select&client_id=0'), + 'name' => 'MOD_QUICKICON_MODULE_MANAGER', + 'access' => array('core.manage', 'com_modules'), + 'group' => 'MOD_QUICKICON_SITE' + ]; + + if ($params->get('show_modules') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_modules&task=modules.getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_plugins')) { + $tmp = [ + 'image' => 'icon-plug', + 'link' => Route::_('index.php?option=com_plugins'), + 'name' => 'MOD_QUICKICON_PLUGIN_MANAGER', + 'access' => array('core.manage', 'com_plugins'), + 'group' => 'MOD_QUICKICON_SITE' + ]; + + if ($params->get('show_plugins') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_plugins&task=plugins.getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_template_styles')) { + $this->buttons[$key][] = [ + 'image' => 'icon-paint-brush', + 'link' => Route::_('index.php?option=com_templates&view=styles&client_id=0'), + 'name' => 'MOD_QUICKICON_TEMPLATE_STYLES', + 'access' => array('core.admin', 'com_templates'), + 'group' => 'MOD_QUICKICON_SITE' + ]; + } + + if ($params->get('show_template_code')) { + $this->buttons[$key][] = [ + 'image' => 'icon-code', + 'link' => Route::_('index.php?option=com_templates&view=templates&client_id=0'), + 'name' => 'MOD_QUICKICON_TEMPLATE_CODE', + 'access' => array('core.admin', 'com_templates'), + 'group' => 'MOD_QUICKICON_SITE' + ]; + } + + if ($params->get('show_checkin')) { + $tmp = [ + 'image' => 'icon-unlock-alt', + 'link' => Route::_('index.php?option=com_checkin'), + 'name' => 'MOD_QUICKICON_CHECKINS', + 'access' => array('core.admin', 'com_checkin'), + 'group' => 'MOD_QUICKICON_SYSTEM' + ]; + + if ($params->get('show_checkin') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_checkin&task=getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_cache')) { + $tmp = [ + 'image' => 'icon-cloud', + 'link' => Route::_('index.php?option=com_cache'), + 'name' => 'MOD_QUICKICON_CACHE', + 'access' => array('core.admin', 'com_cache'), + 'group' => 'MOD_QUICKICON_SYSTEM' + ]; + + if ($params->get('show_cache') == 2) { + $tmp['ajaxurl'] = 'index.php?option=com_cache&task=display.getQuickiconContent&format=json'; + } + + $this->buttons[$key][] = $tmp; + } + + if ($params->get('show_global')) { + $this->buttons[$key][] = [ + 'image' => 'icon-cog', + 'link' => Route::_('index.php?option=com_config'), + 'name' => 'MOD_QUICKICON_GLOBAL_CONFIGURATION', + 'access' => array('core.manage', 'com_config', 'core.admin', 'com_config'), + 'group' => 'MOD_QUICKICON_SYSTEM', + ]; + } + + PluginHelper::importPlugin('quickicon'); + + $arrays = (array) $application->triggerEvent( + 'onGetIcons', + new QuickIconsEvent('onGetIcons', ['context' => $context]) + ); + + foreach ($arrays as $response) { + if (!\is_array($response)) { + continue; + } + + foreach ($response as $icon) { + $default = array( + 'link' => null, + 'image' => null, + 'text' => null, + 'name' => null, + 'linkadd' => null, + 'access' => true, + 'class' => null, + 'group' => 'MOD_QUICKICON', + ); + + $icon = array_merge($default, $icon); + + if (!\is_null($icon['link']) && (!\is_null($icon['text']) || !\is_null($icon['name']))) { + $this->buttons[$key][] = $icon; + } + } + } + } + + return $this->buttons[$key]; + } } diff --git a/administrator/modules/mod_quickicon/tmpl/default.php b/administrator/modules/mod_quickicon/tmpl/default.php index effa3b2e0d9a1..058db0db87e4d 100644 --- a/administrator/modules/mod_quickicon/tmpl/default.php +++ b/administrator/modules/mod_quickicon/tmpl/default.php @@ -1,4 +1,5 @@ - + diff --git a/administrator/modules/mod_sampledata/mod_sampledata.php b/administrator/modules/mod_sampledata/mod_sampledata.php index 7feac851a7841..79ffb01326e69 100644 --- a/administrator/modules/mod_sampledata/mod_sampledata.php +++ b/administrator/modules/mod_sampledata/mod_sampledata.php @@ -1,4 +1,5 @@ getDispatcher() - ->dispatch( - 'onSampledataGetOverview', - AbstractEvent::create( - 'onSampledataGetOverview', - [ - 'subject' => new \stdClass, - ] - ) - ) - ->getArgument('result') ?? []; - } + return Factory::getApplication() + ->getDispatcher() + ->dispatch( + 'onSampledataGetOverview', + AbstractEvent::create( + 'onSampledataGetOverview', + [ + 'subject' => new \stdClass(), + ] + ) + ) + ->getArgument('result') ?? []; + } } diff --git a/administrator/modules/mod_sampledata/tmpl/default.php b/administrator/modules/mod_sampledata/tmpl/default.php index 6effa8da33976..f11599975f1b0 100644 --- a/administrator/modules/mod_sampledata/tmpl/default.php +++ b/administrator/modules/mod_sampledata/tmpl/default.php @@ -1,4 +1,5 @@ getDocument()->getWebAssetManager() - ->registerAndUseScript('mod_sampledata', 'mod_sampledata/sampledata-process.js', [], ['type' => 'module'], ['core']); + ->registerAndUseScript('mod_sampledata', 'mod_sampledata/sampledata-process.js', [], ['type' => 'module'], ['core']); Text::script('MOD_SAMPLEDATA_COMPLETED'); Text::script('MOD_SAMPLEDATA_CONFIRM_START'); @@ -21,37 +22,37 @@ Text::script('MOD_SAMPLEDATA_INVALID_RESPONSE'); $app->getDocument()->addScriptOptions( - 'sample-data', - [ - 'icon' => Uri::root(true) . '/media/system/images/ajax-loader.gif', - ] + 'sample-data', + [ + 'icon' => Uri::root(true) . '/media/system/images/ajax-loader.gif', + ] ); ?> -
      - $item) : ?> -
    • -
      -
      - - title, ENT_QUOTES, 'UTF-8'); ?> -
      - -
      -

      description; ?>

      -
    • - -
    • -
      -
      -
      -
    • - -
    - - - +
      + $item) : ?> +
    • +
      +
      + + title, ENT_QUOTES, 'UTF-8'); ?> +
      + +
      +

      description; ?>

      +
    • + +
    • +
      +
      +
      +
    • + +
    + + + diff --git a/administrator/modules/mod_stats_admin/mod_stats_admin.php b/administrator/modules/mod_stats_admin/mod_stats_admin.php index 1e23913e04cb4..49e9cb3f8c2d1 100644 --- a/administrator/modules/mod_stats_admin/mod_stats_admin.php +++ b/administrator/modules/mod_stats_admin/mod_stats_admin.php @@ -1,4 +1,5 @@ getIdentity(); - - $rows = array(); - $query = $db->getQuery(true); - - $serverinfo = $params->get('serverinfo', 0); - $siteinfo = $params->get('siteinfo', 0); - - $i = 0; - - if ($serverinfo) - { - $rows[$i] = new \stdClass; - $rows[$i]->title = Text::_('MOD_STATS_PHP'); - $rows[$i]->icon = 'cogs'; - $rows[$i]->data = PHP_VERSION; - $i++; - - $rows[$i] = new \stdClass; - $rows[$i]->title = Text::_($db->name); - $rows[$i]->icon = 'database'; - $rows[$i]->data = $db->getVersion(); - $i++; - - $rows[$i] = new \stdClass; - $rows[$i]->title = Text::_('MOD_STATS_CACHING'); - $rows[$i]->icon = 'tachometer-alt'; - $rows[$i]->data = $app->get('caching') ? Text::_('JENABLED') : Text::_('JDISABLED'); - $i++; - - $rows[$i] = new \stdClass; - $rows[$i]->title = Text::_('MOD_STATS_GZIP'); - $rows[$i]->icon = 'bolt'; - $rows[$i]->data = $app->get('gzip') ? Text::_('JENABLED') : Text::_('JDISABLED'); - $i++; - } - - if ($siteinfo) - { - $query->select('COUNT(id) AS count_users') - ->from('#__users'); - $db->setQuery($query); - - try - { - $users = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $users = false; - } - - $query->clear() - ->select('COUNT(id) AS count_items') - ->from('#__content') - ->where('state = 1'); - $db->setQuery($query); - - try - { - $items = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $items = false; - } - - if ($users) - { - $rows[$i] = new \stdClass; - $rows[$i]->title = Text::_('MOD_STATS_USERS'); - $rows[$i]->icon = 'users'; - $rows[$i]->data = $users; - - if ($user->authorise('core.manage', 'com_users')) - { - $rows[$i]->link = Route::_('index.php?option=com_users'); - } - - $i++; - } - - if ($items) - { - $rows[$i] = new \stdClass; - $rows[$i]->title = Text::_('MOD_STATS_ARTICLES'); - $rows[$i]->icon = 'file'; - $rows[$i]->data = $items; - $rows[$i]->link = Route::_('index.php?option=com_content&view=articles&filter[published]=1'); - $i++; - } - } - - // Include additional data defined by published system plugins - PluginHelper::importPlugin('system'); - - $arrays = (array) $app->triggerEvent('onGetStats', array('mod_stats_admin')); - - foreach ($arrays as $response) - { - foreach ($response as $row) - { - // We only add a row if the title and data are given - if (isset($row['title']) && isset($row['data'])) - { - $rows[$i] = new \stdClass; - $rows[$i]->title = $row['title']; - $rows[$i]->icon = $row['icon'] ?? 'info'; - $rows[$i]->data = $row['data']; - $rows[$i]->link = isset($row['link']) ? $row['link'] : null; - $i++; - } - } - } - - return $rows; - } + /** + * Method to retrieve information about the site + * + * @param Registry $params The module parameters + * @param CMSApplication $app The application + * @param DatabaseInterface $db The database + * + * @return array Array containing site information + * + * @since 3.0 + */ + public static function getStats(Registry $params, CMSApplication $app, DatabaseInterface $db) + { + $user = $app->getIdentity(); + + $rows = array(); + $query = $db->getQuery(true); + + $serverinfo = $params->get('serverinfo', 0); + $siteinfo = $params->get('siteinfo', 0); + + $i = 0; + + if ($serverinfo) { + $rows[$i] = new \stdClass(); + $rows[$i]->title = Text::_('MOD_STATS_PHP'); + $rows[$i]->icon = 'cogs'; + $rows[$i]->data = PHP_VERSION; + $i++; + + $rows[$i] = new \stdClass(); + $rows[$i]->title = Text::_($db->name); + $rows[$i]->icon = 'database'; + $rows[$i]->data = $db->getVersion(); + $i++; + + $rows[$i] = new \stdClass(); + $rows[$i]->title = Text::_('MOD_STATS_CACHING'); + $rows[$i]->icon = 'tachometer-alt'; + $rows[$i]->data = $app->get('caching') ? Text::_('JENABLED') : Text::_('JDISABLED'); + $i++; + + $rows[$i] = new \stdClass(); + $rows[$i]->title = Text::_('MOD_STATS_GZIP'); + $rows[$i]->icon = 'bolt'; + $rows[$i]->data = $app->get('gzip') ? Text::_('JENABLED') : Text::_('JDISABLED'); + $i++; + } + + if ($siteinfo) { + $query->select('COUNT(id) AS count_users') + ->from('#__users'); + $db->setQuery($query); + + try { + $users = $db->loadResult(); + } catch (\RuntimeException $e) { + $users = false; + } + + $query->clear() + ->select('COUNT(id) AS count_items') + ->from('#__content') + ->where('state = 1'); + $db->setQuery($query); + + try { + $items = $db->loadResult(); + } catch (\RuntimeException $e) { + $items = false; + } + + if ($users) { + $rows[$i] = new \stdClass(); + $rows[$i]->title = Text::_('MOD_STATS_USERS'); + $rows[$i]->icon = 'users'; + $rows[$i]->data = $users; + + if ($user->authorise('core.manage', 'com_users')) { + $rows[$i]->link = Route::_('index.php?option=com_users'); + } + + $i++; + } + + if ($items) { + $rows[$i] = new \stdClass(); + $rows[$i]->title = Text::_('MOD_STATS_ARTICLES'); + $rows[$i]->icon = 'file'; + $rows[$i]->data = $items; + $rows[$i]->link = Route::_('index.php?option=com_content&view=articles&filter[published]=1'); + $i++; + } + } + + // Include additional data defined by published system plugins + PluginHelper::importPlugin('system'); + + $arrays = (array) $app->triggerEvent('onGetStats', array('mod_stats_admin')); + + foreach ($arrays as $response) { + foreach ($response as $row) { + // We only add a row if the title and data are given + if (isset($row['title']) && isset($row['data'])) { + $rows[$i] = new \stdClass(); + $rows[$i]->title = $row['title']; + $rows[$i]->icon = $row['icon'] ?? 'info'; + $rows[$i]->data = $row['data']; + $rows[$i]->link = isset($row['link']) ? $row['link'] : null; + $i++; + } + } + } + + return $rows; + } } diff --git a/administrator/modules/mod_stats_admin/tmpl/default.php b/administrator/modules/mod_stats_admin/tmpl/default.php index 732c2fcc4f76b..c79e483e02dc9 100644 --- a/administrator/modules/mod_stats_admin/tmpl/default.php +++ b/administrator/modules/mod_stats_admin/tmpl/default.php @@ -1,4 +1,5 @@
      - -
    • - title; ?> + +
    • + title; ?> - link)) : ?> - data; ?> - - data; ?> - -
    • - + link)) : ?> + data; ?> + + data; ?> + + +
    diff --git a/administrator/modules/mod_submenu/mod_submenu.php b/administrator/modules/mod_submenu/mod_submenu.php index 0fd9eb8720fd7..86cea5ee2aefa 100644 --- a/administrator/modules/mod_submenu/mod_submenu.php +++ b/administrator/modules/mod_submenu/mod_submenu.php @@ -1,4 +1,5 @@ get('menutype', '*'); $root = false; -if ($menutype === '*') -{ - $name = $params->get('preset', 'system'); - $root = MenusHelper::loadPreset($name); -} -else -{ - $root = MenusHelper::getMenuItems($menutype, true); +if ($menutype === '*') { + $name = $params->get('preset', 'system'); + $root = MenusHelper::loadPreset($name); +} else { + $root = MenusHelper::getMenuItems($menutype, true); } -if ($root && $root->hasChildren()) -{ - Factory::getLanguage()->load( - 'mod_menu', - JPATH_ADMINISTRATOR, - Factory::getLanguage()->getTag(), - true - ); +if ($root && $root->hasChildren()) { + Factory::getLanguage()->load( + 'mod_menu', + JPATH_ADMINISTRATOR, + Factory::getLanguage()->getTag(), + true + ); - Menu::preprocess($root); + Menu::preprocess($root); - // Render the module layout - require ModuleHelper::getLayoutPath('mod_submenu', $params->get('layout', 'default')); + // Render the module layout + require ModuleHelper::getLayoutPath('mod_submenu', $params->get('layout', 'default')); } diff --git a/administrator/modules/mod_submenu/src/Menu/Menu.php b/administrator/modules/mod_submenu/src/Menu/Menu.php index 61f7a50f6e09a..c02603b9a236b 100644 --- a/administrator/modules/mod_submenu/src/Menu/Menu.php +++ b/administrator/modules/mod_submenu/src/Menu/Menu.php @@ -1,4 +1,5 @@ getIdentity(); - $children = $parent->getChildren(); - $language = Factory::getLanguage(); - - /** - * Trigger onPreprocessMenuItems for the current level of backend menu items. - * $children is an array of MenuItem objects. A plugin can traverse the whole tree, - * but new nodes will only be run through this method if their parents have not been processed yet. - */ - $app->triggerEvent('onPreprocessMenuItems', array('administrator.module.mod_submenu', $children)); - - foreach ($children as $item) - { - if (substr($item->link, 0, 8) === 'special:') - { - $special = substr($item->link, 8); - - if ($special === 'language-forum') - { - $item->link = 'index.php?option=com_admin&view=help&layout=langforum'; - } - } - - $uri = new Uri($item->link); - $query = $uri->getQuery(true); - - /** - * This is needed to populate the element property when the component is no longer - * installed but its core menu items are left behind. - */ - if ($option = $uri->getVar('option')) - { - $item->element = $option; - } - - // Exclude item if is not enabled - if ($item->element && !ComponentHelper::isEnabled($item->element)) - { - $parent->removeChild($item); - continue; - } - - /* - * Multilingual Associations if the site is not set as multilingual and/or Associations is not enabled in - * the Language Filter plugin - */ - - if ($item->element === 'com_associations' && !Associations::isEnabled()) - { - $parent->removeChild($item); - continue; - } - - $itemParams = $item->getParams(); - - // Exclude item with menu item option set to exclude from menu modules - if ($itemParams->get('menu-permission')) - { - @list($action, $asset) = explode(';', $itemParams->get('menu-permission')); - - if (!$user->authorise($action, $asset)) - { - $parent->removeChild($item); - continue; - } - } - - // Populate automatic children for container items - if ($item->type === 'container') - { - $exclude = (array) $itemParams->get('hideitems') ?: array(); - $components = MenusHelper::getMenuItems('main', false, $exclude); - - // We are adding the nodes first to preprocess them, then sort them and add them again. - foreach ($components->getChildren() as $c) - { - if (!$c->hasChildren()) - { - $temp = clone $c; - $c->addChild($temp); - } - - $item->addChild($c); - } - - self::preprocess($item); - $children = ArrayHelper::sortObjects($item->getChildren(), 'text', 1, false, true); - - foreach ($children as $c) - { - $parent->addChild($c); - } - - $parent->removeChild($item); - continue; - } - - // Exclude Mass Mail if disabled in global configuration - if ($item->scope === 'massmail' && ($app->get('massmailoff', 0) == 1)) - { - $parent->removeChild($item); - continue; - } - - if ($item->element === 'com_fields') - { - parse_str($item->link, $query); - - // Only display Fields menus when enabled in the component - $createFields = null; - - if (isset($query['context'])) - { - $createFields = ComponentHelper::getParams(strstr($query['context'], '.', true))->get('custom_fields_enable', 1); - } - - if (!$createFields || !$user->authorise('core.manage', 'com_users')) - { - $parent->removeChild($item); - continue; - } - } - elseif ($item->element === 'com_workflow') - { - parse_str($item->link, $query); - - // Only display Workflow menus when enabled in the component - $workflow = null; - - if (isset($query['extension'])) - { - $parts = explode('.', $query['extension']); - - $workflow = ComponentHelper::getParams($parts[0])->get('workflow_enabled'); - } - - if (!$workflow) - { - $parent->removeChild($item); - continue; - } - - [$assetName] = isset($query['extension']) ? explode('.', $query['extension'], 2) : array('com_workflow'); - } - // Special case for components which only allow super user access - elseif (\in_array($item->element, array('com_config', 'com_privacy', 'com_actionlogs'), true) && !$user->authorise('core.admin')) - { - $parent->removeChild($item); - continue; - } - elseif ($item->element === 'com_joomlaupdate' && !$user->authorise('core.admin')) - { - $parent->removeChild($item); - continue; - } - elseif (($item->link === 'index.php?option=com_installer&view=install' || $item->link === 'index.php?option=com_installer&view=languages') - && !$user->authorise('core.admin')) - { - continue; - } - elseif ($item->element === 'com_admin') - { - parse_str($item->link, $query); - - if (isset($query['view']) && $query['view'] === 'sysinfo' && !$user->authorise('core.admin')) - { - $parent->removeChild($item); - continue; - } - } - elseif ($item->element && !$user->authorise(($item->scope === 'edit') ? 'core.create' : 'core.manage', $item->element)) - { - $parent->removeChild($item); - continue; - } - elseif ($item->element === 'com_menus') - { - // Get badges for Menus containing a Home page. - $iconImage = $item->icon; - - if ($iconImage) - { - if (substr($iconImage, 0, 6) === 'class:' && substr($iconImage, 6) === 'icon-home') - { - $iconImage = ''; - $iconImage .= '' . Text::_('JDEFAULT') . ''; - } - elseif (substr($iconImage, 0, 6) === 'image:') - { - $iconImage = ' ' . substr($iconImage, 6) . ''; - } - - $item->iconImage = $iconImage; - } - } - - if ($item->hasChildren()) - { - self::preprocess($item); - } - - // Ok we passed everything, load language at last only - if ($item->element) - { - $language->load($item->element . '.sys', JPATH_ADMINISTRATOR) || - $language->load($item->element . '.sys', JPATH_ADMINISTRATOR . '/components/' . $item->element); - } - - if ($item->type === 'separator' && $item->getParams()->get('text_separator') == 0) - { - $item->title = ''; - } - - $item->text = Text::_($item->title); - } - } + /** + * Filter and perform other preparatory tasks for loaded menu items based on access rights and module configurations for display + * + * @param MenuItem $parent A menu item to process + * + * @return void + * + * @since 4.0.0 + */ + public static function preprocess($parent) + { + $app = Factory::getApplication(); + $user = $app->getIdentity(); + $children = $parent->getChildren(); + $language = Factory::getLanguage(); + + /** + * Trigger onPreprocessMenuItems for the current level of backend menu items. + * $children is an array of MenuItem objects. A plugin can traverse the whole tree, + * but new nodes will only be run through this method if their parents have not been processed yet. + */ + $app->triggerEvent('onPreprocessMenuItems', array('administrator.module.mod_submenu', $children)); + + foreach ($children as $item) { + if (substr($item->link, 0, 8) === 'special:') { + $special = substr($item->link, 8); + + if ($special === 'language-forum') { + $item->link = 'index.php?option=com_admin&view=help&layout=langforum'; + } + } + + $uri = new Uri($item->link); + $query = $uri->getQuery(true); + + /** + * This is needed to populate the element property when the component is no longer + * installed but its core menu items are left behind. + */ + if ($option = $uri->getVar('option')) { + $item->element = $option; + } + + // Exclude item if is not enabled + if ($item->element && !ComponentHelper::isEnabled($item->element)) { + $parent->removeChild($item); + continue; + } + + /* + * Multilingual Associations if the site is not set as multilingual and/or Associations is not enabled in + * the Language Filter plugin + */ + + if ($item->element === 'com_associations' && !Associations::isEnabled()) { + $parent->removeChild($item); + continue; + } + + $itemParams = $item->getParams(); + + // Exclude item with menu item option set to exclude from menu modules + if ($itemParams->get('menu-permission')) { + @list($action, $asset) = explode(';', $itemParams->get('menu-permission')); + + if (!$user->authorise($action, $asset)) { + $parent->removeChild($item); + continue; + } + } + + // Populate automatic children for container items + if ($item->type === 'container') { + $exclude = (array) $itemParams->get('hideitems') ?: array(); + $components = MenusHelper::getMenuItems('main', false, $exclude); + + // We are adding the nodes first to preprocess them, then sort them and add them again. + foreach ($components->getChildren() as $c) { + if (!$c->hasChildren()) { + $temp = clone $c; + $c->addChild($temp); + } + + $item->addChild($c); + } + + self::preprocess($item); + $children = ArrayHelper::sortObjects($item->getChildren(), 'text', 1, false, true); + + foreach ($children as $c) { + $parent->addChild($c); + } + + $parent->removeChild($item); + continue; + } + + // Exclude Mass Mail if disabled in global configuration + if ($item->scope === 'massmail' && ($app->get('massmailoff', 0) == 1)) { + $parent->removeChild($item); + continue; + } + + if ($item->element === 'com_fields') { + parse_str($item->link, $query); + + // Only display Fields menus when enabled in the component + $createFields = null; + + if (isset($query['context'])) { + $createFields = ComponentHelper::getParams(strstr($query['context'], '.', true))->get('custom_fields_enable', 1); + } + + if (!$createFields || !$user->authorise('core.manage', 'com_users')) { + $parent->removeChild($item); + continue; + } + } elseif ($item->element === 'com_workflow') { + parse_str($item->link, $query); + + // Only display Workflow menus when enabled in the component + $workflow = null; + + if (isset($query['extension'])) { + $parts = explode('.', $query['extension']); + + $workflow = ComponentHelper::getParams($parts[0])->get('workflow_enabled'); + } + + if (!$workflow) { + $parent->removeChild($item); + continue; + } + + [$assetName] = isset($query['extension']) ? explode('.', $query['extension'], 2) : array('com_workflow'); + } elseif (\in_array($item->element, array('com_config', 'com_privacy', 'com_actionlogs'), true) && !$user->authorise('core.admin')) { + // Special case for components which only allow super user access + $parent->removeChild($item); + continue; + } elseif ($item->element === 'com_joomlaupdate' && !$user->authorise('core.admin')) { + $parent->removeChild($item); + continue; + } elseif ( + ($item->link === 'index.php?option=com_installer&view=install' || $item->link === 'index.php?option=com_installer&view=languages') + && !$user->authorise('core.admin') + ) { + continue; + } elseif ($item->element === 'com_admin') { + parse_str($item->link, $query); + + if (isset($query['view']) && $query['view'] === 'sysinfo' && !$user->authorise('core.admin')) { + $parent->removeChild($item); + continue; + } + } elseif ($item->element && !$user->authorise(($item->scope === 'edit') ? 'core.create' : 'core.manage', $item->element)) { + $parent->removeChild($item); + continue; + } elseif ($item->element === 'com_menus') { + // Get badges for Menus containing a Home page. + $iconImage = $item->icon; + + if ($iconImage) { + if (substr($iconImage, 0, 6) === 'class:' && substr($iconImage, 6) === 'icon-home') { + $iconImage = ''; + $iconImage .= '' . Text::_('JDEFAULT') . ''; + } elseif (substr($iconImage, 0, 6) === 'image:') { + $iconImage = ' ' . substr($iconImage, 6) . ''; + } + + $item->iconImage = $iconImage; + } + } + + if ($item->hasChildren()) { + self::preprocess($item); + } + + // Ok we passed everything, load language at last only + if ($item->element) { + $language->load($item->element . '.sys', JPATH_ADMINISTRATOR) || + $language->load($item->element . '.sys', JPATH_ADMINISTRATOR . '/components/' . $item->element); + } + + if ($item->type === 'separator' && $item->getParams()->get('text_separator') == 0) { + $item->title = ''; + } + + $item->text = Text::_($item->title); + } + } } diff --git a/administrator/modules/mod_submenu/tmpl/default.php b/administrator/modules/mod_submenu/tmpl/default.php index 1bf99857c4f58..d79089e894e6f 100644 --- a/administrator/modules/mod_submenu/tmpl/default.php +++ b/administrator/modules/mod_submenu/tmpl/default.php @@ -1,4 +1,5 @@ getChildren() as $child) : ?> - hasChildren()) : ?> -
    -
    - img = $child->img ?? ''; + hasChildren()) : ?> +
    +
    + img = $child->img ?? ''; - if (substr($child->img, 0, 6) === 'class:') - { - $iconImage = ''; - } - elseif (substr($child->img, 0, 6) === 'image:') - { - $iconImage = ''; - } - elseif (!empty($child->img)) - { - $iconImage = ''; - } - elseif ($child->icon) - { - $iconImage = ''; - } - else - { - $iconImage = ''; - } - ?> -

    - - title); ?> -

    -
    -
    - + $sronly = Text::_($item->title) . ' - ' . $title; + ?> + + + + + + + + dashboard) : ?> + + + + + + + + + + +
    +
    + diff --git a/administrator/modules/mod_title/mod_title.php b/administrator/modules/mod_title/mod_title.php index edcbc9bfc2b77..3e1340ffba5f8 100644 --- a/administrator/modules/mod_title/mod_title.php +++ b/administrator/modules/mod_title/mod_title.php @@ -1,4 +1,5 @@ JComponentTitle)) -{ - $title = $app->JComponentTitle; +if (isset($app->JComponentTitle)) { + $title = $app->JComponentTitle; } require ModuleHelper::getLayoutPath('mod_title', $params->get('layout', 'default')); diff --git a/administrator/modules/mod_title/tmpl/default.php b/administrator/modules/mod_title/tmpl/default.php index 7db1059559420..0c24a082b2a65 100644 --- a/administrator/modules/mod_title/tmpl/default.php +++ b/administrator/modules/mod_title/tmpl/default.php @@ -1,4 +1,5 @@
    -
    - -
    +
    + +
    diff --git a/administrator/modules/mod_toolbar/mod_toolbar.php b/administrator/modules/mod_toolbar/mod_toolbar.php index 9b7fabde069a3..9466a9efd76b3 100644 --- a/administrator/modules/mod_toolbar/mod_toolbar.php +++ b/administrator/modules/mod_toolbar/mod_toolbar.php @@ -1,4 +1,5 @@ input->getBool('hidemainmenu'); -if ($hideLinks) -{ - return; +if ($hideLinks) { + return; } // Load the Bootstrap Dropdown HTMLHelper::_('bootstrap.dropdown', '.dropdown-toggle'); ?> diff --git a/administrator/modules/mod_version/mod_version.php b/administrator/modules/mod_version/mod_version.php index 5ce96037c52a8..c94b138b69e15 100644 --- a/administrator/modules/mod_version/mod_version.php +++ b/administrator/modules/mod_version/mod_version.php @@ -1,4 +1,5 @@ getShortVersion(); - } + return '‎' . $version->getShortVersion(); + } } diff --git a/administrator/modules/mod_version/tmpl/default.php b/administrator/modules/mod_version/tmpl/default.php index 5e77f801b56f0..fa0f143a44841 100644 --- a/administrator/modules/mod_version/tmpl/default.php +++ b/administrator/modules/mod_version/tmpl/default.php @@ -1,4 +1,5 @@
    - +
    diff --git a/administrator/templates/atum/component.php b/administrator/templates/atum/component.php index 2913982b05b50..0d52a6bed1064 100644 --- a/administrator/templates/atum/component.php +++ b/administrator/templates/atum/component.php @@ -1,4 +1,5 @@ usePreset('template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')) - ->useStyle('template.active.language') - ->useStyle('template.user') - ->addInlineStyle(':root { + ->useStyle('template.active.language') + ->useStyle('template.user') + ->addInlineStyle(':root { --hue: ' . $matches[1] . '; --template-bg-light: ' . $this->params->get('bg-light', '--template-bg-light') . '; --template-text-dark: ' . $this->params->get('text-dark', '--template-text-dark') . '; @@ -47,12 +48,12 @@ - - - + + + - - + + diff --git a/administrator/templates/atum/cpanel.php b/administrator/templates/atum/cpanel.php index e75d1c51179dd..8a2b1aafdd2c8 100644 --- a/administrator/templates/atum/cpanel.php +++ b/administrator/templates/atum/cpanel.php @@ -1,4 +1,5 @@ guest) -{ - require __DIR__ . '/error_login.php'; -} -else -{ - require __DIR__ . '/error_full.php'; +if ($user->guest) { + require __DIR__ . '/error_login.php'; +} else { + require __DIR__ . '/error_full.php'; } diff --git a/administrator/templates/atum/error_full.php b/administrator/templates/atum/error_full.php index 6886a175da8ff..0014d08860938 100644 --- a/administrator/templates/atum/error_full.php +++ b/administrator/templates/atum/error_full.php @@ -1,4 +1,5 @@ params->get('logoBrandLarge') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; $logoBrandSmall = $this->params->get('logoBrandSmall') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; $logoBrandLargeAlt = empty($this->params->get('logoBrandLargeAlt')) && empty($this->params->get('emptyLogoBrandLargeAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt'), ENT_COMPAT, 'UTF-8') . '"'; $logoBrandSmallAlt = empty($this->params->get('logoBrandSmallAlt')) && empty($this->params->get('emptyLogoBrandSmallAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt'), ENT_COMPAT, 'UTF-8') . '"'; - // Get the hue value - preg_match('#^hsla?\(([0-9]+)[\D]+([0-9]+)[\D]+([0-9]+)[\D]+([0-9](?:.\d+)?)?\)$#i', $this->params->get('hue', 'hsl(214, 63%, 20%)'), $matches); + // Get the hue value + preg_match('#^hsla?\(([0-9]+)[\D]+([0-9]+)[\D]+([0-9]+)[\D]+([0-9](?:.\d+)?)?\)$#i', $this->params->get('hue', 'hsl(214, 63%, 20%)'), $matches); - // Enable assets - $wa->usePreset('template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')) - ->useStyle('template.active.language') - ->useStyle('template.user') - ->addInlineStyle(':root { + // Enable assets + $wa->usePreset('template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')) + ->useStyle('template.active.language') + ->useStyle('template.user') + ->addInlineStyle(':root { --hue: ' . $matches[1] . '; --template-bg-light: ' . $this->params->get('bg-light', '#f0f4fb') . '; --template-text-dark: ' . $this->params->get('text-dark', '#495057') . '; @@ -66,122 +67,122 @@ }'); // Override 'template.active' asset to set correct ltr/rtl dependency -$wa->registerStyle('template.active', '', [], [], ['template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')]); + $wa->registerStyle('template.active', '', [], [], ['template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')]); // Set some meta data -$this->setMetaData('viewport', 'width=device-width, initial-scale=1'); + $this->setMetaData('viewport', 'width=device-width, initial-scale=1'); -$monochrome = (bool) $this->params->get('monochrome'); + $monochrome = (bool) $this->params->get('monochrome'); // @see administrator/templates/atum/html/layouts/status.php -$statusModules = LayoutHelper::render('status', ['modules' => 'status']); -?> + $statusModules = LayoutHelper::render('status', ['modules' => 'status']); + ?> - - - + + + - - - - -
    -
    - - - -
    -
    -
    -
    - -
    -
    -
    - -
    - - -
    -
    -

    -
    - error->getCode(); ?> - error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> -
    - debug) : ?> -
    - renderBacktrace(); ?> - - error->getPrevious()) : ?> - - _error here and in the loop as setError() assigns errors to this property and we need this for the backtrace to work correctly ?> - - setError($this->_error->getPrevious()); ?> - -

    -

    _error->getMessage(), ENT_QUOTES, 'UTF-8'); ?>

    - renderBacktrace(); ?> - setError($this->_error->getPrevious()); ?> - - - setError($this->error); ?> - -
    - -

    - - - -

    -
    - - countModules('bottom')) : ?> - - -
    -
    -
    - - - - - - -
    - + + + + +
    +
    + + + +
    +
    +
    +
    + +
    +
    +
    + +
    + + +
    +
    +

    +
    + error->getCode(); ?> + error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> +
    + debug) : ?> +
    + renderBacktrace(); ?> + + error->getPrevious()) : ?> + + _error here and in the loop as setError() assigns errors to this property and we need this for the backtrace to work correctly ?> + + setError($this->_error->getPrevious()); ?> + +

    +

    _error->getMessage(), ENT_QUOTES, 'UTF-8'); ?>

    + renderBacktrace(); ?> + setError($this->_error->getPrevious()); ?> + + + setError($this->error); ?> + +
    + +

    + + + +

    +
    + + countModules('bottom')) : ?> + + +
    +
    +
    + + + + + + +
    + diff --git a/administrator/templates/atum/error_login.php b/administrator/templates/atum/error_login.php index 68144ad07aebc..fe6dc0f048681 100644 --- a/administrator/templates/atum/error_login.php +++ b/administrator/templates/atum/error_login.php @@ -1,4 +1,5 @@ params->get('logoBrandLarge') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; $loginLogo = $this->params->get('loginLogo') - ? Uri::root() . $this->params->get('loginLogo') - : Uri::root() . 'media/templates/administrator/atum/images/logos/login.svg'; + ? Uri::root() . $this->params->get('loginLogo') + : Uri::root() . 'media/templates/administrator/atum/images/logos/login.svg'; $logoBrandSmall = $this->params->get('logoBrandSmall') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; $logoBrandLargeAlt = empty($this->params->get('logoBrandLargeAlt')) && empty($this->params->get('emptyLogoBrandLargeAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt'), ENT_COMPAT, 'UTF-8') . '"'; $logoBrandSmallAlt = empty($this->params->get('logoBrandSmallAlt')) && empty($this->params->get('emptyLogoBrandSmallAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt'), ENT_COMPAT, 'UTF-8') . '"'; $loginLogoAlt = empty($this->params->get('loginLogoAlt')) && empty($this->params->get('emptyLoginLogoAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('loginLogoAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('loginLogoAlt'), ENT_COMPAT, 'UTF-8') . '"'; - // Get the hue value + // Get the hue value preg_match('#^hsla?\(([0-9]+)[\D]+([0-9]+)[\D]+([0-9]+)[\D]+([0-9](?:.\d+)?)?\)$#i', $this->params->get('hue', 'hsl(214, 63%, 20%)'), $matches); // Enable assets $wa->usePreset('template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')) - ->useStyle('template.active.language') - ->useStyle('template.user') - ->addInlineStyle(':root { + ->useStyle('template.active.language') + ->useStyle('template.user') + ->addInlineStyle(':root { --hue: ' . $matches[1] . '; --template-bg-light: ' . $this->params->get('bg-light', '#f0f4fb') . '; --template-text-dark: ' . $this->params->get('text-dark', '#495057') . '; @@ -83,83 +84,83 @@ - - - + + + - - - - -
    -
    -
    -
    -
    -
    -
    - > -
    -

    - -
    - error->getCode(); ?> - error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> -
    - debug) : ?> -
    - renderBacktrace(); ?> - - error->getPrevious()) : ?> - - _error here and in the loop as setError() assigns errors to this property and we need this for the backtrace to work correctly ?> - - setError($this->_error->getPrevious()); ?> - -

    -

    _error->getMessage(), ENT_QUOTES, 'UTF-8'); ?>

    - renderBacktrace(); ?> - setError($this->_error->getPrevious()); ?> - - - setError($this->error); ?> - -
    - -
    -
    -
    -
    -
    - - -
    - + + + + +
    +
    +
    +
    +
    +
    +
    + > +
    +

    + +
    + error->getCode(); ?> + error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> +
    + debug) : ?> +
    + renderBacktrace(); ?> + + error->getPrevious()) : ?> + + _error here and in the loop as setError() assigns errors to this property and we need this for the backtrace to work correctly ?> + + setError($this->_error->getPrevious()); ?> + +

    +

    _error->getMessage(), ENT_QUOTES, 'UTF-8'); ?>

    + renderBacktrace(); ?> + setError($this->_error->getPrevious()); ?> + + + setError($this->error); ?> + +
    + +
    +
    +
    +
    +
    + + +
    + diff --git a/administrator/templates/atum/html/layouts/chromes/body.php b/administrator/templates/atum/html/layouts/chromes/body.php index b73e693fe638b..ab4c8f317e2d9 100644 --- a/administrator/templates/atum/html/layouts/chromes/body.php +++ b/administrator/templates/atum/html/layouts/chromes/body.php @@ -1,4 +1,5 @@ content === '') -{ - return; +if ((string) $module->content === '') { + return; } $id = $module->id; @@ -41,31 +41,31 @@ ?>
    - < class="card pt-3"> - - isRtl() ? 'start' : 'end'; ?> - - - showtitle) : ?> - < class="card-header">title; ?>> - -
    - content; ?> -
    - > + < class="card pt-3"> + + isRtl() ? 'start' : 'end'; ?> + + + showtitle) : ?> + < class="card-header">title; ?>> + +
    + content; ?> +
    + >
    diff --git a/administrator/templates/atum/html/layouts/chromes/header-item.php b/administrator/templates/atum/html/layouts/chromes/header-item.php index 7e6d474557f08..6beaeadff20a3 100644 --- a/administrator/templates/atum/html/layouts/chromes/header-item.php +++ b/administrator/templates/atum/html/layouts/chromes/header-item.php @@ -1,4 +1,5 @@ content === '') -{ - return; +if ((string) $module->content === '') { + return; } ?>
    - content; ?> + content; ?>
    diff --git a/administrator/templates/atum/html/layouts/chromes/title.php b/administrator/templates/atum/html/layouts/chromes/title.php index c4979a36c2152..23278b578d33e 100644 --- a/administrator/templates/atum/html/layouts/chromes/title.php +++ b/administrator/templates/atum/html/layouts/chromes/title.php @@ -1,4 +1,5 @@ content === '') -{ - return; +if ((string) $module->content === '') { + return; } ?>
    -
    title; ?>
    +
    title; ?>
    content; ?> diff --git a/administrator/templates/atum/html/layouts/chromes/well.php b/administrator/templates/atum/html/layouts/chromes/well.php index 76d5bac523e9b..be61e19d8f5e0 100644 --- a/administrator/templates/atum/html/layouts/chromes/well.php +++ b/administrator/templates/atum/html/layouts/chromes/well.php @@ -1,4 +1,5 @@ content === '') -{ - return; +if ((string) $module->content === '') { + return; } $id = $module->id; @@ -44,39 +44,39 @@ ?>
    - < class="card mb-3 "> - showtitle) : ?> -
    - - isRtl() ? 'start' : 'end'; ?> - - + < class="card mb-3 "> + showtitle) : ?> +
    + + isRtl() ? 'start' : 'end'; ?> + + - showtitle) : ?> - <> - - title, ENT_QUOTES, 'UTF-8'); ?> - > - -
    - -
    - content; ?> -
    - > + showtitle) : ?> + <> + + title, ENT_QUOTES, 'UTF-8'); ?> + > + +
    + +
    + content; ?> +
    + >
    diff --git a/administrator/templates/atum/html/layouts/status.php b/administrator/templates/atum/html/layouts/status.php index 41a28e953104a..364eec92381a4 100644 --- a/administrator/templates/atum/html/layouts/status.php +++ b/administrator/templates/atum/html/layouts/status.php @@ -1,4 +1,5 @@ $mod) -{ - $out = $renderer->render($mod); +foreach ($modules as $key => $mod) { + $out = $renderer->render($mod); - if ($out !== '') - { - if (strpos($out, 'data-bs-toggle="modal"') !== false) - { - $dom = new \DOMDocument; - $dom->loadHTML($out); - $els = $dom->getElementsByTagName('a'); + if ($out !== '') { + if (strpos($out, 'data-bs-toggle="modal"') !== false) { + $dom = new \DOMDocument(); + $dom->loadHTML($out); + $els = $dom->getElementsByTagName('a'); - $moduleCollapsedHtml[] = $dom->saveHTML($els[0]); //$els[0]->nodeValue; - } - else - { - $moduleCollapsedHtml[] = $out; - } + $moduleCollapsedHtml[] = $dom->saveHTML($els[0]); //$els[0]->nodeValue; + } else { + $moduleCollapsedHtml[] = $out; + } - $moduleHtml[] = $out; - } + $moduleHtml[] = $out; + } } ?>
    - ' . $mod . '
    '; - } - ?> -
    - - -
    + ' . $mod . ''; + } + ?> +
    + + +
    diff --git a/administrator/templates/atum/index.php b/administrator/templates/atum/index.php index 734e0ca9e48f6..419590c1d88ab 100644 --- a/administrator/templates/atum/index.php +++ b/administrator/templates/atum/index.php @@ -1,4 +1,5 @@ params->get('logoBrandLarge') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; $logoBrandSmall = $this->params->get('logoBrandSmall') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; $logoBrandLargeAlt = empty($this->params->get('logoBrandLargeAlt')) && empty($this->params->get('emptyLogoBrandLargeAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt'), ENT_COMPAT, 'UTF-8') . '"'; $logoBrandSmallAlt = empty($this->params->get('logoBrandSmallAlt')) && empty($this->params->get('emptyLogoBrandSmallAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt'), ENT_COMPAT, 'UTF-8') . '"'; // Get the hue value preg_match('#^hsla?\(([0-9]+)[\D]+([0-9]+)[\D]+([0-9]+)[\D]+([0-9](?:.\d+)?)?\)$#i', $this->params->get('hue', 'hsl(214, 63%, 20%)'), $matches); // Enable assets $wa->usePreset('template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')) - ->useStyle('template.active.language') - ->useStyle('template.user') - ->addInlineStyle(':root { + ->useStyle('template.active.language') + ->useStyle('template.user') + ->addInlineStyle(':root { --hue: ' . $matches[1] . '; --template-bg-light: ' . $this->params->get('bg-light', '#f0f4fb') . '; --template-text-dark: ' . $this->params->get('text-dark', '#495057') . '; @@ -89,99 +90,99 @@ > - - - + + +
    - - - - - - - - - -
    - - - - -
    -
    -
    - -
    -
    -
    - -
    - - -
    -
    -
    - - -
    -
    - countModules('bottom')) : ?> - - -
    - -
    -
    + + + + + + + + + +
    + + + + +
    +
    +
    + +
    +
    +
    + +
    + + +
    +
    +
    + + +
    +
    + countModules('bottom')) : ?> + + +
    + +
    +
    diff --git a/administrator/templates/atum/login.php b/administrator/templates/atum/login.php index 98c3bb7691b5e..43130e5e7b39f 100644 --- a/administrator/templates/atum/login.php +++ b/administrator/templates/atum/login.php @@ -1,4 +1,5 @@ params->get('logoBrandLarge') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandLarge'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-large.svg'; $loginLogo = $this->params->get('loginLogo') - ? Uri::root() . $this->params->get('loginLogo') - : Uri::root() . 'media/templates/administrator/atum/images/logos/login.svg'; + ? Uri::root() . $this->params->get('loginLogo') + : Uri::root() . 'media/templates/administrator/atum/images/logos/login.svg'; $logoBrandSmall = $this->params->get('logoBrandSmall') - ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) - : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; + ? Uri::root() . htmlspecialchars($this->params->get('logoBrandSmall'), ENT_QUOTES) + : Uri::root() . 'media/templates/administrator/atum/images/logos/brand-small.svg'; $logoBrandLargeAlt = empty($this->params->get('logoBrandLargeAlt')) && empty($this->params->get('emptyLogoBrandLargeAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandLargeAlt'), ENT_COMPAT, 'UTF-8') . '"'; $logoBrandSmallAlt = empty($this->params->get('logoBrandSmallAlt')) && empty($this->params->get('emptyLogoBrandSmallAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('logoBrandSmallAlt'), ENT_COMPAT, 'UTF-8') . '"'; $loginLogoAlt = empty($this->params->get('loginLogoAlt')) && empty($this->params->get('emptyLoginLogoAlt')) - ? 'alt=""' - : 'alt="' . htmlspecialchars($this->params->get('loginLogoAlt'), ENT_COMPAT, 'UTF-8') . '"'; + ? 'alt=""' + : 'alt="' . htmlspecialchars($this->params->get('loginLogoAlt'), ENT_COMPAT, 'UTF-8') . '"'; // Get the hue value preg_match('#^hsla?\(([0-9]+)[\D]+([0-9]+)[\D]+([0-9]+)[\D]+([0-9](?:.\d+)?)?\)$#i', $this->params->get('hue', 'hsl(214, 63%, 20%)'), $matches); // Enable assets $wa->usePreset('template.atum.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')) - ->useStyle('template.active.language') - ->useStyle('template.user') - ->addInlineStyle(':root { + ->useStyle('template.active.language') + ->useStyle('template.user') + ->addInlineStyle(':root { --hue: ' . $matches[1] . '; --template-bg-light: ' . $this->params->get('bg-light', '#f0f4fb') . '; --template-text-dark: ' . $this->params->get('text-dark', '#495057') . '; @@ -89,62 +90,62 @@ - - - + + + - - - - - -
    -
    -
    - -
    - -
    -
    -
    - - -
    - + + + + + +
    +
    +
    + +
    + +
    +
    +
    + + +
    + diff --git a/administrator/templates/system/component.php b/administrator/templates/system/component.php index dd283dae3c972..672f4e6fb6648 100644 --- a/administrator/templates/system/component.php +++ b/administrator/templates/system/component.php @@ -1,4 +1,5 @@ - + - - + + diff --git a/administrator/templates/system/error.php b/administrator/templates/system/error.php index 53c7a91e622a1..ea06bd4e7dc0c 100644 --- a/administrator/templates/system/error.php +++ b/administrator/templates/system/error.php @@ -1,4 +1,5 @@ - - - + + + - - - - - - - -
    -

    error->getCode() ?> -

    -
    -

    - error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> - debug) : ?> -
    error->getFile(), ENT_QUOTES, 'UTF-8');?>:error->getLine(); ?> - -

    -

    - debug) : ?> -
    - renderBacktrace(); ?> - - error->getPrevious()) : ?> - - _error here and in the loop as setError() assigns errors to this property and we need this for the backtrace to work correctly ?> - - setError($this->_error->getPrevious()); ?> - -

    -

    - _error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> -
    _error->getFile(), ENT_QUOTES, 'UTF-8');?>:_error->getLine(); ?> -

    - renderBacktrace(); ?> - setError($this->_error->getPrevious()); ?> - - - setError($this->error); ?> - -
    - -
    + + + + + + + +
    +

    error->getCode() ?> -

    +
    +

    + error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> + debug) : ?> +
    error->getFile(), ENT_QUOTES, 'UTF-8');?>:error->getLine(); ?> + +

    +

    + debug) : ?> +
    + renderBacktrace(); ?> + + error->getPrevious()) : ?> + + _error here and in the loop as setError() assigns errors to this property and we need this for the backtrace to work correctly ?> + + setError($this->_error->getPrevious()); ?> + +

    +

    + _error->getMessage(), ENT_QUOTES, 'UTF-8'); ?> +
    _error->getFile(), ENT_QUOTES, 'UTF-8');?>:_error->getLine(); ?> +

    + renderBacktrace(); ?> + setError($this->_error->getPrevious()); ?> + + + setError($this->error); ?> + +
    + +
    - + diff --git a/administrator/templates/system/index.php b/administrator/templates/system/index.php index 14ed2b2e575c2..c0d47db820eef 100644 --- a/administrator/templates/system/index.php +++ b/administrator/templates/system/index.php @@ -1,4 +1,5 @@ getExtensionFromInput(); - $data['extension'] = $extension; - - // TODO: This is a hack to drop the extension into the global input object - to satisfy how state is built - // we should be able to improve this in the future - $this->input->set('extension', $extension); - - return $data; - } - - /** - * Method to save a record. - * - * @param integer $recordKey The primary key of the item (if exists) - * - * @return integer The record ID on success, false on failure - * - * @since 4.0.6 - */ - protected function save($recordKey = null) - { - $recordId = parent::save($recordKey); - - if (!$recordId) - { - return $recordId; - } - - $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); - - if (empty($data['location'])) - { - return $recordId; - } - - /** @var Category $category */ - $category = $this->getModel('Category')->getTable('Category'); - $category->load((int) $recordId); - - $reference = $category->parent_id; - - if (!empty($data['location_reference'])) - { - $reference = (int) $data['location_reference']; - } - - $category->setLocation($reference, $data['location']); - $category->store(); - - return $recordId; - } - - /** - * Basic display of an item view - * - * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayItem($id = null) - { - $this->modelState->set('filter.extension', $this->getExtensionFromInput()); - - return parent::displayItem($id); - } - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('filter.extension', $this->getExtensionFromInput()); - - return parent::displayList(); - } - - /** - * Get extension from input - * - * @return string - * - * @since 4.0.0 - */ - private function getExtensionFromInput() - { - return $this->input->exists('extension') ? - $this->input->get('extension') : $this->input->post->get('extension'); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'categories'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'categories'; + + /** + * Method to allow extended classes to manipulate the data to be saved for an extension. + * + * @param array $data An array of input data. + * + * @return array + * + * @since 4.0.0 + */ + protected function preprocessSaveData(array $data): array + { + $extension = $this->getExtensionFromInput(); + $data['extension'] = $extension; + + // TODO: This is a hack to drop the extension into the global input object - to satisfy how state is built + // we should be able to improve this in the future + $this->input->set('extension', $extension); + + return $data; + } + + /** + * Method to save a record. + * + * @param integer $recordKey The primary key of the item (if exists) + * + * @return integer The record ID on success, false on failure + * + * @since 4.0.6 + */ + protected function save($recordKey = null) + { + $recordId = parent::save($recordKey); + + if (!$recordId) { + return $recordId; + } + + $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); + + if (empty($data['location'])) { + return $recordId; + } + + /** @var Category $category */ + $category = $this->getModel('Category')->getTable('Category'); + $category->load((int) $recordId); + + $reference = $category->parent_id; + + if (!empty($data['location_reference'])) { + $reference = (int) $data['location_reference']; + } + + $category->setLocation($reference, $data['location']); + $category->store(); + + return $recordId; + } + + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('filter.extension', $this->getExtensionFromInput()); + + return parent::displayItem($id); + } + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('filter.extension', $this->getExtensionFromInput()); + + return parent::displayList(); + } + + /** + * Get extension from input + * + * @return string + * + * @since 4.0.0 + */ + private function getExtensionFromInput() + { + return $this->input->exists('extension') ? + $this->input->get('extension') : $this->input->post->get('extension'); + } } diff --git a/api/components/com_categories/src/View/Categories/JsonapiView.php b/api/components/com_categories/src/View/Categories/JsonapiView.php index c2d244c67fb99..2302646a0d89f 100644 --- a/api/components/com_categories/src/View/Categories/JsonapiView.php +++ b/api/components/com_categories/src/View/Categories/JsonapiView.php @@ -1,4 +1,5 @@ fieldsToRenderList[] = $field->name; - } + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + foreach (FieldsHelper::getFields('com_content.categories') as $field) { + $this->fieldsToRenderList[] = $field->name; + } - return parent::displayList(); - } + return parent::displayList(); + } - /** - * Execute and display a template script. - * - * @param object $item Item - * - * @return string - * - * @since 4.0.0 - */ - public function displayItem($item = null) - { - foreach (FieldsHelper::getFields('com_content.categories') as $field) - { - $this->fieldsToRenderItem[] = $field->name; - } + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + foreach (FieldsHelper::getFields('com_content.categories') as $field) { + $this->fieldsToRenderItem[] = $field->name; + } - if ($item === null) - { - /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ - $model = $this->getModel(); - $item = $this->prepareItem($model->getItem()); - } + if ($item === null) { + /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ + $model = $this->getModel(); + $item = $this->prepareItem($model->getItem()); + } - if ($item->id === null) - { - throw new RouteNotFoundException('Item does not exist'); - } + if ($item->id === null) { + throw new RouteNotFoundException('Item does not exist'); + } - if ($item->extension != $this->getModel()->getState('filter.extension')) - { - throw new RouteNotFoundException('Item does not exist'); - } + if ($item->extension != $this->getModel()->getState('filter.extension')) { + throw new RouteNotFoundException('Item does not exist'); + } - return parent::displayItem($item); - } + return parent::displayItem($item); + } - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - foreach (FieldsHelper::getFields('com_content.categories', $item, true) as $field) - { - $item->{$field->name} = $field->apivalue ?? $field->rawvalue; - } + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + foreach (FieldsHelper::getFields('com_content.categories', $item, true) as $field) { + $item->{$field->name} = $field->apivalue ?? $field->rawvalue; + } - return parent::prepareItem($item); - } + return parent::prepareItem($item); + } } diff --git a/api/components/com_config/src/Controller/ApplicationController.php b/api/components/com_config/src/Controller/ApplicationController.php index c273f891f0323..17bca79911be8 100644 --- a/api/components/com_config/src/Controller/ApplicationController.php +++ b/api/components/com_config/src/Controller/ApplicationController.php @@ -1,4 +1,5 @@ app->getDocument()->getType(); - $viewLayout = $this->input->get('layout', 'default', 'string'); - - try - { - /** @var JsonapiView $view */ - $view = $this->getView( - $this->default_view, - $viewType, - '', - ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] - ); - } - catch (\Exception $e) - { - throw new \RuntimeException($e->getMessage()); - } - - /** @var ApplicationModel $model */ - $model = $this->getModel($this->contentType); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); - } - - // Push the model into the view (as default) - $view->setModel($model, true); - - $view->document = $this->app->getDocument(); - $view->displayList(); - - return $this; - } - - /** - * Method to edit an existing record. - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function edit() - { - /** @var ApplicationModel $model */ - $model = $this->getModel($this->contentType); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); - } - - // Access check. - if (!$this->allowEdit()) - { - throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403); - } - - $data = json_decode($this->input->json->getRaw(), true); - - // Complete data array if needed - $oldData = $model->getData(); - $data = array_replace($oldData, $data); - - // @todo: Not the cleanest thing ever but it works... - Form::addFormPath(JPATH_COMPONENT_ADMINISTRATOR . '/forms'); - - // Must load after serving service-requests - $form = $model->getForm(); - - // Validate the posted data. - $validData = $model->validate($form, $data); - - // Check for validation errors. - if ($validData === false) - { - $errors = $model->getErrors(); - $messages = []; - - for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $messages[] = "{$errors[$i]->getMessage()}"; - } - else - { - $messages[] = "{$errors[$i]}"; - } - } - - throw new InvalidParameterException(implode("\n", $messages)); - } - - if (!$model->save($validData)) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SERVER'), 500); - } - - return $this; - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'application'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'application'; + + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $viewType = $this->app->getDocument()->getType(); + $viewLayout = $this->input->get('layout', 'default', 'string'); + + try { + /** @var JsonapiView $view */ + $view = $this->getView( + $this->default_view, + $viewType, + '', + ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] + ); + } catch (\Exception $e) { + throw new \RuntimeException($e->getMessage()); + } + + /** @var ApplicationModel $model */ + $model = $this->getModel($this->contentType); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); + } + + // Push the model into the view (as default) + $view->setModel($model, true); + + $view->document = $this->app->getDocument(); + $view->displayList(); + + return $this; + } + + /** + * Method to edit an existing record. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function edit() + { + /** @var ApplicationModel $model */ + $model = $this->getModel($this->contentType); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); + } + + // Access check. + if (!$this->allowEdit()) { + throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403); + } + + $data = json_decode($this->input->json->getRaw(), true); + + // Complete data array if needed + $oldData = $model->getData(); + $data = array_replace($oldData, $data); + + // @todo: Not the cleanest thing ever but it works... + Form::addFormPath(JPATH_COMPONENT_ADMINISTRATOR . '/forms'); + + // Must load after serving service-requests + $form = $model->getForm(); + + // Validate the posted data. + $validData = $model->validate($form, $data); + + // Check for validation errors. + if ($validData === false) { + $errors = $model->getErrors(); + $messages = []; + + for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $messages[] = "{$errors[$i]->getMessage()}"; + } else { + $messages[] = "{$errors[$i]}"; + } + } + + throw new InvalidParameterException(implode("\n", $messages)); + } + + if (!$model->save($validData)) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SERVER'), 500); + } + + return $this; + } } diff --git a/api/components/com_config/src/Controller/ComponentController.php b/api/components/com_config/src/Controller/ComponentController.php index ef60de1d7754d..6188ae84596af 100644 --- a/api/components/com_config/src/Controller/ComponentController.php +++ b/api/components/com_config/src/Controller/ComponentController.php @@ -1,4 +1,5 @@ app->getDocument()->getType(); - $viewLayout = $this->input->get('layout', 'default', 'string'); - - try - { - /** @var JsonapiView $view */ - $view = $this->getView( - $this->default_view, - $viewType, - '', - ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] - ); - } - catch (\Exception $e) - { - throw new \RuntimeException($e->getMessage()); - } - - /** @var ComponentModel $model */ - $model = $this->getModel($this->contentType); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); - } - - // Push the model into the view (as default) - $view->setModel($model, true); - $view->set('component_name', $this->input->get('component_name')); - - $view->document = $this->app->getDocument(); - $view->displayList(); - - return $this; - } - - /** - * Method to edit an existing record. - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function edit() - { - /** @var ComponentModel $model */ - $model = $this->getModel($this->contentType); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); - } - - // Access check. - if (!$this->allowEdit()) - { - throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403); - } - - $option = $this->input->get('component_name'); - - // @todo: Not the cleanest thing ever but it works... - Form::addFormPath(JPATH_ADMINISTRATOR . '/components/' . $option); - - // Must load after serving service-requests - $form = $model->getForm(); - - $data = json_decode($this->input->json->getRaw(), true); - - $component = ComponentHelper::getComponent($option); - $oldData = $component->getParams()->toArray(); - $data = array_replace($oldData, $data); - - // Validate the posted data. - $validData = $model->validate($form, $data); - - if ($validData === false) - { - $errors = $model->getErrors(); - $messages = []; - - for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $messages[] = "{$errors[$i]->getMessage()}"; - } - else - { - $messages[] = "{$errors[$i]}"; - } - } - - throw new InvalidParameterException(implode("\n", $messages)); - } - - // Attempt to save the configuration. - $data = [ - 'params' => $validData, - 'id' => ExtensionHelper::getExtensionRecord($option, 'component')->extension_id, - 'option' => $option, - ]; - - if (!$model->save($data)) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SERVER'), 500); - } - - return $this; - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'component'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'component'; + + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $viewType = $this->app->getDocument()->getType(); + $viewLayout = $this->input->get('layout', 'default', 'string'); + + try { + /** @var JsonapiView $view */ + $view = $this->getView( + $this->default_view, + $viewType, + '', + ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] + ); + } catch (\Exception $e) { + throw new \RuntimeException($e->getMessage()); + } + + /** @var ComponentModel $model */ + $model = $this->getModel($this->contentType); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); + } + + // Push the model into the view (as default) + $view->setModel($model, true); + $view->set('component_name', $this->input->get('component_name')); + + $view->document = $this->app->getDocument(); + $view->displayList(); + + return $this; + } + + /** + * Method to edit an existing record. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function edit() + { + /** @var ComponentModel $model */ + $model = $this->getModel($this->contentType); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'), 500); + } + + // Access check. + if (!$this->allowEdit()) { + throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403); + } + + $option = $this->input->get('component_name'); + + // @todo: Not the cleanest thing ever but it works... + Form::addFormPath(JPATH_ADMINISTRATOR . '/components/' . $option); + + // Must load after serving service-requests + $form = $model->getForm(); + + $data = json_decode($this->input->json->getRaw(), true); + + $component = ComponentHelper::getComponent($option); + $oldData = $component->getParams()->toArray(); + $data = array_replace($oldData, $data); + + // Validate the posted data. + $validData = $model->validate($form, $data); + + if ($validData === false) { + $errors = $model->getErrors(); + $messages = []; + + for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $messages[] = "{$errors[$i]->getMessage()}"; + } else { + $messages[] = "{$errors[$i]}"; + } + } + + throw new InvalidParameterException(implode("\n", $messages)); + } + + // Attempt to save the configuration. + $data = [ + 'params' => $validData, + 'id' => ExtensionHelper::getExtensionRecord($option, 'component')->extension_id, + 'option' => $option, + ]; + + if (!$model->save($data)) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SERVER'), 500); + } + + return $this; + } } diff --git a/api/components/com_config/src/View/Application/JsonapiView.php b/api/components/com_config/src/View/Application/JsonapiView.php index 090b2ddb1404d..0aec96f4716d0 100644 --- a/api/components/com_config/src/View/Application/JsonapiView.php +++ b/api/components/com_config/src/View/Application/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); - $items = []; - - foreach ($model->getData() as $key => $value) - { - $item = (object) [$key => $value]; - $items[] = $this->prepareItem($item); - } - - // Set up links for pagination - $currentUrl = Uri::getInstance(); - $currentPageDefaultInformation = ['offset' => 0, 'limit' => 20]; - $currentPageQuery = $currentUrl->getVar('page', $currentPageDefaultInformation); - - $offset = $currentPageQuery['offset']; - $limit = $currentPageQuery['limit']; - $totalItemsCount = \count($items); - $totalPagesAvailable = ceil($totalItemsCount / $limit); - - $items = array_splice($items, $offset, $limit); - - $this->document->addMeta('total-pages', $totalPagesAvailable) - ->addLink('self', (string) $currentUrl); - - // Check for first and previous pages - if ($offset > 0) - { - $firstPage = clone $currentUrl; - $firstPageQuery = $currentPageQuery; - $firstPageQuery['offset'] = 0; - $firstPage->setVar('page', $firstPageQuery); - - $previousPage = clone $currentUrl; - $previousPageQuery = $currentPageQuery; - $previousOffset = $currentPageQuery['offset'] - $limit; - $previousPageQuery['offset'] = $previousOffset >= 0 ? $previousOffset : 0; - $previousPage->setVar('page', $previousPageQuery); - - $this->document->addLink('first', $this->queryEncode((string) $firstPage)) - ->addLink('previous', $this->queryEncode((string) $previousPage)); - } - - // Check for next and last pages - if ($offset + $limit < $totalItemsCount) - { - $nextPage = clone $currentUrl; - $nextPageQuery = $currentPageQuery; - $nextOffset = $currentPageQuery['offset'] + $limit; - $nextPageQuery['offset'] = ($nextOffset > ($totalPagesAvailable * $limit)) ? $totalPagesAvailable - $limit : $nextOffset; - $nextPage->setVar('page', $nextPageQuery); - - $lastPage = clone $currentUrl; - $lastPageQuery = $currentPageQuery; - $lastPageQuery['offset'] = ($totalPagesAvailable - 1) * $limit; - $lastPage->setVar('page', $lastPageQuery); - - $this->document->addLink('next', $this->queryEncode((string) $nextPage)) - ->addLink('last', $this->queryEncode((string) $lastPage)); - } - - $collection = (new Collection($items, new JoomlaSerializer($this->type))); - - // Set the data into the document and render it - $this->document->setData($collection); - - return $this->document->render(); - } - - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - $item->id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; - - return $item; - } + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + /** @var ApplicationModel $model */ + $model = $this->getModel(); + $items = []; + + foreach ($model->getData() as $key => $value) { + $item = (object) [$key => $value]; + $items[] = $this->prepareItem($item); + } + + // Set up links for pagination + $currentUrl = Uri::getInstance(); + $currentPageDefaultInformation = ['offset' => 0, 'limit' => 20]; + $currentPageQuery = $currentUrl->getVar('page', $currentPageDefaultInformation); + + $offset = $currentPageQuery['offset']; + $limit = $currentPageQuery['limit']; + $totalItemsCount = \count($items); + $totalPagesAvailable = ceil($totalItemsCount / $limit); + + $items = array_splice($items, $offset, $limit); + + $this->document->addMeta('total-pages', $totalPagesAvailable) + ->addLink('self', (string) $currentUrl); + + // Check for first and previous pages + if ($offset > 0) { + $firstPage = clone $currentUrl; + $firstPageQuery = $currentPageQuery; + $firstPageQuery['offset'] = 0; + $firstPage->setVar('page', $firstPageQuery); + + $previousPage = clone $currentUrl; + $previousPageQuery = $currentPageQuery; + $previousOffset = $currentPageQuery['offset'] - $limit; + $previousPageQuery['offset'] = $previousOffset >= 0 ? $previousOffset : 0; + $previousPage->setVar('page', $previousPageQuery); + + $this->document->addLink('first', $this->queryEncode((string) $firstPage)) + ->addLink('previous', $this->queryEncode((string) $previousPage)); + } + + // Check for next and last pages + if ($offset + $limit < $totalItemsCount) { + $nextPage = clone $currentUrl; + $nextPageQuery = $currentPageQuery; + $nextOffset = $currentPageQuery['offset'] + $limit; + $nextPageQuery['offset'] = ($nextOffset > ($totalPagesAvailable * $limit)) ? $totalPagesAvailable - $limit : $nextOffset; + $nextPage->setVar('page', $nextPageQuery); + + $lastPage = clone $currentUrl; + $lastPageQuery = $currentPageQuery; + $lastPageQuery['offset'] = ($totalPagesAvailable - 1) * $limit; + $lastPage->setVar('page', $lastPageQuery); + + $this->document->addLink('next', $this->queryEncode((string) $nextPage)) + ->addLink('last', $this->queryEncode((string) $lastPage)); + } + + $collection = (new Collection($items, new JoomlaSerializer($this->type))); + + // Set the data into the document and render it + $this->document->setData($collection); + + return $this->document->render(); + } + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = ExtensionHelper::getExtensionRecord('joomla', 'file')->extension_id; + + return $item; + } } diff --git a/api/components/com_config/src/View/Component/JsonapiView.php b/api/components/com_config/src/View/Component/JsonapiView.php index 1381854646ac1..02eb0345722ac 100644 --- a/api/components/com_config/src/View/Component/JsonapiView.php +++ b/api/components/com_config/src/View/Component/JsonapiView.php @@ -1,4 +1,5 @@ get('component_name')); - - if ($component === null || !$component->enabled) - { - // @todo: exception component unavailable - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_INVALID_COMPONENT_NAME'), 400); - } - - $data = $component->getParams()->toObject(); - } - catch (\Exception $e) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SERVER'), 500, $e); - } - - $items = []; - - foreach ($data as $key => $value) - { - $item = (object) [$key => $value]; - $items[] = $this->prepareItem($item); - } - - // Set up links for pagination - $currentUrl = Uri::getInstance(); - $currentPageDefaultInformation = ['offset' => 0, 'limit' => 20]; - $currentPageQuery = $currentUrl->getVar('page', $currentPageDefaultInformation); - - $offset = $currentPageQuery['offset']; - $limit = $currentPageQuery['limit']; - $totalItemsCount = \count($items); - $totalPagesAvailable = ceil($totalItemsCount / $limit); - - $items = array_splice($items, $offset, $limit); - - $this->document->addMeta('total-pages', $totalPagesAvailable) - ->addLink('self', (string) $currentUrl); - - // Check for first and previous pages - if ($offset > 0) - { - $firstPage = clone $currentUrl; - $firstPageQuery = $currentPageQuery; - $firstPageQuery['offset'] = 0; - $firstPage->setVar('page', $firstPageQuery); - - $previousPage = clone $currentUrl; - $previousPageQuery = $currentPageQuery; - $previousOffset = $currentPageQuery['offset'] - $limit; - $previousPageQuery['offset'] = $previousOffset >= 0 ? $previousOffset : 0; - $previousPage->setVar('page', $previousPageQuery); - - $this->document->addLink('first', $this->queryEncode((string) $firstPage)) - ->addLink('previous', $this->queryEncode((string) $previousPage)); - } - - // Check for next and last pages - if ($offset + $limit < $totalItemsCount) - { - $nextPage = clone $currentUrl; - $nextPageQuery = $currentPageQuery; - $nextOffset = $currentPageQuery['offset'] + $limit; - $nextPageQuery['offset'] = ($nextOffset > ($totalPagesAvailable * $limit)) ? $totalPagesAvailable - $limit : $nextOffset; - $nextPage->setVar('page', $nextPageQuery); - - $lastPage = clone $currentUrl; - $lastPageQuery = $currentPageQuery; - $lastPageQuery['offset'] = ($totalPagesAvailable - 1) * $limit; - $lastPage->setVar('page', $lastPageQuery); - - $this->document->addLink('next', $this->queryEncode((string) $nextPage)) - ->addLink('last', $this->queryEncode((string) $lastPage)); - } - - $collection = (new Collection($items, new JoomlaSerializer($this->type))); - - // Set the data into the document and render it - $this->document->setData($collection); - - return $this->document->render(); - } - - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - $item->id = ExtensionHelper::getExtensionRecord($this->get('component_name'), 'component')->extension_id; - - return $item; - } + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + try { + $component = ComponentHelper::getComponent($this->get('component_name')); + + if ($component === null || !$component->enabled) { + // @todo: exception component unavailable + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_INVALID_COMPONENT_NAME'), 400); + } + + $data = $component->getParams()->toObject(); + } catch (\Exception $e) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_SERVER'), 500, $e); + } + + $items = []; + + foreach ($data as $key => $value) { + $item = (object) [$key => $value]; + $items[] = $this->prepareItem($item); + } + + // Set up links for pagination + $currentUrl = Uri::getInstance(); + $currentPageDefaultInformation = ['offset' => 0, 'limit' => 20]; + $currentPageQuery = $currentUrl->getVar('page', $currentPageDefaultInformation); + + $offset = $currentPageQuery['offset']; + $limit = $currentPageQuery['limit']; + $totalItemsCount = \count($items); + $totalPagesAvailable = ceil($totalItemsCount / $limit); + + $items = array_splice($items, $offset, $limit); + + $this->document->addMeta('total-pages', $totalPagesAvailable) + ->addLink('self', (string) $currentUrl); + + // Check for first and previous pages + if ($offset > 0) { + $firstPage = clone $currentUrl; + $firstPageQuery = $currentPageQuery; + $firstPageQuery['offset'] = 0; + $firstPage->setVar('page', $firstPageQuery); + + $previousPage = clone $currentUrl; + $previousPageQuery = $currentPageQuery; + $previousOffset = $currentPageQuery['offset'] - $limit; + $previousPageQuery['offset'] = $previousOffset >= 0 ? $previousOffset : 0; + $previousPage->setVar('page', $previousPageQuery); + + $this->document->addLink('first', $this->queryEncode((string) $firstPage)) + ->addLink('previous', $this->queryEncode((string) $previousPage)); + } + + // Check for next and last pages + if ($offset + $limit < $totalItemsCount) { + $nextPage = clone $currentUrl; + $nextPageQuery = $currentPageQuery; + $nextOffset = $currentPageQuery['offset'] + $limit; + $nextPageQuery['offset'] = ($nextOffset > ($totalPagesAvailable * $limit)) ? $totalPagesAvailable - $limit : $nextOffset; + $nextPage->setVar('page', $nextPageQuery); + + $lastPage = clone $currentUrl; + $lastPageQuery = $currentPageQuery; + $lastPageQuery['offset'] = ($totalPagesAvailable - 1) * $limit; + $lastPage->setVar('page', $lastPageQuery); + + $this->document->addLink('next', $this->queryEncode((string) $nextPage)) + ->addLink('last', $this->queryEncode((string) $lastPage)); + } + + $collection = (new Collection($items, new JoomlaSerializer($this->type))); + + // Set the data into the document and render it + $this->document->setData($collection); + + return $this->document->render(); + } + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = ExtensionHelper::getExtensionRecord($this->get('component_name'), 'component')->extension_id; + + return $item; + } } diff --git a/api/components/com_contact/src/Controller/ContactController.php b/api/components/com_contact/src/Controller/ContactController.php index b9805d33ab5e9..d23df969bddb8 100644 --- a/api/components/com_contact/src/Controller/ContactController.php +++ b/api/components/com_contact/src/Controller/ContactController.php @@ -1,4 +1,5 @@ name])) - { - !isset($data['com_fields']) && $data['com_fields'] = []; - - $data['com_fields'][$field->name] = $data[$field->name]; - unset($data[$field->name]); - } - } - - return $data; - } - - /** - * Submit contact form - * - * @param integer $id Leave empty if you want to retrieve data from the request - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function submitForm($id = null) - { - if ($id === null) - { - $id = $this->input->post->get('id', 0, 'int'); - } - - $modelName = Inflector::singularize($this->contentType); - - /** @var \Joomla\Component\Contact\Site\Model\ContactModel $model */ - $model = $this->getModel($modelName, 'Site'); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $model->setState('filter.published', 1); - - $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); - $contact = $model->getItem($id); - - if ($contact->id === null) - { - throw new RouteNotFoundException('Item does not exist'); - } - - $contactParams = new Registry($contact->params); - - if (!$contactParams->get('show_email_form')) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_DISPLAY_EMAIL_FORM')); - } - - // Contact plugins - PluginHelper::importPlugin('contact'); - - Form::addFormPath(JPATH_COMPONENT_SITE . '/forms'); - - // Validate the posted data. - $form = $model->getForm(); - - if (!$form) - { - throw new \RuntimeException($model->getError(), 500); - } - - if (!$model->validate($form, $data)) - { - $errors = $model->getErrors(); - $messages = []; - - for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $messages[] = "{$errors[$i]->getMessage()}"; - } - else - { - $messages[] = "{$errors[$i]}"; - } - } - - throw new InvalidParameterException(implode("\n", $messages)); - } - - // Validation succeeded, continue with custom handlers - $results = $this->app->triggerEvent('onValidateContact', [&$contact, &$data]); - - foreach ($results as $result) - { - if ($result instanceof \Exception) - { - throw new InvalidParameterException($result->getMessage()); - } - } - - // Passed Validation: Process the contact plugins to integrate with other applications - $this->app->triggerEvent('onSubmitContact', [&$contact, &$data]); - - // Send the email - $sent = false; - - $params = ComponentHelper::getParams('com_contact'); - - if (!$params->get('custom_reply')) - { - $sent = $this->_sendEmail($data, $contact, $params->get('show_email_copy', 0)); - } - - if (!$sent) - { - throw new SendEmail('Error sending message'); - } - - return $this; - } - - /** - * Method to get a model object, loading it if required. - * - * @param array $data The data to send in the email. - * @param \stdClass $contact The user information to send the email to - * @param boolean $emailCopyToSender True to send a copy of the email to the user. - * - * @return boolean True on success sending the email, false on failure. - * - * @since 1.6.4 - */ - private function _sendEmail($data, $contact, $emailCopyToSender) - { - $app = $this->app; - - Factory::getLanguage()->load('com_contact', JPATH_SITE, $app->getLanguage()->getTag(), true); - - if ($contact->email_to == '' && $contact->user_id != 0) - { - $contact_user = User::getInstance($contact->user_id); - $contact->email_to = $contact_user->get('email'); - } - - $templateData = [ - 'sitename' => $app->get('sitename'), - 'name' => $data['contact_name'], - 'contactname' => $contact->name, - 'email' => PunycodeHelper::emailToPunycode($data['contact_email']), - 'subject' => $data['contact_subject'], - 'body' => stripslashes($data['contact_message']), - 'url' => Uri::base(), - 'customfields' => '', - ]; - - // Load the custom fields - if (!empty($data['com_fields']) && $fields = FieldsHelper::getFields('com_contact.mail', $contact, true, $data['com_fields'])) - { - $output = FieldsHelper::render( - 'com_contact.mail', - 'fields.render', - array( - 'context' => 'com_contact.mail', - 'item' => $contact, - 'fields' => $fields, - ) - ); - - if ($output) - { - $templateData['customfields'] = $output; - } - } - - try - { - $mailer = new MailTemplate('com_contact.mail', $app->getLanguage()->getTag()); - $mailer->addRecipient($contact->email_to); - $mailer->setReplyTo($templateData['email'], $templateData['name']); - $mailer->addTemplateData($templateData); - $sent = $mailer->send(); - - // If we are supposed to copy the sender, do so. - if ($emailCopyToSender == true && !empty($data['contact_email_copy'])) - { - $mailer = new MailTemplate('com_contact.mail.copy', $app->getLanguage()->getTag()); - $mailer->addRecipient($templateData['email']); - $mailer->setReplyTo($templateData['email'], $templateData['name']); - $mailer->addTemplateData($templateData); - $sent = $mailer->send(); - } - } - catch (MailDisabledException | phpMailerException $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $sent = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $sent = false; - } - } - - return $sent; - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'contacts'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'contacts'; + + /** + * Method to allow extended classes to manipulate the data to be saved for an extension. + * + * @param array $data An array of input data. + * + * @return array + * + * @since 4.0.0 + */ + protected function preprocessSaveData(array $data): array + { + foreach (FieldsHelper::getFields('com_contact.contact') as $field) { + if (isset($data[$field->name])) { + !isset($data['com_fields']) && $data['com_fields'] = []; + + $data['com_fields'][$field->name] = $data[$field->name]; + unset($data[$field->name]); + } + } + + return $data; + } + + /** + * Submit contact form + * + * @param integer $id Leave empty if you want to retrieve data from the request + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function submitForm($id = null) + { + if ($id === null) { + $id = $this->input->post->get('id', 0, 'int'); + } + + $modelName = Inflector::singularize($this->contentType); + + /** @var \Joomla\Component\Contact\Site\Model\ContactModel $model */ + $model = $this->getModel($modelName, 'Site'); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $model->setState('filter.published', 1); + + $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); + $contact = $model->getItem($id); + + if ($contact->id === null) { + throw new RouteNotFoundException('Item does not exist'); + } + + $contactParams = new Registry($contact->params); + + if (!$contactParams->get('show_email_form')) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_DISPLAY_EMAIL_FORM')); + } + + // Contact plugins + PluginHelper::importPlugin('contact'); + + Form::addFormPath(JPATH_COMPONENT_SITE . '/forms'); + + // Validate the posted data. + $form = $model->getForm(); + + if (!$form) { + throw new \RuntimeException($model->getError(), 500); + } + + if (!$model->validate($form, $data)) { + $errors = $model->getErrors(); + $messages = []; + + for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $messages[] = "{$errors[$i]->getMessage()}"; + } else { + $messages[] = "{$errors[$i]}"; + } + } + + throw new InvalidParameterException(implode("\n", $messages)); + } + + // Validation succeeded, continue with custom handlers + $results = $this->app->triggerEvent('onValidateContact', [&$contact, &$data]); + + foreach ($results as $result) { + if ($result instanceof \Exception) { + throw new InvalidParameterException($result->getMessage()); + } + } + + // Passed Validation: Process the contact plugins to integrate with other applications + $this->app->triggerEvent('onSubmitContact', [&$contact, &$data]); + + // Send the email + $sent = false; + + $params = ComponentHelper::getParams('com_contact'); + + if (!$params->get('custom_reply')) { + $sent = $this->_sendEmail($data, $contact, $params->get('show_email_copy', 0)); + } + + if (!$sent) { + throw new SendEmail('Error sending message'); + } + + return $this; + } + + /** + * Method to get a model object, loading it if required. + * + * @param array $data The data to send in the email. + * @param \stdClass $contact The user information to send the email to + * @param boolean $emailCopyToSender True to send a copy of the email to the user. + * + * @return boolean True on success sending the email, false on failure. + * + * @since 1.6.4 + */ + private function _sendEmail($data, $contact, $emailCopyToSender) + { + $app = $this->app; + + Factory::getLanguage()->load('com_contact', JPATH_SITE, $app->getLanguage()->getTag(), true); + + if ($contact->email_to == '' && $contact->user_id != 0) { + $contact_user = User::getInstance($contact->user_id); + $contact->email_to = $contact_user->get('email'); + } + + $templateData = [ + 'sitename' => $app->get('sitename'), + 'name' => $data['contact_name'], + 'contactname' => $contact->name, + 'email' => PunycodeHelper::emailToPunycode($data['contact_email']), + 'subject' => $data['contact_subject'], + 'body' => stripslashes($data['contact_message']), + 'url' => Uri::base(), + 'customfields' => '', + ]; + + // Load the custom fields + if (!empty($data['com_fields']) && $fields = FieldsHelper::getFields('com_contact.mail', $contact, true, $data['com_fields'])) { + $output = FieldsHelper::render( + 'com_contact.mail', + 'fields.render', + array( + 'context' => 'com_contact.mail', + 'item' => $contact, + 'fields' => $fields, + ) + ); + + if ($output) { + $templateData['customfields'] = $output; + } + } + + try { + $mailer = new MailTemplate('com_contact.mail', $app->getLanguage()->getTag()); + $mailer->addRecipient($contact->email_to); + $mailer->setReplyTo($templateData['email'], $templateData['name']); + $mailer->addTemplateData($templateData); + $sent = $mailer->send(); + + // If we are supposed to copy the sender, do so. + if ($emailCopyToSender == true && !empty($data['contact_email_copy'])) { + $mailer = new MailTemplate('com_contact.mail.copy', $app->getLanguage()->getTag()); + $mailer->addRecipient($templateData['email']); + $mailer->setReplyTo($templateData['email'], $templateData['name']); + $mailer->addTemplateData($templateData); + $sent = $mailer->send(); + } + } catch (MailDisabledException | phpMailerException $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $sent = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $sent = false; + } + } + + return $sent; + } } diff --git a/api/components/com_contact/src/Serializer/ContactSerializer.php b/api/components/com_contact/src/Serializer/ContactSerializer.php index 23fe7333f3b40..89f7e19020d3e 100644 --- a/api/components/com_contact/src/Serializer/ContactSerializer.php +++ b/api/components/com_contact/src/Serializer/ContactSerializer.php @@ -1,4 +1,5 @@ type); - - foreach ($model->associations as $association) - { - $resources[] = (new Resource($association, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/contact/' . $association->id)); - } - - $collection = new Collection($resources, $serializer); - - return new Relationship($collection); - } - - /** - * Build category relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function category($model) - { - $serializer = new JoomlaSerializer('categories'); - - $resource = (new Resource($model->catid, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/content/categories/' . $model->catid)); - - return new Relationship($resource); - } - - /** - * Build category relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function createdBy($model) - { - $serializer = new JoomlaSerializer('users'); - - $resource = (new Resource($model->created_by, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->created_by)); - - return new Relationship($resource); - } - - /** - * Build editor relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function modifiedBy($model) - { - $serializer = new JoomlaSerializer('users'); - - $resource = (new Resource($model->modified_by, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->modified_by)); - - return new Relationship($resource); - } - - /** - * Build contact user relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function userId($model) - { - $serializer = new JoomlaSerializer('users'); - - $resource = (new Resource($model->user_id, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->user_id)); - - return new Relationship($resource); - } + use TagApiSerializerTrait; + + /** + * Build content relationships by associations + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function languageAssociations($model) + { + $resources = []; + + // @todo: This can't be hardcoded in the future? + $serializer = new JoomlaSerializer($this->type); + + foreach ($model->associations as $association) { + $resources[] = (new Resource($association, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/contact/' . $association->id)); + } + + $collection = new Collection($resources, $serializer); + + return new Relationship($collection); + } + + /** + * Build category relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function category($model) + { + $serializer = new JoomlaSerializer('categories'); + + $resource = (new Resource($model->catid, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/content/categories/' . $model->catid)); + + return new Relationship($resource); + } + + /** + * Build category relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function createdBy($model) + { + $serializer = new JoomlaSerializer('users'); + + $resource = (new Resource($model->created_by, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->created_by)); + + return new Relationship($resource); + } + + /** + * Build editor relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function modifiedBy($model) + { + $serializer = new JoomlaSerializer('users'); + + $resource = (new Resource($model->modified_by, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->modified_by)); + + return new Relationship($resource); + } + + /** + * Build contact user relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function userId($model) + { + $serializer = new JoomlaSerializer('users'); + + $resource = (new Resource($model->user_id, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->user_id)); + + return new Relationship($resource); + } } diff --git a/api/components/com_contact/src/View/Contacts/JsonapiView.php b/api/components/com_contact/src/View/Contacts/JsonapiView.php index de1091fd6a676..7814dfe7e70b5 100644 --- a/api/components/com_contact/src/View/Contacts/JsonapiView.php +++ b/api/components/com_contact/src/View/Contacts/JsonapiView.php @@ -1,4 +1,5 @@ serializer = new ContactSerializer($config['contentType']); - } - - parent::__construct($config); - } - - /** - * Execute and display a template script. - * - * @param array|null $items Array of items - * - * @return string - * - * @since 4.0.0 - */ - public function displayList(array $items = null) - { - foreach (FieldsHelper::getFields('com_contact.contact') as $field) - { - $this->fieldsToRenderList[] = $field->name; - } - - return parent::displayList(); - } - - /** - * Execute and display a template script. - * - * @param object $item Item - * - * @return string - * - * @since 4.0.0 - */ - public function displayItem($item = null) - { - foreach (FieldsHelper::getFields('com_contact.contact') as $field) - { - $this->fieldsToRenderItem[] = $field->name; - } - - if (Multilanguage::isEnabled()) - { - $this->fieldsToRenderItem[] = 'languageAssociations'; - $this->relationship[] = 'languageAssociations'; - } - - return parent::displayItem(); - } - - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - foreach (FieldsHelper::getFields('com_contact.contact', $item, true) as $field) - { - $item->{$field->name} = $field->apivalue ?? $field->rawvalue; - } - - if (Multilanguage::isEnabled() && !empty($item->associations)) - { - $associations = []; - - foreach ($item->associations as $language => $association) - { - $itemId = explode(':', $association)[0]; - - $associations[] = (object) [ - 'id' => $itemId, - 'language' => $language, - ]; - } - - $item->associations = $associations; - } - - if (!empty($item->tags->tags)) - { - $tagsIds = explode(',', $item->tags->tags); - $tagsNames = $item->tagsHelper->getTagNames($tagsIds); - - $item->tags = array_combine($tagsIds, $tagsNames); - } - else - { - $item->tags = []; - } - - if (isset($item->image)) - { - $item->image = ContentHelper::resolve($item->image); - } - - return parent::prepareItem($item); - } + /** + * The fields to render item in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderItem = [ + 'id', + 'alias', + 'name', + 'category', + 'created', + 'created_by', + 'created_by_alias', + 'modified', + 'modified_by', + 'image', + 'tags', + 'featured', + 'publish_up', + 'publish_down', + 'version', + 'hits', + 'metakey', + 'metadesc', + 'metadata', + 'con_position', + 'address', + 'suburb', + 'state', + 'country', + 'postcode', + 'telephone', + 'fax', + 'misc', + 'email_to', + 'default_con', + 'user_id', + 'access', + 'mobile', + 'webpage', + 'sortname1', + 'sortname2', + 'sortname3', + ]; + + /** + * The fields to render items in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderList = [ + 'id', + 'alias', + 'name', + 'category', + 'created', + 'created_by', + 'created_by_alias', + 'modified', + 'modified_by', + 'image', + 'tags', + 'user_id', + ]; + + /** + * The relationships the item has + * + * @var array + * @since 4.0.0 + */ + protected $relationship = [ + 'category', + 'created_by', + 'modified_by', + 'user_id', + 'tags', + ]; + + /** + * Constructor. + * + * @param array $config A named configuration array for object construction. + * contentType: the name (optional) of the content type to use for the serialization + * + * @since 4.0.0 + */ + public function __construct($config = []) + { + if (\array_key_exists('contentType', $config)) { + $this->serializer = new ContactSerializer($config['contentType']); + } + + parent::__construct($config); + } + + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + foreach (FieldsHelper::getFields('com_contact.contact') as $field) { + $this->fieldsToRenderList[] = $field->name; + } + + return parent::displayList(); + } + + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + foreach (FieldsHelper::getFields('com_contact.contact') as $field) { + $this->fieldsToRenderItem[] = $field->name; + } + + if (Multilanguage::isEnabled()) { + $this->fieldsToRenderItem[] = 'languageAssociations'; + $this->relationship[] = 'languageAssociations'; + } + + return parent::displayItem(); + } + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + foreach (FieldsHelper::getFields('com_contact.contact', $item, true) as $field) { + $item->{$field->name} = $field->apivalue ?? $field->rawvalue; + } + + if (Multilanguage::isEnabled() && !empty($item->associations)) { + $associations = []; + + foreach ($item->associations as $language => $association) { + $itemId = explode(':', $association)[0]; + + $associations[] = (object) [ + 'id' => $itemId, + 'language' => $language, + ]; + } + + $item->associations = $associations; + } + + if (!empty($item->tags->tags)) { + $tagsIds = explode(',', $item->tags->tags); + $tagsNames = $item->tagsHelper->getTagNames($tagsIds); + + $item->tags = array_combine($tagsIds, $tagsNames); + } else { + $item->tags = []; + } + + if (isset($item->image)) { + $item->image = ContentHelper::resolve($item->image); + } + + return parent::prepareItem($item); + } } diff --git a/api/components/com_content/src/Controller/ArticlesController.php b/api/components/com_content/src/Controller/ArticlesController.php index bf616d451a866..6e5bac20f5ef9 100644 --- a/api/components/com_content/src/Controller/ArticlesController.php +++ b/api/components/com_content/src/Controller/ArticlesController.php @@ -1,4 +1,5 @@ input->get('filter', [], 'array'); - $filter = InputFilter::getInstance(); - - if (\array_key_exists('author', $apiFilterInfo)) - { - $this->modelState->set('filter.author_id', $filter->clean($apiFilterInfo['author'], 'INT')); - } - - if (\array_key_exists('category', $apiFilterInfo)) - { - $this->modelState->set('filter.category_id', $filter->clean($apiFilterInfo['category'], 'INT')); - } - - if (\array_key_exists('search', $apiFilterInfo)) - { - $this->modelState->set('filter.search', $filter->clean($apiFilterInfo['search'], 'STRING')); - } - - if (\array_key_exists('state', $apiFilterInfo)) - { - $this->modelState->set('filter.published', $filter->clean($apiFilterInfo['state'], 'INT')); - } - - if (\array_key_exists('language', $apiFilterInfo)) - { - $this->modelState->set('filter.language', $filter->clean($apiFilterInfo['language'], 'STRING')); - } - - $apiListInfo = $this->input->get('list', [], 'array'); - - if (array_key_exists('ordering', $apiListInfo)) - { - $this->modelState->set('list.ordering', $filter->clean($apiListInfo['ordering'], 'STRING')); - } - - if (array_key_exists('direction', $apiListInfo)) - { - $this->modelState->set('list.direction', $filter->clean($apiListInfo['direction'], 'STRING')); - } - - return parent::displayList(); - } - - /** - * Method to allow extended classes to manipulate the data to be saved for an extension. - * - * @param array $data An array of input data. - * - * @return array - * - * @since 4.0.0 - */ - protected function preprocessSaveData(array $data): array - { - foreach (FieldsHelper::getFields('com_content.article') as $field) - { - if (isset($data[$field->name])) - { - !isset($data['com_fields']) && $data['com_fields'] = []; - - $data['com_fields'][$field->name] = $data[$field->name]; - unset($data[$field->name]); - } - } - - return $data; - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'articles'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'articles'; + + /** + * Article list view amended to add filtering of data + * + * @return static A BaseController object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $apiFilterInfo = $this->input->get('filter', [], 'array'); + $filter = InputFilter::getInstance(); + + if (\array_key_exists('author', $apiFilterInfo)) { + $this->modelState->set('filter.author_id', $filter->clean($apiFilterInfo['author'], 'INT')); + } + + if (\array_key_exists('category', $apiFilterInfo)) { + $this->modelState->set('filter.category_id', $filter->clean($apiFilterInfo['category'], 'INT')); + } + + if (\array_key_exists('search', $apiFilterInfo)) { + $this->modelState->set('filter.search', $filter->clean($apiFilterInfo['search'], 'STRING')); + } + + if (\array_key_exists('state', $apiFilterInfo)) { + $this->modelState->set('filter.published', $filter->clean($apiFilterInfo['state'], 'INT')); + } + + if (\array_key_exists('language', $apiFilterInfo)) { + $this->modelState->set('filter.language', $filter->clean($apiFilterInfo['language'], 'STRING')); + } + + $apiListInfo = $this->input->get('list', [], 'array'); + + if (array_key_exists('ordering', $apiListInfo)) { + $this->modelState->set('list.ordering', $filter->clean($apiListInfo['ordering'], 'STRING')); + } + + if (array_key_exists('direction', $apiListInfo)) { + $this->modelState->set('list.direction', $filter->clean($apiListInfo['direction'], 'STRING')); + } + + return parent::displayList(); + } + + /** + * Method to allow extended classes to manipulate the data to be saved for an extension. + * + * @param array $data An array of input data. + * + * @return array + * + * @since 4.0.0 + */ + protected function preprocessSaveData(array $data): array + { + foreach (FieldsHelper::getFields('com_content.article') as $field) { + if (isset($data[$field->name])) { + !isset($data['com_fields']) && $data['com_fields'] = []; + + $data['com_fields'][$field->name] = $data[$field->name]; + unset($data[$field->name]); + } + } + + return $data; + } } diff --git a/api/components/com_content/src/Helper/ContentHelper.php b/api/components/com_content/src/Helper/ContentHelper.php index d7e64de506108..60cfd4681b387 100644 --- a/api/components/com_content/src/Helper/ContentHelper.php +++ b/api/components/com_content/src/Helper/ContentHelper.php @@ -1,4 +1,5 @@ type); - - foreach ($model->associations as $association) - { - $resources[] = (new Resource($association, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/content/articles/' . $association->id)); - } - - $collection = new Collection($resources, $serializer); - - return new Relationship($collection); - } - - /** - * Build category relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function category($model) - { - $serializer = new JoomlaSerializer('categories'); - - $resource = (new Resource($model->catid, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/content/categories/' . $model->catid)); - - return new Relationship($resource); - } - - /** - * Build category relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function createdBy($model) - { - $serializer = new JoomlaSerializer('users'); - - $resource = (new Resource($model->created_by, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->created_by)); - - return new Relationship($resource); - } - - /** - * Build editor relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function modifiedBy($model) - { - $serializer = new JoomlaSerializer('users'); - - $resource = (new Resource($model->modified_by, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->modified_by)); - - return new Relationship($resource); - } + /** + * Build content relationships by associations + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function languageAssociations($model) + { + $resources = []; + + // @todo: This can't be hardcoded in the future? + $serializer = new JoomlaSerializer($this->type); + + foreach ($model->associations as $association) { + $resources[] = (new Resource($association, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/content/articles/' . $association->id)); + } + + $collection = new Collection($resources, $serializer); + + return new Relationship($collection); + } + + /** + * Build category relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function category($model) + { + $serializer = new JoomlaSerializer('categories'); + + $resource = (new Resource($model->catid, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/content/categories/' . $model->catid)); + + return new Relationship($resource); + } + + /** + * Build category relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function createdBy($model) + { + $serializer = new JoomlaSerializer('users'); + + $resource = (new Resource($model->created_by, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->created_by)); + + return new Relationship($resource); + } + + /** + * Build editor relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function modifiedBy($model) + { + $serializer = new JoomlaSerializer('users'); + + $resource = (new Resource($model->modified_by, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->modified_by)); + + return new Relationship($resource); + } } diff --git a/api/components/com_content/src/View/Articles/JsonapiView.php b/api/components/com_content/src/View/Articles/JsonapiView.php index 6b4170a544495..6feed013eb735 100644 --- a/api/components/com_content/src/View/Articles/JsonapiView.php +++ b/api/components/com_content/src/View/Articles/JsonapiView.php @@ -1,4 +1,5 @@ serializer = new ContentSerializer($config['contentType']); - } - - parent::__construct($config); - } - - /** - * Execute and display a template script. - * - * @param array|null $items Array of items - * - * @return string - * - * @since 4.0.0 - */ - public function displayList(array $items = null) - { - foreach (FieldsHelper::getFields('com_content.article') as $field) - { - $this->fieldsToRenderList[] = $field->name; - } - - return parent::displayList(); - } - - /** - * Execute and display a template script. - * - * @param object $item Item - * - * @return string - * - * @since 4.0.0 - */ - public function displayItem($item = null) - { - $this->relationship[] = 'modified_by'; - - foreach (FieldsHelper::getFields('com_content.article') as $field) - { - $this->fieldsToRenderItem[] = $field->name; - } - - if (Multilanguage::isEnabled()) - { - $this->fieldsToRenderItem[] = 'languageAssociations'; - $this->relationship[] = 'languageAssociations'; - } - - return parent::displayItem(); - } - - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - $item->text = $item->introtext . ' ' . $item->fulltext; - - // Process the content plugins. - PluginHelper::importPlugin('content'); - Factory::getApplication()->triggerEvent('onContentPrepare', ['com_content.article', &$item, &$item->params]); - - foreach (FieldsHelper::getFields('com_content.article', $item, true) as $field) - { - $item->{$field->name} = $field->apivalue ?? $field->rawvalue; - } - - if (Multilanguage::isEnabled() && !empty($item->associations)) - { - $associations = []; - - foreach ($item->associations as $language => $association) - { - $itemId = explode(':', $association)[0]; - - $associations[] = (object) [ - 'id' => $itemId, - 'language' => $language, - ]; - } - - $item->associations = $associations; - } - - if (!empty($item->tags->tags)) - { - $tagsIds = explode(',', $item->tags->tags); - $tagsNames = $item->tagsHelper->getTagNames($tagsIds); - - $item->tags = array_combine($tagsIds, $tagsNames); - } - else - { - $item->tags = []; - } - - if (isset($item->images)) - { - $registry = new Registry($item->images); - $item->images = $registry->toArray(); - - if (!empty($item->images['image_intro'])) - { - $item->images['image_intro'] = ContentHelper::resolve($item->images['image_intro']); - } - - if (!empty($item->images['image_fulltext'])) - { - $item->images['image_fulltext'] = ContentHelper::resolve($item->images['image_fulltext']); - } - } - - return parent::prepareItem($item); - } + /** + * The fields to render item in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderItem = [ + 'id', + 'typeAlias', + 'asset_id', + 'title', + 'text', + 'tags', + 'language', + 'state', + 'category', + 'images', + 'metakey', + 'metadesc', + 'metadata', + 'access', + 'featured', + 'alias', + 'note', + 'publish_up', + 'publish_down', + 'urls', + 'created', + 'created_by', + 'created_by_alias', + 'modified', + 'modified_by', + 'hits', + 'version', + 'featured_up', + 'featured_down', + ]; + + /** + * The fields to render items in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderList = [ + 'id', + 'typeAlias', + 'asset_id', + 'title', + 'text', + 'tags', + 'language', + 'state', + 'category', + 'images', + 'metakey', + 'metadesc', + 'metadata', + 'access', + 'featured', + 'alias', + 'note', + 'publish_up', + 'publish_down', + 'urls', + 'created', + 'created_by', + 'created_by_alias', + 'modified', + 'hits', + 'version', + 'featured_up', + 'featured_down', + ]; + + /** + * The relationships the item has + * + * @var array + * @since 4.0.0 + */ + protected $relationship = [ + 'category', + 'created_by', + 'tags', + ]; + + /** + * Constructor. + * + * @param array $config A named configuration array for object construction. + * contentType: the name (optional) of the content type to use for the serialization + * + * @since 4.0.0 + */ + public function __construct($config = []) + { + if (\array_key_exists('contentType', $config)) { + $this->serializer = new ContentSerializer($config['contentType']); + } + + parent::__construct($config); + } + + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + foreach (FieldsHelper::getFields('com_content.article') as $field) { + $this->fieldsToRenderList[] = $field->name; + } + + return parent::displayList(); + } + + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + $this->relationship[] = 'modified_by'; + + foreach (FieldsHelper::getFields('com_content.article') as $field) { + $this->fieldsToRenderItem[] = $field->name; + } + + if (Multilanguage::isEnabled()) { + $this->fieldsToRenderItem[] = 'languageAssociations'; + $this->relationship[] = 'languageAssociations'; + } + + return parent::displayItem(); + } + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->text = $item->introtext . ' ' . $item->fulltext; + + // Process the content plugins. + PluginHelper::importPlugin('content'); + Factory::getApplication()->triggerEvent('onContentPrepare', ['com_content.article', &$item, &$item->params]); + + foreach (FieldsHelper::getFields('com_content.article', $item, true) as $field) { + $item->{$field->name} = $field->apivalue ?? $field->rawvalue; + } + + if (Multilanguage::isEnabled() && !empty($item->associations)) { + $associations = []; + + foreach ($item->associations as $language => $association) { + $itemId = explode(':', $association)[0]; + + $associations[] = (object) [ + 'id' => $itemId, + 'language' => $language, + ]; + } + + $item->associations = $associations; + } + + if (!empty($item->tags->tags)) { + $tagsIds = explode(',', $item->tags->tags); + $tagsNames = $item->tagsHelper->getTagNames($tagsIds); + + $item->tags = array_combine($tagsIds, $tagsNames); + } else { + $item->tags = []; + } + + if (isset($item->images)) { + $registry = new Registry($item->images); + $item->images = $registry->toArray(); + + if (!empty($item->images['image_intro'])) { + $item->images['image_intro'] = ContentHelper::resolve($item->images['image_intro']); + } + + if (!empty($item->images['image_fulltext'])) { + $item->images['image_fulltext'] = ContentHelper::resolve($item->images['image_fulltext']); + } + } + + return parent::prepareItem($item); + } } diff --git a/api/components/com_contenthistory/src/Controller/HistoryController.php b/api/components/com_contenthistory/src/Controller/HistoryController.php index fba5a7f2dd75e..3bdf1107a07b2 100644 --- a/api/components/com_contenthistory/src/Controller/HistoryController.php +++ b/api/components/com_contenthistory/src/Controller/HistoryController.php @@ -1,4 +1,5 @@ modelState->set('type_alias', $this->getTypeAliasFromInput()); - $this->modelState->set('type_id', $this->getTypeIdFromInput()); - $this->modelState->set('item_id', $this->getTypeAliasFromInput() . '.' . $this->getItemIdFromInput()); - $this->modelState->set('list.ordering', 'h.save_date'); - $this->modelState->set('list.direction', 'DESC'); - - return parent::displayList(); - } - - /** - * Method to edit an existing record. - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function keep() - { - /** @var HistoryModel $model */ - $model = $this->getModel($this->contentType); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $recordId = $this->input->getInt('id'); - - if (!$recordId) - { - throw new Exception\ResourceNotFound(Text::_('JLIB_APPLICATION_ERROR_RECORD'), 404); - } - - $cid = [$recordId]; - - if (!$model->keep($cid)) - { - throw new Exception\Save(Text::plural('COM_CONTENTHISTORY_N_ITEMS_KEEP_TOGGLE', \count($cid))); - } - - return $this; - } - - /** - * Get item id from input - * - * @return string - * - * @since 4.0.0 - */ - private function getItemIdFromInput() - { - return $this->input->exists('id') ? - $this->input->get('id') : $this->input->post->get('id'); - } - - /** - * Get type id from input - * - * @return string - * - * @since 4.0.0 - */ - private function getTypeIdFromInput() - { - return $this->input->exists('type_id') ? - $this->input->get('type_id') : $this->input->post->get('type_id'); - } - - /** - * Get type alias from input - * - * @return string - * - * @since 4.0.0 - */ - private function getTypeAliasFromInput() - { - return $this->input->exists('type_alias') ? - $this->input->get('type_alias') : $this->input->post->get('type_alias'); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'history'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'history'; + + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('type_alias', $this->getTypeAliasFromInput()); + $this->modelState->set('type_id', $this->getTypeIdFromInput()); + $this->modelState->set('item_id', $this->getTypeAliasFromInput() . '.' . $this->getItemIdFromInput()); + $this->modelState->set('list.ordering', 'h.save_date'); + $this->modelState->set('list.direction', 'DESC'); + + return parent::displayList(); + } + + /** + * Method to edit an existing record. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function keep() + { + /** @var HistoryModel $model */ + $model = $this->getModel($this->contentType); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $recordId = $this->input->getInt('id'); + + if (!$recordId) { + throw new Exception\ResourceNotFound(Text::_('JLIB_APPLICATION_ERROR_RECORD'), 404); + } + + $cid = [$recordId]; + + if (!$model->keep($cid)) { + throw new Exception\Save(Text::plural('COM_CONTENTHISTORY_N_ITEMS_KEEP_TOGGLE', \count($cid))); + } + + return $this; + } + + /** + * Get item id from input + * + * @return string + * + * @since 4.0.0 + */ + private function getItemIdFromInput() + { + return $this->input->exists('id') ? + $this->input->get('id') : $this->input->post->get('id'); + } + + /** + * Get type id from input + * + * @return string + * + * @since 4.0.0 + */ + private function getTypeIdFromInput() + { + return $this->input->exists('type_id') ? + $this->input->get('type_id') : $this->input->post->get('type_id'); + } + + /** + * Get type alias from input + * + * @return string + * + * @since 4.0.0 + */ + private function getTypeAliasFromInput() + { + return $this->input->exists('type_alias') ? + $this->input->get('type_alias') : $this->input->post->get('type_alias'); + } } diff --git a/api/components/com_contenthistory/src/View/History/JsonapiView.php b/api/components/com_contenthistory/src/View/History/JsonapiView.php index 686c049c8922b..0b2b4e90ad73c 100644 --- a/api/components/com_contenthistory/src/View/History/JsonapiView.php +++ b/api/components/com_contenthistory/src/View/History/JsonapiView.php @@ -1,4 +1,5 @@ id = $item->version_id; - unset($item->version_id); + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = $item->version_id; + unset($item->version_id); - $item->version_data = (array) json_decode($item->version_data, true); + $item->version_data = (array) json_decode($item->version_data, true); - return parent::prepareItem($item); - } + return parent::prepareItem($item); + } } diff --git a/api/components/com_fields/src/Controller/FieldsController.php b/api/components/com_fields/src/Controller/FieldsController.php index d3611102f6714..f0033416e8c64 100644 --- a/api/components/com_fields/src/Controller/FieldsController.php +++ b/api/components/com_fields/src/Controller/FieldsController.php @@ -1,4 +1,5 @@ modelState->set('filter.context', $this->getContextFromInput()); + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('filter.context', $this->getContextFromInput()); - return parent::displayItem($id); - } + return parent::displayItem($id); + } - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('filter.context', $this->getContextFromInput()); + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('filter.context', $this->getContextFromInput()); - return parent::displayList(); - } + return parent::displayList(); + } - /** - * Get extension from input - * - * @return string - * - * @since 4.0.0 - */ - private function getContextFromInput() - { - return $this->input->exists('context') ? - $this->input->get('context') : $this->input->post->get('context'); - } + /** + * Get extension from input + * + * @return string + * + * @since 4.0.0 + */ + private function getContextFromInput() + { + return $this->input->exists('context') ? + $this->input->get('context') : $this->input->post->get('context'); + } } diff --git a/api/components/com_fields/src/Controller/GroupsController.php b/api/components/com_fields/src/Controller/GroupsController.php index 85e42217ec930..0da11a598d1ee 100644 --- a/api/components/com_fields/src/Controller/GroupsController.php +++ b/api/components/com_fields/src/Controller/GroupsController.php @@ -1,4 +1,5 @@ modelState->set('filter.context', $this->getContextFromInput()); + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('filter.context', $this->getContextFromInput()); - return parent::displayItem($id); - } + return parent::displayItem($id); + } - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('filter.context', $this->getContextFromInput()); + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('filter.context', $this->getContextFromInput()); - return parent::displayList(); - } + return parent::displayList(); + } - /** - * Get extension from input - * - * @return string - * - * @since 4.0.0 - */ - private function getContextFromInput() - { - return $this->input->exists('context') ? - $this->input->get('context') : $this->input->post->get('context'); - } + /** + * Get extension from input + * + * @return string + * + * @since 4.0.0 + */ + private function getContextFromInput() + { + return $this->input->exists('context') ? + $this->input->get('context') : $this->input->post->get('context'); + } } diff --git a/api/components/com_fields/src/View/Fields/JsonapiView.php b/api/components/com_fields/src/View/Fields/JsonapiView.php index 06adcbcf9d51a..3697dba21ac76 100644 --- a/api/components/com_fields/src/View/Fields/JsonapiView.php +++ b/api/components/com_fields/src/View/Fields/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); - $item = $this->prepareItem($model->getItem()); - } + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + if ($item === null) { + /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ + $model = $this->getModel(); + $item = $this->prepareItem($model->getItem()); + } - if ($item->id === null) - { - throw new RouteNotFoundException('Item does not exist'); - } + if ($item->id === null) { + throw new RouteNotFoundException('Item does not exist'); + } - if ($item->context != $this->getModel()->getState('filter.context')) - { - throw new RouteNotFoundException('Item does not exist'); - } + if ($item->context != $this->getModel()->getState('filter.context')) { + throw new RouteNotFoundException('Item does not exist'); + } - return parent::displayItem($item); - } + return parent::displayItem($item); + } } diff --git a/api/components/com_fields/src/View/Groups/JsonapiView.php b/api/components/com_fields/src/View/Groups/JsonapiView.php index 30d6f55e01e47..21acaa1d02dbf 100644 --- a/api/components/com_fields/src/View/Groups/JsonapiView.php +++ b/api/components/com_fields/src/View/Groups/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); - $item = $this->prepareItem($model->getItem()); - } + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + if ($item === null) { + /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ + $model = $this->getModel(); + $item = $this->prepareItem($model->getItem()); + } - if ($item->id === null) - { - throw new RouteNotFoundException('Item does not exist'); - } + if ($item->id === null) { + throw new RouteNotFoundException('Item does not exist'); + } - if ($item->context != $this->getModel()->getState('filter.context')) - { - throw new RouteNotFoundException('Item does not exist'); - } + if ($item->context != $this->getModel()->getState('filter.context')) { + throw new RouteNotFoundException('Item does not exist'); + } - return parent::displayItem($item); - } + return parent::displayItem($item); + } } diff --git a/api/components/com_installer/src/Controller/ManageController.php b/api/components/com_installer/src/Controller/ManageController.php index c87ebc370f990..74879937c6af2 100644 --- a/api/components/com_installer/src/Controller/ManageController.php +++ b/api/components/com_installer/src/Controller/ManageController.php @@ -1,4 +1,5 @@ input->get('core', $this->input->get->get('core')); + /** + * Extension list view amended to add filtering of data + * + * @return static A BaseController object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $requestBool = $this->input->get('core', $this->input->get->get('core')); - if (!\is_null($requestBool) && $requestBool !== 'true' && $requestBool !== 'false') - { - // Send the error response - $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'core'); + if (!\is_null($requestBool) && $requestBool !== 'true' && $requestBool !== 'false') { + // Send the error response + $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'core'); - throw new InvalidParameterException($error, 400, null, 'core'); - } + throw new InvalidParameterException($error, 400, null, 'core'); + } - if (!\is_null($requestBool)) - { - $this->modelState->set('filter.core', ($requestBool === 'true') ? '1' : '0'); - } + if (!\is_null($requestBool)) { + $this->modelState->set('filter.core', ($requestBool === 'true') ? '1' : '0'); + } - $this->modelState->set('filter.status', $this->input->get('status', $this->input->get->get('status', null, 'INT'), 'INT')); - $this->modelState->set('filter.type', $this->input->get('type', $this->input->get->get('type', null, 'STRING'), 'STRING')); + $this->modelState->set('filter.status', $this->input->get('status', $this->input->get->get('status', null, 'INT'), 'INT')); + $this->modelState->set('filter.type', $this->input->get('type', $this->input->get->get('type', null, 'STRING'), 'STRING')); - return parent::displayList(); - } + return parent::displayList(); + } } diff --git a/api/components/com_installer/src/View/Manage/JsonapiView.php b/api/components/com_installer/src/View/Manage/JsonapiView.php index 9cd75874731d7..7a403bbb14f39 100644 --- a/api/components/com_installer/src/View/Manage/JsonapiView.php +++ b/api/components/com_installer/src/View/Manage/JsonapiView.php @@ -1,4 +1,5 @@ id = $item->extension_id; - unset($item->extension_id); + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = $item->extension_id; + unset($item->extension_id); - return $item; - } + return $item; + } } diff --git a/api/components/com_languages/src/Controller/LanguagesController.php b/api/components/com_languages/src/Controller/LanguagesController.php index 5ef456a9d2416..a801185c9494a 100644 --- a/api/components/com_languages/src/Controller/LanguagesController.php +++ b/api/components/com_languages/src/Controller/LanguagesController.php @@ -1,4 +1,5 @@ modelState->set('filter.language', $this->getLanguageFromInput()); - $this->modelState->set('filter.client', $this->getClientFromInput()); - - return parent::displayItem($id); - } - - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('filter.language', $this->getLanguageFromInput()); - $this->modelState->set('filter.client', $this->getClientFromInput()); - - return parent::displayList(); - } - - /** - * Method to save a record. - * - * @param integer $recordKey The primary key of the item (if exists) - * - * @return integer The record ID on success, false on failure - * - * @since 4.0.0 - */ - protected function save($recordKey = null) - { - /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ - $model = $this->getModel(Inflector::singularize($this->contentType)); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $model->setState('filter.language', $this->input->post->get('lang_code')); - $model->setState('filter.client', $this->input->post->get('app')); - - $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); - - // @todo: Not the cleanest thing ever but it works... - Form::addFormPath(JPATH_COMPONENT_ADMINISTRATOR . '/forms'); - - // Validate the posted data. - $form = $model->getForm($data, false); - - if (!$form) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_FORM_CREATE')); - } - - // Test whether the data is valid. - $validData = $model->validate($form, $data); - - // Check for validation errors. - if ($validData === false) - { - $errors = $model->getErrors(); - $messages = []; - - for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $messages[] = "{$errors[$i]->getMessage()}"; - } - else - { - $messages[] = "{$errors[$i]}"; - } - } - - throw new InvalidParameterException(implode("\n", $messages)); - } - - if (!isset($validData['tags'])) - { - $validData['tags'] = []; - } - - if (!$model->save($validData)) - { - throw new Exception\Save(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError())); - } - - return $validData['key']; - } - - /** - * Removes an item. - * - * @param integer $id The primary key to delete item. - * - * @return void - * - * @since 4.0.0 - */ - public function delete($id = null) - { - $id = $this->input->get('id', '', 'string'); - - $this->input->set('model', $this->contentType); - - $this->modelState->set('filter.language', $this->getLanguageFromInput()); - $this->modelState->set('filter.client', $this->getClientFromInput()); - - parent::delete($id); - } - - /** - * Get client code from input - * - * @return string - * - * @since 4.0.0 - */ - private function getClientFromInput() - { - return $this->input->exists('app') ? $this->input->get('app') : $this->input->post->get('app'); - } - - /** - * Get language code from input - * - * @return string - * - * @since 4.0.0 - */ - private function getLanguageFromInput() - { - return $this->input->exists('lang_code') ? - $this->input->get('lang_code') : $this->input->post->get('lang_code'); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'overrides'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'overrides'; + + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('filter.language', $this->getLanguageFromInput()); + $this->modelState->set('filter.client', $this->getClientFromInput()); + + return parent::displayItem($id); + } + + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('filter.language', $this->getLanguageFromInput()); + $this->modelState->set('filter.client', $this->getClientFromInput()); + + return parent::displayList(); + } + + /** + * Method to save a record. + * + * @param integer $recordKey The primary key of the item (if exists) + * + * @return integer The record ID on success, false on failure + * + * @since 4.0.0 + */ + protected function save($recordKey = null) + { + /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ + $model = $this->getModel(Inflector::singularize($this->contentType)); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $model->setState('filter.language', $this->input->post->get('lang_code')); + $model->setState('filter.client', $this->input->post->get('app')); + + $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); + + // @todo: Not the cleanest thing ever but it works... + Form::addFormPath(JPATH_COMPONENT_ADMINISTRATOR . '/forms'); + + // Validate the posted data. + $form = $model->getForm($data, false); + + if (!$form) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_FORM_CREATE')); + } + + // Test whether the data is valid. + $validData = $model->validate($form, $data); + + // Check for validation errors. + if ($validData === false) { + $errors = $model->getErrors(); + $messages = []; + + for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $messages[] = "{$errors[$i]->getMessage()}"; + } else { + $messages[] = "{$errors[$i]}"; + } + } + + throw new InvalidParameterException(implode("\n", $messages)); + } + + if (!isset($validData['tags'])) { + $validData['tags'] = []; + } + + if (!$model->save($validData)) { + throw new Exception\Save(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError())); + } + + return $validData['key']; + } + + /** + * Removes an item. + * + * @param integer $id The primary key to delete item. + * + * @return void + * + * @since 4.0.0 + */ + public function delete($id = null) + { + $id = $this->input->get('id', '', 'string'); + + $this->input->set('model', $this->contentType); + + $this->modelState->set('filter.language', $this->getLanguageFromInput()); + $this->modelState->set('filter.client', $this->getClientFromInput()); + + parent::delete($id); + } + + /** + * Get client code from input + * + * @return string + * + * @since 4.0.0 + */ + private function getClientFromInput() + { + return $this->input->exists('app') ? $this->input->get('app') : $this->input->post->get('app'); + } + + /** + * Get language code from input + * + * @return string + * + * @since 4.0.0 + */ + private function getLanguageFromInput() + { + return $this->input->exists('lang_code') ? + $this->input->get('lang_code') : $this->input->post->get('lang_code'); + } } diff --git a/api/components/com_languages/src/Controller/StringsController.php b/api/components/com_languages/src/Controller/StringsController.php index 4bdd554f7040e..656ceb4bb29b2 100644 --- a/api/components/com_languages/src/Controller/StringsController.php +++ b/api/components/com_languages/src/Controller/StringsController.php @@ -1,4 +1,5 @@ input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); - - if (!isset($data['searchstring']) || !\is_string($data['searchstring'])) - { - throw new InvalidParameterException("Invalid param 'searchstring'"); - } - - if (!isset($data['searchtype']) || !\in_array($data['searchtype'], ['constant', 'value'])) - { - throw new InvalidParameterException("Invalid param 'searchtype'"); - } - - $app = Factory::getApplication(); - $app->input->set('searchstring', $data['searchstring']); - $app->input->set('searchtype', $data['searchtype']); - $app->input->set('more', 0); - - $viewType = $this->app->getDocument()->getType(); - $viewName = $this->input->get('view', $this->default_view); - $viewLayout = $this->input->get('layout', 'default', 'string'); - - try - { - /** @var \Joomla\Component\Languages\Api\View\Strings\JsonapiView $view */ - $view = $this->getView( - $viewName, - $viewType, - '', - ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] - ); - } - catch (\Exception $e) - { - throw new \RuntimeException($e->getMessage()); - } - - /** @var \Joomla\Component\Languages\Administrator\Model\StringsModel $model */ - $model = $this->getModel($this->contentType, '', ['ignore_request' => true]); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - // Push the model into the view (as default) - $view->setModel($model, true); - - $view->document = $this->app->getDocument(); - $view->displayList(); - - return $this; - } - - /** - * Refresh cache - * - * @return static A \JControllerLegacy object to support chaining. - * - * @throws \Exception - * @since 4.0.0 - */ - public function refresh() - { - /** @var \Joomla\Component\Languages\Administrator\Model\StringsModel $model */ - $model = $this->getModel($this->contentType, '', ['ignore_request' => true]); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $result = $model->refresh(); - - if ($result instanceof \Exception) - { - throw $result; - } - - return $this; - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'strings'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'strings'; + + /** + * Search by languages constants + * + * @return static A \JControllerLegacy object to support chaining. + * + * @throws InvalidParameterException + * @since 4.0.0 + */ + public function search() + { + $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); + + if (!isset($data['searchstring']) || !\is_string($data['searchstring'])) { + throw new InvalidParameterException("Invalid param 'searchstring'"); + } + + if (!isset($data['searchtype']) || !\in_array($data['searchtype'], ['constant', 'value'])) { + throw new InvalidParameterException("Invalid param 'searchtype'"); + } + + $app = Factory::getApplication(); + $app->input->set('searchstring', $data['searchstring']); + $app->input->set('searchtype', $data['searchtype']); + $app->input->set('more', 0); + + $viewType = $this->app->getDocument()->getType(); + $viewName = $this->input->get('view', $this->default_view); + $viewLayout = $this->input->get('layout', 'default', 'string'); + + try { + /** @var \Joomla\Component\Languages\Api\View\Strings\JsonapiView $view */ + $view = $this->getView( + $viewName, + $viewType, + '', + ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] + ); + } catch (\Exception $e) { + throw new \RuntimeException($e->getMessage()); + } + + /** @var \Joomla\Component\Languages\Administrator\Model\StringsModel $model */ + $model = $this->getModel($this->contentType, '', ['ignore_request' => true]); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + // Push the model into the view (as default) + $view->setModel($model, true); + + $view->document = $this->app->getDocument(); + $view->displayList(); + + return $this; + } + + /** + * Refresh cache + * + * @return static A \JControllerLegacy object to support chaining. + * + * @throws \Exception + * @since 4.0.0 + */ + public function refresh() + { + /** @var \Joomla\Component\Languages\Administrator\Model\StringsModel $model */ + $model = $this->getModel($this->contentType, '', ['ignore_request' => true]); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $result = $model->refresh(); + + if ($result instanceof \Exception) { + throw $result; + } + + return $this; + } } diff --git a/api/components/com_languages/src/View/Languages/JsonapiView.php b/api/components/com_languages/src/View/Languages/JsonapiView.php index 803a9896d22c0..9d7f03ffaa111 100644 --- a/api/components/com_languages/src/View/Languages/JsonapiView.php +++ b/api/components/com_languages/src/View/Languages/JsonapiView.php @@ -1,4 +1,5 @@ id = $item->lang_id; - unset($item->lang->id); + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = $item->lang_id; + unset($item->lang->id); - return parent::prepareItem($item); - } + return parent::prepareItem($item); + } } diff --git a/api/components/com_languages/src/View/Overrides/JsonapiView.php b/api/components/com_languages/src/View/Overrides/JsonapiView.php index eb23f85d65048..b346b117990fa 100644 --- a/api/components/com_languages/src/View/Overrides/JsonapiView.php +++ b/api/components/com_languages/src/View/Overrides/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); - $id = $model->getState($model->getName() . '.id'); - $item = $this->prepareItem($model->getItem($id)); + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + /** @var \Joomla\Component\Languages\Administrator\Model\OverrideModel $model */ + $model = $this->getModel(); + $id = $model->getState($model->getName() . '.id'); + $item = $this->prepareItem($model->getItem($id)); - return parent::displayItem($item); - } - /** - * Execute and display a template script. - * - * @param array|null $items Array of items - * - * @return string - * - * @since 4.0.0 - */ - public function displayList(array $items = null) - { - /** @var \Joomla\Component\Languages\Administrator\Model\OverridesModel $model */ - $model = $this->getModel(); - $items = []; + return parent::displayItem($item); + } + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + /** @var \Joomla\Component\Languages\Administrator\Model\OverridesModel $model */ + $model = $this->getModel(); + $items = []; - foreach ($model->getOverrides() as $key => $override) - { - $item = (object) [ - 'key' => $key, - 'override' => $override, - ]; + foreach ($model->getOverrides() as $key => $override) { + $item = (object) [ + 'key' => $key, + 'override' => $override, + ]; - $items[] = $this->prepareItem($item); - } + $items[] = $this->prepareItem($item); + } - return parent::displayList($items); - } + return parent::displayList($items); + } - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - $item->id = $item->key; - $item->value = $item->override; - unset($item->key); - unset($item->override); + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = $item->key; + $item->value = $item->override; + unset($item->key); + unset($item->override); - return parent::prepareItem($item); - } + return parent::prepareItem($item); + } } diff --git a/api/components/com_languages/src/View/Strings/JsonapiView.php b/api/components/com_languages/src/View/Strings/JsonapiView.php index 73766eec29132..2d6d36e6b54a9 100644 --- a/api/components/com_languages/src/View/Strings/JsonapiView.php +++ b/api/components/com_languages/src/View/Strings/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); - $result = $model->search(); + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + /** @var \Joomla\Component\Languages\Administrator\Model\StringsModel $model */ + $model = $this->getModel(); + $result = $model->search(); - if ($result instanceof \Exception) - { - throw $result; - } + if ($result instanceof \Exception) { + throw $result; + } - $items = []; + $items = []; - foreach ($result['results'] as $item) - { - $items[] = $this->prepareItem($item); - } + foreach ($result['results'] as $item) { + $items[] = $this->prepareItem($item); + } - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } - if ($this->type === null) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_CONTENT_TYPE_MISSING'), 400); - } + if ($this->type === null) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_CONTENT_TYPE_MISSING'), 400); + } - $collection = (new Collection($items, new JoomlaSerializer($this->type))) - ->fields([$this->type => $this->fieldsToRenderList]); + $collection = (new Collection($items, new JoomlaSerializer($this->type))) + ->fields([$this->type => $this->fieldsToRenderList]); - // Set the data into the document and render it - $this->document->setData($collection); + // Set the data into the document and render it + $this->document->setData($collection); - return $this->document->render(); - } + return $this->document->render(); + } - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - $item->id = $item->constant; - unset($item->constant); + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = $item->constant; + unset($item->constant); - return parent::prepareItem($item); - } + return parent::prepareItem($item); + } } diff --git a/api/components/com_menus/src/Controller/ItemsController.php b/api/components/com_menus/src/Controller/ItemsController.php index 44f5da57bab61..a958cd4ca322b 100644 --- a/api/components/com_menus/src/Controller/ItemsController.php +++ b/api/components/com_menus/src/Controller/ItemsController.php @@ -1,4 +1,5 @@ modelState->set('filter.client_id', $this->getClientIdFromInput()); - - return parent::displayItem($id); - } - - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); - - return parent::displayList(); - } - - /** - * Method to add a new record. - * - * @return void - * - * @since 4.0.0 - * @throws NotAllowed - * @throws \RuntimeException - */ - public function add() - { - $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); - - if (isset($data['menutype'])) - { - $this->input->set('menutype', $data['menutype']); - $this->input->set('com_menus.items.menutype', $data['menutype']); - } - - isset($data['type']) && $this->input->set('type', $data['type']); - isset($data['parent_id']) && $this->input->set('parent_id', $data['parent_id']); - isset($data['link']) && $this->input->set('link', $data['link']); - - $this->input->set('id', '0'); - - parent::add(); - } - - /** - * Method to edit an existing record. - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function edit() - { - $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); - - if (isset($data['menutype'])) - { - $this->input->set('menutype', $data['menutype']); - $this->input->set('com_menus.items.menutype', $data['menutype']); - } - - isset($data['type']) && $this->input->set('type', $data['type']); - isset($data['parent_id']) && $this->input->set('parent_id', $data['parent_id']); - isset($data['link']) && $this->input->set('link', $data['link']); - - return parent::edit(); - } - - /** - * Return menu items types - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function getTypes() - { - $viewType = $this->app->getDocument()->getType(); - $viewName = $this->input->get('view', $this->default_view); - $viewLayout = $this->input->get('layout', 'default', 'string'); - - try - { - /** @var JsonapiView $view */ - $view = $this->getView( - $viewName, - $viewType, - '', - ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] - ); - } - catch (\Exception $e) - { - throw new \RuntimeException($e->getMessage()); - } - - /** @var ListModel $model */ - $model = $this->getModel('menutypes', '', ['ignore_request' => true]); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $model->setState('client_id', $this->getClientIdFromInput()); - - $view->setModel($model, true); - - $view->document = $this->app->getDocument(); - - $view->displayListTypes(); - - return $this; - } - - /** - * Get client id from input - * - * @return string - * - * @since 4.0.0 - */ - private function getClientIdFromInput() - { - return $this->input->exists('client_id') ? - $this->input->get('client_id') : $this->input->post->get('client_id'); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'items'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'items'; + + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); + + return parent::displayItem($id); + } + + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); + + return parent::displayList(); + } + + /** + * Method to add a new record. + * + * @return void + * + * @since 4.0.0 + * @throws NotAllowed + * @throws \RuntimeException + */ + public function add() + { + $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); + + if (isset($data['menutype'])) { + $this->input->set('menutype', $data['menutype']); + $this->input->set('com_menus.items.menutype', $data['menutype']); + } + + isset($data['type']) && $this->input->set('type', $data['type']); + isset($data['parent_id']) && $this->input->set('parent_id', $data['parent_id']); + isset($data['link']) && $this->input->set('link', $data['link']); + + $this->input->set('id', '0'); + + parent::add(); + } + + /** + * Method to edit an existing record. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function edit() + { + $data = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array'); + + if (isset($data['menutype'])) { + $this->input->set('menutype', $data['menutype']); + $this->input->set('com_menus.items.menutype', $data['menutype']); + } + + isset($data['type']) && $this->input->set('type', $data['type']); + isset($data['parent_id']) && $this->input->set('parent_id', $data['parent_id']); + isset($data['link']) && $this->input->set('link', $data['link']); + + return parent::edit(); + } + + /** + * Return menu items types + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function getTypes() + { + $viewType = $this->app->getDocument()->getType(); + $viewName = $this->input->get('view', $this->default_view); + $viewLayout = $this->input->get('layout', 'default', 'string'); + + try { + /** @var JsonapiView $view */ + $view = $this->getView( + $viewName, + $viewType, + '', + ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] + ); + } catch (\Exception $e) { + throw new \RuntimeException($e->getMessage()); + } + + /** @var ListModel $model */ + $model = $this->getModel('menutypes', '', ['ignore_request' => true]); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $model->setState('client_id', $this->getClientIdFromInput()); + + $view->setModel($model, true); + + $view->document = $this->app->getDocument(); + + $view->displayListTypes(); + + return $this; + } + + /** + * Get client id from input + * + * @return string + * + * @since 4.0.0 + */ + private function getClientIdFromInput() + { + return $this->input->exists('client_id') ? + $this->input->get('client_id') : $this->input->post->get('client_id'); + } } diff --git a/api/components/com_menus/src/Controller/MenusController.php b/api/components/com_menus/src/Controller/MenusController.php index b7424ff70b22f..4a72fb70ea26b 100644 --- a/api/components/com_menus/src/Controller/MenusController.php +++ b/api/components/com_menus/src/Controller/MenusController.php @@ -1,4 +1,5 @@ modelState->set('filter.client_id', $this->getClientIdFromInput()); + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); - return parent::displayItem($id); - } + return parent::displayItem($id); + } - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); - return parent::displayList(); - } + return parent::displayList(); + } - /** - * Get client id from input - * - * @return string - * - * @since 4.0.0 - */ - private function getClientIdFromInput() - { - return $this->input->exists('client_id') ? - $this->input->get('client_id') : $this->input->post->get('client_id'); - } + /** + * Get client id from input + * + * @return string + * + * @since 4.0.0 + */ + private function getClientIdFromInput() + { + return $this->input->exists('client_id') ? + $this->input->get('client_id') : $this->input->post->get('client_id'); + } } diff --git a/api/components/com_menus/src/View/Items/JsonapiView.php b/api/components/com_menus/src/View/Items/JsonapiView.php index 02b5b8d0920f8..002a34fdf4a0a 100644 --- a/api/components/com_menus/src/View/Items/JsonapiView.php +++ b/api/components/com_menus/src/View/Items/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); - $items = []; - - foreach ($model->getTypeOptions() as $type => $data) - { - $groupItems = []; - - foreach ($data as $item) - { - $item->id = implode('/', $item->request); - $item->title = Text::_($item->title); - $item->description = Text::_($item->description); - $item->group = Text::_($type); - - $groupItems[] = $item; - } - - $items = array_merge($items, $groupItems); - } - - // Set up links for pagination - $currentUrl = Uri::getInstance(); - $currentPageDefaultInformation = ['offset' => 0, 'limit' => 20]; - $currentPageQuery = $currentUrl->getVar('page', $currentPageDefaultInformation); - - $offset = $currentPageQuery['offset']; - $limit = $currentPageQuery['limit']; - $totalItemsCount = \count($items); - $totalPagesAvailable = ceil($totalItemsCount / $limit); - - $items = array_splice($items, $offset, $limit); - - $firstPage = clone $currentUrl; - $firstPageQuery = $currentPageQuery; - $firstPageQuery['offset'] = 0; - $firstPage->setVar('page', $firstPageQuery); - - $nextPage = clone $currentUrl; - $nextPageQuery = $currentPageQuery; - $nextOffset = $currentPageQuery['offset'] + $limit; - $nextPageQuery['offset'] = ($nextOffset > ($totalPagesAvailable * $limit)) ? $totalPagesAvailable - $limit : $nextOffset; - $nextPage->setVar('page', $nextPageQuery); - - $previousPage = clone $currentUrl; - $previousPageQuery = $currentPageQuery; - $previousOffset = $currentPageQuery['offset'] - $limit; - $previousPageQuery['offset'] = $previousOffset >= 0 ? $previousOffset : 0; - $previousPage->setVar('page', $previousPageQuery); - - $lastPage = clone $currentUrl; - $lastPageQuery = $currentPageQuery; - $lastPageQuery['offset'] = $totalPagesAvailable - $limit; - $lastPage->setVar('page', $lastPageQuery); - - $collection = (new Collection($items, new JoomlaSerializer('menutypes'))); - - // Set the data into the document and render it - $this->document->addMeta('total-pages', $totalPagesAvailable) - ->setData($collection) - ->addLink('self', (string) $currentUrl) - ->addLink('first', (string) $firstPage) - ->addLink('next', (string) $nextPage) - ->addLink('previous', (string) $previousPage) - ->addLink('last', (string) $lastPage); - - return $this->document->render(); - } - - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - if (\is_string($item->params)) - { - $item->params = json_decode($item->params); - } - - return parent::prepareItem($item); - } + /** + * The fields to render item in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderItem = [ + 'id', + 'parent_id', + 'level', + 'lft', + 'rgt', + 'alias', + 'typeAlias', + 'menutype', + 'title', + 'note', + 'path', + 'link', + 'type', + 'published', + 'component_id', + 'checked_out', + 'checked_out_time', + 'browserNav', + 'access', + 'img', + 'template_style_id', + 'params', + 'home', + 'language', + 'client_id', + 'publish_up', + 'publish_down', + 'request', + 'associations', + 'menuordering', + ]; + + /** + * The fields to render items in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderList = [ + 'id', + 'menutype', + 'title', + 'alias', + 'note', + 'path', + 'link', + 'type', + 'parent_id', + 'level', + 'a.published', + 'component_id', + 'checked_out', + 'checked_out_time', + 'browserNav', + 'access', + 'img', + 'template_style_id', + 'params', + 'lft', + 'rgt', + 'home', + 'language', + 'client_id', + 'enabled', + 'publish_up', + 'publish_down', + 'published', + 'language_title', + 'language_image', + 'language_sef', + 'editor', + 'componentname', + 'access_level', + 'menutype_id', + 'menutype_title', + 'association', + 'name', + ]; + + /** + * Execute and display a list items types. + * + * @return string + * + * @since 4.0.0 + */ + public function displayListTypes() + { + /** @var \Joomla\Component\Menus\Administrator\Model\MenutypesModel $model */ + $model = $this->getModel(); + $items = []; + + foreach ($model->getTypeOptions() as $type => $data) { + $groupItems = []; + + foreach ($data as $item) { + $item->id = implode('/', $item->request); + $item->title = Text::_($item->title); + $item->description = Text::_($item->description); + $item->group = Text::_($type); + + $groupItems[] = $item; + } + + $items = array_merge($items, $groupItems); + } + + // Set up links for pagination + $currentUrl = Uri::getInstance(); + $currentPageDefaultInformation = ['offset' => 0, 'limit' => 20]; + $currentPageQuery = $currentUrl->getVar('page', $currentPageDefaultInformation); + + $offset = $currentPageQuery['offset']; + $limit = $currentPageQuery['limit']; + $totalItemsCount = \count($items); + $totalPagesAvailable = ceil($totalItemsCount / $limit); + + $items = array_splice($items, $offset, $limit); + + $firstPage = clone $currentUrl; + $firstPageQuery = $currentPageQuery; + $firstPageQuery['offset'] = 0; + $firstPage->setVar('page', $firstPageQuery); + + $nextPage = clone $currentUrl; + $nextPageQuery = $currentPageQuery; + $nextOffset = $currentPageQuery['offset'] + $limit; + $nextPageQuery['offset'] = ($nextOffset > ($totalPagesAvailable * $limit)) ? $totalPagesAvailable - $limit : $nextOffset; + $nextPage->setVar('page', $nextPageQuery); + + $previousPage = clone $currentUrl; + $previousPageQuery = $currentPageQuery; + $previousOffset = $currentPageQuery['offset'] - $limit; + $previousPageQuery['offset'] = $previousOffset >= 0 ? $previousOffset : 0; + $previousPage->setVar('page', $previousPageQuery); + + $lastPage = clone $currentUrl; + $lastPageQuery = $currentPageQuery; + $lastPageQuery['offset'] = $totalPagesAvailable - $limit; + $lastPage->setVar('page', $lastPageQuery); + + $collection = (new Collection($items, new JoomlaSerializer('menutypes'))); + + // Set the data into the document and render it + $this->document->addMeta('total-pages', $totalPagesAvailable) + ->setData($collection) + ->addLink('self', (string) $currentUrl) + ->addLink('first', (string) $firstPage) + ->addLink('next', (string) $nextPage) + ->addLink('previous', (string) $previousPage) + ->addLink('last', (string) $lastPage); + + return $this->document->render(); + } + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + if (\is_string($item->params)) { + $item->params = json_decode($item->params); + } + + return parent::prepareItem($item); + } } diff --git a/api/components/com_menus/src/View/Menus/JsonapiView.php b/api/components/com_menus/src/View/Menus/JsonapiView.php index 0750e5a37febd..6e8f21cb91172 100644 --- a/api/components/com_menus/src/View/Menus/JsonapiView.php +++ b/api/components/com_menus/src/View/Menus/JsonapiView.php @@ -1,4 +1,5 @@ id = $item->message_id; - unset($item->message_id); + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = $item->message_id; + unset($item->message_id); - return parent::prepareItem($item); - } + return parent::prepareItem($item); + } } diff --git a/api/components/com_modules/src/Controller/ModulesController.php b/api/components/com_modules/src/Controller/ModulesController.php index bb900e2bbb1dd..4bf825adf70ba 100644 --- a/api/components/com_modules/src/Controller/ModulesController.php +++ b/api/components/com_modules/src/Controller/ModulesController.php @@ -1,4 +1,5 @@ modelState->set('filter.client_id', $this->getClientIdFromInput()); - - return parent::displayItem($id); - } - - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); - - return parent::displayList(); - } - - /** - * Return module items types - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function getTypes() - { - $viewType = $this->app->getDocument()->getType(); - $viewName = $this->input->get('view', $this->default_view); - $viewLayout = $this->input->get('layout', 'default', 'string'); - - try - { - /** @var JsonapiView $view */ - $view = $this->getView( - $viewName, - $viewType, - '', - ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] - ); - } - catch (\Exception $e) - { - throw new \RuntimeException($e->getMessage()); - } - - /** @var SelectModel $model */ - $model = $this->getModel('select', '', ['ignore_request' => true]); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $model->setState('client_id', $this->getClientIdFromInput()); - - $view->setModel($model, true); - - $view->document = $this->app->getDocument(); - - $view->displayListTypes(); - - return $this; - } - - /** - * Get client id from input - * - * @return string - * - * @since 4.0.0 - */ - private function getClientIdFromInput() - { - return $this->input->exists('client_id') ? - $this->input->get('client_id') : $this->input->post->get('client_id'); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'modules'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'modules'; + + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); + + return parent::displayItem($id); + } + + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('filter.client_id', $this->getClientIdFromInput()); + + return parent::displayList(); + } + + /** + * Return module items types + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function getTypes() + { + $viewType = $this->app->getDocument()->getType(); + $viewName = $this->input->get('view', $this->default_view); + $viewLayout = $this->input->get('layout', 'default', 'string'); + + try { + /** @var JsonapiView $view */ + $view = $this->getView( + $viewName, + $viewType, + '', + ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] + ); + } catch (\Exception $e) { + throw new \RuntimeException($e->getMessage()); + } + + /** @var SelectModel $model */ + $model = $this->getModel('select', '', ['ignore_request' => true]); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $model->setState('client_id', $this->getClientIdFromInput()); + + $view->setModel($model, true); + + $view->document = $this->app->getDocument(); + + $view->displayListTypes(); + + return $this; + } + + /** + * Get client id from input + * + * @return string + * + * @since 4.0.0 + */ + private function getClientIdFromInput() + { + return $this->input->exists('client_id') ? + $this->input->get('client_id') : $this->input->post->get('client_id'); + } } diff --git a/api/components/com_modules/src/View/Modules/JsonapiView.php b/api/components/com_modules/src/View/Modules/JsonapiView.php index 0cd3f3f65bc43..dbac55bd59039 100644 --- a/api/components/com_modules/src/View/Modules/JsonapiView.php +++ b/api/components/com_modules/src/View/Modules/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + /** @var \Joomla\CMS\MVC\Model\AdminModel $model */ + $model = $this->getModel(); - if ($item === null) - { - $item = $this->prepareItem($model->getItem()); - } + if ($item === null) { + $item = $this->prepareItem($model->getItem()); + } - if ($item->id === null) - { - throw new RouteNotFoundException('Item does not exist'); - } + if ($item->id === null) { + throw new RouteNotFoundException('Item does not exist'); + } - if ((int) $model->getState('client_id') !== $item->client_id) - { - throw new RouteNotFoundException('Item does not exist'); - } + if ((int) $model->getState('client_id') !== $item->client_id) { + throw new RouteNotFoundException('Item does not exist'); + } - return parent::displayItem($item); - } + return parent::displayItem($item); + } - /** - * Execute and display a list modules types. - * - * @return string - * - * @since 4.0.0 - */ - public function displayListTypes() - { - /** @var SelectModel $model */ - $model = $this->getModel(); - $items = []; + /** + * Execute and display a list modules types. + * + * @return string + * + * @since 4.0.0 + */ + public function displayListTypes() + { + /** @var SelectModel $model */ + $model = $this->getModel(); + $items = []; - foreach ($model->getItems() as $item) - { - $item->id = $item->extension_id; - unset($item->extension_id); + foreach ($model->getItems() as $item) { + $item->id = $item->extension_id; + unset($item->extension_id); - $items[] = $item; - } + $items[] = $item; + } - $this->fieldsToRenderList = ['id', 'name', 'module', 'xml', 'desc']; + $this->fieldsToRenderList = ['id', 'name', 'module', 'xml', 'desc']; - return parent::displayList($items); - } + return parent::displayList($items); + } } diff --git a/api/components/com_newsfeeds/src/Controller/FeedsController.php b/api/components/com_newsfeeds/src/Controller/FeedsController.php index 2612252576691..23034a6fa64ec 100644 --- a/api/components/com_newsfeeds/src/Controller/FeedsController.php +++ b/api/components/com_newsfeeds/src/Controller/FeedsController.php @@ -1,4 +1,5 @@ type); - - foreach ($model->associations as $association) - { - $resources[] = (new Resource($association, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/newsfeeds/feeds/' . $association->id)); - } - - $collection = new Collection($resources, $serializer); - - return new Relationship($collection); - } - - /** - * Build category relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function category($model) - { - $serializer = new JoomlaSerializer('categories'); - - $resource = (new Resource($model->catid, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/newfeeds/categories/' . $model->catid)); - - return new Relationship($resource); - } - - /** - * Build category relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function createdBy($model) - { - $serializer = new JoomlaSerializer('users'); - - $resource = (new Resource($model->created_by, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->created_by)); - - return new Relationship($resource); - } - - /** - * Build editor relationship - * - * @param \stdClass $model Item model - * - * @return Relationship - * - * @since 4.0.0 - */ - public function modifiedBy($model) - { - $serializer = new JoomlaSerializer('users'); - - $resource = (new Resource($model->modified_by, $serializer)) - ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->modified_by)); - - return new Relationship($resource); - } + use TagApiSerializerTrait; + + /** + * Build content relationships by associations + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function languageAssociations($model) + { + $resources = []; + + // @todo: This can't be hardcoded in the future? + $serializer = new JoomlaSerializer($this->type); + + foreach ($model->associations as $association) { + $resources[] = (new Resource($association, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/newsfeeds/feeds/' . $association->id)); + } + + $collection = new Collection($resources, $serializer); + + return new Relationship($collection); + } + + /** + * Build category relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function category($model) + { + $serializer = new JoomlaSerializer('categories'); + + $resource = (new Resource($model->catid, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/newfeeds/categories/' . $model->catid)); + + return new Relationship($resource); + } + + /** + * Build category relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function createdBy($model) + { + $serializer = new JoomlaSerializer('users'); + + $resource = (new Resource($model->created_by, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->created_by)); + + return new Relationship($resource); + } + + /** + * Build editor relationship + * + * @param \stdClass $model Item model + * + * @return Relationship + * + * @since 4.0.0 + */ + public function modifiedBy($model) + { + $serializer = new JoomlaSerializer('users'); + + $resource = (new Resource($model->modified_by, $serializer)) + ->addLink('self', Route::link('site', Uri::root() . 'api/index.php/v1/users/' . $model->modified_by)); + + return new Relationship($resource); + } } diff --git a/api/components/com_newsfeeds/src/View/Feeds/JsonapiView.php b/api/components/com_newsfeeds/src/View/Feeds/JsonapiView.php index 191b642735986..72518fc6c2980 100644 --- a/api/components/com_newsfeeds/src/View/Feeds/JsonapiView.php +++ b/api/components/com_newsfeeds/src/View/Feeds/JsonapiView.php @@ -1,4 +1,5 @@ serializer = new NewsfeedSerializer($config['contentType']); - } - - parent::__construct($config); - } - - /** - * Execute and display a template script. - * - * @param object $item Item - * - * @return string - * - * @since 4.0.0 - */ - public function displayItem($item = null) - { - if (Multilanguage::isEnabled()) - { - $this->fieldsToRenderItem[] = 'languageAssociations'; - $this->relationship[] = 'languageAssociations'; - } - - return parent::displayItem(); - } - - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - if (Multilanguage::isEnabled() && !empty($item->associations)) - { - $associations = []; - - foreach ($item->associations as $language => $association) - { - $itemId = explode(':', $association)[0]; - - $associations[] = (object) [ - 'id' => $itemId, - 'language' => $language, - ]; - } - - $item->associations = $associations; - } - - if (!empty($item->tags->tags)) - { - $tagsIds = explode(',', $item->tags->tags); - $tagsNames = $item->tagsHelper->getTagNames($tagsIds); - - $item->tags = array_combine($tagsIds, $tagsNames); - } - else - { - $item->tags = []; - } - - return parent::prepareItem($item); - } + /** + * The fields to render item in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderItem = [ + 'id', + 'category', + 'name', + 'alias', + 'link', + 'published', + 'numarticles', + 'cache_time', + 'checked_out', + 'checked_out_time', + 'ordering', + 'rtl', + 'access', + 'language', + 'params', + 'created', + 'created_by', + 'created_by_alias', + 'modified', + 'modified_by', + 'metakey', + 'metadesc', + 'metadata', + 'publish_up', + 'publish_down', + 'description', + 'version', + 'hits', + 'images', + 'tags', + ]; + + /** + * The fields to render items in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderList = [ + 'id', + 'name', + 'alias', + 'checked_out', + 'checked_out_time', + 'category', + 'numarticles', + 'cache_time', + 'created_by', + 'published', + 'access', + 'ordering', + 'language', + 'publish_up', + 'publish_down', + 'language_title', + 'language_image', + 'editor', + 'access_level', + 'category_title', + ]; + + /** + * The relationships the item has + * + * @var array + * @since 4.0.0 + */ + protected $relationship = [ + 'category', + 'created_by', + 'modified_by', + 'tags', + ]; + + /** + * Constructor. + * + * @param array $config A named configuration array for object construction. + * contentType: the name (optional) of the content type to use for the serialization + * + * @since 4.0.0 + */ + public function __construct($config = []) + { + if (\array_key_exists('contentType', $config)) { + $this->serializer = new NewsfeedSerializer($config['contentType']); + } + + parent::__construct($config); + } + + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + if (Multilanguage::isEnabled()) { + $this->fieldsToRenderItem[] = 'languageAssociations'; + $this->relationship[] = 'languageAssociations'; + } + + return parent::displayItem(); + } + + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + if (Multilanguage::isEnabled() && !empty($item->associations)) { + $associations = []; + + foreach ($item->associations as $language => $association) { + $itemId = explode(':', $association)[0]; + + $associations[] = (object) [ + 'id' => $itemId, + 'language' => $language, + ]; + } + + $item->associations = $associations; + } + + if (!empty($item->tags->tags)) { + $tagsIds = explode(',', $item->tags->tags); + $tagsNames = $item->tagsHelper->getTagNames($tagsIds); + + $item->tags = array_combine($tagsIds, $tagsNames); + } else { + $item->tags = []; + } + + return parent::prepareItem($item); + } } diff --git a/api/components/com_plugins/src/Controller/PluginsController.php b/api/components/com_plugins/src/Controller/PluginsController.php index 67bfb198175b0..c0668a19a3dc2 100644 --- a/api/components/com_plugins/src/Controller/PluginsController.php +++ b/api/components/com_plugins/src/Controller/PluginsController.php @@ -1,4 +1,5 @@ input->getInt('id'); - - if (!$recordId) - { - throw new Exception\ResourceNotFound(Text::_('JLIB_APPLICATION_ERROR_RECORD'), 404); - } - - $data = json_decode($this->input->json->getRaw(), true); - - foreach ($data as $key => $value) - { - if (!\in_array($key, ['enabled', 'access', 'ordering'])) - { - throw new InvalidParameterException("Invalid parameter {$key}.", 400); - } - } - - /** @var \Joomla\Component\Plugins\Administrator\Model\PluginModel $model */ - $model = $this->getModel(Inflector::singularize($this->contentType), '', ['ignore_request' => true]); - - if (!$model) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); - } - - $item = $model->getItem($recordId); - - if (!isset($item->extension_id)) - { - throw new RouteNotFoundException('Item does not exist'); - } - - $data['folder'] = $item->folder; - $data['element'] = $item->element; - - $this->input->set('data', $data); - - return parent::edit(); - } - - /** - * Plugin list view with filtering of data - * - * @return static A BaseController object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $apiFilterInfo = $this->input->get('filter', [], 'array'); - $filter = InputFilter::getInstance(); - - if (\array_key_exists('element', $apiFilterInfo)) - { - $this->modelState->set('filter.element', $filter->clean($apiFilterInfo['element'], 'STRING')); - } - - if (\array_key_exists('status', $apiFilterInfo)) - { - $this->modelState->set('filter.enabled', $filter->clean($apiFilterInfo['status'], 'INT')); - } - - if (\array_key_exists('search', $apiFilterInfo)) - { - $this->modelState->set('filter.search', $filter->clean($apiFilterInfo['search'], 'STRING')); - } - - if (\array_key_exists('type', $apiFilterInfo)) - { - $this->modelState->set('filter.folder', $filter->clean($apiFilterInfo['type'], 'STRING')); - } - - return parent::displayList(); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'plugins'; + + /** + * The default view for the display method. + * + * @var string + * + * @since 3.0 + */ + protected $default_view = 'plugins'; + + /** + * Method to edit an existing record. + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function edit() + { + $recordId = $this->input->getInt('id'); + + if (!$recordId) { + throw new Exception\ResourceNotFound(Text::_('JLIB_APPLICATION_ERROR_RECORD'), 404); + } + + $data = json_decode($this->input->json->getRaw(), true); + + foreach ($data as $key => $value) { + if (!\in_array($key, ['enabled', 'access', 'ordering'])) { + throw new InvalidParameterException("Invalid parameter {$key}.", 400); + } + } + + /** @var \Joomla\Component\Plugins\Administrator\Model\PluginModel $model */ + $model = $this->getModel(Inflector::singularize($this->contentType), '', ['ignore_request' => true]); + + if (!$model) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE')); + } + + $item = $model->getItem($recordId); + + if (!isset($item->extension_id)) { + throw new RouteNotFoundException('Item does not exist'); + } + + $data['folder'] = $item->folder; + $data['element'] = $item->element; + + $this->input->set('data', $data); + + return parent::edit(); + } + + /** + * Plugin list view with filtering of data + * + * @return static A BaseController object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $apiFilterInfo = $this->input->get('filter', [], 'array'); + $filter = InputFilter::getInstance(); + + if (\array_key_exists('element', $apiFilterInfo)) { + $this->modelState->set('filter.element', $filter->clean($apiFilterInfo['element'], 'STRING')); + } + + if (\array_key_exists('status', $apiFilterInfo)) { + $this->modelState->set('filter.enabled', $filter->clean($apiFilterInfo['status'], 'INT')); + } + + if (\array_key_exists('search', $apiFilterInfo)) { + $this->modelState->set('filter.search', $filter->clean($apiFilterInfo['search'], 'STRING')); + } + + if (\array_key_exists('type', $apiFilterInfo)) { + $this->modelState->set('filter.folder', $filter->clean($apiFilterInfo['type'], 'STRING')); + } + + return parent::displayList(); + } } diff --git a/api/components/com_plugins/src/View/Plugins/JsonapiView.php b/api/components/com_plugins/src/View/Plugins/JsonapiView.php index c26d50b874951..f0c118d30da7e 100644 --- a/api/components/com_plugins/src/View/Plugins/JsonapiView.php +++ b/api/components/com_plugins/src/View/Plugins/JsonapiView.php @@ -1,4 +1,5 @@ id = $item->extension_id; - unset($item->extension_id); + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + $item->id = $item->extension_id; + unset($item->extension_id); - return $item; - } + return $item; + } } diff --git a/api/components/com_privacy/src/Controller/ConsentsController.php b/api/components/com_privacy/src/Controller/ConsentsController.php index 66949d2da7a09..97df5be753978 100644 --- a/api/components/com_privacy/src/Controller/ConsentsController.php +++ b/api/components/com_privacy/src/Controller/ConsentsController.php @@ -1,4 +1,5 @@ input->get('id', 0, 'int'); - } - - $this->input->set('model', $this->contentType); - - return parent::displayItem($id); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'consents'; + + /** + * The default view for the display method. + * + * @var string + * @since 3.0 + */ + protected $default_view = 'consents'; + + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + if ($id === null) { + $id = $this->input->get('id', 0, 'int'); + } + + $this->input->set('model', $this->contentType); + + return parent::displayItem($id); + } } diff --git a/api/components/com_privacy/src/Controller/RequestsController.php b/api/components/com_privacy/src/Controller/RequestsController.php index 772d1b0feeacd..c7716be2fb1bb 100644 --- a/api/components/com_privacy/src/Controller/RequestsController.php +++ b/api/components/com_privacy/src/Controller/RequestsController.php @@ -1,4 +1,5 @@ input->get('id', 0, 'int'); - } + /** + * Export request data + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function export($id = null) + { + if ($id === null) { + $id = $this->input->get('id', 0, 'int'); + } - $viewType = $this->app->getDocument()->getType(); - $viewName = $this->input->get('view', $this->default_view); - $viewLayout = $this->input->get('layout', 'default', 'string'); + $viewType = $this->app->getDocument()->getType(); + $viewName = $this->input->get('view', $this->default_view); + $viewLayout = $this->input->get('layout', 'default', 'string'); - try - { - /** @var JsonapiView $view */ - $view = $this->getView( - $viewName, - $viewType, - '', - ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] - ); - } - catch (\Exception $e) - { - throw new \RuntimeException($e->getMessage()); - } + try { + /** @var JsonapiView $view */ + $view = $this->getView( + $viewName, + $viewType, + '', + ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType] + ); + } catch (\Exception $e) { + throw new \RuntimeException($e->getMessage()); + } - $model = $this->getModel('export'); + $model = $this->getModel('export'); - try - { - $modelName = $model->getName(); - } - catch (\Exception $e) - { - throw new \RuntimeException($e->getMessage()); - } + try { + $modelName = $model->getName(); + } catch (\Exception $e) { + throw new \RuntimeException($e->getMessage()); + } - $model->setState($modelName . '.request_id', $id); + $model->setState($modelName . '.request_id', $id); - $view->setModel($model, true); + $view->setModel($model, true); - $view->document = $this->app->getDocument(); - $view->export(); + $view->document = $this->app->getDocument(); + $view->export(); - return $this; - } + return $this; + } } diff --git a/api/components/com_privacy/src/View/Consents/JsonapiView.php b/api/components/com_privacy/src/View/Consents/JsonapiView.php index 60f8faddeb176..56d6cbc0e97aa 100644 --- a/api/components/com_privacy/src/View/Consents/JsonapiView.php +++ b/api/components/com_privacy/src/View/Consents/JsonapiView.php @@ -1,4 +1,5 @@ get('state')->get($this->getName() . '.id'); - - if ($id === null) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ITEMID_MISSING')); - } - - /** @var \Joomla\CMS\MVC\Model\ListModel $model */ - $model = $this->getModel(); - $displayItem = null; - - foreach ($model->getItems() as $item) - { - $item = $this->prepareItem($item); - - if ($item->id === $id) - { - $displayItem = $item; - break; - } - } - - if ($displayItem === null) - { - throw new RouteNotFoundException('Item does not exist'); - } - - // Check for errors. - if (\count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - if ($this->type === null) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_CONTENT_TYPE_MISSING')); - } - - $serializer = new JoomlaSerializer($this->type); - $element = (new Resource($displayItem, $serializer)) - ->fields([$this->type => $this->fieldsToRenderItem]); - - $this->document->setData($element); - $this->document->addLink('self', Uri::current()); - - return $this->document->render(); - } + /** + * The fields to render item in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderItem = [ + 'id', + 'user_id', + 'state', + 'created', + 'subject', + 'body', + 'remind', + 'token', + 'username', + ]; + + /** + * The fields to render items in the documents + * + * @var array + * @since 4.0.0 + */ + protected $fieldsToRenderList = [ + 'id', + 'user_id', + 'state', + 'created', + 'subject', + 'body', + 'remind', + 'token', + 'username', + ]; + + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + $id = $this->get('state')->get($this->getName() . '.id'); + + if ($id === null) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ITEMID_MISSING')); + } + + /** @var \Joomla\CMS\MVC\Model\ListModel $model */ + $model = $this->getModel(); + $displayItem = null; + + foreach ($model->getItems() as $item) { + $item = $this->prepareItem($item); + + if ($item->id === $id) { + $displayItem = $item; + break; + } + } + + if ($displayItem === null) { + throw new RouteNotFoundException('Item does not exist'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + if ($this->type === null) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_CONTENT_TYPE_MISSING')); + } + + $serializer = new JoomlaSerializer($this->type); + $element = (new Resource($displayItem, $serializer)) + ->fields([$this->type => $this->fieldsToRenderItem]); + + $this->document->setData($element); + $this->document->addLink('self', Uri::current()); + + return $this->document->render(); + } } diff --git a/api/components/com_privacy/src/View/Requests/JsonapiView.php b/api/components/com_privacy/src/View/Requests/JsonapiView.php index 65cdefc6bc8ea..b8d747c3c921a 100644 --- a/api/components/com_privacy/src/View/Requests/JsonapiView.php +++ b/api/components/com_privacy/src/View/Requests/JsonapiView.php @@ -1,4 +1,5 @@ getModel(); + /** + * Execute and display a template script. + * + * @return string + * + * @since 4.0.0 + */ + public function export() + { + /** @var ExportModel $model */ + $model = $this->getModel(); - $exportData = $model->collectDataForExportRequest(); + $exportData = $model->collectDataForExportRequest(); - if ($exportData == false) - { - throw new RouteNotFoundException('Item does not exist'); - } + if ($exportData == false) { + throw new RouteNotFoundException('Item does not exist'); + } - $serializer = new JoomlaSerializer('export'); - $element = (new Resource($exportData, $serializer)); + $serializer = new JoomlaSerializer('export'); + $element = (new Resource($exportData, $serializer)); - $this->document->setData($element); - $this->document->addLink('self', Uri::current()); + $this->document->setData($element); + $this->document->addLink('self', Uri::current()); - return $this->document->render(); - } + return $this->document->render(); + } } diff --git a/api/components/com_redirect/src/Controller/RedirectController.php b/api/components/com_redirect/src/Controller/RedirectController.php index 58a1ba43b6ed1..fd54176ecdb26 100644 --- a/api/components/com_redirect/src/Controller/RedirectController.php +++ b/api/components/com_redirect/src/Controller/RedirectController.php @@ -1,4 +1,5 @@ modelState->set('client_id', $this->getClientIdFromInput()); + /** + * Basic display of an item view + * + * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayItem($id = null) + { + $this->modelState->set('client_id', $this->getClientIdFromInput()); - return parent::displayItem($id); - } + return parent::displayItem($id); + } - /** - * Basic display of a list view - * - * @return static A \JControllerLegacy object to support chaining. - * - * @since 4.0.0 - */ - public function displayList() - { - $this->modelState->set('client_id', $this->getClientIdFromInput()); + /** + * Basic display of a list view + * + * @return static A \JControllerLegacy object to support chaining. + * + * @since 4.0.0 + */ + public function displayList() + { + $this->modelState->set('client_id', $this->getClientIdFromInput()); - return parent::displayList(); - } + return parent::displayList(); + } - /** - * Method to allow extended classes to manipulate the data to be saved for an extension. - * - * @param array $data An array of input data. - * - * @return array - * - * @since 4.0.0 - * @throws InvalidParameterException - */ - protected function preprocessSaveData(array $data): array - { - $data['client_id'] = $this->getClientIdFromInput(); + /** + * Method to allow extended classes to manipulate the data to be saved for an extension. + * + * @param array $data An array of input data. + * + * @return array + * + * @since 4.0.0 + * @throws InvalidParameterException + */ + protected function preprocessSaveData(array $data): array + { + $data['client_id'] = $this->getClientIdFromInput(); - // If we are updating an item the template is a readonly property based on the ID - if ($this->input->getMethod() === 'PATCH') - { - if (\array_key_exists('template', $data)) - { - throw new InvalidParameterException('The template property cannot be modified for an existing style'); - } + // If we are updating an item the template is a readonly property based on the ID + if ($this->input->getMethod() === 'PATCH') { + if (\array_key_exists('template', $data)) { + throw new InvalidParameterException('The template property cannot be modified for an existing style'); + } - $model = $this->getModel(Inflector::singularize($this->contentType), '', ['ignore_request' => true]); - $data['template'] = $model->getItem($this->input->getInt('id'))->template; - } + $model = $this->getModel(Inflector::singularize($this->contentType), '', ['ignore_request' => true]); + $data['template'] = $model->getItem($this->input->getInt('id'))->template; + } - return $data; - } + return $data; + } - /** - * Get client id from input - * - * @return string - * - * @since 4.0.0 - */ - private function getClientIdFromInput() - { - return $this->input->exists('client_id') ? $this->input->get('client_id') : $this->input->post->get('client_id'); - } + /** + * Get client id from input + * + * @return string + * + * @since 4.0.0 + */ + private function getClientIdFromInput() + { + return $this->input->exists('client_id') ? $this->input->get('client_id') : $this->input->post->get('client_id'); + } } diff --git a/api/components/com_templates/src/View/Styles/JsonapiView.php b/api/components/com_templates/src/View/Styles/JsonapiView.php index 880716be6f618..4bbe848a51d8b 100644 --- a/api/components/com_templates/src/View/Styles/JsonapiView.php +++ b/api/components/com_templates/src/View/Styles/JsonapiView.php @@ -1,4 +1,5 @@ client_id != $this->getModel()->getState('client_id')) - { - throw new RouteNotFoundException('Item does not exist'); - } + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + if ($item->client_id != $this->getModel()->getState('client_id')) { + throw new RouteNotFoundException('Item does not exist'); + } - return parent::prepareItem($item); - } + return parent::prepareItem($item); + } } diff --git a/api/components/com_users/src/Controller/GroupsController.php b/api/components/com_users/src/Controller/GroupsController.php index 8c49ef4b9cf73..33cf77b0b4ffd 100644 --- a/api/components/com_users/src/Controller/GroupsController.php +++ b/api/components/com_users/src/Controller/GroupsController.php @@ -1,4 +1,5 @@ name])) - { - !isset($data['com_fields']) && $data['com_fields'] = []; - - $data['com_fields'][$field->name] = $data[$field->name]; - unset($data[$field->name]); - } - } - - if (isset($data['password']) && $this->task !== 'add') - { - $data['password2'] = $data['password']; - } - - return $data; - } - - /** - * User list view with filtering of data - * - * @return static A BaseController object to support chaining. - * - * @since 4.0.0 - * @throws InvalidParameterException - */ - public function displayList() - { - $apiFilterInfo = $this->input->get('filter', [], 'array'); - $filter = InputFilter::getInstance(); - - if (\array_key_exists('state', $apiFilterInfo)) - { - $this->modelState->set('filter.state', $filter->clean($apiFilterInfo['state'], 'INT')); - } - - if (\array_key_exists('active', $apiFilterInfo)) - { - $this->modelState->set('filter.active', $filter->clean($apiFilterInfo['active'], 'INT')); - } - - if (\array_key_exists('groupid', $apiFilterInfo)) - { - $this->modelState->set('filter.group_id', $filter->clean($apiFilterInfo['groupid'], 'INT')); - } - - if (\array_key_exists('search', $apiFilterInfo)) - { - $this->modelState->set('filter.search', $filter->clean($apiFilterInfo['search'], 'STRING')); - } - - if (\array_key_exists('registrationDateStart', $apiFilterInfo)) - { - $registrationStartInput = $filter->clean($apiFilterInfo['registrationDateStart'], 'STRING'); - $registrationStartDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $registrationStartInput); - - if (!$registrationStartDate) - { - // Send the error response - $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'registrationDateStart'); - - throw new InvalidParameterException($error, 400, null, 'registrationDateStart'); - } - - $this->modelState->set('filter.registrationDateStart', $registrationStartDate); - } - - if (\array_key_exists('registrationDateEnd', $apiFilterInfo)) - { - $registrationEndInput = $filter->clean($apiFilterInfo['registrationDateEnd'], 'STRING'); - $registrationEndDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $registrationEndInput); - - if (!$registrationEndDate) - { - // Send the error response - $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'registrationDateEnd'); - throw new InvalidParameterException($error, 400, null, 'registrationDateEnd'); - } - - $this->modelState->set('filter.registrationDateEnd', $registrationEndDate); - } - elseif (\array_key_exists('registrationDateStart', $apiFilterInfo) - && !\array_key_exists('registrationDateEnd', $apiFilterInfo)) - { - // If no end date specified the end date is now - $this->modelState->set('filter.registrationDateEnd', new Date); - } - - if (\array_key_exists('lastVisitDateStart', $apiFilterInfo)) - { - $lastVisitStartInput = $filter->clean($apiFilterInfo['lastVisitDateStart'], 'STRING'); - $lastVisitStartDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $lastVisitStartInput); - - if (!$lastVisitStartDate) - { - // Send the error response - $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'lastVisitDateStart'); - throw new InvalidParameterException($error, 400, null, 'lastVisitDateStart'); - } - - $this->modelState->set('filter.lastVisitStart', $lastVisitStartDate); - } - - if (\array_key_exists('lastVisitDateEnd', $apiFilterInfo)) - { - $lastVisitEndInput = $filter->clean($apiFilterInfo['lastVisitDateEnd'], 'STRING'); - $lastVisitEndDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $lastVisitEndInput); - - if (!$lastVisitEndDate) - { - // Send the error response - $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'lastVisitDateEnd'); - - throw new InvalidParameterException($error, 400, null, 'lastVisitDateEnd'); - } - - $this->modelState->set('filter.lastVisitEnd', $lastVisitEndDate); - } - elseif (\array_key_exists('lastVisitDateStart', $apiFilterInfo) - && !\array_key_exists('lastVisitDateEnd', $apiFilterInfo)) - { - // If no end date specified the end date is now - $this->modelState->set('filter.lastVisitEnd', new Date); - } - - return parent::displayList(); - } + /** + * The content type of the item. + * + * @var string + * @since 4.0.0 + */ + protected $contentType = 'users'; + + /** + * The default view for the display method. + * + * @var string + * @since 4.0.0 + */ + protected $default_view = 'users'; + + /** + * Method to allow extended classes to manipulate the data to be saved for an extension. + * + * @param array $data An array of input data. + * + * @return array + * + * @since 4.0.0 + */ + protected function preprocessSaveData(array $data): array + { + foreach (FieldsHelper::getFields('com_users.user') as $field) { + if (isset($data[$field->name])) { + !isset($data['com_fields']) && $data['com_fields'] = []; + + $data['com_fields'][$field->name] = $data[$field->name]; + unset($data[$field->name]); + } + } + + if (isset($data['password']) && $this->task !== 'add') { + $data['password2'] = $data['password']; + } + + return $data; + } + + /** + * User list view with filtering of data + * + * @return static A BaseController object to support chaining. + * + * @since 4.0.0 + * @throws InvalidParameterException + */ + public function displayList() + { + $apiFilterInfo = $this->input->get('filter', [], 'array'); + $filter = InputFilter::getInstance(); + + if (\array_key_exists('state', $apiFilterInfo)) { + $this->modelState->set('filter.state', $filter->clean($apiFilterInfo['state'], 'INT')); + } + + if (\array_key_exists('active', $apiFilterInfo)) { + $this->modelState->set('filter.active', $filter->clean($apiFilterInfo['active'], 'INT')); + } + + if (\array_key_exists('groupid', $apiFilterInfo)) { + $this->modelState->set('filter.group_id', $filter->clean($apiFilterInfo['groupid'], 'INT')); + } + + if (\array_key_exists('search', $apiFilterInfo)) { + $this->modelState->set('filter.search', $filter->clean($apiFilterInfo['search'], 'STRING')); + } + + if (\array_key_exists('registrationDateStart', $apiFilterInfo)) { + $registrationStartInput = $filter->clean($apiFilterInfo['registrationDateStart'], 'STRING'); + $registrationStartDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $registrationStartInput); + + if (!$registrationStartDate) { + // Send the error response + $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'registrationDateStart'); + + throw new InvalidParameterException($error, 400, null, 'registrationDateStart'); + } + + $this->modelState->set('filter.registrationDateStart', $registrationStartDate); + } + + if (\array_key_exists('registrationDateEnd', $apiFilterInfo)) { + $registrationEndInput = $filter->clean($apiFilterInfo['registrationDateEnd'], 'STRING'); + $registrationEndDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $registrationEndInput); + + if (!$registrationEndDate) { + // Send the error response + $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'registrationDateEnd'); + throw new InvalidParameterException($error, 400, null, 'registrationDateEnd'); + } + + $this->modelState->set('filter.registrationDateEnd', $registrationEndDate); + } elseif ( + \array_key_exists('registrationDateStart', $apiFilterInfo) + && !\array_key_exists('registrationDateEnd', $apiFilterInfo) + ) { + // If no end date specified the end date is now + $this->modelState->set('filter.registrationDateEnd', new Date()); + } + + if (\array_key_exists('lastVisitDateStart', $apiFilterInfo)) { + $lastVisitStartInput = $filter->clean($apiFilterInfo['lastVisitDateStart'], 'STRING'); + $lastVisitStartDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $lastVisitStartInput); + + if (!$lastVisitStartDate) { + // Send the error response + $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'lastVisitDateStart'); + throw new InvalidParameterException($error, 400, null, 'lastVisitDateStart'); + } + + $this->modelState->set('filter.lastVisitStart', $lastVisitStartDate); + } + + if (\array_key_exists('lastVisitDateEnd', $apiFilterInfo)) { + $lastVisitEndInput = $filter->clean($apiFilterInfo['lastVisitDateEnd'], 'STRING'); + $lastVisitEndDate = Date::createFromFormat(\DateTimeInterface::RFC3339, $lastVisitEndInput); + + if (!$lastVisitEndDate) { + // Send the error response + $error = Text::sprintf('JLIB_FORM_VALIDATE_FIELD_INVALID', 'lastVisitDateEnd'); + + throw new InvalidParameterException($error, 400, null, 'lastVisitDateEnd'); + } + + $this->modelState->set('filter.lastVisitEnd', $lastVisitEndDate); + } elseif ( + \array_key_exists('lastVisitDateStart', $apiFilterInfo) + && !\array_key_exists('lastVisitDateEnd', $apiFilterInfo) + ) { + // If no end date specified the end date is now + $this->modelState->set('filter.lastVisitEnd', new Date()); + } + + return parent::displayList(); + } } diff --git a/api/components/com_users/src/View/Groups/JsonapiView.php b/api/components/com_users/src/View/Groups/JsonapiView.php index 34e70606616d0..9cd42b5772871 100644 --- a/api/components/com_users/src/View/Groups/JsonapiView.php +++ b/api/components/com_users/src/View/Groups/JsonapiView.php @@ -1,4 +1,5 @@ fieldsToRenderList[] = $field->name; - } + /** + * Execute and display a template script. + * + * @param array|null $items Array of items + * + * @return string + * + * @since 4.0.0 + */ + public function displayList(array $items = null) + { + foreach (FieldsHelper::getFields('com_users.user') as $field) { + $this->fieldsToRenderList[] = $field->name; + } - return parent::displayList(); - } + return parent::displayList(); + } - /** - * Execute and display a template script. - * - * @param object $item Item - * - * @return string - * - * @since 4.0.0 - */ - public function displayItem($item = null) - { - foreach (FieldsHelper::getFields('com_users.user') as $field) - { - $this->fieldsToRenderItem[] = $field->name; - } + /** + * Execute and display a template script. + * + * @param object $item Item + * + * @return string + * + * @since 4.0.0 + */ + public function displayItem($item = null) + { + foreach (FieldsHelper::getFields('com_users.user') as $field) { + $this->fieldsToRenderItem[] = $field->name; + } - return parent::displayItem(); - } + return parent::displayItem(); + } - /** - * Prepare item before render. - * - * @param object $item The model item - * - * @return object - * - * @since 4.0.0 - */ - protected function prepareItem($item) - { - if (empty($item->username)) - { - throw new RouteNotFoundException('Item does not exist'); - } + /** + * Prepare item before render. + * + * @param object $item The model item + * + * @return object + * + * @since 4.0.0 + */ + protected function prepareItem($item) + { + if (empty($item->username)) { + throw new RouteNotFoundException('Item does not exist'); + } - foreach (FieldsHelper::getFields('com_users.user', $item, true) as $field) - { - $item->{$field->name} = $field->apivalue ?? $field->rawvalue; - } + foreach (FieldsHelper::getFields('com_users.user', $item, true) as $field) { + $item->{$field->name} = $field->apivalue ?? $field->rawvalue; + } - return parent::prepareItem($item); - } + return parent::prepareItem($item); + } } diff --git a/api/includes/app.php b/api/includes/app.php index 50c934cd6c80c..9f10825f9f10b 100644 --- a/api/includes/app.php +++ b/api/includes/app.php @@ -1,4 +1,5 @@ alias('session', 'session.cli') - ->alias('JSession', 'session.cli') - ->alias(\Joomla\CMS\Session\Session::class, 'session.cli') - ->alias(\Joomla\Session\Session::class, 'session.cli') - ->alias(\Joomla\Session\SessionInterface::class, 'session.cli'); + ->alias('JSession', 'session.cli') + ->alias(\Joomla\CMS\Session\Session::class, 'session.cli') + ->alias(\Joomla\Session\Session::class, 'session.cli') + ->alias(\Joomla\Session\SessionInterface::class, 'session.cli'); // Instantiate the application. $app = $container->get(\Joomla\CMS\Application\ApiApplication::class); diff --git a/api/includes/defines.php b/api/includes/defines.php index 63922bd809819..6148b98c0969d 100644 --- a/api/includes/defines.php +++ b/api/includes/defines.php @@ -1,4 +1,5 @@ isInDevelopmentState()))) -{ - if (file_exists(JPATH_INSTALLATION . '/index.php')) - { - header('HTTP/1.1 500 Internal Server Error'); - echo json_encode( - array('error' => 'You must install Joomla to use the API') - ); - - exit(); - } - else - { - header('HTTP/1.1 500 Internal Server Error'); - echo json_encode( - array('error' => 'No configuration file found and no installation code available. Exiting...') - ); - - exit; - } +if ( + !file_exists(JPATH_CONFIGURATION . '/configuration.php') + || (filesize(JPATH_CONFIGURATION . '/configuration.php') < 10) + || (file_exists(JPATH_INSTALLATION . '/index.php') && (false === (new Version())->isInDevelopmentState())) +) { + if (file_exists(JPATH_INSTALLATION . '/index.php')) { + header('HTTP/1.1 500 Internal Server Error'); + echo json_encode( + array('error' => 'You must install Joomla to use the API') + ); + + exit(); + } else { + header('HTTP/1.1 500 Internal Server Error'); + echo json_encode( + array('error' => 'No configuration file found and no installation code available. Exiting...') + ); + + exit; + } } // Pre-Load configuration. Don't remove the Output Buffering due to BOM issues, see JCode 26026 @@ -45,64 +44,59 @@ ob_end_clean(); // System configuration. -$config = new JConfig; +$config = new JConfig(); // Set the error_reporting -switch ($config->error_reporting) -{ - case 'default': - case '-1': - break; +switch ($config->error_reporting) { + case 'default': + case '-1': + break; - case 'none': - case '0': - error_reporting(0); + case 'none': + case '0': + error_reporting(0); - break; + break; - case 'simple': - error_reporting(E_ERROR | E_WARNING | E_PARSE); - ini_set('display_errors', 1); + case 'simple': + error_reporting(E_ERROR | E_WARNING | E_PARSE); + ini_set('display_errors', 1); - break; + break; - case 'maximum': - case 'development': // <= Stays for backward compatibility, @TODO: can be removed in 5.0 - error_reporting(E_ALL); - ini_set('display_errors', 1); + case 'maximum': + case 'development': // <= Stays for backward compatibility, @TODO: can be removed in 5.0 + error_reporting(E_ALL); + ini_set('display_errors', 1); - break; + break; - default: - error_reporting($config->error_reporting); - ini_set('display_errors', 1); + default: + error_reporting($config->error_reporting); + ini_set('display_errors', 1); - break; + break; } define('JDEBUG', $config->debug); // Check deprecation logging -if (empty($config->log_deprecated)) -{ - // Reset handler for E_USER_DEPRECATED - set_error_handler(null, E_USER_DEPRECATED); -} -else -{ - // Make sure handler for E_USER_DEPRECATED is registered - set_error_handler(['Joomla\CMS\Exception\ExceptionHandler', 'handleUserDeprecatedErrors'], E_USER_DEPRECATED); +if (empty($config->log_deprecated)) { + // Reset handler for E_USER_DEPRECATED + set_error_handler(null, E_USER_DEPRECATED); +} else { + // Make sure handler for E_USER_DEPRECATED is registered + set_error_handler(['Joomla\CMS\Exception\ExceptionHandler', 'handleUserDeprecatedErrors'], E_USER_DEPRECATED); } -if (JDEBUG || $config->error_reporting === 'maximum') -{ - // Set new Exception handler with debug enabled - $errorHandler->setExceptionHandler( - [ - new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), - 'renderException', - ] - ); +if (JDEBUG || $config->error_reporting === 'maximum') { + // Set new Exception handler with debug enabled + $errorHandler->setExceptionHandler( + [ + new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), + 'renderException', + ] + ); } /** @@ -111,15 +105,12 @@ * We need to do this as high up the stack as we can, as the default in \Joomla\Utilities\IpHelper is to * $allowIpOverride = true which is the wrong default for a generic site NOT behind a trusted proxy/load balancer. */ -if (property_exists($config, 'behind_loadbalancer') && $config->behind_loadbalancer == 1) -{ - // If Joomla is configured to be behind a trusted proxy/load balancer, allow HTTP Headers to override the REMOTE_ADDR - IpHelper::setAllowIpOverrides(true); -} -else -{ - // We disable the allowing of IP overriding using headers by default. - IpHelper::setAllowIpOverrides(false); +if (property_exists($config, 'behind_loadbalancer') && $config->behind_loadbalancer == 1) { + // If Joomla is configured to be behind a trusted proxy/load balancer, allow HTTP Headers to override the REMOTE_ADDR + IpHelper::setAllowIpOverrides(true); +} else { + // We disable the allowing of IP overriding using headers by default. + IpHelper::setAllowIpOverrides(false); } unset($config); diff --git a/api/index.php b/api/index.php index 0f65a63313ebd..08dac50c4a97b 100644 --- a/api/index.php +++ b/api/index.php @@ -1,4 +1,5 @@ sprintf('Joomla requires PHP version %s to run', JOOMLA_MINIMUM_PHP)) - ); +if (version_compare(PHP_VERSION, JOOMLA_MINIMUM_PHP, '<')) { + header('HTTP/1.1 500 Internal Server Error'); + echo json_encode( + array('error' => sprintf('Joomla requires PHP version %s to run', JOOMLA_MINIMUM_PHP)) + ); - return; + return; } /** diff --git a/build/psr12/.editorconfig b/build/psr12/.editorconfig new file mode 100644 index 0000000000000..6ff9778775236 --- /dev/null +++ b/build/psr12/.editorconfig @@ -0,0 +1,16 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{js,json,scss,css,vue}] +indent_size = 2 diff --git a/build/psr12/clean_errors.php b/build/psr12/clean_errors.php new file mode 100644 index 0000000000000..257cd183d49f8 --- /dev/null +++ b/build/psr12/clean_errors.php @@ -0,0 +1,174 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +$tmpDir = dirname(__DIR__) . '/tmp/psr12'; + +$cleaned = []; + +$json = file_get_contents($tmpDir . '/cleanup.json'); + +$data = json_decode($json, JSON_OBJECT_AS_ARRAY); + +// Fixing the later issues in a file first should allow us to preserve the line value per error +$data = array_reverse($data); + +foreach ($data as $error) { + $file = file_get_contents($error['file']); + switch ($error['cleanup']) { + // Remove defined JEXEC statement, PSR-12 doesn't allow functional and symbol code in the same file + case 'definedJEXEC': + $file = str_replace([ + /** + * I know this looks silly but makes it more clear what's happening + * We remove the different types of execution check from files which + * only defines symbols (like classes). + * + * The order is important. + */ + "\defined('_JEXEC') || die();", + "defined('_JEXEC') || die();", + "\defined('_JEXEC') || die;", + "defined('_JEXEC') || die;", + "\defined('_JEXEC') or die();", + "defined('_JEXEC') or die();", + "\defined('_JEXEC') or die;", + "defined('_JEXEC') or die;", + "\defined('JPATH_PLATFORM') or die();", + "defined('JPATH_PLATFORM') or die();", + "\defined('JPATH_PLATFORM') or die;", + "defined('JPATH_PLATFORM') or die;", + "\defined('JPATH_BASE') or die();", + "defined('JPATH_BASE') or die();", + "\defined('JPATH_BASE') or die;", + "defined('JPATH_BASE') or die;", + /** + * We have variants of comments in front of the 'defined die' statement + * which we would like to remove too. + * + * The order is important. + */ + "// No direct access.", + "// No direct access", + "// no direct access", + "// Restrict direct access", + "// Protect from unauthorized access", + ], '', $file); + break; + + // Not all files need a namespace + case 'MissingNamespace': + // We search for the end of the first doc block and add the exception for this file + $pos = strpos($file, ' */'); + $file = substr_replace( + $file, + "\n * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace\n", + $pos, + 0 + ); + + break; + // Not all classes have to be camelcase + case 'ValidClassNameNotCamelCaps': + // We search for the end of the first doc block and add the exception for this file + $pos = strpos($file, ' */'); + $file = substr_replace( + $file, + "\n * @phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps\n", + $pos, + 0 + ); + + break; + + case 'ConstantVisibility': + // add public to const declaration if defined in a class + $fileContent = file($error['file']); + $fileContent[$error['line'] - 1] = substr_replace($fileContent[$error['line'] - 1], 'public ', $error['column'] - 1, 0); + $file = implode('', $fileContent); + + break; + + case 'SpaceAfterCloseBrace': + // We only move single comments (starting with //) to the next line + + $fileContent = file($error['file']); + + $lineNo = $error['line']; + + // We skip blank lines + do { + $nextLine = ltrim($fileContent[$lineNo]); + if (empty($nextLine)) { + $lineNo = $lineNo + 1; + $nextLine = ltrim($fileContent[$lineNo]); + } + } while (empty($nextLine)); + + $sourceLineStartNo = $lineNo; + $sourceLineEndNo = $lineNo; + $found = false; + + while (substr(ltrim($fileContent[$sourceLineEndNo]), 0, 2) === '//') { + $sourceLineEndNo++; + $found = true; + } + + if ($sourceLineStartNo === $sourceLineEndNo) { + if (substr(ltrim($fileContent[$sourceLineStartNo]), 0, 2) === '/*') { + while (substr(ltrim($fileContent[$sourceLineEndNo]), 0, 2) !== '*/') { + $sourceLineEndNo++; + } + $sourceLineEndNo++; + $found = true; + } + } + + if (!$found) { + echo "Unrecoverable error while running SpaceAfterCloseBrace cleanup"; + var_dump($error['file'], $sourceLineStartNo, $sourceLineEndNo); + die(1); + } + $targetLineNo = $sourceLineEndNo + 1; + + // Adjust the indentation to match the next line of code + for ($indent = 0; $indent <= strlen($fileContent[$targetLineNo]); $indent++) { + if ($fileContent[$targetLineNo][$indent] !== ' ') { + break; + } + } + + $replace = []; + for ($i = $sourceLineStartNo; $i < $sourceLineEndNo; $i++) { + $newLine = ltrim($fileContent[$i]); + // Fix codeblocks not starting with /** + if (substr($newLine, 0, 2) === '/*') { + $newLine = "/**\n"; + } + + $localIndent = $indent; + if ($newLine[0] === '*') { + $localIndent++; + } + $replace[] = str_repeat(' ', $localIndent) . $newLine; + } + array_unshift($replace, $fileContent[$sourceLineEndNo]); + + array_splice($fileContent, $sourceLineStartNo, count($replace), $replace); + + $file = implode('', $fileContent); + + break; + } + + file_put_contents($error['file'], $file); + $cleaned[] = $error['file'] . ' ' . $error['cleanup']; +} + +file_put_contents($tmpDir . '/cleaned.log', implode("\n", $cleaned)); diff --git a/build/psr12/phpcs.joomla.report.php b/build/psr12/phpcs.joomla.report.php new file mode 100644 index 0000000000000..3707baf9a8168 --- /dev/null +++ b/build/psr12/phpcs.joomla.report.php @@ -0,0 +1,316 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Reports; + +use PHP_CodeSniffer\Config; +use PHP_CodeSniffer\Files\File; + +use function array_keys; +use function array_merge; +use function array_values; +use function file_exists; +use function file_get_contents; +use function file_put_contents; +use function json_encode; +use function str_replace; + +use const JSON_OBJECT_AS_ARRAY; +use const JSON_PRETTY_PRINT; + + +class Joomla implements \PHP_CodeSniffer\Reports\Report +{ + private $tmpDir = __DIR__ . '/../tmp/psr12'; + + private $html = ''; + + private $preProcessing = []; + + /** + * Generate a partial report for a single processed file. + * + * Function should return TRUE if it printed or stored data about the file + * and FALSE if it ignored the file. Returning TRUE indicates that the file and + * its data should be counted in the grand totals. + * + * @param array $report Prepared report data. + * @param \PHP_CodeSniffer\File $phpcsFile The file being reported on. + * @param bool $showSources Show sources? + * @param int $width Maximum allowed line width. + * + * @return bool + */ + public function generateFileReport($report, File $phpcsFile, $showSources = false, $width = 80) + { + if ($report['errors'] === 0 && $report['warnings'] === 0) { + return false; + } + + $template = [ + 'headline' => $report['filename'], + 'text' => 'Errors: ' . $report['errors'] . ' Warnings: ' . $report['warnings'] . ' Fixable: ' . $report['fixable'], + ]; + + $this->html = << +

    {$template['headline']}

    +

    {$template['text']}

    + HTML; + + foreach ($report['messages'] as $line => $lineErrors) { + foreach ($lineErrors as $column => $colErrors) { + foreach ($colErrors as $error) { + $error['type'] = strtolower($error['type']); + if ($phpcsFile->config->encoding !== 'utf-8') { + $error['message'] = iconv($phpcsFile->config->encoding, 'utf-8', $error['message']); + } + + $error['fixable'] = $error['fixable'] === true ? 'Yes' : 'No'; + + $this->html .= << + Line: $line + Column: $column + Fixable: {$error['fixable']} + Severity: {$error['severity']} + Rule: {$error['source']} +
    {$error['message']}
    + + HTML; + $this->prepareProcessing($report['filename'],$phpcsFile, $line, $column, $error); + } + } + } + + $this->html .= << + HTML; + + $this->writeFile(); + + return true; + } + + private function prepareProcessing($file, $phpcsFile, $line, $column, $error) { + + switch ($error['source']) { + case 'PSR1.Files.SideEffects.FoundWithSymbols': + $fileContent = file_get_contents($file); + + if ( + strpos($fileContent, "defined('_JEXEC')") !== false + || strpos($fileContent, "defined('JPATH_PLATFORM')") !== false + || strpos($fileContent, "defined('JPATH_BASE')") !== false + ) { + $this->preProcessing[] = [ + 'file' => $file, + 'line' => $line, + 'column' => $column, + 'cleanup' => 'definedJEXEC' + ]; + } else { + $targetFile = $this->tmpDir . '/' . $error['source'] . '.txt'; + $fileContent = ''; + if (file_exists($targetFile)) { + $fileContent = file_get_contents($targetFile); + } + + static $replace = null; + + if ($replace === null) { + $replace = [ + "\\" => '/', + dirname(dirname(__DIR__)) . '/' => '', + '.' => '\.', + ]; + } + + $fileContent .= " " . str_replace(array_keys($replace), $replace, $file) . "\n"; + file_put_contents($targetFile, $fileContent); + } + break; + + case 'PSR1.Classes.ClassDeclaration.MissingNamespace': + $this->preProcessing[] = [ + 'file' => $file, + 'line' => $line, + 'column' => $column, + 'cleanup' => 'MissingNamespace' + ]; + break; + + case 'Squiz.Classes.ValidClassName.NotCamelCaps': + if ( + strpos($file, 'localise') !== false + || strpos($file, 'recaptcha_invisible') !== false + ) { + $this->preProcessing[] = [ + 'file' => $file, + 'line' => $line, + 'column' => $column, + 'cleanup' => 'ValidClassNameNotCamelCaps' + ]; + } + break; + + case 'Squiz.ControlStructures.ControlSignature.SpaceAfterCloseBrace': + $this->preProcessing[] = [ + 'file' => $file, + 'line' => $line, + 'column' => $column, + 'cleanup' => 'SpaceAfterCloseBrace' + ]; + break; + + case 'PSR12.Properties.ConstantVisibility.NotFound': + $this->preProcessing[] = [ + 'file' => $file, + 'line' => $line, + 'column' => $column, + 'cleanup' => 'ConstantVisibility' + ]; + break; + + case 'PSR2.Classes.PropertyDeclaration.Underscore': + case 'PSR2.Methods.MethodDeclaration.Underscore': + case 'PSR1.Classes.ClassDeclaration.MultipleClasses': + case 'PSR1.Methods.CamelCapsMethodName.NotCamelCaps': + + $targetFile = $this->tmpDir . '/' . $error['source'] . '.txt'; + $fileContent = ''; + if (file_exists($targetFile)) { + $fileContent = file_get_contents($targetFile); + } + + static $replace = null; + + if ($replace === null) { + $replace = [ + "\\" => '/', + dirname(dirname(__DIR__)) . '/' => '', + '.' => '\.', + ]; + } + + $fileContent .= " " . str_replace(array_keys($replace), $replace, $file) . "\n"; + file_put_contents($targetFile, $fileContent); + break; + } + } + + /** + * Prints all violations for processed files, in a proprietary XML format. + * + * @param string $cachedData Any partial report data that was returned from + * generateFileReport during the run. + * @param int $totalFiles Total number of files processed during the run. + * @param int $totalErrors Total number of errors found during the run. + * @param int $totalWarnings Total number of warnings found during the run. + * @param int $totalFixable Total number of problems that can be fixed. + * @param bool $showSources Show sources? + * @param int $width Maximum allowed line width. + * @param bool $interactive Are we running in interactive mode? + * @param bool $toScreen Is the report being printed to screen? + * + * @return void + */ + public function generate( + $cachedData, + $totalFiles, + $totalErrors, + $totalWarnings, + $totalFixable, + $showSources = false, + $width = 80, + $interactive = false, + $toScreen = true + ) { + + $preprocessing = []; + if (file_exists($this->tmpDir .'/cleanup.json')) { + $preprocessing = json_decode(file_get_contents($this->tmpDir .'/cleanup.json'), JSON_OBJECT_AS_ARRAY); + } + + $preprocessing = array_merge($this->preProcessing, $preprocessing); + file_put_contents($this->tmpDir .'/cleanup.json', json_encode($preprocessing, JSON_PRETTY_PRINT)); + } + + private function getTemplate($section) + { + $sections = [ + 'header' => << + + Report + + + +
    +
    + + +
    +
    Check
    + HTML, + 'footer' => << +
    +
    + + + HTML, + 'line' => << +

    %HEADLINE%

    +

    %TEXT%

    +
    +
    %ERROR%
    +
    + + HTML + ]; + + return $sections[$section]; + } + + private function htmlAddBlock($headline, $text, $error) + { + $line = $this->getTemplate('line'); + + $replace = [ + '%HEADLINE%' => $headline, + '%TEXT%' => $text, + '%ERROR%' => $error, + ]; + + $this->html .= str_replace(array_keys($replace), $replace, $line); + } + + private function writeFile() + { + $file = $this->tmpDir . '/result.html'; + + if (file_exists($file)) { + $html = file_get_contents($file); + } else { + $html = $this->getTemplate('header'); + $html .= ''; + $html .= $this->getTemplate('footer'); + } + + $html = str_replace('', $this->html . '', $html); + + file_put_contents($this->tmpDir . '/result.html', $html); + } +} diff --git a/build/psr12/psr12_converter.php b/build/psr12/psr12_converter.php new file mode 100644 index 0000000000000..2e29c1d9e8556 --- /dev/null +++ b/build/psr12/psr12_converter.php @@ -0,0 +1,281 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +// Set defaults +$root = dirname(dirname(__DIR__)); +$php = 'php'; +$git = 'git'; +$tmpDir = $root . '/build/tmp/psr12'; +$checkPath = false; +$tasks = [ + 'CBF' => false, + 'CS' => false, + 'CLEAN' => false, + 'CMS' => false, + 'BRANCH' => false, +]; + +$script = array_shift($argv); + +if (empty($argv)) { + echo << [path] + + Description: + The converter has several tasks which can be run separately. + You can combine them seperated by a comma (,). + + Tasks: + * CBF + This task executes the PHP Code Beautifier and Fixer. It + does the heavy lifting making the code PSR-12 compatible. + (beware this tasks modifies many files) + * CS + This task executes the PHP Code Sniffer. It collects all + issues and generates a HTML report in build/tmp/psr12. + Also it generates a collection of issues which can't be + fixed by CBF. This information is saved as json in + build/tmp/psr12/cleanup.json. If this option is activated + the tmp directory is cleaned before running. + * CLEAN + This tasks loads the cleanup.json generated by the CS task + and changes the cms specific files. After completing this + task it re-runs the CBF and CS task. + * CMS + This tasks activates all other tasks and automatically + commits the changes after both CBF runs. Usually only + needed for the first cms conversion. + * BRANCH + This tasks updates all files changed by the current + branch compared to the psr12anchor tag. This allows + to update a create pull request. + + Path: + Providing a path will only check the directories or files + specified. It's possible to add multiple files and folder + seperated by a comma (,). + + + TEXT; + die(1); +} + +foreach ($argv as $arg) { + if (substr($arg, 0, 2) === '--') { + $argi = explode('=', $arg, 2); + switch ($argi[0]) { + case '--task': + foreach ($tasks as $task => $value) { + if (stripos($argi[1], $task) !== false) { + $tasks[$task] = true; + } + } + break; + } + } else { + $checkPath = $arg; + break; + } +} + +if ($tasks['CMS']) { + $tasks['CBF'] = true; + $tasks['CS'] = true; + $tasks['CLEAN'] = true; +} + +if ($tasks['BRANCH']) { + $tasks['CMS'] = true; + $tasks['CBF'] = true; + $tasks['CS'] = true; + $tasks['CLEAN'] = true; + + $cmd = $git . ' --no-pager diff --name-only psr12anchor..HEAD'; + exec($cmd, $output, $result); + if ($result !== 0) { + die('Unable to find changes for this branch'); + } + foreach($output as $k => $line) { + if (substr($line, -4) !== '.php') { + unset($output[$k]); + } + } + + $checkPath = implode(',', $output); +} + +$items = []; +if ($checkPath) { + $items = explode(',', $checkPath); +} else { + $items[] = 'index.php'; + $items[] = 'administrator/index.php'; + + $baseFolders = [ + 'administrator/components', + 'administrator/includes', + 'administrator/language', + 'administrator/modules', + 'administrator/templates', + 'api', + 'cli', + 'components', + 'includes', + 'installation', + 'language', + 'layouts', + 'libraries', + 'modules', + 'plugins', + 'templates', + ]; + + foreach ($baseFolders as $folder) { + $dir = dir($root . '/' . $folder); + while (false !== ($entry = $dir->read())) { + if (($entry === ".") || ($entry === "..")) { + continue; + } + if (!is_dir($dir->path . '/' . $entry)) { + if (substr($entry, -4) !== '.php') { + continue; + } + } + if ( + $folder === 'libraries' + && ( + $entry === 'php-encryption' + || $entry === 'phpass' + || $entry === 'vendor' + ) + ) { + continue; + } + $items[] = str_replace($root . '/', '', $dir->path) . '/' . $entry; + } + $dir->close(); + } +} +$executedTasks = implode( + ',', + array_keys( + array_filter($tasks, function ($task) { + return $task; + }) + ) +); +$executedPaths = implode("\n", $items); + +echo << + + The Joomla CMS PSR-12 exceptions. + + + build/* + cache/* + docs/* + logs/* + media/* + node_modules/* + tmp/* + tests/* + + + libraries/php-encryption/* + libraries/phpass/* + libraries/vendor/* + + stubs.php + configuration.php + + + + + + + + + + + + + + administrator/components/com_banners/src/Controller/BannersController\.php + + + + administrator/components/com_banners/src/Model/DownloadModel\.php + administrator/components/com_banners/src/Table/BannerTable\.php + administrator/components/com_banners/src/Table/ClientTable\.php + administrator/components/com_cache/src/Model/CacheModel\.php + administrator/components/com_contact/src/Table/ContactTable\.php + administrator/components/com_fields/src/Table/FieldTable\.php + administrator/components/com_fields/src/Table/GroupTable\.php + administrator/components/com_finder/src/Table/FilterTable\.php + administrator/components/com_finder/src/Table/LinkTable\.php + administrator/components/com_installer/src/Model/DatabaseModel\.php + administrator/components/com_installer/src/Model/InstallModel\.php + administrator/components/com_installer/src/Table/UpdatesiteTable\.php + administrator/components/com_mails/src/Table/TemplateTable\.php + administrator/components/com_menus/src/Helper/MenusHelper\.php + administrator/components/com_menus/src/Model/MenuModel\.php + administrator/components/com_newsfeeds/src/Table/NewsfeedTable\.php + administrator/components/com_plugins/src/Model/PluginModel\.php + administrator/components/com_privacy/src/Table/RequestTable\.php + administrator/components/com_scheduler/src/Table/TaskTable\.php + administrator/components/com_tags/src/Table/TagTable\.php + administrator/components/com_templates/src/Model/StyleModel\.php + administrator/components/com_users/src/Model/UserModel\.php + administrator/components/com_users/src/Table/NoteTable\.php + administrator/components/com_workflow/src/Table/StageTable\.php + administrator/components/com_workflow/src/Table/TransitionTable\.php + administrator/components/com_workflow/src/Table/WorkflowTable\.php + components/com_banners/src/Model/BannerModel\.php + components/com_contact/src/Model/CategoriesModel\.php + components/com_contact/src/Model/CategoryModel\.php + components/com_contact/src/Model/ContactModel\.php + components/com_content/src/Model/ArchiveModel\.php + components/com_content/src/Model/ArticleModel\.php + components/com_content/src/Model/CategoriesModel\.php + components/com_content/src/Model/CategoryModel\.php + components/com_content/src/Model/FeaturedModel\.php + components/com_newsfeeds/src/Model/CategoriesModel\.php + components/com_newsfeeds/src/Model/CategoryModel\.php + components/com_newsfeeds/src/Model/NewsfeedModel\.php + components/com_tags/src/Model/TagsModel\.php + installation/src/View/Preinstall/HtmlView\.php + libraries/src/Adapter/Adapter\.php + libraries/src/Application/ApplicationHelper\.php + libraries/src/Cache/Cache\.php + libraries/src/Cache/CacheStorage\.php + libraries/src/Cache/Controller/OutputController\.php + libraries/src/Cache/Controller/PageController\.php + libraries/src/Cache/Storage/FileStorage\.php + libraries/src/Cache/Storage/MemcachedStorage\.php + libraries/src/Cache/Storage/RedisStorage\.php + libraries/src/Categories/Categories\.php + libraries/src/Categories/CategoryNode\.php + libraries/src/Client/FtpClient\.php + libraries/src/Document/Document\.php + libraries/src/Document/DocumentRenderer\.php + libraries/src/Document/ErrorDocument\.php + libraries/src/Document/HtmlDocument\.php + libraries/src/Document/JsonDocument\.php + libraries/src/Document/OpensearchDocument\.php + libraries/src/Document/Renderer/Feed/AtomRenderer\.php + libraries/src/Document/Renderer/Feed/RssRenderer\.php + libraries/src/Editor/Editor\.php + libraries/src/Encrypt/Totp\.php + libraries/src/Form/Field/CaptchaField\.php + libraries/src/Input/Json\.php + libraries/src/MVC/Model/DatabaseAwareTrait\.php + libraries/src/MVC/Model/FormBehaviorTrait\.php + libraries/src/MVC/Model/ItemModel\.php + libraries/src/MVC/Model/StateBehaviorTrait\.php + libraries/src/MVC/View/AbstractView\.php + libraries/src/MVC/View/HtmlView\.php + libraries/src/MVC/View/JsonView\.php + libraries/src/Object/CMSObject\.php + libraries/src/Plugin/CMSPlugin\.php + libraries/src/Router/Route\.php + libraries/src/Table/Category\.php + libraries/src/Table/Content\.php + libraries/src/Table/CoreContent\.php + libraries/src/Table/Extension\.php + libraries/src/Table/Menu\.php + libraries/src/Table/Module\.php + libraries/src/Table/Nested\.php + libraries/src/Table/Table\.php + libraries/src/Table/Update\.php + libraries/src/Table/User\.php + libraries/src/Toolbar/Toolbar\.php + libraries/src/Tree/ImmutableNodeTrait\.php + libraries/src/Updater/UpdateAdapter\.php + libraries/src/User/User\.php + plugins/editors/tinymce/tinymce\.php + plugins/system/cache/cache\.php + + + + administrator/components/com_content/src/Service/HTML/Icon\.php + administrator/components/com_installer/src/Controller/InstallController\.php + administrator/components/com_installer/src/Model/DiscoverModel\.php + administrator/components/com_installer/src/Model/WarningsModel\.php + administrator/components/com_users/src/Service/HTML/Users\.php + components/com_content/helpers/icon\.php + components/com_content/helpers/icon\.php + libraries/src/Filesystem/Stream\.php + libraries/src/Filesystem/Streams/StreamString\.php + libraries/src/Installer/Adapter/ComponentAdapter\.php + libraries/src/Installer/Adapter/LanguageAdapter\.php + libraries/src/Installer/Adapter/ModuleAdapter\.php + libraries/src/Installer/Installer\.php + libraries/src/Installer/InstallerAdapter\.php + libraries/src/Language/Transliterate\.php + libraries/src/Mail/Mail\.php + libraries/src/Pagination/Pagination\.php + libraries/src/Toolbar/ToolbarHelper\.php + libraries/src/Utility/BufferStreamHandler\.php + + + + + libraries/cms\.php + libraries/loader\.php + + + administrator/components/com_fields/src/Model/FieldsModel\.php + administrator/components/com_fields/src/Model/GroupsModel\.php + administrator/components/com_fields/src/Table/FieldTable\.php + administrator/components/com_fields/src/Table/GroupTable\.php + administrator/components/com_finder/src/Model/MapsModel\.php + administrator/components/com_installer/src/Model/InstallerModel\.php + administrator/components/com_installer/src/Model/InstallModel\.php + administrator/components/com_installer/src/Model/LanguagesModel\.php + administrator/components/com_installer/src/Model/UpdateModel\.php + administrator/components/com_login/src/Model/LoginModel\.php + administrator/components/com_modules/src/Model/ModulesModel\.php + administrator/components/com_plugins/src/Model/PluginsModel\.php + administrator/components/com_scheduler/src/Model/TasksModel\.php + administrator/components/com_scheduler/src/Table/TaskTable\.php + administrator/components/com_users/src/Model/UsersModel\.php + administrator/components/com_workflow/src/Table/StageTable\.php + administrator/components/com_workflow/src/Table/TransitionTable\.php + administrator/components/com_workflow/src/Table/WorkflowTable\.php + api/components/com_contact/src/Controller/ContactController\.php + components/com_config/src/View/Config/HtmlView\.php + components/com_config/src/View/Modules/HtmlView\.php + components/com_config/src/View/Templates/HtmlView\.php + components/com_contact/src/Controller/ContactController\.php + components/com_contact/src/View/Contact/HtmlView\.php + components/com_contact/src/View/Featured/HtmlView\.php + components/com_contact/src/View/Form/HtmlView\.php + components/com_content/src/Model/CategoryModel\.php + components/com_content/src/View/Archive/HtmlView\.php + components/com_content/src/View/Article/HtmlView\.php + components/com_content/src/View/Featured/HtmlView\.php + components/com_content/src/View/Form/HtmlView\.php + components/com_newsfeeds/src/View/Newsfeed/HtmlView\.php + components/com_tags/src/Helper/RouteHelper\.php + components/com_tags/src/View/Tag/HtmlView\.php + components/com_tags/src/View/Tags/HtmlView\.php + installation/src/Form/Field/Installation/LanguageField\.php + libraries/src/Cache/Cache\.php + libraries/src/Cache/CacheStorage\.php + libraries/src/Cache/Controller/CallbackController\.php + libraries/src/Cache/Controller/PageController\.php + libraries/src/Cache/Controller/ViewController\.php + libraries/src/Cache/Storage/FileStorage\.php + libraries/src/Cache/Storage/MemcachedStorage\.php + libraries/src/Captcha/Captcha\.php + libraries/src/Categories/Categories\.php + libraries/src/Client/FtpClient\.php + libraries/src/Document/Document\.php + libraries/src/Document/DocumentRenderer\.php + libraries/src/Document/HtmlDocument\.php + libraries/src/Editor/Editor\.php + libraries/src/Encrypt/Base32\.php + libraries/src/Environment/Browser\.php + libraries/src/Feed/FeedFactory\.php + libraries/src/Filesystem/Folder\.php + libraries/src/Filesystem/Stream\.php + libraries/src/Filesystem/Support/StringController\.php + libraries/src/HTML/Helpers/Grid\.php + libraries/src/Installer/Adapter/ComponentAdapter\.php + libraries/src/Installer/Adapter/LanguageAdapter\.php + libraries/src/Installer/Adapter/ModuleAdapter\.php + libraries/src/Installer/Adapter/PackageAdapter\.php + libraries/src/MVC/Model/BaseDatabaseModel\.php + libraries/src/MVC/Model/LegacyModelLoaderTrait\.php + libraries/src/MVC/Model/ListModel\.php + libraries/src/MVC/View/HtmlView\.php + libraries/src/Pagination/Pagination\.php + libraries/src/Table/Category\.php + libraries/src/Table/Category\.php + libraries/src/Table/Content\.php + libraries/src/Table/Language\.php + libraries/src/Table/MenuType\.php + libraries/src/Table/Module\.php + libraries/src/Table/Nested\.php + libraries/src/Table/Table\.php + libraries/src/Toolbar/Button/ConfirmButton\.php + libraries/src/Toolbar/Button/HelpButton\.php + libraries/src/Toolbar/Button/PopupButton\.php + libraries/src/Toolbar/Button/StandardButton\.php + libraries/src/Updater/Adapter/CollectionAdapter\.php + libraries/src/Updater/Adapter/ExtensionAdapter\.php + libraries/src/Updater/Update\.php + libraries/src/Updater/UpdateAdapter\.php + modules/mod_articles_category/src/Helper/ArticlesCategoryHelper\.php + plugins/content/emailcloak/emailcloak\.php + plugins/content/joomla/joomla\.php + plugins/content/loadmodule/loadmodule\.php + plugins/content/pagebreak/pagebreak\.php + plugins/editors/none/none\.php + plugins/user/joomla/joomla\.php + + + + + administrator/components/com_joomlaupdate/finalisation\.php + + + + + administrator/components/com_installer/src/Model/DatabaseModel\.php + libraries/src/Client/FtpClient\.php + libraries/src/Filesystem/Path\.php + libraries/src/Filesystem/Streams/StreamString\.php + + + index\.php + administrator/index\.php + administrator/components/com_joomlaupdate/extract\.php + administrator/components/com_joomlaupdate/restore_finalisation\.php + administrator/includes/app\.php + administrator/includes/defines\.php + administrator/includes/framework\.php + api/includes/app\.php + api/includes/defines\.php + api/includes/framework\.php + api/index\.php + cli/joomla\.php + includes/app\.php + includes/defines\.php + includes/framework\.php + installation/includes/app\.php + installation/includes/defines\.php + installation/index\.php + libraries/cms\.php + libraries/bootstrap\.php + libraries/import\.php + libraries/import\.legacy\.php + libraries/loader\.php + + diff --git a/cli/joomla.php b/cli/joomla.php index fb078e1265d01..6b5bea7d05b57 100644 --- a/cli/joomla.php +++ b/cli/joomla.php @@ -1,4 +1,5 @@ alias('session', 'session.cli') - ->alias('JSession', 'session.cli') - ->alias(\Joomla\CMS\Session\Session::class, 'session.cli') - ->alias(\Joomla\Session\Session::class, 'session.cli') - ->alias(\Joomla\Session\SessionInterface::class, 'session.cli'); + ->alias('JSession', 'session.cli') + ->alias(\Joomla\CMS\Session\Session::class, 'session.cli') + ->alias(\Joomla\Session\Session::class, 'session.cli') + ->alias(\Joomla\Session\SessionInterface::class, 'session.cli'); $app = \Joomla\CMS\Factory::getContainer()->get(\Joomla\Console\Application::class); \Joomla\CMS\Factory::$application = $app; diff --git a/components/com_ajax/ajax.php b/components/com_ajax/ajax.php index 1b8fcc87b5718..20674d4be208e 100644 --- a/components/com_ajax/ajax.php +++ b/components/com_ajax/ajax.php @@ -1,4 +1,5 @@ get('module')) -{ - $module = $input->get('module'); - $table = Table::getInstance('extension'); - $moduleId = $table->find(array('type' => 'module', 'element' => 'mod_' . $module)); - - if ($moduleId && $table->load($moduleId) && $table->enabled) - { - $helperFile = JPATH_BASE . '/modules/mod_' . $module . '/helper.php'; - - if (strpos($module, '_')) - { - $parts = explode('_', $module); - } - elseif (strpos($module, '-')) - { - $parts = explode('-', $module); - } - - if ($parts) - { - $class = 'Mod'; - - foreach ($parts as $part) - { - $class .= ucfirst($part); - } - - $class .= 'Helper'; - } - else - { - $class = 'Mod' . ucfirst($module) . 'Helper'; - } - - $method = $input->get('method') ?: 'get'; - - $moduleInstance = $app->bootModule('mod_' . $module, $app->getName()); - - if ($moduleInstance instanceof \Joomla\CMS\Helper\HelperFactoryInterface && $helper = $moduleInstance->getHelper(substr($class, 3))) - { - $results = method_exists($helper, $method . 'Ajax') ? $helper->{$method . 'Ajax'}() : null; - } - - if ($results === null && is_file($helperFile)) - { - JLoader::register($class, $helperFile); - - if (method_exists($class, $method . 'Ajax')) - { - // Load language file for module - $basePath = JPATH_BASE; - $lang = Factory::getLanguage(); - $lang->load('mod_' . $module, $basePath) - || $lang->load('mod_' . $module, $basePath . '/modules/mod_' . $module); - - try - { - $results = call_user_func($class . '::' . $method . 'Ajax'); - } - catch (Exception $e) - { - $results = $e; - } - } - // Method does not exist - else - { - $results = new LogicException(Text::sprintf('COM_AJAX_METHOD_NOT_EXISTS', $method . 'Ajax'), 404); - } - } - // The helper file does not exist - elseif ($results === null) - { - $results = new RuntimeException(Text::sprintf('COM_AJAX_FILE_NOT_EXISTS', 'mod_' . $module . '/helper.php'), 404); - } - } - // Module is not published, you do not have access to it, or it is not assigned to the current menu item - else - { - $results = new LogicException(Text::sprintf('COM_AJAX_MODULE_NOT_ACCESSIBLE', 'mod_' . $module), 404); - } -} -/* - * Plugin support by default is based on the "Ajax" plugin group. - * An optional 'group' variable can be passed via the URL. - * - * The plugin event triggered is onAjaxFoo, where 'foo' is - * the value of the 'plugin' variable passed via the URL - * (i.e. index.php?option=com_ajax&plugin=foo) - * - */ -elseif ($input->get('plugin')) -{ - $group = $input->get('group', 'ajax'); - PluginHelper::importPlugin($group); - $plugin = ucfirst($input->get('plugin')); - - try - { - $results = Factory::getApplication()->triggerEvent('onAjax' . $plugin); - } - catch (Exception $e) - { - $results = $e; - } -} -/* - * Template support. - * - * tplFooHelper::getAjax() is called where 'foo' is the value - * of the 'template' variable passed via the URL - * (i.e. index.php?option=com_ajax&template=foo). - * - */ -elseif ($input->get('template')) -{ - $template = $input->get('template'); - $table = Table::getInstance('extension'); - $templateId = $table->find(array('type' => 'template', 'element' => $template)); - - if ($templateId && $table->load($templateId) && $table->enabled) - { - $basePath = ($table->client_id) ? JPATH_ADMINISTRATOR : JPATH_SITE; - $helperFile = $basePath . '/templates/' . $template . '/helper.php'; - - if (strpos($template, '_')) - { - $parts = explode('_', $template); - } - elseif (strpos($template, '-')) - { - $parts = explode('-', $template); - } - - if ($parts) - { - $class = 'Tpl'; - - foreach ($parts as $part) - { - $class .= ucfirst($part); - } - - $class .= 'Helper'; - } - else - { - $class = 'Tpl' . ucfirst($template) . 'Helper'; - } - - $method = $input->get('method') ?: 'get'; - - if (is_file($helperFile)) - { - JLoader::register($class, $helperFile); - - if (method_exists($class, $method . 'Ajax')) - { - // Load language file for template - $lang = Factory::getLanguage(); - $lang->load('tpl_' . $template, $basePath) - || $lang->load('tpl_' . $template, $basePath . '/templates/' . $template); - - try - { - $results = call_user_func($class . '::' . $method . 'Ajax'); - } - catch (Exception $e) - { - $results = $e; - } - } - // Method does not exist - else - { - $results = new LogicException(Text::sprintf('COM_AJAX_METHOD_NOT_EXISTS', $method . 'Ajax'), 404); - } - } - // The helper file does not exist - else - { - $results = new RuntimeException(Text::sprintf('COM_AJAX_FILE_NOT_EXISTS', 'tpl_' . $template . '/helper.php'), 404); - } - } - // Template is not assigned to the current menu item - else - { - $results = new LogicException(Text::sprintf('COM_AJAX_TEMPLATE_NOT_ACCESSIBLE', 'tpl_' . $template), 404); - } +if (!$format) { + $results = new InvalidArgumentException(Text::_('COM_AJAX_SPECIFY_FORMAT'), 404); +} elseif ($input->get('module')) { + /** + * Module support. + * + * modFooHelper::getAjax() is called where 'foo' is the value + * of the 'module' variable passed via the URL + * (i.e. index.php?option=com_ajax&module=foo). + * + */ + $module = $input->get('module'); + $table = Table::getInstance('extension'); + $moduleId = $table->find(array('type' => 'module', 'element' => 'mod_' . $module)); + + if ($moduleId && $table->load($moduleId) && $table->enabled) { + $helperFile = JPATH_BASE . '/modules/mod_' . $module . '/helper.php'; + + if (strpos($module, '_')) { + $parts = explode('_', $module); + } elseif (strpos($module, '-')) { + $parts = explode('-', $module); + } + + if ($parts) { + $class = 'Mod'; + + foreach ($parts as $part) { + $class .= ucfirst($part); + } + + $class .= 'Helper'; + } else { + $class = 'Mod' . ucfirst($module) . 'Helper'; + } + + $method = $input->get('method') ?: 'get'; + + $moduleInstance = $app->bootModule('mod_' . $module, $app->getName()); + + if ($moduleInstance instanceof \Joomla\CMS\Helper\HelperFactoryInterface && $helper = $moduleInstance->getHelper(substr($class, 3))) { + $results = method_exists($helper, $method . 'Ajax') ? $helper->{$method . 'Ajax'}() : null; + } + + if ($results === null && is_file($helperFile)) { + JLoader::register($class, $helperFile); + + if (method_exists($class, $method . 'Ajax')) { + // Load language file for module + $basePath = JPATH_BASE; + $lang = Factory::getLanguage(); + $lang->load('mod_' . $module, $basePath) + || $lang->load('mod_' . $module, $basePath . '/modules/mod_' . $module); + + try { + $results = call_user_func($class . '::' . $method . 'Ajax'); + } catch (Exception $e) { + $results = $e; + } + } else { + // Method does not exist + $results = new LogicException(Text::sprintf('COM_AJAX_METHOD_NOT_EXISTS', $method . 'Ajax'), 404); + } + } elseif ($results === null) { + // The helper file does not exist + $results = new RuntimeException(Text::sprintf('COM_AJAX_FILE_NOT_EXISTS', 'mod_' . $module . '/helper.php'), 404); + } + } else { + // Module is not published, you do not have access to it, or it is not assigned to the current menu item + $results = new LogicException(Text::sprintf('COM_AJAX_MODULE_NOT_ACCESSIBLE', 'mod_' . $module), 404); + } +} elseif ($input->get('plugin')) { + /** + * Plugin support by default is based on the "Ajax" plugin group. + * An optional 'group' variable can be passed via the URL. + * + * The plugin event triggered is onAjaxFoo, where 'foo' is + * the value of the 'plugin' variable passed via the URL + * (i.e. index.php?option=com_ajax&plugin=foo) + * + */ + $group = $input->get('group', 'ajax'); + PluginHelper::importPlugin($group); + $plugin = ucfirst($input->get('plugin')); + + try { + $results = Factory::getApplication()->triggerEvent('onAjax' . $plugin); + } catch (Exception $e) { + $results = $e; + } +} elseif ($input->get('template')) { + /** + * Template support. + * + * tplFooHelper::getAjax() is called where 'foo' is the value + * of the 'template' variable passed via the URL + * (i.e. index.php?option=com_ajax&template=foo). + * + */ + $template = $input->get('template'); + $table = Table::getInstance('extension'); + $templateId = $table->find(array('type' => 'template', 'element' => $template)); + + if ($templateId && $table->load($templateId) && $table->enabled) { + $basePath = ($table->client_id) ? JPATH_ADMINISTRATOR : JPATH_SITE; + $helperFile = $basePath . '/templates/' . $template . '/helper.php'; + + if (strpos($template, '_')) { + $parts = explode('_', $template); + } elseif (strpos($template, '-')) { + $parts = explode('-', $template); + } + + if ($parts) { + $class = 'Tpl'; + + foreach ($parts as $part) { + $class .= ucfirst($part); + } + + $class .= 'Helper'; + } else { + $class = 'Tpl' . ucfirst($template) . 'Helper'; + } + + $method = $input->get('method') ?: 'get'; + + if (is_file($helperFile)) { + JLoader::register($class, $helperFile); + + if (method_exists($class, $method . 'Ajax')) { + // Load language file for template + $lang = Factory::getLanguage(); + $lang->load('tpl_' . $template, $basePath) + || $lang->load('tpl_' . $template, $basePath . '/templates/' . $template); + + try { + $results = call_user_func($class . '::' . $method . 'Ajax'); + } catch (Exception $e) { + $results = $e; + } + } else { + // Method does not exist + $results = new LogicException(Text::sprintf('COM_AJAX_METHOD_NOT_EXISTS', $method . 'Ajax'), 404); + } + } else { + // The helper file does not exist + $results = new RuntimeException(Text::sprintf('COM_AJAX_FILE_NOT_EXISTS', 'tpl_' . $template . '/helper.php'), 404); + } + } else { + // Template is not assigned to the current menu item + $results = new LogicException(Text::sprintf('COM_AJAX_TEMPLATE_NOT_ACCESSIBLE', 'tpl_' . $template), 404); + } } // Return the results in the desired format -switch ($format) -{ - // JSONinzed - case 'json': - echo new JsonResponse($results, null, false, $input->get('ignoreMessages', true, 'bool')); - - break; - - // Handle as raw format - default: - // Output exception - if ($results instanceof Exception) - { - // Log an error - Log::add($results->getMessage(), Log::ERROR); - - // Set status header code - $app->setHeader('status', $results->getCode(), true); - - // Echo exception type and message - $out = get_class($results) . ': ' . $results->getMessage(); - } - // Output string/ null - elseif (is_scalar($results)) - { - $out = (string) $results; - } - // Output array/ object - else - { - $out = implode((array) $results); - } - - echo $out; - - break; +switch ($format) { + // JSONinzed + case 'json': + echo new JsonResponse($results, null, false, $input->get('ignoreMessages', true, 'bool')); + + break; + + // Handle as raw format + default: + // Output exception + if ($results instanceof Exception) { + // Log an error + Log::add($results->getMessage(), Log::ERROR); + + // Set status header code + $app->setHeader('status', $results->getCode(), true); + + // Echo exception type and message + $out = get_class($results) . ': ' . $results->getMessage(); + } elseif (is_scalar($results)) { + // Output string/ null + $out = (string) $results; + } else { + // Output array/ object + $out = implode((array) $results); + } + + echo $out; + + break; } diff --git a/components/com_banners/src/Controller/DisplayController.php b/components/com_banners/src/Controller/DisplayController.php index 75b3f4f667b9e..14c6e5a1ef32e 100644 --- a/components/com_banners/src/Controller/DisplayController.php +++ b/components/com_banners/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->getInt('id', 0); + /** + * Method when a banner is clicked on. + * + * @return void + * + * @since 1.5 + */ + public function click() + { + $id = $this->input->getInt('id', 0); - if ($id) - { - /** @var \Joomla\Component\Banners\Site\Model\BannerModel $model */ - $model = $this->getModel('Banner', 'Site', array('ignore_request' => true)); - $model->setState('banner.id', $id); - $model->click(); - $this->setRedirect($model->getUrl()); - } - } + if ($id) { + /** @var \Joomla\Component\Banners\Site\Model\BannerModel $model */ + $model = $this->getModel('Banner', 'Site', array('ignore_request' => true)); + $model->setState('banner.id', $id); + $model->click(); + $this->setRedirect($model->getUrl()); + } + } } diff --git a/components/com_banners/src/Helper/BannerHelper.php b/components/com_banners/src/Helper/BannerHelper.php index 3476a5841c758..a34db03e989a4 100644 --- a/components/com_banners/src/Helper/BannerHelper.php +++ b/components/com_banners/src/Helper/BannerHelper.php @@ -1,4 +1,5 @@ getItem(); - - if (empty($item)) - { - throw new \Exception(Text::_('JERROR_PAGE_NOT_FOUND'), 404); - } - - $id = (int) $this->getState('banner.id'); - - // Update click count - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->update($db->quoteName('#__banners')) - ->set($db->quoteName('clicks') . ' = ' . $db->quoteName('clicks') . ' + 1') - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500); - } - - // Track clicks - $trackClicks = $item->track_clicks; - - if ($trackClicks < 0 && $item->cid) - { - $trackClicks = $item->client_track_clicks; - } - - if ($trackClicks < 0) - { - $config = ComponentHelper::getParams('com_banners'); - $trackClicks = $config->get('track_clicks'); - } - - if ($trackClicks > 0) - { - $trackDate = Factory::getDate()->format('Y-m-d H:00:00'); - $trackDate = Factory::getDate($trackDate)->toSql(); - - $query = $db->getQuery(true); - - $query->select($db->quoteName('count')) - ->from($db->quoteName('#__banner_tracks')) - ->where( - [ - $db->quoteName('track_type') . ' = 2', - $db->quoteName('banner_id') . ' = :id', - $db->quoteName('track_date') . ' = :trackDate', - ] - ) - ->bind(':id', $id, ParameterType::INTEGER) - ->bind(':trackDate', $trackDate); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500); - } - - $count = $db->loadResult(); - - $query = $db->getQuery(true); - - if ($count) - { - // Update count - $query->update($db->quoteName('#__banner_tracks')) - ->set($db->quoteName('count') . ' = ' . $db->quoteName('count') . ' + 1') - ->where( - [ - $db->quoteName('track_type') . ' = 2', - $db->quoteName('banner_id') . ' = :id', - $db->quoteName('track_date') . ' = :trackDate', - ] - ) - ->bind(':id', $id, ParameterType::INTEGER) - ->bind(':trackDate', $trackDate); - } - else - { - // Insert new count - $query->insert($db->quoteName('#__banner_tracks')) - ->columns( - [ - $db->quoteName('count'), - $db->quoteName('track_type'), - $db->quoteName('banner_id'), - $db->quoteName('track_date'), - ] - ) - ->values('1, 2 , :id, :trackDate') - ->bind(':id', $id, ParameterType::INTEGER) - ->bind(':trackDate', $trackDate); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - throw new \Exception($e->getMessage(), 500); - } - } - } - - /** - * Get the data for a banner. - * - * @return object - * - * @since 1.6 - */ - public function &getItem() - { - if (!isset($this->_item)) - { - /** @var \Joomla\CMS\Cache\Controller\CallbackController $cache */ - $cache = Factory::getCache('com_banners', 'callback'); - - $id = (int) $this->getState('banner.id'); - - // For PHP 5.3 compat we can't use $this in the lambda function below, so grab the database driver now to use it - $db = $this->getDatabase(); - - $loader = function ($id) use ($db) - { - $query = $db->getQuery(true); - - $query->select( - [ - $db->quoteName('a.clickurl'), - $db->quoteName('a.cid'), - $db->quoteName('a.track_clicks'), - $db->quoteName('cl.track_clicks', 'client_track_clicks'), - ] - ) - ->from($db->quoteName('#__banners', 'a')) - ->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('a.cid')) - ->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - - $db->setQuery($query); - - return $db->loadObject(); - }; - - try - { - $this->_item = $cache->get($loader, array($id), md5(__METHOD__ . $id)); - } - catch (CacheExceptionInterface $e) - { - $this->_item = $loader($id); - } - } - - return $this->_item; - } - - /** - * Get the URL for a banner - * - * @return string - * - * @since 1.5 - */ - public function getUrl() - { - $item = $this->getItem(); - $url = $item->clickurl; - - // Check for links - if (!preg_match('#http[s]?://|index[2]?\.php#', $url)) - { - $url = "http://$url"; - } - - return $url; - } + /** + * Cached item object + * + * @var object + * @since 1.6 + */ + protected $_item; + + /** + * Clicks the URL, incrementing the counter + * + * @return void + * + * @since 1.5 + * @throws \Exception + */ + public function click() + { + $item = $this->getItem(); + + if (empty($item)) { + throw new \Exception(Text::_('JERROR_PAGE_NOT_FOUND'), 404); + } + + $id = (int) $this->getState('banner.id'); + + // Update click count + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->update($db->quoteName('#__banners')) + ->set($db->quoteName('clicks') . ' = ' . $db->quoteName('clicks') . ' + 1') + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500); + } + + // Track clicks + $trackClicks = $item->track_clicks; + + if ($trackClicks < 0 && $item->cid) { + $trackClicks = $item->client_track_clicks; + } + + if ($trackClicks < 0) { + $config = ComponentHelper::getParams('com_banners'); + $trackClicks = $config->get('track_clicks'); + } + + if ($trackClicks > 0) { + $trackDate = Factory::getDate()->format('Y-m-d H:00:00'); + $trackDate = Factory::getDate($trackDate)->toSql(); + + $query = $db->getQuery(true); + + $query->select($db->quoteName('count')) + ->from($db->quoteName('#__banner_tracks')) + ->where( + [ + $db->quoteName('track_type') . ' = 2', + $db->quoteName('banner_id') . ' = :id', + $db->quoteName('track_date') . ' = :trackDate', + ] + ) + ->bind(':id', $id, ParameterType::INTEGER) + ->bind(':trackDate', $trackDate); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500); + } + + $count = $db->loadResult(); + + $query = $db->getQuery(true); + + if ($count) { + // Update count + $query->update($db->quoteName('#__banner_tracks')) + ->set($db->quoteName('count') . ' = ' . $db->quoteName('count') . ' + 1') + ->where( + [ + $db->quoteName('track_type') . ' = 2', + $db->quoteName('banner_id') . ' = :id', + $db->quoteName('track_date') . ' = :trackDate', + ] + ) + ->bind(':id', $id, ParameterType::INTEGER) + ->bind(':trackDate', $trackDate); + } else { + // Insert new count + $query->insert($db->quoteName('#__banner_tracks')) + ->columns( + [ + $db->quoteName('count'), + $db->quoteName('track_type'), + $db->quoteName('banner_id'), + $db->quoteName('track_date'), + ] + ) + ->values('1, 2 , :id, :trackDate') + ->bind(':id', $id, ParameterType::INTEGER) + ->bind(':trackDate', $trackDate); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + throw new \Exception($e->getMessage(), 500); + } + } + } + + /** + * Get the data for a banner. + * + * @return object + * + * @since 1.6 + */ + public function &getItem() + { + if (!isset($this->_item)) { + /** @var \Joomla\CMS\Cache\Controller\CallbackController $cache */ + $cache = Factory::getCache('com_banners', 'callback'); + + $id = (int) $this->getState('banner.id'); + + // For PHP 5.3 compat we can't use $this in the lambda function below, so grab the database driver now to use it + $db = $this->getDatabase(); + + $loader = function ($id) use ($db) { + $query = $db->getQuery(true); + + $query->select( + [ + $db->quoteName('a.clickurl'), + $db->quoteName('a.cid'), + $db->quoteName('a.track_clicks'), + $db->quoteName('cl.track_clicks', 'client_track_clicks'), + ] + ) + ->from($db->quoteName('#__banners', 'a')) + ->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('a.cid')) + ->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + $db->setQuery($query); + + return $db->loadObject(); + }; + + try { + $this->_item = $cache->get($loader, array($id), md5(__METHOD__ . $id)); + } catch (CacheExceptionInterface $e) { + $this->_item = $loader($id); + } + } + + return $this->_item; + } + + /** + * Get the URL for a banner + * + * @return string + * + * @since 1.5 + */ + public function getUrl() + { + $item = $this->getItem(); + $url = $item->clickurl; + + // Check for links + if (!preg_match('#http[s]?://|index[2]?\.php#', $url)) { + $url = "http://$url"; + } + + return $url; + } } diff --git a/components/com_banners/src/Model/BannersModel.php b/components/com_banners/src/Model/BannersModel.php index 9b5316aac5bee..5f97e8da74603 100644 --- a/components/com_banners/src/Model/BannersModel.php +++ b/components/com_banners/src/Model/BannersModel.php @@ -1,4 +1,5 @@ getState('filter.search'); - $id .= ':' . $this->getState('filter.tag_search'); - $id .= ':' . $this->getState('filter.client_id'); - $id .= ':' . serialize($this->getState('filter.category_id')); - $id .= ':' . serialize($this->getState('filter.keywords')); - - return parent::getStoreId($id); - } - - /** - * Method to get a DatabaseQuery object for retrieving the data set from a database. - * - * @return DatabaseQuery A DatabaseQuery object to retrieve the data set. - * - * @since 1.6 - */ - protected function getListQuery() - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $ordering = $this->getState('filter.ordering'); - $tagSearch = $this->getState('filter.tag_search'); - $cid = (int) $this->getState('filter.client_id'); - $categoryId = $this->getState('filter.category_id'); - $keywords = $this->getState('filter.keywords'); - $randomise = ($ordering === 'random'); - $nowDate = Factory::getDate()->toSql(); - - $query->select( - [ - $db->quoteName('a.id'), - $db->quoteName('a.type'), - $db->quoteName('a.name'), - $db->quoteName('a.clickurl'), - $db->quoteName('a.sticky'), - $db->quoteName('a.cid'), - $db->quoteName('a.description'), - $db->quoteName('a.params'), - $db->quoteName('a.custombannercode'), - $db->quoteName('a.track_impressions'), - $db->quoteName('cl.track_impressions', 'client_track_impressions'), - ] - ) - ->from($db->quoteName('#__banners', 'a')) - ->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('a.cid')) - ->where($db->quoteName('a.state') . ' = 1') - ->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_up') . ' IS NULL', - $db->quoteName('a.publish_up') . ' <= :nowDate1', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_down') . ' IS NULL', - $db->quoteName('a.publish_down') . ' >= :nowDate2', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('a.imptotal') . ' = 0', - $db->quoteName('a.impmade') . ' < ' . $db->quoteName('a.imptotal'), - ], - 'OR' - ) - ->bind([':nowDate1', ':nowDate2'], $nowDate); - - if ($cid) - { - $query->where( - [ - $db->quoteName('a.cid') . ' = :clientId', - $db->quoteName('cl.state') . ' = 1', - ] - ) - ->bind(':clientId', $cid, ParameterType::INTEGER); - } - - // Filter by a single or group of categories - if (is_numeric($categoryId)) - { - $categoryId = (int) $categoryId; - $type = $this->getState('filter.category_id.include', true) ? ' = ' : ' <> '; - - // Add subcategory check - if ($this->getState('filter.subcategories', false)) - { - $levels = (int) $this->getState('filter.max_category_levels', '1'); - - // Create a subquery for the subcategory list - $subQuery = $db->getQuery(true); - $subQuery->select($db->quoteName('sub.id')) - ->from($db->quoteName('#__categories', 'sub')) - ->join( - 'INNER', - $db->quoteName('#__categories', 'this'), - $db->quoteName('sub.lft') . ' > ' . $db->quoteName('this.lft') - . ' AND ' . $db->quoteName('sub.rgt') . ' < ' . $db->quoteName('this.rgt') - ) - ->where( - [ - $db->quoteName('this.id') . ' = :categoryId1', - $db->quoteName('sub.level') . ' <= ' . $db->quoteName('this.level') . ' + :levels', - ] - ); - - // Add the subquery to the main query - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.catid') . $type . ':categoryId2', - $db->quoteName('a.catid') . ' IN (' . $subQuery . ')', - ], - 'OR' - ) - ->bind([':categoryId1', ':categoryId2'], $categoryId, ParameterType::INTEGER) - ->bind(':levels', $levels, ParameterType::INTEGER); - } - else - { - $query->where($db->quoteName('a.catid') . $type . ':categoryId') - ->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } - } - elseif (is_array($categoryId) && (count($categoryId) > 0)) - { - $categoryId = ArrayHelper::toInteger($categoryId); - - if ($this->getState('filter.category_id.include', true)) - { - $query->whereIn($db->quoteName('a.catid'), $categoryId); - } - else - { - $query->whereNotIn($db->quoteName('a.catid'), $categoryId); - } - } - - if ($tagSearch) - { - if (!$keywords) - { - // No keywords, select nothing. - $query->where('0 != 0'); - } - else - { - $temp = array(); - $config = ComponentHelper::getParams('com_banners'); - $prefix = $config->get('metakey_prefix'); - - if ($categoryId) - { - $query->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('a.catid') . ' = ' . $db->quoteName('cat.id')); - } - - foreach ($keywords as $key => $keyword) - { - $regexp = '[[:<:]]' . $keyword . '[[:>:]]'; - $valuesToBind = [$keyword, $keyword, $regexp]; - - if ($cid) - { - $valuesToBind[] = $regexp; - } - - if ($categoryId) - { - $valuesToBind[] = $regexp; - } - - // Because values to $query->bind() are passed by reference, using $query->bindArray() here instead to prevent overwriting. - $bounded = $query->bindArray($valuesToBind, ParameterType::STRING); - - $condition1 = $db->quoteName('a.own_prefix') . ' = 1' - . ' AND ' . $db->quoteName('a.metakey_prefix') - . ' = SUBSTRING(' . $bounded[0] . ',1,LENGTH(' . $db->quoteName('a.metakey_prefix') . '))' - . ' OR ' . $db->quoteName('a.own_prefix') . ' = 0' - . ' AND ' . $db->quoteName('cl.own_prefix') . ' = 1' - . ' AND ' . $db->quoteName('cl.metakey_prefix') - . ' = SUBSTRING(' . $bounded[1] . ',1,LENGTH(' . $db->quoteName('cl.metakey_prefix') . '))' - . ' OR ' . $db->quoteName('a.own_prefix') . ' = 0' - . ' AND ' . $db->quoteName('cl.own_prefix') . ' = 0' - . ' AND ' . ($prefix == substr($keyword, 0, strlen($prefix)) ? '0 = 0' : '0 != 0'); - - $condition2 = $db->quoteName('a.metakey') . ' ' . $query->regexp($bounded[2]); - - if ($cid) - { - $condition2 .= ' OR ' . $db->quoteName('cl.metakey') . ' ' . $query->regexp($bounded[3]) . ' '; - } - - if ($categoryId) - { - $condition2 .= ' OR ' . $db->quoteName('cat.metakey') . ' ' . $query->regexp($bounded[4]) . ' '; - } - - $temp[] = "($condition1) AND ($condition2)"; - } - - $query->where('(' . implode(' OR ', $temp) . ')'); - } - } - - // Filter by language - if ($this->getState('filter.language')) - { - $query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); - } - - $query->order($db->quoteName('a.sticky') . ' DESC, ' . ($randomise ? $query->rand() : $db->quoteName('a.ordering'))); - - return $query; - } - - /** - * Get a list of banners. - * - * @return array - * - * @since 1.6 - */ - public function getItems() - { - if ($this->getState('filter.tag_search')) - { - // Filter out empty keywords. - $keywords = array_values(array_filter(array_map('trim', $this->getState('filter.keywords')), 'strlen')); - - // Re-set state before running the query. - $this->setState('filter.keywords', $keywords); - - // If no keywords are provided, avoid running the query. - if (!$keywords) - { - $this->cache['items'] = array(); - - return $this->cache['items']; - } - } - - if (!isset($this->cache['items'])) - { - $this->cache['items'] = parent::getItems(); - - foreach ($this->cache['items'] as &$item) - { - $item->params = new Registry($item->params); - } - } - - return $this->cache['items']; - } - - /** - * Makes impressions on a list of banners - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - public function impress() - { - $trackDate = Factory::getDate()->format('Y-m-d H:00:00'); - $trackDate = Factory::getDate($trackDate)->toSql(); - $items = $this->getItems(); - $db = $this->getDatabase(); - $bid = []; - - if (!count($items)) - { - return; - } - - foreach ($items as $item) - { - $bid[] = (int) $item->id; - } - - // Increment impression made - $query = $db->getQuery(true); - $query->update($db->quoteName('#__banners')) - ->set($db->quoteName('impmade') . ' = ' . $db->quoteName('impmade') . ' + 1') - ->whereIn($db->quoteName('id'), $bid); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (ExecutionFailureException $e) - { - throw new \Exception($e->getMessage(), 500); - } - - foreach ($items as $item) - { - // Track impressions - $trackImpressions = $item->track_impressions; - - if ($trackImpressions < 0 && $item->cid) - { - $trackImpressions = $item->client_track_impressions; - } - - if ($trackImpressions < 0) - { - $config = ComponentHelper::getParams('com_banners'); - $trackImpressions = $config->get('track_impressions'); - } - - if ($trackImpressions > 0) - { - // Is track already created? - // Update count - $query = $db->getQuery(true); - $query->update($db->quoteName('#__banner_tracks')) - ->set($db->quoteName('count') . ' = ' . $db->quoteName('count') . ' + 1') - ->where( - [ - $db->quoteName('track_type') . ' = 1', - $db->quoteName('banner_id') . ' = :id', - $db->quoteName('track_date') . ' = :trackDate', - ] - ) - ->bind(':id', $item->id, ParameterType::INTEGER) - ->bind(':trackDate', $trackDate); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (ExecutionFailureException $e) - { - throw new \Exception($e->getMessage(), 500); - } - - if ($db->getAffectedRows() === 0) - { - // Insert new count - $query = $db->getQuery(true); - $query->insert($db->quoteName('#__banner_tracks')) - ->columns( - [ - $db->quoteName('count'), - $db->quoteName('track_type'), - $db->quoteName('banner_id'), - $db->quoteName('track_date'), - ] - ) - ->values('1, 1, :id, :trackDate') - ->bind(':id', $item->id, ParameterType::INTEGER) - ->bind(':trackDate', $trackDate); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (ExecutionFailureException $e) - { - throw new \Exception($e->getMessage(), 500); - } - } - } - } - } + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . $this->getState('filter.tag_search'); + $id .= ':' . $this->getState('filter.client_id'); + $id .= ':' . serialize($this->getState('filter.category_id')); + $id .= ':' . serialize($this->getState('filter.keywords')); + + return parent::getStoreId($id); + } + + /** + * Method to get a DatabaseQuery object for retrieving the data set from a database. + * + * @return DatabaseQuery A DatabaseQuery object to retrieve the data set. + * + * @since 1.6 + */ + protected function getListQuery() + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $ordering = $this->getState('filter.ordering'); + $tagSearch = $this->getState('filter.tag_search'); + $cid = (int) $this->getState('filter.client_id'); + $categoryId = $this->getState('filter.category_id'); + $keywords = $this->getState('filter.keywords'); + $randomise = ($ordering === 'random'); + $nowDate = Factory::getDate()->toSql(); + + $query->select( + [ + $db->quoteName('a.id'), + $db->quoteName('a.type'), + $db->quoteName('a.name'), + $db->quoteName('a.clickurl'), + $db->quoteName('a.sticky'), + $db->quoteName('a.cid'), + $db->quoteName('a.description'), + $db->quoteName('a.params'), + $db->quoteName('a.custombannercode'), + $db->quoteName('a.track_impressions'), + $db->quoteName('cl.track_impressions', 'client_track_impressions'), + ] + ) + ->from($db->quoteName('#__banners', 'a')) + ->join('LEFT', $db->quoteName('#__banner_clients', 'cl'), $db->quoteName('cl.id') . ' = ' . $db->quoteName('a.cid')) + ->where($db->quoteName('a.state') . ' = 1') + ->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_up') . ' IS NULL', + $db->quoteName('a.publish_up') . ' <= :nowDate1', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_down') . ' IS NULL', + $db->quoteName('a.publish_down') . ' >= :nowDate2', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('a.imptotal') . ' = 0', + $db->quoteName('a.impmade') . ' < ' . $db->quoteName('a.imptotal'), + ], + 'OR' + ) + ->bind([':nowDate1', ':nowDate2'], $nowDate); + + if ($cid) { + $query->where( + [ + $db->quoteName('a.cid') . ' = :clientId', + $db->quoteName('cl.state') . ' = 1', + ] + ) + ->bind(':clientId', $cid, ParameterType::INTEGER); + } + + // Filter by a single or group of categories + if (is_numeric($categoryId)) { + $categoryId = (int) $categoryId; + $type = $this->getState('filter.category_id.include', true) ? ' = ' : ' <> '; + + // Add subcategory check + if ($this->getState('filter.subcategories', false)) { + $levels = (int) $this->getState('filter.max_category_levels', '1'); + + // Create a subquery for the subcategory list + $subQuery = $db->getQuery(true); + $subQuery->select($db->quoteName('sub.id')) + ->from($db->quoteName('#__categories', 'sub')) + ->join( + 'INNER', + $db->quoteName('#__categories', 'this'), + $db->quoteName('sub.lft') . ' > ' . $db->quoteName('this.lft') + . ' AND ' . $db->quoteName('sub.rgt') . ' < ' . $db->quoteName('this.rgt') + ) + ->where( + [ + $db->quoteName('this.id') . ' = :categoryId1', + $db->quoteName('sub.level') . ' <= ' . $db->quoteName('this.level') . ' + :levels', + ] + ); + + // Add the subquery to the main query + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.catid') . $type . ':categoryId2', + $db->quoteName('a.catid') . ' IN (' . $subQuery . ')', + ], + 'OR' + ) + ->bind([':categoryId1', ':categoryId2'], $categoryId, ParameterType::INTEGER) + ->bind(':levels', $levels, ParameterType::INTEGER); + } else { + $query->where($db->quoteName('a.catid') . $type . ':categoryId') + ->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } + } elseif (is_array($categoryId) && (count($categoryId) > 0)) { + $categoryId = ArrayHelper::toInteger($categoryId); + + if ($this->getState('filter.category_id.include', true)) { + $query->whereIn($db->quoteName('a.catid'), $categoryId); + } else { + $query->whereNotIn($db->quoteName('a.catid'), $categoryId); + } + } + + if ($tagSearch) { + if (!$keywords) { + // No keywords, select nothing. + $query->where('0 != 0'); + } else { + $temp = array(); + $config = ComponentHelper::getParams('com_banners'); + $prefix = $config->get('metakey_prefix'); + + if ($categoryId) { + $query->join('LEFT', $db->quoteName('#__categories', 'cat'), $db->quoteName('a.catid') . ' = ' . $db->quoteName('cat.id')); + } + + foreach ($keywords as $key => $keyword) { + $regexp = '[[:<:]]' . $keyword . '[[:>:]]'; + $valuesToBind = [$keyword, $keyword, $regexp]; + + if ($cid) { + $valuesToBind[] = $regexp; + } + + if ($categoryId) { + $valuesToBind[] = $regexp; + } + + // Because values to $query->bind() are passed by reference, using $query->bindArray() here instead to prevent overwriting. + $bounded = $query->bindArray($valuesToBind, ParameterType::STRING); + + $condition1 = $db->quoteName('a.own_prefix') . ' = 1' + . ' AND ' . $db->quoteName('a.metakey_prefix') + . ' = SUBSTRING(' . $bounded[0] . ',1,LENGTH(' . $db->quoteName('a.metakey_prefix') . '))' + . ' OR ' . $db->quoteName('a.own_prefix') . ' = 0' + . ' AND ' . $db->quoteName('cl.own_prefix') . ' = 1' + . ' AND ' . $db->quoteName('cl.metakey_prefix') + . ' = SUBSTRING(' . $bounded[1] . ',1,LENGTH(' . $db->quoteName('cl.metakey_prefix') . '))' + . ' OR ' . $db->quoteName('a.own_prefix') . ' = 0' + . ' AND ' . $db->quoteName('cl.own_prefix') . ' = 0' + . ' AND ' . ($prefix == substr($keyword, 0, strlen($prefix)) ? '0 = 0' : '0 != 0'); + + $condition2 = $db->quoteName('a.metakey') . ' ' . $query->regexp($bounded[2]); + + if ($cid) { + $condition2 .= ' OR ' . $db->quoteName('cl.metakey') . ' ' . $query->regexp($bounded[3]) . ' '; + } + + if ($categoryId) { + $condition2 .= ' OR ' . $db->quoteName('cat.metakey') . ' ' . $query->regexp($bounded[4]) . ' '; + } + + $temp[] = "($condition1) AND ($condition2)"; + } + + $query->where('(' . implode(' OR ', $temp) . ')'); + } + } + + // Filter by language + if ($this->getState('filter.language')) { + $query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); + } + + $query->order($db->quoteName('a.sticky') . ' DESC, ' . ($randomise ? $query->rand() : $db->quoteName('a.ordering'))); + + return $query; + } + + /** + * Get a list of banners. + * + * @return array + * + * @since 1.6 + */ + public function getItems() + { + if ($this->getState('filter.tag_search')) { + // Filter out empty keywords. + $keywords = array_values(array_filter(array_map('trim', $this->getState('filter.keywords')), 'strlen')); + + // Re-set state before running the query. + $this->setState('filter.keywords', $keywords); + + // If no keywords are provided, avoid running the query. + if (!$keywords) { + $this->cache['items'] = array(); + + return $this->cache['items']; + } + } + + if (!isset($this->cache['items'])) { + $this->cache['items'] = parent::getItems(); + + foreach ($this->cache['items'] as &$item) { + $item->params = new Registry($item->params); + } + } + + return $this->cache['items']; + } + + /** + * Makes impressions on a list of banners + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + public function impress() + { + $trackDate = Factory::getDate()->format('Y-m-d H:00:00'); + $trackDate = Factory::getDate($trackDate)->toSql(); + $items = $this->getItems(); + $db = $this->getDatabase(); + $bid = []; + + if (!count($items)) { + return; + } + + foreach ($items as $item) { + $bid[] = (int) $item->id; + } + + // Increment impression made + $query = $db->getQuery(true); + $query->update($db->quoteName('#__banners')) + ->set($db->quoteName('impmade') . ' = ' . $db->quoteName('impmade') . ' + 1') + ->whereIn($db->quoteName('id'), $bid); + $db->setQuery($query); + + try { + $db->execute(); + } catch (ExecutionFailureException $e) { + throw new \Exception($e->getMessage(), 500); + } + + foreach ($items as $item) { + // Track impressions + $trackImpressions = $item->track_impressions; + + if ($trackImpressions < 0 && $item->cid) { + $trackImpressions = $item->client_track_impressions; + } + + if ($trackImpressions < 0) { + $config = ComponentHelper::getParams('com_banners'); + $trackImpressions = $config->get('track_impressions'); + } + + if ($trackImpressions > 0) { + // Is track already created? + // Update count + $query = $db->getQuery(true); + $query->update($db->quoteName('#__banner_tracks')) + ->set($db->quoteName('count') . ' = ' . $db->quoteName('count') . ' + 1') + ->where( + [ + $db->quoteName('track_type') . ' = 1', + $db->quoteName('banner_id') . ' = :id', + $db->quoteName('track_date') . ' = :trackDate', + ] + ) + ->bind(':id', $item->id, ParameterType::INTEGER) + ->bind(':trackDate', $trackDate); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (ExecutionFailureException $e) { + throw new \Exception($e->getMessage(), 500); + } + + if ($db->getAffectedRows() === 0) { + // Insert new count + $query = $db->getQuery(true); + $query->insert($db->quoteName('#__banner_tracks')) + ->columns( + [ + $db->quoteName('count'), + $db->quoteName('track_type'), + $db->quoteName('banner_id'), + $db->quoteName('track_date'), + ] + ) + ->values('1, 1, :id, :trackDate') + ->bind(':id', $item->id, ParameterType::INTEGER) + ->bind(':trackDate', $trackDate); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (ExecutionFailureException $e) { + throw new \Exception($e->getMessage(), 500); + } + } + } + } + } } diff --git a/components/com_banners/src/Service/Category.php b/components/com_banners/src/Service/Category.php index 7c02083006dcb..bc95db659903b 100644 --- a/components/com_banners/src/Service/Category.php +++ b/components/com_banners/src/Service/Category.php @@ -1,4 +1,5 @@ registerTask('apply', 'save'); - } - - /** - * Method to handle cancel - * - * @return void - * - * @since 3.2 - */ - public function cancel() - { - // Redirect back to home(base) page - $this->setRedirect(Uri::base()); - } - - /** - * Method to save global configuration. - * - * @return boolean True on success. - * - * @since 3.2 - */ - public function save() - { - // Check for request forgeries. - $this->checkToken(); - - // Check if the user is authorized to do this. - if (!$this->app->getIdentity()->authorise('core.admin')) - { - $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR')); - $this->app->redirect('index.php'); - } - - // Set FTP credentials, if given. - ClientHelper::setCredentialsFromRequest('ftp'); - - $model = $this->getModel(); - - $form = $model->getForm(); - $data = $this->app->input->post->get('jform', array(), 'array'); - - // Validate the posted data. - $return = $model->validate($form, $data); - - // Check for validation errors. - if ($return === false) - { - /* - * The validate method enqueued all messages for us, so we just need to redirect back. - */ - - // Save the data in the session. - $this->app->setUserState('com_config.config.global.data', $data); - - // Redirect back to the edit screen. - $this->app->redirect(Route::_('index.php?option=com_config&view=config', false)); - } - - // Attempt to save the configuration. - $data = $return; - - // Access backend com_config - $saveClass = $this->factory->createController('Application', 'Administrator', [], $this->app, $this->input); - - // Get a document object - $document = $this->app->getDocument(); - - // Set backend required params - $document->setType('json'); - - // Execute backend controller - $return = $saveClass->save(); - - // Reset params back after requesting from service - $document->setType('html'); - - // Check the return value. - if ($return === false) - { - /* - * The save method enqueued all messages for us, so we just need to redirect back. - */ - - // Save the data in the session. - $this->app->setUserState('com_config.config.global.data', $data); - - // Save failed, go back to the screen and display a notice. - $this->app->redirect(Route::_('index.php?option=com_config&view=config', false)); - } - - // Redirect back to com_config display - $this->app->enqueueMessage(Text::_('COM_CONFIG_SAVE_SUCCESS')); - $this->app->redirect(Route::_('index.php?option=com_config&view=config', false)); - - return true; - } + /** + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The JApplication for the dispatcher + * @param \Joomla\CMS\Input\Input|null $input The Input object for the request + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('apply', 'save'); + } + + /** + * Method to handle cancel + * + * @return void + * + * @since 3.2 + */ + public function cancel() + { + // Redirect back to home(base) page + $this->setRedirect(Uri::base()); + } + + /** + * Method to save global configuration. + * + * @return boolean True on success. + * + * @since 3.2 + */ + public function save() + { + // Check for request forgeries. + $this->checkToken(); + + // Check if the user is authorized to do this. + if (!$this->app->getIdentity()->authorise('core.admin')) { + $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR')); + $this->app->redirect('index.php'); + } + + // Set FTP credentials, if given. + ClientHelper::setCredentialsFromRequest('ftp'); + + $model = $this->getModel(); + + $form = $model->getForm(); + $data = $this->app->input->post->get('jform', array(), 'array'); + + // Validate the posted data. + $return = $model->validate($form, $data); + + // Check for validation errors. + if ($return === false) { + /* + * The validate method enqueued all messages for us, so we just need to redirect back. + */ + + // Save the data in the session. + $this->app->setUserState('com_config.config.global.data', $data); + + // Redirect back to the edit screen. + $this->app->redirect(Route::_('index.php?option=com_config&view=config', false)); + } + + // Attempt to save the configuration. + $data = $return; + + // Access backend com_config + $saveClass = $this->factory->createController('Application', 'Administrator', [], $this->app, $this->input); + + // Get a document object + $document = $this->app->getDocument(); + + // Set backend required params + $document->setType('json'); + + // Execute backend controller + $return = $saveClass->save(); + + // Reset params back after requesting from service + $document->setType('html'); + + // Check the return value. + if ($return === false) { + /* + * The save method enqueued all messages for us, so we just need to redirect back. + */ + + // Save the data in the session. + $this->app->setUserState('com_config.config.global.data', $data); + + // Save failed, go back to the screen and display a notice. + $this->app->redirect(Route::_('index.php?option=com_config&view=config', false)); + } + + // Redirect back to com_config display + $this->app->enqueueMessage(Text::_('COM_CONFIG_SAVE_SUCCESS')); + $this->app->redirect(Route::_('index.php?option=com_config&view=config', false)); + + return true; + } } diff --git a/components/com_config/src/Controller/DisplayController.php b/components/com_config/src/Controller/DisplayController.php index daadff242b4c9..8a26c4735fa55 100644 --- a/components/com_config/src/Controller/DisplayController.php +++ b/components/com_config/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ setRedirect(Uri::base()); - } + /** + * Method to handle cancel + * + * @return void + * + * @since 3.2 + */ + public function cancel() + { + // Redirect back to home(base) page + $this->setRedirect(Uri::base()); + } } diff --git a/components/com_config/src/Controller/ModulesController.php b/components/com_config/src/Controller/ModulesController.php index 5000e4cc91156..d6029eee61b28 100644 --- a/components/com_config/src/Controller/ModulesController.php +++ b/components/com_config/src/Controller/ModulesController.php @@ -1,4 +1,5 @@ registerTask('apply', 'save'); - } - - /** - * Method to handle cancel - * - * @return void - * - * @since 3.2 - */ - public function cancel() - { - // Redirect back to home(base) page - $this->setRedirect(Uri::base()); - } - - /** - * Method to save module editing. - * - * @return void - * - * @since 3.2 - */ - public function save() - { - // Check for request forgeries. - $this->checkToken(); - - // Check if the user is authorized to do this. - $user = $this->app->getIdentity(); - - if (!$user->authorise('module.edit.frontend', 'com_modules.module.' . $this->input->get('id'))) - { - $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $this->app->redirect('index.php'); - } - - // Set FTP credentials, if given. - ClientHelper::setCredentialsFromRequest('ftp'); - - // Get submitted module id - $moduleId = '&id=' . $this->input->getInt('id'); - - // Get returnUri - $returnUri = $this->input->post->get('return', null, 'base64'); - $redirect = ''; - - if (!empty($returnUri)) - { - $redirect = '&return=' . $returnUri; - } - - /** @var AdministratorApplication $app */ - $app = Factory::getContainer()->get(AdministratorApplication::class); - - // Reset Uri cache. - Uri::reset(); - - // Get a document object - $document = $this->app->getDocument(); - - // Load application dependencies. - $app->loadLanguage($this->app->getLanguage()); - $app->loadDocument($document); - $app->loadIdentity($user); - - /** @var \Joomla\CMS\Dispatcher\ComponentDispatcher $dispatcher */ - $dispatcher = $app->bootComponent('com_modules')->getDispatcher($app); - - /** @var ModuleController $controllerClass */ - $controllerClass = $dispatcher->getController('Module'); - - // Set backend required params - $document->setType('json'); - - // Execute backend controller - Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_modules/forms'); - $return = $controllerClass->save(); - - // Reset params back after requesting from service - $document->setType('html'); - - // Check the return value. - if ($return === false) - { - // Save the data in the session. - $data = $this->input->post->get('jform', array(), 'array'); - - $this->app->setUserState('com_config.modules.global.data', $data); - - // Save failed, go back to the screen and display a notice. - $this->app->enqueueMessage(Text::_('JERROR_SAVE_FAILED')); - $this->app->redirect(Route::_('index.php?option=com_config&view=modules' . $moduleId . $redirect, false)); - } - - // Redirect back to com_config display - $this->app->enqueueMessage(Text::_('COM_CONFIG_MODULES_SAVE_SUCCESS')); - - // Set the redirect based on the task. - switch ($this->input->getCmd('task')) - { - case 'apply': - $this->app->redirect(Route::_('index.php?option=com_config&view=modules' . $moduleId . $redirect, false)); - break; - - case 'save': - default: - - if (!empty($returnUri)) - { - $redirect = base64_decode(urldecode($returnUri)); - - // Don't redirect to an external URL. - if (!Uri::isInternal($redirect)) - { - $redirect = Uri::base(); - } - } - else - { - $redirect = Uri::base(); - } - - $this->setRedirect($redirect); - break; - } - } + /** + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The Application for the dispatcher + * @param \Joomla\CMS\Input\Input|null $input The Input object for the request + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('apply', 'save'); + } + + /** + * Method to handle cancel + * + * @return void + * + * @since 3.2 + */ + public function cancel() + { + // Redirect back to home(base) page + $this->setRedirect(Uri::base()); + } + + /** + * Method to save module editing. + * + * @return void + * + * @since 3.2 + */ + public function save() + { + // Check for request forgeries. + $this->checkToken(); + + // Check if the user is authorized to do this. + $user = $this->app->getIdentity(); + + if (!$user->authorise('module.edit.frontend', 'com_modules.module.' . $this->input->get('id'))) { + $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $this->app->redirect('index.php'); + } + + // Set FTP credentials, if given. + ClientHelper::setCredentialsFromRequest('ftp'); + + // Get submitted module id + $moduleId = '&id=' . $this->input->getInt('id'); + + // Get returnUri + $returnUri = $this->input->post->get('return', null, 'base64'); + $redirect = ''; + + if (!empty($returnUri)) { + $redirect = '&return=' . $returnUri; + } + + /** @var AdministratorApplication $app */ + $app = Factory::getContainer()->get(AdministratorApplication::class); + + // Reset Uri cache. + Uri::reset(); + + // Get a document object + $document = $this->app->getDocument(); + + // Load application dependencies. + $app->loadLanguage($this->app->getLanguage()); + $app->loadDocument($document); + $app->loadIdentity($user); + + /** @var \Joomla\CMS\Dispatcher\ComponentDispatcher $dispatcher */ + $dispatcher = $app->bootComponent('com_modules')->getDispatcher($app); + + /** @var ModuleController $controllerClass */ + $controllerClass = $dispatcher->getController('Module'); + + // Set backend required params + $document->setType('json'); + + // Execute backend controller + Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_modules/forms'); + $return = $controllerClass->save(); + + // Reset params back after requesting from service + $document->setType('html'); + + // Check the return value. + if ($return === false) { + // Save the data in the session. + $data = $this->input->post->get('jform', array(), 'array'); + + $this->app->setUserState('com_config.modules.global.data', $data); + + // Save failed, go back to the screen and display a notice. + $this->app->enqueueMessage(Text::_('JERROR_SAVE_FAILED')); + $this->app->redirect(Route::_('index.php?option=com_config&view=modules' . $moduleId . $redirect, false)); + } + + // Redirect back to com_config display + $this->app->enqueueMessage(Text::_('COM_CONFIG_MODULES_SAVE_SUCCESS')); + + // Set the redirect based on the task. + switch ($this->input->getCmd('task')) { + case 'apply': + $this->app->redirect(Route::_('index.php?option=com_config&view=modules' . $moduleId . $redirect, false)); + break; + + case 'save': + default: + if (!empty($returnUri)) { + $redirect = base64_decode(urldecode($returnUri)); + + // Don't redirect to an external URL. + if (!Uri::isInternal($redirect)) { + $redirect = Uri::base(); + } + } else { + $redirect = Uri::base(); + } + + $this->setRedirect($redirect); + break; + } + } } diff --git a/components/com_config/src/Controller/TemplatesController.php b/components/com_config/src/Controller/TemplatesController.php index 6cc36fcaad69a..1a02b72666a32 100644 --- a/components/com_config/src/Controller/TemplatesController.php +++ b/components/com_config/src/Controller/TemplatesController.php @@ -1,4 +1,5 @@ registerTask('apply', 'save'); - } - - /** - * Method to handle cancel - * - * @return boolean True on success. - * - * @since 3.2 - */ - public function cancel() - { - // Redirect back to home(base) page - $this->setRedirect(Uri::base()); - } - - /** - * Method to save global configuration. - * - * @return boolean True on success. - * - * @since 3.2 - */ - public function save() - { - // Check for request forgeries. - $this->checkToken(); - - // Check if the user is authorized to do this. - if (!$this->app->getIdentity()->authorise('core.admin')) - { - $this->setRedirect('index.php', Text::_('JERROR_ALERTNOAUTHOR')); - - return false; - } - - // Set FTP credentials, if given. - ClientHelper::setCredentialsFromRequest('ftp'); - - $app = $this->app; - - // Access backend com_templates - $controllerClass = $app->bootComponent('com_templates') - ->getMVCFactory()->createController('Style', 'Administrator', [], $app, $app->input); - - // Get a document object - $document = $app->getDocument(); - - // Set backend required params - $document->setType('json'); - $this->input->set('id', $app->getTemplate(true)->id); - - // Execute backend controller - $return = $controllerClass->save(); - - // Reset params back after requesting from service - $document->setType('html'); - - // Check the return value. - if ($return === false) - { - // Save failed, go back to the screen and display a notice. - $this->setMessage(Text::sprintf('JERROR_SAVE_FAILED'), 'error'); - $this->setRedirect(Route::_('index.php?option=com_config&view=templates', false)); - - return false; - } - - // Set the success message. - $this->setMessage(Text::_('COM_CONFIG_SAVE_SUCCESS')); - - // Redirect back to com_config display - $this->setRedirect(Route::_('index.php?option=com_config&view=templates', false)); - - return true; - } + /** + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The Application for the dispatcher + * @param \Joomla\CMS\Input\Input|null $input The Input object for the request + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // Apply, Save & New, and Save As copy should be standard on forms. + $this->registerTask('apply', 'save'); + } + + /** + * Method to handle cancel + * + * @return boolean True on success. + * + * @since 3.2 + */ + public function cancel() + { + // Redirect back to home(base) page + $this->setRedirect(Uri::base()); + } + + /** + * Method to save global configuration. + * + * @return boolean True on success. + * + * @since 3.2 + */ + public function save() + { + // Check for request forgeries. + $this->checkToken(); + + // Check if the user is authorized to do this. + if (!$this->app->getIdentity()->authorise('core.admin')) { + $this->setRedirect('index.php', Text::_('JERROR_ALERTNOAUTHOR')); + + return false; + } + + // Set FTP credentials, if given. + ClientHelper::setCredentialsFromRequest('ftp'); + + $app = $this->app; + + // Access backend com_templates + $controllerClass = $app->bootComponent('com_templates') + ->getMVCFactory()->createController('Style', 'Administrator', [], $app, $app->input); + + // Get a document object + $document = $app->getDocument(); + + // Set backend required params + $document->setType('json'); + $this->input->set('id', $app->getTemplate(true)->id); + + // Execute backend controller + $return = $controllerClass->save(); + + // Reset params back after requesting from service + $document->setType('html'); + + // Check the return value. + if ($return === false) { + // Save failed, go back to the screen and display a notice. + $this->setMessage(Text::sprintf('JERROR_SAVE_FAILED'), 'error'); + $this->setRedirect(Route::_('index.php?option=com_config&view=templates', false)); + + return false; + } + + // Set the success message. + $this->setMessage(Text::_('COM_CONFIG_SAVE_SUCCESS')); + + // Redirect back to com_config display + $this->setRedirect(Route::_('index.php?option=com_config&view=templates', false)); + + return true; + } } diff --git a/components/com_config/src/Dispatcher/Dispatcher.php b/components/com_config/src/Dispatcher/Dispatcher.php index 336f520bad3c2..92570ffbd2603 100644 --- a/components/com_config/src/Dispatcher/Dispatcher.php +++ b/components/com_config/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ input->getCmd('task', 'display'); - $view = $this->input->get('view'); - $user = $this->app->getIdentity(); + $task = $this->input->getCmd('task', 'display'); + $view = $this->input->get('view'); + $user = $this->app->getIdentity(); - if (substr($task, 0, 8) === 'modules.' || $view === 'modules') - { - if (!$user->authorise('module.edit.frontend', 'com_modules.module.' . $this->input->get('id'))) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } - elseif (!$user->authorise('core.admin')) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + if (substr($task, 0, 8) === 'modules.' || $view === 'modules') { + if (!$user->authorise('module.edit.frontend', 'com_modules.module.' . $this->input->get('id'))) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } elseif (!$user->authorise('core.admin')) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } } diff --git a/components/com_config/src/Model/ConfigModel.php b/components/com_config/src/Model/ConfigModel.php index 381ff67a69fed..08fa3313ab953 100644 --- a/components/com_config/src/Model/ConfigModel.php +++ b/components/com_config/src/Model/ConfigModel.php @@ -1,4 +1,5 @@ loadForm('com_config.config', 'config', array('control' => 'jform', 'load_data' => $loadData)); + /** + * Method to get a form object. + * + * @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 mixed A JForm object on success, false on failure + * + * @since 3.2 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_config.config', 'config', array('control' => 'jform', 'load_data' => $loadData)); - if (empty($form)) - { - return false; - } + if (empty($form)) { + return false; + } - return $form; - } + return $form; + } } diff --git a/components/com_config/src/Model/FormModel.php b/components/com_config/src/Model/FormModel.php index ccdbf7116771d..992e45bcde129 100644 --- a/components/com_config/src/Model/FormModel.php +++ b/components/com_config/src/Model/FormModel.php @@ -1,4 +1,5 @@ getTable(); - - if (!$table->load($pk)) - { - throw new \RuntimeException($table->getError()); - } - - // Check if this is the user has previously checked out the row. - if (!is_null($table->checked_out) && $table->checked_out != $user->get('id') && !$user->authorise('core.admin', 'com_checkin')) - { - throw new \RuntimeException($table->getError()); - } - - // Attempt to check the row in. - if (!$table->checkIn($pk)) - { - throw new \RuntimeException($table->getError()); - } - } - - return true; - } - - /** - * Method to check-out a row for editing. - * - * @param integer $pk The numeric id of the primary key. - * - * @return boolean False on failure or error, true otherwise. - * - * @since 3.2 - */ - public function checkout($pk = null) - { - // Only attempt to check the row in if it exists. - if ($pk) - { - $user = Factory::getUser(); - - // Get an instance of the row to checkout. - $table = $this->getTable(); - - if (!$table->load($pk)) - { - throw new \RuntimeException($table->getError()); - } - - // Check if this is the user having previously checked out the row. - if (!is_null($table->checked_out) && $table->checked_out != $user->get('id')) - { - throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_CHECKOUT_USER_MISMATCH')); - } - - // Attempt to check the row out. - if (!$table->checkOut($user->get('id'), $pk)) - { - throw new \RuntimeException($table->getError()); - } - } - - return true; - } - - /** - * Method to get a form object. - * - * @param string $name The name of the form. - * @param string $source The form source. Can be XML string if file flag is set to false. - * @param array $options Optional array of options for the form creation. - * @param boolean $clear Optional argument to force load a new form. - * @param string $xpath An optional xpath to search for the fields. - * - * @return mixed JForm object on success, False on error. - * - * @see JForm - * @since 3.2 - */ - protected function loadForm($name, $source = null, $options = array(), $clear = false, $xpath = false) - { - // Handle the optional arguments. - $options['control'] = ArrayHelper::getValue($options, 'control', false); - - // Create a signature hash. - $hash = sha1($source . serialize($options)); - - // Check if we can use a previously loaded form. - if (isset($this->_forms[$hash]) && !$clear) - { - return $this->_forms[$hash]; - } - - // Register the paths for the form. - Form::addFormPath(JPATH_SITE . '/components/com_config/forms'); - Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_config/forms'); - - try - { - // Get the form. - $form = Form::getInstance($name, $source, $options, false, $xpath); - - if (isset($options['load_data']) && $options['load_data']) - { - // Get the data for the form. - $data = $this->loadFormData(); - } - else - { - $data = array(); - } - - // Allow for additional modification of the form, and events to be triggered. - // We pass the data because plugins may require it. - $this->preprocessForm($form, $data); - - // Load the data into the form after the plugins have operated. - $form->bind($data); - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage()); - - return false; - } - - // Store the form for later. - $this->_forms[$hash] = $form; - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return array The default data is an empty array. - * - * @since 3.2 - */ - protected function loadFormData() - { - return array(); - } - - /** - * Method to allow derived classes to preprocess the data. - * - * @param string $context The context identifier. - * @param mixed &$data The data to be processed. It gets altered directly. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 3.2 - */ - protected function preprocessData($context, &$data, $group = 'content') - { - // Get the dispatcher and load the users plugins. - PluginHelper::importPlugin('content'); - - // Trigger the data preparation event. - Factory::getApplication()->triggerEvent('onContentPrepareData', array($context, $data)); - } - - /** - * Method to allow derived classes to preprocess the form. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @see \Joomla\CMS\Form\FormField - * @since 3.2 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - // Import the appropriate plugin group. - PluginHelper::importPlugin($group); - - // Trigger the form preparation event. - Factory::getApplication()->triggerEvent('onContentPrepareForm', array($form, $data)); - } - - /** - * Method to validate the form data. - * - * @param Form $form The form to validate against. - * @param array $data The data to validate. - * @param string $group The name of the field group to validate. - * - * @return mixed Array of filtered data if valid, false otherwise. - * - * @see \Joomla\CMS\Form\FormRule - * @see JFilterInput - * @since 3.2 - */ - public function validate($form, $data, $group = null) - { - // Filter and validate the form data. - $data = $form->filter($data); - $return = $form->validate($data, $group); - - // Check for an error. - if ($return instanceof \Exception) - { - Factory::getApplication()->enqueueMessage($return->getMessage(), 'error'); - - return false; - } - - // Check the validation results. - if ($return === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $message) - { - if ($message instanceof \Exception) - { - $message = $message->getMessage(); - } - - Factory::getApplication()->enqueueMessage($message, 'error'); - } - - return false; - } - - return $data; - } + /** + * Array of form objects. + * + * @var array + * @since 3.2 + */ + protected $forms = array(); + + /** + * Method to checkin a row. + * + * @param integer $pk The numeric id of the primary key. + * + * @return boolean False on failure or error, true otherwise. + * + * @since 3.2 + * @throws \RuntimeException + */ + public function checkin($pk = null) + { + // Only attempt to check the row in if it exists. + if ($pk) { + $user = Factory::getUser(); + + // Get an instance of the row to checkin. + $table = $this->getTable(); + + if (!$table->load($pk)) { + throw new \RuntimeException($table->getError()); + } + + // Check if this is the user has previously checked out the row. + if (!is_null($table->checked_out) && $table->checked_out != $user->get('id') && !$user->authorise('core.admin', 'com_checkin')) { + throw new \RuntimeException($table->getError()); + } + + // Attempt to check the row in. + if (!$table->checkIn($pk)) { + throw new \RuntimeException($table->getError()); + } + } + + return true; + } + + /** + * Method to check-out a row for editing. + * + * @param integer $pk The numeric id of the primary key. + * + * @return boolean False on failure or error, true otherwise. + * + * @since 3.2 + */ + public function checkout($pk = null) + { + // Only attempt to check the row in if it exists. + if ($pk) { + $user = Factory::getUser(); + + // Get an instance of the row to checkout. + $table = $this->getTable(); + + if (!$table->load($pk)) { + throw new \RuntimeException($table->getError()); + } + + // Check if this is the user having previously checked out the row. + if (!is_null($table->checked_out) && $table->checked_out != $user->get('id')) { + throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_CHECKOUT_USER_MISMATCH')); + } + + // Attempt to check the row out. + if (!$table->checkOut($user->get('id'), $pk)) { + throw new \RuntimeException($table->getError()); + } + } + + return true; + } + + /** + * Method to get a form object. + * + * @param string $name The name of the form. + * @param string $source The form source. Can be XML string if file flag is set to false. + * @param array $options Optional array of options for the form creation. + * @param boolean $clear Optional argument to force load a new form. + * @param string $xpath An optional xpath to search for the fields. + * + * @return mixed JForm object on success, False on error. + * + * @see JForm + * @since 3.2 + */ + protected function loadForm($name, $source = null, $options = array(), $clear = false, $xpath = false) + { + // Handle the optional arguments. + $options['control'] = ArrayHelper::getValue($options, 'control', false); + + // Create a signature hash. + $hash = sha1($source . serialize($options)); + + // Check if we can use a previously loaded form. + if (isset($this->_forms[$hash]) && !$clear) { + return $this->_forms[$hash]; + } + + // Register the paths for the form. + Form::addFormPath(JPATH_SITE . '/components/com_config/forms'); + Form::addFormPath(JPATH_ADMINISTRATOR . '/components/com_config/forms'); + + try { + // Get the form. + $form = Form::getInstance($name, $source, $options, false, $xpath); + + if (isset($options['load_data']) && $options['load_data']) { + // Get the data for the form. + $data = $this->loadFormData(); + } else { + $data = array(); + } + + // Allow for additional modification of the form, and events to be triggered. + // We pass the data because plugins may require it. + $this->preprocessForm($form, $data); + + // Load the data into the form after the plugins have operated. + $form->bind($data); + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage()); + + return false; + } + + // Store the form for later. + $this->_forms[$hash] = $form; + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return array The default data is an empty array. + * + * @since 3.2 + */ + protected function loadFormData() + { + return array(); + } + + /** + * Method to allow derived classes to preprocess the data. + * + * @param string $context The context identifier. + * @param mixed &$data The data to be processed. It gets altered directly. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 3.2 + */ + protected function preprocessData($context, &$data, $group = 'content') + { + // Get the dispatcher and load the users plugins. + PluginHelper::importPlugin('content'); + + // Trigger the data preparation event. + Factory::getApplication()->triggerEvent('onContentPrepareData', array($context, $data)); + } + + /** + * Method to allow derived classes to preprocess the form. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @see \Joomla\CMS\Form\FormField + * @since 3.2 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + // Import the appropriate plugin group. + PluginHelper::importPlugin($group); + + // Trigger the form preparation event. + Factory::getApplication()->triggerEvent('onContentPrepareForm', array($form, $data)); + } + + /** + * Method to validate the form data. + * + * @param Form $form The form to validate against. + * @param array $data The data to validate. + * @param string $group The name of the field group to validate. + * + * @return mixed Array of filtered data if valid, false otherwise. + * + * @see \Joomla\CMS\Form\FormRule + * @see JFilterInput + * @since 3.2 + */ + public function validate($form, $data, $group = null) + { + // Filter and validate the form data. + $data = $form->filter($data); + $return = $form->validate($data, $group); + + // Check for an error. + if ($return instanceof \Exception) { + Factory::getApplication()->enqueueMessage($return->getMessage(), 'error'); + + return false; + } + + // Check the validation results. + if ($return === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $message) { + if ($message instanceof \Exception) { + $message = $message->getMessage(); + } + + Factory::getApplication()->enqueueMessage($message, 'error'); + } + + return false; + } + + return $data; + } } diff --git a/components/com_config/src/Model/ModulesModel.php b/components/com_config/src/Model/ModulesModel.php index 9c8871187b0f7..5a79c609bca8d 100644 --- a/components/com_config/src/Model/ModulesModel.php +++ b/components/com_config/src/Model/ModulesModel.php @@ -1,4 +1,5 @@ input->getInt('id'); - - $this->setState('module.id', $pk); - } - - /** - * Method to get the record form. - * - * @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 Form A Form object on success, false on failure - * - * @since 3.2 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_config.modules', 'modules', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to preprocess the form - * - * @param Form $form A form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 3.2 - * @throws \Exception if there is an error loading the form. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $lang = Factory::getLanguage(); - $module = $this->getState()->get('module.name'); - $basePath = JPATH_BASE; - - $formFile = Path::clean($basePath . '/modules/' . $module . '/' . $module . '.xml'); - - // Load the core and/or local language file(s). - $lang->load($module, $basePath) - || $lang->load($module, $basePath . '/modules/' . $module); - - if (file_exists($formFile)) - { - // Get the module form. - if (!$form->loadFile($formFile, false, '//config')) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Attempt to load the xml file. - if (!$xml = simplexml_load_file($formFile)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - } - - // Load the default advanced params - Form::addFormPath(JPATH_BASE . '/components/com_config/model/form'); - $form->loadFile('modules_advanced', false); - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to get list of module positions in current template - * - * @return array - * - * @since 3.2 - */ - public function getPositions() - { - $lang = Factory::getLanguage(); - $templateName = Factory::getApplication()->getTemplate(); - - // Load templateDetails.xml file - $path = Path::clean(JPATH_BASE . '/templates/' . $templateName . '/templateDetails.xml'); - $currentTemplatePositions = array(); - - if (file_exists($path)) - { - $xml = simplexml_load_file($path); - - if (isset($xml->positions[0])) - { - // Load language files - $lang->load('tpl_' . $templateName . '.sys', JPATH_BASE) - || $lang->load('tpl_' . $templateName . '.sys', JPATH_BASE . '/templates/' . $templateName); - - foreach ($xml->positions[0] as $position) - { - $value = (string) $position; - $text = preg_replace('/[^a-zA-Z0-9_\-]/', '_', 'TPL_' . strtoupper($templateName) . '_POSITION_' . strtoupper($value)); - - // Construct list of positions - $currentTemplatePositions[] = self::createOption($value, Text::_($text) . ' [' . $value . ']'); - } - } - } - - $templateGroups = array(); - - // Add an empty value to be able to deselect a module position - $option = self::createOption(); - $templateGroups[''] = self::createOptionGroup('', array($option)); - - $templateGroups[$templateName] = self::createOptionGroup($templateName, $currentTemplatePositions); - - // Add custom position to options - $customGroupText = Text::_('COM_MODULES_CUSTOM_POSITION'); - - $editPositions = true; - $customPositions = self::getActivePositions(0, $editPositions); - $templateGroups[$customGroupText] = self::createOptionGroup($customGroupText, $customPositions); - - return $templateGroups; - } - - /** - * Get a list of modules positions - * - * @param integer $clientId Client ID - * @param boolean $editPositions Allow to edit the positions - * - * @return array A list of positions - * - * @since 3.6.3 - */ - public static function getActivePositions($clientId, $editPositions = false) - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select('DISTINCT position') - ->from($db->quoteName('#__modules')) - ->where($db->quoteName('client_id') . ' = ' . (int) $clientId) - ->order($db->quoteName('position')); - - $db->setQuery($query); - - try - { - $positions = $db->loadColumn(); - $positions = is_array($positions) ? $positions : array(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return; - } - - // Build the list - $options = array(); - - foreach ($positions as $position) - { - if (!$position && !$editPositions) - { - $options[] = HTMLHelper::_('select.option', 'none', ':: ' . Text::_('JNONE') . ' ::'); - } - else - { - $options[] = HTMLHelper::_('select.option', $position, $position); - } - } - - return $options; - } - - /** - * Create and return a new Option - * - * @param string $value The option value [optional] - * @param string $text The option text [optional] - * - * @return object The option as an object (stdClass instance) - * - * @since 3.6.3 - */ - private static function createOption($value = '', $text = '') - { - if (empty($text)) - { - $text = $value; - } - - $option = new \stdClass; - $option->value = $value; - $option->text = $text; - - return $option; - } - - /** - * Create and return a new Option Group - * - * @param string $label Value and label for group [optional] - * @param array $options Array of options to insert into group [optional] - * - * @return array Return the new group as an array - * - * @since 3.6.3 - */ - private static function createOptionGroup($label = '', $options = array()) - { - $group = array(); - $group['value'] = $label; - $group['text'] = $label; - $group['items'] = $options; - - return $group; - } + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 3.2 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the User state. + $pk = $app->input->getInt('id'); + + $this->setState('module.id', $pk); + } + + /** + * Method to get the record form. + * + * @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 Form A Form object on success, false on failure + * + * @since 3.2 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_config.modules', 'modules', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to preprocess the form + * + * @param Form $form A form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 3.2 + * @throws \Exception if there is an error loading the form. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $lang = Factory::getLanguage(); + $module = $this->getState()->get('module.name'); + $basePath = JPATH_BASE; + + $formFile = Path::clean($basePath . '/modules/' . $module . '/' . $module . '.xml'); + + // Load the core and/or local language file(s). + $lang->load($module, $basePath) + || $lang->load($module, $basePath . '/modules/' . $module); + + if (file_exists($formFile)) { + // Get the module form. + if (!$form->loadFile($formFile, false, '//config')) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Attempt to load the xml file. + if (!$xml = simplexml_load_file($formFile)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + } + + // Load the default advanced params + Form::addFormPath(JPATH_BASE . '/components/com_config/model/form'); + $form->loadFile('modules_advanced', false); + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to get list of module positions in current template + * + * @return array + * + * @since 3.2 + */ + public function getPositions() + { + $lang = Factory::getLanguage(); + $templateName = Factory::getApplication()->getTemplate(); + + // Load templateDetails.xml file + $path = Path::clean(JPATH_BASE . '/templates/' . $templateName . '/templateDetails.xml'); + $currentTemplatePositions = array(); + + if (file_exists($path)) { + $xml = simplexml_load_file($path); + + if (isset($xml->positions[0])) { + // Load language files + $lang->load('tpl_' . $templateName . '.sys', JPATH_BASE) + || $lang->load('tpl_' . $templateName . '.sys', JPATH_BASE . '/templates/' . $templateName); + + foreach ($xml->positions[0] as $position) { + $value = (string) $position; + $text = preg_replace('/[^a-zA-Z0-9_\-]/', '_', 'TPL_' . strtoupper($templateName) . '_POSITION_' . strtoupper($value)); + + // Construct list of positions + $currentTemplatePositions[] = self::createOption($value, Text::_($text) . ' [' . $value . ']'); + } + } + } + + $templateGroups = array(); + + // Add an empty value to be able to deselect a module position + $option = self::createOption(); + $templateGroups[''] = self::createOptionGroup('', array($option)); + + $templateGroups[$templateName] = self::createOptionGroup($templateName, $currentTemplatePositions); + + // Add custom position to options + $customGroupText = Text::_('COM_MODULES_CUSTOM_POSITION'); + + $editPositions = true; + $customPositions = self::getActivePositions(0, $editPositions); + $templateGroups[$customGroupText] = self::createOptionGroup($customGroupText, $customPositions); + + return $templateGroups; + } + + /** + * Get a list of modules positions + * + * @param integer $clientId Client ID + * @param boolean $editPositions Allow to edit the positions + * + * @return array A list of positions + * + * @since 3.6.3 + */ + public static function getActivePositions($clientId, $editPositions = false) + { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select('DISTINCT position') + ->from($db->quoteName('#__modules')) + ->where($db->quoteName('client_id') . ' = ' . (int) $clientId) + ->order($db->quoteName('position')); + + $db->setQuery($query); + + try { + $positions = $db->loadColumn(); + $positions = is_array($positions) ? $positions : array(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return; + } + + // Build the list + $options = array(); + + foreach ($positions as $position) { + if (!$position && !$editPositions) { + $options[] = HTMLHelper::_('select.option', 'none', ':: ' . Text::_('JNONE') . ' ::'); + } else { + $options[] = HTMLHelper::_('select.option', $position, $position); + } + } + + return $options; + } + + /** + * Create and return a new Option + * + * @param string $value The option value [optional] + * @param string $text The option text [optional] + * + * @return object The option as an object (stdClass instance) + * + * @since 3.6.3 + */ + private static function createOption($value = '', $text = '') + { + if (empty($text)) { + $text = $value; + } + + $option = new \stdClass(); + $option->value = $value; + $option->text = $text; + + return $option; + } + + /** + * Create and return a new Option Group + * + * @param string $label Value and label for group [optional] + * @param array $options Array of options to insert into group [optional] + * + * @return array Return the new group as an array + * + * @since 3.6.3 + */ + private static function createOptionGroup($label = '', $options = array()) + { + $group = array(); + $group['value'] = $label; + $group['text'] = $label; + $group['items'] = $options; + + return $group; + } } diff --git a/components/com_config/src/Model/TemplatesModel.php b/components/com_config/src/Model/TemplatesModel.php index f9f901679f054..9f3793b8b51dc 100644 --- a/components/com_config/src/Model/TemplatesModel.php +++ b/components/com_config/src/Model/TemplatesModel.php @@ -1,4 +1,5 @@ setState('params', ComponentHelper::getParams('com_templates')); - } - - /** - * Method to get the record form. - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A JForm object on success, false on failure - * - * @since 3.2 - */ - public function getForm($data = array(), $loadData = true) - { - try - { - // Get the form. - $form = $this->loadForm('com_config.templates', 'templates', array('load_data' => $loadData)); - - $data = array(); - $this->preprocessForm($form, $data); - - // Load the data into the form - $form->bind($data); - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage()); - - return false; - } - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to preprocess the form - * - * @param Form $form A form object. - * @param mixed $data The data expected for the form. - * @param string $group Plugin group to load - * - * @return void - * - * @since 3.2 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $lang = Factory::getLanguage(); - - $template = Factory::getApplication()->getTemplate(); - - // Load the core and/or local language file(s). - $lang->load('tpl_' . $template, JPATH_BASE) - || $lang->load('tpl_' . $template, JPATH_BASE . '/templates/' . $template); - - // Look for com_config.xml, which contains fields to display - $formFile = Path::clean(JPATH_BASE . '/templates/' . $template . '/com_config.xml'); - - if (!file_exists($formFile)) - { - // If com_config.xml not found, fall back to templateDetails.xml - $formFile = Path::clean(JPATH_BASE . '/templates/' . $template . '/templateDetails.xml'); - } - - // Get the template form. - if (file_exists($formFile) && !$form->loadFile($formFile, false, '//config')) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Attempt to load the xml file. - if (!$xml = simplexml_load_file($formFile)) - { - throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); - } - - // Trigger the default form events. - parent::preprocessForm($form, $data, $group); - } + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return null + * + * @since 3.2 + */ + protected function populateState() + { + parent::populateState(); + + $this->setState('params', ComponentHelper::getParams('com_templates')); + } + + /** + * Method to get the record form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A JForm object on success, false on failure + * + * @since 3.2 + */ + public function getForm($data = array(), $loadData = true) + { + try { + // Get the form. + $form = $this->loadForm('com_config.templates', 'templates', array('load_data' => $loadData)); + + $data = array(); + $this->preprocessForm($form, $data); + + // Load the data into the form + $form->bind($data); + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage()); + + return false; + } + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to preprocess the form + * + * @param Form $form A form object. + * @param mixed $data The data expected for the form. + * @param string $group Plugin group to load + * + * @return void + * + * @since 3.2 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $lang = Factory::getLanguage(); + + $template = Factory::getApplication()->getTemplate(); + + // Load the core and/or local language file(s). + $lang->load('tpl_' . $template, JPATH_BASE) + || $lang->load('tpl_' . $template, JPATH_BASE . '/templates/' . $template); + + // Look for com_config.xml, which contains fields to display + $formFile = Path::clean(JPATH_BASE . '/templates/' . $template . '/com_config.xml'); + + if (!file_exists($formFile)) { + // If com_config.xml not found, fall back to templateDetails.xml + $formFile = Path::clean(JPATH_BASE . '/templates/' . $template . '/templateDetails.xml'); + } + + // Get the template form. + if (file_exists($formFile) && !$form->loadFile($formFile, false, '//config')) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Attempt to load the xml file. + if (!$xml = simplexml_load_file($formFile)) { + throw new \Exception(Text::_('JERROR_LOADFILE_FAILED')); + } + + // Trigger the default form events. + parent::preprocessForm($form, $data, $group); + } } diff --git a/components/com_config/src/Service/Router.php b/components/com_config/src/Service/Router.php index a2935a9425925..0346fc65425da 100644 --- a/components/com_config/src/Service/Router.php +++ b/components/com_config/src/Service/Router.php @@ -1,4 +1,5 @@ registerView(new RouterViewConfiguration('config')); - $this->registerView(new RouterViewConfiguration('templates')); + /** + * Config Component router constructor + * + * @param SiteApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + */ + public function __construct(SiteApplication $app, AbstractMenu $menu) + { + $this->registerView(new RouterViewConfiguration('config')); + $this->registerView(new RouterViewConfiguration('templates')); - parent::__construct($app, $menu); + parent::__construct($app, $menu); - $this->attachRule(new MenuRules($this)); - $this->attachRule(new StandardRules($this)); - $this->attachRule(new NomenuRules($this)); - } + $this->attachRule(new MenuRules($this)); + $this->attachRule(new StandardRules($this)); + $this->attachRule(new NomenuRules($this)); + } } diff --git a/components/com_config/src/View/Config/HtmlView.php b/components/com_config/src/View/Config/HtmlView.php index 6cb0b6ff1636e..eef98b1ee4a5c 100644 --- a/components/com_config/src/View/Config/HtmlView.php +++ b/components/com_config/src/View/Config/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser(); - $this->userIsSuperAdmin = $user->authorise('core.admin'); - - // Access backend com_config - $requestController = new RequestController; - - // Execute backend controller - $serviceData = json_decode($requestController->getJson(), true); - - $form = $this->getForm(); - - if ($form) - { - $form->bind($serviceData); - } - - $this->form = $form; - $this->data = $serviceData; - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 4.0.0 - */ - protected function _prepareDocument() - { - $params = Factory::getApplication()->getParams(); - - // Because the application sets a default page title, we need to get it - // right from the menu item itself - - $this->setDocumentTitle($params->get('page_title', '')); - - if ($params->get('menu-meta_description')) - { - $this->document->setDescription($params->get('menu-meta_description')); - } - - if ($params->get('robots')) - { - $this->document->setMetaData('robots', $params->get('robots')); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - $this->params = &$params; - } + /** + * The form object + * + * @var \Joomla\CMS\Form\Form + * + * @since 3.2 + */ + public $form; + + /** + * The data to be displayed in the form + * + * @var array + * + * @since 3.2 + */ + public $data; + + /** + * Is the current user a super administrator? + * + * @var boolean + * + * @since 3.2 + */ + protected $userIsSuperAdmin; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 3.2 + */ + public function display($tpl = null) + { + $user = $this->getCurrentUser(); + $this->userIsSuperAdmin = $user->authorise('core.admin'); + + // Access backend com_config + $requestController = new RequestController(); + + // Execute backend controller + $serviceData = json_decode($requestController->getJson(), true); + + $form = $this->getForm(); + + if ($form) { + $form->bind($serviceData); + } + + $this->form = $form; + $this->data = $serviceData; + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 4.0.0 + */ + protected function _prepareDocument() + { + $params = Factory::getApplication()->getParams(); + + // Because the application sets a default page title, we need to get it + // right from the menu item itself + + $this->setDocumentTitle($params->get('page_title', '')); + + if ($params->get('menu-meta_description')) { + $this->document->setDescription($params->get('menu-meta_description')); + } + + if ($params->get('robots')) { + $this->document->setMetaData('robots', $params->get('robots')); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + $this->params = &$params; + } } diff --git a/components/com_config/src/View/Modules/HtmlView.php b/components/com_config/src/View/Modules/HtmlView.php index e5e3f5be92925..660254dedf553 100644 --- a/components/com_config/src/View/Modules/HtmlView.php +++ b/components/com_config/src/View/Modules/HtmlView.php @@ -1,4 +1,5 @@ getLanguage(); - $lang->load('', JPATH_ADMINISTRATOR, $lang->getTag()); - $lang->load('com_modules', JPATH_ADMINISTRATOR, $lang->getTag()); + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 3.2 + */ + public function display($tpl = null) + { + $lang = Factory::getApplication()->getLanguage(); + $lang->load('', JPATH_ADMINISTRATOR, $lang->getTag()); + $lang->load('com_modules', JPATH_ADMINISTRATOR, $lang->getTag()); - // @todo Move and clean up - $module = (new \Joomla\Component\Modules\Administrator\Model\ModuleModel)->getItem(Factory::getApplication()->input->getInt('id')); + // @todo Move and clean up + $module = (new \Joomla\Component\Modules\Administrator\Model\ModuleModel())->getItem(Factory::getApplication()->input->getInt('id')); - $moduleData = $module->getProperties(); - unset($moduleData['xml']); + $moduleData = $module->getProperties(); + unset($moduleData['xml']); - /** @var \Joomla\Component\Config\Site\Model\ModulesModel $model */ - $model = $this->getModel(); + /** @var \Joomla\Component\Config\Site\Model\ModulesModel $model */ + $model = $this->getModel(); - // Need to add module name to the state of model - $model->getState()->set('module.name', $moduleData['module']); + // Need to add module name to the state of model + $model->getState()->set('module.name', $moduleData['module']); - /** @var Form form */ - $this->form = $this->get('form'); - $this->positions = $this->get('positions'); - $this->item = $moduleData; + /** @var Form form */ + $this->form = $this->get('form'); + $this->positions = $this->get('positions'); + $this->item = $moduleData; - if ($this->form) - { - $this->form->bind($moduleData); - } + if ($this->form) { + $this->form->bind($moduleData); + } - $this->_prepareDocument(); + $this->_prepareDocument(); - parent::display($tpl); - } + parent::display($tpl); + } - /** - * Prepares the document. - * - * @return void - * - * @since 4.0.0 - */ - protected function _prepareDocument() - { - // There is no menu item for this so we have to use the title from the component - $this->setDocumentTitle(Text::_('COM_CONFIG_MODULES_SETTINGS_TITLE')); - } + /** + * Prepares the document. + * + * @return void + * + * @since 4.0.0 + */ + protected function _prepareDocument() + { + // There is no menu item for this so we have to use the title from the component + $this->setDocumentTitle(Text::_('COM_CONFIG_MODULES_SETTINGS_TITLE')); + } } diff --git a/components/com_config/src/View/Templates/HtmlView.php b/components/com_config/src/View/Templates/HtmlView.php index 39478d9b5d734..3d0175b2b6fc9 100644 --- a/components/com_config/src/View/Templates/HtmlView.php +++ b/components/com_config/src/View/Templates/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser(); - $this->userIsSuperAdmin = $user->authorise('core.admin'); - - $app = Factory::getApplication(); - - $app->input->set('id', $app->getTemplate(true)->id); - - /** @var MVCFactory $factory */ - $factory = $app->bootComponent('com_templates')->getMVCFactory(); - - $view = $factory->createView('Style', 'Administrator', 'Json'); - $view->setModel($factory->createModel('Style', 'Administrator'), true); - - $view->document = $this->document; - - $json = $view->display(); - - // Execute backend controller - $serviceData = json_decode($json, true); - - // Access backend com_config - $requestController = new RequestController; - - // Execute backend controller - $configData = json_decode($requestController->getJson(), true); - - $data = array_merge($configData, $serviceData); - - /** @var Form $form */ - $form = $this->getForm(); - - if ($form) - { - $form->bind($data); - } - - $this->form = $form; - - $this->data = $serviceData; - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 4.0.0 - */ - protected function _prepareDocument() - { - $params = Factory::getApplication()->getParams(); - - // Because the application sets a default page title, we need to get it - // right from the menu item itself - $this->setDocumentTitle($params->get('page_title', '')); - - if ($params->get('menu-meta_description')) - { - $this->document->setDescription($params->get('menu-meta_description')); - } - - if ($params->get('robots')) - { - $this->document->setMetaData('robots', $params->get('robots')); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - $this->params = &$params; - } + /** + * The data to be displayed in the form + * + * @var array + * + * @since 3.2 + */ + public $item; + + /** + * The form object + * + * @var Form + * + * @since 3.2 + */ + public $form; + + /** + * Is the current user a super administrator? + * + * @var boolean + * + * @since 3.2 + */ + protected $userIsSuperAdmin; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * Method to render the view. + * + * @return void + * + * @since 3.2 + */ + public function display($tpl = null) + { + $user = $this->getCurrentUser(); + $this->userIsSuperAdmin = $user->authorise('core.admin'); + + $app = Factory::getApplication(); + + $app->input->set('id', $app->getTemplate(true)->id); + + /** @var MVCFactory $factory */ + $factory = $app->bootComponent('com_templates')->getMVCFactory(); + + $view = $factory->createView('Style', 'Administrator', 'Json'); + $view->setModel($factory->createModel('Style', 'Administrator'), true); + + $view->document = $this->document; + + $json = $view->display(); + + // Execute backend controller + $serviceData = json_decode($json, true); + + // Access backend com_config + $requestController = new RequestController(); + + // Execute backend controller + $configData = json_decode($requestController->getJson(), true); + + $data = array_merge($configData, $serviceData); + + /** @var Form $form */ + $form = $this->getForm(); + + if ($form) { + $form->bind($data); + } + + $this->form = $form; + + $this->data = $serviceData; + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 4.0.0 + */ + protected function _prepareDocument() + { + $params = Factory::getApplication()->getParams(); + + // Because the application sets a default page title, we need to get it + // right from the menu item itself + $this->setDocumentTitle($params->get('page_title', '')); + + if ($params->get('menu-meta_description')) { + $this->document->setDescription($params->get('menu-meta_description')); + } + + if ($params->get('robots')) { + $this->document->setMetaData('robots', $params->get('robots')); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + $this->params = &$params; + } } diff --git a/components/com_config/tmpl/config/default.php b/components/com_config/tmpl/config/default.php index 95398edb0da8b..f7f88c3e1e1c0 100644 --- a/components/com_config/tmpl/config/default.php +++ b/components/com_config/tmpl/config/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_config.config') - ->useScript('inlinehelp'); + ->useScript('form.validate') + ->useScript('com_config.config') + ->useScript('inlinehelp'); ?> params->get('show_page_heading')) : ?> - +
    -
    - -
    - - loadTemplate('site'); ?> - loadTemplate('seo'); ?> - loadTemplate('metadata'); ?> - - - - -
    - - -
    +
    + +
    + + loadTemplate('site'); ?> + loadTemplate('seo'); ?> + loadTemplate('metadata'); ?> + + + + +
    + + +
    diff --git a/components/com_config/tmpl/config/default_metadata.php b/components/com_config/tmpl/config/default_metadata.php index 4daf35c503e09..f361d59281319 100644 --- a/components/com_config/tmpl/config/default_metadata.php +++ b/components/com_config/tmpl/config/default_metadata.php @@ -1,4 +1,5 @@
    - + - form->getFieldset('metadata') as $field) : ?> -
    - label; ?> - input; ?> - description): ?> -
    - description) ?> -
    - -
    - + form->getFieldset('metadata') as $field) : ?> +
    + label; ?> + input; ?> + description) : ?> +
    + description) ?> +
    + +
    +
    diff --git a/components/com_config/tmpl/config/default_seo.php b/components/com_config/tmpl/config/default_seo.php index 0c26e03d712d3..911390985f711 100644 --- a/components/com_config/tmpl/config/default_seo.php +++ b/components/com_config/tmpl/config/default_seo.php @@ -1,4 +1,5 @@
    - + - form->getFieldset('seo') as $field) : ?> -
    - label; ?> - input; ?> - description): ?> -
    - description) ?> -
    - -
    - + form->getFieldset('seo') as $field) : ?> +
    + label; ?> + input; ?> + description) : ?> +
    + description) ?> +
    + +
    +
    diff --git a/components/com_config/tmpl/config/default_site.php b/components/com_config/tmpl/config/default_site.php index 1e13994ea8c9e..571b7de73cb3b 100644 --- a/components/com_config/tmpl/config/default_site.php +++ b/components/com_config/tmpl/config/default_site.php @@ -1,4 +1,5 @@
    - + - form->getFieldset('site') as $field) : ?> -
    - label; ?> - input; ?> - description): ?> -
    - description) ?> -
    - -
    - + form->getFieldset('site') as $field) : ?> +
    + label; ?> + input; ?> + description) : ?> +
    + description) ?> +
    + +
    +
    diff --git a/components/com_config/tmpl/modules/default.php b/components/com_config/tmpl/modules/default.php index fe76134ce3394..6fc48f5163366 100644 --- a/components/com_config/tmpl/modules/default.php +++ b/components/com_config/tmpl/modules/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_config.modules'); + ->useScript('form.validate') + ->useScript('com_config.modules'); $editorText = false; $moduleXml = JPATH_SITE . '/modules/' . $this->item['module'] . '/' . $this->item['module'] . '.xml'; -if (File::exists($moduleXml)) -{ - $xml = simplexml_load_file($moduleXml); +if (File::exists($moduleXml)) { + $xml = simplexml_load_file($moduleXml); - if (isset($xml->customContent)) - { - $editorText = true; - } + if (isset($xml->customContent)) { + $editorText = true; + } } // If multi-language site, make language read-only -if (Multilanguage::isEnabled()) -{ - $this->form->setFieldAttribute('language', 'readonly', 'true'); +if (Multilanguage::isEnabled()) { + $this->form->setFieldAttribute('language', 'readonly', 'true'); } ?>
    -
    -
    - - -
    - - item['title']; ?> -    - - item['module']; ?> -
    -
    - -
    -
    - -
    -
    - form->getLabel('title'); ?> -
    -
    - form->getInput('title'); ?> -
    -
    -
    -
    - form->getLabel('showtitle'); ?> -
    -
    - form->getInput('showtitle'); ?> -
    -
    -
    -
    - form->getLabel('position'); ?> -
    -
    - form->getInput('position'); ?> -
    -
    - -
    - - authorise('core.edit.state', 'com_modules.module.' . $this->item['id'])) : ?> -
    -
    - form->getLabel('published'); ?> -
    -
    - form->getInput('published'); ?> -
    -
    - - -
    -
    - form->getLabel('publish_up'); ?> -
    -
    - form->getInput('publish_up'); ?> -
    -
    -
    -
    - form->getLabel('publish_down'); ?> -
    -
    - form->getInput('publish_down'); ?> -
    -
    - -
    -
    - form->getLabel('access'); ?> -
    -
    - form->getInput('access'); ?> -
    -
    -
    -
    - form->getLabel('ordering'); ?> -
    -
    - form->getInput('ordering'); ?> -
    -
    - - -
    -
    - form->getLabel('language'); ?> -
    -
    - form->getInput('language'); ?> -
    -
    - - -
    -
    - form->getLabel('note'); ?> -
    -
    - form->getInput('note'); ?> -
    -
    - -
    - -
    - loadTemplate('options'); ?> -
    - - -
    - form->getInput('content'); ?> -
    - -
    - - - - - -
    -
    - - - -
    -
    -
    +
    +
    + + +
    + + item['title']; ?> +    + + item['module']; ?> +
    +
    + +
    +
    + +
    +
    + form->getLabel('title'); ?> +
    +
    + form->getInput('title'); ?> +
    +
    +
    +
    + form->getLabel('showtitle'); ?> +
    +
    + form->getInput('showtitle'); ?> +
    +
    +
    +
    + form->getLabel('position'); ?> +
    +
    + form->getInput('position'); ?> +
    +
    + +
    + + authorise('core.edit.state', 'com_modules.module.' . $this->item['id'])) : ?> +
    +
    + form->getLabel('published'); ?> +
    +
    + form->getInput('published'); ?> +
    +
    + + +
    +
    + form->getLabel('publish_up'); ?> +
    +
    + form->getInput('publish_up'); ?> +
    +
    +
    +
    + form->getLabel('publish_down'); ?> +
    +
    + form->getInput('publish_down'); ?> +
    +
    + +
    +
    + form->getLabel('access'); ?> +
    +
    + form->getInput('access'); ?> +
    +
    +
    +
    + form->getLabel('ordering'); ?> +
    +
    + form->getInput('ordering'); ?> +
    +
    + + +
    +
    + form->getLabel('language'); ?> +
    +
    + form->getInput('language'); ?> +
    +
    + + +
    +
    + form->getLabel('note'); ?> +
    +
    + form->getInput('note'); ?> +
    +
    + +
    + +
    + loadTemplate('options'); ?> +
    + + +
    + form->getInput('content'); ?> +
    + +
    + + + + + +
    +
    + + + +
    +
    +
    diff --git a/components/com_config/tmpl/modules/default_options.php b/components/com_config/tmpl/modules/default_options.php index 28c4c73007070..e22325152df33 100644 --- a/components/com_config/tmpl/modules/default_options.php +++ b/components/com_config/tmpl/modules/default_options.php @@ -1,4 +1,5 @@ $fieldSet) : - -$label = !empty($fieldSet->label) ? $fieldSet->label : 'COM_MODULES_' . strtoupper($name) . '_FIELDSET_LABEL'; -$class = isset($fieldSet->class) && !empty($fieldSet->class) ? $fieldSet->class : ''; + $label = !empty($fieldSet->label) ? $fieldSet->label : 'COM_MODULES_' . strtoupper($name) . '_FIELDSET_LABEL'; + $class = isset($fieldSet->class) && !empty($fieldSet->class) ? $fieldSet->class : ''; -if (isset($fieldSet->description) && trim($fieldSet->description)) : -echo '

    ' . $this->escape(Text::_($fieldSet->description)) . '

    '; -endif; -?> - + if (isset($fieldSet->description) && trim($fieldSet->description)) : + echo '

    ' . $this->escape(Text::_($fieldSet->description)) . '

    '; + endif; + ?> + - + diff --git a/components/com_config/tmpl/templates/default.php b/components/com_config/tmpl/templates/default.php index 2c0a86135de7e..c3531496379c3 100644 --- a/components/com_config/tmpl/templates/default.php +++ b/components/com_config/tmpl/templates/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_config.templates'); + ->useScript('form.validate') + ->useScript('com_config.templates'); ?> params->get('show_page_heading')) : ?> - +
    -
    -
    -
    - loadTemplate('options'); ?> -
    -
    -
    - - - - -
    - - +
    +
    +
    + loadTemplate('options'); ?> +
    +
    +
    + + + + +
    + +
    diff --git a/components/com_config/tmpl/templates/default_options.php b/components/com_config/tmpl/templates/default_options.php index ca9f12253870a..a8c8be624b923 100644 --- a/components/com_config/tmpl/templates/default_options.php +++ b/components/com_config/tmpl/templates/default_options.php @@ -1,4 +1,5 @@ form->renderFieldset('com_config'); -} -else -{ - // Fall-back to display all in params - foreach ($fieldSets as $name => $fieldSet) - { - $label = !empty($fieldSet->label) ? $fieldSet->label : 'COM_CONFIG_' . $name . '_FIELDSET_LABEL'; - - if (isset($fieldSet->description) && trim($fieldSet->description)) - { - echo '

    ' . $this->escape(Text::_($fieldSet->description)) . '

    '; - } - - echo $this->form->renderFieldset($name); - - } +if (!empty($fieldSets['com_config'])) { + echo $this->form->renderFieldset('com_config'); +} else { + // Fall-back to display all in params + foreach ($fieldSets as $name => $fieldSet) { + $label = !empty($fieldSet->label) ? $fieldSet->label : 'COM_CONFIG_' . $name . '_FIELDSET_LABEL'; + + if (isset($fieldSet->description) && trim($fieldSet->description)) { + echo '

    ' . $this->escape(Text::_($fieldSet->description)) . '

    '; + } + + echo $this->form->renderFieldset($name); + } } diff --git a/components/com_contact/helpers/route.php b/components/com_contact/helpers/route.php index 797298b1193c4..4531aa498252f 100644 --- a/components/com_contact/helpers/route.php +++ b/components/com_contact/helpers/route.php @@ -1,13 +1,14 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt - */ -defined('_JEXEC') or die; + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + */ use Joomla\Component\Contact\Site\Helper\RouteHelper; diff --git a/components/com_contact/layouts/field/render.php b/components/com_contact/layouts/field/render.php index 79d3be469fcac..3583365b3e416 100644 --- a/components/com_contact/layouts/field/render.php +++ b/components/com_contact/layouts/field/render.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; -if (!array_key_exists('field', $displayData)) -{ - return; +if (!array_key_exists('field', $displayData)) { + return; } $field = $displayData['field']; @@ -22,26 +23,24 @@ $showLabel = $field->params->get('showlabel'); $labelClass = $field->params->get('label_render_class'); -if ($field->context == 'com_contact.mail') -{ - // Prepare the value for the contact form mail - $value = html_entity_decode($value); +if ($field->context == 'com_contact.mail') { + // Prepare the value for the contact form mail + $value = html_entity_decode($value); - echo ($showLabel ? $label . ': ' : '') . $value . "\r\n"; - return; + echo ($showLabel ? $label . ': ' : '') . $value . "\r\n"; + return; } -if (!strlen($value)) -{ - return; +if (!strlen($value)) { + return; } ?>
    - - : - + + : +
    - +
    diff --git a/components/com_contact/layouts/fields/render.php b/components/com_contact/layouts/fields/render.php index eb0ab9ec70a4c..1f4821b5d5c74 100644 --- a/components/com_contact/layouts/fields/render.php +++ b/components/com_contact/layouts/fields/render.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\Component\Fields\Administrator\Helper\FieldsHelper; // Check if we have all the data -if (!array_key_exists('item', $displayData) || !array_key_exists('context', $displayData)) -{ - return; +if (!array_key_exists('item', $displayData) || !array_key_exists('context', $displayData)) { + return; } // Setting up for display $item = $displayData['item']; -if (!$item) -{ - return; +if (!$item) { + return; } $context = $displayData['context']; -if (!$context) -{ - return; +if (!$context) { + return; } $parts = explode('.', $context); $component = $parts[0]; $fields = null; -if (array_key_exists('fields', $displayData)) -{ - $fields = $displayData['fields']; -} -else -{ - $fields = $item->jcfields ?: FieldsHelper::getFields($context, $item, true); +if (array_key_exists('fields', $displayData)) { + $fields = $displayData['fields']; +} else { + $fields = $item->jcfields ?: FieldsHelper::getFields($context, $item, true); } -if (!$fields) -{ - return; +if (!$fields) { + return; } // Check if we have mail context in first element $isMail = (reset($fields)->context == 'com_contact.mail'); -if (!$isMail) -{ - // Print the container tag - echo '
    '; +if (!$isMail) { + // Print the container tag + echo '
    '; } // Loop through the fields and print them -foreach ($fields as $field) -{ - // If the value is empty do nothing - if (!strlen($field->value) && !$isMail) - { - continue; - } - - $layout = $field->params->get('layout', 'render'); - echo FieldsHelper::render($context, 'field.' . $layout, array('field' => $field)); -} +foreach ($fields as $field) { + // If the value is empty do nothing + if (!strlen($field->value) && !$isMail) { + continue; + } -if (!$isMail) -{ - // Close the container - echo '
    '; + $layout = $field->params->get('layout', 'render'); + echo FieldsHelper::render($context, 'field.' . $layout, array('field' => $field)); } +if (!$isMail) { + // Close the container + echo '
    '; +} diff --git a/components/com_contact/src/Controller/ContactController.php b/components/com_contact/src/Controller/ContactController.php index 48b1130edb0e6..b89e144978af2 100644 --- a/components/com_contact/src/Controller/ContactController.php +++ b/components/com_contact/src/Controller/ContactController.php @@ -1,4 +1,5 @@ true)) - { - return parent::getModel($name, $prefix, array('ignore_request' => false)); - } - - /** - * Method to submit the contact form and send an email. - * - * @return boolean True on success sending the email. False on failure. - * - * @since 1.5.19 - */ - public function submit() - { - // Check for request forgeries. - $this->checkToken(); - - $app = Factory::getApplication(); - $model = $this->getModel('contact'); - $stub = $this->input->getString('id'); - $id = (int) $stub; - - // Get the data from POST - $data = $this->input->post->get('jform', array(), 'array'); - - // Get item - $model->setState('filter.published', 1); - $contact = $model->getItem($id); - - if ($contact === false) - { - $this->setMessage($model->getError(), 'error'); - - return false; - } - - // Get item params, take menu parameters into account if necessary - $active = $app->getMenu()->getActive(); - $stateParams = clone $model->getState()->get('params'); - - // If the current view is the active item and a contact view for this contact, then the menu item params take priority - if ($active && strpos($active->link, 'view=contact') && strpos($active->link, '&id=' . (int) $contact->id)) - { - // $item->params are the contact params, $temp are the menu item params - // Merge so that the menu item params take priority - $contact->params->merge($stateParams); - } - else - { - // Current view is not a single contact, so the contact params take priority here - $stateParams->merge($contact->params); - $contact->params = $stateParams; - } - - // Check if the contact form is enabled - if (!$contact->params->get('show_email_form')) - { - $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false)); - - return false; - } - - // Check for a valid session cookie - if ($contact->params->get('validate_session', 0)) - { - if (Factory::getSession()->getState() !== 'active') - { - $this->app->enqueueMessage(Text::_('JLIB_ENVIRONMENT_SESSION_INVALID'), 'warning'); - - // Save the data in the session. - $this->app->setUserState('com_contact.contact.data', $data); - - // Redirect back to the contact form. - $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false)); - - return false; - } - } - - // Contact plugins - PluginHelper::importPlugin('contact'); - - // Validate the posted data. - $form = $model->getForm(); - - if (!$form) - { - throw new \Exception($model->getError(), 500); - } - - if (!$model->validate($form, $data)) - { - $errors = $model->getErrors(); - - foreach ($errors as $error) - { - $errorMessage = $error; - - if ($error instanceof \Exception) - { - $errorMessage = $error->getMessage(); - } - - $app->enqueueMessage($errorMessage, 'error'); - } - - $app->setUserState('com_contact.contact.data', $data); - - $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false)); - - return false; - } - - // Validation succeeded, continue with custom handlers - $results = $this->app->triggerEvent('onValidateContact', array(&$contact, &$data)); - - foreach ($results as $result) - { - if ($result instanceof \Exception) - { - return false; - } - } - - // Passed Validation: Process the contact plugins to integrate with other applications - $this->app->triggerEvent('onSubmitContact', array(&$contact, &$data)); - - // Send the email - $sent = false; - - if (!$contact->params->get('custom_reply')) - { - $sent = $this->_sendEmail($data, $contact, $contact->params->get('show_email_copy', 0)); - } - - $msg = ''; - - // Set the success message if it was a success - if ($sent) - { - $msg = Text::_('COM_CONTACT_EMAIL_THANKS'); - } - - // Flush the data from the session - $this->app->setUserState('com_contact.contact.data', null); - - // Redirect if it is set in the parameters, otherwise redirect back to where we came from - if ($contact->params->get('redirect')) - { - $this->setRedirect($contact->params->get('redirect'), $msg); - } - else - { - $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false), $msg); - } - - return true; - } - - /** - * Method to get a model object, loading it if required. - * - * @param array $data The data to send in the email. - * @param \stdClass $contact The user information to send the email to - * @param boolean $emailCopyToSender True to send a copy of the email to the user. - * - * @return boolean True on success sending the email, false on failure. - * - * @since 1.6.4 - */ - private function _sendEmail($data, $contact, $emailCopyToSender) - { - $app = $this->app; - - if ($contact->email_to == '' && $contact->user_id != 0) - { - $contact_user = User::getInstance($contact->user_id); - $contact->email_to = $contact_user->get('email'); - } - - $templateData = [ - 'sitename' => $app->get('sitename'), - 'name' => $data['contact_name'], - 'contactname' => $contact->name, - 'email' => PunycodeHelper::emailToPunycode($data['contact_email']), - 'subject' => $data['contact_subject'], - 'body' => stripslashes($data['contact_message']), - 'url' => Uri::base(), - 'customfields' => '' - ]; - - // Load the custom fields - if (!empty($data['com_fields']) && $fields = FieldsHelper::getFields('com_contact.mail', $contact, true, $data['com_fields'])) - { - $output = FieldsHelper::render( - 'com_contact.mail', - 'fields.render', - array( - 'context' => 'com_contact.mail', - 'item' => $contact, - 'fields' => $fields, - ) - ); - - if ($output) - { - $templateData['customfields'] = $output; - } - } - - try - { - $mailer = new MailTemplate('com_contact.mail', $app->getLanguage()->getTag()); - $mailer->addRecipient($contact->email_to); - $mailer->setReplyTo($templateData['email'], $templateData['name']); - $mailer->addTemplateData($templateData); - $sent = $mailer->send(); - - // If we are supposed to copy the sender, do so. - if ($emailCopyToSender == true && !empty($data['contact_email_copy'])) - { - $mailer = new MailTemplate('com_contact.mail.copy', $app->getLanguage()->getTag()); - $mailer->addRecipient($templateData['email']); - $mailer->setReplyTo($templateData['email'], $templateData['name']); - $mailer->addTemplateData($templateData); - $sent = $mailer->send(); - } - } - catch (MailDisabledException | phpMailerException $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $sent = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $sent = false; - } - } - - return $sent; - } - - /** - * Method override to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowAdd($data = array()) - { - if ($categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('catid'), 'int')) - { - $user = $this->app->getIdentity(); - - // If the category has been passed in the data or URL check it. - return $user->authorise('core.create', 'com_contact.category.' . $categoryId); - } - - // In the absence of better information, revert to the component permissions. - return parent::allowAdd(); - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key; default is id. - * - * @return boolean - * - * @since 4.0.0 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - - if (!$recordId) - { - return false; - } - - // Need to do a lookup from the model. - $record = $this->getModel()->getItem($recordId); - $categoryId = (int) $record->catid; - - if ($categoryId) - { - $user = $this->app->getIdentity(); - - // The category has been set. Check the category permissions. - if ($user->authorise('core.edit', $this->option . '.category.' . $categoryId)) - { - return true; - } - - // Fallback on edit.own. - if ($user->authorise('core.edit.own', $this->option . '.category.' . $categoryId)) - { - return ($record->created_by === $user->id); - } - - return false; - } - - // Since there is no asset tracking, revert to the component permissions. - return parent::allowEdit($data, $key); - } - - /** - * Method to cancel an edit. - * - * @param string $key The name of the primary key of the URL variable. - * - * @return boolean True if access level checks pass, false otherwise. - * - * @since 4.0.0 - */ - public function cancel($key = null) - { - $result = parent::cancel($key); - - $this->setRedirect(Route::_($this->getReturnPage(), false)); - - return $result; - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 4.0.0 - */ - protected function getRedirectToItemAppend($recordId = 0, $urlVar = 'id') - { - // Need to override the parent method completely. - $tmpl = $this->input->get('tmpl'); - - $append = ''; - - // Setup redirect info. - if ($tmpl) - { - $append .= '&tmpl=' . $tmpl; - } - - $append .= '&layout=edit'; - - $append .= '&' . $urlVar . '=' . (int) $recordId; - - $itemId = $this->input->getInt('Itemid'); - $return = $this->getReturnPage(); - $catId = $this->input->getInt('catid'); - - if ($itemId) - { - $append .= '&Itemid=' . $itemId; - } - - if ($catId) - { - $append .= '&catid=' . $catId; - } - - if ($return) - { - $append .= '&return=' . base64_encode($return); - } - - return $append; - } - - /** - * Get the return URL. - * - * If a "return" variable has been passed in the request - * - * @return string The return URL. - * - * @since 4.0.0 - */ - protected function getReturnPage() - { - $return = $this->input->get('return', null, 'base64'); - - if (empty($return) || !Uri::isInternal(base64_decode($return))) - { - return Uri::base(); - } - - return base64_decode($return); - } + use VersionableControllerTrait; + + /** + * The URL view item variable. + * + * @var string + * @since 4.0.0 + */ + protected $view_item = 'form'; + + /** + * The URL view list variable. + * + * @var string + * @since 4.0.0 + */ + protected $view_list = 'categories'; + + /** + * 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 \Joomla\CMS\MVC\Model\BaseDatabaseModel The model. + * + * @since 1.6.4 + */ + public function getModel($name = 'form', $prefix = '', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, array('ignore_request' => false)); + } + + /** + * Method to submit the contact form and send an email. + * + * @return boolean True on success sending the email. False on failure. + * + * @since 1.5.19 + */ + public function submit() + { + // Check for request forgeries. + $this->checkToken(); + + $app = Factory::getApplication(); + $model = $this->getModel('contact'); + $stub = $this->input->getString('id'); + $id = (int) $stub; + + // Get the data from POST + $data = $this->input->post->get('jform', array(), 'array'); + + // Get item + $model->setState('filter.published', 1); + $contact = $model->getItem($id); + + if ($contact === false) { + $this->setMessage($model->getError(), 'error'); + + return false; + } + + // Get item params, take menu parameters into account if necessary + $active = $app->getMenu()->getActive(); + $stateParams = clone $model->getState()->get('params'); + + // If the current view is the active item and a contact view for this contact, then the menu item params take priority + if ($active && strpos($active->link, 'view=contact') && strpos($active->link, '&id=' . (int) $contact->id)) { + // $item->params are the contact params, $temp are the menu item params + // Merge so that the menu item params take priority + $contact->params->merge($stateParams); + } else { + // Current view is not a single contact, so the contact params take priority here + $stateParams->merge($contact->params); + $contact->params = $stateParams; + } + + // Check if the contact form is enabled + if (!$contact->params->get('show_email_form')) { + $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false)); + + return false; + } + + // Check for a valid session cookie + if ($contact->params->get('validate_session', 0)) { + if (Factory::getSession()->getState() !== 'active') { + $this->app->enqueueMessage(Text::_('JLIB_ENVIRONMENT_SESSION_INVALID'), 'warning'); + + // Save the data in the session. + $this->app->setUserState('com_contact.contact.data', $data); + + // Redirect back to the contact form. + $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false)); + + return false; + } + } + + // Contact plugins + PluginHelper::importPlugin('contact'); + + // Validate the posted data. + $form = $model->getForm(); + + if (!$form) { + throw new \Exception($model->getError(), 500); + } + + if (!$model->validate($form, $data)) { + $errors = $model->getErrors(); + + foreach ($errors as $error) { + $errorMessage = $error; + + if ($error instanceof \Exception) { + $errorMessage = $error->getMessage(); + } + + $app->enqueueMessage($errorMessage, 'error'); + } + + $app->setUserState('com_contact.contact.data', $data); + + $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false)); + + return false; + } + + // Validation succeeded, continue with custom handlers + $results = $this->app->triggerEvent('onValidateContact', array(&$contact, &$data)); + + foreach ($results as $result) { + if ($result instanceof \Exception) { + return false; + } + } + + // Passed Validation: Process the contact plugins to integrate with other applications + $this->app->triggerEvent('onSubmitContact', array(&$contact, &$data)); + + // Send the email + $sent = false; + + if (!$contact->params->get('custom_reply')) { + $sent = $this->_sendEmail($data, $contact, $contact->params->get('show_email_copy', 0)); + } + + $msg = ''; + + // Set the success message if it was a success + if ($sent) { + $msg = Text::_('COM_CONTACT_EMAIL_THANKS'); + } + + // Flush the data from the session + $this->app->setUserState('com_contact.contact.data', null); + + // Redirect if it is set in the parameters, otherwise redirect back to where we came from + if ($contact->params->get('redirect')) { + $this->setRedirect($contact->params->get('redirect'), $msg); + } else { + $this->setRedirect(Route::_('index.php?option=com_contact&view=contact&id=' . $stub . '&catid=' . $contact->catid, false), $msg); + } + + return true; + } + + /** + * Method to get a model object, loading it if required. + * + * @param array $data The data to send in the email. + * @param \stdClass $contact The user information to send the email to + * @param boolean $emailCopyToSender True to send a copy of the email to the user. + * + * @return boolean True on success sending the email, false on failure. + * + * @since 1.6.4 + */ + private function _sendEmail($data, $contact, $emailCopyToSender) + { + $app = $this->app; + + if ($contact->email_to == '' && $contact->user_id != 0) { + $contact_user = User::getInstance($contact->user_id); + $contact->email_to = $contact_user->get('email'); + } + + $templateData = [ + 'sitename' => $app->get('sitename'), + 'name' => $data['contact_name'], + 'contactname' => $contact->name, + 'email' => PunycodeHelper::emailToPunycode($data['contact_email']), + 'subject' => $data['contact_subject'], + 'body' => stripslashes($data['contact_message']), + 'url' => Uri::base(), + 'customfields' => '' + ]; + + // Load the custom fields + if (!empty($data['com_fields']) && $fields = FieldsHelper::getFields('com_contact.mail', $contact, true, $data['com_fields'])) { + $output = FieldsHelper::render( + 'com_contact.mail', + 'fields.render', + array( + 'context' => 'com_contact.mail', + 'item' => $contact, + 'fields' => $fields, + ) + ); + + if ($output) { + $templateData['customfields'] = $output; + } + } + + try { + $mailer = new MailTemplate('com_contact.mail', $app->getLanguage()->getTag()); + $mailer->addRecipient($contact->email_to); + $mailer->setReplyTo($templateData['email'], $templateData['name']); + $mailer->addTemplateData($templateData); + $sent = $mailer->send(); + + // If we are supposed to copy the sender, do so. + if ($emailCopyToSender == true && !empty($data['contact_email_copy'])) { + $mailer = new MailTemplate('com_contact.mail.copy', $app->getLanguage()->getTag()); + $mailer->addRecipient($templateData['email']); + $mailer->setReplyTo($templateData['email'], $templateData['name']); + $mailer->addTemplateData($templateData); + $sent = $mailer->send(); + } + } catch (MailDisabledException | phpMailerException $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $sent = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $sent = false; + } + } + + return $sent; + } + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowAdd($data = array()) + { + if ($categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('catid'), 'int')) { + $user = $this->app->getIdentity(); + + // If the category has been passed in the data or URL check it. + return $user->authorise('core.create', 'com_contact.category.' . $categoryId); + } + + // In the absence of better information, revert to the component permissions. + return parent::allowAdd(); + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key; default is id. + * + * @return boolean + * + * @since 4.0.0 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + + if (!$recordId) { + return false; + } + + // Need to do a lookup from the model. + $record = $this->getModel()->getItem($recordId); + $categoryId = (int) $record->catid; + + if ($categoryId) { + $user = $this->app->getIdentity(); + + // The category has been set. Check the category permissions. + if ($user->authorise('core.edit', $this->option . '.category.' . $categoryId)) { + return true; + } + + // Fallback on edit.own. + if ($user->authorise('core.edit.own', $this->option . '.category.' . $categoryId)) { + return ($record->created_by === $user->id); + } + + return false; + } + + // Since there is no asset tracking, revert to the component permissions. + return parent::allowEdit($data, $key); + } + + /** + * Method to cancel an edit. + * + * @param string $key The name of the primary key of the URL variable. + * + * @return boolean True if access level checks pass, false otherwise. + * + * @since 4.0.0 + */ + public function cancel($key = null) + { + $result = parent::cancel($key); + + $this->setRedirect(Route::_($this->getReturnPage(), false)); + + return $result; + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 4.0.0 + */ + protected function getRedirectToItemAppend($recordId = 0, $urlVar = 'id') + { + // Need to override the parent method completely. + $tmpl = $this->input->get('tmpl'); + + $append = ''; + + // Setup redirect info. + if ($tmpl) { + $append .= '&tmpl=' . $tmpl; + } + + $append .= '&layout=edit'; + + $append .= '&' . $urlVar . '=' . (int) $recordId; + + $itemId = $this->input->getInt('Itemid'); + $return = $this->getReturnPage(); + $catId = $this->input->getInt('catid'); + + if ($itemId) { + $append .= '&Itemid=' . $itemId; + } + + if ($catId) { + $append .= '&catid=' . $catId; + } + + if ($return) { + $append .= '&return=' . base64_encode($return); + } + + return $append; + } + + /** + * Get the return URL. + * + * If a "return" variable has been passed in the request + * + * @return string The return URL. + * + * @since 4.0.0 + */ + protected function getReturnPage() + { + $return = $this->input->get('return', null, 'base64'); + + if (empty($return) || !Uri::isInternal(base64_decode($return))) { + return Uri::base(); + } + + return base64_decode($return); + } } diff --git a/components/com_contact/src/Controller/DisplayController.php b/components/com_contact/src/Controller/DisplayController.php index 6c1788b5988f1..66d2fa05fd5ce 100644 --- a/components/com_contact/src/Controller/DisplayController.php +++ b/components/com_contact/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input; + /** + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The Application for the dispatcher + * @param \Joomla\CMS\Input\Input|null $input The Input object for the request + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + // Contact frontpage Editor contacts proxying. + $input = Factory::getApplication()->input; - if ($input->get('view') === 'contacts' && $input->get('layout') === 'modal') - { - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - } + if ($input->get('view') === 'contacts' && $input->get('layout') === 'modal') { + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + } - parent::__construct($config, $factory, $app, $input); - } + parent::__construct($config, $factory, $app, $input); + } - /** - * Method to display a view. - * - * @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()}. - * - * @return DisplayController This object to support chaining. - * - * @since 1.5 - */ - public function display($cachable = false, $urlparams = array()) - { - if (Factory::getApplication()->getUserState('com_contact.contact.data') === null) - { - $cachable = true; - } + /** + * Method to display a view. + * + * @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()}. + * + * @return DisplayController This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = array()) + { + if (Factory::getApplication()->getUserState('com_contact.contact.data') === null) { + $cachable = true; + } - // Set the default view name and format from the Request. - $vName = $this->input->get('view', 'categories'); - $this->input->set('view', $vName); + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'categories'); + $this->input->set('view', $vName); - if ($this->app->getIdentity()->get('id')) - { - $cachable = false; - } + if ($this->app->getIdentity()->get('id')) { + $cachable = false; + } - $safeurlparams = array('catid' => 'INT', 'id' => 'INT', 'cid' => 'ARRAY', 'year' => 'INT', 'month' => 'INT', - 'limit' => 'UINT', 'limitstart' => 'UINT', 'showall' => 'INT', 'return' => 'BASE64', 'filter' => 'STRING', - 'filter_order' => 'CMD', 'filter_order_Dir' => 'CMD', 'filter-search' => 'STRING', 'print' => 'BOOLEAN', - 'lang' => 'CMD'); + $safeurlparams = array('catid' => 'INT', 'id' => 'INT', 'cid' => 'ARRAY', 'year' => 'INT', 'month' => 'INT', + 'limit' => 'UINT', 'limitstart' => 'UINT', 'showall' => 'INT', 'return' => 'BASE64', 'filter' => 'STRING', + 'filter_order' => 'CMD', 'filter_order_Dir' => 'CMD', 'filter-search' => 'STRING', 'print' => 'BOOLEAN', + 'lang' => 'CMD'); - parent::display($cachable, $safeurlparams); + parent::display($cachable, $safeurlparams); - return $this; - } + return $this; + } } diff --git a/components/com_contact/src/Dispatcher/Dispatcher.php b/components/com_contact/src/Dispatcher/Dispatcher.php index 537798c969180..fb90e26a31fe7 100644 --- a/components/com_contact/src/Dispatcher/Dispatcher.php +++ b/components/com_contact/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ input->get('view') === 'contacts' && $this->input->get('layout') === 'modal') - { - if (!$this->app->getIdentity()->authorise('core.create', 'com_contact')) - { - $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'warning'); + /** + * Dispatch a controller task. Redirecting the user if appropriate. + * + * @return void + * + * @since 4.0.0 + */ + public function dispatch() + { + if ($this->input->get('view') === 'contacts' && $this->input->get('layout') === 'modal') { + if (!$this->app->getIdentity()->authorise('core.create', 'com_contact')) { + $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'warning'); - return; - } + return; + } - $this->app->getLanguage()->load('com_contact', JPATH_ADMINISTRATOR); - } + $this->app->getLanguage()->load('com_contact', JPATH_ADMINISTRATOR); + } - parent::dispatch(); - } + parent::dispatch(); + } } diff --git a/components/com_contact/src/Helper/AssociationHelper.php b/components/com_contact/src/Helper/AssociationHelper.php index 14e014ee7f844..08d58b59b40ef 100644 --- a/components/com_contact/src/Helper/AssociationHelper.php +++ b/components/com_contact/src/Helper/AssociationHelper.php @@ -1,4 +1,5 @@ input; - $view = $view ?? $jinput->get('view'); - $id = empty($id) ? $jinput->getInt('id') : $id; - - if ($view === 'contact') - { - if ($id) - { - $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $id); - - $return = array(); - - foreach ($associations as $tag => $item) - { - $return[$tag] = RouteHelper::getContactRoute($item->id, (int) $item->catid, $item->language); - } - - return $return; - } - } - - if ($view === 'category' || $view === 'categories') - { - return self::getCategoryAssociations($id, 'com_contact'); - } - - return array(); - - } + /** + * Method to get the associations for a given item + * + * @param integer $id Id of the item + * @param string $view Name of the view + * + * @return array Array of associations for the item + * + * @since 3.0 + */ + public static function getAssociations($id = 0, $view = null) + { + $jinput = Factory::getApplication()->input; + $view = $view ?? $jinput->get('view'); + $id = empty($id) ? $jinput->getInt('id') : $id; + + if ($view === 'contact') { + if ($id) { + $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $id); + + $return = array(); + + foreach ($associations as $tag => $item) { + $return[$tag] = RouteHelper::getContactRoute($item->id, (int) $item->catid, $item->language); + } + + return $return; + } + } + + if ($view === 'category' || $view === 'categories') { + return self::getCategoryAssociations($id, 'com_contact'); + } + + return array(); + } } diff --git a/components/com_contact/src/Helper/RouteHelper.php b/components/com_contact/src/Helper/RouteHelper.php index 10937c0dbeeac..7158204ef1626 100644 --- a/components/com_contact/src/Helper/RouteHelper.php +++ b/components/com_contact/src/Helper/RouteHelper.php @@ -1,4 +1,5 @@ 1) - { - $link .= '&catid=' . $catid; - } + if ($catid > 1) { + $link .= '&catid=' . $catid; + } - if ($language && $language !== '*' && Multilanguage::isEnabled()) - { - $link .= '&lang=' . $language; - } + if ($language && $language !== '*' && Multilanguage::isEnabled()) { + $link .= '&lang=' . $language; + } - return $link; - } + return $link; + } - /** - * Get the URL route for a contact category from a contact category ID and language - * - * @param mixed $catid The id of the contact's category either an integer id or an instance of CategoryNode - * @param mixed $language The id of the language being used. - * - * @return string The link to the contact - * - * @since 1.5 - */ - public static function getCategoryRoute($catid, $language = 0) - { - if ($catid instanceof CategoryNode) - { - $id = $catid->id; - } - else - { - $id = (int) $catid; - } + /** + * Get the URL route for a contact category from a contact category ID and language + * + * @param mixed $catid The id of the contact's category either an integer id or an instance of CategoryNode + * @param mixed $language The id of the language being used. + * + * @return string The link to the contact + * + * @since 1.5 + */ + public static function getCategoryRoute($catid, $language = 0) + { + if ($catid instanceof CategoryNode) { + $id = $catid->id; + } else { + $id = (int) $catid; + } - if ($id < 1) - { - $link = ''; - } - else - { - // Create the link - $link = 'index.php?option=com_contact&view=category&id=' . $id; + if ($id < 1) { + $link = ''; + } else { + // Create the link + $link = 'index.php?option=com_contact&view=category&id=' . $id; - if ($language && $language !== '*' && Multilanguage::isEnabled()) - { - $link .= '&lang=' . $language; - } - } + if ($language && $language !== '*' && Multilanguage::isEnabled()) { + $link .= '&lang=' . $language; + } + } - return $link; - } + return $link; + } } diff --git a/components/com_contact/src/Model/CategoriesModel.php b/components/com_contact/src/Model/CategoriesModel.php index d5ee42aa1af3e..98b1d3ecf31e7 100644 --- a/components/com_contact/src/Model/CategoriesModel.php +++ b/components/com_contact/src/Model/CategoriesModel.php @@ -1,4 +1,5 @@ setState('filter.extension', $this->_extension); - - // Get the parent id if defined. - $parentId = $app->input->getInt('id'); - $this->setState('filter.parentId', $parentId); - - $params = $app->getParams(); - $this->setState('params', $params); - - $this->setState('filter.published', 1); - $this->setState('filter.access', true); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.extension'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.parentId'); - - return parent::getStoreId($id); - } - - /** - * Redefine the function and add some properties to make the styling easier - * - * @return mixed An array of data items on success, false on failure. - */ - public function getItems() - { - if ($this->_items === null) - { - $app = Factory::getApplication(); - $menu = $app->getMenu(); - $active = $menu->getActive(); - - if ($active) - { - $params = $active->getParams(); - } - else - { - $params = new Registry; - } - - $options = array(); - $options['countItems'] = $params->get('show_cat_items_cat', 1) || !$params->get('show_empty_categories_cat', 0); - $categories = Categories::getInstance('Contact', $options); - $this->_parent = $categories->get($this->getState('filter.parentId', 'root')); - - if (is_object($this->_parent)) - { - $this->_items = $this->_parent->getChildren(); - } - else - { - $this->_items = false; - } - } - - return $this->_items; - } - - /** - * Gets the id of the parent category for the selected list of categories - * - * @return integer The id of the parent category - * - * @since 1.6.0 - */ - public function getParent() - { - if (!is_object($this->_parent)) - { - $this->getItems(); - } - - return $this->_parent; - } + /** + * Model context string. + * + * @var string + */ + public $_context = 'com_contact.categories'; + + /** + * The category context (allows other extensions to derived from this model). + * + * @var string + */ + protected $_extension = 'com_contact'; + + /** + * Parent category of the current one + * + * @var CategoryNode|null + */ + private $_parent = null; + + /** + * Array of child-categories + * + * @var CategoryNode[]|null + */ + private $_items = null; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + $this->setState('filter.extension', $this->_extension); + + // Get the parent id if defined. + $parentId = $app->input->getInt('id'); + $this->setState('filter.parentId', $parentId); + + $params = $app->getParams(); + $this->setState('params', $params); + + $this->setState('filter.published', 1); + $this->setState('filter.access', true); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.extension'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.parentId'); + + return parent::getStoreId($id); + } + + /** + * Redefine the function and add some properties to make the styling easier + * + * @return mixed An array of data items on success, false on failure. + */ + public function getItems() + { + if ($this->_items === null) { + $app = Factory::getApplication(); + $menu = $app->getMenu(); + $active = $menu->getActive(); + + if ($active) { + $params = $active->getParams(); + } else { + $params = new Registry(); + } + + $options = array(); + $options['countItems'] = $params->get('show_cat_items_cat', 1) || !$params->get('show_empty_categories_cat', 0); + $categories = Categories::getInstance('Contact', $options); + $this->_parent = $categories->get($this->getState('filter.parentId', 'root')); + + if (is_object($this->_parent)) { + $this->_items = $this->_parent->getChildren(); + } else { + $this->_items = false; + } + } + + return $this->_items; + } + + /** + * Gets the id of the parent category for the selected list of categories + * + * @return integer The id of the parent category + * + * @since 1.6.0 + */ + public function getParent() + { + if (!is_object($this->_parent)) { + $this->getItems(); + } + + return $this->_parent; + } } diff --git a/components/com_contact/src/Model/CategoryModel.php b/components/com_contact/src/Model/CategoryModel.php index 93ca40860791e..b03e1630391c3 100644 --- a/components/com_contact/src/Model/CategoryModel.php +++ b/components/com_contact/src/Model/CategoryModel.php @@ -1,4 +1,5 @@ _params)) - { - $item->params = new Registry($item->params); - } - - // Some contexts may not use tags data at all, so we allow callers to disable loading tag data - if ($this->getState('load_tags', true)) - { - $item->tags = new TagsHelper; - $taggedItems[$item->id] = $item; - } - } - - // Load tags of all items. - if ($taggedItems) - { - $tagsHelper = new TagsHelper; - $itemIds = \array_keys($taggedItems); - - foreach ($tagsHelper->getMultipleItemTags('com_contact.contact', $itemIds) as $id => $tags) - { - $taggedItems[$id]->tags->itemTags = $tags; - } - } - - return $items; - } - - /** - * Method to build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery An SQL query - * - * @since 1.6 - */ - protected function getListQuery() - { - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - - // Create a new query object. - $db = $this->getDatabase(); - - /** @var \Joomla\Database\DatabaseQuery $query */ - $query = $db->getQuery(true); - - $query->select($this->getState('list.select', 'a.*')) - ->select($this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS slug') - ->select($this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS catslug') - /** - * @todo: we actually should be doing it but it's wrong this way - * . ' CASE WHEN CHAR_LENGTH(a.alias) THEN CONCAT_WS(\':\', a.id, a.alias) ELSE a.id END as slug, ' - * . ' CASE WHEN CHAR_LENGTH(c.alias) THEN CONCAT_WS(\':\', c.id, c.alias) ELSE c.id END AS catslug '); - */ - ->from($db->quoteName('#__contact_details', 'a')) - ->leftJoin($db->quoteName('#__categories', 'c') . ' ON c.id = a.catid') - ->whereIn($db->quoteName('a.access'), $groups); - - // Filter by category. - if ($categoryId = $this->getState('category.id')) - { - $query->where($db->quoteName('a.catid') . ' = :acatid') - ->whereIn($db->quoteName('c.access'), $groups); - $query->bind(':acatid', $categoryId, ParameterType::INTEGER); - } - - // Join over the users for the author and modified_by names. - $query->select("CASE WHEN a.created_by_alias > ' ' THEN a.created_by_alias ELSE ua.name END AS author") - ->select('ua.email AS author_email') - ->leftJoin($db->quoteName('#__users', 'ua') . ' ON ua.id = a.created_by') - ->leftJoin($db->quoteName('#__users', 'uam') . ' ON uam.id = a.modified_by'); - - // Filter by state - $state = $this->getState('filter.published'); - - if (is_numeric($state)) - { - $query->where($db->quoteName('a.published') . ' = :published'); - $query->bind(':published', $state, ParameterType::INTEGER); - } - else - { - $query->whereIn($db->quoteName('c.published'), [0,1,2]); - } - - // Filter by start and end dates. - $nowDate = Factory::getDate()->toSql(); - - if ($this->getState('filter.publish_date')) - { - $query->where('(' . $db->quoteName('a.publish_up') - . ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publish_up)' - ) - ->where('(' . $db->quoteName('a.publish_down') - . ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publish_down)' - ) - ->bind(':publish_up', $nowDate) - ->bind(':publish_down', $nowDate); - } - - // Filter by search in title - $search = $this->getState('list.filter'); - - if (!empty($search)) - { - $search = '%' . trim($search) . '%'; - $query->where($db->quoteName('a.name') . ' LIKE :name '); - $query->bind(':name', $search); - } - - // Filter on the language. - if ($this->getState('filter.language')) - { - $query->whereIn($db->quoteName('a.language'), [Factory::getApplication()->getLanguage()->getTag(), '*'], ParameterType::STRING); - } - - // Set sortname ordering if selected - if ($this->getState('list.ordering') === 'sortname') - { - $query->order($db->escape('a.sortname1') . ' ' . $db->escape($this->getState('list.direction', 'ASC'))) - ->order($db->escape('a.sortname2') . ' ' . $db->escape($this->getState('list.direction', 'ASC'))) - ->order($db->escape('a.sortname3') . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - } - elseif ($this->getState('list.ordering') === 'featuredordering') - { - $query->order($db->escape('a.featured') . ' DESC') - ->order($db->escape('a.ordering') . ' ASC'); - } - else - { - $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - } - - return $query; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = null, $direction = null) - { - $app = Factory::getApplication(); - $params = ComponentHelper::getParams('com_contact'); - - // Get list ordering default from the parameters - if ($menu = $app->getMenu()->getActive()) - { - $menuParams = $menu->getParams(); - } - else - { - $menuParams = new Registry; - } - - $mergedParams = clone $params; - $mergedParams->merge($menuParams); - - // List state information - $format = $app->input->getWord('format'); - - if ($format === 'feed') - { - $limit = $app->get('feed_limit'); - } - else - { - $limit = $app->getUserStateFromRequest( - 'com_contact.category.list.limit', - 'limit', - $mergedParams->get('contacts_display_num', $app->get('list_limit')), - 'uint' - ); - } - - $this->setState('list.limit', $limit); - - $limitstart = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.start', $limitstart); - - // Optional filter text - $itemid = $app->input->get('Itemid', 0, 'int'); - $search = $app->getUserStateFromRequest('com_contact.category.list.' . $itemid . '.filter-search', 'filter-search', '', 'string'); - $this->setState('list.filter', $search); - - $orderCol = $app->input->get('filter_order', $mergedParams->get('initial_sort', 'ordering')); - - if (!in_array($orderCol, $this->filter_fields)) - { - $orderCol = 'ordering'; - } - - $this->setState('list.ordering', $orderCol); - - $listOrder = $app->input->get('filter_order_Dir', 'ASC'); - - if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) - { - $listOrder = 'ASC'; - } - - $this->setState('list.direction', $listOrder); - - $id = $app->input->get('id', 0, 'int'); - $this->setState('category.id', $id); - - $user = Factory::getUser(); - - if ((!$user->authorise('core.edit.state', 'com_contact')) && (!$user->authorise('core.edit', 'com_contact'))) - { - // Limit to published for people who can't edit or edit.state. - $this->setState('filter.published', 1); - - // Filter by start and end dates. - $this->setState('filter.publish_date', true); - } - - $this->setState('filter.language', Multilanguage::isEnabled()); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Method to get category data for the current category - * - * @return object The category object - * - * @since 1.5 - */ - public function getCategory() - { - if (!is_object($this->_item)) - { - $app = Factory::getApplication(); - $menu = $app->getMenu(); - $active = $menu->getActive(); - - if ($active) - { - $params = $active->getParams(); - } - else - { - $params = new Registry; - } - - $options = array(); - $options['countItems'] = $params->get('show_cat_items', 1) || $params->get('show_empty_categories', 0); - $categories = Categories::getInstance('Contact', $options); - $this->_item = $categories->get($this->getState('category.id', 'root')); - - if (is_object($this->_item)) - { - $this->_children = $this->_item->getChildren(); - $this->_parent = false; - - if ($this->_item->getParent()) - { - $this->_parent = $this->_item->getParent(); - } - - $this->_rightsibling = $this->_item->getSibling(); - $this->_leftsibling = $this->_item->getSibling(false); - } - else - { - $this->_children = false; - $this->_parent = false; - } - } - - return $this->_item; - } - - /** - * Get the parent category. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function getParent() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_parent; - } - - /** - * Get the sibling (adjacent) categories. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function &getLeftSibling() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_leftsibling; - } - - /** - * Get the sibling (adjacent) categories. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function &getRightSibling() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_rightsibling; - } - - /** - * Get the child categories. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function &getChildren() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_children; - } - - /** - * Generate column expression for slug or catslug. - * - * @param \Joomla\Database\DatabaseQuery $query Current query instance. - * @param string $id Column id name. - * @param string $alias Column alias name. - * - * @return string - * - * @since 4.0.0 - */ - private function getSlugColumn($query, $id, $alias) - { - return 'CASE WHEN ' - . $query->charLength($alias, '!=', '0') - . ' THEN ' - . $query->concatenate(array($query->castAsChar($id), $alias), ':') - . ' ELSE ' - . $query->castAsChar($id) . ' END'; - } - - /** - * Increment the hit counter for the category. - * - * @param integer $pk Optional primary key of the category to increment. - * - * @return boolean True if successful; false otherwise and internal error set. - * - * @since 3.2 - */ - public function hit($pk = 0) - { - $input = Factory::getApplication()->input; - $hitcount = $input->getInt('hitcount', 1); - - if ($hitcount) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('category.id'); - - $table = Table::getInstance('Category'); - $table->hit($pk); - } - - return true; - } + /** + * Category item data + * + * @var CategoryNode + */ + protected $_item; + + /** + * Array of contacts in the category + * + * @var \stdClass[] + */ + protected $_articles; + + /** + * Category left and right of this one + * + * @var CategoryNode[]|null + */ + protected $_siblings; + + /** + * Array of child-categories + * + * @var CategoryNode[]|null + */ + protected $_children; + + /** + * Parent category of the current one + * + * @var CategoryNode|null + */ + protected $_parent; + + /** + * The category that applies. + * + * @var object + */ + protected $_category; + + /** + * The list of other contact categories. + * + * @var array + */ + protected $_categories; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'con_position', 'a.con_position', + 'suburb', 'a.suburb', + 'state', 'a.state', + 'country', 'a.country', + 'ordering', 'a.ordering', + 'sortname', + 'sortname1', 'a.sortname1', + 'sortname2', 'a.sortname2', + 'sortname3', 'a.sortname3', + 'featuredordering', 'a.featured' + ); + } + + parent::__construct($config); + } + + /** + * Method to get a list of items. + * + * @return mixed An array of objects on success, false on failure. + */ + public function getItems() + { + // Invoke the parent getItems method to get the main list + $items = parent::getItems(); + + if ($items === false) { + return false; + } + + $taggedItems = []; + + // Convert the params field into an object, saving original in _params + foreach ($items as $item) { + if (!isset($this->_params)) { + $item->params = new Registry($item->params); + } + + // Some contexts may not use tags data at all, so we allow callers to disable loading tag data + if ($this->getState('load_tags', true)) { + $item->tags = new TagsHelper(); + $taggedItems[$item->id] = $item; + } + } + + // Load tags of all items. + if ($taggedItems) { + $tagsHelper = new TagsHelper(); + $itemIds = \array_keys($taggedItems); + + foreach ($tagsHelper->getMultipleItemTags('com_contact.contact', $itemIds) as $id => $tags) { + $taggedItems[$id]->tags->itemTags = $tags; + } + } + + return $items; + } + + /** + * Method to build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery An SQL query + * + * @since 1.6 + */ + protected function getListQuery() + { + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + + // Create a new query object. + $db = $this->getDatabase(); + + /** @var \Joomla\Database\DatabaseQuery $query */ + $query = $db->getQuery(true); + + $query->select($this->getState('list.select', 'a.*')) + ->select($this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS slug') + ->select($this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS catslug') + /** + * @todo: we actually should be doing it but it's wrong this way + * . ' CASE WHEN CHAR_LENGTH(a.alias) THEN CONCAT_WS(\':\', a.id, a.alias) ELSE a.id END as slug, ' + * . ' CASE WHEN CHAR_LENGTH(c.alias) THEN CONCAT_WS(\':\', c.id, c.alias) ELSE c.id END AS catslug '); + */ + ->from($db->quoteName('#__contact_details', 'a')) + ->leftJoin($db->quoteName('#__categories', 'c') . ' ON c.id = a.catid') + ->whereIn($db->quoteName('a.access'), $groups); + + // Filter by category. + if ($categoryId = $this->getState('category.id')) { + $query->where($db->quoteName('a.catid') . ' = :acatid') + ->whereIn($db->quoteName('c.access'), $groups); + $query->bind(':acatid', $categoryId, ParameterType::INTEGER); + } + + // Join over the users for the author and modified_by names. + $query->select("CASE WHEN a.created_by_alias > ' ' THEN a.created_by_alias ELSE ua.name END AS author") + ->select('ua.email AS author_email') + ->leftJoin($db->quoteName('#__users', 'ua') . ' ON ua.id = a.created_by') + ->leftJoin($db->quoteName('#__users', 'uam') . ' ON uam.id = a.modified_by'); + + // Filter by state + $state = $this->getState('filter.published'); + + if (is_numeric($state)) { + $query->where($db->quoteName('a.published') . ' = :published'); + $query->bind(':published', $state, ParameterType::INTEGER); + } else { + $query->whereIn($db->quoteName('c.published'), [0,1,2]); + } + + // Filter by start and end dates. + $nowDate = Factory::getDate()->toSql(); + + if ($this->getState('filter.publish_date')) { + $query->where('(' . $db->quoteName('a.publish_up') + . ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publish_up)') + ->where('(' . $db->quoteName('a.publish_down') + . ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publish_down)') + ->bind(':publish_up', $nowDate) + ->bind(':publish_down', $nowDate); + } + + // Filter by search in title + $search = $this->getState('list.filter'); + + if (!empty($search)) { + $search = '%' . trim($search) . '%'; + $query->where($db->quoteName('a.name') . ' LIKE :name '); + $query->bind(':name', $search); + } + + // Filter on the language. + if ($this->getState('filter.language')) { + $query->whereIn($db->quoteName('a.language'), [Factory::getApplication()->getLanguage()->getTag(), '*'], ParameterType::STRING); + } + + // Set sortname ordering if selected + if ($this->getState('list.ordering') === 'sortname') { + $query->order($db->escape('a.sortname1') . ' ' . $db->escape($this->getState('list.direction', 'ASC'))) + ->order($db->escape('a.sortname2') . ' ' . $db->escape($this->getState('list.direction', 'ASC'))) + ->order($db->escape('a.sortname3') . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + } elseif ($this->getState('list.ordering') === 'featuredordering') { + $query->order($db->escape('a.featured') . ' DESC') + ->order($db->escape('a.ordering') . ' ASC'); + } else { + $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + } + + return $query; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + $params = ComponentHelper::getParams('com_contact'); + + // Get list ordering default from the parameters + if ($menu = $app->getMenu()->getActive()) { + $menuParams = $menu->getParams(); + } else { + $menuParams = new Registry(); + } + + $mergedParams = clone $params; + $mergedParams->merge($menuParams); + + // List state information + $format = $app->input->getWord('format'); + + if ($format === 'feed') { + $limit = $app->get('feed_limit'); + } else { + $limit = $app->getUserStateFromRequest( + 'com_contact.category.list.limit', + 'limit', + $mergedParams->get('contacts_display_num', $app->get('list_limit')), + 'uint' + ); + } + + $this->setState('list.limit', $limit); + + $limitstart = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.start', $limitstart); + + // Optional filter text + $itemid = $app->input->get('Itemid', 0, 'int'); + $search = $app->getUserStateFromRequest('com_contact.category.list.' . $itemid . '.filter-search', 'filter-search', '', 'string'); + $this->setState('list.filter', $search); + + $orderCol = $app->input->get('filter_order', $mergedParams->get('initial_sort', 'ordering')); + + if (!in_array($orderCol, $this->filter_fields)) { + $orderCol = 'ordering'; + } + + $this->setState('list.ordering', $orderCol); + + $listOrder = $app->input->get('filter_order_Dir', 'ASC'); + + if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) { + $listOrder = 'ASC'; + } + + $this->setState('list.direction', $listOrder); + + $id = $app->input->get('id', 0, 'int'); + $this->setState('category.id', $id); + + $user = Factory::getUser(); + + if ((!$user->authorise('core.edit.state', 'com_contact')) && (!$user->authorise('core.edit', 'com_contact'))) { + // Limit to published for people who can't edit or edit.state. + $this->setState('filter.published', 1); + + // Filter by start and end dates. + $this->setState('filter.publish_date', true); + } + + $this->setState('filter.language', Multilanguage::isEnabled()); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Method to get category data for the current category + * + * @return object The category object + * + * @since 1.5 + */ + public function getCategory() + { + if (!is_object($this->_item)) { + $app = Factory::getApplication(); + $menu = $app->getMenu(); + $active = $menu->getActive(); + + if ($active) { + $params = $active->getParams(); + } else { + $params = new Registry(); + } + + $options = array(); + $options['countItems'] = $params->get('show_cat_items', 1) || $params->get('show_empty_categories', 0); + $categories = Categories::getInstance('Contact', $options); + $this->_item = $categories->get($this->getState('category.id', 'root')); + + if (is_object($this->_item)) { + $this->_children = $this->_item->getChildren(); + $this->_parent = false; + + if ($this->_item->getParent()) { + $this->_parent = $this->_item->getParent(); + } + + $this->_rightsibling = $this->_item->getSibling(); + $this->_leftsibling = $this->_item->getSibling(false); + } else { + $this->_children = false; + $this->_parent = false; + } + } + + return $this->_item; + } + + /** + * Get the parent category. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function getParent() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_parent; + } + + /** + * Get the sibling (adjacent) categories. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function &getLeftSibling() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_leftsibling; + } + + /** + * Get the sibling (adjacent) categories. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function &getRightSibling() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_rightsibling; + } + + /** + * Get the child categories. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function &getChildren() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_children; + } + + /** + * Generate column expression for slug or catslug. + * + * @param \Joomla\Database\DatabaseQuery $query Current query instance. + * @param string $id Column id name. + * @param string $alias Column alias name. + * + * @return string + * + * @since 4.0.0 + */ + private function getSlugColumn($query, $id, $alias) + { + return 'CASE WHEN ' + . $query->charLength($alias, '!=', '0') + . ' THEN ' + . $query->concatenate(array($query->castAsChar($id), $alias), ':') + . ' ELSE ' + . $query->castAsChar($id) . ' END'; + } + + /** + * Increment the hit counter for the category. + * + * @param integer $pk Optional primary key of the category to increment. + * + * @return boolean True if successful; false otherwise and internal error set. + * + * @since 3.2 + */ + public function hit($pk = 0) + { + $input = Factory::getApplication()->input; + $hitcount = $input->getInt('hitcount', 1); + + if ($hitcount) { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('category.id'); + + $table = Table::getInstance('Category'); + $table->hit($pk); + } + + return true; + } } diff --git a/components/com_contact/src/Model/ContactModel.php b/components/com_contact/src/Model/ContactModel.php index 54def0d43450a..c53dd65856622 100644 --- a/components/com_contact/src/Model/ContactModel.php +++ b/components/com_contact/src/Model/ContactModel.php @@ -1,4 +1,5 @@ get(SiteApplication::class); - - if (Factory::getApplication()->isClient('api')) - { - // @todo: remove this - $app->loadLanguage(); - $this->setState('contact.id', Factory::getApplication()->input->post->getInt('id')); - } - else - { - $this->setState('contact.id', $app->input->getInt('id')); - } - - $this->setState('params', $app->getParams()); - - $user = Factory::getUser(); - - if ((!$user->authorise('core.edit.state', 'com_contact')) && (!$user->authorise('core.edit', 'com_contact'))) - { - $this->setState('filter.published', 1); - $this->setState('filter.archived', 2); - } - } - - /** - * Method to get the contact form. - * The base form is loaded from XML and then an event is fired - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - $form = $this->loadForm('com_contact.contact', 'contact', array('control' => 'jform', 'load_data' => true)); - - if (empty($form)) - { - return false; - } - - $temp = clone $this->getState('params'); - $contact = $this->_item[$this->getState('contact.id')]; - $active = Factory::getContainer()->get(SiteApplication::class)->getMenu()->getActive(); - - if ($active) - { - // If the current view is the active item and a contact view for this contact, then the menu item params take priority - if (strpos($active->link, 'view=contact') && strpos($active->link, '&id=' . (int) $contact->id)) - { - // $contact->params are the contact params, $temp are the menu item params - // Merge so that the menu item params take priority - $contact->params->merge($temp); - } - else - { - // Current view is not a single contact, so the contact params take priority here - // Merge the menu item params with the contact params so that the contact params take priority - $temp->merge($contact->params); - $contact->params = $temp; - } - } - else - { - // Merge so that contact params take priority - $temp->merge($contact->params); - $contact->params = $temp; - } - - if (!$contact->params->get('show_email_copy', 0)) - { - $form->removeField('contact_email_copy'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return array The default data is an empty array. - * - * @since 1.6.2 - */ - protected function loadFormData() - { - $data = (array) Factory::getApplication()->getUserState('com_contact.contact.data', array()); - - if (empty($data['language']) && Multilanguage::isEnabled()) - { - $data['language'] = Factory::getLanguage()->getTag(); - } - - // Add contact catid to contact form data, so fields plugin can work properly - if (empty($data['catid'])) - { - $data['catid'] = $this->getItem()->catid; - } - - $this->preprocessData('com_contact.contact', $data); - - return $data; - } - - /** - * Gets a contact - * - * @param integer $pk Id for the contact - * - * @return mixed Object or null - * - * @since 1.6.0 - */ - public function getItem($pk = null) - { - $pk = $pk ?: (int) $this->getState('contact.id'); - - if ($this->_item === null) - { - $this->_item = array(); - } - - if (!isset($this->_item[$pk])) - { - try - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->select($this->getState('item.select', 'a.*')) - ->select($this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS slug') - ->select($this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS catslug') - ->from($db->quoteName('#__contact_details', 'a')) - - // Join on category table. - ->select('c.title AS category_title, c.alias AS category_alias, c.access AS category_access') - ->leftJoin($db->quoteName('#__categories', 'c'), 'c.id = a.catid') - - // Join over the categories to get parent category titles - ->select('parent.title AS parent_title, parent.id AS parent_id, parent.path AS parent_route, parent.alias AS parent_alias') - ->leftJoin($db->quoteName('#__categories', 'parent'), 'parent.id = c.parent_id') - ->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $pk, ParameterType::INTEGER); - - // Filter by start and end dates. - $nowDate = Factory::getDate()->toSql(); - - // Filter by published state. - $published = $this->getState('filter.published'); - $archived = $this->getState('filter.archived'); - - if (is_numeric($published)) - { - $queryString = $db->quoteName('a.published') . ' = :published'; - - if ($archived !== null) - { - $queryString = '(' . $queryString . ' OR ' . $db->quoteName('a.published') . ' = :archived)'; - $query->bind(':archived', $archived, ParameterType::INTEGER); - } - - $query->where($queryString) - ->where('(' . $db->quoteName('a.publish_up') . ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publish_up)') - ->where('(' . $db->quoteName('a.publish_down') . ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publish_down)') - ->bind(':published', $published, ParameterType::INTEGER) - ->bind(':publish_up', $nowDate) - ->bind(':publish_down', $nowDate); - } - - $db->setQuery($query); - $data = $db->loadObject(); - - if (empty($data)) - { - throw new \Exception(Text::_('COM_CONTACT_ERROR_CONTACT_NOT_FOUND'), 404); - } - - // Check for published state if filter set. - if ((is_numeric($published) || is_numeric($archived)) && (($data->published != $published) && ($data->published != $archived))) - { - throw new \Exception(Text::_('COM_CONTACT_ERROR_CONTACT_NOT_FOUND'), 404); - } - - /** - * In case some entity params have been set to "use global", those are - * represented as an empty string and must be "overridden" by merging - * the component and / or menu params here. - */ - $registry = new Registry($data->params); - - $data->params = clone $this->getState('params'); - $data->params->merge($registry); - - $registry = new Registry($data->metadata); - $data->metadata = $registry; - - // Some contexts may not use tags data at all, so we allow callers to disable loading tag data - if ($this->getState('load_tags', true)) - { - $data->tags = new TagsHelper; - $data->tags->getItemTags('com_contact.contact', $data->id); - } - - // Compute access permissions. - if (($access = $this->getState('filter.access'))) - { - // If the access filter has been set, we already know this user can view. - $data->params->set('access-view', true); - } - else - { - // If no access filter is set, the layout takes some responsibility for display of limited information. - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - - if ($data->catid == 0 || $data->category_access === null) - { - $data->params->set('access-view', in_array($data->access, $groups)); - } - else - { - $data->params->set('access-view', in_array($data->access, $groups) && in_array($data->category_access, $groups)); - } - } - - $this->_item[$pk] = $data; - } - catch (\Exception $e) - { - if ($e->getCode() == 404) - { - // Need to go through the error handler to allow Redirect to work. - throw $e; - } - else - { - $this->setError($e); - $this->_item[$pk] = false; - } - } - } - - if ($this->_item[$pk]) - { - $this->buildContactExtendedData($this->_item[$pk]); - } - - return $this->_item[$pk]; - } - - /** - * Load extended data (profile, articles) for a contact - * - * @param object $contact The contact object - * - * @return void - */ - protected function buildContactExtendedData($contact) - { - $db = $this->getDatabase(); - $nowDate = Factory::getDate()->toSql(); - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - $published = $this->getState('filter.published'); - $query = $db->getQuery(true); - - // If we are showing a contact list, then the contact parameters take priority - // So merge the contact parameters with the merged parameters - if ($this->getState('params')->get('show_contact_list')) - { - $this->getState('params')->merge($contact->params); - } - - // Get the com_content articles by the linked user - if ((int) $contact->user_id && $this->getState('params')->get('show_articles')) - { - $query->select('a.id') - ->select('a.title') - ->select('a.state') - ->select('a.access') - ->select('a.catid') - ->select('a.created') - ->select('a.language') - ->select('a.publish_up') - ->select('a.introtext') - ->select('a.images') - ->select($this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS slug') - ->select($this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS catslug') - ->from($db->quoteName('#__content', 'a')) - ->leftJoin($db->quoteName('#__categories', 'c') . ' ON a.catid = c.id') - ->where($db->quoteName('a.created_by') . ' = :created_by') - ->whereIn($db->quoteName('a.access'), $groups) - ->bind(':created_by', $contact->user_id, ParameterType::INTEGER) - ->order('a.publish_up DESC'); - - // Filter per language if plugin published - if (Multilanguage::isEnabled()) - { - $language = [Factory::getLanguage()->getTag(), $db->quote('*')]; - $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); - } - - if (is_numeric($published)) - { - $query->where('a.state IN (1,2)') - ->where('(' . $db->quoteName('a.publish_up') . ' IS NULL' . - ' OR ' . $db->quoteName('a.publish_up') . ' <= :now1)' - ) - ->where('(' . $db->quoteName('a.publish_down') . ' IS NULL' . - ' OR ' . $db->quoteName('a.publish_down') . ' >= :now2)' - ) - ->bind([':now1', ':now2'], $nowDate); - } - - // Number of articles to display from config/menu params - $articles_display_num = $this->getState('params')->get('articles_display_num', 10); - - // Use contact setting? - if ($articles_display_num === 'use_contact') - { - $articles_display_num = $contact->params->get('articles_display_num', 10); - - // Use global? - if ((string) $articles_display_num === '') - { - $articles_display_num = ComponentHelper::getParams('com_contact')->get('articles_display_num', 10); - } - } - - $query->setLimit((int) $articles_display_num); - $db->setQuery($query); - $contact->articles = $db->loadObjectList(); - } - else - { - $contact->articles = null; - } - - // Get the profile information for the linked user - $userModel = $this->bootComponent('com_users')->getMVCFactory() - ->createModel('User', 'Administrator', ['ignore_request' => true]); - $data = $userModel->getItem((int) $contact->user_id); - - PluginHelper::importPlugin('user'); - - // Get the form. - Form::addFormPath(JPATH_SITE . '/components/com_users/forms'); - - $form = Form::getInstance('com_users.profile', 'profile'); - - // Trigger the form preparation event. - Factory::getApplication()->triggerEvent('onContentPrepareForm', array($form, $data)); - - // Trigger the data preparation event. - Factory::getApplication()->triggerEvent('onContentPrepareData', array('com_users.profile', $data)); - - // Load the data into the form after the plugins have operated. - $form->bind($data); - $contact->profile = $form; - } - - /** - * Generate column expression for slug or catslug. - * - * @param QueryInterface $query Current query instance. - * @param string $id Column id name. - * @param string $alias Column alias name. - * - * @return string - * - * @since 4.0.0 - */ - private function getSlugColumn($query, $id, $alias) - { - return 'CASE WHEN ' - . $query->charLength($alias, '!=', '0') - . ' THEN ' - . $query->concatenate(array($query->castAsChar($id), $alias), ':') - . ' ELSE ' - . $query->castAsChar($id) . ' END'; - } - - /** - * Increment the hit counter for the contact. - * - * @param integer $pk Optional primary key of the contact to increment. - * - * @return boolean True if successful; false otherwise and internal error set. - * - * @since 3.0 - */ - public function hit($pk = 0) - { - $input = Factory::getApplication()->input; - $hitcount = $input->getInt('hitcount', 1); - - if ($hitcount) - { - $pk = $pk ?: (int) $this->getState('contact.id'); - - $table = $this->getTable('Contact'); - $table->hit($pk); - } - - return true; - } + /** + * The name of the view for a single item + * + * @var string + * @since 1.6 + */ + protected $view_item = 'contact'; + + /** + * A loaded item + * + * @var \stdClass + * @since 1.6 + */ + protected $_item = null; + + /** + * Model context string. + * + * @var string + */ + protected $_context = 'com_contact.contact'; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + /** @var SiteApplication $app */ + $app = Factory::getContainer()->get(SiteApplication::class); + + if (Factory::getApplication()->isClient('api')) { + // @todo: remove this + $app->loadLanguage(); + $this->setState('contact.id', Factory::getApplication()->input->post->getInt('id')); + } else { + $this->setState('contact.id', $app->input->getInt('id')); + } + + $this->setState('params', $app->getParams()); + + $user = Factory::getUser(); + + if ((!$user->authorise('core.edit.state', 'com_contact')) && (!$user->authorise('core.edit', 'com_contact'))) { + $this->setState('filter.published', 1); + $this->setState('filter.archived', 2); + } + } + + /** + * Method to get the contact form. + * The base form is loaded from XML and then an event is fired + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + $form = $this->loadForm('com_contact.contact', 'contact', array('control' => 'jform', 'load_data' => true)); + + if (empty($form)) { + return false; + } + + $temp = clone $this->getState('params'); + $contact = $this->_item[$this->getState('contact.id')]; + $active = Factory::getContainer()->get(SiteApplication::class)->getMenu()->getActive(); + + if ($active) { + // If the current view is the active item and a contact view for this contact, then the menu item params take priority + if (strpos($active->link, 'view=contact') && strpos($active->link, '&id=' . (int) $contact->id)) { + // $contact->params are the contact params, $temp are the menu item params + // Merge so that the menu item params take priority + $contact->params->merge($temp); + } else { + // Current view is not a single contact, so the contact params take priority here + // Merge the menu item params with the contact params so that the contact params take priority + $temp->merge($contact->params); + $contact->params = $temp; + } + } else { + // Merge so that contact params take priority + $temp->merge($contact->params); + $contact->params = $temp; + } + + if (!$contact->params->get('show_email_copy', 0)) { + $form->removeField('contact_email_copy'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return array The default data is an empty array. + * + * @since 1.6.2 + */ + protected function loadFormData() + { + $data = (array) Factory::getApplication()->getUserState('com_contact.contact.data', array()); + + if (empty($data['language']) && Multilanguage::isEnabled()) { + $data['language'] = Factory::getLanguage()->getTag(); + } + + // Add contact catid to contact form data, so fields plugin can work properly + if (empty($data['catid'])) { + $data['catid'] = $this->getItem()->catid; + } + + $this->preprocessData('com_contact.contact', $data); + + return $data; + } + + /** + * Gets a contact + * + * @param integer $pk Id for the contact + * + * @return mixed Object or null + * + * @since 1.6.0 + */ + public function getItem($pk = null) + { + $pk = $pk ?: (int) $this->getState('contact.id'); + + if ($this->_item === null) { + $this->_item = array(); + } + + if (!isset($this->_item[$pk])) { + try { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select($this->getState('item.select', 'a.*')) + ->select($this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS slug') + ->select($this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS catslug') + ->from($db->quoteName('#__contact_details', 'a')) + + // Join on category table. + ->select('c.title AS category_title, c.alias AS category_alias, c.access AS category_access') + ->leftJoin($db->quoteName('#__categories', 'c'), 'c.id = a.catid') + + // Join over the categories to get parent category titles + ->select('parent.title AS parent_title, parent.id AS parent_id, parent.path AS parent_route, parent.alias AS parent_alias') + ->leftJoin($db->quoteName('#__categories', 'parent'), 'parent.id = c.parent_id') + ->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $pk, ParameterType::INTEGER); + + // Filter by start and end dates. + $nowDate = Factory::getDate()->toSql(); + + // Filter by published state. + $published = $this->getState('filter.published'); + $archived = $this->getState('filter.archived'); + + if (is_numeric($published)) { + $queryString = $db->quoteName('a.published') . ' = :published'; + + if ($archived !== null) { + $queryString = '(' . $queryString . ' OR ' . $db->quoteName('a.published') . ' = :archived)'; + $query->bind(':archived', $archived, ParameterType::INTEGER); + } + + $query->where($queryString) + ->where('(' . $db->quoteName('a.publish_up') . ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publish_up)') + ->where('(' . $db->quoteName('a.publish_down') . ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publish_down)') + ->bind(':published', $published, ParameterType::INTEGER) + ->bind(':publish_up', $nowDate) + ->bind(':publish_down', $nowDate); + } + + $db->setQuery($query); + $data = $db->loadObject(); + + if (empty($data)) { + throw new \Exception(Text::_('COM_CONTACT_ERROR_CONTACT_NOT_FOUND'), 404); + } + + // Check for published state if filter set. + if ((is_numeric($published) || is_numeric($archived)) && (($data->published != $published) && ($data->published != $archived))) { + throw new \Exception(Text::_('COM_CONTACT_ERROR_CONTACT_NOT_FOUND'), 404); + } + + /** + * In case some entity params have been set to "use global", those are + * represented as an empty string and must be "overridden" by merging + * the component and / or menu params here. + */ + $registry = new Registry($data->params); + + $data->params = clone $this->getState('params'); + $data->params->merge($registry); + + $registry = new Registry($data->metadata); + $data->metadata = $registry; + + // Some contexts may not use tags data at all, so we allow callers to disable loading tag data + if ($this->getState('load_tags', true)) { + $data->tags = new TagsHelper(); + $data->tags->getItemTags('com_contact.contact', $data->id); + } + + // Compute access permissions. + if (($access = $this->getState('filter.access'))) { + // If the access filter has been set, we already know this user can view. + $data->params->set('access-view', true); + } else { + // If no access filter is set, the layout takes some responsibility for display of limited information. + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + + if ($data->catid == 0 || $data->category_access === null) { + $data->params->set('access-view', in_array($data->access, $groups)); + } else { + $data->params->set('access-view', in_array($data->access, $groups) && in_array($data->category_access, $groups)); + } + } + + $this->_item[$pk] = $data; + } catch (\Exception $e) { + if ($e->getCode() == 404) { + // Need to go through the error handler to allow Redirect to work. + throw $e; + } else { + $this->setError($e); + $this->_item[$pk] = false; + } + } + } + + if ($this->_item[$pk]) { + $this->buildContactExtendedData($this->_item[$pk]); + } + + return $this->_item[$pk]; + } + + /** + * Load extended data (profile, articles) for a contact + * + * @param object $contact The contact object + * + * @return void + */ + protected function buildContactExtendedData($contact) + { + $db = $this->getDatabase(); + $nowDate = Factory::getDate()->toSql(); + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + $published = $this->getState('filter.published'); + $query = $db->getQuery(true); + + // If we are showing a contact list, then the contact parameters take priority + // So merge the contact parameters with the merged parameters + if ($this->getState('params')->get('show_contact_list')) { + $this->getState('params')->merge($contact->params); + } + + // Get the com_content articles by the linked user + if ((int) $contact->user_id && $this->getState('params')->get('show_articles')) { + $query->select('a.id') + ->select('a.title') + ->select('a.state') + ->select('a.access') + ->select('a.catid') + ->select('a.created') + ->select('a.language') + ->select('a.publish_up') + ->select('a.introtext') + ->select('a.images') + ->select($this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS slug') + ->select($this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS catslug') + ->from($db->quoteName('#__content', 'a')) + ->leftJoin($db->quoteName('#__categories', 'c') . ' ON a.catid = c.id') + ->where($db->quoteName('a.created_by') . ' = :created_by') + ->whereIn($db->quoteName('a.access'), $groups) + ->bind(':created_by', $contact->user_id, ParameterType::INTEGER) + ->order('a.publish_up DESC'); + + // Filter per language if plugin published + if (Multilanguage::isEnabled()) { + $language = [Factory::getLanguage()->getTag(), $db->quote('*')]; + $query->whereIn($db->quoteName('a.language'), $language, ParameterType::STRING); + } + + if (is_numeric($published)) { + $query->where('a.state IN (1,2)') + ->where('(' . $db->quoteName('a.publish_up') . ' IS NULL' . + ' OR ' . $db->quoteName('a.publish_up') . ' <= :now1)') + ->where('(' . $db->quoteName('a.publish_down') . ' IS NULL' . + ' OR ' . $db->quoteName('a.publish_down') . ' >= :now2)') + ->bind([':now1', ':now2'], $nowDate); + } + + // Number of articles to display from config/menu params + $articles_display_num = $this->getState('params')->get('articles_display_num', 10); + + // Use contact setting? + if ($articles_display_num === 'use_contact') { + $articles_display_num = $contact->params->get('articles_display_num', 10); + + // Use global? + if ((string) $articles_display_num === '') { + $articles_display_num = ComponentHelper::getParams('com_contact')->get('articles_display_num', 10); + } + } + + $query->setLimit((int) $articles_display_num); + $db->setQuery($query); + $contact->articles = $db->loadObjectList(); + } else { + $contact->articles = null; + } + + // Get the profile information for the linked user + $userModel = $this->bootComponent('com_users')->getMVCFactory() + ->createModel('User', 'Administrator', ['ignore_request' => true]); + $data = $userModel->getItem((int) $contact->user_id); + + PluginHelper::importPlugin('user'); + + // Get the form. + Form::addFormPath(JPATH_SITE . '/components/com_users/forms'); + + $form = Form::getInstance('com_users.profile', 'profile'); + + // Trigger the form preparation event. + Factory::getApplication()->triggerEvent('onContentPrepareForm', array($form, $data)); + + // Trigger the data preparation event. + Factory::getApplication()->triggerEvent('onContentPrepareData', array('com_users.profile', $data)); + + // Load the data into the form after the plugins have operated. + $form->bind($data); + $contact->profile = $form; + } + + /** + * Generate column expression for slug or catslug. + * + * @param QueryInterface $query Current query instance. + * @param string $id Column id name. + * @param string $alias Column alias name. + * + * @return string + * + * @since 4.0.0 + */ + private function getSlugColumn($query, $id, $alias) + { + return 'CASE WHEN ' + . $query->charLength($alias, '!=', '0') + . ' THEN ' + . $query->concatenate(array($query->castAsChar($id), $alias), ':') + . ' ELSE ' + . $query->castAsChar($id) . ' END'; + } + + /** + * Increment the hit counter for the contact. + * + * @param integer $pk Optional primary key of the contact to increment. + * + * @return boolean True if successful; false otherwise and internal error set. + * + * @since 3.0 + */ + public function hit($pk = 0) + { + $input = Factory::getApplication()->input; + $hitcount = $input->getInt('hitcount', 1); + + if ($hitcount) { + $pk = $pk ?: (int) $this->getState('contact.id'); + + $table = $this->getTable('Contact'); + $table->hit($pk); + } + + return true; + } } diff --git a/components/com_contact/src/Model/FeaturedModel.php b/components/com_contact/src/Model/FeaturedModel.php index f654f45cf6f4a..0860e10b9dead 100644 --- a/components/com_contact/src/Model/FeaturedModel.php +++ b/components/com_contact/src/Model/FeaturedModel.php @@ -1,4 +1,5 @@ _params)) - { - $item->params = new Registry($item->params); - } - } - - return $items; - } - - /** - * Method to build an SQL query to load the list data. - * - * @return string An SQL query - * - * @since 1.6 - */ - protected function getListQuery() - { - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select required fields from the categories. - $query->select($this->getState('list.select', 'a.*')) - ->from($db->quoteName('#__contact_details', 'a')) - ->where($db->quoteName('a.featured') . ' = 1') - ->whereIn($db->quoteName('a.access'), $groups) - ->innerJoin($db->quoteName('#__categories', 'c') . ' ON c.id = a.catid') - ->whereIn($db->quoteName('c.access'), $groups); - - // Filter by category. - if ($categoryId = $this->getState('category.id')) - { - $query->where($db->quoteName('a.catid') . ' = :catid'); - $query->bind(':catid', $categoryId, ParameterType::INTEGER); - } - - $query->select('c.published as cat_published, c.published AS parents_published') - ->where('c.published = 1'); - - // Filter by state - $state = $this->getState('filter.published'); - - if (is_numeric($state)) - { - $query->where($db->quoteName('a.published') . ' = :published'); - $query->bind(':published', $state, ParameterType::INTEGER); - - // Filter by start and end dates. - $nowDate = Factory::getDate()->toSql(); - - $query->where('(' . $db->quoteName('a.publish_up') . - ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publish_up)' - ) - ->where('(' . $db->quoteName('a.publish_down') . - ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publish_down)' - ) - ->bind(':publish_up', $nowDate) - ->bind(':publish_down', $nowDate); - } - - // Filter by search in title - $search = $this->getState('list.filter'); - - // Filter by search in title - if (!empty($search)) - { - $search = '%' . trim($search) . '%'; - $query->where($db->quoteName('a.name') . ' LIKE :name '); - $query->bind(':name', $search); - } - - // Filter by language - if ($this->getState('filter.language')) - { - $query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 1.6 - */ - protected function populateState($ordering = null, $direction = null) - { - $app = Factory::getApplication(); - $params = ComponentHelper::getParams('com_contact'); - - // List state information - $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->get('list_limit'), 'uint'); - $this->setState('list.limit', $limit); - - $limitstart = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.start', $limitstart); - - // Optional filter text - $this->setState('list.filter', $app->input->getString('filter-search')); - - $orderCol = $app->input->get('filter_order', 'ordering'); - - if (!in_array($orderCol, $this->filter_fields)) - { - $orderCol = 'ordering'; - } - - $this->setState('list.ordering', $orderCol); - - $listOrder = $app->input->get('filter_order_Dir', 'ASC'); - - if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) - { - $listOrder = 'ASC'; - } - - $this->setState('list.direction', $listOrder); - - $user = Factory::getUser(); - - if ((!$user->authorise('core.edit.state', 'com_contact')) && (!$user->authorise('core.edit', 'com_contact'))) - { - // Limit to published for people who can't edit or edit.state. - $this->setState('filter.published', 1); - - // Filter by start and end dates. - $this->setState('filter.publish_date', true); - } - - $this->setState('filter.language', Multilanguage::isEnabled()); - - // Load the parameters. - $this->setState('params', $params); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'con_position', 'a.con_position', + 'suburb', 'a.suburb', + 'state', 'a.state', + 'country', 'a.country', + 'ordering', 'a.ordering', + ); + } + + parent::__construct($config); + } + + /** + * Method to get a list of items. + * + * @return mixed An array of objects on success, false on failure. + */ + public function getItems() + { + // Invoke the parent getItems method to get the main list + $items = parent::getItems(); + + // Convert the params field into an object, saving original in _params + for ($i = 0, $n = count($items); $i < $n; $i++) { + $item = &$items[$i]; + + if (!isset($this->_params)) { + $item->params = new Registry($item->params); + } + } + + return $items; + } + + /** + * Method to build an SQL query to load the list data. + * + * @return string An SQL query + * + * @since 1.6 + */ + protected function getListQuery() + { + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select required fields from the categories. + $query->select($this->getState('list.select', 'a.*')) + ->from($db->quoteName('#__contact_details', 'a')) + ->where($db->quoteName('a.featured') . ' = 1') + ->whereIn($db->quoteName('a.access'), $groups) + ->innerJoin($db->quoteName('#__categories', 'c') . ' ON c.id = a.catid') + ->whereIn($db->quoteName('c.access'), $groups); + + // Filter by category. + if ($categoryId = $this->getState('category.id')) { + $query->where($db->quoteName('a.catid') . ' = :catid'); + $query->bind(':catid', $categoryId, ParameterType::INTEGER); + } + + $query->select('c.published as cat_published, c.published AS parents_published') + ->where('c.published = 1'); + + // Filter by state + $state = $this->getState('filter.published'); + + if (is_numeric($state)) { + $query->where($db->quoteName('a.published') . ' = :published'); + $query->bind(':published', $state, ParameterType::INTEGER); + + // Filter by start and end dates. + $nowDate = Factory::getDate()->toSql(); + + $query->where('(' . $db->quoteName('a.publish_up') . + ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publish_up)') + ->where('(' . $db->quoteName('a.publish_down') . + ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publish_down)') + ->bind(':publish_up', $nowDate) + ->bind(':publish_down', $nowDate); + } + + // Filter by search in title + $search = $this->getState('list.filter'); + + // Filter by search in title + if (!empty($search)) { + $search = '%' . trim($search) . '%'; + $query->where($db->quoteName('a.name') . ' LIKE :name '); + $query->bind(':name', $search); + } + + // Filter by language + if ($this->getState('filter.language')) { + $query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + $params = ComponentHelper::getParams('com_contact'); + + // List state information + $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->get('list_limit'), 'uint'); + $this->setState('list.limit', $limit); + + $limitstart = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.start', $limitstart); + + // Optional filter text + $this->setState('list.filter', $app->input->getString('filter-search')); + + $orderCol = $app->input->get('filter_order', 'ordering'); + + if (!in_array($orderCol, $this->filter_fields)) { + $orderCol = 'ordering'; + } + + $this->setState('list.ordering', $orderCol); + + $listOrder = $app->input->get('filter_order_Dir', 'ASC'); + + if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) { + $listOrder = 'ASC'; + } + + $this->setState('list.direction', $listOrder); + + $user = Factory::getUser(); + + if ((!$user->authorise('core.edit.state', 'com_contact')) && (!$user->authorise('core.edit', 'com_contact'))) { + // Limit to published for people who can't edit or edit.state. + $this->setState('filter.published', 1); + + // Filter by start and end dates. + $this->setState('filter.publish_date', true); + } + + $this->setState('filter.language', Multilanguage::isEnabled()); + + // Load the parameters. + $this->setState('params', $params); + } } diff --git a/components/com_contact/src/Model/FormModel.php b/components/com_contact/src/Model/FormModel.php index 8afefd64f50c9..3aa5db11f3b51 100644 --- a/components/com_contact/src/Model/FormModel.php +++ b/components/com_contact/src/Model/FormModel.php @@ -1,4 +1,5 @@ getState('contact.id') && Associations::isEnabled()) - { - $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $id); - - // Make fields read only - if (!empty($associations)) - { - $form->setFieldAttribute('language', 'readonly', 'true'); - $form->setFieldAttribute('language', 'filter', 'unset'); - } - } - - return $form; - } - - /** - * Method to get contact data. - * - * @param integer $itemId The id of the contact. - * - * @return mixed Contact item data object on success, false on failure. - * - * @throws Exception - * - * @since 4.0.0 - */ - public function getItem($itemId = null) - { - $itemId = (int) (!empty($itemId)) ? $itemId : $this->getState('contact.id'); - - // Get a row instance. - $table = $this->getTable(); - - // Attempt to load the row. - try - { - if (!$table->load($itemId)) - { - return false; - } - } - catch (Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage()); - - return false; - } - - $properties = $table->getProperties(); - $value = ArrayHelper::toObject($properties, \Joomla\CMS\Object\CMSObject::class); - - // Convert field to Registry. - $value->params = new Registry($value->params); - - // Convert the metadata field to an array. - $registry = new Registry($value->metadata); - $value->metadata = $registry->toArray(); - - if ($itemId) - { - $value->tags = new TagsHelper; - $value->tags->getTagIds($value->id, 'com_contact.contact'); - $value->metadata['tags'] = $value->tags; - } - - return $value; - } - - /** - * Get the return URL. - * - * @return string The return URL. - * - * @since 4.0.0 - */ - public function getReturnPage() - { - return base64_encode($this->getState('return_page', '')); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 4.0.0 - * - * @throws Exception - */ - public function save($data) - { - // Associations are not edited in frontend ATM so we have to inherit them - if (Associations::isEnabled() && !empty($data['id']) - && $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $data['id'])) - { - foreach ($associations as $tag => $associated) - { - $associations[$tag] = (int) $associated->id; - } - - $data['associations'] = $associations; - } - - return parent::save($data); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 4.0.0 - * - * @throws Exception - */ - protected function populateState() - { - $app = Factory::getApplication(); - - // Load state from the request. - $pk = $app->input->getInt('id'); - $this->setState('contact.id', $pk); - - $this->setState('contact.catid', $app->input->getInt('catid')); - - $return = $app->input->get('return', '', 'base64'); - $this->setState('return_page', base64_decode($return)); - - // Load the parameters. - $params = $app->getParams(); - $this->setState('params', $params); - - $this->setState('layout', $app->input->getString('layout')); - } - - /** - * Allows preprocessing of the JForm object. - * - * @param Form $form The form object - * @param array $data The data to be merged into the form object - * @param string $group The plugin group to be executed - * - * @return void - * - * @since 4.0.0 - */ - protected function preprocessForm(Form $form, $data, $group = 'contact') - { - if (!Multilanguage::isEnabled()) - { - $form->setFieldAttribute('language', 'type', 'hidden'); - $form->setFieldAttribute('language', 'default', '*'); - } - - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return bool|Table A Table object - * - * @since 4.0.0 - - * @throws Exception - */ - public function getTable($name = 'Contact', $prefix = 'Administrator', $options = array()) - { - return parent::getTable($name, $prefix, $options); - } + /** + * Model typeAlias string. Used for version history. + * + * @var string + * + * @since 4.0.0 + */ + public $typeAlias = 'com_contact.contact'; + + /** + * Name of the form + * + * @var string + * + * @since 4.0.0 + */ + protected $formName = 'form'; + + /** + * Method to get the row form. + * + * @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 Form|boolean A Form object on success, false on failure + * + * @since 4.0.0 + */ + public function getForm($data = array(), $loadData = true) + { + $form = parent::getForm($data, $loadData); + + // Prevent messing with article language and category when editing existing contact with associations + if ($id = $this->getState('contact.id') && Associations::isEnabled()) { + $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $id); + + // Make fields read only + if (!empty($associations)) { + $form->setFieldAttribute('language', 'readonly', 'true'); + $form->setFieldAttribute('language', 'filter', 'unset'); + } + } + + return $form; + } + + /** + * Method to get contact data. + * + * @param integer $itemId The id of the contact. + * + * @return mixed Contact item data object on success, false on failure. + * + * @throws Exception + * + * @since 4.0.0 + */ + public function getItem($itemId = null) + { + $itemId = (int) (!empty($itemId)) ? $itemId : $this->getState('contact.id'); + + // Get a row instance. + $table = $this->getTable(); + + // Attempt to load the row. + try { + if (!$table->load($itemId)) { + return false; + } + } catch (Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage()); + + return false; + } + + $properties = $table->getProperties(); + $value = ArrayHelper::toObject($properties, \Joomla\CMS\Object\CMSObject::class); + + // Convert field to Registry. + $value->params = new Registry($value->params); + + // Convert the metadata field to an array. + $registry = new Registry($value->metadata); + $value->metadata = $registry->toArray(); + + if ($itemId) { + $value->tags = new TagsHelper(); + $value->tags->getTagIds($value->id, 'com_contact.contact'); + $value->metadata['tags'] = $value->tags; + } + + return $value; + } + + /** + * Get the return URL. + * + * @return string The return URL. + * + * @since 4.0.0 + */ + public function getReturnPage() + { + return base64_encode($this->getState('return_page', '')); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 4.0.0 + * + * @throws Exception + */ + public function save($data) + { + // Associations are not edited in frontend ATM so we have to inherit them + if ( + Associations::isEnabled() && !empty($data['id']) + && $associations = Associations::getAssociations('com_contact', '#__contact_details', 'com_contact.item', $data['id']) + ) { + foreach ($associations as $tag => $associated) { + $associations[$tag] = (int) $associated->id; + } + + $data['associations'] = $associations; + } + + return parent::save($data); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 4.0.0 + * + * @throws Exception + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load state from the request. + $pk = $app->input->getInt('id'); + $this->setState('contact.id', $pk); + + $this->setState('contact.catid', $app->input->getInt('catid')); + + $return = $app->input->get('return', '', 'base64'); + $this->setState('return_page', base64_decode($return)); + + // Load the parameters. + $params = $app->getParams(); + $this->setState('params', $params); + + $this->setState('layout', $app->input->getString('layout')); + } + + /** + * Allows preprocessing of the JForm object. + * + * @param Form $form The form object + * @param array $data The data to be merged into the form object + * @param string $group The plugin group to be executed + * + * @return void + * + * @since 4.0.0 + */ + protected function preprocessForm(Form $form, $data, $group = 'contact') + { + if (!Multilanguage::isEnabled()) { + $form->setFieldAttribute('language', 'type', 'hidden'); + $form->setFieldAttribute('language', 'default', '*'); + } + + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return bool|Table A Table object + * + * @since 4.0.0 + + * @throws Exception + */ + public function getTable($name = 'Contact', $prefix = 'Administrator', $options = array()) + { + return parent::getTable($name, $prefix, $options); + } } diff --git a/components/com_contact/src/Rule/ContactEmailMessageRule.php b/components/com_contact/src/Rule/ContactEmailMessageRule.php index 3bbfb5ea1ad58..21cd248ce7803 100644 --- a/components/com_contact/src/Rule/ContactEmailMessageRule.php +++ b/components/com_contact/src/Rule/ContactEmailMessageRule.php @@ -1,4 +1,5 @@ tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. - * @param Form $form The form object for which the field is being tested. - * - * @return boolean True if the value is valid, false otherwise. - */ - public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) - { - $params = ComponentHelper::getParams('com_contact'); - $banned = $params->get('banned_text'); + /** + * Method to test a message for banned words + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form $form The form object for which the field is being tested. + * + * @return boolean True if the value is valid, false otherwise. + */ + public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + $params = ComponentHelper::getParams('com_contact'); + $banned = $params->get('banned_text'); - if ($banned) - { - foreach (explode(';', $banned) as $item) - { - if ($item != '' && StringHelper::stristr($value, $item) !== false) - { - return false; - } - } - } + if ($banned) { + foreach (explode(';', $banned) as $item) { + if ($item != '' && StringHelper::stristr($value, $item) !== false) { + return false; + } + } + } - return true; - } + return true; + } } diff --git a/components/com_contact/src/Rule/ContactEmailRule.php b/components/com_contact/src/Rule/ContactEmailRule.php index f9bdf9d986ce4..0641c6fdeb307 100644 --- a/components/com_contact/src/Rule/ContactEmailRule.php +++ b/components/com_contact/src/Rule/ContactEmailRule.php @@ -1,4 +1,5 @@ tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. - * @param Form $form The form object for which the field is being tested. - * - * @return boolean True if the value is valid, false otherwise. - */ - public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) - { - if (!parent::test($element, $value, $group, $input, $form)) - { - return false; - } + /** + * Method to test for banned email addresses + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form $form The form object for which the field is being tested. + * + * @return boolean True if the value is valid, false otherwise. + */ + public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + if (!parent::test($element, $value, $group, $input, $form)) { + return false; + } - $params = ComponentHelper::getParams('com_contact'); - $banned = $params->get('banned_email'); + $params = ComponentHelper::getParams('com_contact'); + $banned = $params->get('banned_email'); - if ($banned) - { - foreach (explode(';', $banned) as $item) - { - if ($item != '' && StringHelper::stristr($value, $item) !== false) - { - return false; - } - } - } + if ($banned) { + foreach (explode(';', $banned) as $item) { + if ($item != '' && StringHelper::stristr($value, $item) !== false) { + return false; + } + } + } - return true; - } + return true; + } } diff --git a/components/com_contact/src/Rule/ContactEmailSubjectRule.php b/components/com_contact/src/Rule/ContactEmailSubjectRule.php index cc3ea2247d182..c454c18e18720 100644 --- a/components/com_contact/src/Rule/ContactEmailSubjectRule.php +++ b/components/com_contact/src/Rule/ContactEmailSubjectRule.php @@ -1,4 +1,5 @@ tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. - * @param Form $form The form object for which the field is being tested. - * - * @return boolean True if the value is valid, false otherwise - */ - public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) - { - $params = ComponentHelper::getParams('com_contact'); - $banned = $params->get('banned_subject'); + /** + * Method to test for a banned subject + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form $form The form object for which the field is being tested. + * + * @return boolean True if the value is valid, false otherwise + */ + public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + $params = ComponentHelper::getParams('com_contact'); + $banned = $params->get('banned_subject'); - if ($banned) - { - foreach (explode(';', $banned) as $item) - { - if ($item != '' && StringHelper::stristr($value, $item) !== false) - { - return false; - } - } - } + if ($banned) { + foreach (explode(';', $banned) as $item) { + if ($item != '' && StringHelper::stristr($value, $item) !== false) { + return false; + } + } + } - return true; - } + return true; + } } diff --git a/components/com_contact/src/Service/Category.php b/components/com_contact/src/Service/Category.php index 0b45769ec178c..1bccc5106eaf0 100644 --- a/components/com_contact/src/Service/Category.php +++ b/components/com_contact/src/Service/Category.php @@ -1,4 +1,5 @@ categoryFactory = $categoryFactory; - $this->db = $db; - - $params = ComponentHelper::getParams('com_contact'); - $this->noIDs = (bool) $params->get('sef_ids'); - $categories = new RouterViewConfiguration('categories'); - $categories->setKey('id'); - $this->registerView($categories); - $category = new RouterViewConfiguration('category'); - $category->setKey('id')->setParent($categories, 'catid')->setNestable(); - $this->registerView($category); - $contact = new RouterViewConfiguration('contact'); - $contact->setKey('id')->setParent($category, 'catid'); - $this->registerView($contact); - $this->registerView(new RouterViewConfiguration('featured')); - $form = new RouterViewConfiguration('form'); - $form->setKey('id'); - $this->registerView($form); - - parent::__construct($app, $menu); - - $this->attachRule(new MenuRules($this)); - $this->attachRule(new StandardRules($this)); - $this->attachRule(new NomenuRules($this)); - } - - /** - * Method to get the segment(s) for a category - * - * @param string $id ID of the category to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getCategorySegment($id, $query) - { - $category = $this->getCategories()->get($id); - - if ($category) - { - $path = array_reverse($category->getPath(), true); - $path[0] = '1:root'; - - if ($this->noIDs) - { - foreach ($path as &$segment) - { - list($id, $segment) = explode(':', $segment, 2); - } - } - - return $path; - } - - return array(); - } - - /** - * Method to get the segment(s) for a category - * - * @param string $id ID of the category to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getCategoriesSegment($id, $query) - { - return $this->getCategorySegment($id, $query); - } - - /** - * Method to get the segment(s) for a contact - * - * @param string $id ID of the contact to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getContactSegment($id, $query) - { - if (!strpos($id, ':')) - { - $id = (int) $id; - $dbquery = $this->db->getQuery(true); - $dbquery->select($this->db->quoteName('alias')) - ->from($this->db->quoteName('#__contact_details')) - ->where($this->db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $this->db->setQuery($dbquery); - - $id .= ':' . $this->db->loadResult(); - } - - if ($this->noIDs) - { - list($void, $segment) = explode(':', $id, 2); - - return array($void => $segment); - } - - return array((int) $id => $id); - } - - /** - * Method to get the segment(s) for a form - * - * @param string $id ID of the contact form to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - * - * @since 4.0.0 - */ - public function getFormSegment($id, $query) - { - return $this->getContactSegment($id, $query); - } - - /** - * Method to get the id for a category - * - * @param string $segment Segment to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getCategoryId($segment, $query) - { - if (isset($query['id'])) - { - $category = $this->getCategories(['access' => false])->get($query['id']); - - if ($category) - { - foreach ($category->getChildren() as $child) - { - if ($this->noIDs) - { - if ($child->alias == $segment) - { - return $child->id; - } - } - else - { - if ($child->id == (int) $segment) - { - return $child->id; - } - } - } - } - } - - return false; - } - - /** - * Method to get the segment(s) for a category - * - * @param string $segment Segment to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getCategoriesId($segment, $query) - { - return $this->getCategoryId($segment, $query); - } - - /** - * Method to get the segment(s) for a contact - * - * @param string $segment Segment of the contact to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getContactId($segment, $query) - { - if ($this->noIDs) - { - $dbquery = $this->db->getQuery(true); - $dbquery->select($this->db->quoteName('id')) - ->from($this->db->quoteName('#__contact_details')) - ->where( - [ - $this->db->quoteName('alias') . ' = :alias', - $this->db->quoteName('catid') . ' = :catid', - ] - ) - ->bind(':alias', $segment) - ->bind(':catid', $query['id'], ParameterType::INTEGER); - $this->db->setQuery($dbquery); - - return (int) $this->db->loadResult(); - } - - return (int) $segment; - } - - /** - * Method to get categories from cache - * - * @param array $options The options for retrieving categories - * - * @return CategoryInterface The object containing categories - * - * @since 4.0.0 - */ - private function getCategories(array $options = []): CategoryInterface - { - $key = serialize($options); - - if (!isset($this->categoryCache[$key])) - { - $this->categoryCache[$key] = $this->categoryFactory->createCategory($options); - } - - return $this->categoryCache[$key]; - } + /** + * Flag to remove IDs + * + * @var boolean + */ + protected $noIDs = false; + + /** + * The category factory + * + * @var CategoryFactoryInterface + * + * @since 4.0.0 + */ + private $categoryFactory; + + /** + * The category cache + * + * @var array + * + * @since 4.0.0 + */ + private $categoryCache = []; + + /** + * The db + * + * @var DatabaseInterface + * + * @since 4.0.0 + */ + private $db; + + /** + * Content Component router constructor + * + * @param SiteApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + * @param CategoryFactoryInterface $categoryFactory The category object + * @param DatabaseInterface $db The database object + */ + public function __construct(SiteApplication $app, AbstractMenu $menu, CategoryFactoryInterface $categoryFactory, DatabaseInterface $db) + { + $this->categoryFactory = $categoryFactory; + $this->db = $db; + + $params = ComponentHelper::getParams('com_contact'); + $this->noIDs = (bool) $params->get('sef_ids'); + $categories = new RouterViewConfiguration('categories'); + $categories->setKey('id'); + $this->registerView($categories); + $category = new RouterViewConfiguration('category'); + $category->setKey('id')->setParent($categories, 'catid')->setNestable(); + $this->registerView($category); + $contact = new RouterViewConfiguration('contact'); + $contact->setKey('id')->setParent($category, 'catid'); + $this->registerView($contact); + $this->registerView(new RouterViewConfiguration('featured')); + $form = new RouterViewConfiguration('form'); + $form->setKey('id'); + $this->registerView($form); + + parent::__construct($app, $menu); + + $this->attachRule(new MenuRules($this)); + $this->attachRule(new StandardRules($this)); + $this->attachRule(new NomenuRules($this)); + } + + /** + * Method to get the segment(s) for a category + * + * @param string $id ID of the category to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getCategorySegment($id, $query) + { + $category = $this->getCategories()->get($id); + + if ($category) { + $path = array_reverse($category->getPath(), true); + $path[0] = '1:root'; + + if ($this->noIDs) { + foreach ($path as &$segment) { + list($id, $segment) = explode(':', $segment, 2); + } + } + + return $path; + } + + return array(); + } + + /** + * Method to get the segment(s) for a category + * + * @param string $id ID of the category to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getCategoriesSegment($id, $query) + { + return $this->getCategorySegment($id, $query); + } + + /** + * Method to get the segment(s) for a contact + * + * @param string $id ID of the contact to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getContactSegment($id, $query) + { + if (!strpos($id, ':')) { + $id = (int) $id; + $dbquery = $this->db->getQuery(true); + $dbquery->select($this->db->quoteName('alias')) + ->from($this->db->quoteName('#__contact_details')) + ->where($this->db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $this->db->setQuery($dbquery); + + $id .= ':' . $this->db->loadResult(); + } + + if ($this->noIDs) { + list($void, $segment) = explode(':', $id, 2); + + return array($void => $segment); + } + + return array((int) $id => $id); + } + + /** + * Method to get the segment(s) for a form + * + * @param string $id ID of the contact form to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + * + * @since 4.0.0 + */ + public function getFormSegment($id, $query) + { + return $this->getContactSegment($id, $query); + } + + /** + * Method to get the id for a category + * + * @param string $segment Segment to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getCategoryId($segment, $query) + { + if (isset($query['id'])) { + $category = $this->getCategories(['access' => false])->get($query['id']); + + if ($category) { + foreach ($category->getChildren() as $child) { + if ($this->noIDs) { + if ($child->alias == $segment) { + return $child->id; + } + } else { + if ($child->id == (int) $segment) { + return $child->id; + } + } + } + } + } + + return false; + } + + /** + * Method to get the segment(s) for a category + * + * @param string $segment Segment to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getCategoriesId($segment, $query) + { + return $this->getCategoryId($segment, $query); + } + + /** + * Method to get the segment(s) for a contact + * + * @param string $segment Segment of the contact to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getContactId($segment, $query) + { + if ($this->noIDs) { + $dbquery = $this->db->getQuery(true); + $dbquery->select($this->db->quoteName('id')) + ->from($this->db->quoteName('#__contact_details')) + ->where( + [ + $this->db->quoteName('alias') . ' = :alias', + $this->db->quoteName('catid') . ' = :catid', + ] + ) + ->bind(':alias', $segment) + ->bind(':catid', $query['id'], ParameterType::INTEGER); + $this->db->setQuery($dbquery); + + return (int) $this->db->loadResult(); + } + + return (int) $segment; + } + + /** + * Method to get categories from cache + * + * @param array $options The options for retrieving categories + * + * @return CategoryInterface The object containing categories + * + * @since 4.0.0 + */ + private function getCategories(array $options = []): CategoryInterface + { + $key = serialize($options); + + if (!isset($this->categoryCache[$key])) { + $this->categoryCache[$key] = $this->categoryFactory->createCategory($options); + } + + return $this->categoryCache[$key]; + } } diff --git a/components/com_contact/src/View/Categories/HtmlView.php b/components/com_contact/src/View/Categories/HtmlView.php index 9f447cbe98f68..7e0289409c2df 100644 --- a/components/com_contact/src/View/Categories/HtmlView.php +++ b/components/com_contact/src/View/Categories/HtmlView.php @@ -1,4 +1,5 @@ description = $item->address; - } + $item->description = $item->address; + } } diff --git a/components/com_contact/src/View/Category/HtmlView.php b/components/com_contact/src/View/Category/HtmlView.php index 74bf75ca8c40c..bcf3415abf794 100644 --- a/components/com_contact/src/View/Category/HtmlView.php +++ b/components/com_contact/src/View/Category/HtmlView.php @@ -1,4 +1,5 @@ pagination->hideEmptyLimitstart = true; - - // Prepare the data. - // Compute the contact slug. - foreach ($this->items as $item) - { - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - $temp = $item->params; - $item->params = clone $this->params; - $item->params->merge($temp); - - if ($item->params->get('show_email_headings', 0) == 1) - { - $item->email_to = trim($item->email_to); - - if (!empty($item->email_to) && MailHelper::isEmailAddress($item->email_to)) - { - $item->email_to = HTMLHelper::_('email.cloak', $item->email_to); - } - else - { - $item->email_to = ''; - } - } - } - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - */ - protected function prepareDocument() - { - parent::prepareDocument(); - - parent::addFeed(); - - if ($this->menuItemMatchCategory) - { - // If the active menu item is linked directly to the category being displayed, no further process is needed - return; - } - - // Get ID of the category from active menu item - $menu = $this->menu; - - if ($menu && $menu->component == 'com_contact' && isset($menu->query['view']) - && in_array($menu->query['view'], ['categories', 'category'])) - { - $id = $menu->query['id']; - } - else - { - $id = 0; - } - - $path = [['title' => $this->category->title, 'link' => '']]; - $category = $this->category->getParent(); - - while ($category !== null && $category->id != $id && $category->id !== 'root') - { - $path[] = ['title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)]; - $category = $category->getParent(); - } - - $path = array_reverse($path); - - foreach ($path as $item) - { - $this->pathway->addItem($item['title'], $item['link']); - } - } + /** + * @var string The name of the extension for the category + * @since 3.2 + */ + protected $extension = 'com_contact'; + + /** + * @var string Default title to use for page title + * @since 3.2 + */ + protected $defaultPageTitle = 'COM_CONTACT_DEFAULT_PAGE_TITLE'; + + /** + * @var string The name of the view to link individual items to + * @since 3.2 + */ + protected $viewName = 'contact'; + + /** + * Run the standard Joomla plugins + * + * @var boolean + * @since 3.5 + */ + protected $runPlugins = true; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + parent::commonCategoryDisplay(); + + // Flag indicates to not add limitstart=0 to URL + $this->pagination->hideEmptyLimitstart = true; + + // Prepare the data. + // Compute the contact slug. + foreach ($this->items as $item) { + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + $temp = $item->params; + $item->params = clone $this->params; + $item->params->merge($temp); + + if ($item->params->get('show_email_headings', 0) == 1) { + $item->email_to = trim($item->email_to); + + if (!empty($item->email_to) && MailHelper::isEmailAddress($item->email_to)) { + $item->email_to = HTMLHelper::_('email.cloak', $item->email_to); + } else { + $item->email_to = ''; + } + } + } + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + */ + protected function prepareDocument() + { + parent::prepareDocument(); + + parent::addFeed(); + + if ($this->menuItemMatchCategory) { + // If the active menu item is linked directly to the category being displayed, no further process is needed + return; + } + + // Get ID of the category from active menu item + $menu = $this->menu; + + if ( + $menu && $menu->component == 'com_contact' && isset($menu->query['view']) + && in_array($menu->query['view'], ['categories', 'category']) + ) { + $id = $menu->query['id']; + } else { + $id = 0; + } + + $path = [['title' => $this->category->title, 'link' => '']]; + $category = $this->category->getParent(); + + while ($category !== null && $category->id != $id && $category->id !== 'root') { + $path[] = ['title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)]; + $category = $category->getParent(); + } + + $path = array_reverse($path); + + foreach ($path as $item) { + $this->pathway->addItem($item['title'], $item['link']); + } + } } diff --git a/components/com_contact/src/View/Contact/HtmlView.php b/components/com_contact/src/View/Contact/HtmlView.php index 0419adf686b98..7c7fcd01558eb 100644 --- a/components/com_contact/src/View/Contact/HtmlView.php +++ b/components/com_contact/src/View/Contact/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser(); - $state = $this->get('State'); - $item = $this->get('Item'); - $this->form = $this->get('Form'); - $params = $state->get('params'); - $contacts = []; - - $temp = clone $params; - - $active = $app->getMenu()->getActive(); - - // If the current view is the active item and a contact view for this contact, then the menu item params take priority - if ($active - && $active->component == 'com_contact' - && isset($active->query['view'], $active->query['id']) - && $active->query['view'] == 'contact' - && $active->query['id'] == $item->id) - { - $this->menuItemMatchContact = true; - - // Load layout from active query (in case it is an alternative menu item) - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - // Check for alternative layout of contact - elseif ($layout = $item->params->get('contact_layout')) - { - $this->setLayout($layout); - } - - $item->params->merge($temp); - } - else - { - // Merge so that contact params take priority - $temp->merge($item->params); - $item->params = $temp; - - if ($layout = $item->params->get('contact_layout')) - { - $this->setLayout($layout); - } - } - - // Collect extra contact information when this information is required - if ($item && $item->params->get('show_contact_list')) - { - // Get Category Model data - /** @var \Joomla\Component\Contact\Site\Model\CategoryModel $categoryModel */ - $categoryModel = $app->bootComponent('com_contact')->getMVCFactory() - ->createModel('Category', 'Site', ['ignore_request' => true]); - - $categoryModel->setState('category.id', $item->catid); - $categoryModel->setState('list.ordering', 'a.name'); - $categoryModel->setState('list.direction', 'asc'); - $categoryModel->setState('filter.published', 1); - - $contacts = $categoryModel->getItems(); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check if access is not public - $groups = $user->getAuthorisedViewLevels(); - - if (!in_array($item->access, $groups) || !in_array($item->category_access, $groups)) - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $app->setHeader('status', 403, true); - - return false; - } - - $options['category_id'] = $item->catid; - $options['order by'] = 'a.default_con DESC, a.ordering ASC'; - - /** - * Handle email cloaking - * - * Keep a copy of the raw email address so it can - * still be accessed in the layout if needed. - */ - $item->email_raw = $item->email_to; - - if ($item->email_to && $item->params->get('show_email')) - { - $item->email_to = HTMLHelper::_('email.cloak', $item->email_to, (bool) $item->params->get('add_mailto_link', true)); - } - - if ($item->params->get('show_street_address') || $item->params->get('show_suburb') || $item->params->get('show_state') - || $item->params->get('show_postcode') || $item->params->get('show_country')) - { - if (!empty($item->address) || !empty($item->suburb) || !empty($item->state) || !empty($item->country) || !empty($item->postcode)) - { - $item->params->set('address_check', 1); - } - } - else - { - $item->params->set('address_check', 0); - } - - // Manage the display mode for contact detail groups - switch ($item->params->get('contact_icons')) - { - case 1: - // Text - $item->params->set('marker_address', Text::_('COM_CONTACT_ADDRESS') . ': '); - $item->params->set('marker_email', Text::_('JGLOBAL_EMAIL') . ': '); - $item->params->set('marker_telephone', Text::_('COM_CONTACT_TELEPHONE') . ': '); - $item->params->set('marker_fax', Text::_('COM_CONTACT_FAX') . ': '); - $item->params->set('marker_mobile', Text::_('COM_CONTACT_MOBILE') . ': '); - $item->params->set('marker_webpage', Text::_('COM_CONTACT_WEBPAGE') . ': '); - $item->params->set('marker_misc', Text::_('COM_CONTACT_OTHER_INFORMATION') . ': '); - $item->params->set('marker_class', 'jicons-text'); - break; - - case 2: - // None - $item->params->set('marker_address', ''); - $item->params->set('marker_email', ''); - $item->params->set('marker_telephone', ''); - $item->params->set('marker_mobile', ''); - $item->params->set('marker_fax', ''); - $item->params->set('marker_misc', ''); - $item->params->set('marker_webpage', ''); - $item->params->set('marker_class', 'jicons-none'); - break; - - default: - if ($item->params->get('icon_address')) - { - $item->params->set( - 'marker_address', - HTMLHelper::_('image', $item->params->get('icon_address', ''), Text::_('COM_CONTACT_ADDRESS'), false) - ); - } - - if ($item->params->get('icon_email')) - { - $item->params->set( - 'marker_email', - HTMLHelper::_('image', $item->params->get('icon_email', ''), Text::_('COM_CONTACT_EMAIL'), false) - ); - } - - if ($item->params->get('icon_telephone')) - { - $item->params->set( - 'marker_telephone', - HTMLHelper::_('image', $item->params->get('icon_telephone', ''), Text::_('COM_CONTACT_TELEPHONE'), false) - ); - } - - if ($item->params->get('icon_fax', '')) - { - $item->params->set( - 'marker_fax', - HTMLHelper::_('image', $item->params->get('icon_fax', ''), Text::_('COM_CONTACT_FAX'), false) - ); - } - - if ($item->params->get('icon_misc')) - { - $item->params->set( - 'marker_misc', - HTMLHelper::_('image', $item->params->get('icon_misc', ''), Text::_('COM_CONTACT_OTHER_INFORMATION'), false) - ); - } - - if ($item->params->get('icon_mobile')) - { - $item->params->set( - 'marker_mobile', - HTMLHelper::_('image', $item->params->get('icon_mobile', ''), Text::_('COM_CONTACT_MOBILE'), false) - ); - } - - if ($item->params->get('icon_webpage')) - { - $item->params->set( - 'marker_webpage', - HTMLHelper::_('image', $item->params->get('icon_webpage', ''), Text::_('COM_CONTACT_WEBPAGE'), false) - ); - } - - $item->params->set('marker_class', 'jicons-icons'); - break; - } - - // Add links to contacts - if ($item->params->get('show_contact_list') && count($contacts) > 1) - { - foreach ($contacts as &$contact) - { - $contact->link = Route::_(RouteHelper::getContactRoute($contact->slug, $contact->catid, $contact->language)); - } - - $item->link = Route::_(RouteHelper::getContactRoute($item->slug, $item->catid, $item->language), false); - } - - // Process the content plugins. - PluginHelper::importPlugin('content'); - $offset = $state->get('list.offset'); - - // Fix for where some plugins require a text attribute - $item->text = ''; - - if (!empty($item->misc)) - { - $item->text = $item->misc; - } - - $app->triggerEvent('onContentPrepare', array ('com_contact.contact', &$item, &$item->params, $offset)); - - // Store the events for later - $item->event = new \stdClass; - $results = $app->triggerEvent('onContentAfterTitle', array('com_contact.contact', &$item, &$item->params, $offset)); - $item->event->afterDisplayTitle = trim(implode("\n", $results)); - - $results = $app->triggerEvent('onContentBeforeDisplay', array('com_contact.contact', &$item, &$item->params, $offset)); - $item->event->beforeDisplayContent = trim(implode("\n", $results)); - - $results = $app->triggerEvent('onContentAfterDisplay', array('com_contact.contact', &$item, &$item->params, $offset)); - $item->event->afterDisplayContent = trim(implode("\n", $results)); - - if (!empty($item->text)) - { - $item->misc = $item->text; - } - - $contactUser = null; - - if ($item->params->get('show_user_custom_fields') && $item->user_id && $contactUser = Factory::getUser($item->user_id)) - { - $contactUser->text = ''; - $app->triggerEvent('onContentPrepare', array ('com_users.user', &$contactUser, &$item->params, 0)); - - if (!isset($contactUser->jcfields)) - { - $contactUser->jcfields = array(); - } - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($item->params->get('pageclass_sfx', '')); - - $this->params = &$item->params; - $this->state = &$state; - $this->item = &$item; - $this->user = &$user; - $this->contacts = &$contacts; - $this->contactUser = $contactUser; - - $model = $this->getModel(); - $model->hit(); - - $captchaSet = $item->params->get('captcha', $app->get('captcha', '0')); - - foreach (PluginHelper::getPlugin('captcha') as $plugin) - { - if ($captchaSet === $plugin->name) - { - $this->captchaEnabled = true; - break; - } - } - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - * - * @since 1.6 - */ - protected function _prepareDocument() - { - $app = Factory::getApplication(); - $pathway = $app->getPathway(); - - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = $app->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_CONTACT_DEFAULT_PAGE_TITLE')); - } - - $title = $this->params->get('page_title', ''); - - // If the menu item does not concern this contact - if (!$this->menuItemMatchContact) - { - // If this is not a single contact menu item, set the page title to the contact title - if ($this->item->name) - { - $title = $this->item->name; - } - - // Get ID of the category from active menu item - if ($menu && $menu->component == 'com_contact' && isset($menu->query['view']) - && in_array($menu->query['view'], ['categories', 'category'])) - { - $id = $menu->query['id']; - } - else - { - $id = 0; - } - - $path = array(array('title' => $this->item->name, 'link' => '')); - $category = Categories::getInstance('Contact')->get($this->item->catid); - - while ($category !== null && $category->id != $id && $category->id !== 'root') - { - $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)); - $category = $category->getParent(); - } - - $path = array_reverse($path); - - foreach ($path as $item) - { - $pathway->addItem($item['title'], $item['link']); - } - } - - if (empty($title)) - { - $title = $this->item->name; - } - - $this->setDocumentTitle($title); - - if ($this->item->metadesc) - { - $this->document->setDescription($this->item->metadesc); - } - elseif ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - - $mdata = $this->item->metadata->toArray(); - - foreach ($mdata as $k => $v) - { - if ($v) - { - $this->document->setMetaData($k, $v); - } - } - } + /** + * The item model state + * + * @var \Joomla\Registry\Registry + * + * @since 1.6 + */ + protected $state; + + /** + * The form object for the contact item + * + * @var \Joomla\CMS\Form\Form + * + * @since 1.6 + */ + protected $form; + + /** + * The item object details + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 1.6 + */ + protected $item; + + /** + * The page to return to on submission + * + * @var string + * + * @since 1.6 + * + * @todo Implement this functionality + */ + protected $return_page = ''; + + /** + * Should we show a captcha form for the submission of the contact request? + * + * @var boolean + * + * @since 3.6.3 + */ + protected $captchaEnabled = false; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * The user object + * + * @var \Joomla\CMS\User\User + * + * @since 4.0.0 + */ + protected $user; + + /** + * Other contacts in this contacts category + * + * @var array + * + * @since 4.0.0 + */ + protected $contacts; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The flag to mark if the active menu item is linked to the contact being displayed + * + * @var boolean + * + * @since 4.0.0 + */ + protected $menuItemMatchContact = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void|boolean + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $user = $this->getCurrentUser(); + $state = $this->get('State'); + $item = $this->get('Item'); + $this->form = $this->get('Form'); + $params = $state->get('params'); + $contacts = []; + + $temp = clone $params; + + $active = $app->getMenu()->getActive(); + + // If the current view is the active item and a contact view for this contact, then the menu item params take priority + if ( + $active + && $active->component == 'com_contact' + && isset($active->query['view'], $active->query['id']) + && $active->query['view'] == 'contact' + && $active->query['id'] == $item->id + ) { + $this->menuItemMatchContact = true; + + // Load layout from active query (in case it is an alternative menu item) + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } elseif ($layout = $item->params->get('contact_layout')) { + // Check for alternative layout of contact + $this->setLayout($layout); + } + + $item->params->merge($temp); + } else { + // Merge so that contact params take priority + $temp->merge($item->params); + $item->params = $temp; + + if ($layout = $item->params->get('contact_layout')) { + $this->setLayout($layout); + } + } + + // Collect extra contact information when this information is required + if ($item && $item->params->get('show_contact_list')) { + // Get Category Model data + /** @var \Joomla\Component\Contact\Site\Model\CategoryModel $categoryModel */ + $categoryModel = $app->bootComponent('com_contact')->getMVCFactory() + ->createModel('Category', 'Site', ['ignore_request' => true]); + + $categoryModel->setState('category.id', $item->catid); + $categoryModel->setState('list.ordering', 'a.name'); + $categoryModel->setState('list.direction', 'asc'); + $categoryModel->setState('filter.published', 1); + + $contacts = $categoryModel->getItems(); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check if access is not public + $groups = $user->getAuthorisedViewLevels(); + + if (!in_array($item->access, $groups) || !in_array($item->category_access, $groups)) { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $app->setHeader('status', 403, true); + + return false; + } + + $options['category_id'] = $item->catid; + $options['order by'] = 'a.default_con DESC, a.ordering ASC'; + + /** + * Handle email cloaking + * + * Keep a copy of the raw email address so it can + * still be accessed in the layout if needed. + */ + $item->email_raw = $item->email_to; + + if ($item->email_to && $item->params->get('show_email')) { + $item->email_to = HTMLHelper::_('email.cloak', $item->email_to, (bool) $item->params->get('add_mailto_link', true)); + } + + if ( + $item->params->get('show_street_address') || $item->params->get('show_suburb') || $item->params->get('show_state') + || $item->params->get('show_postcode') || $item->params->get('show_country') + ) { + if (!empty($item->address) || !empty($item->suburb) || !empty($item->state) || !empty($item->country) || !empty($item->postcode)) { + $item->params->set('address_check', 1); + } + } else { + $item->params->set('address_check', 0); + } + + // Manage the display mode for contact detail groups + switch ($item->params->get('contact_icons')) { + case 1: + // Text + $item->params->set('marker_address', Text::_('COM_CONTACT_ADDRESS') . ': '); + $item->params->set('marker_email', Text::_('JGLOBAL_EMAIL') . ': '); + $item->params->set('marker_telephone', Text::_('COM_CONTACT_TELEPHONE') . ': '); + $item->params->set('marker_fax', Text::_('COM_CONTACT_FAX') . ': '); + $item->params->set('marker_mobile', Text::_('COM_CONTACT_MOBILE') . ': '); + $item->params->set('marker_webpage', Text::_('COM_CONTACT_WEBPAGE') . ': '); + $item->params->set('marker_misc', Text::_('COM_CONTACT_OTHER_INFORMATION') . ': '); + $item->params->set('marker_class', 'jicons-text'); + break; + + case 2: + // None + $item->params->set('marker_address', ''); + $item->params->set('marker_email', ''); + $item->params->set('marker_telephone', ''); + $item->params->set('marker_mobile', ''); + $item->params->set('marker_fax', ''); + $item->params->set('marker_misc', ''); + $item->params->set('marker_webpage', ''); + $item->params->set('marker_class', 'jicons-none'); + break; + + default: + if ($item->params->get('icon_address')) { + $item->params->set( + 'marker_address', + HTMLHelper::_('image', $item->params->get('icon_address', ''), Text::_('COM_CONTACT_ADDRESS'), false) + ); + } + + if ($item->params->get('icon_email')) { + $item->params->set( + 'marker_email', + HTMLHelper::_('image', $item->params->get('icon_email', ''), Text::_('COM_CONTACT_EMAIL'), false) + ); + } + + if ($item->params->get('icon_telephone')) { + $item->params->set( + 'marker_telephone', + HTMLHelper::_('image', $item->params->get('icon_telephone', ''), Text::_('COM_CONTACT_TELEPHONE'), false) + ); + } + + if ($item->params->get('icon_fax', '')) { + $item->params->set( + 'marker_fax', + HTMLHelper::_('image', $item->params->get('icon_fax', ''), Text::_('COM_CONTACT_FAX'), false) + ); + } + + if ($item->params->get('icon_misc')) { + $item->params->set( + 'marker_misc', + HTMLHelper::_('image', $item->params->get('icon_misc', ''), Text::_('COM_CONTACT_OTHER_INFORMATION'), false) + ); + } + + if ($item->params->get('icon_mobile')) { + $item->params->set( + 'marker_mobile', + HTMLHelper::_('image', $item->params->get('icon_mobile', ''), Text::_('COM_CONTACT_MOBILE'), false) + ); + } + + if ($item->params->get('icon_webpage')) { + $item->params->set( + 'marker_webpage', + HTMLHelper::_('image', $item->params->get('icon_webpage', ''), Text::_('COM_CONTACT_WEBPAGE'), false) + ); + } + + $item->params->set('marker_class', 'jicons-icons'); + break; + } + + // Add links to contacts + if ($item->params->get('show_contact_list') && count($contacts) > 1) { + foreach ($contacts as &$contact) { + $contact->link = Route::_(RouteHelper::getContactRoute($contact->slug, $contact->catid, $contact->language)); + } + + $item->link = Route::_(RouteHelper::getContactRoute($item->slug, $item->catid, $item->language), false); + } + + // Process the content plugins. + PluginHelper::importPlugin('content'); + $offset = $state->get('list.offset'); + + // Fix for where some plugins require a text attribute + $item->text = ''; + + if (!empty($item->misc)) { + $item->text = $item->misc; + } + + $app->triggerEvent('onContentPrepare', array ('com_contact.contact', &$item, &$item->params, $offset)); + + // Store the events for later + $item->event = new \stdClass(); + $results = $app->triggerEvent('onContentAfterTitle', array('com_contact.contact', &$item, &$item->params, $offset)); + $item->event->afterDisplayTitle = trim(implode("\n", $results)); + + $results = $app->triggerEvent('onContentBeforeDisplay', array('com_contact.contact', &$item, &$item->params, $offset)); + $item->event->beforeDisplayContent = trim(implode("\n", $results)); + + $results = $app->triggerEvent('onContentAfterDisplay', array('com_contact.contact', &$item, &$item->params, $offset)); + $item->event->afterDisplayContent = trim(implode("\n", $results)); + + if (!empty($item->text)) { + $item->misc = $item->text; + } + + $contactUser = null; + + if ($item->params->get('show_user_custom_fields') && $item->user_id && $contactUser = Factory::getUser($item->user_id)) { + $contactUser->text = ''; + $app->triggerEvent('onContentPrepare', array ('com_users.user', &$contactUser, &$item->params, 0)); + + if (!isset($contactUser->jcfields)) { + $contactUser->jcfields = array(); + } + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($item->params->get('pageclass_sfx', '')); + + $this->params = &$item->params; + $this->state = &$state; + $this->item = &$item; + $this->user = &$user; + $this->contacts = &$contacts; + $this->contactUser = $contactUser; + + $model = $this->getModel(); + $model->hit(); + + $captchaSet = $item->params->get('captcha', $app->get('captcha', '0')); + + foreach (PluginHelper::getPlugin('captcha') as $plugin) { + if ($captchaSet === $plugin->name) { + $this->captchaEnabled = true; + break; + } + } + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + * + * @since 1.6 + */ + protected function _prepareDocument() + { + $app = Factory::getApplication(); + $pathway = $app->getPathway(); + + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = $app->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_CONTACT_DEFAULT_PAGE_TITLE')); + } + + $title = $this->params->get('page_title', ''); + + // If the menu item does not concern this contact + if (!$this->menuItemMatchContact) { + // If this is not a single contact menu item, set the page title to the contact title + if ($this->item->name) { + $title = $this->item->name; + } + + // Get ID of the category from active menu item + if ( + $menu && $menu->component == 'com_contact' && isset($menu->query['view']) + && in_array($menu->query['view'], ['categories', 'category']) + ) { + $id = $menu->query['id']; + } else { + $id = 0; + } + + $path = array(array('title' => $this->item->name, 'link' => '')); + $category = Categories::getInstance('Contact')->get($this->item->catid); + + while ($category !== null && $category->id != $id && $category->id !== 'root') { + $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)); + $category = $category->getParent(); + } + + $path = array_reverse($path); + + foreach ($path as $item) { + $pathway->addItem($item['title'], $item['link']); + } + } + + if (empty($title)) { + $title = $this->item->name; + } + + $this->setDocumentTitle($title); + + if ($this->item->metadesc) { + $this->document->setDescription($this->item->metadesc); + } elseif ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + + $mdata = $this->item->metadata->toArray(); + + foreach ($mdata as $k => $v) { + if ($v) { + $this->document->setMetaData($k, $v); + } + } + } } diff --git a/components/com_contact/src/View/Contact/VcfView.php b/components/com_contact/src/View/Contact/VcfView.php index 14d05d9745869..991bf64f3157b 100644 --- a/components/com_contact/src/View/Contact/VcfView.php +++ b/components/com_contact/src/View/Contact/VcfView.php @@ -1,4 +1,5 @@ get('Item'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - $this->document->setMimeEncoding('text/directory', true); - - // Compute lastname, firstname and middlename - $item->name = trim($item->name); - - // "Lastname, Firstname Middlename" format support - // e.g. "de Gaulle, Charles" - $namearray = explode(',', $item->name); - - if (count($namearray) > 1) - { - $lastname = $namearray[0]; - $card_name = $lastname; - $name_and_midname = trim($namearray[1]); - - $firstname = ''; - - if (!empty($name_and_midname)) - { - $namearray = explode(' ', $name_and_midname); - - $firstname = $namearray[0]; - $middlename = (count($namearray) > 1) ? $namearray[1] : ''; - $card_name = $firstname . ' ' . ($middlename ? $middlename . ' ' : '') . $card_name; - } - } - // "Firstname Middlename Lastname" format support - else - { - $namearray = explode(' ', $item->name); - - $middlename = (count($namearray) > 2) ? $namearray[1] : ''; - $firstname = array_shift($namearray); - $lastname = count($namearray) ? end($namearray) : ''; - $card_name = $firstname . ($middlename ? ' ' . $middlename : '') . ($lastname ? ' ' . $lastname : ''); - } - - $rev = date('c', strtotime($item->modified)); - - Factory::getApplication()->setHeader('Content-disposition', 'attachment; filename="' . $card_name . '.vcf"', true); - - $vcard = []; - $vcard[] .= 'BEGIN:VCARD'; - $vcard[] .= 'VERSION:3.0'; - $vcard[] = 'N:' . $lastname . ';' . $firstname . ';' . $middlename; - $vcard[] = 'FN:' . $item->name; - $vcard[] = 'TITLE:' . $item->con_position; - $vcard[] = 'TEL;TYPE=WORK,VOICE:' . $item->telephone; - $vcard[] = 'TEL;TYPE=WORK,FAX:' . $item->fax; - $vcard[] = 'TEL;TYPE=WORK,MOBILE:' . $item->mobile; - $vcard[] = 'ADR;TYPE=WORK:;;' . $item->address . ';' . $item->suburb . ';' . $item->state . ';' . $item->postcode . ';' . $item->country; - $vcard[] = 'LABEL;TYPE=WORK:' . $item->address . "\n" . $item->suburb . "\n" . $item->state . "\n" . $item->postcode . "\n" . $item->country; - $vcard[] = 'EMAIL;TYPE=PREF,INTERNET:' . $item->email_to; - $vcard[] = 'URL:' . $item->webpage; - $vcard[] = 'REV:' . $rev . 'Z'; - $vcard[] = 'END:VCARD'; - - echo implode("\n", $vcard); - } + /** + * The contact item + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $item; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return string A string if successful + * + * @throws GenericDataException + */ + public function display($tpl = null) + { + // Get model data. + $item = $this->get('Item'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + $this->document->setMimeEncoding('text/directory', true); + + // Compute lastname, firstname and middlename + $item->name = trim($item->name); + + // "Lastname, Firstname Middlename" format support + // e.g. "de Gaulle, Charles" + $namearray = explode(',', $item->name); + + if (count($namearray) > 1) { + $lastname = $namearray[0]; + $card_name = $lastname; + $name_and_midname = trim($namearray[1]); + + $firstname = ''; + + if (!empty($name_and_midname)) { + $namearray = explode(' ', $name_and_midname); + + $firstname = $namearray[0]; + $middlename = (count($namearray) > 1) ? $namearray[1] : ''; + $card_name = $firstname . ' ' . ($middlename ? $middlename . ' ' : '') . $card_name; + } + } else { + // "Firstname Middlename Lastname" format support + $namearray = explode(' ', $item->name); + + $middlename = (count($namearray) > 2) ? $namearray[1] : ''; + $firstname = array_shift($namearray); + $lastname = count($namearray) ? end($namearray) : ''; + $card_name = $firstname . ($middlename ? ' ' . $middlename : '') . ($lastname ? ' ' . $lastname : ''); + } + + $rev = date('c', strtotime($item->modified)); + + Factory::getApplication()->setHeader('Content-disposition', 'attachment; filename="' . $card_name . '.vcf"', true); + + $vcard = []; + $vcard[] .= 'BEGIN:VCARD'; + $vcard[] .= 'VERSION:3.0'; + $vcard[] = 'N:' . $lastname . ';' . $firstname . ';' . $middlename; + $vcard[] = 'FN:' . $item->name; + $vcard[] = 'TITLE:' . $item->con_position; + $vcard[] = 'TEL;TYPE=WORK,VOICE:' . $item->telephone; + $vcard[] = 'TEL;TYPE=WORK,FAX:' . $item->fax; + $vcard[] = 'TEL;TYPE=WORK,MOBILE:' . $item->mobile; + $vcard[] = 'ADR;TYPE=WORK:;;' . $item->address . ';' . $item->suburb . ';' . $item->state . ';' . $item->postcode . ';' . $item->country; + $vcard[] = 'LABEL;TYPE=WORK:' . $item->address . "\n" . $item->suburb . "\n" . $item->state . "\n" . $item->postcode . "\n" . $item->country; + $vcard[] = 'EMAIL;TYPE=PREF,INTERNET:' . $item->email_to; + $vcard[] = 'URL:' . $item->webpage; + $vcard[] = 'REV:' . $rev . 'Z'; + $vcard[] = 'END:VCARD'; + + echo implode("\n", $vcard); + } } diff --git a/components/com_contact/src/View/Featured/HtmlView.php b/components/com_contact/src/View/Featured/HtmlView.php index 61e370842ed0e..6101ac7199e2a 100644 --- a/components/com_contact/src/View/Featured/HtmlView.php +++ b/components/com_contact/src/View/Featured/HtmlView.php @@ -1,4 +1,5 @@ getParams(); - - // Get some data from the models - $state = $this->get('State'); - $items = $this->get('Items'); - $category = $this->get('Category'); - $children = $this->get('Children'); - $parent = $this->get('Parent'); - $pagination = $this->get('Pagination'); - - // Flag indicates to not add limitstart=0 to URL - $pagination->hideEmptyLimitstart = true; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Prepare the data. - // Compute the contact slug. - for ($i = 0, $n = count($items); $i < $n; $i++) - { - $item = &$items[$i]; - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - $temp = $item->params; - $item->params = clone $params; - $item->params->merge($temp); - - if ($item->params->get('show_email', 0) == 1) - { - $item->email_to = trim($item->email_to); - - if (!empty($item->email_to) && MailHelper::isEmailAddress($item->email_to)) - { - $item->email_to = HTMLHelper::_('email.cloak', $item->email_to); - } - else - { - $item->email_to = ''; - } - } - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $maxLevel = $params->get('maxLevel', -1); - $this->maxLevel = &$maxLevel; - $this->state = &$state; - $this->items = &$items; - $this->category = &$category; - $this->children = &$children; - $this->params = &$params; - $this->parent = &$parent; - $this->pagination = &$pagination; - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - * - * @since 1.6 - */ - protected function _prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_CONTACT_DEFAULT_PAGE_TITLE')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The item model state + * + * @var \Joomla\Registry\Registry + * + * @since 1.6.0 + */ + protected $state; + + /** + * The item details + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 1.6.0 + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 1.6.0 + */ + protected $pagination; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * Method to display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $params = $app->getParams(); + + // Get some data from the models + $state = $this->get('State'); + $items = $this->get('Items'); + $category = $this->get('Category'); + $children = $this->get('Children'); + $parent = $this->get('Parent'); + $pagination = $this->get('Pagination'); + + // Flag indicates to not add limitstart=0 to URL + $pagination->hideEmptyLimitstart = true; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Prepare the data. + // Compute the contact slug. + for ($i = 0, $n = count($items); $i < $n; $i++) { + $item = &$items[$i]; + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + $temp = $item->params; + $item->params = clone $params; + $item->params->merge($temp); + + if ($item->params->get('show_email', 0) == 1) { + $item->email_to = trim($item->email_to); + + if (!empty($item->email_to) && MailHelper::isEmailAddress($item->email_to)) { + $item->email_to = HTMLHelper::_('email.cloak', $item->email_to); + } else { + $item->email_to = ''; + } + } + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $maxLevel = $params->get('maxLevel', -1); + $this->maxLevel = &$maxLevel; + $this->state = &$state; + $this->items = &$items; + $this->category = &$category; + $this->children = &$children; + $this->params = &$params; + $this->parent = &$parent; + $this->pagination = &$pagination; + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + * + * @since 1.6 + */ + protected function _prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_CONTACT_DEFAULT_PAGE_TITLE')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_contact/src/View/Form/HtmlView.php b/components/com_contact/src/View/Form/HtmlView.php index 498db543fae8c..fdca6a75dc853 100644 --- a/components/com_contact/src/View/Form/HtmlView.php +++ b/components/com_contact/src/View/Form/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser(); - $app = Factory::getApplication(); - - // Get model data. - $this->state = $this->get('State'); - $this->item = $this->get('Item'); - $this->form = $this->get('Form'); - $this->return_page = $this->get('ReturnPage'); - - if (empty($this->item->id)) - { - $authorised = $user->authorise('core.create', 'com_contact') || count($user->getAuthorisedCategories('com_contact', 'core.create')); - } - else - { - // Since we don't track these assets at the item level, use the category id. - $canDo = ContactHelper::getActions('com_contact', 'category', $this->item->catid); - $authorised = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by === $user->id); - } - - if ($authorised !== true) - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $app->setHeader('status', 403, true); - - return false; - } - - $this->item->tags = new TagsHelper; - - if (!empty($this->item->id)) - { - $this->item->tags->getItemTags('com_contact.contact', $this->item->id); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - $app->enqueueMessage(implode("\n", $errors), 'error'); - - return false; - } - - // Create a shortcut to the parameters. - $this->params = $this->state->params; - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); - - // Override global params with contact specific params - $this->params->merge($this->item->params); - - // Propose current language as default when creating new contact - if (empty($this->item->id) && Multilanguage::isEnabled()) - { - $lang = Factory::getLanguage()->getTag(); - $this->form->setFieldAttribute('language', 'default', $lang); - } - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - * - * @throws \Exception - * - * @since 4.0.0 - */ - protected function _prepareDocument() - { - $app = Factory::getApplication(); - - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = $app->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_CONTACT_FORM_EDIT_CONTACT')); - } - - $title = $this->params->def('page_title', Text::_('COM_CONTACT_FORM_EDIT_CONTACT')); - - $this->setDocumentTitle($title); - - $pathway = $app->getPathWay(); - $pathway->addItem($title, ''); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('menu-meta_keywords')) - { - $this->document->setMetaData('keywords', $this->params->get('menu-meta_keywords')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * @var \Joomla\CMS\Form\Form + * @since 4.0.0 + */ + protected $form; + + /** + * @var object + * @since 4.0.0 + */ + protected $item; + + /** + * @var string + * @since 4.0.0 + */ + protected $return_page; + + /** + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx; + + /** + * @var \Joomla\Registry\Registry + * @since 4.0.0 + */ + protected $state; + + /** + * @var \Joomla\Registry\Registry + * @since 4.0.0 + */ + protected $params; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void|boolean + * + * @throws \Exception + * @since 4.0.0 + */ + public function display($tpl = null) + { + $user = $this->getCurrentUser(); + $app = Factory::getApplication(); + + // Get model data. + $this->state = $this->get('State'); + $this->item = $this->get('Item'); + $this->form = $this->get('Form'); + $this->return_page = $this->get('ReturnPage'); + + if (empty($this->item->id)) { + $authorised = $user->authorise('core.create', 'com_contact') || count($user->getAuthorisedCategories('com_contact', 'core.create')); + } else { + // Since we don't track these assets at the item level, use the category id. + $canDo = ContactHelper::getActions('com_contact', 'category', $this->item->catid); + $authorised = $canDo->get('core.edit') || ($canDo->get('core.edit.own') && $this->item->created_by === $user->id); + } + + if ($authorised !== true) { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $app->setHeader('status', 403, true); + + return false; + } + + $this->item->tags = new TagsHelper(); + + if (!empty($this->item->id)) { + $this->item->tags->getItemTags('com_contact.contact', $this->item->id); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + $app->enqueueMessage(implode("\n", $errors), 'error'); + + return false; + } + + // Create a shortcut to the parameters. + $this->params = $this->state->params; + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); + + // Override global params with contact specific params + $this->params->merge($this->item->params); + + // Propose current language as default when creating new contact + if (empty($this->item->id) && Multilanguage::isEnabled()) { + $lang = Factory::getLanguage()->getTag(); + $this->form->setFieldAttribute('language', 'default', $lang); + } + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + * + * @throws \Exception + * + * @since 4.0.0 + */ + protected function _prepareDocument() + { + $app = Factory::getApplication(); + + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = $app->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_CONTACT_FORM_EDIT_CONTACT')); + } + + $title = $this->params->def('page_title', Text::_('COM_CONTACT_FORM_EDIT_CONTACT')); + + $this->setDocumentTitle($title); + + $pathway = $app->getPathWay(); + $pathway->addItem($title, ''); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('menu-meta_keywords')) { + $this->document->setMetaData('keywords', $this->params->get('menu-meta_keywords')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_contact/tmpl/categories/default.php b/components/com_contact/tmpl/categories/default.php index a8c558dafc292..2e23579be8a5b 100644 --- a/components/com_contact/tmpl/categories/default.php +++ b/components/com_contact/tmpl/categories/default.php @@ -1,4 +1,5 @@
    - loadTemplate('items'); - ?> + loadTemplate('items'); + ?>
    diff --git a/components/com_contact/tmpl/categories/default_items.php b/components/com_contact/tmpl/categories/default_items.php index d18c34fc3e59a..5f8a812d86663 100644 --- a/components/com_contact/tmpl/categories/default_items.php +++ b/components/com_contact/tmpl/categories/default_items.php @@ -1,4 +1,5 @@ maxLevelcat != 0 && count($this->items[$this->parent->id]) > 0) : -?> - items[$this->parent->id] as $id => $item) : ?> - params->get('show_empty_categories_cat') || $item->numitems || count($item->getChildren())) : ?> -
    - - params->get('show_subcat_desc_cat') == 1) : ?> - description) : ?> -
    - description, '', 'com_contact.categories'); ?> -
    - - + ?> + items[$this->parent->id] as $id => $item) : ?> + params->get('show_empty_categories_cat') || $item->numitems || count($item->getChildren())) : ?> +
    + + params->get('show_subcat_desc_cat') == 1) : ?> + description) : ?> +
    + description, '', 'com_contact.categories'); ?> +
    + + - maxLevelcat > 1 && count($item->getChildren()) > 0) : ?> -
    - items[$item->id] = $item->getChildren(); - $this->parent = $item; - $this->maxLevelcat--; - echo $this->loadTemplate('items'); - $this->parent = $item->getParent(); - $this->maxLevelcat++; - ?> -
    - -
    - - + maxLevelcat > 1 && count($item->getChildren()) > 0) : ?> +
    + items[$item->id] = $item->getChildren(); + $this->parent = $item; + $this->maxLevelcat--; + echo $this->loadTemplate('items'); + $this->parent = $item->getParent(); + $this->maxLevelcat++; + ?> +
    + +
    + + diff --git a/components/com_contact/tmpl/category/default.php b/components/com_contact/tmpl/category/default.php index 84581b57c8ece..b25d7830b1637 100644 --- a/components/com_contact/tmpl/category/default.php +++ b/components/com_contact/tmpl/category/default.php @@ -1,4 +1,5 @@
    - subtemplatename = 'items'; - echo LayoutHelper::render('joomla.content.category_default', $this); - ?> + subtemplatename = 'items'; + echo LayoutHelper::render('joomla.content.category_default', $this); + ?>
    diff --git a/components/com_contact/tmpl/category/default_children.php b/components/com_contact/tmpl/category/default_children.php index ed2ed64012e80..0febb97df8dc8 100644 --- a/components/com_contact/tmpl/category/default_children.php +++ b/components/com_contact/tmpl/category/default_children.php @@ -1,4 +1,5 @@ maxLevel != 0 && count($this->children[$this->category->id]) > 0) : -?> + ?>
      -children[$this->category->id] as $id => $child) : ?> - params->get('show_empty_categories') || $child->numitems || count($child->getChildren())) : ?> -
    • -

      - - escape($child->title); ?> - + children[$this->category->id] as $id => $child) : ?> + params->get('show_empty_categories') || $child->numitems || count($child->getChildren())) : ?> +
    • +

      + + escape($child->title); ?> + - params->get('show_cat_items') == 1) : ?> - numitems; ?> - -

      + params->get('show_cat_items') == 1) : ?> + numitems; ?> + +
    • - params->get('show_subcat_desc') == 1) : ?> - description) : ?> -
      - description, '', 'com_contact.category'); ?> -
      - - + params->get('show_subcat_desc') == 1) : ?> + description) : ?> +
      + description, '', 'com_contact.category'); ?> +
      + + - getChildren()) > 0 ) : - $this->children[$child->id] = $child->getChildren(); - $this->category = $child; - $this->maxLevel--; - echo $this->loadTemplate('children'); - $this->category = $child->getParent(); - $this->maxLevel++; - endif; ?> -
    • - - + getChildren()) > 0) : + $this->children[$child->id] = $child->getChildren(); + $this->category = $child; + $this->maxLevel--; + echo $this->loadTemplate('children'); + $this->category = $child->getParent(); + $this->maxLevel++; + endif; ?> + + +
    diff --git a/components/com_contact/tmpl/category/default_items.php b/components/com_contact/tmpl/category/default_items.php index 05229d773dfd7..e073ca252e0dc 100644 --- a/components/com_contact/tmpl/category/default_items.php +++ b/components/com_contact/tmpl/category/default_items.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('com_contact.contacts-list') - ->useScript('core'); + ->useScript('core'); $canDo = ContactHelper::getActions('com_contact', 'category', $this->category->id); $canEdit = $canDo->get('core.edit'); @@ -31,185 +32,185 @@ $listDirn = $this->escape($this->state->get('list.direction')); ?>
    -
    - params->get('filter_field')) : ?> -
    - - - - -
    - - - params->get('show_pagination_limit')) : ?> -
    - - pagination->getLimitBox(); ?> -
    - - - items)) : ?> - params->get('show_no_contacts', 1)) : ?> -
    - - -
    - - - - - - params->get('show_headings')) : ?> - - - - - get('core.edit.own') && $item->created_by === $userId)) : ?> - - - - - - - items as $i => $item) : ?> - items[$i]->published == 0) : ?> - - - - - - - get('core.edit.own') && $item->created_by === $userId)) : ?> - - - - - - - - get('core.create')) : ?> - category, $this->category->params); ?> - - - params->get('show_pagination', 2)) : ?> -
    - params->def('show_pagination_results', 1)) : ?> -

    - pagination->getPagesCounter(); ?> -

    - - - pagination->getPagesLinks(); ?> -
    - -
    - - -
    -
    +
    + params->get('filter_field')) : ?> +
    + + + + +
    + + + params->get('show_pagination_limit')) : ?> +
    + + pagination->getLimitBox(); ?> +
    + + + items)) : ?> + params->get('show_no_contacts', 1)) : ?> +
    + + +
    + + + + + + params->get('show_headings')) : ?> + + + + + get('core.edit.own') && $item->created_by === $userId)) : ?> + + + + + + + items as $i => $item) : ?> + items[$i]->published == 0) : ?> + + + + + + + get('core.edit.own') && $item->created_by === $userId)) : ?> + + + + + + + + get('core.create')) : ?> + category, $this->category->params); ?> + + + params->get('show_pagination', 2)) : ?> +
    + params->def('show_pagination_results', 1)) : ?> +

    + pagination->getPagesCounter(); ?> +

    + + + pagination->getPagesLinks(); ?> +
    + +
    + + +
    +
    diff --git a/components/com_contact/tmpl/contact/default.php b/components/com_contact/tmpl/contact/default.php index 24d326583bdbe..2d216b1e48f8f 100644 --- a/components/com_contact/tmpl/contact/default.php +++ b/components/com_contact/tmpl/contact/default.php @@ -1,4 +1,5 @@
    - get('show_page_heading')) : ?> -

    - escape($tparams->get('page_heading')); ?> -

    - - - item->name && $tparams->get('show_name')) : ?> - - - - -
    -
    -
    - item, $tparams); ?> -
    -
    -
    - - - get('show_contact_category'); ?> - - -

    - item->category_title; ?> -

    - - item->catid, $this->item->language); ?> -

    - - escape($this->item->category_title); ?> - -

    - - - item->event->afterDisplayTitle; ?> - - get('show_contact_list') && count($this->contacts) > 1) : ?> -
    - - contacts, - 'select_contact', - 'class="form-select" onchange="document.location.href = this.value"', 'link', 'name', $this->item->link); - ?> -
    - - - get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> -
    - item->tagLayout = new FileLayout('joomla.content.tags'); ?> - item->tagLayout->render($this->item->tags->itemTags); ?> -
    - - - item->event->beforeDisplayContent; ?> - - params->get('show_info', 1)) : ?> - -
    - ' . Text::_('COM_CONTACT_DETAILS') . '

    '; ?> - - item->image && $tparams->get('show_image')) : ?> -
    - $this->item->image, - 'alt' => $this->item->name, - 'itemprop' => 'image', - ] - ); ?> -
    - - - item->con_position && $tparams->get('show_position')) : ?> -
    -
    :
    -
    - item->con_position; ?> -
    -
    - - -
    - loadTemplate('address'); ?> - - get('allow_vcard')) : ?> - - - - -
    - - - - - get('show_email_form') && ($this->item->email_to || $this->item->user_id)) : ?> - ' . Text::_('COM_CONTACT_EMAIL_FORM') . '

    '; ?> - - loadTemplate('form'); ?> - - - get('show_links')) : ?> - loadTemplate('links'); ?> - - - get('show_articles') && $this->item->user_id && $this->item->articles) : ?> - ' . Text::_('JGLOBAL_ARTICLES') . ''; ?> - - loadTemplate('articles'); ?> - - - get('show_profile') && $this->item->user_id && PluginHelper::isEnabled('user', 'profile')) : ?> - ' . Text::_('COM_CONTACT_PROFILE') . ''; ?> - - loadTemplate('profile'); ?> - - - get('show_user_custom_fields') && $this->contactUser) : ?> - loadTemplate('user_custom_fields'); ?> - - - item->misc && $tparams->get('show_misc')) : ?> - ' . Text::_('COM_CONTACT_OTHER_INFORMATION') . ''; ?> - -
    -
    -
    - params->get('marker_misc')) : ?> - - - - - params->get('marker_misc'); ?> - - -
    -
    - - item->misc; ?> - -
    -
    -
    - - item->event->afterDisplayContent; ?> + get('show_page_heading')) : ?> +

    + escape($tparams->get('page_heading')); ?> +

    + + + item->name && $tparams->get('show_name')) : ?> + + + + +
    +
    +
    + item, $tparams); ?> +
    +
    +
    + + + get('show_contact_category'); ?> + + +

    + item->category_title; ?> +

    + + item->catid, $this->item->language); ?> +

    + + escape($this->item->category_title); ?> + +

    + + + item->event->afterDisplayTitle; ?> + + get('show_contact_list') && count($this->contacts) > 1) : ?> +
    + + contacts, + 'select_contact', + 'class="form-select" onchange="document.location.href = this.value"', + 'link', + 'name', + $this->item->link + ); + ?> +
    + + + get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> +
    + item->tagLayout = new FileLayout('joomla.content.tags'); ?> + item->tagLayout->render($this->item->tags->itemTags); ?> +
    + + + item->event->beforeDisplayContent; ?> + + params->get('show_info', 1)) : ?> +
    + ' . Text::_('COM_CONTACT_DETAILS') . ''; ?> + + item->image && $tparams->get('show_image')) : ?> +
    + $this->item->image, + 'alt' => $this->item->name, + 'itemprop' => 'image', + ] + ); ?> +
    + + + item->con_position && $tparams->get('show_position')) : ?> +
    +
    :
    +
    + item->con_position; ?> +
    +
    + + +
    + loadTemplate('address'); ?> + + get('allow_vcard')) : ?> + + + + +
    +
    + + + + get('show_email_form') && ($this->item->email_to || $this->item->user_id)) : ?> + ' . Text::_('COM_CONTACT_EMAIL_FORM') . ''; ?> + + loadTemplate('form'); ?> + + + get('show_links')) : ?> + loadTemplate('links'); ?> + + + get('show_articles') && $this->item->user_id && $this->item->articles) : ?> + ' . Text::_('JGLOBAL_ARTICLES') . ''; ?> + + loadTemplate('articles'); ?> + + + get('show_profile') && $this->item->user_id && PluginHelper::isEnabled('user', 'profile')) : ?> + ' . Text::_('COM_CONTACT_PROFILE') . ''; ?> + + loadTemplate('profile'); ?> + + + get('show_user_custom_fields') && $this->contactUser) : ?> + loadTemplate('user_custom_fields'); ?> + + + item->misc && $tparams->get('show_misc')) : ?> + ' . Text::_('COM_CONTACT_OTHER_INFORMATION') . ''; ?> + +
    +
    +
    + params->get('marker_misc')) : ?> + + + + + params->get('marker_misc'); ?> + + +
    +
    + + item->misc; ?> + +
    +
    +
    + + item->event->afterDisplayContent; ?> diff --git a/components/com_contact/tmpl/contact/default_address.php b/components/com_contact/tmpl/contact/default_address.php index fbec2f6e2a880..c6dfdf110a9b9 100644 --- a/components/com_contact/tmpl/contact/default_address.php +++ b/components/com_contact/tmpl/contact/default_address.php @@ -1,4 +1,5 @@
    - params->get('address_check') > 0) && - ($this->item->address || $this->item->suburb || $this->item->state || $this->item->country || $this->item->postcode)) : ?> -
    - params->get('marker_address')) : ?> - - - - params->get('marker_address'); ?> - - -
    + params->get('address_check') > 0) && + ($this->item->address || $this->item->suburb || $this->item->state || $this->item->country || $this->item->postcode) +) : ?> +
    + params->get('marker_address')) : ?> + + + + params->get('marker_address'); ?> + + +
    - item->address && $this->params->get('show_street_address')) : ?> -
    - - item->address, false); ?> - -
    - + item->address && $this->params->get('show_street_address')) : ?> +
    + + item->address, false); ?> + +
    + - item->suburb && $this->params->get('show_suburb')) : ?> -
    - - item->suburb; ?> - -
    - - item->state && $this->params->get('show_state')) : ?> -
    - - item->state; ?> - -
    - - item->postcode && $this->params->get('show_postcode')) : ?> -
    - - item->postcode; ?> - -
    - - item->country && $this->params->get('show_country')) : ?> -
    - - item->country; ?> - -
    - - + item->suburb && $this->params->get('show_suburb')) : ?> +
    + + item->suburb; ?> + +
    + + item->state && $this->params->get('show_state')) : ?> +
    + + item->state; ?> + +
    + + item->postcode && $this->params->get('show_postcode')) : ?> +
    + + item->postcode; ?> + +
    + + item->country && $this->params->get('show_country')) : ?> +
    + + item->country; ?> + +
    + + item->email_to && $this->params->get('show_email')) : ?> -
    - params->get('marker_email')) : ?> - - - - params->get('marker_email'); ?> - - -
    -
    - - item->email_to; ?> - -
    +
    + params->get('marker_email')) : ?> + + + + params->get('marker_email'); ?> + + +
    +
    + + item->email_to; ?> + +
    item->telephone && $this->params->get('show_telephone')) : ?> -
    - params->get('marker_telephone')) : ?> - - - - params->get('marker_telephone'); ?> - - -
    -
    - - item->telephone; ?> - -
    +
    + params->get('marker_telephone')) : ?> + + + + params->get('marker_telephone'); ?> + + +
    +
    + + item->telephone; ?> + +
    item->fax && $this->params->get('show_fax')) : ?> -
    - params->get('marker_fax')) : ?> - - - - params->get('marker_fax'); ?> - - -
    -
    - - item->fax; ?> - -
    +
    + params->get('marker_fax')) : ?> + + + + params->get('marker_fax'); ?> + + +
    +
    + + item->fax; ?> + +
    item->mobile && $this->params->get('show_mobile')) : ?> -
    - params->get('marker_mobile')) : ?> - - - - params->get('marker_mobile'); ?> - - -
    -
    - - item->mobile; ?> - -
    +
    + params->get('marker_mobile')) : ?> + + + + params->get('marker_mobile'); ?> + + +
    +
    + + item->mobile; ?> + +
    item->webpage && $this->params->get('show_webpage')) : ?> -
    - params->get('marker_webpage')) : ?> - - - - params->get('marker_webpage'); ?> - - -
    -
    - - - -
    +
    + params->get('marker_webpage')) : ?> + + + + params->get('marker_webpage'); ?> + + +
    +
    + + + +
    diff --git a/components/com_contact/tmpl/contact/default_articles.php b/components/com_contact/tmpl/contact/default_articles.php index c16a95e5f8588..c17d00c54ddc0 100644 --- a/components/com_contact/tmpl/contact/default_articles.php +++ b/components/com_contact/tmpl/contact/default_articles.php @@ -1,4 +1,5 @@ params->get('show_articles')) : ?>
    -
      - item->articles as $article) : ?> -
    • - slug, $article->catid, $article->language)), htmlspecialchars($article->title, ENT_COMPAT, 'UTF-8')); ?> -
    • - -
    +
      + item->articles as $article) : ?> +
    • + slug, $article->catid, $article->language)), htmlspecialchars($article->title, ENT_COMPAT, 'UTF-8')); ?> +
    • + +
    diff --git a/components/com_contact/tmpl/contact/default_form.php b/components/com_contact/tmpl/contact/default_form.php index 8bfe273b904b2..303c13f609781 100644 --- a/components/com_contact/tmpl/contact/default_form.php +++ b/components/com_contact/tmpl/contact/default_form.php @@ -1,4 +1,5 @@
    -
    - form->getFieldsets() as $fieldset) : ?> - name === 'captcha' && !$this->captchaEnabled) : ?> - - - form->getFieldset($fieldset->name); ?> - -
    - label) && ($legend = trim(Text::_($fieldset->label))) !== '') : ?> - - - - renderField(); ?> - -
    - - -
    -
    - - - - - - -
    -
    -
    +
    + form->getFieldsets() as $fieldset) : ?> + name === 'captcha' && !$this->captchaEnabled) : ?> + + + form->getFieldset($fieldset->name); ?> + +
    + label) && ($legend = trim(Text::_($fieldset->label))) !== '') : ?> + + + + renderField(); ?> + +
    + + +
    +
    + + + + + + +
    +
    +
    diff --git a/components/com_contact/tmpl/contact/default_links.php b/components/com_contact/tmpl/contact/default_links.php index 64b95ad8d49cf..0b95aae7448cb 100644 --- a/components/com_contact/tmpl/contact/default_links.php +++ b/components/com_contact/tmpl/contact/default_links.php @@ -1,4 +1,5 @@ ' . Text::_('COM_CONTACT_LINKS') . ''; ?> diff --git a/components/com_contact/tmpl/contact/default_profile.php b/components/com_contact/tmpl/contact/default_profile.php index f64d411a3f4dd..b4e0134a04f38 100644 --- a/components/com_contact/tmpl/contact/default_profile.php +++ b/components/com_contact/tmpl/contact/default_profile.php @@ -1,4 +1,5 @@ item->profile->getFieldset('profile'); ?> -
    -
    - value) : - echo '
    ' . $profile->label . '
    '; - $profile->text = htmlspecialchars($profile->value, ENT_COMPAT, 'UTF-8'); - - switch ($profile->id) : - case 'profile_website': - $v_http = substr($profile->value, 0, 4); - - if ($v_http === 'http') : - echo '
    ' . PunycodeHelper::urlToUTF8($profile->text) . '
    '; - else : - echo '
    ' . PunycodeHelper::urlToUTF8($profile->text) . '
    '; - endif; - break; - - case 'profile_dob': - echo '
    ' . HTMLHelper::_('date', $profile->text, Text::_('DATE_FORMAT_LC4'), false) . '
    '; - break; - - default: - echo '
    ' . $profile->text . '
    '; - break; - endswitch; - endif; - endforeach; ?> -
    -
    + $fields = $this->item->profile->getFieldset('profile'); ?> +
    +
    + value) : + echo '
    ' . $profile->label . '
    '; + $profile->text = htmlspecialchars($profile->value, ENT_COMPAT, 'UTF-8'); + + switch ($profile->id) : + case 'profile_website': + $v_http = substr($profile->value, 0, 4); + + if ($v_http === 'http') : + echo '
    ' . PunycodeHelper::urlToUTF8($profile->text) . '
    '; + else : + echo '
    ' . PunycodeHelper::urlToUTF8($profile->text) . '
    '; + endif; + break; + + case 'profile_dob': + echo '
    ' . HTMLHelper::_('date', $profile->text, Text::_('DATE_FORMAT_LC4'), false) . '
    '; + break; + + default: + echo '
    ' . $profile->text . '
    '; + break; + endswitch; + endif; + endforeach; ?> +
    +
    diff --git a/components/com_contact/tmpl/contact/default_user_custom_fields.php b/components/com_contact/tmpl/contact/default_user_custom_fields.php index 839a70c757db7..a00b9412b1450 100644 --- a/components/com_contact/tmpl/contact/default_user_custom_fields.php +++ b/components/com_contact/tmpl/contact/default_user_custom_fields.php @@ -1,4 +1,5 @@ contactUser) : ?> - + contactUser->jcfields as $field) : ?> - value && (in_array('-1', $displayGroups) || in_array($field->group_id, $displayGroups))) : ?> - group_title][] = $field; ?> - + value && (in_array('-1', $displayGroups) || in_array($field->group_id, $displayGroups))) : ?> + group_title][] = $field; ?> + $fields) : ?> - - ' . ($groupTitle ?: Text::_('COM_CONTACT_USER_FIELDS')) . ''; ?> - -
    -
    - - value) : ?> - - - - params->get('showlabel')) : ?> - ' . Text::_($field->label) . ''; ?> - - - ' . $field->value . ''; ?> - -
    -
    + + ' . ($groupTitle ?: Text::_('COM_CONTACT_USER_FIELDS')) . ''; ?> + +
    +
    + + value) : ?> + + + + params->get('showlabel')) : ?> + ' . Text::_($field->label) . ''; ?> + + + ' . $field->value . ''; ?> + +
    +
    diff --git a/components/com_contact/tmpl/featured/default.php b/components/com_contact/tmpl/featured/default.php index f0093ac62c42a..d87e8d6716e95 100644 --- a/components/com_contact/tmpl/featured/default.php +++ b/components/com_contact/tmpl/featured/default.php @@ -1,4 +1,5 @@ diff --git a/components/com_contact/tmpl/featured/default_items.php b/components/com_contact/tmpl/featured/default_items.php index 75b31fe853750..1fd0130ad96ce 100644 --- a/components/com_contact/tmpl/featured/default_items.php +++ b/components/com_contact/tmpl/featured/default_items.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('com_contact.contacts-list') - ->useScript('core'); + ->useScript('core'); $listOrder = $this->escape($this->state->get('list.ordering')); $listDirn = $this->escape($this->state->get('list.direction')); ?> diff --git a/components/com_contact/tmpl/form/edit.php b/components/com_contact/tmpl/form/edit.php index cdb20b89ae85d..d5f33d6f52a56 100644 --- a/components/com_contact/tmpl/form/edit.php +++ b/components/com_contact/tmpl/form/edit.php @@ -1,4 +1,5 @@ useCoreUI = true; ?>
    - params->get('show_page_heading')) : ?> - - + params->get('show_page_heading')) : ?> + + -
    -
    - tab_name, ['active' => 'details', 'recall' => true, 'breakpoint' => 768]); ?> - tab_name, 'details', empty($this->item->id) ? Text::_('COM_CONTACT_NEW_CONTACT') : Text::_('COM_CONTACT_EDIT_CONTACT')); ?> - form->renderField('name'); ?> + +
    + tab_name, ['active' => 'details', 'recall' => true, 'breakpoint' => 768]); ?> + tab_name, 'details', empty($this->item->id) ? Text::_('COM_CONTACT_NEW_CONTACT') : Text::_('COM_CONTACT_EDIT_CONTACT')); ?> + form->renderField('name'); ?> - item->id)) : ?> - form->renderField('alias'); ?> - + item->id)) : ?> + form->renderField('alias'); ?> + - form->renderFieldset('details'); ?> - + form->renderFieldset('details'); ?> + - tab_name, 'misc', Text::_('COM_CONTACT_FIELDSET_MISCELLANEOUS')); ?> - form->getInput('misc'); ?> - + tab_name, 'misc', Text::_('COM_CONTACT_FIELDSET_MISCELLANEOUS')); ?> + form->getInput('misc'); ?> + - - tab_name, 'language', Text::_('JFIELD_LANGUAGE_LABEL')); ?> - form->renderField('language'); ?> - - - form->renderField('language'); ?> - + + tab_name, 'language', Text::_('JFIELD_LANGUAGE_LABEL')); ?> + form->renderField('language'); ?> + + + form->renderField('language'); ?> + - - + + - - - -
    -
    - - - params->get('save_history', 0) && $this->item->id) : ?> - form->getInput('contenthistory'); ?> - -
    - + + + +
    +
    + + + params->get('save_history', 0) && $this->item->id) : ?> + form->getInput('contenthistory'); ?> + +
    +
    diff --git a/components/com_content/helpers/icon.php b/components/com_content/helpers/icon.php index 3aa3126a48828..dc80a4854a8b3 100644 --- a/components/com_content/helpers/icon.php +++ b/components/com_content/helpers/icon.php @@ -1,13 +1,14 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt - */ -defined('_JEXEC') or die; + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + */ use Joomla\CMS\Language\Text; use Joomla\Registry\Registry; @@ -20,86 +21,86 @@ */ abstract class JHtmlIcon { - /** - * Method to generate a link to the create item page for the given category - * - * @param object $category The category information - * @param Registry $params The item parameters - * @param array $attribs Optional attributes for the link - * @param boolean $legacy True to use legacy images, false to use icomoon based graphic - * - * @return string The HTML markup for the create item link - * - * @deprecated 5.0 Use the class \Joomla\Component\Content\Administrator\Service\HTML\Icon instead - */ - public static function create($category, $params, $attribs = array(), $legacy = false) - { - return self::getIcon()->create($category, $params, $attribs, $legacy); - } + /** + * Method to generate a link to the create item page for the given category + * + * @param object $category The category information + * @param Registry $params The item parameters + * @param array $attribs Optional attributes for the link + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML markup for the create item link + * + * @deprecated 5.0 Use the class \Joomla\Component\Content\Administrator\Service\HTML\Icon instead + */ + public static function create($category, $params, $attribs = array(), $legacy = false) + { + return self::getIcon()->create($category, $params, $attribs, $legacy); + } - /** - * Display an edit icon for the article. - * - * This icon will not display in a popup window, nor if the article is trashed. - * Edit access checks must be performed in the calling code. - * - * @param object $article The article information - * @param Registry $params The item parameters - * @param array $attribs Optional attributes for the link - * @param boolean $legacy True to use legacy images, false to use icomoon based graphic - * - * @return string The HTML for the article edit icon. - * - * @since 1.6 - * - * @deprecated 5.0 Use the class \Joomla\Component\Content\Administrator\Service\HTML\Icon instead - */ - public static function edit($article, $params, $attribs = array(), $legacy = false) - { - return self::getIcon()->edit($article, $params, $attribs, $legacy); - } + /** + * Display an edit icon for the article. + * + * This icon will not display in a popup window, nor if the article is trashed. + * Edit access checks must be performed in the calling code. + * + * @param object $article The article information + * @param Registry $params The item parameters + * @param array $attribs Optional attributes for the link + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML for the article edit icon. + * + * @since 1.6 + * + * @deprecated 5.0 Use the class \Joomla\Component\Content\Administrator\Service\HTML\Icon instead + */ + public static function edit($article, $params, $attribs = array(), $legacy = false) + { + return self::getIcon()->edit($article, $params, $attribs, $legacy); + } - /** - * Method to generate a popup link to print an article - * - * @param object $article The article information - * @param Registry $params The item parameters - * @param array $attribs Optional attributes for the link - * @param boolean $legacy True to use legacy images, false to use icomoon based graphic - * - * @return string The HTML markup for the popup link - * - * @deprecated 5.0 Use the class \Joomla\Component\Content\Administrator\Service\HTML\Icon instead - */ - public static function print_popup($article, $params, $attribs = array(), $legacy = false) - { - throw new \Exception(Text::_('COM_CONTENT_ERROR_PRINT_POPUP')); - } + /** + * Method to generate a popup link to print an article + * + * @param object $article The article information + * @param Registry $params The item parameters + * @param array $attribs Optional attributes for the link + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML markup for the popup link + * + * @deprecated 5.0 Use the class \Joomla\Component\Content\Administrator\Service\HTML\Icon instead + */ + public static function print_popup($article, $params, $attribs = array(), $legacy = false) + { + throw new \Exception(Text::_('COM_CONTENT_ERROR_PRINT_POPUP')); + } - /** - * Method to generate a link to print an article - * - * @param object $article Not used, @deprecated for 4.0 - * @param Registry $params The item parameters - * @param array $attribs Not used, @deprecated for 4.0 - * @param boolean $legacy True to use legacy images, false to use icomoon based graphic - * - * @return string The HTML markup for the popup link - * - * @deprecated 5.0 Use the class \Joomla\Component\Content\Administrator\Service\HTML\Icon instead - */ - public static function print_screen($article, $params, $attribs = array(), $legacy = false) - { - return self::getIcon()->print_screen($params, $legacy); - } + /** + * Method to generate a link to print an article + * + * @param object $article Not used, @deprecated for 4.0 + * @param Registry $params The item parameters + * @param array $attribs Not used, @deprecated for 4.0 + * @param boolean $legacy True to use legacy images, false to use icomoon based graphic + * + * @return string The HTML markup for the popup link + * + * @deprecated 5.0 Use the class \Joomla\Component\Content\Administrator\Service\HTML\Icon instead + */ + public static function print_screen($article, $params, $attribs = array(), $legacy = false) + { + return self::getIcon()->print_screen($params, $legacy); + } - /** - * Creates an icon instance. - * - * @return \Joomla\Component\Content\Administrator\Service\HTML\Icon - */ - private static function getIcon() - { - return (new \Joomla\Component\Content\Administrator\Service\HTML\Icon(Joomla\CMS\Factory::getApplication())); - } + /** + * Creates an icon instance. + * + * @return \Joomla\Component\Content\Administrator\Service\HTML\Icon + */ + private static function getIcon() + { + return (new \Joomla\Component\Content\Administrator\Service\HTML\Icon(Joomla\CMS\Factory::getApplication())); + } } diff --git a/components/com_content/src/Controller/ArticleController.php b/components/com_content/src/Controller/ArticleController.php index b6005eb5b9a98..017fb5995cbf2 100644 --- a/components/com_content/src/Controller/ArticleController.php +++ b/components/com_content/src/Controller/ArticleController.php @@ -1,4 +1,5 @@ setRedirect($this->getReturnPage()); - - return; - } - - // Redirect to the edit screen. - $this->setRedirect( - Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_item . '&a_id=0' - . $this->getRedirectToItemAppend(), false - ) - ); - - return true; - } - - /** - * Method override to check if you can add a new record. - * - * @param array $data An array of input data. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowAdd($data = array()) - { - $user = $this->app->getIdentity(); - $categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('catid'), 'int'); - $allow = null; - - if ($categoryId) - { - // If the category has been passed in the data or URL check it. - $allow = $user->authorise('core.create', 'com_content.category.' . $categoryId); - } - - if ($allow === null) - { - // In the absence of better information, revert to the component permissions. - return parent::allowAdd(); - } - else - { - return $allow; - } - } - - /** - * Method override to check if you can edit an existing record. - * - * @param array $data An array of input data. - * @param string $key The name of the key for the primary key; default is id. - * - * @return boolean - * - * @since 1.6 - */ - protected function allowEdit($data = array(), $key = 'id') - { - $recordId = (int) isset($data[$key]) ? $data[$key] : 0; - $user = $this->app->getIdentity(); - - // Zero record (id:0), return component edit permission by calling parent controller method - if (!$recordId) - { - return parent::allowEdit($data, $key); - } - - // Check edit on the record asset (explicit or inherited) - if ($user->authorise('core.edit', 'com_content.article.' . $recordId)) - { - return true; - } - - // Check edit own on the record asset (explicit or inherited) - if ($user->authorise('core.edit.own', 'com_content.article.' . $recordId)) - { - // Existing record already has an owner, get it - $record = $this->getModel()->getItem($recordId); - - if (empty($record)) - { - return false; - } - - // Grant if current user is owner of the record - return $user->get('id') == $record->created_by; - } - - return false; - } - - /** - * Method to cancel an edit. - * - * @param string $key The name of the primary key of the URL variable. - * - * @return boolean True if access level checks pass, false otherwise. - * - * @since 1.6 - */ - public function cancel($key = 'a_id') - { - $result = parent::cancel($key); - - /** @var SiteApplication $app */ - $app = Factory::getApplication(); - - // Load the parameters. - $params = $app->getParams(); - - $customCancelRedir = (bool) $params->get('custom_cancel_redirect'); - - if ($customCancelRedir) - { - $cancelMenuitemId = (int) $params->get('cancel_redirect_menuitem'); - - if ($cancelMenuitemId > 0) - { - $item = $app->getMenu()->getItem($cancelMenuitemId); - $lang = ''; - - if (Multilanguage::isEnabled()) - { - $lang = !is_null($item) && $item->language != '*' ? '&lang=' . $item->language : ''; - } - - // Redirect to the user specified return page. - $redirlink = $item->link . $lang . '&Itemid=' . $cancelMenuitemId; - } - else - { - // Redirect to the same article submission form (clean form). - $redirlink = $app->getMenu()->getActive()->link . '&Itemid=' . $app->getMenu()->getActive()->id; - } - } - else - { - $menuitemId = (int) $params->get('redirect_menuitem'); - - if ($menuitemId > 0) - { - $lang = ''; - $item = $app->getMenu()->getItem($menuitemId); - - if (Multilanguage::isEnabled()) - { - $lang = !is_null($item) && $item->language != '*' ? '&lang=' . $item->language : ''; - } - - // Redirect to the general (redirect_menuitem) user specified return page. - $redirlink = $item->link . $lang . '&Itemid=' . $menuitemId; - } - else - { - // Redirect to the return page. - $redirlink = $this->getReturnPage(); - } - } - - $this->setRedirect(Route::_($redirlink, false)); - - return $result; - } - - /** - * Method to edit an existing record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key - * (sometimes required to avoid router collisions). - * - * @return boolean True if access level check and checkout passes, false otherwise. - * - * @since 1.6 - */ - public function edit($key = null, $urlVar = 'a_id') - { - $result = parent::edit($key, $urlVar); - - if (!$result) - { - $this->setRedirect(Route::_($this->getReturnPage(), false)); - } - - return $result; - } - - /** - * 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 object The model. - * - * @since 1.5 - */ - public function getModel($name = 'Form', $prefix = 'Site', $config = array('ignore_request' => true)) - { - return parent::getModel($name, $prefix, $config); - } - - /** - * Gets the URL arguments to append to an item redirect. - * - * @param integer $recordId The primary key id for the item. - * @param string $urlVar The name of the URL variable for the id. - * - * @return string The arguments to append to the redirect URL. - * - * @since 1.6 - */ - protected function getRedirectToItemAppend($recordId = null, $urlVar = 'a_id') - { - // Need to override the parent method completely. - $tmpl = $this->input->get('tmpl'); - - $append = ''; - - // Setup redirect info. - if ($tmpl) - { - $append .= '&tmpl=' . $tmpl; - } - - // @todo This is a bandaid, not a long term solution. - /** - * if ($layout) - * { - * $append .= '&layout=' . $layout; - * } - */ - - $append .= '&layout=edit'; - - if ($recordId) - { - $append .= '&' . $urlVar . '=' . $recordId; - } - - $itemId = $this->input->getInt('Itemid'); - $return = $this->getReturnPage(); - $catId = $this->input->getInt('catid'); - - if ($itemId) - { - $append .= '&Itemid=' . $itemId; - } - - if ($catId) - { - $append .= '&catid=' . $catId; - } - - if ($return) - { - $append .= '&return=' . base64_encode($return); - } - - return $append; - } - - /** - * Get the return URL. - * - * If a "return" variable has been passed in the request - * - * @return string The return URL. - * - * @since 1.6 - */ - protected function getReturnPage() - { - $return = $this->input->get('return', null, 'base64'); - - if (empty($return) || !Uri::isInternal(base64_decode($return))) - { - return Uri::base(); - } - else - { - return base64_decode($return); - } - } - - /** - * Method to save a record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return boolean True if successful, false otherwise. - * - * @since 1.6 - */ - public function save($key = null, $urlVar = 'a_id') - { - $result = parent::save($key, $urlVar); - - if (\in_array($this->getTask(), ['save2copy', 'apply'], true)) - { - return $result; - } - - $app = Factory::getApplication(); - $articleId = $app->input->getInt('a_id'); - - // Load the parameters. - $params = $app->getParams(); - $menuitem = (int) $params->get('redirect_menuitem'); - - // Check for redirection after submission when creating a new article only - if ($menuitem > 0 && $articleId == 0) - { - $lang = ''; - - if (Multilanguage::isEnabled()) - { - $item = $app->getMenu()->getItem($menuitem); - $lang = !is_null($item) && $item->language != '*' ? '&lang=' . $item->language : ''; - } - - // If ok, redirect to the return page. - if ($result) - { - $this->setRedirect(Route::_('index.php?Itemid=' . $menuitem . $lang, false)); - } - } - elseif ($this->getTask() === 'save2copy') - { - // Redirect to the article page, use the redirect url set from parent controller - } - else - { - // If ok, redirect to the return page. - if ($result) - { - $this->setRedirect(Route::_($this->getReturnPage(), false)); - } - } - - return $result; - } - - /** - * Method to reload a record. - * - * @param string $key The name of the primary key of the URL variable. - * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). - * - * @return void - * - * @since 3.8.0 - */ - public function reload($key = null, $urlVar = 'a_id') - { - parent::reload($key, $urlVar); - } - - /** - * Method to save a vote. - * - * @return void - * - * @since 1.6 - */ - public function vote() - { - // Check for request forgeries. - $this->checkToken(); - - $user_rating = $this->input->getInt('user_rating', -1); - - if ($user_rating > -1) - { - $url = $this->input->getString('url', ''); - $id = $this->input->getInt('id', 0); - $viewName = $this->input->getString('view', $this->default_view); - $model = $this->getModel($viewName); - - // Don't redirect to an external URL. - if (!Uri::isInternal($url)) - { - $url = Route::_('index.php'); - } - - if ($model->storeVote($id, $user_rating)) - { - $this->setRedirect($url, Text::_('COM_CONTENT_ARTICLE_VOTE_SUCCESS')); - } - else - { - $this->setRedirect($url, Text::_('COM_CONTENT_ARTICLE_VOTE_FAILURE')); - } - } - } + use VersionableControllerTrait; + + /** + * The URL view item variable. + * + * @var string + * @since 1.6 + */ + protected $view_item = 'form'; + + /** + * The URL view list variable. + * + * @var string + * @since 1.6 + */ + protected $view_list = 'categories'; + + /** + * The URL edit variable. + * + * @var string + * @since 3.2 + */ + protected $urlVar = 'a.id'; + + /** + * Method to add a new record. + * + * @return mixed True if the record can be added, an error object if not. + * + * @since 1.6 + */ + public function add() + { + if (!parent::add()) { + // Redirect to the return page. + $this->setRedirect($this->getReturnPage()); + + return; + } + + // Redirect to the edit screen. + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option . '&view=' . $this->view_item . '&a_id=0' + . $this->getRedirectToItemAppend(), + false + ) + ); + + return true; + } + + /** + * Method override to check if you can add a new record. + * + * @param array $data An array of input data. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowAdd($data = array()) + { + $user = $this->app->getIdentity(); + $categoryId = ArrayHelper::getValue($data, 'catid', $this->input->getInt('catid'), 'int'); + $allow = null; + + if ($categoryId) { + // If the category has been passed in the data or URL check it. + $allow = $user->authorise('core.create', 'com_content.category.' . $categoryId); + } + + if ($allow === null) { + // In the absence of better information, revert to the component permissions. + return parent::allowAdd(); + } else { + return $allow; + } + } + + /** + * Method override to check if you can edit an existing record. + * + * @param array $data An array of input data. + * @param string $key The name of the key for the primary key; default is id. + * + * @return boolean + * + * @since 1.6 + */ + protected function allowEdit($data = array(), $key = 'id') + { + $recordId = (int) isset($data[$key]) ? $data[$key] : 0; + $user = $this->app->getIdentity(); + + // Zero record (id:0), return component edit permission by calling parent controller method + if (!$recordId) { + return parent::allowEdit($data, $key); + } + + // Check edit on the record asset (explicit or inherited) + if ($user->authorise('core.edit', 'com_content.article.' . $recordId)) { + return true; + } + + // Check edit own on the record asset (explicit or inherited) + if ($user->authorise('core.edit.own', 'com_content.article.' . $recordId)) { + // Existing record already has an owner, get it + $record = $this->getModel()->getItem($recordId); + + if (empty($record)) { + return false; + } + + // Grant if current user is owner of the record + return $user->get('id') == $record->created_by; + } + + return false; + } + + /** + * Method to cancel an edit. + * + * @param string $key The name of the primary key of the URL variable. + * + * @return boolean True if access level checks pass, false otherwise. + * + * @since 1.6 + */ + public function cancel($key = 'a_id') + { + $result = parent::cancel($key); + + /** @var SiteApplication $app */ + $app = Factory::getApplication(); + + // Load the parameters. + $params = $app->getParams(); + + $customCancelRedir = (bool) $params->get('custom_cancel_redirect'); + + if ($customCancelRedir) { + $cancelMenuitemId = (int) $params->get('cancel_redirect_menuitem'); + + if ($cancelMenuitemId > 0) { + $item = $app->getMenu()->getItem($cancelMenuitemId); + $lang = ''; + + if (Multilanguage::isEnabled()) { + $lang = !is_null($item) && $item->language != '*' ? '&lang=' . $item->language : ''; + } + + // Redirect to the user specified return page. + $redirlink = $item->link . $lang . '&Itemid=' . $cancelMenuitemId; + } else { + // Redirect to the same article submission form (clean form). + $redirlink = $app->getMenu()->getActive()->link . '&Itemid=' . $app->getMenu()->getActive()->id; + } + } else { + $menuitemId = (int) $params->get('redirect_menuitem'); + + if ($menuitemId > 0) { + $lang = ''; + $item = $app->getMenu()->getItem($menuitemId); + + if (Multilanguage::isEnabled()) { + $lang = !is_null($item) && $item->language != '*' ? '&lang=' . $item->language : ''; + } + + // Redirect to the general (redirect_menuitem) user specified return page. + $redirlink = $item->link . $lang . '&Itemid=' . $menuitemId; + } else { + // Redirect to the return page. + $redirlink = $this->getReturnPage(); + } + } + + $this->setRedirect(Route::_($redirlink, false)); + + return $result; + } + + /** + * Method to edit an existing record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key + * (sometimes required to avoid router collisions). + * + * @return boolean True if access level check and checkout passes, false otherwise. + * + * @since 1.6 + */ + public function edit($key = null, $urlVar = 'a_id') + { + $result = parent::edit($key, $urlVar); + + if (!$result) { + $this->setRedirect(Route::_($this->getReturnPage(), false)); + } + + return $result; + } + + /** + * 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 object The model. + * + * @since 1.5 + */ + public function getModel($name = 'Form', $prefix = 'Site', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Gets the URL arguments to append to an item redirect. + * + * @param integer $recordId The primary key id for the item. + * @param string $urlVar The name of the URL variable for the id. + * + * @return string The arguments to append to the redirect URL. + * + * @since 1.6 + */ + protected function getRedirectToItemAppend($recordId = null, $urlVar = 'a_id') + { + // Need to override the parent method completely. + $tmpl = $this->input->get('tmpl'); + + $append = ''; + + // Setup redirect info. + if ($tmpl) { + $append .= '&tmpl=' . $tmpl; + } + + // @todo This is a bandaid, not a long term solution. + /** + * if ($layout) + * { + * $append .= '&layout=' . $layout; + * } + */ + + $append .= '&layout=edit'; + + if ($recordId) { + $append .= '&' . $urlVar . '=' . $recordId; + } + + $itemId = $this->input->getInt('Itemid'); + $return = $this->getReturnPage(); + $catId = $this->input->getInt('catid'); + + if ($itemId) { + $append .= '&Itemid=' . $itemId; + } + + if ($catId) { + $append .= '&catid=' . $catId; + } + + if ($return) { + $append .= '&return=' . base64_encode($return); + } + + return $append; + } + + /** + * Get the return URL. + * + * If a "return" variable has been passed in the request + * + * @return string The return URL. + * + * @since 1.6 + */ + protected function getReturnPage() + { + $return = $this->input->get('return', null, 'base64'); + + if (empty($return) || !Uri::isInternal(base64_decode($return))) { + return Uri::base(); + } else { + return base64_decode($return); + } + } + + /** + * Method to save a record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return boolean True if successful, false otherwise. + * + * @since 1.6 + */ + public function save($key = null, $urlVar = 'a_id') + { + $result = parent::save($key, $urlVar); + + if (\in_array($this->getTask(), ['save2copy', 'apply'], true)) { + return $result; + } + + $app = Factory::getApplication(); + $articleId = $app->input->getInt('a_id'); + + // Load the parameters. + $params = $app->getParams(); + $menuitem = (int) $params->get('redirect_menuitem'); + + // Check for redirection after submission when creating a new article only + if ($menuitem > 0 && $articleId == 0) { + $lang = ''; + + if (Multilanguage::isEnabled()) { + $item = $app->getMenu()->getItem($menuitem); + $lang = !is_null($item) && $item->language != '*' ? '&lang=' . $item->language : ''; + } + + // If ok, redirect to the return page. + if ($result) { + $this->setRedirect(Route::_('index.php?Itemid=' . $menuitem . $lang, false)); + } + } elseif ($this->getTask() === 'save2copy') { + // Redirect to the article page, use the redirect url set from parent controller + } else { + // If ok, redirect to the return page. + if ($result) { + $this->setRedirect(Route::_($this->getReturnPage(), false)); + } + } + + return $result; + } + + /** + * Method to reload a record. + * + * @param string $key The name of the primary key of the URL variable. + * @param string $urlVar The name of the URL variable if different from the primary key (sometimes required to avoid router collisions). + * + * @return void + * + * @since 3.8.0 + */ + public function reload($key = null, $urlVar = 'a_id') + { + parent::reload($key, $urlVar); + } + + /** + * Method to save a vote. + * + * @return void + * + * @since 1.6 + */ + public function vote() + { + // Check for request forgeries. + $this->checkToken(); + + $user_rating = $this->input->getInt('user_rating', -1); + + if ($user_rating > -1) { + $url = $this->input->getString('url', ''); + $id = $this->input->getInt('id', 0); + $viewName = $this->input->getString('view', $this->default_view); + $model = $this->getModel($viewName); + + // Don't redirect to an external URL. + if (!Uri::isInternal($url)) { + $url = Route::_('index.php'); + } + + if ($model->storeVote($id, $user_rating)) { + $this->setRedirect($url, Text::_('COM_CONTENT_ARTICLE_VOTE_SUCCESS')); + } else { + $this->setRedirect($url, Text::_('COM_CONTENT_ARTICLE_VOTE_FAILURE')); + } + } + } } diff --git a/components/com_content/src/Controller/DisplayController.php b/components/com_content/src/Controller/DisplayController.php index 498e5760c652e..1095ca72ac4a7 100644 --- a/components/com_content/src/Controller/DisplayController.php +++ b/components/com_content/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input = Factory::getApplication()->input; + /** + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The Application for the dispatcher + * @param \Joomla\CMS\Input\Input|null $input The Input object for the request + * + * @since 3.0.1 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + $this->input = Factory::getApplication()->input; - // Article frontpage Editor pagebreak proxying: - if ($this->input->get('view') === 'article' && $this->input->get('layout') === 'pagebreak') - { - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - } - // Article frontpage Editor article proxying: - elseif ($this->input->get('view') === 'articles' && $this->input->get('layout') === 'modal') - { - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - } + // Article frontpage Editor pagebreak proxying: + if ($this->input->get('view') === 'article' && $this->input->get('layout') === 'pagebreak') { + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + } elseif ($this->input->get('view') === 'articles' && $this->input->get('layout') === 'modal') { + // Article frontpage Editor article proxying: + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + } - parent::__construct($config, $factory, $app, $input); - } + parent::__construct($config, $factory, $app, $input); + } - /** - * Method to display a view. - * - * @param boolean $cachable If true, the view output will be cached. - * @param boolean $urlparams An array of safe URL parameters and their variable types, for valid values see {@link JFilterInput::clean()}. - * - * @return DisplayController This object to support chaining. - * - * @since 1.5 - */ - public function display($cachable = false, $urlparams = false) - { - $cachable = true; + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached. + * @param boolean $urlparams An array of safe URL parameters and their variable types, for valid values see {@link JFilterInput::clean()}. + * + * @return DisplayController This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $cachable = true; - /** - * Set the default view name and format from the Request. - * Note we are using a_id to avoid collisions with the router and the return page. - * Frontend is a bit messier than the backend. - */ - $id = $this->input->getInt('a_id'); - $vName = $this->input->getCmd('view', 'categories'); - $this->input->set('view', $vName); + /** + * Set the default view name and format from the Request. + * Note we are using a_id to avoid collisions with the router and the return page. + * Frontend is a bit messier than the backend. + */ + $id = $this->input->getInt('a_id'); + $vName = $this->input->getCmd('view', 'categories'); + $this->input->set('view', $vName); - $user = $this->app->getIdentity(); + $user = $this->app->getIdentity(); - if ($user->get('id') - || ($this->input->getMethod() === 'POST' - && (($vName === 'category' && $this->input->get('layout') !== 'blog') || $vName === 'archive' ))) - { - $cachable = false; - } + if ( + $user->get('id') + || ($this->input->getMethod() === 'POST' + && (($vName === 'category' && $this->input->get('layout') !== 'blog') || $vName === 'archive' )) + ) { + $cachable = false; + } - $safeurlparams = array( - 'catid' => 'INT', - 'id' => 'INT', - 'cid' => 'ARRAY', - 'year' => 'INT', - 'month' => 'INT', - 'limit' => 'UINT', - 'limitstart' => 'UINT', - 'showall' => 'INT', - 'return' => 'BASE64', - 'filter' => 'STRING', - 'filter_order' => 'CMD', - 'filter_order_Dir' => 'CMD', - 'filter-search' => 'STRING', - 'print' => 'BOOLEAN', - 'lang' => 'CMD', - 'Itemid' => 'INT'); + $safeurlparams = array( + 'catid' => 'INT', + 'id' => 'INT', + 'cid' => 'ARRAY', + 'year' => 'INT', + 'month' => 'INT', + 'limit' => 'UINT', + 'limitstart' => 'UINT', + 'showall' => 'INT', + 'return' => 'BASE64', + 'filter' => 'STRING', + 'filter_order' => 'CMD', + 'filter_order_Dir' => 'CMD', + 'filter-search' => 'STRING', + 'print' => 'BOOLEAN', + 'lang' => 'CMD', + 'Itemid' => 'INT'); - // Check for edit form. - if ($vName === 'form' && !$this->checkEditId('com_content.edit.article', $id)) - { - // Somehow the person just went to the form - we don't allow that. - throw new \Exception(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 403); - } + // Check for edit form. + if ($vName === 'form' && !$this->checkEditId('com_content.edit.article', $id)) { + // Somehow the person just went to the form - we don't allow that. + throw new \Exception(Text::sprintf('JLIB_APPLICATION_ERROR_UNHELD_ID', $id), 403); + } - if ($vName === 'article') - { - // Get/Create the model - if ($model = $this->getModel($vName)) - { - if (ComponentHelper::getParams('com_content')->get('record_hits', 1) == 1) - { - $model->hit(); - } - } - } + if ($vName === 'article') { + // Get/Create the model + if ($model = $this->getModel($vName)) { + if (ComponentHelper::getParams('com_content')->get('record_hits', 1) == 1) { + $model->hit(); + } + } + } - parent::display($cachable, $safeurlparams); + parent::display($cachable, $safeurlparams); - return $this; - } + return $this; + } } diff --git a/components/com_content/src/Dispatcher/Dispatcher.php b/components/com_content/src/Dispatcher/Dispatcher.php index 7efdf0c367e6a..71bd28cd68dea 100644 --- a/components/com_content/src/Dispatcher/Dispatcher.php +++ b/components/com_content/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ input->get('view') === 'articles' && $this->input->get('layout') === 'modal') - || ($this->input->get('view') === 'article' && $this->input->get('layout') === 'pagebreak'); - - if ($checkCreateEdit) - { - // Can create in any category (component permission) or at least in one category - $canCreateRecords = $this->app->getIdentity()->authorise('core.create', 'com_content') - || count($this->app->getIdentity()->getAuthorisedCategories('com_content', 'core.create')) > 0; - - // Instead of checking edit on all records, we can use **same** check as the form editing view - $values = (array) $this->app->getUserState('com_content.edit.article.id'); - $isEditingRecords = count($values); - $hasAccess = $canCreateRecords || $isEditingRecords; - - if (!$hasAccess) - { - $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'warning'); - - return; - } - } - - parent::dispatch(); - } + /** + * Dispatch a controller task. Redirecting the user if appropriate. + * + * @return void + * + * @since 4.0.0 + */ + public function dispatch() + { + $checkCreateEdit = ($this->input->get('view') === 'articles' && $this->input->get('layout') === 'modal') + || ($this->input->get('view') === 'article' && $this->input->get('layout') === 'pagebreak'); + + if ($checkCreateEdit) { + // Can create in any category (component permission) or at least in one category + $canCreateRecords = $this->app->getIdentity()->authorise('core.create', 'com_content') + || count($this->app->getIdentity()->getAuthorisedCategories('com_content', 'core.create')) > 0; + + // Instead of checking edit on all records, we can use **same** check as the form editing view + $values = (array) $this->app->getUserState('com_content.edit.article.id'); + $isEditingRecords = count($values); + $hasAccess = $canCreateRecords || $isEditingRecords; + + if (!$hasAccess) { + $this->app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'warning'); + + return; + } + } + + parent::dispatch(); + } } diff --git a/components/com_content/src/Helper/AssociationHelper.php b/components/com_content/src/Helper/AssociationHelper.php index 0699a3c1aeab4..ab3433529744d 100644 --- a/components/com_content/src/Helper/AssociationHelper.php +++ b/components/com_content/src/Helper/AssociationHelper.php @@ -1,4 +1,5 @@ input; - $view = $view ?? $jinput->get('view'); - $component = $jinput->getCmd('option'); - $id = empty($id) ? $jinput->getInt('id') : $id; - - if ($layout === null && $jinput->get('view') == $view && $component == 'com_content') - { - $layout = $jinput->get('layout', '', 'string'); - } - - if ($view === 'article') - { - if ($id) - { - $user = Factory::getUser(); - $groups = implode(',', $user->getAuthorisedViewLevels()); - $db = Factory::getDbo(); - $advClause = array(); - - // Filter by user groups - $advClause[] = 'c2.access IN (' . $groups . ')'; - - // Filter by current language - $advClause[] = 'c2.language != ' . $db->quote(Factory::getLanguage()->getTag()); - - if (!$user->authorise('core.edit.state', 'com_content') && !$user->authorise('core.edit', 'com_content')) - { - // Filter by start and end dates. - $date = Factory::getDate(); - - $nowDate = $db->quote($date->toSql()); - - $advClause[] = '(c2.publish_up IS NULL OR c2.publish_up <= ' . $nowDate . ')'; - $advClause[] = '(c2.publish_down IS NULL OR c2.publish_down >= ' . $nowDate . ')'; - - // Filter by published - $advClause[] = 'c2.state = 1'; - } - - $associations = Associations::getAssociations( - 'com_content', - '#__content', - 'com_content.item', - $id, - 'id', - 'alias', - 'catid', - $advClause - ); - - $return = array(); - - foreach ($associations as $tag => $item) - { - $return[$tag] = RouteHelper::getArticleRoute($item->id, (int) $item->catid, $item->language, $layout); - } - - return $return; - } - } - - if ($view === 'category' || $view === 'categories') - { - return self::getCategoryAssociations($id, 'com_content', $layout); - } - - return array(); - } - - /** - * Method to display in frontend the associations for a given article - * - * @param integer $id Id of the article - * - * @return array An array containing the association URL and the related language object - * - * @since 3.7.0 - */ - public static function displayAssociations($id) - { - $return = array(); - - if ($associations = self::getAssociations($id, 'article')) - { - $levels = Factory::getUser()->getAuthorisedViewLevels(); - $languages = LanguageHelper::getLanguages(); - - foreach ($languages as $language) - { - // Do not display language when no association - if (empty($associations[$language->lang_code])) - { - continue; - } - - // Do not display language without frontend UI - if (!array_key_exists($language->lang_code, LanguageHelper::getInstalledLanguages(0))) - { - continue; - } - - // Do not display language without specific home menu - if (!array_key_exists($language->lang_code, Multilanguage::getSiteHomePages())) - { - continue; - } - - // Do not display language without authorized access level - if (isset($language->access) && $language->access && !in_array($language->access, $levels)) - { - continue; - } - - $return[$language->lang_code] = array('item' => $associations[$language->lang_code], 'language' => $language); - } - } - - return $return; - } + /** + * Method to get the associations for a given item + * + * @param integer $id Id of the item + * @param string $view Name of the view + * @param string $layout View layout + * + * @return array Array of associations for the item + * + * @since 3.0 + */ + public static function getAssociations($id = 0, $view = null, $layout = null) + { + $jinput = Factory::getApplication()->input; + $view = $view ?? $jinput->get('view'); + $component = $jinput->getCmd('option'); + $id = empty($id) ? $jinput->getInt('id') : $id; + + if ($layout === null && $jinput->get('view') == $view && $component == 'com_content') { + $layout = $jinput->get('layout', '', 'string'); + } + + if ($view === 'article') { + if ($id) { + $user = Factory::getUser(); + $groups = implode(',', $user->getAuthorisedViewLevels()); + $db = Factory::getDbo(); + $advClause = array(); + + // Filter by user groups + $advClause[] = 'c2.access IN (' . $groups . ')'; + + // Filter by current language + $advClause[] = 'c2.language != ' . $db->quote(Factory::getLanguage()->getTag()); + + if (!$user->authorise('core.edit.state', 'com_content') && !$user->authorise('core.edit', 'com_content')) { + // Filter by start and end dates. + $date = Factory::getDate(); + + $nowDate = $db->quote($date->toSql()); + + $advClause[] = '(c2.publish_up IS NULL OR c2.publish_up <= ' . $nowDate . ')'; + $advClause[] = '(c2.publish_down IS NULL OR c2.publish_down >= ' . $nowDate . ')'; + + // Filter by published + $advClause[] = 'c2.state = 1'; + } + + $associations = Associations::getAssociations( + 'com_content', + '#__content', + 'com_content.item', + $id, + 'id', + 'alias', + 'catid', + $advClause + ); + + $return = array(); + + foreach ($associations as $tag => $item) { + $return[$tag] = RouteHelper::getArticleRoute($item->id, (int) $item->catid, $item->language, $layout); + } + + return $return; + } + } + + if ($view === 'category' || $view === 'categories') { + return self::getCategoryAssociations($id, 'com_content', $layout); + } + + return array(); + } + + /** + * Method to display in frontend the associations for a given article + * + * @param integer $id Id of the article + * + * @return array An array containing the association URL and the related language object + * + * @since 3.7.0 + */ + public static function displayAssociations($id) + { + $return = array(); + + if ($associations = self::getAssociations($id, 'article')) { + $levels = Factory::getUser()->getAuthorisedViewLevels(); + $languages = LanguageHelper::getLanguages(); + + foreach ($languages as $language) { + // Do not display language when no association + if (empty($associations[$language->lang_code])) { + continue; + } + + // Do not display language without frontend UI + if (!array_key_exists($language->lang_code, LanguageHelper::getInstalledLanguages(0))) { + continue; + } + + // Do not display language without specific home menu + if (!array_key_exists($language->lang_code, Multilanguage::getSiteHomePages())) { + continue; + } + + // Do not display language without authorized access level + if (isset($language->access) && $language->access && !in_array($language->access, $levels)) { + continue; + } + + $return[$language->lang_code] = array('item' => $associations[$language->lang_code], 'language' => $language); + } + } + + return $return; + } } diff --git a/components/com_content/src/Helper/QueryHelper.php b/components/com_content/src/Helper/QueryHelper.php index 3de4456ba6ca4..72c9aebfa4218 100644 --- a/components/com_content/src/Helper/QueryHelper.php +++ b/components/com_content/src/Helper/QueryHelper.php @@ -1,4 +1,5 @@ getQuery(true)->rand(); - break; - - case 'vote': - $orderby = 'a.id DESC '; - - if (PluginHelper::isEnabled('content', 'vote')) - { - $orderby = 'rating_count DESC '; - } - break; - - case 'rvote': - $orderby = 'a.id ASC '; - - if (PluginHelper::isEnabled('content', 'vote')) - { - $orderby = 'rating_count ASC '; - } - break; - - case 'rank': - $orderby = 'a.id DESC '; - - if (PluginHelper::isEnabled('content', 'vote')) - { - $orderby = 'rating DESC '; - } - break; - - case 'rrank': - $orderby = 'a.id ASC '; - - if (PluginHelper::isEnabled('content', 'vote')) - { - $orderby = 'rating ASC '; - } - break; - - default: - $orderby = 'a.ordering'; - break; - } - - return $orderby; - } - - /** - * Translate an order code to a field for primary category ordering. - * - * @param string $orderDate The ordering code. - * @param DatabaseInterface $db The database - * - * @return string The SQL field(s) to order by. - * - * @since 1.6 - */ - public static function getQueryDate($orderDate, DatabaseInterface $db = null) - { - $db = $db ?: Factory::getDbo(); - - switch ($orderDate) - { - case 'modified' : - $queryDate = ' CASE WHEN a.modified IS NULL THEN a.created ELSE a.modified END'; - break; - - // Use created if publish_up is not set - case 'published' : - $queryDate = ' CASE WHEN a.publish_up IS NULL THEN a.created ELSE a.publish_up END '; - break; - - case 'unpublished' : - $queryDate = ' CASE WHEN a.publish_down IS NULL THEN a.created ELSE a.publish_down END '; - break; - case 'created' : - default : - $queryDate = ' a.created '; - break; - } - - return $queryDate; - } - - /** - * Get join information for the voting query. - * - * @param \Joomla\Registry\Registry $params An options object for the article. - * - * @return array A named array with "select" and "join" keys. - * - * @since 1.5 - * - * @deprecated 5.0 Deprecated without replacement, not used in core - */ - public static function buildVotingQuery($params = null) - { - if (!$params) - { - $params = ComponentHelper::getParams('com_content'); - } - - $voting = $params->get('show_vote'); - - if ($voting) - { - // Calculate voting count - $select = ' , ROUND(v.rating_sum / v.rating_count) AS rating, v.rating_count'; - $join = ' LEFT JOIN #__content_rating AS v ON a.id = v.content_id'; - } - else - { - $select = ''; - $join = ''; - } - - return array('select' => $select, 'join' => $join); - } + /** + * Translate an order code to a field for primary category ordering. + * + * @param string $orderby The ordering code. + * + * @return string The SQL field(s) to order by. + * + * @since 1.5 + */ + public static function orderbyPrimary($orderby) + { + switch ($orderby) { + case 'alpha': + $orderby = 'c.path, '; + break; + + case 'ralpha': + $orderby = 'c.path DESC, '; + break; + + case 'order': + $orderby = 'c.lft, '; + break; + + default: + $orderby = ''; + break; + } + + return $orderby; + } + + /** + * Translate an order code to a field for secondary category ordering. + * + * @param string $orderby The ordering code. + * @param string $orderDate The ordering code for the date. + * @param DatabaseInterface $db The database + * + * @return string The SQL field(s) to order by. + * + * @since 1.5 + */ + public static function orderbySecondary($orderby, $orderDate = 'created', DatabaseInterface $db = null) + { + $db = $db ?: Factory::getDbo(); + + $queryDate = self::getQueryDate($orderDate, $db); + + switch ($orderby) { + case 'date': + $orderby = $queryDate; + break; + + case 'rdate': + $orderby = $queryDate . ' DESC '; + break; + + case 'alpha': + $orderby = 'a.title'; + break; + + case 'ralpha': + $orderby = 'a.title DESC'; + break; + + case 'hits': + $orderby = 'a.hits DESC'; + break; + + case 'rhits': + $orderby = 'a.hits'; + break; + + case 'rorder': + $orderby = 'a.ordering DESC'; + break; + + case 'author': + $orderby = 'author'; + break; + + case 'rauthor': + $orderby = 'author DESC'; + break; + + case 'front': + $orderby = 'a.featured DESC, fp.ordering, ' . $queryDate . ' DESC '; + break; + + case 'random': + $orderby = $db->getQuery(true)->rand(); + break; + + case 'vote': + $orderby = 'a.id DESC '; + + if (PluginHelper::isEnabled('content', 'vote')) { + $orderby = 'rating_count DESC '; + } + break; + + case 'rvote': + $orderby = 'a.id ASC '; + + if (PluginHelper::isEnabled('content', 'vote')) { + $orderby = 'rating_count ASC '; + } + break; + + case 'rank': + $orderby = 'a.id DESC '; + + if (PluginHelper::isEnabled('content', 'vote')) { + $orderby = 'rating DESC '; + } + break; + + case 'rrank': + $orderby = 'a.id ASC '; + + if (PluginHelper::isEnabled('content', 'vote')) { + $orderby = 'rating ASC '; + } + break; + + default: + $orderby = 'a.ordering'; + break; + } + + return $orderby; + } + + /** + * Translate an order code to a field for primary category ordering. + * + * @param string $orderDate The ordering code. + * @param DatabaseInterface $db The database + * + * @return string The SQL field(s) to order by. + * + * @since 1.6 + */ + public static function getQueryDate($orderDate, DatabaseInterface $db = null) + { + $db = $db ?: Factory::getDbo(); + + switch ($orderDate) { + case 'modified': + $queryDate = ' CASE WHEN a.modified IS NULL THEN a.created ELSE a.modified END'; + break; + + // Use created if publish_up is not set + case 'published': + $queryDate = ' CASE WHEN a.publish_up IS NULL THEN a.created ELSE a.publish_up END '; + break; + + case 'unpublished': + $queryDate = ' CASE WHEN a.publish_down IS NULL THEN a.created ELSE a.publish_down END '; + break; + case 'created': + default: + $queryDate = ' a.created '; + break; + } + + return $queryDate; + } + + /** + * Get join information for the voting query. + * + * @param \Joomla\Registry\Registry $params An options object for the article. + * + * @return array A named array with "select" and "join" keys. + * + * @since 1.5 + * + * @deprecated 5.0 Deprecated without replacement, not used in core + */ + public static function buildVotingQuery($params = null) + { + if (!$params) { + $params = ComponentHelper::getParams('com_content'); + } + + $voting = $params->get('show_vote'); + + if ($voting) { + // Calculate voting count + $select = ' , ROUND(v.rating_sum / v.rating_count) AS rating, v.rating_count'; + $join = ' LEFT JOIN #__content_rating AS v ON a.id = v.content_id'; + } else { + $select = ''; + $join = ''; + } + + return array('select' => $select, 'join' => $join); + } } diff --git a/components/com_content/src/Helper/RouteHelper.php b/components/com_content/src/Helper/RouteHelper.php index 91b3441f7533d..ea1df8572f845 100644 --- a/components/com_content/src/Helper/RouteHelper.php +++ b/components/com_content/src/Helper/RouteHelper.php @@ -1,4 +1,5 @@ 1) - { - $link .= '&catid=' . $catid; - } + if ((int) $catid > 1) { + $link .= '&catid=' . $catid; + } - if ($language && $language !== '*' && Multilanguage::isEnabled()) - { - $link .= '&lang=' . $language; - } + if ($language && $language !== '*' && Multilanguage::isEnabled()) { + $link .= '&lang=' . $language; + } - if ($layout) - { - $link .= '&layout=' . $layout; - } + if ($layout) { + $link .= '&layout=' . $layout; + } - return $link; - } + return $link; + } - /** - * Get the category route. - * - * @param integer $catid The category ID. - * @param integer $language The language code. - * @param string $layout The layout value. - * - * @return string The article route. - * - * @since 1.5 - */ - public static function getCategoryRoute($catid, $language = 0, $layout = null) - { - if ($catid instanceof CategoryNode) - { - $id = $catid->id; - } - else - { - $id = (int) $catid; - } + /** + * Get the category route. + * + * @param integer $catid The category ID. + * @param integer $language The language code. + * @param string $layout The layout value. + * + * @return string The article route. + * + * @since 1.5 + */ + public static function getCategoryRoute($catid, $language = 0, $layout = null) + { + if ($catid instanceof CategoryNode) { + $id = $catid->id; + } else { + $id = (int) $catid; + } - if ($id < 1) - { - return ''; - } + if ($id < 1) { + return ''; + } - $link = 'index.php?option=com_content&view=category&id=' . $id; + $link = 'index.php?option=com_content&view=category&id=' . $id; - if ($language && $language !== '*' && Multilanguage::isEnabled()) - { - $link .= '&lang=' . $language; - } + if ($language && $language !== '*' && Multilanguage::isEnabled()) { + $link .= '&lang=' . $language; + } - if ($layout) - { - $link .= '&layout=' . $layout; - } + if ($layout) { + $link .= '&layout=' . $layout; + } - return $link; - } + return $link; + } - /** - * Get the form route. - * - * @param integer $id The form ID. - * - * @return string The article route. - * - * @since 1.5 - */ - public static function getFormRoute($id) - { - return 'index.php?option=com_content&task=article.edit&a_id=' . (int) $id; - } + /** + * Get the form route. + * + * @param integer $id The form ID. + * + * @return string The article route. + * + * @since 1.5 + */ + public static function getFormRoute($id) + { + return 'index.php?option=com_content&task=article.edit&a_id=' . (int) $id; + } } diff --git a/components/com_content/src/Model/ArchiveModel.php b/components/com_content/src/Model/ArchiveModel.php index af8ce8ce263be..82578983d511c 100644 --- a/components/com_content/src/Model/ArchiveModel.php +++ b/components/com_content/src/Model/ArchiveModel.php @@ -1,4 +1,5 @@ state->get('params'); - - // Filter on archived articles - $this->setState('filter.published', ContentComponent::CONDITION_ARCHIVED); - - // Filter on month, year - $this->setState('filter.month', $app->input->getInt('month')); - $this->setState('filter.year', $app->input->getInt('year')); - - // Optional filter text - $this->setState('list.filter', $app->input->getString('filter-search')); - - // Get list limit - $itemid = $app->input->get('Itemid', 0, 'int'); - $limit = $app->getUserStateFromRequest('com_content.archive.list' . $itemid . '.limit', 'limit', $params->get('display_num', 20), 'uint'); - $this->setState('list.limit', $limit); - - // Set the archive ordering - $articleOrderby = $params->get('orderby_sec', 'rdate'); - $articleOrderDate = $params->get('order_date'); - - // No category ordering - $secondary = QueryHelper::orderbySecondary($articleOrderby, $articleOrderDate, $this->getDatabase()); - - $this->setState('list.ordering', $secondary . ', a.created DESC'); - $this->setState('list.direction', ''); - } - - /** - * Get the master query for retrieving a list of articles subject to the model state. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - $params = $this->state->params; - $app = Factory::getApplication(); - $catids = $app->input->get('catid', array(), 'array'); - $catids = array_values(array_diff($catids, array(''))); - - $articleOrderDate = $params->get('order_date'); - - // Create a new query object. - $db = $this->getDatabase(); - $query = parent::getListQuery(); - - // Add routing for archive - $query->select( - [ - $this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS ' . $db->quoteName('slug'), - $this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS ' . $db->quoteName('catslug'), - ] - ); - - // Filter on month, year - // First, get the date field - $queryDate = QueryHelper::getQueryDate($articleOrderDate, $this->getDatabase()); - - if ($month = (int) $this->getState('filter.month')) - { - $query->where($query->month($queryDate) . ' = :month') - ->bind(':month', $month, ParameterType::INTEGER); - } - - if ($year = (int) $this->getState('filter.year')) - { - $query->where($query->year($queryDate) . ' = :year') - ->bind(':year', $year, ParameterType::INTEGER); - } - - if (count($catids) > 0) - { - $query->whereIn($db->quoteName('c.id'), $catids); - } - - return $query; - } - - /** - * Method to get the archived article list - * - * @access public - * @return array - */ - public function getData() - { - $app = Factory::getApplication(); - - // Lets load the content if it doesn't already exist - if (empty($this->_data)) - { - // Get the page/component configuration - $params = $app->getParams(); - - // Get the pagination request variables - $limit = $app->input->get('limit', $params->get('display_num', 20), 'uint'); - $limitstart = $app->input->get('limitstart', 0, 'uint'); - - $query = $this->_buildQuery(); - - $this->_data = $this->_getList($query, $limitstart, $limit); - } - - return $this->_data; - } - - /** - * Gets the archived articles years - * - * @return array - * - * @since 3.6.0 - */ - public function getYears() - { - $db = $this->getDatabase(); - $nowDate = Factory::getDate()->toSql(); - $query = $db->getQuery(true); - $years = $query->year($db->quoteName('c.created')); - - $query->select('DISTINCT ' . $years) - ->from($db->quoteName('#__content', 'c')) - ->where($db->quoteName('c.state') . ' = ' . ContentComponent::CONDITION_ARCHIVED) - ->extendWhere( - 'AND', - [ - $db->quoteName('c.publish_up') . ' IS NULL', - $db->quoteName('c.publish_up') . ' <= :publishUp', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('c.publish_down') . ' IS NULL', - $db->quoteName('c.publish_down') . ' >= :publishDown', - ], - 'OR' - ) - ->bind(':publishUp', $nowDate) - ->bind(':publishDown', $nowDate) - ->order('1 ASC'); - - $db->setQuery($query); - - return $db->loadColumn(); - } - - /** - * Generate column expression for slug or catslug. - * - * @param \Joomla\Database\DatabaseQuery $query Current query instance. - * @param string $id Column id name. - * @param string $alias Column alias name. - * - * @return string - * - * @since 4.0.0 - */ - private function getSlugColumn($query, $id, $alias) - { - $db = $this->getDatabase(); - - return 'CASE WHEN ' - . $query->charLength($db->quoteName($alias), '!=', '0') - . ' THEN ' - . $query->concatenate([$query->castAsChar($db->quoteName($id)), $db->quoteName($alias)], ':') - . ' ELSE ' - . $query->castAsChar($id) . ' END'; - } + /** + * Model context string. + * + * @var string + */ + public $_context = 'com_content.archive'; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering The field to order on. + * @param string $direction The direction to order on. + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + parent::populateState(); + + $app = Factory::getApplication(); + + // Add archive properties + $params = $this->state->get('params'); + + // Filter on archived articles + $this->setState('filter.published', ContentComponent::CONDITION_ARCHIVED); + + // Filter on month, year + $this->setState('filter.month', $app->input->getInt('month')); + $this->setState('filter.year', $app->input->getInt('year')); + + // Optional filter text + $this->setState('list.filter', $app->input->getString('filter-search')); + + // Get list limit + $itemid = $app->input->get('Itemid', 0, 'int'); + $limit = $app->getUserStateFromRequest('com_content.archive.list' . $itemid . '.limit', 'limit', $params->get('display_num', 20), 'uint'); + $this->setState('list.limit', $limit); + + // Set the archive ordering + $articleOrderby = $params->get('orderby_sec', 'rdate'); + $articleOrderDate = $params->get('order_date'); + + // No category ordering + $secondary = QueryHelper::orderbySecondary($articleOrderby, $articleOrderDate, $this->getDatabase()); + + $this->setState('list.ordering', $secondary . ', a.created DESC'); + $this->setState('list.direction', ''); + } + + /** + * Get the master query for retrieving a list of articles subject to the model state. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + $params = $this->state->params; + $app = Factory::getApplication(); + $catids = $app->input->get('catid', array(), 'array'); + $catids = array_values(array_diff($catids, array(''))); + + $articleOrderDate = $params->get('order_date'); + + // Create a new query object. + $db = $this->getDatabase(); + $query = parent::getListQuery(); + + // Add routing for archive + $query->select( + [ + $this->getSlugColumn($query, 'a.id', 'a.alias') . ' AS ' . $db->quoteName('slug'), + $this->getSlugColumn($query, 'c.id', 'c.alias') . ' AS ' . $db->quoteName('catslug'), + ] + ); + + // Filter on month, year + // First, get the date field + $queryDate = QueryHelper::getQueryDate($articleOrderDate, $this->getDatabase()); + + if ($month = (int) $this->getState('filter.month')) { + $query->where($query->month($queryDate) . ' = :month') + ->bind(':month', $month, ParameterType::INTEGER); + } + + if ($year = (int) $this->getState('filter.year')) { + $query->where($query->year($queryDate) . ' = :year') + ->bind(':year', $year, ParameterType::INTEGER); + } + + if (count($catids) > 0) { + $query->whereIn($db->quoteName('c.id'), $catids); + } + + return $query; + } + + /** + * Method to get the archived article list + * + * @access public + * @return array + */ + public function getData() + { + $app = Factory::getApplication(); + + // Lets load the content if it doesn't already exist + if (empty($this->_data)) { + // Get the page/component configuration + $params = $app->getParams(); + + // Get the pagination request variables + $limit = $app->input->get('limit', $params->get('display_num', 20), 'uint'); + $limitstart = $app->input->get('limitstart', 0, 'uint'); + + $query = $this->_buildQuery(); + + $this->_data = $this->_getList($query, $limitstart, $limit); + } + + return $this->_data; + } + + /** + * Gets the archived articles years + * + * @return array + * + * @since 3.6.0 + */ + public function getYears() + { + $db = $this->getDatabase(); + $nowDate = Factory::getDate()->toSql(); + $query = $db->getQuery(true); + $years = $query->year($db->quoteName('c.created')); + + $query->select('DISTINCT ' . $years) + ->from($db->quoteName('#__content', 'c')) + ->where($db->quoteName('c.state') . ' = ' . ContentComponent::CONDITION_ARCHIVED) + ->extendWhere( + 'AND', + [ + $db->quoteName('c.publish_up') . ' IS NULL', + $db->quoteName('c.publish_up') . ' <= :publishUp', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('c.publish_down') . ' IS NULL', + $db->quoteName('c.publish_down') . ' >= :publishDown', + ], + 'OR' + ) + ->bind(':publishUp', $nowDate) + ->bind(':publishDown', $nowDate) + ->order('1 ASC'); + + $db->setQuery($query); + + return $db->loadColumn(); + } + + /** + * Generate column expression for slug or catslug. + * + * @param \Joomla\Database\DatabaseQuery $query Current query instance. + * @param string $id Column id name. + * @param string $alias Column alias name. + * + * @return string + * + * @since 4.0.0 + */ + private function getSlugColumn($query, $id, $alias) + { + $db = $this->getDatabase(); + + return 'CASE WHEN ' + . $query->charLength($db->quoteName($alias), '!=', '0') + . ' THEN ' + . $query->concatenate([$query->castAsChar($db->quoteName($id)), $db->quoteName($alias)], ':') + . ' ELSE ' + . $query->castAsChar($id) . ' END'; + } } diff --git a/components/com_content/src/Model/ArticleModel.php b/components/com_content/src/Model/ArticleModel.php index b3daabdca6fca..620f08f32b54d 100644 --- a/components/com_content/src/Model/ArticleModel.php +++ b/components/com_content/src/Model/ArticleModel.php @@ -1,4 +1,5 @@ input->getInt('id'); - $this->setState('article.id', $pk); - - $offset = $app->input->getUint('limitstart'); - $this->setState('list.offset', $offset); - - // Load the parameters. - $params = $app->getParams(); - $this->setState('params', $params); - - $user = Factory::getUser(); - - // If $pk is set then authorise on complete asset, else on component only - $asset = empty($pk) ? 'com_content' : 'com_content.article.' . $pk; - - if ((!$user->authorise('core.edit.state', $asset)) && (!$user->authorise('core.edit', $asset))) - { - $this->setState('filter.published', ContentComponent::CONDITION_PUBLISHED); - $this->setState('filter.archived', ContentComponent::CONDITION_ARCHIVED); - } - - $this->setState('filter.language', Multilanguage::isEnabled()); - } - - /** - * Method to get article data. - * - * @param integer $pk The id of the article. - * - * @return object|boolean Menu item data object on success, boolean false - */ - public function getItem($pk = null) - { - $user = Factory::getUser(); - - $pk = (int) ($pk ?: $this->getState('article.id')); - - if ($this->_item === null) - { - $this->_item = array(); - } - - if (!isset($this->_item[$pk])) - { - try - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - $query->select( - $this->getState( - 'item.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.asset_id'), - $db->quoteName('a.title'), - $db->quoteName('a.alias'), - $db->quoteName('a.introtext'), - $db->quoteName('a.fulltext'), - $db->quoteName('a.state'), - $db->quoteName('a.catid'), - $db->quoteName('a.created'), - $db->quoteName('a.created_by'), - $db->quoteName('a.created_by_alias'), - $db->quoteName('a.modified'), - $db->quoteName('a.modified_by'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.publish_up'), - $db->quoteName('a.publish_down'), - $db->quoteName('a.images'), - $db->quoteName('a.urls'), - $db->quoteName('a.attribs'), - $db->quoteName('a.version'), - $db->quoteName('a.ordering'), - $db->quoteName('a.metakey'), - $db->quoteName('a.metadesc'), - $db->quoteName('a.access'), - $db->quoteName('a.hits'), - $db->quoteName('a.metadata'), - $db->quoteName('a.featured'), - $db->quoteName('a.language'), - ] - ) - ) - ->select( - [ - $db->quoteName('fp.featured_up'), - $db->quoteName('fp.featured_down'), - $db->quoteName('c.title', 'category_title'), - $db->quoteName('c.alias', 'category_alias'), - $db->quoteName('c.access', 'category_access'), - $db->quoteName('c.language', 'category_language'), - $db->quoteName('fp.ordering'), - $db->quoteName('u.name', 'author'), - $db->quoteName('parent.title', 'parent_title'), - $db->quoteName('parent.id', 'parent_id'), - $db->quoteName('parent.path', 'parent_route'), - $db->quoteName('parent.alias', 'parent_alias'), - $db->quoteName('parent.language', 'parent_language'), - 'ROUND(' . $db->quoteName('v.rating_sum') . ' / ' . $db->quoteName('v.rating_count') . ', 1) AS ' - . $db->quoteName('rating'), - $db->quoteName('v.rating_count', 'rating_count'), - ] - ) - ->from($db->quoteName('#__content', 'a')) - ->join( - 'INNER', - $db->quoteName('#__categories', 'c'), - $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid') - ) - ->join('LEFT', $db->quoteName('#__content_frontpage', 'fp'), $db->quoteName('fp.content_id') . ' = ' . $db->quoteName('a.id')) - ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('a.created_by')) - ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')) - ->join('LEFT', $db->quoteName('#__content_rating', 'v'), $db->quoteName('a.id') . ' = ' . $db->quoteName('v.content_id')) - ->where( - [ - $db->quoteName('a.id') . ' = :pk', - $db->quoteName('c.published') . ' > 0', - ] - ) - ->bind(':pk', $pk, ParameterType::INTEGER); - - // Filter by language - if ($this->getState('filter.language')) - { - $query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); - } - - if (!$user->authorise('core.edit.state', 'com_content.article.' . $pk) - && !$user->authorise('core.edit', 'com_content.article.' . $pk) - ) - { - // Filter by start and end dates. - $nowDate = Factory::getDate()->toSql(); - - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_up') . ' IS NULL', - $db->quoteName('a.publish_up') . ' <= :publishUp', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_down') . ' IS NULL', - $db->quoteName('a.publish_down') . ' >= :publishDown', - ], - 'OR' - ) - ->bind([':publishUp', ':publishDown'], $nowDate); - } - - // Filter by published state. - $published = $this->getState('filter.published'); - $archived = $this->getState('filter.archived'); - - if (is_numeric($published)) - { - $query->whereIn($db->quoteName('a.state'), [(int) $published, (int) $archived]); - } - - $db->setQuery($query); - - $data = $db->loadObject(); - - if (empty($data)) - { - throw new \Exception(Text::_('COM_CONTENT_ERROR_ARTICLE_NOT_FOUND'), 404); - } - - // Check for published state if filter set. - if ((is_numeric($published) || is_numeric($archived)) && ($data->state != $published && $data->state != $archived)) - { - throw new \Exception(Text::_('COM_CONTENT_ERROR_ARTICLE_NOT_FOUND'), 404); - } - - // Convert parameter fields to objects. - $registry = new Registry($data->attribs); - - $data->params = clone $this->getState('params'); - $data->params->merge($registry); - - $data->metadata = new Registry($data->metadata); - - // Technically guest could edit an article, but lets not check that to improve performance a little. - if (!$user->get('guest')) - { - $userId = $user->get('id'); - $asset = 'com_content.article.' . $data->id; - - // Check general edit permission first. - if ($user->authorise('core.edit', $asset)) - { - $data->params->set('access-edit', true); - } - - // Now check if edit.own is available. - elseif (!empty($userId) && $user->authorise('core.edit.own', $asset)) - { - // Check for a valid user and that they are the owner. - if ($userId == $data->created_by) - { - $data->params->set('access-edit', true); - } - } - } - - // Compute view access permissions. - if ($access = $this->getState('filter.access')) - { - // If the access filter has been set, we already know this user can view. - $data->params->set('access-view', true); - } - else - { - // If no access filter is set, the layout takes some responsibility for display of limited information. - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - - if ($data->catid == 0 || $data->category_access === null) - { - $data->params->set('access-view', in_array($data->access, $groups)); - } - else - { - $data->params->set('access-view', in_array($data->access, $groups) && in_array($data->category_access, $groups)); - } - } - - $this->_item[$pk] = $data; - } - catch (\Exception $e) - { - if ($e->getCode() == 404) - { - // Need to go through the error handler to allow Redirect to work. - throw $e; - } - else - { - $this->setError($e); - $this->_item[$pk] = false; - } - } - } - - return $this->_item[$pk]; - } - - /** - * Increment the hit counter for the article. - * - * @param integer $pk Optional primary key of the article to increment. - * - * @return boolean True if successful; false otherwise and internal error set. - */ - public function hit($pk = 0) - { - $input = Factory::getApplication()->input; - $hitcount = $input->getInt('hitcount', 1); - - if ($hitcount) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('article.id'); - - $table = Table::getInstance('Content', 'JTable'); - $table->hit($pk); - } - - return true; - } - - /** - * Save user vote on article - * - * @param integer $pk Joomla Article Id - * @param integer $rate Voting rate - * - * @return boolean Return true on success - */ - public function storeVote($pk = 0, $rate = 0) - { - $pk = (int) $pk; - $rate = (int) $rate; - - if ($rate >= 1 && $rate <= 5 && $pk > 0) - { - $userIP = IpHelper::getIp(); - - // Initialize variables. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Create the base select statement. - $query->select('*') - ->from($db->quoteName('#__content_rating')) - ->where($db->quoteName('content_id') . ' = :pk') - ->bind(':pk', $pk, ParameterType::INTEGER); - - // Set the query and load the result. - $db->setQuery($query); - - // Check for a database error. - try - { - $rating = $db->loadObject(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - // There are no ratings yet, so lets insert our rating - if (!$rating) - { - $query = $db->getQuery(true); - - // Create the base insert statement. - $query->insert($db->quoteName('#__content_rating')) - ->columns( - [ - $db->quoteName('content_id'), - $db->quoteName('lastip'), - $db->quoteName('rating_sum'), - $db->quoteName('rating_count'), - ] - ) - ->values(':pk, :ip, :rate, 1') - ->bind(':pk', $pk, ParameterType::INTEGER) - ->bind(':ip', $userIP) - ->bind(':rate', $rate, ParameterType::INTEGER); - - // Set the query and execute the insert. - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - } - else - { - if ($userIP != $rating->lastip) - { - $query = $db->getQuery(true); - - // Create the base update statement. - $query->update($db->quoteName('#__content_rating')) - ->set( - [ - $db->quoteName('rating_count') . ' = ' . $db->quoteName('rating_count') . ' + 1', - $db->quoteName('rating_sum') . ' = ' . $db->quoteName('rating_sum') . ' + :rate', - $db->quoteName('lastip') . ' = :ip', - ] - ) - ->where($db->quoteName('content_id') . ' = :pk') - ->bind(':rate', $rate, ParameterType::INTEGER) - ->bind(':ip', $userIP) - ->bind(':pk', $pk, ParameterType::INTEGER); - - // Set the query and execute the update. - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - } - else - { - return false; - } - } - - $this->cleanCache(); - - return true; - } - - Factory::getApplication()->enqueueMessage(Text::sprintf('COM_CONTENT_INVALID_RATING', $rate), 'error'); - - return false; - } - - /** - * Cleans the cache of com_content and content modules - * - * @param string $group The cache group - * @param integer $clientId @deprecated 5.0 No longer used. - * - * @return void - * - * @since 3.9.9 - */ - protected function cleanCache($group = null, $clientId = 0) - { - parent::cleanCache('com_content'); - parent::cleanCache('mod_articles_archive'); - parent::cleanCache('mod_articles_categories'); - parent::cleanCache('mod_articles_category'); - parent::cleanCache('mod_articles_latest'); - parent::cleanCache('mod_articles_news'); - parent::cleanCache('mod_articles_popular'); - } + /** + * Model context string. + * + * @var string + */ + protected $_context = 'com_content.article'; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @since 1.6 + * + * @return void + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load state from the request. + $pk = $app->input->getInt('id'); + $this->setState('article.id', $pk); + + $offset = $app->input->getUint('limitstart'); + $this->setState('list.offset', $offset); + + // Load the parameters. + $params = $app->getParams(); + $this->setState('params', $params); + + $user = Factory::getUser(); + + // If $pk is set then authorise on complete asset, else on component only + $asset = empty($pk) ? 'com_content' : 'com_content.article.' . $pk; + + if ((!$user->authorise('core.edit.state', $asset)) && (!$user->authorise('core.edit', $asset))) { + $this->setState('filter.published', ContentComponent::CONDITION_PUBLISHED); + $this->setState('filter.archived', ContentComponent::CONDITION_ARCHIVED); + } + + $this->setState('filter.language', Multilanguage::isEnabled()); + } + + /** + * Method to get article data. + * + * @param integer $pk The id of the article. + * + * @return object|boolean Menu item data object on success, boolean false + */ + public function getItem($pk = null) + { + $user = Factory::getUser(); + + $pk = (int) ($pk ?: $this->getState('article.id')); + + if ($this->_item === null) { + $this->_item = array(); + } + + if (!isset($this->_item[$pk])) { + try { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + $query->select( + $this->getState( + 'item.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.asset_id'), + $db->quoteName('a.title'), + $db->quoteName('a.alias'), + $db->quoteName('a.introtext'), + $db->quoteName('a.fulltext'), + $db->quoteName('a.state'), + $db->quoteName('a.catid'), + $db->quoteName('a.created'), + $db->quoteName('a.created_by'), + $db->quoteName('a.created_by_alias'), + $db->quoteName('a.modified'), + $db->quoteName('a.modified_by'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.publish_up'), + $db->quoteName('a.publish_down'), + $db->quoteName('a.images'), + $db->quoteName('a.urls'), + $db->quoteName('a.attribs'), + $db->quoteName('a.version'), + $db->quoteName('a.ordering'), + $db->quoteName('a.metakey'), + $db->quoteName('a.metadesc'), + $db->quoteName('a.access'), + $db->quoteName('a.hits'), + $db->quoteName('a.metadata'), + $db->quoteName('a.featured'), + $db->quoteName('a.language'), + ] + ) + ) + ->select( + [ + $db->quoteName('fp.featured_up'), + $db->quoteName('fp.featured_down'), + $db->quoteName('c.title', 'category_title'), + $db->quoteName('c.alias', 'category_alias'), + $db->quoteName('c.access', 'category_access'), + $db->quoteName('c.language', 'category_language'), + $db->quoteName('fp.ordering'), + $db->quoteName('u.name', 'author'), + $db->quoteName('parent.title', 'parent_title'), + $db->quoteName('parent.id', 'parent_id'), + $db->quoteName('parent.path', 'parent_route'), + $db->quoteName('parent.alias', 'parent_alias'), + $db->quoteName('parent.language', 'parent_language'), + 'ROUND(' . $db->quoteName('v.rating_sum') . ' / ' . $db->quoteName('v.rating_count') . ', 1) AS ' + . $db->quoteName('rating'), + $db->quoteName('v.rating_count', 'rating_count'), + ] + ) + ->from($db->quoteName('#__content', 'a')) + ->join( + 'INNER', + $db->quoteName('#__categories', 'c'), + $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid') + ) + ->join('LEFT', $db->quoteName('#__content_frontpage', 'fp'), $db->quoteName('fp.content_id') . ' = ' . $db->quoteName('a.id')) + ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('a.created_by')) + ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')) + ->join('LEFT', $db->quoteName('#__content_rating', 'v'), $db->quoteName('a.id') . ' = ' . $db->quoteName('v.content_id')) + ->where( + [ + $db->quoteName('a.id') . ' = :pk', + $db->quoteName('c.published') . ' > 0', + ] + ) + ->bind(':pk', $pk, ParameterType::INTEGER); + + // Filter by language + if ($this->getState('filter.language')) { + $query->whereIn($db->quoteName('a.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); + } + + if ( + !$user->authorise('core.edit.state', 'com_content.article.' . $pk) + && !$user->authorise('core.edit', 'com_content.article.' . $pk) + ) { + // Filter by start and end dates. + $nowDate = Factory::getDate()->toSql(); + + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_up') . ' IS NULL', + $db->quoteName('a.publish_up') . ' <= :publishUp', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_down') . ' IS NULL', + $db->quoteName('a.publish_down') . ' >= :publishDown', + ], + 'OR' + ) + ->bind([':publishUp', ':publishDown'], $nowDate); + } + + // Filter by published state. + $published = $this->getState('filter.published'); + $archived = $this->getState('filter.archived'); + + if (is_numeric($published)) { + $query->whereIn($db->quoteName('a.state'), [(int) $published, (int) $archived]); + } + + $db->setQuery($query); + + $data = $db->loadObject(); + + if (empty($data)) { + throw new \Exception(Text::_('COM_CONTENT_ERROR_ARTICLE_NOT_FOUND'), 404); + } + + // Check for published state if filter set. + if ((is_numeric($published) || is_numeric($archived)) && ($data->state != $published && $data->state != $archived)) { + throw new \Exception(Text::_('COM_CONTENT_ERROR_ARTICLE_NOT_FOUND'), 404); + } + + // Convert parameter fields to objects. + $registry = new Registry($data->attribs); + + $data->params = clone $this->getState('params'); + $data->params->merge($registry); + + $data->metadata = new Registry($data->metadata); + + // Technically guest could edit an article, but lets not check that to improve performance a little. + if (!$user->get('guest')) { + $userId = $user->get('id'); + $asset = 'com_content.article.' . $data->id; + + // Check general edit permission first. + if ($user->authorise('core.edit', $asset)) { + $data->params->set('access-edit', true); + } elseif (!empty($userId) && $user->authorise('core.edit.own', $asset)) { + // Now check if edit.own is available. + // Check for a valid user and that they are the owner. + if ($userId == $data->created_by) { + $data->params->set('access-edit', true); + } + } + } + + // Compute view access permissions. + if ($access = $this->getState('filter.access')) { + // If the access filter has been set, we already know this user can view. + $data->params->set('access-view', true); + } else { + // If no access filter is set, the layout takes some responsibility for display of limited information. + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + + if ($data->catid == 0 || $data->category_access === null) { + $data->params->set('access-view', in_array($data->access, $groups)); + } else { + $data->params->set('access-view', in_array($data->access, $groups) && in_array($data->category_access, $groups)); + } + } + + $this->_item[$pk] = $data; + } catch (\Exception $e) { + if ($e->getCode() == 404) { + // Need to go through the error handler to allow Redirect to work. + throw $e; + } else { + $this->setError($e); + $this->_item[$pk] = false; + } + } + } + + return $this->_item[$pk]; + } + + /** + * Increment the hit counter for the article. + * + * @param integer $pk Optional primary key of the article to increment. + * + * @return boolean True if successful; false otherwise and internal error set. + */ + public function hit($pk = 0) + { + $input = Factory::getApplication()->input; + $hitcount = $input->getInt('hitcount', 1); + + if ($hitcount) { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('article.id'); + + $table = Table::getInstance('Content', 'JTable'); + $table->hit($pk); + } + + return true; + } + + /** + * Save user vote on article + * + * @param integer $pk Joomla Article Id + * @param integer $rate Voting rate + * + * @return boolean Return true on success + */ + public function storeVote($pk = 0, $rate = 0) + { + $pk = (int) $pk; + $rate = (int) $rate; + + if ($rate >= 1 && $rate <= 5 && $pk > 0) { + $userIP = IpHelper::getIp(); + + // Initialize variables. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Create the base select statement. + $query->select('*') + ->from($db->quoteName('#__content_rating')) + ->where($db->quoteName('content_id') . ' = :pk') + ->bind(':pk', $pk, ParameterType::INTEGER); + + // Set the query and load the result. + $db->setQuery($query); + + // Check for a database error. + try { + $rating = $db->loadObject(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + // There are no ratings yet, so lets insert our rating + if (!$rating) { + $query = $db->getQuery(true); + + // Create the base insert statement. + $query->insert($db->quoteName('#__content_rating')) + ->columns( + [ + $db->quoteName('content_id'), + $db->quoteName('lastip'), + $db->quoteName('rating_sum'), + $db->quoteName('rating_count'), + ] + ) + ->values(':pk, :ip, :rate, 1') + ->bind(':pk', $pk, ParameterType::INTEGER) + ->bind(':ip', $userIP) + ->bind(':rate', $rate, ParameterType::INTEGER); + + // Set the query and execute the insert. + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + } else { + if ($userIP != $rating->lastip) { + $query = $db->getQuery(true); + + // Create the base update statement. + $query->update($db->quoteName('#__content_rating')) + ->set( + [ + $db->quoteName('rating_count') . ' = ' . $db->quoteName('rating_count') . ' + 1', + $db->quoteName('rating_sum') . ' = ' . $db->quoteName('rating_sum') . ' + :rate', + $db->quoteName('lastip') . ' = :ip', + ] + ) + ->where($db->quoteName('content_id') . ' = :pk') + ->bind(':rate', $rate, ParameterType::INTEGER) + ->bind(':ip', $userIP) + ->bind(':pk', $pk, ParameterType::INTEGER); + + // Set the query and execute the update. + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + } else { + return false; + } + } + + $this->cleanCache(); + + return true; + } + + Factory::getApplication()->enqueueMessage(Text::sprintf('COM_CONTENT_INVALID_RATING', $rate), 'error'); + + return false; + } + + /** + * Cleans the cache of com_content and content modules + * + * @param string $group The cache group + * @param integer $clientId @deprecated 5.0 No longer used. + * + * @return void + * + * @since 3.9.9 + */ + protected function cleanCache($group = null, $clientId = 0) + { + parent::cleanCache('com_content'); + parent::cleanCache('mod_articles_archive'); + parent::cleanCache('mod_articles_categories'); + parent::cleanCache('mod_articles_category'); + parent::cleanCache('mod_articles_latest'); + parent::cleanCache('mod_articles_news'); + parent::cleanCache('mod_articles_popular'); + } } diff --git a/components/com_content/src/Model/ArticlesModel.php b/components/com_content/src/Model/ArticlesModel.php index fe1ab6aefda2c..dc2163a1ce2fd 100644 --- a/components/com_content/src/Model/ArticlesModel.php +++ b/components/com_content/src/Model/ArticlesModel.php @@ -1,4 +1,5 @@ input->get('limit', $app->get('list_limit', 0), 'uint'); - $this->setState('list.limit', $value); - - $value = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.start', $value); - - $value = $app->input->get('filter_tag', 0, 'uint'); - $this->setState('filter.tag', $value); - - $orderCol = $app->input->get('filter_order', 'a.ordering'); - - if (!in_array($orderCol, $this->filter_fields)) - { - $orderCol = 'a.ordering'; - } - - $this->setState('list.ordering', $orderCol); - - $listOrder = $app->input->get('filter_order_Dir', 'ASC'); - - if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) - { - $listOrder = 'ASC'; - } - - $this->setState('list.direction', $listOrder); - - $params = $app->getParams(); - $this->setState('params', $params); - - $user = Factory::getUser(); - - if ((!$user->authorise('core.edit.state', 'com_content')) && (!$user->authorise('core.edit', 'com_content'))) - { - // Filter on published for those who do not have edit or edit.state rights. - $this->setState('filter.published', ContentComponent::CONDITION_PUBLISHED); - } - - $this->setState('filter.language', Multilanguage::isEnabled()); - - // Process show_noauth parameter - if ((!$params->get('show_noauth')) || (!ComponentHelper::getParams('com_content')->get('show_noauth'))) - { - $this->setState('filter.access', true); - } - else - { - $this->setState('filter.access', false); - } - - $this->setState('layout', $app->input->getString('layout')); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - * - * @since 1.6 - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . serialize($this->getState('filter.published')); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.featured'); - $id .= ':' . serialize($this->getState('filter.article_id')); - $id .= ':' . $this->getState('filter.article_id.include'); - $id .= ':' . serialize($this->getState('filter.category_id')); - $id .= ':' . $this->getState('filter.category_id.include'); - $id .= ':' . serialize($this->getState('filter.author_id')); - $id .= ':' . $this->getState('filter.author_id.include'); - $id .= ':' . serialize($this->getState('filter.author_alias')); - $id .= ':' . $this->getState('filter.author_alias.include'); - $id .= ':' . $this->getState('filter.date_filtering'); - $id .= ':' . $this->getState('filter.date_field'); - $id .= ':' . $this->getState('filter.start_date_range'); - $id .= ':' . $this->getState('filter.end_date_range'); - $id .= ':' . $this->getState('filter.relative_date'); - $id .= ':' . serialize($this->getState('filter.tag')); - - return parent::getStoreId($id); - } - - /** - * Get the master query for retrieving a list of articles subject to the model state. - * - * @return \Joomla\Database\DatabaseQuery - * - * @since 1.6 - */ - protected function getListQuery() - { - $user = Factory::getUser(); - - // Create a new query object. - $db = $this->getDatabase(); - - /** @var \Joomla\Database\DatabaseQuery $query */ - $query = $db->getQuery(true); - - $nowDate = Factory::getDate()->toSql(); - - $conditionArchived = ContentComponent::CONDITION_ARCHIVED; - $conditionUnpublished = ContentComponent::CONDITION_UNPUBLISHED; - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - [ - $db->quoteName('a.id'), - $db->quoteName('a.title'), - $db->quoteName('a.alias'), - $db->quoteName('a.introtext'), - $db->quoteName('a.fulltext'), - $db->quoteName('a.checked_out'), - $db->quoteName('a.checked_out_time'), - $db->quoteName('a.catid'), - $db->quoteName('a.created'), - $db->quoteName('a.created_by'), - $db->quoteName('a.created_by_alias'), - $db->quoteName('a.modified'), - $db->quoteName('a.modified_by'), - // Use created if publish_up is null - 'CASE WHEN ' . $db->quoteName('a.publish_up') . ' IS NULL THEN ' . $db->quoteName('a.created') - . ' ELSE ' . $db->quoteName('a.publish_up') . ' END AS ' . $db->quoteName('publish_up'), - $db->quoteName('a.publish_down'), - $db->quoteName('a.images'), - $db->quoteName('a.urls'), - $db->quoteName('a.attribs'), - $db->quoteName('a.metadata'), - $db->quoteName('a.metakey'), - $db->quoteName('a.metadesc'), - $db->quoteName('a.access'), - $db->quoteName('a.hits'), - $db->quoteName('a.featured'), - $db->quoteName('a.language'), - $query->length($db->quoteName('a.fulltext')) . ' AS ' . $db->quoteName('readmore'), - $db->quoteName('a.ordering'), - ] - ) - ) - ->select( - [ - $db->quoteName('fp.featured_up'), - $db->quoteName('fp.featured_down'), - // Published/archived article in archived category is treated as archived article. If category is not published then force 0. - 'CASE WHEN ' . $db->quoteName('c.published') . ' = 2 AND ' . $db->quoteName('a.state') . ' > 0 THEN ' . $conditionArchived - . ' WHEN ' . $db->quoteName('c.published') . ' != 1 THEN ' . $conditionUnpublished - . ' ELSE ' . $db->quoteName('a.state') . ' END AS ' . $db->quoteName('state'), - $db->quoteName('c.title', 'category_title'), - $db->quoteName('c.path', 'category_route'), - $db->quoteName('c.access', 'category_access'), - $db->quoteName('c.alias', 'category_alias'), - $db->quoteName('c.language', 'category_language'), - $db->quoteName('c.published'), - $db->quoteName('c.published', 'parents_published'), - $db->quoteName('c.lft'), - 'CASE WHEN ' . $db->quoteName('a.created_by_alias') . ' > ' . $db->quote(' ') . ' THEN ' . $db->quoteName('a.created_by_alias') - . ' ELSE ' . $db->quoteName('ua.name') . ' END AS ' . $db->quoteName('author'), - $db->quoteName('ua.email', 'author_email'), - $db->quoteName('uam.name', 'modified_by_name'), - $db->quoteName('parent.title', 'parent_title'), - $db->quoteName('parent.id', 'parent_id'), - $db->quoteName('parent.path', 'parent_route'), - $db->quoteName('parent.alias', 'parent_alias'), - $db->quoteName('parent.language', 'parent_language'), - ] - ) - ->from($db->quoteName('#__content', 'a')) - ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) - ->join('LEFT', $db->quoteName('#__users', 'ua'), $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_by')) - ->join('LEFT', $db->quoteName('#__users', 'uam'), $db->quoteName('uam.id') . ' = ' . $db->quoteName('a.modified_by')) - ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')); - - $params = $this->getState('params'); - $orderby_sec = $params->get('orderby_sec'); - - // Join over the frontpage articles if required. - $frontpageJoin = 'LEFT'; - - if ($this->getState('filter.frontpage')) - { - if ($orderby_sec === 'front') - { - $query->select($db->quoteName('fp.ordering')); - $frontpageJoin = 'INNER'; - } - else - { - $query->where($db->quoteName('a.featured') . ' = 1'); - } - - $query->where( - [ - '(' . $db->quoteName('fp.featured_up') . ' IS NULL OR ' . $db->quoteName('fp.featured_up') . ' <= :frontpageUp)', - '(' . $db->quoteName('fp.featured_down') . ' IS NULL OR ' . $db->quoteName('fp.featured_down') . ' >= :frontpageDown)', - ] - ) - ->bind(':frontpageUp', $nowDate) - ->bind(':frontpageDown', $nowDate); - } - elseif ($orderby_sec === 'front' || $this->getState('list.ordering') === 'fp.ordering') - { - $query->select($db->quoteName('fp.ordering')); - } - - $query->join($frontpageJoin, $db->quoteName('#__content_frontpage', 'fp'), $db->quoteName('fp.content_id') . ' = ' . $db->quoteName('a.id')); - - if (PluginHelper::isEnabled('content', 'vote')) - { - // Join on voting table - $query->select( - [ - 'COALESCE(NULLIF(ROUND(' . $db->quoteName('v.rating_sum') . ' / ' . $db->quoteName('v.rating_count') . ', 1), 0), 0)' - . ' AS ' . $db->quoteName('rating'), - 'COALESCE(NULLIF(' . $db->quoteName('v.rating_count') . ', 0), 0) AS ' . $db->quoteName('rating_count'), - ] - ) - ->join('LEFT', $db->quoteName('#__content_rating', 'v'), $db->quoteName('a.id') . ' = ' . $db->quoteName('v.content_id')); - } - - // Filter by access level. - if ($this->getState('filter.access', true)) - { - $groups = $this->getState('filter.viewlevels', $user->getAuthorisedViewLevels()); - $query->whereIn($db->quoteName('a.access'), $groups) - ->whereIn($db->quoteName('c.access'), $groups); - } - - // Filter by published state - $condition = $this->getState('filter.published'); - - if (is_numeric($condition) && $condition == 2) - { - /** - * If category is archived then article has to be published or archived. - * Or category is published then article has to be archived. - */ - $query->where('((' . $db->quoteName('c.published') . ' = 2 AND ' . $db->quoteName('a.state') . ' > :conditionUnpublished)' - . ' OR (' . $db->quoteName('c.published') . ' = 1 AND ' . $db->quoteName('a.state') . ' = :conditionArchived))' - ) - ->bind(':conditionUnpublished', $conditionUnpublished, ParameterType::INTEGER) - ->bind(':conditionArchived', $conditionArchived, ParameterType::INTEGER); - } - elseif (is_numeric($condition)) - { - $condition = (int) $condition; - - // Category has to be published - $query->where($db->quoteName('c.published') . ' = 1 AND ' . $db->quoteName('a.state') . ' = :condition') - ->bind(':condition', $condition, ParameterType::INTEGER); - } - elseif (is_array($condition)) - { - // Category has to be published - $query->where( - $db->quoteName('c.published') . ' = 1 AND ' . $db->quoteName('a.state') - . ' IN (' . implode(',', $query->bindArray($condition)) . ')' - ); - } - - // Filter by featured state - $featured = $this->getState('filter.featured'); - - switch ($featured) - { - case 'hide': - $query->where($db->quoteName('a.featured') . ' = 0'); - break; - - case 'only': - $query->where( - [ - $db->quoteName('a.featured') . ' = 1', - '(' . $db->quoteName('fp.featured_up') . ' IS NULL OR ' . $db->quoteName('fp.featured_up') . ' <= :featuredUp)', - '(' . $db->quoteName('fp.featured_down') . ' IS NULL OR ' . $db->quoteName('fp.featured_down') . ' >= :featuredDown)', - ] - ) - ->bind(':featuredUp', $nowDate) - ->bind(':featuredDown', $nowDate); - break; - - case 'show': - default: - // Normally we do not discriminate between featured/unfeatured items. - break; - } - - // Filter by a single or group of articles. - $articleId = $this->getState('filter.article_id'); - - if (is_numeric($articleId)) - { - $articleId = (int) $articleId; - $type = $this->getState('filter.article_id.include', true) ? ' = ' : ' <> '; - $query->where($db->quoteName('a.id') . $type . ':articleId') - ->bind(':articleId', $articleId, ParameterType::INTEGER); - } - elseif (is_array($articleId)) - { - $articleId = ArrayHelper::toInteger($articleId); - - if ($this->getState('filter.article_id.include', true)) - { - $query->whereIn($db->quoteName('a.id'), $articleId); - } - else - { - $query->whereNotIn($db->quoteName('a.id'), $articleId); - } - } - - // Filter by a single or group of categories - $categoryId = $this->getState('filter.category_id'); - - if (is_numeric($categoryId)) - { - $type = $this->getState('filter.category_id.include', true) ? ' = ' : ' <> '; - - // Add subcategory check - $includeSubcategories = $this->getState('filter.subcategories', false); - - if ($includeSubcategories) - { - $categoryId = (int) $categoryId; - $levels = (int) $this->getState('filter.max_category_levels', 1); - - // Create a subquery for the subcategory list - $subQuery = $db->getQuery(true) - ->select($db->quoteName('sub.id')) - ->from($db->quoteName('#__categories', 'sub')) - ->join( - 'INNER', - $db->quoteName('#__categories', 'this'), - $db->quoteName('sub.lft') . ' > ' . $db->quoteName('this.lft') - . ' AND ' . $db->quoteName('sub.rgt') . ' < ' . $db->quoteName('this.rgt') - ) - ->where($db->quoteName('this.id') . ' = :subCategoryId'); - - $query->bind(':subCategoryId', $categoryId, ParameterType::INTEGER); - - if ($levels >= 0) - { - $subQuery->where($db->quoteName('sub.level') . ' <= ' . $db->quoteName('this.level') . ' + :levels'); - $query->bind(':levels', $levels, ParameterType::INTEGER); - } - - // Add the subquery to the main query - $query->where( - '(' . $db->quoteName('a.catid') . $type . ':categoryId OR ' . $db->quoteName('a.catid') . ' IN (' . $subQuery . '))' - ); - $query->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } - else - { - $query->where($db->quoteName('a.catid') . $type . ':categoryId'); - $query->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } - } - elseif (is_array($categoryId) && (count($categoryId) > 0)) - { - $categoryId = ArrayHelper::toInteger($categoryId); - - if (!empty($categoryId)) - { - if ($this->getState('filter.category_id.include', true)) - { - $query->whereIn($db->quoteName('a.catid'), $categoryId); - } - else - { - $query->whereNotIn($db->quoteName('a.catid'), $categoryId); - } - } - } - - // Filter by author - $authorId = $this->getState('filter.author_id'); - $authorWhere = ''; - - if (is_numeric($authorId)) - { - $authorId = (int) $authorId; - $type = $this->getState('filter.author_id.include', true) ? ' = ' : ' <> '; - $authorWhere = $db->quoteName('a.created_by') . $type . ':authorId'; - $query->bind(':authorId', $authorId, ParameterType::INTEGER); - } - elseif (is_array($authorId)) - { - $authorId = array_values(array_filter($authorId, 'is_numeric')); - - if ($authorId) - { - $type = $this->getState('filter.author_id.include', true) ? ' IN' : ' NOT IN'; - $authorWhere = $db->quoteName('a.created_by') . $type . ' (' . implode(',', $query->bindArray($authorId)) . ')'; - } - } - - // Filter by author alias - $authorAlias = $this->getState('filter.author_alias'); - $authorAliasWhere = ''; - - if (is_string($authorAlias)) - { - $type = $this->getState('filter.author_alias.include', true) ? ' = ' : ' <> '; - $authorAliasWhere = $db->quoteName('a.created_by_alias') . $type . ':authorAlias'; - $query->bind(':authorAlias', $authorAlias); - } - elseif (\is_array($authorAlias) && !empty($authorAlias)) - { - $type = $this->getState('filter.author_alias.include', true) ? ' IN' : ' NOT IN'; - $authorAliasWhere = $db->quoteName('a.created_by_alias') . $type - . ' (' . implode(',', $query->bindArray($authorAlias, ParameterType::STRING)) . ')'; - } - - if (!empty($authorWhere) && !empty($authorAliasWhere)) - { - $query->where('(' . $authorWhere . ' OR ' . $authorAliasWhere . ')'); - } - elseif (!empty($authorWhere) || !empty($authorAliasWhere)) - { - // One of these is empty, the other is not so we just add both - $query->where($authorWhere . $authorAliasWhere); - } - - // Filter by start and end dates. - if ((!$user->authorise('core.edit.state', 'com_content')) && (!$user->authorise('core.edit', 'com_content'))) - { - $query->where( - [ - '(' . $db->quoteName('a.publish_up') . ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publishUp)', - '(' . $db->quoteName('a.publish_down') . ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publishDown)', - ] - ) - ->bind(':publishUp', $nowDate) - ->bind(':publishDown', $nowDate); - } - - // Filter by Date Range or Relative Date - $dateFiltering = $this->getState('filter.date_filtering', 'off'); - $dateField = $db->escape($this->getState('filter.date_field', 'a.created')); - - switch ($dateFiltering) - { - case 'range': - $startDateRange = $this->getState('filter.start_date_range', ''); - $endDateRange = $this->getState('filter.end_date_range', ''); - - if ($startDateRange || $endDateRange) - { - $query->where($db->quoteName($dateField) . ' IS NOT NULL'); - - if ($startDateRange) - { - $query->where($db->quoteName($dateField) . ' >= :startDateRange') - ->bind(':startDateRange', $startDateRange); - } - - if ($endDateRange) - { - $query->where($db->quoteName($dateField) . ' <= :endDateRange') - ->bind(':endDateRange', $endDateRange); - } - } - - break; - - case 'relative': - $relativeDate = (int) $this->getState('filter.relative_date', 0); - $query->where( - $db->quoteName($dateField) . ' IS NOT NULL AND ' - . $db->quoteName($dateField) . ' >= ' . $query->dateAdd($db->quote($nowDate), -1 * $relativeDate, 'DAY') - ); - break; - - case 'off': - default: - break; - } - - // Process the filter for list views with user-entered filters - if (is_object($params) && ($params->get('filter_field') !== 'hide') && ($filter = $this->getState('list.filter'))) - { - // Clean filter variable - $filter = StringHelper::strtolower($filter); - $monthFilter = $filter; - $hitsFilter = (int) $filter; - $textFilter = '%' . $filter . '%'; - - switch ($params->get('filter_field')) - { - case 'author': - $query->where( - 'LOWER(CASE WHEN ' . $db->quoteName('a.created_by_alias') . ' > ' . $db->quote(' ') - . ' THEN ' . $db->quoteName('a.created_by_alias') . ' ELSE ' . $db->quoteName('ua.name') . ' END) LIKE :search' - ) - ->bind(':search', $textFilter); - break; - - case 'hits': - $query->where($db->quoteName('a.hits') . ' >= :hits') - ->bind(':hits', $hitsFilter, ParameterType::INTEGER); - break; - - case 'month': - if ($monthFilter != '') - { - $monthStart = date("Y-m-d", strtotime($monthFilter)) . ' 00:00:00'; - $monthEnd = date("Y-m-t", strtotime($monthFilter)) . ' 23:59:59'; - - $query->where( - [ - ':monthStart <= CASE WHEN a.publish_up IS NULL THEN a.created ELSE a.publish_up END', - ':monthEnd >= CASE WHEN a.publish_up IS NULL THEN a.created ELSE a.publish_up END', - ] - ) - ->bind(':monthStart', $monthStart) - ->bind(':monthEnd', $monthEnd); - } - break; - - case 'title': - default: - // Default to 'title' if parameter is not valid - $query->where('LOWER(' . $db->quoteName('a.title') . ') LIKE :search') - ->bind(':search', $textFilter); - break; - } - } - - // Filter by language - if ($this->getState('filter.language')) - { - $query->whereIn($db->quoteName('a.language'), [Factory::getApplication()->getLanguage()->getTag(), '*'], ParameterType::STRING); - } - - // Filter by a single or group of tags. - $tagId = $this->getState('filter.tag'); - - if (is_array($tagId) && count($tagId) === 1) - { - $tagId = current($tagId); - } - - if (is_array($tagId)) - { - $tagId = ArrayHelper::toInteger($tagId); - - if ($tagId) - { - $subQuery = $db->getQuery(true) - ->select('DISTINCT ' . $db->quoteName('content_item_id')) - ->from($db->quoteName('#__contentitem_tag_map')) - ->where( - [ - $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tagId)) . ')', - $db->quoteName('type_alias') . ' = ' . $db->quote('com_content.article'), - ] - ); - - $query->join( - 'INNER', - '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - ); - } - } - elseif ($tagId = (int) $tagId) - { - $query->join( - 'INNER', - $db->quoteName('#__contentitem_tag_map', 'tagmap'), - $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') - . ' AND ' . $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_content.article') - ) - ->where($db->quoteName('tagmap.tag_id') . ' = :tagId') - ->bind(':tagId', $tagId, ParameterType::INTEGER); - } - - // Add the list ordering clause. - $query->order( - $db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) - ); - - return $query; - } - - /** - * Method to get a list of articles. - * - * Overridden to inject convert the attribs field into a Registry object. - * - * @return mixed An array of objects on success, false on failure. - * - * @since 1.6 - */ - public function getItems() - { - $items = parent::getItems(); - - $user = Factory::getUser(); - $userId = $user->get('id'); - $guest = $user->get('guest'); - $groups = $user->getAuthorisedViewLevels(); - $input = Factory::getApplication()->input; - - // Get the global params - $globalParams = ComponentHelper::getParams('com_content', true); - - $taggedItems = []; - - // Convert the parameter fields into objects. - foreach ($items as $item) - { - $articleParams = new Registry($item->attribs); - - // Unpack readmore and layout params - $item->alternative_readmore = $articleParams->get('alternative_readmore'); - $item->layout = $articleParams->get('layout'); - - $item->params = clone $this->getState('params'); - - /** - * For blogs, article params override menu item params only if menu param = 'use_article' - * Otherwise, menu item params control the layout - * If menu item is 'use_article' and there is no article param, use global - */ - if (($input->getString('layout') === 'blog') || ($input->getString('view') === 'featured') - || ($this->getState('params')->get('layout_type') === 'blog')) - { - // Create an array of just the params set to 'use_article' - $menuParamsArray = $this->getState('params')->toArray(); - $articleArray = array(); - - foreach ($menuParamsArray as $key => $value) - { - if ($value === 'use_article') - { - // If the article has a value, use it - if ($articleParams->get($key) != '') - { - // Get the value from the article - $articleArray[$key] = $articleParams->get($key); - } - else - { - // Otherwise, use the global value - $articleArray[$key] = $globalParams->get($key); - } - } - } - - // Merge the selected article params - if (count($articleArray) > 0) - { - $articleParams = new Registry($articleArray); - $item->params->merge($articleParams); - } - } - else - { - // For non-blog layouts, merge all of the article params - $item->params->merge($articleParams); - } - - // Get display date - switch ($item->params->get('list_show_date')) - { - case 'modified': - $item->displayDate = $item->modified; - break; - - case 'published': - $item->displayDate = ($item->publish_up == 0) ? $item->created : $item->publish_up; - break; - - default: - case 'created': - $item->displayDate = $item->created; - break; - } - - /** - * Compute the asset access permissions. - * Technically guest could edit an article, but lets not check that to improve performance a little. - */ - if (!$guest) - { - $asset = 'com_content.article.' . $item->id; - - // Check general edit permission first. - if ($user->authorise('core.edit', $asset)) - { - $item->params->set('access-edit', true); - } - - // Now check if edit.own is available. - elseif (!empty($userId) && $user->authorise('core.edit.own', $asset)) - { - // Check for a valid user and that they are the owner. - if ($userId == $item->created_by) - { - $item->params->set('access-edit', true); - } - } - } - - $access = $this->getState('filter.access'); - - if ($access) - { - // If the access filter has been set, we already have only the articles this user can view. - $item->params->set('access-view', true); - } - else - { - // If no access filter is set, the layout takes some responsibility for display of limited information. - if ($item->catid == 0 || $item->category_access === null) - { - $item->params->set('access-view', in_array($item->access, $groups)); - } - else - { - $item->params->set('access-view', in_array($item->access, $groups) && in_array($item->category_access, $groups)); - } - } - - // Some contexts may not use tags data at all, so we allow callers to disable loading tag data - if ($this->getState('load_tags', $item->params->get('show_tags', '1'))) - { - $item->tags = new TagsHelper; - $taggedItems[$item->id] = $item; - } - - if (Associations::isEnabled() && $item->params->get('show_associations')) - { - $item->associations = AssociationHelper::displayAssociations($item->id); - } - } - - // Load tags of all items. - if ($taggedItems) - { - $tagsHelper = new TagsHelper; - $itemIds = \array_keys($taggedItems); - - foreach ($tagsHelper->getMultipleItemTags('com_content.article', $itemIds) as $id => $tags) - { - $taggedItems[$id]->tags->itemTags = $tags; - } - } - - return $items; - } - - /** - * Method to get the starting number of items for the data set. - * - * @return integer The starting number of items available in the data set. - * - * @since 3.0.1 - */ - public function getStart() - { - return $this->getState('list.start'); - } - - /** - * Count Items by Month - * - * @return mixed An array of objects on success, false on failure. - * - * @since 3.9.0 - */ - public function countItemsByMonth() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Get the list query. - $listQuery = $this->getListQuery(); - $bounded = $listQuery->getBounded(); - - // Bind list query variables to our new query. - $keys = array_keys($bounded); - $values = array_column($bounded, 'value'); - $dataTypes = array_column($bounded, 'dataType'); - - $query->bind($keys, $values, $dataTypes); - - $query - ->select( - 'DATE(' . - $query->concatenate( - array( - $query->year($db->quoteName('publish_up')), - $db->quote('-'), - $query->month($db->quoteName('publish_up')), - $db->quote('-01') - ) - ) . ') AS ' . $db->quoteName('d') - ) - ->select('COUNT(*) AS ' . $db->quoteName('c')) - ->from('(' . $this->getListQuery() . ') AS ' . $db->quoteName('b')) - ->group($db->quoteName('d')) - ->order($db->quoteName('d') . ' DESC'); - - return $db->setQuery($query)->loadObjectList(); - } + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @see \JController + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'alias', 'a.alias', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'catid', 'a.catid', 'category_title', + 'state', 'a.state', + 'access', 'a.access', 'access_level', + 'created', 'a.created', + 'created_by', 'a.created_by', + 'ordering', 'a.ordering', + 'featured', 'a.featured', + 'language', 'a.language', + 'hits', 'a.hits', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'images', 'a.images', + 'urls', 'a.urls', + 'filter_tag', + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * This method should only be called once per instantiation and is designed + * to be called on the first call to the getState() method unless the model + * configuration flag to ignore the request is set. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.0.1 + */ + protected function populateState($ordering = 'ordering', $direction = 'ASC') + { + $app = Factory::getApplication(); + + // List state information + $value = $app->input->get('limit', $app->get('list_limit', 0), 'uint'); + $this->setState('list.limit', $value); + + $value = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.start', $value); + + $value = $app->input->get('filter_tag', 0, 'uint'); + $this->setState('filter.tag', $value); + + $orderCol = $app->input->get('filter_order', 'a.ordering'); + + if (!in_array($orderCol, $this->filter_fields)) { + $orderCol = 'a.ordering'; + } + + $this->setState('list.ordering', $orderCol); + + $listOrder = $app->input->get('filter_order_Dir', 'ASC'); + + if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) { + $listOrder = 'ASC'; + } + + $this->setState('list.direction', $listOrder); + + $params = $app->getParams(); + $this->setState('params', $params); + + $user = Factory::getUser(); + + if ((!$user->authorise('core.edit.state', 'com_content')) && (!$user->authorise('core.edit', 'com_content'))) { + // Filter on published for those who do not have edit or edit.state rights. + $this->setState('filter.published', ContentComponent::CONDITION_PUBLISHED); + } + + $this->setState('filter.language', Multilanguage::isEnabled()); + + // Process show_noauth parameter + if ((!$params->get('show_noauth')) || (!ComponentHelper::getParams('com_content')->get('show_noauth'))) { + $this->setState('filter.access', true); + } else { + $this->setState('filter.access', false); + } + + $this->setState('layout', $app->input->getString('layout')); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . serialize($this->getState('filter.published')); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.featured'); + $id .= ':' . serialize($this->getState('filter.article_id')); + $id .= ':' . $this->getState('filter.article_id.include'); + $id .= ':' . serialize($this->getState('filter.category_id')); + $id .= ':' . $this->getState('filter.category_id.include'); + $id .= ':' . serialize($this->getState('filter.author_id')); + $id .= ':' . $this->getState('filter.author_id.include'); + $id .= ':' . serialize($this->getState('filter.author_alias')); + $id .= ':' . $this->getState('filter.author_alias.include'); + $id .= ':' . $this->getState('filter.date_filtering'); + $id .= ':' . $this->getState('filter.date_field'); + $id .= ':' . $this->getState('filter.start_date_range'); + $id .= ':' . $this->getState('filter.end_date_range'); + $id .= ':' . $this->getState('filter.relative_date'); + $id .= ':' . serialize($this->getState('filter.tag')); + + return parent::getStoreId($id); + } + + /** + * Get the master query for retrieving a list of articles subject to the model state. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + $user = Factory::getUser(); + + // Create a new query object. + $db = $this->getDatabase(); + + /** @var \Joomla\Database\DatabaseQuery $query */ + $query = $db->getQuery(true); + + $nowDate = Factory::getDate()->toSql(); + + $conditionArchived = ContentComponent::CONDITION_ARCHIVED; + $conditionUnpublished = ContentComponent::CONDITION_UNPUBLISHED; + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + [ + $db->quoteName('a.id'), + $db->quoteName('a.title'), + $db->quoteName('a.alias'), + $db->quoteName('a.introtext'), + $db->quoteName('a.fulltext'), + $db->quoteName('a.checked_out'), + $db->quoteName('a.checked_out_time'), + $db->quoteName('a.catid'), + $db->quoteName('a.created'), + $db->quoteName('a.created_by'), + $db->quoteName('a.created_by_alias'), + $db->quoteName('a.modified'), + $db->quoteName('a.modified_by'), + // Use created if publish_up is null + 'CASE WHEN ' . $db->quoteName('a.publish_up') . ' IS NULL THEN ' . $db->quoteName('a.created') + . ' ELSE ' . $db->quoteName('a.publish_up') . ' END AS ' . $db->quoteName('publish_up'), + $db->quoteName('a.publish_down'), + $db->quoteName('a.images'), + $db->quoteName('a.urls'), + $db->quoteName('a.attribs'), + $db->quoteName('a.metadata'), + $db->quoteName('a.metakey'), + $db->quoteName('a.metadesc'), + $db->quoteName('a.access'), + $db->quoteName('a.hits'), + $db->quoteName('a.featured'), + $db->quoteName('a.language'), + $query->length($db->quoteName('a.fulltext')) . ' AS ' . $db->quoteName('readmore'), + $db->quoteName('a.ordering'), + ] + ) + ) + ->select( + [ + $db->quoteName('fp.featured_up'), + $db->quoteName('fp.featured_down'), + // Published/archived article in archived category is treated as archived article. If category is not published then force 0. + 'CASE WHEN ' . $db->quoteName('c.published') . ' = 2 AND ' . $db->quoteName('a.state') . ' > 0 THEN ' . $conditionArchived + . ' WHEN ' . $db->quoteName('c.published') . ' != 1 THEN ' . $conditionUnpublished + . ' ELSE ' . $db->quoteName('a.state') . ' END AS ' . $db->quoteName('state'), + $db->quoteName('c.title', 'category_title'), + $db->quoteName('c.path', 'category_route'), + $db->quoteName('c.access', 'category_access'), + $db->quoteName('c.alias', 'category_alias'), + $db->quoteName('c.language', 'category_language'), + $db->quoteName('c.published'), + $db->quoteName('c.published', 'parents_published'), + $db->quoteName('c.lft'), + 'CASE WHEN ' . $db->quoteName('a.created_by_alias') . ' > ' . $db->quote(' ') . ' THEN ' . $db->quoteName('a.created_by_alias') + . ' ELSE ' . $db->quoteName('ua.name') . ' END AS ' . $db->quoteName('author'), + $db->quoteName('ua.email', 'author_email'), + $db->quoteName('uam.name', 'modified_by_name'), + $db->quoteName('parent.title', 'parent_title'), + $db->quoteName('parent.id', 'parent_id'), + $db->quoteName('parent.path', 'parent_route'), + $db->quoteName('parent.alias', 'parent_alias'), + $db->quoteName('parent.language', 'parent_language'), + ] + ) + ->from($db->quoteName('#__content', 'a')) + ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) + ->join('LEFT', $db->quoteName('#__users', 'ua'), $db->quoteName('ua.id') . ' = ' . $db->quoteName('a.created_by')) + ->join('LEFT', $db->quoteName('#__users', 'uam'), $db->quoteName('uam.id') . ' = ' . $db->quoteName('a.modified_by')) + ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')); + + $params = $this->getState('params'); + $orderby_sec = $params->get('orderby_sec'); + + // Join over the frontpage articles if required. + $frontpageJoin = 'LEFT'; + + if ($this->getState('filter.frontpage')) { + if ($orderby_sec === 'front') { + $query->select($db->quoteName('fp.ordering')); + $frontpageJoin = 'INNER'; + } else { + $query->where($db->quoteName('a.featured') . ' = 1'); + } + + $query->where( + [ + '(' . $db->quoteName('fp.featured_up') . ' IS NULL OR ' . $db->quoteName('fp.featured_up') . ' <= :frontpageUp)', + '(' . $db->quoteName('fp.featured_down') . ' IS NULL OR ' . $db->quoteName('fp.featured_down') . ' >= :frontpageDown)', + ] + ) + ->bind(':frontpageUp', $nowDate) + ->bind(':frontpageDown', $nowDate); + } elseif ($orderby_sec === 'front' || $this->getState('list.ordering') === 'fp.ordering') { + $query->select($db->quoteName('fp.ordering')); + } + + $query->join($frontpageJoin, $db->quoteName('#__content_frontpage', 'fp'), $db->quoteName('fp.content_id') . ' = ' . $db->quoteName('a.id')); + + if (PluginHelper::isEnabled('content', 'vote')) { + // Join on voting table + $query->select( + [ + 'COALESCE(NULLIF(ROUND(' . $db->quoteName('v.rating_sum') . ' / ' . $db->quoteName('v.rating_count') . ', 1), 0), 0)' + . ' AS ' . $db->quoteName('rating'), + 'COALESCE(NULLIF(' . $db->quoteName('v.rating_count') . ', 0), 0) AS ' . $db->quoteName('rating_count'), + ] + ) + ->join('LEFT', $db->quoteName('#__content_rating', 'v'), $db->quoteName('a.id') . ' = ' . $db->quoteName('v.content_id')); + } + + // Filter by access level. + if ($this->getState('filter.access', true)) { + $groups = $this->getState('filter.viewlevels', $user->getAuthorisedViewLevels()); + $query->whereIn($db->quoteName('a.access'), $groups) + ->whereIn($db->quoteName('c.access'), $groups); + } + + // Filter by published state + $condition = $this->getState('filter.published'); + + if (is_numeric($condition) && $condition == 2) { + /** + * If category is archived then article has to be published or archived. + * Or category is published then article has to be archived. + */ + $query->where('((' . $db->quoteName('c.published') . ' = 2 AND ' . $db->quoteName('a.state') . ' > :conditionUnpublished)' + . ' OR (' . $db->quoteName('c.published') . ' = 1 AND ' . $db->quoteName('a.state') . ' = :conditionArchived))') + ->bind(':conditionUnpublished', $conditionUnpublished, ParameterType::INTEGER) + ->bind(':conditionArchived', $conditionArchived, ParameterType::INTEGER); + } elseif (is_numeric($condition)) { + $condition = (int) $condition; + + // Category has to be published + $query->where($db->quoteName('c.published') . ' = 1 AND ' . $db->quoteName('a.state') . ' = :condition') + ->bind(':condition', $condition, ParameterType::INTEGER); + } elseif (is_array($condition)) { + // Category has to be published + $query->where( + $db->quoteName('c.published') . ' = 1 AND ' . $db->quoteName('a.state') + . ' IN (' . implode(',', $query->bindArray($condition)) . ')' + ); + } + + // Filter by featured state + $featured = $this->getState('filter.featured'); + + switch ($featured) { + case 'hide': + $query->where($db->quoteName('a.featured') . ' = 0'); + break; + + case 'only': + $query->where( + [ + $db->quoteName('a.featured') . ' = 1', + '(' . $db->quoteName('fp.featured_up') . ' IS NULL OR ' . $db->quoteName('fp.featured_up') . ' <= :featuredUp)', + '(' . $db->quoteName('fp.featured_down') . ' IS NULL OR ' . $db->quoteName('fp.featured_down') . ' >= :featuredDown)', + ] + ) + ->bind(':featuredUp', $nowDate) + ->bind(':featuredDown', $nowDate); + break; + + case 'show': + default: + // Normally we do not discriminate between featured/unfeatured items. + break; + } + + // Filter by a single or group of articles. + $articleId = $this->getState('filter.article_id'); + + if (is_numeric($articleId)) { + $articleId = (int) $articleId; + $type = $this->getState('filter.article_id.include', true) ? ' = ' : ' <> '; + $query->where($db->quoteName('a.id') . $type . ':articleId') + ->bind(':articleId', $articleId, ParameterType::INTEGER); + } elseif (is_array($articleId)) { + $articleId = ArrayHelper::toInteger($articleId); + + if ($this->getState('filter.article_id.include', true)) { + $query->whereIn($db->quoteName('a.id'), $articleId); + } else { + $query->whereNotIn($db->quoteName('a.id'), $articleId); + } + } + + // Filter by a single or group of categories + $categoryId = $this->getState('filter.category_id'); + + if (is_numeric($categoryId)) { + $type = $this->getState('filter.category_id.include', true) ? ' = ' : ' <> '; + + // Add subcategory check + $includeSubcategories = $this->getState('filter.subcategories', false); + + if ($includeSubcategories) { + $categoryId = (int) $categoryId; + $levels = (int) $this->getState('filter.max_category_levels', 1); + + // Create a subquery for the subcategory list + $subQuery = $db->getQuery(true) + ->select($db->quoteName('sub.id')) + ->from($db->quoteName('#__categories', 'sub')) + ->join( + 'INNER', + $db->quoteName('#__categories', 'this'), + $db->quoteName('sub.lft') . ' > ' . $db->quoteName('this.lft') + . ' AND ' . $db->quoteName('sub.rgt') . ' < ' . $db->quoteName('this.rgt') + ) + ->where($db->quoteName('this.id') . ' = :subCategoryId'); + + $query->bind(':subCategoryId', $categoryId, ParameterType::INTEGER); + + if ($levels >= 0) { + $subQuery->where($db->quoteName('sub.level') . ' <= ' . $db->quoteName('this.level') . ' + :levels'); + $query->bind(':levels', $levels, ParameterType::INTEGER); + } + + // Add the subquery to the main query + $query->where( + '(' . $db->quoteName('a.catid') . $type . ':categoryId OR ' . $db->quoteName('a.catid') . ' IN (' . $subQuery . '))' + ); + $query->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } else { + $query->where($db->quoteName('a.catid') . $type . ':categoryId'); + $query->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } + } elseif (is_array($categoryId) && (count($categoryId) > 0)) { + $categoryId = ArrayHelper::toInteger($categoryId); + + if (!empty($categoryId)) { + if ($this->getState('filter.category_id.include', true)) { + $query->whereIn($db->quoteName('a.catid'), $categoryId); + } else { + $query->whereNotIn($db->quoteName('a.catid'), $categoryId); + } + } + } + + // Filter by author + $authorId = $this->getState('filter.author_id'); + $authorWhere = ''; + + if (is_numeric($authorId)) { + $authorId = (int) $authorId; + $type = $this->getState('filter.author_id.include', true) ? ' = ' : ' <> '; + $authorWhere = $db->quoteName('a.created_by') . $type . ':authorId'; + $query->bind(':authorId', $authorId, ParameterType::INTEGER); + } elseif (is_array($authorId)) { + $authorId = array_values(array_filter($authorId, 'is_numeric')); + + if ($authorId) { + $type = $this->getState('filter.author_id.include', true) ? ' IN' : ' NOT IN'; + $authorWhere = $db->quoteName('a.created_by') . $type . ' (' . implode(',', $query->bindArray($authorId)) . ')'; + } + } + + // Filter by author alias + $authorAlias = $this->getState('filter.author_alias'); + $authorAliasWhere = ''; + + if (is_string($authorAlias)) { + $type = $this->getState('filter.author_alias.include', true) ? ' = ' : ' <> '; + $authorAliasWhere = $db->quoteName('a.created_by_alias') . $type . ':authorAlias'; + $query->bind(':authorAlias', $authorAlias); + } elseif (\is_array($authorAlias) && !empty($authorAlias)) { + $type = $this->getState('filter.author_alias.include', true) ? ' IN' : ' NOT IN'; + $authorAliasWhere = $db->quoteName('a.created_by_alias') . $type + . ' (' . implode(',', $query->bindArray($authorAlias, ParameterType::STRING)) . ')'; + } + + if (!empty($authorWhere) && !empty($authorAliasWhere)) { + $query->where('(' . $authorWhere . ' OR ' . $authorAliasWhere . ')'); + } elseif (!empty($authorWhere) || !empty($authorAliasWhere)) { + // One of these is empty, the other is not so we just add both + $query->where($authorWhere . $authorAliasWhere); + } + + // Filter by start and end dates. + if ((!$user->authorise('core.edit.state', 'com_content')) && (!$user->authorise('core.edit', 'com_content'))) { + $query->where( + [ + '(' . $db->quoteName('a.publish_up') . ' IS NULL OR ' . $db->quoteName('a.publish_up') . ' <= :publishUp)', + '(' . $db->quoteName('a.publish_down') . ' IS NULL OR ' . $db->quoteName('a.publish_down') . ' >= :publishDown)', + ] + ) + ->bind(':publishUp', $nowDate) + ->bind(':publishDown', $nowDate); + } + + // Filter by Date Range or Relative Date + $dateFiltering = $this->getState('filter.date_filtering', 'off'); + $dateField = $db->escape($this->getState('filter.date_field', 'a.created')); + + switch ($dateFiltering) { + case 'range': + $startDateRange = $this->getState('filter.start_date_range', ''); + $endDateRange = $this->getState('filter.end_date_range', ''); + + if ($startDateRange || $endDateRange) { + $query->where($db->quoteName($dateField) . ' IS NOT NULL'); + + if ($startDateRange) { + $query->where($db->quoteName($dateField) . ' >= :startDateRange') + ->bind(':startDateRange', $startDateRange); + } + + if ($endDateRange) { + $query->where($db->quoteName($dateField) . ' <= :endDateRange') + ->bind(':endDateRange', $endDateRange); + } + } + + break; + + case 'relative': + $relativeDate = (int) $this->getState('filter.relative_date', 0); + $query->where( + $db->quoteName($dateField) . ' IS NOT NULL AND ' + . $db->quoteName($dateField) . ' >= ' . $query->dateAdd($db->quote($nowDate), -1 * $relativeDate, 'DAY') + ); + break; + + case 'off': + default: + break; + } + + // Process the filter for list views with user-entered filters + if (is_object($params) && ($params->get('filter_field') !== 'hide') && ($filter = $this->getState('list.filter'))) { + // Clean filter variable + $filter = StringHelper::strtolower($filter); + $monthFilter = $filter; + $hitsFilter = (int) $filter; + $textFilter = '%' . $filter . '%'; + + switch ($params->get('filter_field')) { + case 'author': + $query->where( + 'LOWER(CASE WHEN ' . $db->quoteName('a.created_by_alias') . ' > ' . $db->quote(' ') + . ' THEN ' . $db->quoteName('a.created_by_alias') . ' ELSE ' . $db->quoteName('ua.name') . ' END) LIKE :search' + ) + ->bind(':search', $textFilter); + break; + + case 'hits': + $query->where($db->quoteName('a.hits') . ' >= :hits') + ->bind(':hits', $hitsFilter, ParameterType::INTEGER); + break; + + case 'month': + if ($monthFilter != '') { + $monthStart = date("Y-m-d", strtotime($monthFilter)) . ' 00:00:00'; + $monthEnd = date("Y-m-t", strtotime($monthFilter)) . ' 23:59:59'; + + $query->where( + [ + ':monthStart <= CASE WHEN a.publish_up IS NULL THEN a.created ELSE a.publish_up END', + ':monthEnd >= CASE WHEN a.publish_up IS NULL THEN a.created ELSE a.publish_up END', + ] + ) + ->bind(':monthStart', $monthStart) + ->bind(':monthEnd', $monthEnd); + } + break; + + case 'title': + default: + // Default to 'title' if parameter is not valid + $query->where('LOWER(' . $db->quoteName('a.title') . ') LIKE :search') + ->bind(':search', $textFilter); + break; + } + } + + // Filter by language + if ($this->getState('filter.language')) { + $query->whereIn($db->quoteName('a.language'), [Factory::getApplication()->getLanguage()->getTag(), '*'], ParameterType::STRING); + } + + // Filter by a single or group of tags. + $tagId = $this->getState('filter.tag'); + + if (is_array($tagId) && count($tagId) === 1) { + $tagId = current($tagId); + } + + if (is_array($tagId)) { + $tagId = ArrayHelper::toInteger($tagId); + + if ($tagId) { + $subQuery = $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('content_item_id')) + ->from($db->quoteName('#__contentitem_tag_map')) + ->where( + [ + $db->quoteName('tag_id') . ' IN (' . implode(',', $query->bindArray($tagId)) . ')', + $db->quoteName('type_alias') . ' = ' . $db->quote('com_content.article'), + ] + ); + + $query->join( + 'INNER', + '(' . $subQuery . ') AS ' . $db->quoteName('tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + ); + } + } elseif ($tagId = (int) $tagId) { + $query->join( + 'INNER', + $db->quoteName('#__contentitem_tag_map', 'tagmap'), + $db->quoteName('tagmap.content_item_id') . ' = ' . $db->quoteName('a.id') + . ' AND ' . $db->quoteName('tagmap.type_alias') . ' = ' . $db->quote('com_content.article') + ) + ->where($db->quoteName('tagmap.tag_id') . ' = :tagId') + ->bind(':tagId', $tagId, ParameterType::INTEGER); + } + + // Add the list ordering clause. + $query->order( + $db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC')) + ); + + return $query; + } + + /** + * Method to get a list of articles. + * + * Overridden to inject convert the attribs field into a Registry object. + * + * @return mixed An array of objects on success, false on failure. + * + * @since 1.6 + */ + public function getItems() + { + $items = parent::getItems(); + + $user = Factory::getUser(); + $userId = $user->get('id'); + $guest = $user->get('guest'); + $groups = $user->getAuthorisedViewLevels(); + $input = Factory::getApplication()->input; + + // Get the global params + $globalParams = ComponentHelper::getParams('com_content', true); + + $taggedItems = []; + + // Convert the parameter fields into objects. + foreach ($items as $item) { + $articleParams = new Registry($item->attribs); + + // Unpack readmore and layout params + $item->alternative_readmore = $articleParams->get('alternative_readmore'); + $item->layout = $articleParams->get('layout'); + + $item->params = clone $this->getState('params'); + + /** + * For blogs, article params override menu item params only if menu param = 'use_article' + * Otherwise, menu item params control the layout + * If menu item is 'use_article' and there is no article param, use global + */ + if ( + ($input->getString('layout') === 'blog') || ($input->getString('view') === 'featured') + || ($this->getState('params')->get('layout_type') === 'blog') + ) { + // Create an array of just the params set to 'use_article' + $menuParamsArray = $this->getState('params')->toArray(); + $articleArray = array(); + + foreach ($menuParamsArray as $key => $value) { + if ($value === 'use_article') { + // If the article has a value, use it + if ($articleParams->get($key) != '') { + // Get the value from the article + $articleArray[$key] = $articleParams->get($key); + } else { + // Otherwise, use the global value + $articleArray[$key] = $globalParams->get($key); + } + } + } + + // Merge the selected article params + if (count($articleArray) > 0) { + $articleParams = new Registry($articleArray); + $item->params->merge($articleParams); + } + } else { + // For non-blog layouts, merge all of the article params + $item->params->merge($articleParams); + } + + // Get display date + switch ($item->params->get('list_show_date')) { + case 'modified': + $item->displayDate = $item->modified; + break; + + case 'published': + $item->displayDate = ($item->publish_up == 0) ? $item->created : $item->publish_up; + break; + + default: + case 'created': + $item->displayDate = $item->created; + break; + } + + /** + * Compute the asset access permissions. + * Technically guest could edit an article, but lets not check that to improve performance a little. + */ + if (!$guest) { + $asset = 'com_content.article.' . $item->id; + + // Check general edit permission first. + if ($user->authorise('core.edit', $asset)) { + $item->params->set('access-edit', true); + } elseif (!empty($userId) && $user->authorise('core.edit.own', $asset)) { + // Now check if edit.own is available. + // Check for a valid user and that they are the owner. + if ($userId == $item->created_by) { + $item->params->set('access-edit', true); + } + } + } + + $access = $this->getState('filter.access'); + + if ($access) { + // If the access filter has been set, we already have only the articles this user can view. + $item->params->set('access-view', true); + } else { + // If no access filter is set, the layout takes some responsibility for display of limited information. + if ($item->catid == 0 || $item->category_access === null) { + $item->params->set('access-view', in_array($item->access, $groups)); + } else { + $item->params->set('access-view', in_array($item->access, $groups) && in_array($item->category_access, $groups)); + } + } + + // Some contexts may not use tags data at all, so we allow callers to disable loading tag data + if ($this->getState('load_tags', $item->params->get('show_tags', '1'))) { + $item->tags = new TagsHelper(); + $taggedItems[$item->id] = $item; + } + + if (Associations::isEnabled() && $item->params->get('show_associations')) { + $item->associations = AssociationHelper::displayAssociations($item->id); + } + } + + // Load tags of all items. + if ($taggedItems) { + $tagsHelper = new TagsHelper(); + $itemIds = \array_keys($taggedItems); + + foreach ($tagsHelper->getMultipleItemTags('com_content.article', $itemIds) as $id => $tags) { + $taggedItems[$id]->tags->itemTags = $tags; + } + } + + return $items; + } + + /** + * Method to get the starting number of items for the data set. + * + * @return integer The starting number of items available in the data set. + * + * @since 3.0.1 + */ + public function getStart() + { + return $this->getState('list.start'); + } + + /** + * Count Items by Month + * + * @return mixed An array of objects on success, false on failure. + * + * @since 3.9.0 + */ + public function countItemsByMonth() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Get the list query. + $listQuery = $this->getListQuery(); + $bounded = $listQuery->getBounded(); + + // Bind list query variables to our new query. + $keys = array_keys($bounded); + $values = array_column($bounded, 'value'); + $dataTypes = array_column($bounded, 'dataType'); + + $query->bind($keys, $values, $dataTypes); + + $query + ->select( + 'DATE(' . + $query->concatenate( + array( + $query->year($db->quoteName('publish_up')), + $db->quote('-'), + $query->month($db->quoteName('publish_up')), + $db->quote('-01') + ) + ) . ') AS ' . $db->quoteName('d') + ) + ->select('COUNT(*) AS ' . $db->quoteName('c')) + ->from('(' . $this->getListQuery() . ') AS ' . $db->quoteName('b')) + ->group($db->quoteName('d')) + ->order($db->quoteName('d') . ' DESC'); + + return $db->setQuery($query)->loadObjectList(); + } } diff --git a/components/com_content/src/Model/CategoriesModel.php b/components/com_content/src/Model/CategoriesModel.php index b3a3a4ebcac42..cdb64d48077e3 100644 --- a/components/com_content/src/Model/CategoriesModel.php +++ b/components/com_content/src/Model/CategoriesModel.php @@ -1,4 +1,5 @@ setState('filter.extension', $this->_extension); - - // Get the parent id if defined. - $parentId = $app->input->getInt('id'); - $this->setState('filter.parentId', $parentId); - - $params = $app->getParams(); - $this->setState('params', $params); - - $this->setState('filter.published', 1); - $this->setState('filter.access', true); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.extension'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.parentId'); - - return parent::getStoreId($id); - } - - /** - * Redefine the function and add some properties to make the styling easier - * - * @param bool $recursive True if you want to return children recursively. - * - * @return mixed An array of data items on success, false on failure. - * - * @since 1.6 - */ - public function getItems($recursive = false) - { - $store = $this->getStoreId(); - - if (!isset($this->cache[$store])) - { - $app = Factory::getApplication(); - $menu = $app->getMenu(); - $active = $menu->getActive(); - - if ($active) - { - $params = $active->getParams(); - } - else - { - $params = new Registry; - } - - $options = array(); - $options['countItems'] = $params->get('show_cat_num_articles_cat', 1) || !$params->get('show_empty_categories_cat', 0); - $categories = Categories::getInstance('Content', $options); - $this->_parent = $categories->get($this->getState('filter.parentId', 'root')); - - if (is_object($this->_parent)) - { - $this->cache[$store] = $this->_parent->getChildren($recursive); - } - else - { - $this->cache[$store] = false; - } - } - - return $this->cache[$store]; - } - - /** - * Get the parent. - * - * @return object An array of data items on success, false on failure. - * - * @since 1.6 - */ - public function getParent() - { - if (!is_object($this->_parent)) - { - $this->getItems(); - } - - return $this->_parent; - } + /** + * Model context string. + * + * @var string + */ + public $_context = 'com_content.categories'; + + /** + * The category context (allows other extensions to derived from this model). + * + * @var string + */ + protected $_extension = 'com_content'; + + /** + * Parent category of the current one + * + * @var CategoryNode|null + */ + private $_parent = null; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering The field to order on. + * @param string $direction The direction to order on. + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + $this->setState('filter.extension', $this->_extension); + + // Get the parent id if defined. + $parentId = $app->input->getInt('id'); + $this->setState('filter.parentId', $parentId); + + $params = $app->getParams(); + $this->setState('params', $params); + + $this->setState('filter.published', 1); + $this->setState('filter.access', true); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.extension'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.parentId'); + + return parent::getStoreId($id); + } + + /** + * Redefine the function and add some properties to make the styling easier + * + * @param bool $recursive True if you want to return children recursively. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 1.6 + */ + public function getItems($recursive = false) + { + $store = $this->getStoreId(); + + if (!isset($this->cache[$store])) { + $app = Factory::getApplication(); + $menu = $app->getMenu(); + $active = $menu->getActive(); + + if ($active) { + $params = $active->getParams(); + } else { + $params = new Registry(); + } + + $options = array(); + $options['countItems'] = $params->get('show_cat_num_articles_cat', 1) || !$params->get('show_empty_categories_cat', 0); + $categories = Categories::getInstance('Content', $options); + $this->_parent = $categories->get($this->getState('filter.parentId', 'root')); + + if (is_object($this->_parent)) { + $this->cache[$store] = $this->_parent->getChildren($recursive); + } else { + $this->cache[$store] = false; + } + } + + return $this->cache[$store]; + } + + /** + * Get the parent. + * + * @return object An array of data items on success, false on failure. + * + * @since 1.6 + */ + public function getParent() + { + if (!is_object($this->_parent)) { + $this->getItems(); + } + + return $this->_parent; + } } diff --git a/components/com_content/src/Model/CategoryModel.php b/components/com_content/src/Model/CategoryModel.php index ff3c8753cd5ef..b37932e517774 100644 --- a/components/com_content/src/Model/CategoryModel.php +++ b/components/com_content/src/Model/CategoryModel.php @@ -1,4 +1,5 @@ input->getInt('id'); - - $this->setState('category.id', $pk); - - // Load the parameters. Merge Global and Menu Item params into new object - $params = $app->getParams(); - - if ($menu = $app->getMenu()->getActive()) - { - $menuParams = $menu->getParams(); - } - else - { - $menuParams = new Registry; - } - - $mergedParams = clone $menuParams; - $mergedParams->merge($params); - - $this->setState('params', $mergedParams); - $user = Factory::getUser(); - - $asset = 'com_content'; - - if ($pk) - { - $asset .= '.category.' . $pk; - } - - if ((!$user->authorise('core.edit.state', $asset)) && (!$user->authorise('core.edit', $asset))) - { - // Limit to published for people who can't edit or edit.state. - $this->setState('filter.published', 1); - } - else - { - $this->setState('filter.published', [0, 1]); - } - - // Process show_noauth parameter - if (!$params->get('show_noauth')) - { - $this->setState('filter.access', true); - } - else - { - $this->setState('filter.access', false); - } - - $itemid = $app->input->get('id', 0, 'int') . ':' . $app->input->get('Itemid', 0, 'int'); - - $value = $this->getUserStateFromRequest('com_content.category.filter.' . $itemid . '.tag', 'filter_tag', 0, 'int', false); - $this->setState('filter.tag', $value); - - // Optional filter text - $search = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter-search', 'filter-search', '', 'string'); - $this->setState('list.filter', $search); - - // Filter.order - $orderCol = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order', 'filter_order', '', 'string'); - - if (!in_array($orderCol, $this->filter_fields)) - { - $orderCol = 'a.ordering'; - } - - $this->setState('list.ordering', $orderCol); - - $listOrder = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order_Dir', 'filter_order_Dir', '', 'cmd'); - - if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) - { - $listOrder = 'ASC'; - } - - $this->setState('list.direction', $listOrder); - - $this->setState('list.start', $app->input->get('limitstart', 0, 'uint')); - - // Set limit for query. If list, use parameter. If blog, add blog parameters for limit. - if (($app->input->get('layout') === 'blog') || $params->get('layout_type') === 'blog') - { - $limit = $params->get('num_leading_articles') + $params->get('num_intro_articles') + $params->get('num_links'); - $this->setState('list.links', $params->get('num_links')); - } - else - { - $limit = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.limit', 'limit', $params->get('display_num'), 'uint'); - } - - $this->setState('list.limit', $limit); - - // Set the depth of the category query based on parameter - $showSubcategories = $params->get('show_subcategory_content', '0'); - - if ($showSubcategories) - { - $this->setState('filter.max_category_levels', $params->get('show_subcategory_content', '1')); - $this->setState('filter.subcategories', true); - } - - $this->setState('filter.language', Multilanguage::isEnabled()); - - $this->setState('layout', $app->input->getString('layout')); - - // Set the featured articles state - $this->setState('filter.featured', $params->get('show_featured')); - } - - /** - * Get the articles in the category - * - * @return array|bool An array of articles or false if an error occurs. - * - * @since 1.5 - */ - public function getItems() - { - $limit = $this->getState('list.limit'); - - if ($this->_articles === null && $category = $this->getCategory()) - { - $model = $this->bootComponent('com_content')->getMVCFactory() - ->createModel('Articles', 'Site', ['ignore_request' => true]); - $model->setState('params', Factory::getApplication()->getParams()); - $model->setState('filter.category_id', $category->id); - $model->setState('filter.published', $this->getState('filter.published')); - $model->setState('filter.access', $this->getState('filter.access')); - $model->setState('filter.language', $this->getState('filter.language')); - $model->setState('filter.featured', $this->getState('filter.featured')); - $model->setState('list.ordering', $this->_buildContentOrderBy()); - $model->setState('list.start', $this->getState('list.start')); - $model->setState('list.limit', $limit); - $model->setState('list.direction', $this->getState('list.direction')); - $model->setState('list.filter', $this->getState('list.filter')); - $model->setState('filter.tag', $this->getState('filter.tag')); - - // Filter.subcategories indicates whether to include articles from subcategories in the list or blog - $model->setState('filter.subcategories', $this->getState('filter.subcategories')); - $model->setState('filter.max_category_levels', $this->getState('filter.max_category_levels')); - $model->setState('list.links', $this->getState('list.links')); - - if ($limit >= 0) - { - $this->_articles = $model->getItems(); - - if ($this->_articles === false) - { - $this->setError($model->getError()); - } - } - else - { - $this->_articles = array(); - } - - $this->_pagination = $model->getPagination(); - } - - return $this->_articles; - } - - /** - * Build the orderby for the query - * - * @return string $orderby portion of query - * - * @since 1.5 - */ - protected function _buildContentOrderBy() - { - $app = Factory::getApplication(); - $db = $this->getDatabase(); - $params = $this->state->params; - $itemid = $app->input->get('id', 0, 'int') . ':' . $app->input->get('Itemid', 0, 'int'); - $orderCol = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order', 'filter_order', '', 'string'); - $orderDirn = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order_Dir', 'filter_order_Dir', '', 'cmd'); - $orderby = ' '; - - if (!in_array($orderCol, $this->filter_fields)) - { - $orderCol = null; - } - - if (!in_array(strtoupper($orderDirn), array('ASC', 'DESC', ''))) - { - $orderDirn = 'ASC'; - } - - if ($orderCol && $orderDirn) - { - $orderby .= $db->escape($orderCol) . ' ' . $db->escape($orderDirn) . ', '; - } - - $articleOrderby = $params->get('orderby_sec', 'rdate'); - $articleOrderDate = $params->get('order_date'); - $categoryOrderby = $params->def('orderby_pri', ''); - $secondary = QueryHelper::orderbySecondary($articleOrderby, $articleOrderDate, $this->getDatabase()) . ', '; - $primary = QueryHelper::orderbyPrimary($categoryOrderby); - - $orderby .= $primary . ' ' . $secondary . ' a.created '; - - return $orderby; - } - - /** - * Method to get a JPagination object for the data set. - * - * @return \Joomla\CMS\Pagination\Pagination A JPagination object for the data set. - * - * @since 3.0.1 - */ - public function getPagination() - { - if (empty($this->_pagination)) - { - return null; - } - - return $this->_pagination; - } - - /** - * Method to get category data for the current category - * - * @return object - * - * @since 1.5 - */ - public function getCategory() - { - if (!is_object($this->_item)) - { - if (isset($this->state->params)) - { - $params = $this->state->params; - $options = array(); - $options['countItems'] = $params->get('show_cat_num_articles', 1) || !$params->get('show_empty_categories_cat', 0); - $options['access'] = $params->get('check_access_rights', 1); - } - else - { - $options['countItems'] = 0; - } - - $categories = Categories::getInstance('Content', $options); - $this->_item = $categories->get($this->getState('category.id', 'root')); - - // Compute selected asset permissions. - if (is_object($this->_item)) - { - $user = Factory::getUser(); - $asset = 'com_content.category.' . $this->_item->id; - - // Check general create permission. - if ($user->authorise('core.create', $asset)) - { - $this->_item->getParams()->set('access-create', true); - } - - // @todo: Why aren't we lazy loading the children and siblings? - $this->_children = $this->_item->getChildren(); - $this->_parent = false; - - if ($this->_item->getParent()) - { - $this->_parent = $this->_item->getParent(); - } - - $this->_rightsibling = $this->_item->getSibling(); - $this->_leftsibling = $this->_item->getSibling(false); - } - else - { - $this->_children = false; - $this->_parent = false; - } - } - - return $this->_item; - } - - /** - * Get the parent category. - * - * @return mixed An array of categories or false if an error occurs. - * - * @since 1.6 - */ - public function getParent() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_parent; - } - - /** - * Get the left sibling (adjacent) categories. - * - * @return mixed An array of categories or false if an error occurs. - * - * @since 1.6 - */ - public function &getLeftSibling() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_leftsibling; - } - - /** - * Get the right sibling (adjacent) categories. - * - * @return mixed An array of categories or false if an error occurs. - * - * @since 1.6 - */ - public function &getRightSibling() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_rightsibling; - } - - /** - * Get the child categories. - * - * @return mixed An array of categories or false if an error occurs. - * - * @since 1.6 - */ - public function &getChildren() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - // Order subcategories - if ($this->_children) - { - $params = $this->getState()->get('params'); - - $orderByPri = $params->get('orderby_pri'); - - if ($orderByPri === 'alpha' || $orderByPri === 'ralpha') - { - $this->_children = ArrayHelper::sortObjects($this->_children, 'title', ($orderByPri === 'alpha') ? 1 : (-1)); - } - } - - return $this->_children; - } - - /** - * Increment the hit counter for the category. - * - * @param int $pk Optional primary key of the category to increment. - * - * @return boolean True if successful; false otherwise and internal error set. - */ - public function hit($pk = 0) - { - $input = Factory::getApplication()->input; - $hitcount = $input->getInt('hitcount', 1); - - if ($hitcount) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('category.id'); - - $table = Table::getInstance('Category', 'JTable'); - $table->hit($pk); - } - - return true; - } + /** + * Category items data + * + * @var array + */ + protected $_item = null; + + /** + * Array of articles in the category + * + * @var \stdClass[] + */ + protected $_articles = null; + + /** + * Category left and right of this one + * + * @var CategoryNode[]|null + */ + protected $_siblings = null; + + /** + * Array of child-categories + * + * @var CategoryNode[]|null + */ + protected $_children = null; + + /** + * Parent category of the current one + * + * @var CategoryNode|null + */ + protected $_parent = null; + + /** + * Model context string. + * + * @var string + */ + protected $_context = 'com_content.category'; + + /** + * The category that applies. + * + * @var object + */ + protected $_category = null; + + /** + * The list of categories. + * + * @var array + */ + protected $_categories = null; + + /** + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'alias', 'a.alias', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'catid', 'a.catid', 'category_title', + 'state', 'a.state', + 'access', 'a.access', 'access_level', + 'created', 'a.created', + 'created_by', 'a.created_by', + 'modified', 'a.modified', + 'ordering', 'a.ordering', + 'featured', 'a.featured', + 'language', 'a.language', + 'hits', 'a.hits', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'author', 'a.author', + 'filter_tag' + ); + } + + parent::__construct($config); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering The field to order on. + * @param string $direction The direction to order on. + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + $pk = $app->input->getInt('id'); + + $this->setState('category.id', $pk); + + // Load the parameters. Merge Global and Menu Item params into new object + $params = $app->getParams(); + + if ($menu = $app->getMenu()->getActive()) { + $menuParams = $menu->getParams(); + } else { + $menuParams = new Registry(); + } + + $mergedParams = clone $menuParams; + $mergedParams->merge($params); + + $this->setState('params', $mergedParams); + $user = Factory::getUser(); + + $asset = 'com_content'; + + if ($pk) { + $asset .= '.category.' . $pk; + } + + if ((!$user->authorise('core.edit.state', $asset)) && (!$user->authorise('core.edit', $asset))) { + // Limit to published for people who can't edit or edit.state. + $this->setState('filter.published', 1); + } else { + $this->setState('filter.published', [0, 1]); + } + + // Process show_noauth parameter + if (!$params->get('show_noauth')) { + $this->setState('filter.access', true); + } else { + $this->setState('filter.access', false); + } + + $itemid = $app->input->get('id', 0, 'int') . ':' . $app->input->get('Itemid', 0, 'int'); + + $value = $this->getUserStateFromRequest('com_content.category.filter.' . $itemid . '.tag', 'filter_tag', 0, 'int', false); + $this->setState('filter.tag', $value); + + // Optional filter text + $search = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter-search', 'filter-search', '', 'string'); + $this->setState('list.filter', $search); + + // Filter.order + $orderCol = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order', 'filter_order', '', 'string'); + + if (!in_array($orderCol, $this->filter_fields)) { + $orderCol = 'a.ordering'; + } + + $this->setState('list.ordering', $orderCol); + + $listOrder = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order_Dir', 'filter_order_Dir', '', 'cmd'); + + if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) { + $listOrder = 'ASC'; + } + + $this->setState('list.direction', $listOrder); + + $this->setState('list.start', $app->input->get('limitstart', 0, 'uint')); + + // Set limit for query. If list, use parameter. If blog, add blog parameters for limit. + if (($app->input->get('layout') === 'blog') || $params->get('layout_type') === 'blog') { + $limit = $params->get('num_leading_articles') + $params->get('num_intro_articles') + $params->get('num_links'); + $this->setState('list.links', $params->get('num_links')); + } else { + $limit = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.limit', 'limit', $params->get('display_num'), 'uint'); + } + + $this->setState('list.limit', $limit); + + // Set the depth of the category query based on parameter + $showSubcategories = $params->get('show_subcategory_content', '0'); + + if ($showSubcategories) { + $this->setState('filter.max_category_levels', $params->get('show_subcategory_content', '1')); + $this->setState('filter.subcategories', true); + } + + $this->setState('filter.language', Multilanguage::isEnabled()); + + $this->setState('layout', $app->input->getString('layout')); + + // Set the featured articles state + $this->setState('filter.featured', $params->get('show_featured')); + } + + /** + * Get the articles in the category + * + * @return array|bool An array of articles or false if an error occurs. + * + * @since 1.5 + */ + public function getItems() + { + $limit = $this->getState('list.limit'); + + if ($this->_articles === null && $category = $this->getCategory()) { + $model = $this->bootComponent('com_content')->getMVCFactory() + ->createModel('Articles', 'Site', ['ignore_request' => true]); + $model->setState('params', Factory::getApplication()->getParams()); + $model->setState('filter.category_id', $category->id); + $model->setState('filter.published', $this->getState('filter.published')); + $model->setState('filter.access', $this->getState('filter.access')); + $model->setState('filter.language', $this->getState('filter.language')); + $model->setState('filter.featured', $this->getState('filter.featured')); + $model->setState('list.ordering', $this->_buildContentOrderBy()); + $model->setState('list.start', $this->getState('list.start')); + $model->setState('list.limit', $limit); + $model->setState('list.direction', $this->getState('list.direction')); + $model->setState('list.filter', $this->getState('list.filter')); + $model->setState('filter.tag', $this->getState('filter.tag')); + + // Filter.subcategories indicates whether to include articles from subcategories in the list or blog + $model->setState('filter.subcategories', $this->getState('filter.subcategories')); + $model->setState('filter.max_category_levels', $this->getState('filter.max_category_levels')); + $model->setState('list.links', $this->getState('list.links')); + + if ($limit >= 0) { + $this->_articles = $model->getItems(); + + if ($this->_articles === false) { + $this->setError($model->getError()); + } + } else { + $this->_articles = array(); + } + + $this->_pagination = $model->getPagination(); + } + + return $this->_articles; + } + + /** + * Build the orderby for the query + * + * @return string $orderby portion of query + * + * @since 1.5 + */ + protected function _buildContentOrderBy() + { + $app = Factory::getApplication(); + $db = $this->getDatabase(); + $params = $this->state->params; + $itemid = $app->input->get('id', 0, 'int') . ':' . $app->input->get('Itemid', 0, 'int'); + $orderCol = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order', 'filter_order', '', 'string'); + $orderDirn = $app->getUserStateFromRequest('com_content.category.list.' . $itemid . '.filter_order_Dir', 'filter_order_Dir', '', 'cmd'); + $orderby = ' '; + + if (!in_array($orderCol, $this->filter_fields)) { + $orderCol = null; + } + + if (!in_array(strtoupper($orderDirn), array('ASC', 'DESC', ''))) { + $orderDirn = 'ASC'; + } + + if ($orderCol && $orderDirn) { + $orderby .= $db->escape($orderCol) . ' ' . $db->escape($orderDirn) . ', '; + } + + $articleOrderby = $params->get('orderby_sec', 'rdate'); + $articleOrderDate = $params->get('order_date'); + $categoryOrderby = $params->def('orderby_pri', ''); + $secondary = QueryHelper::orderbySecondary($articleOrderby, $articleOrderDate, $this->getDatabase()) . ', '; + $primary = QueryHelper::orderbyPrimary($categoryOrderby); + + $orderby .= $primary . ' ' . $secondary . ' a.created '; + + return $orderby; + } + + /** + * Method to get a JPagination object for the data set. + * + * @return \Joomla\CMS\Pagination\Pagination A JPagination object for the data set. + * + * @since 3.0.1 + */ + public function getPagination() + { + if (empty($this->_pagination)) { + return null; + } + + return $this->_pagination; + } + + /** + * Method to get category data for the current category + * + * @return object + * + * @since 1.5 + */ + public function getCategory() + { + if (!is_object($this->_item)) { + if (isset($this->state->params)) { + $params = $this->state->params; + $options = array(); + $options['countItems'] = $params->get('show_cat_num_articles', 1) || !$params->get('show_empty_categories_cat', 0); + $options['access'] = $params->get('check_access_rights', 1); + } else { + $options['countItems'] = 0; + } + + $categories = Categories::getInstance('Content', $options); + $this->_item = $categories->get($this->getState('category.id', 'root')); + + // Compute selected asset permissions. + if (is_object($this->_item)) { + $user = Factory::getUser(); + $asset = 'com_content.category.' . $this->_item->id; + + // Check general create permission. + if ($user->authorise('core.create', $asset)) { + $this->_item->getParams()->set('access-create', true); + } + + // @todo: Why aren't we lazy loading the children and siblings? + $this->_children = $this->_item->getChildren(); + $this->_parent = false; + + if ($this->_item->getParent()) { + $this->_parent = $this->_item->getParent(); + } + + $this->_rightsibling = $this->_item->getSibling(); + $this->_leftsibling = $this->_item->getSibling(false); + } else { + $this->_children = false; + $this->_parent = false; + } + } + + return $this->_item; + } + + /** + * Get the parent category. + * + * @return mixed An array of categories or false if an error occurs. + * + * @since 1.6 + */ + public function getParent() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_parent; + } + + /** + * Get the left sibling (adjacent) categories. + * + * @return mixed An array of categories or false if an error occurs. + * + * @since 1.6 + */ + public function &getLeftSibling() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_leftsibling; + } + + /** + * Get the right sibling (adjacent) categories. + * + * @return mixed An array of categories or false if an error occurs. + * + * @since 1.6 + */ + public function &getRightSibling() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_rightsibling; + } + + /** + * Get the child categories. + * + * @return mixed An array of categories or false if an error occurs. + * + * @since 1.6 + */ + public function &getChildren() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + // Order subcategories + if ($this->_children) { + $params = $this->getState()->get('params'); + + $orderByPri = $params->get('orderby_pri'); + + if ($orderByPri === 'alpha' || $orderByPri === 'ralpha') { + $this->_children = ArrayHelper::sortObjects($this->_children, 'title', ($orderByPri === 'alpha') ? 1 : (-1)); + } + } + + return $this->_children; + } + + /** + * Increment the hit counter for the category. + * + * @param int $pk Optional primary key of the category to increment. + * + * @return boolean True if successful; false otherwise and internal error set. + */ + public function hit($pk = 0) + { + $input = Factory::getApplication()->input; + $hitcount = $input->getInt('hitcount', 1); + + if ($hitcount) { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('category.id'); + + $table = Table::getInstance('Category', 'JTable'); + $table->hit($pk); + } + + return true; + } } diff --git a/components/com_content/src/Model/FeaturedModel.php b/components/com_content/src/Model/FeaturedModel.php index 9934bc5ce0e97..8cad54da87509 100644 --- a/components/com_content/src/Model/FeaturedModel.php +++ b/components/com_content/src/Model/FeaturedModel.php @@ -1,4 +1,5 @@ input; - $user = $app->getIdentity(); - - // List state information - $limitstart = $input->getUint('limitstart', 0); - $this->setState('list.start', $limitstart); - - $params = $this->state->params; - - if ($menu = $app->getMenu()->getActive()) - { - $menuParams = $menu->getParams(); - } - else - { - $menuParams = new Registry; - } - - $mergedParams = clone $menuParams; - $mergedParams->merge($params); - - $this->setState('params', $mergedParams); - - $limit = $params->get('num_leading_articles') + $params->get('num_intro_articles') + $params->get('num_links'); - $this->setState('list.limit', $limit); - $this->setState('list.links', $params->get('num_links')); - - $this->setState('filter.frontpage', true); - - if ((!$user->authorise('core.edit.state', 'com_content')) && (!$user->authorise('core.edit', 'com_content'))) - { - // Filter on published for those who do not have edit or edit.state rights. - $this->setState('filter.published', ContentComponent::CONDITION_PUBLISHED); - } - else - { - $this->setState('filter.published', [ContentComponent::CONDITION_UNPUBLISHED, ContentComponent::CONDITION_PUBLISHED]); - } - - // Process show_noauth parameter - if (!$params->get('show_noauth')) - { - $this->setState('filter.access', true); - } - else - { - $this->setState('filter.access', false); - } - - // Check for category selection - if ($params->get('featured_categories') && implode(',', $params->get('featured_categories')) == true) - { - $featuredCategories = $params->get('featured_categories'); - $this->setState('filter.frontpage.categories', $featuredCategories); - } - - $articleOrderby = $params->get('orderby_sec', 'rdate'); - $articleOrderDate = $params->get('order_date'); - $categoryOrderby = $params->def('orderby_pri', ''); - - $secondary = QueryHelper::orderbySecondary($articleOrderby, $articleOrderDate, $this->getDatabase()); - $primary = QueryHelper::orderbyPrimary($categoryOrderby); - - $this->setState('list.ordering', $primary . $secondary . ', a.created DESC'); - $this->setState('list.direction', ''); - } - - /** - * Method to get a list of articles. - * - * @return mixed An array of objects on success, false on failure. - */ - public function getItems() - { - $params = clone $this->getState('params'); - $limit = $params->get('num_leading_articles') + $params->get('num_intro_articles') + $params->get('num_links'); - - if ($limit > 0) - { - $this->setState('list.limit', $limit); - - return parent::getItems(); - } - - return array(); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= $this->getState('filter.frontpage'); - - return parent::getStoreId($id); - } - - /** - * Get the list of items. - * - * @return \Joomla\Database\DatabaseQuery - */ - protected function getListQuery() - { - // Create a new query object. - $query = parent::getListQuery(); - - // Filter by categories - $featuredCategories = $this->getState('filter.frontpage.categories'); - - if (is_array($featuredCategories) && !in_array('', $featuredCategories)) - { - $query->where('a.catid IN (' . implode(',', ArrayHelper::toInteger($featuredCategories)) . ')'); - } - - return $query; - } + /** + * Model context string. + * + * @var string + */ + public $_context = 'com_content.frontpage'; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering The field to order on. + * @param string $direction The direction to order on. + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + parent::populateState($ordering, $direction); + + $app = Factory::getApplication(); + $input = $app->input; + $user = $app->getIdentity(); + + // List state information + $limitstart = $input->getUint('limitstart', 0); + $this->setState('list.start', $limitstart); + + $params = $this->state->params; + + if ($menu = $app->getMenu()->getActive()) { + $menuParams = $menu->getParams(); + } else { + $menuParams = new Registry(); + } + + $mergedParams = clone $menuParams; + $mergedParams->merge($params); + + $this->setState('params', $mergedParams); + + $limit = $params->get('num_leading_articles') + $params->get('num_intro_articles') + $params->get('num_links'); + $this->setState('list.limit', $limit); + $this->setState('list.links', $params->get('num_links')); + + $this->setState('filter.frontpage', true); + + if ((!$user->authorise('core.edit.state', 'com_content')) && (!$user->authorise('core.edit', 'com_content'))) { + // Filter on published for those who do not have edit or edit.state rights. + $this->setState('filter.published', ContentComponent::CONDITION_PUBLISHED); + } else { + $this->setState('filter.published', [ContentComponent::CONDITION_UNPUBLISHED, ContentComponent::CONDITION_PUBLISHED]); + } + + // Process show_noauth parameter + if (!$params->get('show_noauth')) { + $this->setState('filter.access', true); + } else { + $this->setState('filter.access', false); + } + + // Check for category selection + if ($params->get('featured_categories') && implode(',', $params->get('featured_categories')) == true) { + $featuredCategories = $params->get('featured_categories'); + $this->setState('filter.frontpage.categories', $featuredCategories); + } + + $articleOrderby = $params->get('orderby_sec', 'rdate'); + $articleOrderDate = $params->get('order_date'); + $categoryOrderby = $params->def('orderby_pri', ''); + + $secondary = QueryHelper::orderbySecondary($articleOrderby, $articleOrderDate, $this->getDatabase()); + $primary = QueryHelper::orderbyPrimary($categoryOrderby); + + $this->setState('list.ordering', $primary . $secondary . ', a.created DESC'); + $this->setState('list.direction', ''); + } + + /** + * Method to get a list of articles. + * + * @return mixed An array of objects on success, false on failure. + */ + public function getItems() + { + $params = clone $this->getState('params'); + $limit = $params->get('num_leading_articles') + $params->get('num_intro_articles') + $params->get('num_links'); + + if ($limit > 0) { + $this->setState('list.limit', $limit); + + return parent::getItems(); + } + + return array(); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= $this->getState('filter.frontpage'); + + return parent::getStoreId($id); + } + + /** + * Get the list of items. + * + * @return \Joomla\Database\DatabaseQuery + */ + protected function getListQuery() + { + // Create a new query object. + $query = parent::getListQuery(); + + // Filter by categories + $featuredCategories = $this->getState('filter.frontpage.categories'); + + if (is_array($featuredCategories) && !in_array('', $featuredCategories)) { + $query->where('a.catid IN (' . implode(',', ArrayHelper::toInteger($featuredCategories)) . ')'); + } + + return $query; + } } diff --git a/components/com_content/src/Model/FormModel.php b/components/com_content/src/Model/FormModel.php index a5368ba1fef53..fd141fd89c339 100644 --- a/components/com_content/src/Model/FormModel.php +++ b/components/com_content/src/Model/FormModel.php @@ -1,4 +1,5 @@ getParams(); - $this->setState('params', $params); - - if ($params && $params->get('enable_category') == 1 && $params->get('catid')) - { - $catId = $params->get('catid'); - } - else - { - $catId = 0; - } - - // Load state from the request. - $pk = $app->input->getInt('a_id'); - $this->setState('article.id', $pk); - - $this->setState('article.catid', $app->input->getInt('catid', $catId)); - - $return = $app->input->get('return', '', 'base64'); - $this->setState('return_page', base64_decode($return)); - - $this->setState('layout', $app->input->getString('layout')); - } - - /** - * Method to get article data. - * - * @param integer $itemId The id of the article. - * - * @return mixed Content item data object on success, false on failure. - */ - public function getItem($itemId = null) - { - $itemId = (int) (!empty($itemId)) ? $itemId : $this->getState('article.id'); - - // Get a row instance. - $table = $this->getTable(); - - // Attempt to load the row. - $return = $table->load($itemId); - - // Check for a table object error. - if ($return === false && $table->getError()) - { - $this->setError($table->getError()); - - return false; - } - - $properties = $table->getProperties(1); - $value = ArrayHelper::toObject($properties, CMSObject::class); - - // Convert attrib field to Registry. - $value->params = new Registry($value->attribs); - - // Compute selected asset permissions. - $user = Factory::getUser(); - $userId = $user->get('id'); - $asset = 'com_content.article.' . $value->id; - - // Check general edit permission first. - if ($user->authorise('core.edit', $asset)) - { - $value->params->set('access-edit', true); - } - - // Now check if edit.own is available. - elseif (!empty($userId) && $user->authorise('core.edit.own', $asset)) - { - // Check for a valid user and that they are the owner. - if ($userId == $value->created_by) - { - $value->params->set('access-edit', true); - } - } - - // Check edit state permission. - if ($itemId) - { - // Existing item - $value->params->set('access-change', $user->authorise('core.edit.state', $asset)); - } - else - { - // New item. - $catId = (int) $this->getState('article.catid'); - - if ($catId) - { - $value->params->set('access-change', $user->authorise('core.edit.state', 'com_content.category.' . $catId)); - $value->catid = $catId; - } - else - { - $value->params->set('access-change', $user->authorise('core.edit.state', 'com_content')); - } - } - - $value->articletext = $value->introtext; - - if (!empty($value->fulltext)) - { - $value->articletext .= '
    ' . $value->fulltext; - } - - // Convert the metadata field to an array. - $registry = new Registry($value->metadata); - $value->metadata = $registry->toArray(); - - if ($itemId) - { - $value->tags = new TagsHelper; - $value->tags->getTagIds($value->id, 'com_content.article'); - $value->metadata['tags'] = $value->tags; - } - - return $value; - } - - /** - * Get the return URL. - * - * @return string The return URL. - * - * @since 1.6 - */ - public function getReturnPage() - { - return base64_encode($this->getState('return_page', '')); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return boolean True on success. - * - * @since 3.2 - */ - public function save($data) - { - // Associations are not edited in frontend ATM so we have to inherit them - if (Associations::isEnabled() && !empty($data['id']) - && $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', $data['id'])) - { - foreach ($associations as $tag => $associated) - { - $associations[$tag] = (int) $associated->id; - } - - $data['associations'] = $associations; - } - - if (!Multilanguage::isEnabled()) - { - $data['language'] = '*'; - } - - return parent::save($data); - } - - /** - * Method to get the record form. - * - * @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 Form|boolean A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = [], $loadData = true) - { - $form = parent::getForm($data, $loadData); - - if (empty($form)) - { - return false; - } - - $app = Factory::getApplication(); - $user = $app->getIdentity(); - - // On edit article, we get ID of article from article.id state, but on save, we use data from input - $id = (int) $this->getState('article.id', $app->input->getInt('a_id')); - - // Existing record. We can't edit the category in frontend if not edit.state. - if ($id > 0 && !$user->authorise('core.edit.state', 'com_content.article.' . $id)) - { - $form->setFieldAttribute('catid', 'readonly', 'true'); - $form->setFieldAttribute('catid', 'required', 'false'); - $form->setFieldAttribute('catid', 'filter', 'unset'); - } - - // Prevent messing with article language and category when editing existing article with associations - if ($this->getState('article.id') && Associations::isEnabled()) - { - $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', $id); - - // Make fields read only - if (!empty($associations)) - { - $form->setFieldAttribute('language', 'readonly', 'true'); - $form->setFieldAttribute('catid', 'readonly', 'true'); - $form->setFieldAttribute('language', 'filter', 'unset'); - $form->setFieldAttribute('catid', 'filter', 'unset'); - } - } - - return $form; - } - - /** - * Allows preprocessing of the JForm object. - * - * @param Form $form The form object - * @param array $data The data to be merged into the form object - * @param string $group The plugin group to be executed - * - * @return void - * - * @since 3.7.0 - */ - protected function preprocessForm(Form $form, $data, $group = 'content') - { - $params = $this->getState()->get('params'); - - if ($params && $params->get('enable_category') == 1 && $params->get('catid')) - { - $form->setFieldAttribute('catid', 'default', $params->get('catid')); - $form->setFieldAttribute('catid', 'readonly', 'true'); - - if (Multilanguage::isEnabled()) - { - $categoryId = (int) $params->get('catid'); - - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('language')) - ->from($db->quoteName('#__categories')) - ->where($db->quoteName('id') . ' = :categoryId') - ->bind(':categoryId', $categoryId, ParameterType::INTEGER); - $db->setQuery($query); - - $result = $db->loadResult(); - - if ($result != '*') - { - $form->setFieldAttribute('language', 'readonly', 'true'); - $form->setFieldAttribute('language', 'default', $result); - } - } - } - - if (!Multilanguage::isEnabled()) - { - $form->setFieldAttribute('language', 'type', 'hidden'); - $form->setFieldAttribute('language', 'default', '*'); - } - - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 4.0.0 - * @throws \Exception - */ - public function getTable($name = 'Article', $prefix = 'Administrator', $options = array()) - { - return parent::getTable($name, $prefix, $options); - } + /** + * Model typeAlias string. Used for version history. + * + * @var string + */ + public $typeAlias = 'com_content.article'; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load the parameters. + $params = $app->getParams(); + $this->setState('params', $params); + + if ($params && $params->get('enable_category') == 1 && $params->get('catid')) { + $catId = $params->get('catid'); + } else { + $catId = 0; + } + + // Load state from the request. + $pk = $app->input->getInt('a_id'); + $this->setState('article.id', $pk); + + $this->setState('article.catid', $app->input->getInt('catid', $catId)); + + $return = $app->input->get('return', '', 'base64'); + $this->setState('return_page', base64_decode($return)); + + $this->setState('layout', $app->input->getString('layout')); + } + + /** + * Method to get article data. + * + * @param integer $itemId The id of the article. + * + * @return mixed Content item data object on success, false on failure. + */ + public function getItem($itemId = null) + { + $itemId = (int) (!empty($itemId)) ? $itemId : $this->getState('article.id'); + + // Get a row instance. + $table = $this->getTable(); + + // Attempt to load the row. + $return = $table->load($itemId); + + // Check for a table object error. + if ($return === false && $table->getError()) { + $this->setError($table->getError()); + + return false; + } + + $properties = $table->getProperties(1); + $value = ArrayHelper::toObject($properties, CMSObject::class); + + // Convert attrib field to Registry. + $value->params = new Registry($value->attribs); + + // Compute selected asset permissions. + $user = Factory::getUser(); + $userId = $user->get('id'); + $asset = 'com_content.article.' . $value->id; + + // Check general edit permission first. + if ($user->authorise('core.edit', $asset)) { + $value->params->set('access-edit', true); + } elseif (!empty($userId) && $user->authorise('core.edit.own', $asset)) { + // Now check if edit.own is available. + // Check for a valid user and that they are the owner. + if ($userId == $value->created_by) { + $value->params->set('access-edit', true); + } + } + + // Check edit state permission. + if ($itemId) { + // Existing item + $value->params->set('access-change', $user->authorise('core.edit.state', $asset)); + } else { + // New item. + $catId = (int) $this->getState('article.catid'); + + if ($catId) { + $value->params->set('access-change', $user->authorise('core.edit.state', 'com_content.category.' . $catId)); + $value->catid = $catId; + } else { + $value->params->set('access-change', $user->authorise('core.edit.state', 'com_content')); + } + } + + $value->articletext = $value->introtext; + + if (!empty($value->fulltext)) { + $value->articletext .= '
    ' . $value->fulltext; + } + + // Convert the metadata field to an array. + $registry = new Registry($value->metadata); + $value->metadata = $registry->toArray(); + + if ($itemId) { + $value->tags = new TagsHelper(); + $value->tags->getTagIds($value->id, 'com_content.article'); + $value->metadata['tags'] = $value->tags; + } + + return $value; + } + + /** + * Get the return URL. + * + * @return string The return URL. + * + * @since 1.6 + */ + public function getReturnPage() + { + return base64_encode($this->getState('return_page', '')); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @since 3.2 + */ + public function save($data) + { + // Associations are not edited in frontend ATM so we have to inherit them + if ( + Associations::isEnabled() && !empty($data['id']) + && $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', $data['id']) + ) { + foreach ($associations as $tag => $associated) { + $associations[$tag] = (int) $associated->id; + } + + $data['associations'] = $associations; + } + + if (!Multilanguage::isEnabled()) { + $data['language'] = '*'; + } + + return parent::save($data); + } + + /** + * Method to get the record form. + * + * @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 Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = [], $loadData = true) + { + $form = parent::getForm($data, $loadData); + + if (empty($form)) { + return false; + } + + $app = Factory::getApplication(); + $user = $app->getIdentity(); + + // On edit article, we get ID of article from article.id state, but on save, we use data from input + $id = (int) $this->getState('article.id', $app->input->getInt('a_id')); + + // Existing record. We can't edit the category in frontend if not edit.state. + if ($id > 0 && !$user->authorise('core.edit.state', 'com_content.article.' . $id)) { + $form->setFieldAttribute('catid', 'readonly', 'true'); + $form->setFieldAttribute('catid', 'required', 'false'); + $form->setFieldAttribute('catid', 'filter', 'unset'); + } + + // Prevent messing with article language and category when editing existing article with associations + if ($this->getState('article.id') && Associations::isEnabled()) { + $associations = Associations::getAssociations('com_content', '#__content', 'com_content.item', $id); + + // Make fields read only + if (!empty($associations)) { + $form->setFieldAttribute('language', 'readonly', 'true'); + $form->setFieldAttribute('catid', 'readonly', 'true'); + $form->setFieldAttribute('language', 'filter', 'unset'); + $form->setFieldAttribute('catid', 'filter', 'unset'); + } + } + + return $form; + } + + /** + * Allows preprocessing of the JForm object. + * + * @param Form $form The form object + * @param array $data The data to be merged into the form object + * @param string $group The plugin group to be executed + * + * @return void + * + * @since 3.7.0 + */ + protected function preprocessForm(Form $form, $data, $group = 'content') + { + $params = $this->getState()->get('params'); + + if ($params && $params->get('enable_category') == 1 && $params->get('catid')) { + $form->setFieldAttribute('catid', 'default', $params->get('catid')); + $form->setFieldAttribute('catid', 'readonly', 'true'); + + if (Multilanguage::isEnabled()) { + $categoryId = (int) $params->get('catid'); + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('language')) + ->from($db->quoteName('#__categories')) + ->where($db->quoteName('id') . ' = :categoryId') + ->bind(':categoryId', $categoryId, ParameterType::INTEGER); + $db->setQuery($query); + + $result = $db->loadResult(); + + if ($result != '*') { + $form->setFieldAttribute('language', 'readonly', 'true'); + $form->setFieldAttribute('language', 'default', $result); + } + } + } + + if (!Multilanguage::isEnabled()) { + $form->setFieldAttribute('language', 'type', 'hidden'); + $form->setFieldAttribute('language', 'default', '*'); + } + + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 4.0.0 + * @throws \Exception + */ + public function getTable($name = 'Article', $prefix = 'Administrator', $options = array()) + { + return parent::getTable($name, $prefix, $options); + } } diff --git a/components/com_content/src/Service/Category.php b/components/com_content/src/Service/Category.php index 106f19b56b225..2ffe9755984ff 100644 --- a/components/com_content/src/Service/Category.php +++ b/components/com_content/src/Service/Category.php @@ -1,4 +1,5 @@ categoryFactory = $categoryFactory; - $this->db = $db; - - $params = ComponentHelper::getParams('com_content'); - $this->noIDs = (bool) $params->get('sef_ids'); - $categories = new RouterViewConfiguration('categories'); - $categories->setKey('id'); - $this->registerView($categories); - $category = new RouterViewConfiguration('category'); - $category->setKey('id')->setParent($categories, 'catid')->setNestable()->addLayout('blog'); - $this->registerView($category); - $article = new RouterViewConfiguration('article'); - $article->setKey('id')->setParent($category, 'catid'); - $this->registerView($article); - $this->registerView(new RouterViewConfiguration('archive')); - $this->registerView(new RouterViewConfiguration('featured')); - $form = new RouterViewConfiguration('form'); - $form->setKey('a_id'); - $this->registerView($form); - - parent::__construct($app, $menu); - - $this->attachRule(new MenuRules($this)); - $this->attachRule(new StandardRules($this)); - $this->attachRule(new NomenuRules($this)); - } - - /** - * Method to get the segment(s) for a category - * - * @param string $id ID of the category to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getCategorySegment($id, $query) - { - $category = $this->getCategories(['access' => true])->get($id); - - if ($category) - { - $path = array_reverse($category->getPath(), true); - $path[0] = '1:root'; - - if ($this->noIDs) - { - foreach ($path as &$segment) - { - list($id, $segment) = explode(':', $segment, 2); - } - } - - return $path; - } - - return array(); - } - - /** - * Method to get the segment(s) for a category - * - * @param string $id ID of the category to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getCategoriesSegment($id, $query) - { - return $this->getCategorySegment($id, $query); - } - - /** - * Method to get the segment(s) for an article - * - * @param string $id ID of the article to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getArticleSegment($id, $query) - { - if (!strpos($id, ':')) - { - $id = (int) $id; - $dbquery = $this->db->getQuery(true); - $dbquery->select($this->db->quoteName('alias')) - ->from($this->db->quoteName('#__content')) - ->where($this->db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $this->db->setQuery($dbquery); - - $id .= ':' . $this->db->loadResult(); - } - - if ($this->noIDs) - { - list($void, $segment) = explode(':', $id, 2); - - return array($void => $segment); - } - - return array((int) $id => $id); - } - - /** - * Method to get the segment(s) for a form - * - * @param string $id ID of the article form to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - * - * @since 3.7.3 - */ - public function getFormSegment($id, $query) - { - return $this->getArticleSegment($id, $query); - } - - /** - * Method to get the id for a category - * - * @param string $segment Segment to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getCategoryId($segment, $query) - { - if (isset($query['id'])) - { - $category = $this->getCategories(['access' => false])->get($query['id']); - - if ($category) - { - foreach ($category->getChildren() as $child) - { - if ($this->noIDs) - { - if ($child->alias == $segment) - { - return $child->id; - } - } - else - { - if ($child->id == (int) $segment) - { - return $child->id; - } - } - } - } - } - - return false; - } - - /** - * Method to get the segment(s) for a category - * - * @param string $segment Segment to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getCategoriesId($segment, $query) - { - return $this->getCategoryId($segment, $query); - } - - /** - * Method to get the segment(s) for an article - * - * @param string $segment Segment of the article to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getArticleId($segment, $query) - { - if ($this->noIDs) - { - $dbquery = $this->db->getQuery(true); - $dbquery->select($this->db->quoteName('id')) - ->from($this->db->quoteName('#__content')) - ->where( - [ - $this->db->quoteName('alias') . ' = :alias', - $this->db->quoteName('catid') . ' = :catid', - ] - ) - ->bind(':alias', $segment) - ->bind(':catid', $query['id'], ParameterType::INTEGER); - $this->db->setQuery($dbquery); - - return (int) $this->db->loadResult(); - } - - return (int) $segment; - } - - /** - * Method to get categories from cache - * - * @param array $options The options for retrieving categories - * - * @return CategoryInterface The object containing categories - * - * @since 4.0.0 - */ - private function getCategories(array $options = []): CategoryInterface - { - $key = serialize($options); - - if (!isset($this->categoryCache[$key])) - { - $this->categoryCache[$key] = $this->categoryFactory->createCategory($options); - } - - return $this->categoryCache[$key]; - } + /** + * Flag to remove IDs + * + * @var boolean + */ + protected $noIDs = false; + + /** + * The category factory + * + * @var CategoryFactoryInterface + * + * @since 4.0.0 + */ + private $categoryFactory; + + /** + * The category cache + * + * @var array + * + * @since 4.0.0 + */ + private $categoryCache = []; + + /** + * The db + * + * @var DatabaseInterface + * + * @since 4.0.0 + */ + private $db; + + /** + * Content Component router constructor + * + * @param SiteApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + * @param CategoryFactoryInterface $categoryFactory The category object + * @param DatabaseInterface $db The database object + */ + public function __construct(SiteApplication $app, AbstractMenu $menu, CategoryFactoryInterface $categoryFactory, DatabaseInterface $db) + { + $this->categoryFactory = $categoryFactory; + $this->db = $db; + + $params = ComponentHelper::getParams('com_content'); + $this->noIDs = (bool) $params->get('sef_ids'); + $categories = new RouterViewConfiguration('categories'); + $categories->setKey('id'); + $this->registerView($categories); + $category = new RouterViewConfiguration('category'); + $category->setKey('id')->setParent($categories, 'catid')->setNestable()->addLayout('blog'); + $this->registerView($category); + $article = new RouterViewConfiguration('article'); + $article->setKey('id')->setParent($category, 'catid'); + $this->registerView($article); + $this->registerView(new RouterViewConfiguration('archive')); + $this->registerView(new RouterViewConfiguration('featured')); + $form = new RouterViewConfiguration('form'); + $form->setKey('a_id'); + $this->registerView($form); + + parent::__construct($app, $menu); + + $this->attachRule(new MenuRules($this)); + $this->attachRule(new StandardRules($this)); + $this->attachRule(new NomenuRules($this)); + } + + /** + * Method to get the segment(s) for a category + * + * @param string $id ID of the category to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getCategorySegment($id, $query) + { + $category = $this->getCategories(['access' => true])->get($id); + + if ($category) { + $path = array_reverse($category->getPath(), true); + $path[0] = '1:root'; + + if ($this->noIDs) { + foreach ($path as &$segment) { + list($id, $segment) = explode(':', $segment, 2); + } + } + + return $path; + } + + return array(); + } + + /** + * Method to get the segment(s) for a category + * + * @param string $id ID of the category to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getCategoriesSegment($id, $query) + { + return $this->getCategorySegment($id, $query); + } + + /** + * Method to get the segment(s) for an article + * + * @param string $id ID of the article to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getArticleSegment($id, $query) + { + if (!strpos($id, ':')) { + $id = (int) $id; + $dbquery = $this->db->getQuery(true); + $dbquery->select($this->db->quoteName('alias')) + ->from($this->db->quoteName('#__content')) + ->where($this->db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $this->db->setQuery($dbquery); + + $id .= ':' . $this->db->loadResult(); + } + + if ($this->noIDs) { + list($void, $segment) = explode(':', $id, 2); + + return array($void => $segment); + } + + return array((int) $id => $id); + } + + /** + * Method to get the segment(s) for a form + * + * @param string $id ID of the article form to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + * + * @since 3.7.3 + */ + public function getFormSegment($id, $query) + { + return $this->getArticleSegment($id, $query); + } + + /** + * Method to get the id for a category + * + * @param string $segment Segment to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getCategoryId($segment, $query) + { + if (isset($query['id'])) { + $category = $this->getCategories(['access' => false])->get($query['id']); + + if ($category) { + foreach ($category->getChildren() as $child) { + if ($this->noIDs) { + if ($child->alias == $segment) { + return $child->id; + } + } else { + if ($child->id == (int) $segment) { + return $child->id; + } + } + } + } + } + + return false; + } + + /** + * Method to get the segment(s) for a category + * + * @param string $segment Segment to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getCategoriesId($segment, $query) + { + return $this->getCategoryId($segment, $query); + } + + /** + * Method to get the segment(s) for an article + * + * @param string $segment Segment of the article to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getArticleId($segment, $query) + { + if ($this->noIDs) { + $dbquery = $this->db->getQuery(true); + $dbquery->select($this->db->quoteName('id')) + ->from($this->db->quoteName('#__content')) + ->where( + [ + $this->db->quoteName('alias') . ' = :alias', + $this->db->quoteName('catid') . ' = :catid', + ] + ) + ->bind(':alias', $segment) + ->bind(':catid', $query['id'], ParameterType::INTEGER); + $this->db->setQuery($dbquery); + + return (int) $this->db->loadResult(); + } + + return (int) $segment; + } + + /** + * Method to get categories from cache + * + * @param array $options The options for retrieving categories + * + * @return CategoryInterface The object containing categories + * + * @since 4.0.0 + */ + private function getCategories(array $options = []): CategoryInterface + { + $key = serialize($options); + + if (!isset($this->categoryCache[$key])) { + $this->categoryCache[$key] = $this->categoryFactory->createCategory($options); + } + + return $this->categoryCache[$key]; + } } diff --git a/components/com_content/src/View/Archive/HtmlView.php b/components/com_content/src/View/Archive/HtmlView.php index ff86fccf5d37e..b2f99d91f07fd 100644 --- a/components/com_content/src/View/Archive/HtmlView.php +++ b/components/com_content/src/View/Archive/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser(); - $state = $this->get('State'); - $items = $this->get('Items'); - $pagination = $this->get('Pagination'); - - if ($errors = $this->getModel()->getErrors()) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Flag indicates to not add limitstart=0 to URL - $pagination->hideEmptyLimitstart = true; - - // Get the page/component configuration - $params = &$state->params; - - PluginHelper::importPlugin('content'); - - foreach ($items as $item) - { - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - - // No link for ROOT category - if ($item->parent_alias === 'root') - { - $item->parent_id = null; - } - - $item->event = new \stdClass; - - // Old plugins: Ensure that text property is available - if (!isset($item->text)) - { - $item->text = $item->introtext; - } - - Factory::getApplication()->triggerEvent('onContentPrepare', array('com_content.archive', &$item, &$item->params, 0)); - - // Old plugins: Use processed text as introtext - $item->introtext = $item->text; - - $results = Factory::getApplication()->triggerEvent('onContentAfterTitle', array('com_content.archive', &$item, &$item->params, 0)); - $item->event->afterDisplayTitle = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentBeforeDisplay', array('com_content.archive', &$item, &$item->params, 0)); - $item->event->beforeDisplayContent = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentAfterDisplay', array('com_content.archive', &$item, &$item->params, 0)); - $item->event->afterDisplayContent = trim(implode("\n", $results)); - } - - $form = new \stdClass; - - // Month Field - $months = array( - '' => Text::_('COM_CONTENT_MONTH'), - '1' => Text::_('JANUARY_SHORT'), - '2' => Text::_('FEBRUARY_SHORT'), - '3' => Text::_('MARCH_SHORT'), - '4' => Text::_('APRIL_SHORT'), - '5' => Text::_('MAY_SHORT'), - '6' => Text::_('JUNE_SHORT'), - '7' => Text::_('JULY_SHORT'), - '8' => Text::_('AUGUST_SHORT'), - '9' => Text::_('SEPTEMBER_SHORT'), - '10' => Text::_('OCTOBER_SHORT'), - '11' => Text::_('NOVEMBER_SHORT'), - '12' => Text::_('DECEMBER_SHORT') - ); - $form->monthField = HTMLHelper::_( - 'select.genericlist', - $months, - 'month', - array( - 'list.attr' => 'class="form-select"', - 'list.select' => $state->get('filter.month'), - 'option.key' => null - ) - ); - - // Year Field - $this->years = $this->getModel()->getYears(); - $years = array(); - $years[] = HTMLHelper::_('select.option', null, Text::_('JYEAR')); - - for ($i = 0, $iMax = count($this->years); $i < $iMax; $i++) - { - $years[] = HTMLHelper::_('select.option', $this->years[$i], $this->years[$i]); - } - - $form->yearField = HTMLHelper::_( - 'select.genericlist', - $years, - 'year', - array('list.attr' => 'class="form-select"', 'list.select' => $state->get('filter.year')) - ); - $form->limitField = $pagination->getLimitBox(); - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - - $this->filter = $state->get('list.filter'); - $this->form = &$form; - $this->items = &$items; - $this->params = &$params; - $this->user = &$user; - $this->pagination = &$pagination; - $this->pagination->setAdditionalUrlParam('month', $state->get('filter.month')); - $this->pagination->setAdditionalUrlParam('year', $state->get('filter.year')); - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - */ - protected function _prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('JGLOBAL_ARTICLES')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state = null; + + /** + * An array containing archived articles + * + * @var \stdClass[] + */ + protected $items = array(); + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination|null + */ + protected $pagination = null; + + /** + * The years that are available to filter on. + * + * @var array + * + * @since 3.6.0 + */ + protected $years = array(); + + /** + * Object containing the year, month and limit field to be displayed + * + * @var \stdClass|null + * + * @since 4.0.0 + */ + protected $form = null; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * The search query used on any archived articles (note this may not be displayed depending on the value of the + * filter_field component parameter) + * + * @var string + * + * @since 4.0.0 + */ + protected $filter = ''; + + /** + * The user object + * + * @var \Joomla\CMS\User\User + * + * @since 4.0.0 + */ + protected $user = null; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @throws GenericDataException + */ + public function display($tpl = null) + { + $user = $this->getCurrentUser(); + $state = $this->get('State'); + $items = $this->get('Items'); + $pagination = $this->get('Pagination'); + + if ($errors = $this->getModel()->getErrors()) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Flag indicates to not add limitstart=0 to URL + $pagination->hideEmptyLimitstart = true; + + // Get the page/component configuration + $params = &$state->params; + + PluginHelper::importPlugin('content'); + + foreach ($items as $item) { + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + + // No link for ROOT category + if ($item->parent_alias === 'root') { + $item->parent_id = null; + } + + $item->event = new \stdClass(); + + // Old plugins: Ensure that text property is available + if (!isset($item->text)) { + $item->text = $item->introtext; + } + + Factory::getApplication()->triggerEvent('onContentPrepare', array('com_content.archive', &$item, &$item->params, 0)); + + // Old plugins: Use processed text as introtext + $item->introtext = $item->text; + + $results = Factory::getApplication()->triggerEvent('onContentAfterTitle', array('com_content.archive', &$item, &$item->params, 0)); + $item->event->afterDisplayTitle = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent('onContentBeforeDisplay', array('com_content.archive', &$item, &$item->params, 0)); + $item->event->beforeDisplayContent = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent('onContentAfterDisplay', array('com_content.archive', &$item, &$item->params, 0)); + $item->event->afterDisplayContent = trim(implode("\n", $results)); + } + + $form = new \stdClass(); + + // Month Field + $months = array( + '' => Text::_('COM_CONTENT_MONTH'), + '1' => Text::_('JANUARY_SHORT'), + '2' => Text::_('FEBRUARY_SHORT'), + '3' => Text::_('MARCH_SHORT'), + '4' => Text::_('APRIL_SHORT'), + '5' => Text::_('MAY_SHORT'), + '6' => Text::_('JUNE_SHORT'), + '7' => Text::_('JULY_SHORT'), + '8' => Text::_('AUGUST_SHORT'), + '9' => Text::_('SEPTEMBER_SHORT'), + '10' => Text::_('OCTOBER_SHORT'), + '11' => Text::_('NOVEMBER_SHORT'), + '12' => Text::_('DECEMBER_SHORT') + ); + $form->monthField = HTMLHelper::_( + 'select.genericlist', + $months, + 'month', + array( + 'list.attr' => 'class="form-select"', + 'list.select' => $state->get('filter.month'), + 'option.key' => null + ) + ); + + // Year Field + $this->years = $this->getModel()->getYears(); + $years = array(); + $years[] = HTMLHelper::_('select.option', null, Text::_('JYEAR')); + + for ($i = 0, $iMax = count($this->years); $i < $iMax; $i++) { + $years[] = HTMLHelper::_('select.option', $this->years[$i], $this->years[$i]); + } + + $form->yearField = HTMLHelper::_( + 'select.genericlist', + $years, + 'year', + array('list.attr' => 'class="form-select"', 'list.select' => $state->get('filter.year')) + ); + $form->limitField = $pagination->getLimitBox(); + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + + $this->filter = $state->get('list.filter'); + $this->form = &$form; + $this->items = &$items; + $this->params = &$params; + $this->user = &$user; + $this->pagination = &$pagination; + $this->pagination->setAdditionalUrlParam('month', $state->get('filter.month')); + $this->pagination->setAdditionalUrlParam('year', $state->get('filter.year')); + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + */ + protected function _prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('JGLOBAL_ARTICLES')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_content/src/View/Article/HtmlView.php b/components/com_content/src/View/Article/HtmlView.php index 737561243eaea..9a372261ac4a5 100644 --- a/components/com_content/src/View/Article/HtmlView.php +++ b/components/com_content/src/View/Article/HtmlView.php @@ -1,4 +1,5 @@ getLayout() == 'pagebreak') - { - parent::display($tpl); - - return; - } - - $app = Factory::getApplication(); - $user = $this->getCurrentUser(); - - $this->item = $this->get('Item'); - $this->print = $app->input->getBool('print', false); - $this->state = $this->get('State'); - $this->user = $user; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Create a shortcut for $item. - $item = $this->item; - $item->tagLayout = new FileLayout('joomla.content.tags'); - - // Add router helpers. - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - - // No link for ROOT category - if ($item->parent_alias === 'root') - { - $item->parent_id = null; - } - - // @todo Change based on shownoauth - $item->readmore_link = Route::_(RouteHelper::getArticleRoute($item->slug, $item->catid, $item->language)); - - // Merge article params. If this is single-article view, menu params override article params - // Otherwise, article params override menu item params - $this->params = $this->state->get('params'); - $active = $app->getMenu()->getActive(); - $temp = clone $this->params; - - // Check to see which parameters should take priority. If the active menu item link to the current article, then - // the menu item params take priority - if ($active - && $active->component == 'com_content' - && isset($active->query['view'], $active->query['id']) - && $active->query['view'] == 'article' - && $active->query['id'] == $item->id) - { - $this->menuItemMatchArticle = true; - - // Load layout from active query (in case it is an alternative menu item) - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - // Check for alternative layout of article - elseif ($layout = $item->params->get('article_layout')) - { - $this->setLayout($layout); - } - - // $item->params are the article params, $temp are the menu item params - // Merge so that the menu item params take priority - $item->params->merge($temp); - } - else - { - // The active menu item is not linked to this article, so the article params take priority here - // Merge the menu item params with the article params so that the article params take priority - $temp->merge($item->params); - $item->params = $temp; - - // Check for alternative layouts (since we are not in a single-article menu item) - // Single-article menu item layout takes priority over alt layout for an article - if ($layout = $item->params->get('article_layout')) - { - $this->setLayout($layout); - } - } - - $offset = $this->state->get('list.offset'); - - // Check the view access to the article (the model has already computed the values). - if ($item->params->get('access-view') == false && ($item->params->get('show_noauth', '0') == '0')) - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $app->setHeader('status', 403, true); - - return; - } - - /** - * Check for no 'access-view' and empty fulltext, - * - Redirect guest users to login - * - Deny access to logged users with 403 code - * NOTE: we do not recheck for no access-view + show_noauth disabled ... since it was checked above - */ - if ($item->params->get('access-view') == false && !strlen($item->fulltext)) - { - if ($this->user->get('guest')) - { - $return = base64_encode(Uri::getInstance()); - $login_url_with_return = Route::_('index.php?option=com_users&view=login&return=' . $return); - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'notice'); - $app->redirect($login_url_with_return, 403); - } - else - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $app->setHeader('status', 403, true); - - return; - } - } - - /** - * NOTE: The following code (usually) sets the text to contain the fulltext, but it is the - * responsibility of the layout to check 'access-view' and only use "introtext" for guests - */ - if ($item->params->get('show_intro', '1') == '1') - { - $item->text = $item->introtext . ' ' . $item->fulltext; - } - elseif ($item->fulltext) - { - $item->text = $item->fulltext; - } - else - { - $item->text = $item->introtext; - } - - $item->tags = new TagsHelper; - $item->tags->getItemTags('com_content.article', $this->item->id); - - if (Associations::isEnabled() && $item->params->get('show_associations')) - { - $item->associations = AssociationHelper::displayAssociations($item->id); - } - - // Process the content plugins. - PluginHelper::importPlugin('content'); - $this->dispatchEvent(new Event('onContentPrepare', array('com_content.article', &$item, &$item->params, $offset))); - - $item->event = new \stdClass; - $results = Factory::getApplication()->triggerEvent('onContentAfterTitle', array('com_content.article', &$item, &$item->params, $offset)); - $item->event->afterDisplayTitle = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentBeforeDisplay', array('com_content.article', &$item, &$item->params, $offset)); - $item->event->beforeDisplayContent = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentAfterDisplay', array('com_content.article', &$item, &$item->params, $offset)); - $item->event->afterDisplayContent = trim(implode("\n", $results)); - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->item->params->get('pageclass_sfx', '')); - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - */ - protected function _prepareDocument() - { - $app = Factory::getApplication(); - $pathway = $app->getPathway(); - - /** - * Because the application sets a default page title, - * we need to get it from the menu item itself - */ - $menu = $app->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('JGLOBAL_ARTICLES')); - } - - $title = $this->params->get('page_title', ''); - - // If the menu item is not linked to this article - if (!$this->menuItemMatchArticle) - { - // If a browser page title is defined, use that, then fall back to the article title if set, then fall back to the page_title option - $title = $this->item->params->get('article_page_title', $this->item->title ?: $title); - - // Get ID of the category from active menu item - if ($menu && $menu->component == 'com_content' && isset($menu->query['view']) - && in_array($menu->query['view'], ['categories', 'category'])) - { - $id = $menu->query['id']; - } - else - { - $id = 0; - } - - $path = array(array('title' => $this->item->title, 'link' => '')); - $category = Categories::getInstance('Content')->get($this->item->catid); - - while ($category !== null && $category->id != $id && $category->id !== 'root') - { - $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)); - $category = $category->getParent(); - } - - $path = array_reverse($path); - - foreach ($path as $item) - { - $pathway->addItem($item['title'], $item['link']); - } - } - - if (empty($title)) - { - $title = $this->item->title; - } - - $this->setDocumentTitle($title); - - if ($this->item->metadesc) - { - $this->document->setDescription($this->item->metadesc); - } - elseif ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - - if ($app->get('MetaAuthor') == '1') - { - $author = $this->item->created_by_alias ?: $this->item->author; - $this->document->setMetaData('author', $author); - } - - $mdata = $this->item->metadata->toArray(); - - foreach ($mdata as $k => $v) - { - if ($v) - { - $this->document->setMetaData($k, $v); - } - } - - // If there is a pagebreak heading or title, add it to the page title - if (!empty($this->item->page_title)) - { - $this->item->title = $this->item->title . ' - ' . $this->item->page_title; - $this->setDocumentTitle( - $this->item->page_title . ' - ' . Text::sprintf('PLG_CONTENT_PAGEBREAK_PAGE_NUM', $this->state->get('list.offset') + 1) - ); - } - - if ($this->print) - { - $this->document->setMetaData('robots', 'noindex, nofollow'); - } - } + /** + * The article object + * + * @var \stdClass + */ + protected $item; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * Should the print button be displayed or not? + * + * @var boolean + */ + protected $print = false; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * The user object + * + * @var \Joomla\CMS\User\User|null + */ + protected $user = null; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The flag to mark if the active menu item is linked to the being displayed article + * + * @var boolean + */ + protected $menuItemMatchArticle = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + if ($this->getLayout() == 'pagebreak') { + parent::display($tpl); + + return; + } + + $app = Factory::getApplication(); + $user = $this->getCurrentUser(); + + $this->item = $this->get('Item'); + $this->print = $app->input->getBool('print', false); + $this->state = $this->get('State'); + $this->user = $user; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Create a shortcut for $item. + $item = $this->item; + $item->tagLayout = new FileLayout('joomla.content.tags'); + + // Add router helpers. + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + + // No link for ROOT category + if ($item->parent_alias === 'root') { + $item->parent_id = null; + } + + // @todo Change based on shownoauth + $item->readmore_link = Route::_(RouteHelper::getArticleRoute($item->slug, $item->catid, $item->language)); + + // Merge article params. If this is single-article view, menu params override article params + // Otherwise, article params override menu item params + $this->params = $this->state->get('params'); + $active = $app->getMenu()->getActive(); + $temp = clone $this->params; + + // Check to see which parameters should take priority. If the active menu item link to the current article, then + // the menu item params take priority + if ( + $active + && $active->component == 'com_content' + && isset($active->query['view'], $active->query['id']) + && $active->query['view'] == 'article' + && $active->query['id'] == $item->id + ) { + $this->menuItemMatchArticle = true; + + // Load layout from active query (in case it is an alternative menu item) + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } elseif ($layout = $item->params->get('article_layout')) { + // Check for alternative layout of article + $this->setLayout($layout); + } + + // $item->params are the article params, $temp are the menu item params + // Merge so that the menu item params take priority + $item->params->merge($temp); + } else { + // The active menu item is not linked to this article, so the article params take priority here + // Merge the menu item params with the article params so that the article params take priority + $temp->merge($item->params); + $item->params = $temp; + + // Check for alternative layouts (since we are not in a single-article menu item) + // Single-article menu item layout takes priority over alt layout for an article + if ($layout = $item->params->get('article_layout')) { + $this->setLayout($layout); + } + } + + $offset = $this->state->get('list.offset'); + + // Check the view access to the article (the model has already computed the values). + if ($item->params->get('access-view') == false && ($item->params->get('show_noauth', '0') == '0')) { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $app->setHeader('status', 403, true); + + return; + } + + /** + * Check for no 'access-view' and empty fulltext, + * - Redirect guest users to login + * - Deny access to logged users with 403 code + * NOTE: we do not recheck for no access-view + show_noauth disabled ... since it was checked above + */ + if ($item->params->get('access-view') == false && !strlen($item->fulltext)) { + if ($this->user->get('guest')) { + $return = base64_encode(Uri::getInstance()); + $login_url_with_return = Route::_('index.php?option=com_users&view=login&return=' . $return); + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'notice'); + $app->redirect($login_url_with_return, 403); + } else { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $app->setHeader('status', 403, true); + + return; + } + } + + /** + * NOTE: The following code (usually) sets the text to contain the fulltext, but it is the + * responsibility of the layout to check 'access-view' and only use "introtext" for guests + */ + if ($item->params->get('show_intro', '1') == '1') { + $item->text = $item->introtext . ' ' . $item->fulltext; + } elseif ($item->fulltext) { + $item->text = $item->fulltext; + } else { + $item->text = $item->introtext; + } + + $item->tags = new TagsHelper(); + $item->tags->getItemTags('com_content.article', $this->item->id); + + if (Associations::isEnabled() && $item->params->get('show_associations')) { + $item->associations = AssociationHelper::displayAssociations($item->id); + } + + // Process the content plugins. + PluginHelper::importPlugin('content'); + $this->dispatchEvent(new Event('onContentPrepare', array('com_content.article', &$item, &$item->params, $offset))); + + $item->event = new \stdClass(); + $results = Factory::getApplication()->triggerEvent('onContentAfterTitle', array('com_content.article', &$item, &$item->params, $offset)); + $item->event->afterDisplayTitle = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent('onContentBeforeDisplay', array('com_content.article', &$item, &$item->params, $offset)); + $item->event->beforeDisplayContent = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent('onContentAfterDisplay', array('com_content.article', &$item, &$item->params, $offset)); + $item->event->afterDisplayContent = trim(implode("\n", $results)); + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->item->params->get('pageclass_sfx', '')); + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + */ + protected function _prepareDocument() + { + $app = Factory::getApplication(); + $pathway = $app->getPathway(); + + /** + * Because the application sets a default page title, + * we need to get it from the menu item itself + */ + $menu = $app->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('JGLOBAL_ARTICLES')); + } + + $title = $this->params->get('page_title', ''); + + // If the menu item is not linked to this article + if (!$this->menuItemMatchArticle) { + // If a browser page title is defined, use that, then fall back to the article title if set, then fall back to the page_title option + $title = $this->item->params->get('article_page_title', $this->item->title ?: $title); + + // Get ID of the category from active menu item + if ( + $menu && $menu->component == 'com_content' && isset($menu->query['view']) + && in_array($menu->query['view'], ['categories', 'category']) + ) { + $id = $menu->query['id']; + } else { + $id = 0; + } + + $path = array(array('title' => $this->item->title, 'link' => '')); + $category = Categories::getInstance('Content')->get($this->item->catid); + + while ($category !== null && $category->id != $id && $category->id !== 'root') { + $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)); + $category = $category->getParent(); + } + + $path = array_reverse($path); + + foreach ($path as $item) { + $pathway->addItem($item['title'], $item['link']); + } + } + + if (empty($title)) { + $title = $this->item->title; + } + + $this->setDocumentTitle($title); + + if ($this->item->metadesc) { + $this->document->setDescription($this->item->metadesc); + } elseif ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + + if ($app->get('MetaAuthor') == '1') { + $author = $this->item->created_by_alias ?: $this->item->author; + $this->document->setMetaData('author', $author); + } + + $mdata = $this->item->metadata->toArray(); + + foreach ($mdata as $k => $v) { + if ($v) { + $this->document->setMetaData($k, $v); + } + } + + // If there is a pagebreak heading or title, add it to the page title + if (!empty($this->item->page_title)) { + $this->item->title = $this->item->title . ' - ' . $this->item->page_title; + $this->setDocumentTitle( + $this->item->page_title . ' - ' . Text::sprintf('PLG_CONTENT_PAGEBREAK_PAGE_NUM', $this->state->get('list.offset') + 1) + ); + } + + if ($this->print) { + $this->document->setMetaData('robots', 'noindex, nofollow'); + } + } } diff --git a/components/com_content/src/View/Categories/HtmlView.php b/components/com_content/src/View/Categories/HtmlView.php index f16ff6d57e27e..8b9e74033e8ce 100644 --- a/components/com_content/src/View/Categories/HtmlView.php +++ b/components/com_content/src/View/Categories/HtmlView.php @@ -1,4 +1,5 @@ getParams(); - $item->description = ''; - $obj = json_decode($item->images); + /** + * Method to reconcile non-standard names from components to usage in this class. + * Typically overridden in the component feed view class. + * + * @param object $item The item for a feed, an element of the $items array. + * + * @return void + * + * @since 3.2 + */ + protected function reconcileNames($item) + { + // Get description, intro_image, author and date + $app = Factory::getApplication(); + $params = $app->getParams(); + $item->description = ''; + $obj = json_decode($item->images); - if (!empty($obj->image_intro)) - { - $item->description = '

    ' . HTMLHelper::_('image', $obj->image_intro, $obj->image_intro_alt) . '

    '; - } + if (!empty($obj->image_intro)) { + $item->description = '

    ' . HTMLHelper::_('image', $obj->image_intro, $obj->image_intro_alt) . '

    '; + } - $item->description .= ($params->get('feed_summary', 0) ? $item->introtext . $item->fulltext : $item->introtext); + $item->description .= ($params->get('feed_summary', 0) ? $item->introtext . $item->fulltext : $item->introtext); - // Add readmore link to description if introtext is shown, show_readmore is true and fulltext exists - if (!$item->params->get('feed_summary', 0) && $item->params->get('feed_show_readmore', 0) && $item->fulltext) - { - // Compute the article slug - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + // Add readmore link to description if introtext is shown, show_readmore is true and fulltext exists + if (!$item->params->get('feed_summary', 0) && $item->params->get('feed_show_readmore', 0) && $item->fulltext) { + // Compute the article slug + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - // URL link to article - $link = Route::_( - RouteHelper::getArticleRoute($item->slug, $item->catid, $item->language), - true, - $app->get('force_ssl') == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE, - true - ); + // URL link to article + $link = Route::_( + RouteHelper::getArticleRoute($item->slug, $item->catid, $item->language), + true, + $app->get('force_ssl') == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE, + true + ); - $item->description .= '

    ' - . Text::_('COM_CONTENT_FEED_READMORE') . '

    '; - } + $item->description .= '

    ' + . Text::_('COM_CONTENT_FEED_READMORE') . '

    '; + } - $item->author = $item->created_by_alias ?: $item->author; - } + $item->author = $item->created_by_alias ?: $item->author; + } } diff --git a/components/com_content/src/View/Category/HtmlView.php b/components/com_content/src/View/Category/HtmlView.php index 90e31bf226918..1e79dbb148d3c 100644 --- a/components/com_content/src/View/Category/HtmlView.php +++ b/components/com_content/src/View/Category/HtmlView.php @@ -1,4 +1,5 @@ pagination->hideEmptyLimitstart = true; - - // Prepare the data - // Get the metrics for the structural page layout. - $params = $this->params; - $numLeading = $params->def('num_leading_articles', 1); - $numIntro = $params->def('num_intro_articles', 4); - $numLinks = $params->def('num_links', 4); - $this->vote = PluginHelper::isEnabled('content', 'vote'); - - PluginHelper::importPlugin('content'); - - $app = Factory::getApplication(); - - // Compute the article slugs and prepare introtext (runs content plugins). - foreach ($this->items as $item) - { - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - - // No link for ROOT category - if ($item->parent_alias === 'root') - { - $item->parent_id = null; - } - - $item->event = new \stdClass; - - // Old plugins: Ensure that text property is available - if (!isset($item->text)) - { - $item->text = $item->introtext; - } - - $app->triggerEvent('onContentPrepare', array('com_content.category', &$item, &$item->params, 0)); - - // Old plugins: Use processed text as introtext - $item->introtext = $item->text; - - $results = $app->triggerEvent('onContentAfterTitle', array('com_content.category', &$item, &$item->params, 0)); - $item->event->afterDisplayTitle = trim(implode("\n", $results)); - - $results = $app->triggerEvent('onContentBeforeDisplay', array('com_content.category', &$item, &$item->params, 0)); - $item->event->beforeDisplayContent = trim(implode("\n", $results)); - - $results = $app->triggerEvent('onContentAfterDisplay', array('com_content.category', &$item, &$item->params, 0)); - $item->event->afterDisplayContent = trim(implode("\n", $results)); - } - - // For blog layouts, preprocess the breakdown of leading, intro and linked articles. - // This makes it much easier for the designer to just interrogate the arrays. - if ($params->get('layout_type') === 'blog' || $this->getLayout() === 'blog') - { - foreach ($this->items as $i => $item) - { - if ($i < $numLeading) - { - $this->lead_items[] = $item; - } - - elseif ($i >= $numLeading && $i < $numLeading + $numIntro) - { - $this->intro_items[] = $item; - } - - elseif ($i < $numLeading + $numIntro + $numLinks) - { - $this->link_items[] = $item; - } - } - } - - // Because the application sets a default page title, - // we need to get it from the menu item itself - $active = $app->getMenu()->getActive(); - - if ($this->menuItemMatchCategory) - { - $this->params->def('page_heading', $this->params->get('page_title', $active->title)); - $title = $this->params->get('page_title', $active->title); - } - else - { - $this->params->def('page_heading', $this->category->title); - $title = $this->category->title; - $this->params->set('page_title', $title); - } - - if (empty($title)) - { - $title = $this->category->title; - } - - $this->setDocumentTitle($title); - - if ($this->category->metadesc) - { - $this->document->setDescription($this->category->metadesc); - } - elseif ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - - if (!is_object($this->category->metadata)) - { - $this->category->metadata = new Registry($this->category->metadata); - } - - if (($app->get('MetaAuthor') == '1') && $this->category->get('author', '')) - { - $this->document->setMetaData('author', $this->category->get('author', '')); - } - - $mdata = $this->category->metadata->toArray(); - - foreach ($mdata as $k => $v) - { - if ($v) - { - $this->document->setMetaData($k, $v); - } - } - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - */ - protected function prepareDocument() - { - parent::prepareDocument(); - - parent::addFeed(); - - if ($this->menuItemMatchCategory) - { - // If the active menu item is linked directly to the category being displayed, no further process is needed - return; - } - - // Get ID of the category from active menu item - $menu = $this->menu; - - if ($menu && $menu->component == 'com_content' && isset($menu->query['view']) - && in_array($menu->query['view'], ['categories', 'category'])) - { - $id = $menu->query['id']; - } - else - { - $id = 0; - } - - $path = [['title' => $this->category->title, 'link' => '']]; - $category = $this->category->getParent(); - - while ($category !== null && $category->id !== 'root' && $category->id != $id) - { - $path[] = ['title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)]; - $category = $category->getParent(); - } - - $path = array_reverse($path); - - foreach ($path as $item) - { - $this->pathway->addItem($item['title'], $item['link']); - } - } + /** + * @var array Array of leading items for blog display + * @since 3.2 + */ + protected $lead_items = array(); + + /** + * @var array Array of intro items for blog display + * @since 3.2 + */ + protected $intro_items = array(); + + /** + * @var array Array of links in blog display + * @since 3.2 + */ + protected $link_items = array(); + + /** + * @var string The name of the extension for the category + * @since 3.2 + */ + protected $extension = 'com_content'; + + /** + * @var string Default title to use for page title + * @since 3.2 + */ + protected $defaultPageTitle = 'JGLOBAL_ARTICLES'; + + /** + * @var string The name of the view to link individual items to + * @since 3.2 + */ + protected $viewName = 'article'; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + parent::commonCategoryDisplay(); + + // Flag indicates to not add limitstart=0 to URL + $this->pagination->hideEmptyLimitstart = true; + + // Prepare the data + // Get the metrics for the structural page layout. + $params = $this->params; + $numLeading = $params->def('num_leading_articles', 1); + $numIntro = $params->def('num_intro_articles', 4); + $numLinks = $params->def('num_links', 4); + $this->vote = PluginHelper::isEnabled('content', 'vote'); + + PluginHelper::importPlugin('content'); + + $app = Factory::getApplication(); + + // Compute the article slugs and prepare introtext (runs content plugins). + foreach ($this->items as $item) { + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + + // No link for ROOT category + if ($item->parent_alias === 'root') { + $item->parent_id = null; + } + + $item->event = new \stdClass(); + + // Old plugins: Ensure that text property is available + if (!isset($item->text)) { + $item->text = $item->introtext; + } + + $app->triggerEvent('onContentPrepare', array('com_content.category', &$item, &$item->params, 0)); + + // Old plugins: Use processed text as introtext + $item->introtext = $item->text; + + $results = $app->triggerEvent('onContentAfterTitle', array('com_content.category', &$item, &$item->params, 0)); + $item->event->afterDisplayTitle = trim(implode("\n", $results)); + + $results = $app->triggerEvent('onContentBeforeDisplay', array('com_content.category', &$item, &$item->params, 0)); + $item->event->beforeDisplayContent = trim(implode("\n", $results)); + + $results = $app->triggerEvent('onContentAfterDisplay', array('com_content.category', &$item, &$item->params, 0)); + $item->event->afterDisplayContent = trim(implode("\n", $results)); + } + + // For blog layouts, preprocess the breakdown of leading, intro and linked articles. + // This makes it much easier for the designer to just interrogate the arrays. + if ($params->get('layout_type') === 'blog' || $this->getLayout() === 'blog') { + foreach ($this->items as $i => $item) { + if ($i < $numLeading) { + $this->lead_items[] = $item; + } elseif ($i >= $numLeading && $i < $numLeading + $numIntro) { + $this->intro_items[] = $item; + } elseif ($i < $numLeading + $numIntro + $numLinks) { + $this->link_items[] = $item; + } + } + } + + // Because the application sets a default page title, + // we need to get it from the menu item itself + $active = $app->getMenu()->getActive(); + + if ($this->menuItemMatchCategory) { + $this->params->def('page_heading', $this->params->get('page_title', $active->title)); + $title = $this->params->get('page_title', $active->title); + } else { + $this->params->def('page_heading', $this->category->title); + $title = $this->category->title; + $this->params->set('page_title', $title); + } + + if (empty($title)) { + $title = $this->category->title; + } + + $this->setDocumentTitle($title); + + if ($this->category->metadesc) { + $this->document->setDescription($this->category->metadesc); + } elseif ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + + if (!is_object($this->category->metadata)) { + $this->category->metadata = new Registry($this->category->metadata); + } + + if (($app->get('MetaAuthor') == '1') && $this->category->get('author', '')) { + $this->document->setMetaData('author', $this->category->get('author', '')); + } + + $mdata = $this->category->metadata->toArray(); + + foreach ($mdata as $k => $v) { + if ($v) { + $this->document->setMetaData($k, $v); + } + } + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + */ + protected function prepareDocument() + { + parent::prepareDocument(); + + parent::addFeed(); + + if ($this->menuItemMatchCategory) { + // If the active menu item is linked directly to the category being displayed, no further process is needed + return; + } + + // Get ID of the category from active menu item + $menu = $this->menu; + + if ( + $menu && $menu->component == 'com_content' && isset($menu->query['view']) + && in_array($menu->query['view'], ['categories', 'category']) + ) { + $id = $menu->query['id']; + } else { + $id = 0; + } + + $path = [['title' => $this->category->title, 'link' => '']]; + $category = $this->category->getParent(); + + while ($category !== null && $category->id !== 'root' && $category->id != $id) { + $path[] = ['title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)]; + $category = $category->getParent(); + } + + $path = array_reverse($path); + + foreach ($path as $item) { + $this->pathway->addItem($item['title'], $item['link']); + } + } } diff --git a/components/com_content/src/View/Featured/FeedView.php b/components/com_content/src/View/Featured/FeedView.php index 1a037b07c135f..6534e7f99421d 100644 --- a/components/com_content/src/View/Featured/FeedView.php +++ b/components/com_content/src/View/Featured/FeedView.php @@ -1,4 +1,5 @@ getParams(); - $feedEmail = $app->get('feed_email', 'none'); - $siteEmail = $app->get('mailfrom'); - - $this->document->link = Route::_('index.php?option=com_content&view=featured'); - - // Get some data from the model - $app->input->set('limit', $app->get('feed_limit')); - $categories = Categories::getInstance('Content'); - $rows = $this->get('Items'); - - foreach ($rows as $row) - { - // Strip html from feed item title - $title = htmlspecialchars($row->title, ENT_QUOTES, 'UTF-8'); - $title = html_entity_decode($title, ENT_COMPAT, 'UTF-8'); - - // Compute the article slug - $row->slug = $row->alias ? ($row->id . ':' . $row->alias) : $row->id; - - // URL link to article - $link = RouteHelper::getArticleRoute($row->slug, $row->catid, $row->language); - - $description = ''; - $obj = json_decode($row->images); - - if (!empty($obj->image_intro)) - { - $description = '

    ' . HTMLHelper::_('image', $obj->image_intro, $obj->image_intro_alt) . '

    '; - } - - $description .= ($params->get('feed_summary', 0) ? $row->introtext . $row->fulltext : $row->introtext); - $author = $row->created_by_alias ?: $row->author; - - // Load individual item creator class - $item = new FeedItem; - $item->title = $title; - $item->link = Route::_($link); - $item->date = $row->publish_up; - $item->category = array(); - - // All featured articles are categorized as "Featured" - $item->category[] = Text::_('JFEATURED'); - - for ($item_category = $categories->get($row->catid); $item_category !== null; $item_category = $item_category->getParent()) - { - // Only add non-root categories - if ($item_category->id > 1) - { - $item->category[] = $item_category->title; - } - } - - $item->author = $author; - - if ($feedEmail === 'site') - { - $item->authorEmail = $siteEmail; - } - elseif ($feedEmail === 'author') - { - $item->authorEmail = $row->author_email; - } - - // Add readmore link to description if introtext is shown, show_readmore is true and fulltext exists - if (!$params->get('feed_summary', 0) && $params->get('feed_show_readmore', 0) && $row->fulltext) - { - $link = Route::_($link, true, $app->get('force_ssl') == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE, true); - $description .= '

    ' - . Text::_('COM_CONTENT_FEED_READMORE') . '

    '; - } - - // Load item description and add div - $item->description = '
    ' . $description . '
    '; - - // Loads item info into rss array - $this->document->addItem($item); - } - } + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + */ + public function display($tpl = null) + { + // Parameters + $app = Factory::getApplication(); + $params = $app->getParams(); + $feedEmail = $app->get('feed_email', 'none'); + $siteEmail = $app->get('mailfrom'); + + $this->document->link = Route::_('index.php?option=com_content&view=featured'); + + // Get some data from the model + $app->input->set('limit', $app->get('feed_limit')); + $categories = Categories::getInstance('Content'); + $rows = $this->get('Items'); + + foreach ($rows as $row) { + // Strip html from feed item title + $title = htmlspecialchars($row->title, ENT_QUOTES, 'UTF-8'); + $title = html_entity_decode($title, ENT_COMPAT, 'UTF-8'); + + // Compute the article slug + $row->slug = $row->alias ? ($row->id . ':' . $row->alias) : $row->id; + + // URL link to article + $link = RouteHelper::getArticleRoute($row->slug, $row->catid, $row->language); + + $description = ''; + $obj = json_decode($row->images); + + if (!empty($obj->image_intro)) { + $description = '

    ' . HTMLHelper::_('image', $obj->image_intro, $obj->image_intro_alt) . '

    '; + } + + $description .= ($params->get('feed_summary', 0) ? $row->introtext . $row->fulltext : $row->introtext); + $author = $row->created_by_alias ?: $row->author; + + // Load individual item creator class + $item = new FeedItem(); + $item->title = $title; + $item->link = Route::_($link); + $item->date = $row->publish_up; + $item->category = array(); + + // All featured articles are categorized as "Featured" + $item->category[] = Text::_('JFEATURED'); + + for ($item_category = $categories->get($row->catid); $item_category !== null; $item_category = $item_category->getParent()) { + // Only add non-root categories + if ($item_category->id > 1) { + $item->category[] = $item_category->title; + } + } + + $item->author = $author; + + if ($feedEmail === 'site') { + $item->authorEmail = $siteEmail; + } elseif ($feedEmail === 'author') { + $item->authorEmail = $row->author_email; + } + + // Add readmore link to description if introtext is shown, show_readmore is true and fulltext exists + if (!$params->get('feed_summary', 0) && $params->get('feed_show_readmore', 0) && $row->fulltext) { + $link = Route::_($link, true, $app->get('force_ssl') == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE, true); + $description .= '

    ' + . Text::_('COM_CONTENT_FEED_READMORE') . '

    '; + } + + // Load item description and add div + $item->description = '
    ' . $description . '
    '; + + // Loads item info into rss array + $this->document->addItem($item); + } + } } diff --git a/components/com_content/src/View/Featured/HtmlView.php b/components/com_content/src/View/Featured/HtmlView.php index e84d4d554da0a..2fccd5282f528 100644 --- a/components/com_content/src/View/Featured/HtmlView.php +++ b/components/com_content/src/View/Featured/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser(); - - $state = $this->get('State'); - $items = $this->get('Items'); - $pagination = $this->get('Pagination'); - - // Flag indicates to not add limitstart=0 to URL - $pagination->hideEmptyLimitstart = true; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - /** @var \Joomla\Registry\Registry $params */ - $params = &$state->params; - - // PREPARE THE DATA - - // Get the metrics for the structural page layout. - $numLeading = (int) $params->def('num_leading_articles', 1); - $numIntro = (int) $params->def('num_intro_articles', 4); - - PluginHelper::importPlugin('content'); - - // Compute the article slugs and prepare introtext (runs content plugins). - foreach ($items as &$item) - { - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - - // No link for ROOT category - if ($item->parent_alias === 'root') - { - $item->parent_id = null; - } - - $item->event = new \stdClass; - - // Old plugins: Ensure that text property is available - if (!isset($item->text)) - { - $item->text = $item->introtext; - } - - Factory::getApplication()->triggerEvent('onContentPrepare', array('com_content.featured', &$item, &$item->params, 0)); - - // Old plugins: Use processed text as introtext - $item->introtext = $item->text; - - $results = Factory::getApplication()->triggerEvent('onContentAfterTitle', array('com_content.featured', &$item, &$item->params, 0)); - $item->event->afterDisplayTitle = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentBeforeDisplay', array('com_content.featured', &$item, &$item->params, 0)); - $item->event->beforeDisplayContent = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentAfterDisplay', array('com_content.featured', &$item, &$item->params, 0)); - $item->event->afterDisplayContent = trim(implode("\n", $results)); - } - - // Preprocess the breakdown of leading, intro and linked articles. - // This makes it much easier for the designer to just integrate the arrays. - $max = count($items); - - // The first group is the leading articles. - $limit = $numLeading; - - for ($i = 0; $i < $limit && $i < $max; $i++) - { - $this->lead_items[$i] = &$items[$i]; - } - - // The second group is the intro articles. - $limit = $numLeading + $numIntro; - - // Order articles across, then down (or single column mode) - for ($i = $numLeading; $i < $limit && $i < $max; $i++) - { - $this->intro_items[$i] = &$items[$i]; - } - - // The remainder are the links. - for ($i = $numLeading + $numIntro; $i < $max; $i++) - { - $this->link_items[$i] = &$items[$i]; - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - - $this->params = &$params; - $this->items = &$items; - $this->pagination = &$pagination; - $this->user = &$user; - $this->db = Factory::getDbo(); - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - */ - protected function _prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('JGLOBAL_ARTICLES')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - - // Add feed links - if ($this->params->get('show_feed_link', 1)) - { - $link = '&format=feed&limitstart='; - $attribs = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); - $this->document->addHeadLink(Route::_($link . '&type=rss'), 'alternate', 'rel', $attribs); - $attribs = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); - $this->document->addHeadLink(Route::_($link . '&type=atom'), 'alternate', 'rel', $attribs); - } - } + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state = null; + + /** + * The featured articles array + * + * @var \stdClass[] + */ + protected $items = null; + + /** + * The pagination object. + * + * @var \Joomla\CMS\Pagination\Pagination + */ + protected $pagination = null; + + /** + * The featured articles to be displayed as lead items. + * + * @var \stdClass[] + */ + protected $lead_items = array(); + + /** + * The featured articles to be displayed as intro items. + * + * @var \stdClass[] + */ + protected $intro_items = array(); + + /** + * The featured articles to be displayed as link items. + * + * @var \stdClass[] + */ + protected $link_items = array(); + + /** + * @var \Joomla\Database\DatabaseDriver + * + * @since 3.6.3 + * + * @deprecated 5.0 Will be removed without replacement + */ + protected $db; + + /** + * The user object + * + * @var \Joomla\CMS\User\User|null + */ + protected $user = null; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $user = $this->getCurrentUser(); + + $state = $this->get('State'); + $items = $this->get('Items'); + $pagination = $this->get('Pagination'); + + // Flag indicates to not add limitstart=0 to URL + $pagination->hideEmptyLimitstart = true; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + /** @var \Joomla\Registry\Registry $params */ + $params = &$state->params; + + // PREPARE THE DATA + + // Get the metrics for the structural page layout. + $numLeading = (int) $params->def('num_leading_articles', 1); + $numIntro = (int) $params->def('num_intro_articles', 4); + + PluginHelper::importPlugin('content'); + + // Compute the article slugs and prepare introtext (runs content plugins). + foreach ($items as &$item) { + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + + // No link for ROOT category + if ($item->parent_alias === 'root') { + $item->parent_id = null; + } + + $item->event = new \stdClass(); + + // Old plugins: Ensure that text property is available + if (!isset($item->text)) { + $item->text = $item->introtext; + } + + Factory::getApplication()->triggerEvent('onContentPrepare', array('com_content.featured', &$item, &$item->params, 0)); + + // Old plugins: Use processed text as introtext + $item->introtext = $item->text; + + $results = Factory::getApplication()->triggerEvent('onContentAfterTitle', array('com_content.featured', &$item, &$item->params, 0)); + $item->event->afterDisplayTitle = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent('onContentBeforeDisplay', array('com_content.featured', &$item, &$item->params, 0)); + $item->event->beforeDisplayContent = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent('onContentAfterDisplay', array('com_content.featured', &$item, &$item->params, 0)); + $item->event->afterDisplayContent = trim(implode("\n", $results)); + } + + // Preprocess the breakdown of leading, intro and linked articles. + // This makes it much easier for the designer to just integrate the arrays. + $max = count($items); + + // The first group is the leading articles. + $limit = $numLeading; + + for ($i = 0; $i < $limit && $i < $max; $i++) { + $this->lead_items[$i] = &$items[$i]; + } + + // The second group is the intro articles. + $limit = $numLeading + $numIntro; + + // Order articles across, then down (or single column mode) + for ($i = $numLeading; $i < $limit && $i < $max; $i++) { + $this->intro_items[$i] = &$items[$i]; + } + + // The remainder are the links. + for ($i = $numLeading + $numIntro; $i < $max; $i++) { + $this->link_items[$i] = &$items[$i]; + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + + $this->params = &$params; + $this->items = &$items; + $this->pagination = &$pagination; + $this->user = &$user; + $this->db = Factory::getDbo(); + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + */ + protected function _prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('JGLOBAL_ARTICLES')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + + // Add feed links + if ($this->params->get('show_feed_link', 1)) { + $link = '&format=feed&limitstart='; + $attribs = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); + $this->document->addHeadLink(Route::_($link . '&type=rss'), 'alternate', 'rel', $attribs); + $attribs = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); + $this->document->addHeadLink(Route::_($link . '&type=atom'), 'alternate', 'rel', $attribs); + } + } } diff --git a/components/com_content/src/View/Form/HtmlView.php b/components/com_content/src/View/Form/HtmlView.php index eb685043ea54d..eb3ebd7e54bf1 100644 --- a/components/com_content/src/View/Form/HtmlView.php +++ b/components/com_content/src/View/Form/HtmlView.php @@ -1,4 +1,5 @@ getIdentity(); - - // Get model data. - $this->state = $this->get('State'); - $this->item = $this->get('Item'); - $this->form = $this->get('Form'); - $this->return_page = $this->get('ReturnPage'); - - if (empty($this->item->id)) - { - $catid = $this->state->params->get('catid'); - - if ($this->state->params->get('enable_category') == 1 && $catid) - { - $authorised = $user->authorise('core.create', 'com_content.category.' . $catid); - } - else - { - $authorised = $user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')); - } - } - else - { - $authorised = $this->item->params->get('access-edit'); - } - - if ($authorised !== true) - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $app->setHeader('status', 403, true); - - return false; - } - - $this->item->tags = new TagsHelper; - - if (!empty($this->item->id)) - { - $this->item->tags->getItemTags('com_content.article', $this->item->id); - - $this->item->images = json_decode($this->item->images); - $this->item->urls = json_decode($this->item->urls); - - $tmp = new \stdClass; - $tmp->images = $this->item->images; - $tmp->urls = $this->item->urls; - $this->form->bind($tmp); - } - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Create a shortcut to the parameters. - $params = &$this->state->params; - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - - $this->params = $params; - - // Override global params with article specific params - $this->params->merge($this->item->params); - $this->user = $user; - - // Propose current language as default when creating new article - if (empty($this->item->id) && Multilanguage::isEnabled() && $params->get('enable_category') != 1) - { - $lang = Factory::getLanguage()->getTag(); - $this->form->setFieldAttribute('language', 'default', $lang); - } - - $captchaSet = $params->get('captcha', Factory::getApplication()->get('captcha', '0')); - - foreach (PluginHelper::getPlugin('captcha') as $plugin) - { - if ($captchaSet === $plugin->name) - { - $this->captchaEnabled = true; - break; - } - } - - // If the article is being edited and the current user has permission to create article - if ($this->item->id - && ($user->authorise('core.create', 'com_content') || \count($user->getAuthorisedCategories('com_content', 'core.create')))) - { - $this->showSaveAsCopy = true; - } - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - */ - protected function _prepareDocument() - { - $app = Factory::getApplication(); - - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = $app->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_CONTENT_FORM_EDIT_ARTICLE')); - } - - $title = $this->params->def('page_title', Text::_('COM_CONTENT_FORM_EDIT_ARTICLE')); - - $this->setDocumentTitle($title); - - $app->getPathway()->addItem($title); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The item being created + * + * @var \stdClass + */ + protected $item; + + /** + * The page to return to after the article is submitted + * + * @var string + */ + protected $return_page = ''; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params = null; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The user object + * + * @var \Joomla\CMS\User\User + * + * @since 4.0.0 + */ + protected $user = null; + + /** + * Should we show a captcha form for the submission of the article? + * + * @var boolean + * + * @since 3.7.0 + */ + protected $captchaEnabled = false; + + /** + * Should we show Save As Copy button? + * + * @var boolean + * @since 4.1.0 + */ + protected $showSaveAsCopy = false; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void|boolean + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $user = $app->getIdentity(); + + // Get model data. + $this->state = $this->get('State'); + $this->item = $this->get('Item'); + $this->form = $this->get('Form'); + $this->return_page = $this->get('ReturnPage'); + + if (empty($this->item->id)) { + $catid = $this->state->params->get('catid'); + + if ($this->state->params->get('enable_category') == 1 && $catid) { + $authorised = $user->authorise('core.create', 'com_content.category.' . $catid); + } else { + $authorised = $user->authorise('core.create', 'com_content') || count($user->getAuthorisedCategories('com_content', 'core.create')); + } + } else { + $authorised = $this->item->params->get('access-edit'); + } + + if ($authorised !== true) { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $app->setHeader('status', 403, true); + + return false; + } + + $this->item->tags = new TagsHelper(); + + if (!empty($this->item->id)) { + $this->item->tags->getItemTags('com_content.article', $this->item->id); + + $this->item->images = json_decode($this->item->images); + $this->item->urls = json_decode($this->item->urls); + + $tmp = new \stdClass(); + $tmp->images = $this->item->images; + $tmp->urls = $this->item->urls; + $this->form->bind($tmp); + } + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Create a shortcut to the parameters. + $params = &$this->state->params; + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + + $this->params = $params; + + // Override global params with article specific params + $this->params->merge($this->item->params); + $this->user = $user; + + // Propose current language as default when creating new article + if (empty($this->item->id) && Multilanguage::isEnabled() && $params->get('enable_category') != 1) { + $lang = Factory::getLanguage()->getTag(); + $this->form->setFieldAttribute('language', 'default', $lang); + } + + $captchaSet = $params->get('captcha', Factory::getApplication()->get('captcha', '0')); + + foreach (PluginHelper::getPlugin('captcha') as $plugin) { + if ($captchaSet === $plugin->name) { + $this->captchaEnabled = true; + break; + } + } + + // If the article is being edited and the current user has permission to create article + if ( + $this->item->id + && ($user->authorise('core.create', 'com_content') || \count($user->getAuthorisedCategories('com_content', 'core.create'))) + ) { + $this->showSaveAsCopy = true; + } + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + */ + protected function _prepareDocument() + { + $app = Factory::getApplication(); + + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = $app->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_CONTENT_FORM_EDIT_ARTICLE')); + } + + $title = $this->params->def('page_title', Text::_('COM_CONTENT_FORM_EDIT_ARTICLE')); + + $this->setDocumentTitle($title); + + $app->getPathway()->addItem($title); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_content/tmpl/archive/default.php b/components/com_content/tmpl/archive/default.php index 95d7a2f98b094..e749e93a4554b 100644 --- a/components/com_content/tmpl/archive/default.php +++ b/components/com_content/tmpl/archive/default.php @@ -1,4 +1,5 @@
    params->get('show_page_heading')) : ?> - +
    -
    - +
    loadTemplate('items'); ?>
    diff --git a/components/com_content/tmpl/archive/default_items.php b/components/com_content/tmpl/archive/default_items.php index 5e83c2b56262d..e080d636878a7 100644 --- a/components/com_content/tmpl/archive/default_items.php +++ b/components/com_content/tmpl/archive/default_items.php @@ -1,4 +1,5 @@ params; ?>
    - items as $i => $item) : ?> - params->get('info_block_position', 0); ?> -
    - + + + event->afterDisplayContent; ?> +
    +
    - params->def('show_pagination_results', 1)) : ?> -

    - pagination->getPagesCounter(); ?> -

    - -
    - pagination->getPagesLinks(); ?> -
    + params->def('show_pagination_results', 1)) : ?> +

    + pagination->getPagesCounter(); ?> +

    + +
    + pagination->getPagesLinks(); ?> +
    diff --git a/components/com_content/tmpl/article/default.php b/components/com_content/tmpl/article/default.php index 160dae0ef1ada..41e4d6394caf0 100644 --- a/components/com_content/tmpl/article/default.php +++ b/components/com_content/tmpl/article/default.php @@ -1,4 +1,5 @@ item->publish_down) && $this->item->publish_down < $currentDate; ?>
    - - params->get('show_page_heading')) : ?> - - item->pagination) && !$this->item->paginationposition && $this->item->paginationrelative) - { - echo $this->item->pagination; - } - ?> + + params->get('show_page_heading')) : ?> + + item->pagination) && !$this->item->paginationposition && $this->item->paginationrelative) { + echo $this->item->pagination; + } + ?> - get('show_modify_date') || $params->get('show_publish_date') || $params->get('show_create_date') - || $params->get('show_hits') || $params->get('show_category') || $params->get('show_parent_category') || $params->get('show_author') || $assocParam; ?> + get('show_modify_date') || $params->get('show_publish_date') || $params->get('show_create_date') + || $params->get('show_hits') || $params->get('show_category') || $params->get('show_parent_category') || $params->get('show_author') || $assocParam; ?> - get('show_title')) : ?> - - - - $params, 'item' => $this->item)); ?> - + get('show_title')) : ?> + + + + $params, 'item' => $this->item)); ?> + - - item->event->afterDisplayTitle; ?> + + item->event->afterDisplayTitle; ?> - - $this->item, 'params' => $params, 'position' => 'above')); ?> - + + $this->item, 'params' => $params, 'position' => 'above')); ?> + - get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> - item->tagLayout = new FileLayout('joomla.content.tags'); ?> + get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> + item->tagLayout = new FileLayout('joomla.content.tags'); ?> - item->tagLayout->render($this->item->tags->itemTags); ?> - + item->tagLayout->render($this->item->tags->itemTags); ?> + - - item->event->beforeDisplayContent; ?> + + item->event->beforeDisplayContent; ?> - get('urls_position', 0) === 0) : ?> - loadTemplate('links'); ?> - - get('access-view')) : ?> - item); ?> - item->pagination) && !$this->item->paginationposition && !$this->item->paginationrelative) : - echo $this->item->pagination; - endif; - ?> - item->toc)) : - echo $this->item->toc; - endif; ?> -
    - item->text; ?> -
    + get('urls_position', 0) === 0) : ?> + loadTemplate('links'); ?> + + get('access-view')) : ?> + item); ?> + item->pagination) && !$this->item->paginationposition && !$this->item->paginationrelative) : + echo $this->item->pagination; + endif; + ?> + item->toc)) : + echo $this->item->toc; + endif; ?> +
    + item->text; ?> +
    - - - $this->item, 'params' => $params, 'position' => 'below')); ?> - - get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> - item->tagLayout = new FileLayout('joomla.content.tags'); ?> - item->tagLayout->render($this->item->tags->itemTags); ?> - - + + + $this->item, 'params' => $params, 'position' => 'below')); ?> + + get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> + item->tagLayout = new FileLayout('joomla.content.tags'); ?> + item->tagLayout->render($this->item->tags->itemTags); ?> + + - item->pagination) && $this->item->paginationposition && !$this->item->paginationrelative) : - echo $this->item->pagination; - ?> - - get('urls_position', 0) === 1) : ?> - loadTemplate('links'); ?> - - - get('show_noauth') == true && $user->get('guest')) : ?> - item); ?> - item->introtext); ?> - - get('show_readmore') && $this->item->fulltext != null) : ?> - getMenu(); ?> - getActive(); ?> - id; ?> - - setVar('return', base64_encode(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language))); ?> - $this->item, 'params' => $params, 'link' => $link)); ?> - - - item->pagination) && $this->item->paginationposition && $this->item->paginationrelative) : - echo $this->item->pagination; - ?> - - - item->event->afterDisplayContent; ?> + item->pagination) && $this->item->paginationposition && !$this->item->paginationrelative) : + echo $this->item->pagination; + ?> + + get('urls_position', 0) === 1) : ?> + loadTemplate('links'); ?> + + + get('show_noauth') == true && $user->get('guest')) : ?> + item); ?> + item->introtext); ?> + + get('show_readmore') && $this->item->fulltext != null) : ?> + getMenu(); ?> + getActive(); ?> + id; ?> + + setVar('return', base64_encode(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language))); ?> + $this->item, 'params' => $params, 'link' => $link)); ?> + + + item->pagination) && $this->item->paginationposition && $this->item->paginationrelative) : + echo $this->item->pagination; + ?> + + + item->event->afterDisplayContent; ?>
    diff --git a/components/com_content/tmpl/article/default_links.php b/components/com_content/tmpl/article/default_links.php index 51e25ed33cca2..a8e4c427406b9 100644 --- a/components/com_content/tmpl/article/default_links.php +++ b/components/com_content/tmpl/article/default_links.php @@ -1,4 +1,5 @@ item->params; if ($urls && (!empty($urls->urla) || !empty($urls->urlb) || !empty($urls->urlc))) : -?> + ?>
    -
      - urla, $urls->urlatext, $urls->targeta, 'a'), - array($urls->urlb, $urls->urlbtext, $urls->targetb, 'b'), - array($urls->urlc, $urls->urlctext, $urls->targetc, 'c') - ); - foreach ($urlarray as $url) : - $link = $url[0]; - $label = $url[1]; - $target = $url[2]; - $id = $url[3]; +
        + urla, $urls->urlatext, $urls->targeta, 'a'), + array($urls->urlb, $urls->urlbtext, $urls->targetb, 'b'), + array($urls->urlc, $urls->urlctext, $urls->targetc, 'c') + ); + foreach ($urlarray as $url) : + $link = $url[0]; + $label = $url[1]; + $target = $url[2]; + $id = $url[3]; - if ( ! $link) : - continue; - endif; + if (! $link) : + continue; + endif; - // If no label is present, take the link - $label = $label ?: $link; + // If no label is present, take the link + $label = $label ?: $link; - // If no target is present, use the default - $target = $target ?: $params->get('target' . $id); - ?> -
      • - get('target' . $id); + ?> +
      • + ' . - htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ''; - break; + switch ($target) { + case 1: + // Open in a new window + echo '' . + htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ''; + break; - case 2: - // Open in a popup window - $attribs = 'toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=600,height=600'; - echo "" . - htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ''; - break; - case 3: - echo '' . - htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ' '; - echo HTMLHelper::_( - 'bootstrap.renderModal', - 'linkModal', - array( - 'url' => $link, - 'title' => $label, - 'height' => '100%', - 'width' => '100%', - 'modalWidth' => '500', - 'bodyHeight' => '500', - 'footer' => '' - ) - ); - break; + case 2: + // Open in a popup window + $attribs = 'toolbar=no,location=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=600,height=600'; + echo "" . + htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ''; + break; + case 3: + echo '' . + htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ' '; + echo HTMLHelper::_( + 'bootstrap.renderModal', + 'linkModal', + array( + 'url' => $link, + 'title' => $label, + 'height' => '100%', + 'width' => '100%', + 'modalWidth' => '500', + 'bodyHeight' => '500', + 'footer' => '' + ) + ); + break; - default: - // Open in parent window - echo '' . - htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ' '; - break; - } - ?> -
      • - -
      + default: + // Open in parent window + echo '' . + htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ' '; + break; + } + ?> + + +
    diff --git a/components/com_content/tmpl/categories/default.php b/components/com_content/tmpl/categories/default.php index 19a6961ac357e..a1bd6b924c19b 100644 --- a/components/com_content/tmpl/categories/default.php +++ b/components/com_content/tmpl/categories/default.php @@ -1,4 +1,5 @@
    - loadTemplate('items'); - ?> + loadTemplate('items'); + ?>
    diff --git a/components/com_content/tmpl/categories/default_items.php b/components/com_content/tmpl/categories/default_items.php index a591a4a8e4fb8..2ed17e9fbaa03 100644 --- a/components/com_content/tmpl/categories/default_items.php +++ b/components/com_content/tmpl/categories/default_items.php @@ -1,4 +1,5 @@ maxLevelcat != 0 && count($this->items[$this->parent->id]) > 0) : -?> -
    - items[$this->parent->id] as $id => $item) : ?> - params->get('show_empty_categories_cat') || $item->numitems || count($item->getChildren())) : ?> -
    -
    -
    - - escape($item->title); ?> - params->get('show_cat_num_articles_cat') == 1) :?> - -   - numitems; ?> - - -
    - getChildren()) > 0 && $this->maxLevelcat > 1) : ?> - - -
    - params->get('show_description_image') && $item->getParams()->get('image')) : ?> - getParams()->get('image'), $item->getParams()->get('image_alt')); ?> - - params->get('show_subcat_desc_cat') == 1) : ?> - description) : ?> -
    - description, '', 'com_content.categories'); ?> -
    - - + ?> +
    + items[$this->parent->id] as $id => $item) : ?> + params->get('show_empty_categories_cat') || $item->numitems || count($item->getChildren())) : ?> +
    +
    +
    + + escape($item->title); ?> + params->get('show_cat_num_articles_cat') == 1) :?> + +   + numitems; ?> + + +
    + getChildren()) > 0 && $this->maxLevelcat > 1) : ?> + + +
    + params->get('show_description_image') && $item->getParams()->get('image')) : ?> + getParams()->get('image'), $item->getParams()->get('image_alt')); ?> + + params->get('show_subcat_desc_cat') == 1) : ?> + description) : ?> +
    + description, '', 'com_content.categories'); ?> +
    + + - getChildren()) > 0 && $this->maxLevelcat > 1) : ?> - - -
    - - -
    + getChildren()) > 0 && $this->maxLevelcat > 1) : ?> + + +
    + + +
    diff --git a/components/com_content/tmpl/category/blog.php b/components/com_content/tmpl/category/blog.php index cd6d656572c82..d25185dd63ddf 100644 --- a/components/com_content/tmpl/category/blog.php +++ b/components/com_content/tmpl/category/blog.php @@ -1,4 +1,5 @@ diff --git a/components/com_content/tmpl/category/blog_children.php b/components/com_content/tmpl/category/blog_children.php index 7f4c921b13df8..6757330021bb8 100644 --- a/components/com_content/tmpl/category/blog_children.php +++ b/components/com_content/tmpl/category/blog_children.php @@ -1,4 +1,5 @@ getAuthorisedViewLevels(); if ($this->maxLevel != 0 && count($this->children[$this->category->id]) > 0) : ?> + children[$this->category->id] as $id => $child) : ?> + + access, $groups)) : ?> + params->get('show_empty_categories') || $child->numitems || count($child->getChildren())) : ?> + - - - + maxLevel > 1 && count($child->getChildren()) > 0) : ?> + + + + + + format('Y-m-d H:i:s'); $isUnpublished = ($this->item->state == ContentComponent::CONDITION_UNPUBLISHED || $this->item->publish_up > $currentDate) - || ($this->item->publish_down < $currentDate && $this->item->publish_down !== null); + || ($this->item->publish_down < $currentDate && $this->item->publish_down !== null); ?> item); ?>
    - -
    - - - item); ?> - - - $params, 'item' => $this->item)); ?> - - - - get('show_modify_date') || $params->get('show_publish_date') || $params->get('show_create_date') - || $params->get('show_hits') || $params->get('show_category') || $params->get('show_parent_category') || $params->get('show_author') || $assocParam); ?> - - - $this->item, 'params' => $params, 'position' => 'above')); ?> - - get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> - item->tags->itemTags); ?> - - - get('show_intro')) : ?> - - item->event->afterDisplayTitle; ?> - - - - item->event->beforeDisplayContent; ?> - - item->introtext; ?> - - - - $this->item, 'params' => $params, 'position' => 'below')); ?> - - get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> - item->tags->itemTags); ?> - - - - get('show_readmore') && $this->item->readmore) : - if ($params->get('access-view')) : - $link = Route::_(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language)); - else : - $menu = Factory::getApplication()->getMenu(); - $active = $menu->getActive(); - $itemId = $active->id; - $link = new Uri(Route::_('index.php?option=com_users&view=login&Itemid=' . $itemId, false)); - $link->setVar('return', base64_encode(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language))); - endif; ?> - - $this->item, 'params' => $params, 'link' => $link)); ?> - - - - -
    - - - - item->event->afterDisplayContent; ?> + +
    + + + item); ?> + + + $params, 'item' => $this->item)); ?> + + + + get('show_modify_date') || $params->get('show_publish_date') || $params->get('show_create_date') + || $params->get('show_hits') || $params->get('show_category') || $params->get('show_parent_category') || $params->get('show_author') || $assocParam); ?> + + + $this->item, 'params' => $params, 'position' => 'above')); ?> + + get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> + item->tags->itemTags); ?> + + + get('show_intro')) : ?> + + item->event->afterDisplayTitle; ?> + + + + item->event->beforeDisplayContent; ?> + + item->introtext; ?> + + + + $this->item, 'params' => $params, 'position' => 'below')); ?> + + get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> + item->tags->itemTags); ?> + + + + get('show_readmore') && $this->item->readmore) : + if ($params->get('access-view')) : + $link = Route::_(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language)); + else : + $menu = Factory::getApplication()->getMenu(); + $active = $menu->getActive(); + $itemId = $active->id; + $link = new Uri(Route::_('index.php?option=com_users&view=login&Itemid=' . $itemId, false)); + $link->setVar('return', base64_encode(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language))); + endif; ?> + + $this->item, 'params' => $params, 'link' => $link)); ?> + + + + +
    + + + + item->event->afterDisplayContent; ?>
    diff --git a/components/com_content/tmpl/category/blog_links.php b/components/com_content/tmpl/category/blog_links.php index 0027a357a6f1b..83250ff6b89c8 100644 --- a/components/com_content/tmpl/category/blog_links.php +++ b/components/com_content/tmpl/category/blog_links.php @@ -1,4 +1,5 @@ diff --git a/components/com_content/tmpl/category/default.php b/components/com_content/tmpl/category/default.php index ab946a6c69bf2..d2e30dc61c26c 100644 --- a/components/com_content/tmpl/category/default.php +++ b/components/com_content/tmpl/category/default.php @@ -1,4 +1,5 @@ params->get('filter_field') === 'tag') && (Multilanguage::isEnabled())) -{ - $tagfilter = ComponentHelper::getParams('com_tags')->get('tag_list_language_filter'); +if (($this->params->get('filter_field') === 'tag') && (Multilanguage::isEnabled())) { + $tagfilter = ComponentHelper::getParams('com_tags')->get('tag_list_language_filter'); - switch ($tagfilter) - { - case 'current_language': - $langFilter = Factory::getApplication()->getLanguage()->getTag(); - break; + switch ($tagfilter) { + case 'current_language': + $langFilter = Factory::getApplication()->getLanguage()->getTag(); + break; - case 'all': - $langFilter = false; - break; + case 'all': + $langFilter = false; + break; - default: - $langFilter = $tagfilter; - } + default: + $langFilter = $tagfilter; + } } // Check for at least one editable article $isEditable = false; -if (!empty($this->items)) -{ - foreach ($this->items as $article) - { - if ($article->params->get('access-edit')) - { - $isEditable = true; - break; - } - } +if (!empty($this->items)) { + foreach ($this->items as $article) { + if ($article->params->get('access-edit')) { + $isEditable = true; + break; + } + } } $currentDate = Factory::getDate()->format('Y-m-d H:i:s'); ?> diff --git a/components/com_content/tmpl/category/default_children.php b/components/com_content/tmpl/category/default_children.php index bb8b5334f1ce3..21d741a432e5d 100644 --- a/components/com_content/tmpl/category/default_children.php +++ b/components/com_content/tmpl/category/default_children.php @@ -1,4 +1,5 @@ children[$this->category->id]) > 0) : ?> - children[$this->category->id] as $id => $child) : ?> - - access, $groups)) : ?> - params->get('show_empty_categories') || $child->getNumItems(true) || count($child->getChildren())) : ?> - - - - + + + + diff --git a/components/com_content/tmpl/featured/default.php b/components/com_content/tmpl/featured/default.php index fd46e43cd6258..04bc1bdd8ef14 100644 --- a/components/com_content/tmpl/featured/default.php +++ b/components/com_content/tmpl/featured/default.php @@ -1,4 +1,5 @@ diff --git a/components/com_content/tmpl/featured/default_item.php b/components/com_content/tmpl/featured/default_item.php index 250764ebd9204..1d43493d89273 100644 --- a/components/com_content/tmpl/featured/default_item.php +++ b/components/com_content/tmpl/featured/default_item.php @@ -1,4 +1,5 @@ item); ?>
    - -
    - - - get('show_title')) : ?> -

    - get('link_titles') && $params->get('access-view')) : ?> - - - escape($this->item->title); ?> - -

    - - - item->state == ContentComponent::CONDITION_UNPUBLISHED) : ?> - - - - - - - - - - - $params, 'item' => $this->item)); ?> - - - - item->event->afterDisplayTitle; ?> - - - get('show_modify_date') || $params->get('show_publish_date') || $params->get('show_create_date') - || $params->get('show_hits') || $params->get('show_category') || $params->get('show_parent_category') || $params->get('show_author') || $assocParam); ?> - - - $this->item, 'params' => $params, 'position' => 'above')); ?> - - get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> - item->tags->itemTags); ?> - - - - item->event->beforeDisplayContent; ?> - - item->introtext; ?> - - - - $this->item, 'params' => $params, 'position' => 'below')); ?> - - get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> - item->tags->itemTags); ?> - - - - get('show_readmore') && $this->item->readmore) : - if ($params->get('access-view')) : - $link = Route::_(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language)); - else : - $menu = Factory::getApplication()->getMenu(); - $active = $menu->getActive(); - $itemId = $active->id; - $link = new Uri(Route::_('index.php?option=com_users&view=login&Itemid=' . $itemId, false)); - $link->setVar('return', base64_encode(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language))); - endif; ?> - - $this->item, 'params' => $params, 'link' => $link)); ?> - - - - -
    - + +
    + + + get('show_title')) : ?> +

    + get('link_titles') && $params->get('access-view')) : ?> + + + escape($this->item->title); ?> + +

    + + + item->state == ContentComponent::CONDITION_UNPUBLISHED) : ?> + + + + + + + + + + + $params, 'item' => $this->item)); ?> + + + + item->event->afterDisplayTitle; ?> + + + get('show_modify_date') || $params->get('show_publish_date') || $params->get('show_create_date') + || $params->get('show_hits') || $params->get('show_category') || $params->get('show_parent_category') || $params->get('show_author') || $assocParam); ?> + + + $this->item, 'params' => $params, 'position' => 'above')); ?> + + get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> + item->tags->itemTags); ?> + + + + item->event->beforeDisplayContent; ?> + + item->introtext; ?> + + + + $this->item, 'params' => $params, 'position' => 'below')); ?> + + get('show_tags', 1) && !empty($this->item->tags->itemTags)) : ?> + item->tags->itemTags); ?> + + + + get('show_readmore') && $this->item->readmore) : + if ($params->get('access-view')) : + $link = Route::_(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language)); + else : + $menu = Factory::getApplication()->getMenu(); + $active = $menu->getActive(); + $itemId = $active->id; + $link = new Uri(Route::_('index.php?option=com_users&view=login&Itemid=' . $itemId, false)); + $link->setVar('return', base64_encode(RouteHelper::getArticleRoute($this->item->slug, $this->item->catid, $this->item->language))); + endif; ?> + + $this->item, 'params' => $params, 'link' => $link)); ?> + + + + +
    +
    diff --git a/components/com_content/tmpl/featured/default_links.php b/components/com_content/tmpl/featured/default_links.php index 3f87a6826ace4..083f2b9f89afc 100644 --- a/components/com_content/tmpl/featured/default_links.php +++ b/components/com_content/tmpl/featured/default_links.php @@ -1,4 +1,5 @@ diff --git a/components/com_content/tmpl/form/edit.php b/components/com_content/tmpl/form/edit.php index 1437c88d14546..7477afd63e7a6 100644 --- a/components/com_content/tmpl/form/edit.php +++ b/components/com_content/tmpl/form/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate') - ->useScript('com_content.form-edit'); + ->useScript('form.validate') + ->useScript('com_content.form-edit'); $this->tab_name = 'com-content-form'; $this->ignore_fieldsets = array('image-intro', 'image-full', 'jmetadata', 'item_associations'); @@ -31,153 +32,152 @@ // This checks if the editor config options have ever been saved. If they haven't they will fall back to the original settings. $editoroptions = isset($params->show_publishing_options); -if (!$editoroptions) -{ - $params->show_urls_images_frontend = '0'; +if (!$editoroptions) { + $params->show_urls_images_frontend = '0'; } ?>
    - get('show_page_heading')) : ?> - - - -
    -
    - tab_name, ['active' => 'editor', 'recall' => true, 'breakpoint' => 768]); ?> - - tab_name, 'editor', Text::_('COM_CONTENT_ARTICLE_CONTENT')); ?> - form->renderField('title'); ?> - - item->id)) : ?> - form->renderField('alias'); ?> - - - form->renderField('articletext'); ?> - - captchaEnabled) : ?> - form->renderField('captcha'); ?> - - - - get('show_urls_images_frontend')) : ?> - tab_name, 'images', Text::_('COM_CONTENT_IMAGES_AND_URLS')); ?> - form->renderField('image_intro', 'images'); ?> - form->renderField('image_intro_alt', 'images'); ?> - form->renderField('image_intro_alt_empty', 'images'); ?> - form->renderField('image_intro_caption', 'images'); ?> - form->renderField('float_intro', 'images'); ?> - form->renderField('image_fulltext', 'images'); ?> - form->renderField('image_fulltext_alt', 'images'); ?> - form->renderField('image_fulltext_alt_empty', 'images'); ?> - form->renderField('image_fulltext_caption', 'images'); ?> - form->renderField('float_fulltext', 'images'); ?> - form->renderField('urla', 'urls'); ?> - form->renderField('urlatext', 'urls'); ?> -
    -
    - form->getInput('targeta', 'urls'); ?> -
    -
    - form->renderField('urlb', 'urls'); ?> - form->renderField('urlbtext', 'urls'); ?> -
    -
    - form->getInput('targetb', 'urls'); ?> -
    -
    - form->renderField('urlc', 'urls'); ?> - form->renderField('urlctext', 'urls'); ?> -
    -
    - form->getInput('targetc', 'urls'); ?> -
    -
    - - - - - - tab_name, 'publishing', Text::_('COM_CONTENT_PUBLISHING')); ?> - - form->renderField('transition'); ?> - form->renderField('state'); ?> - form->renderField('catid'); ?> - form->renderField('tags'); ?> - form->renderField('note'); ?> - get('save_history', 0)) : ?> - form->renderField('version_note'); ?> - - get('show_publishing_options', 1) == 1) : ?> - form->renderField('created_by_alias'); ?> - - item->params->get('access-change')) : ?> - form->renderField('featured'); ?> - get('show_publishing_options', 1) == 1) : ?> - form->renderField('featured_up'); ?> - form->renderField('featured_down'); ?> - form->renderField('publish_up'); ?> - form->renderField('publish_down'); ?> - - - form->renderField('access'); ?> - item->id)) : ?> -
    -
    -
    -
    - -
    -
    - - - - - tab_name, 'language', Text::_('JFIELD_LANGUAGE_LABEL')); ?> - form->renderField('language'); ?> - - - form->renderField('language'); ?> - - - get('show_publishing_options', 1) == 1) : ?> - tab_name, 'metadata', Text::_('COM_CONTENT_METADATA')); ?> - form->renderField('metadesc'); ?> - form->renderField('metakey'); ?> - - - - - - - - -
    -
    - - - showSaveAsCopy) : ?> - - - - get('save_history', 0) && $this->item->id) : ?> - form->getInput('contenthistory'); ?> - -
    -
    + get('show_page_heading')) : ?> + + + +
    +
    + tab_name, ['active' => 'editor', 'recall' => true, 'breakpoint' => 768]); ?> + + tab_name, 'editor', Text::_('COM_CONTENT_ARTICLE_CONTENT')); ?> + form->renderField('title'); ?> + + item->id)) : ?> + form->renderField('alias'); ?> + + + form->renderField('articletext'); ?> + + captchaEnabled) : ?> + form->renderField('captcha'); ?> + + + + get('show_urls_images_frontend')) : ?> + tab_name, 'images', Text::_('COM_CONTENT_IMAGES_AND_URLS')); ?> + form->renderField('image_intro', 'images'); ?> + form->renderField('image_intro_alt', 'images'); ?> + form->renderField('image_intro_alt_empty', 'images'); ?> + form->renderField('image_intro_caption', 'images'); ?> + form->renderField('float_intro', 'images'); ?> + form->renderField('image_fulltext', 'images'); ?> + form->renderField('image_fulltext_alt', 'images'); ?> + form->renderField('image_fulltext_alt_empty', 'images'); ?> + form->renderField('image_fulltext_caption', 'images'); ?> + form->renderField('float_fulltext', 'images'); ?> + form->renderField('urla', 'urls'); ?> + form->renderField('urlatext', 'urls'); ?> +
    +
    + form->getInput('targeta', 'urls'); ?> +
    +
    + form->renderField('urlb', 'urls'); ?> + form->renderField('urlbtext', 'urls'); ?> +
    +
    + form->getInput('targetb', 'urls'); ?> +
    +
    + form->renderField('urlc', 'urls'); ?> + form->renderField('urlctext', 'urls'); ?> +
    +
    + form->getInput('targetc', 'urls'); ?> +
    +
    + + + + + + tab_name, 'publishing', Text::_('COM_CONTENT_PUBLISHING')); ?> + + form->renderField('transition'); ?> + form->renderField('state'); ?> + form->renderField('catid'); ?> + form->renderField('tags'); ?> + form->renderField('note'); ?> + get('save_history', 0)) : ?> + form->renderField('version_note'); ?> + + get('show_publishing_options', 1) == 1) : ?> + form->renderField('created_by_alias'); ?> + + item->params->get('access-change')) : ?> + form->renderField('featured'); ?> + get('show_publishing_options', 1) == 1) : ?> + form->renderField('featured_up'); ?> + form->renderField('featured_down'); ?> + form->renderField('publish_up'); ?> + form->renderField('publish_down'); ?> + + + form->renderField('access'); ?> + item->id)) : ?> +
    +
    +
    +
    + +
    +
    + + + + + tab_name, 'language', Text::_('JFIELD_LANGUAGE_LABEL')); ?> + form->renderField('language'); ?> + + + form->renderField('language'); ?> + + + get('show_publishing_options', 1) == 1) : ?> + tab_name, 'metadata', Text::_('COM_CONTENT_METADATA')); ?> + form->renderField('metadesc'); ?> + form->renderField('metakey'); ?> + + + + + + + + +
    +
    + + + showSaveAsCopy) : ?> + + + + get('save_history', 0) && $this->item->id) : ?> + form->getInput('contenthistory'); ?> + +
    +
    diff --git a/components/com_contenthistory/src/Controller/DisplayController.php b/components/com_contenthistory/src/Controller/DisplayController.php index 29bf90b0df382..4e15dae3a72eb 100644 --- a/components/com_contenthistory/src/Controller/DisplayController.php +++ b/components/com_contenthistory/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ app->getLanguage()->load($this->option, JPATH_ADMINISTRATOR) || - $this->app->getLanguage()->load($this->option, JPATH_SITE); - } + /** + * Load the language + * + * @since 4.0.0 + * + * @return void + */ + protected function loadLanguage() + { + // Load common and local language files. + $this->app->getLanguage()->load($this->option, JPATH_ADMINISTRATOR) || + $this->app->getLanguage()->load($this->option, JPATH_SITE); + } - /** - * Method to check component access permission - * - * @since 4.0.0 - * - * @return void - * - * @throws \Exception|NotAllowed - */ - protected function checkAccess() - { - // Check the user has permission to access this component if in the backend - if ($this->app->getIdentity()->guest) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + /** + * Method to check component access permission + * + * @since 4.0.0 + * + * @return void + * + * @throws \Exception|NotAllowed + */ + protected function checkAccess() + { + // Check the user has permission to access this component if in the backend + if ($this->app->getIdentity()->guest) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } - /** - * 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 4.0.0 - */ - public function getController(string $name, string $client = '', array $config = array()): BaseController - { - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - $client = 'Administrator'; + /** + * 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 4.0.0 + */ + public function getController(string $name, string $client = '', array $config = array()): BaseController + { + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + $client = 'Administrator'; - return parent::getController($name, $client, $config); - } + return parent::getController($name, $client, $config); + } } diff --git a/components/com_fields/layouts/field/render.php b/components/com_fields/layouts/field/render.php index 0623fd7355a39..cd6e4144e044f 100644 --- a/components/com_fields/layouts/field/render.php +++ b/components/com_fields/layouts/field/render.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\CMS\Language\Text; -if (!array_key_exists('field', $displayData)) -{ - return; +if (!array_key_exists('field', $displayData)) { + return; } $field = $displayData['field']; @@ -24,19 +25,18 @@ $labelClass = $field->params->get('label_render_class'); $valueClass = $field->params->get('value_render_class'); -if ($value == '') -{ - return; +if ($value == '') { + return; } ?> - : + : - + - + diff --git a/components/com_fields/layouts/fields/render.php b/components/com_fields/layouts/fields/render.php index c84630be6bed9..80a8a5c4a0801 100644 --- a/components/com_fields/layouts/fields/render.php +++ b/components/com_fields/layouts/fields/render.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ + defined('_JEXEC') or die; use Joomla\Component\Fields\Administrator\Helper\FieldsHelper; // Check if we have all the data -if (!array_key_exists('item', $displayData) || !array_key_exists('context', $displayData)) -{ - return; +if (!array_key_exists('item', $displayData) || !array_key_exists('context', $displayData)) { + return; } // Setting up for display $item = $displayData['item']; -if (!$item) -{ - return; +if (!$item) { + return; } $context = $displayData['context']; -if (!$context) -{ - return; +if (!$context) { + return; } $parts = explode('.', $context); $component = $parts[0]; $fields = null; -if (array_key_exists('fields', $displayData)) -{ - $fields = $displayData['fields']; -} -else -{ - $fields = $item->jcfields ?: FieldsHelper::getFields($context, $item, true); +if (array_key_exists('fields', $displayData)) { + $fields = $displayData['fields']; +} else { + $fields = $item->jcfields ?: FieldsHelper::getFields($context, $item, true); } -if (empty($fields)) -{ - return; +if (empty($fields)) { + return; } $output = array(); -foreach ($fields as $field) -{ - // If the value is empty do nothing - if (!isset($field->value) || trim($field->value) === '') - { - continue; - } - - $class = $field->name . ' ' . $field->params->get('render_class'); - $layout = $field->params->get('layout', 'render'); - $content = FieldsHelper::render($context, 'field.' . $layout, array('field' => $field)); - - // If the content is empty do nothing - if (trim($content) === '') - { - continue; - } - - $output[] = '
  • ' . $content . '
  • '; +foreach ($fields as $field) { + // If the value is empty do nothing + if (!isset($field->value) || trim($field->value) === '') { + continue; + } + + $class = $field->name . ' ' . $field->params->get('render_class'); + $layout = $field->params->get('layout', 'render'); + $content = FieldsHelper::render($context, 'field.' . $layout, array('field' => $field)); + + // If the content is empty do nothing + if (trim($content) === '') { + continue; + } + + $output[] = '
  • ' . $content . '
  • '; } -if (empty($output)) -{ - return; +if (empty($output)) { + return; } ?> diff --git a/components/com_fields/src/Controller/DisplayController.php b/components/com_fields/src/Controller/DisplayController.php index d96c9257159eb..806d2a867e44c 100644 --- a/components/com_fields/src/Controller/DisplayController.php +++ b/components/com_fields/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ get('view') === 'fields' && $input->get('layout') === 'modal') { + // Load the backend language file. + $app->getLanguage()->load('com_fields', JPATH_ADMINISTRATOR); - /** - * @param array $config An optional associative array of configuration settings. - * Recognized key values include 'name', 'default_task', 'model_path', and - * 'view_path' (this list is not meant to be comprehensive). - * @param MVCFactoryInterface|null $factory The factory. - * @param CMSApplication|null $app The Application for the dispatcher - * @param \Joomla\CMS\Input\Input|null $input The request's input object - * - * @since 3.7.0 - */ - public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) - { - // Frontpage Editor Fields Button proxying. - if ($input->get('view') === 'fields' && $input->get('layout') === 'modal') - { - // Load the backend language file. - $app->getLanguage()->load('com_fields', JPATH_ADMINISTRATOR); - - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - } + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + } - parent::__construct($config, $factory, $app, $input); - } + parent::__construct($config, $factory, $app, $input); + } } diff --git a/components/com_fields/src/Dispatcher/Dispatcher.php b/components/com_fields/src/Dispatcher/Dispatcher.php index af013bbc662be..b7b061e56e42b 100644 --- a/components/com_fields/src/Dispatcher/Dispatcher.php +++ b/components/com_fields/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ input->get('view') !== 'fields' || $this->input->get('layout') !== 'modal') - { - return; - } + if ($this->input->get('view') !== 'fields' || $this->input->get('layout') !== 'modal') { + return; + } - $context = $this->app->getUserStateFromRequest('com_fields.fields.context', 'context', 'com_content.article', 'CMD'); - $parts = FieldsHelper::extract($context); + $context = $this->app->getUserStateFromRequest('com_fields.fields.context', 'context', 'com_content.article', 'CMD'); + $parts = FieldsHelper::extract($context); - if (!$this->app->getIdentity()->authorise('core.create', $parts[0]) - || !$this->app->getIdentity()->authorise('core.edit', $parts[0])) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR')); - } - } + if ( + !$this->app->getIdentity()->authorise('core.create', $parts[0]) + || !$this->app->getIdentity()->authorise('core.edit', $parts[0]) + ) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR')); + } + } } diff --git a/components/com_finder/helpers/route.php b/components/com_finder/helpers/route.php index 4cfe9367583a9..70a77df17ced2 100644 --- a/components/com_finder/helpers/route.php +++ b/components/com_finder/helpers/route.php @@ -1,13 +1,14 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt - */ -defined('_JEXEC') or die; + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + */ use Joomla\Component\Finder\Site\Helper\RouteHelper; diff --git a/components/com_finder/src/Controller/DisplayController.php b/components/com_finder/src/Controller/DisplayController.php index 5552cd63f6adb..7497ddeb951e8 100644 --- a/components/com_finder/src/Controller/DisplayController.php +++ b/components/com_finder/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input; - $cachable = true; - - // Load plugin language files. - LanguageHelper::loadPluginLanguage(); - - // Set the default view name and format from the Request. - $viewName = $input->get('view', 'search', 'word'); - $input->set('view', $viewName); - - // Don't cache view for search queries - if ($input->get('q', null, 'string') || $input->get('f', null, 'int') || $input->get('t', null, 'array')) - { - $cachable = false; - } - - $safeurlparams = array( - 'f' => 'INT', - 'lang' => 'CMD' - ); - - return parent::display($cachable, $safeurlparams); - } + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached. [optional] + * @param array $urlparams An array of safe URL parameters and their variable types, + * for valid values see {@link \JFilterInput::clean()}. [optional] + * + * @return static This object is to support chaining. + * + * @since 2.5 + */ + public function display($cachable = false, $urlparams = array()) + { + $input = Factory::getApplication()->input; + $cachable = true; + + // Load plugin language files. + LanguageHelper::loadPluginLanguage(); + + // Set the default view name and format from the Request. + $viewName = $input->get('view', 'search', 'word'); + $input->set('view', $viewName); + + // Don't cache view for search queries + if ($input->get('q', null, 'string') || $input->get('f', null, 'int') || $input->get('t', null, 'array')) { + $cachable = false; + } + + $safeurlparams = array( + 'f' => 'INT', + 'lang' => 'CMD' + ); + + return parent::display($cachable, $safeurlparams); + } } diff --git a/components/com_finder/src/Controller/SuggestionsController.php b/components/com_finder/src/Controller/SuggestionsController.php index e8f04b7be641c..43dc4dc9f0087 100644 --- a/components/com_finder/src/Controller/SuggestionsController.php +++ b/components/com_finder/src/Controller/SuggestionsController.php @@ -1,4 +1,5 @@ app; - $app->mimeType = 'application/json'; - - // Ensure caching is disabled as it depends on the query param in the model - $app->allowCache(false); - - $suggestions = $this->getSuggestions(); - - // Send the response. - $app->setHeader('Content-Type', $app->mimeType . '; charset=' . $app->charSet); - $app->sendHeaders(); - echo '{ "suggestions": ' . json_encode($suggestions) . ' }'; - } - - /** - * Method to find search query suggestions for OpenSearch - * - * @return void - * - * @since 4.0.0 - */ - public function opensearchsuggest() - { - $app = $this->app; - $app->mimeType = 'application/json'; - $result = array(); - $result[] = $app->input->request->get('q', '', 'string'); - - $result[] = $this->getSuggestions(); - - // Ensure caching is disabled as it depends on the query param in the model - $app->allowCache(false); - - // Send the response. - $app->setHeader('Content-Type', $app->mimeType . '; charset=' . $app->charSet); - $app->sendHeaders(); - echo json_encode($result); - } - - /** - * Method to retrieve the data from the database - * - * @return array The suggested words - * - * @since 3.4 - */ - protected function getSuggestions() - { - $return = array(); - - $params = ComponentHelper::getParams('com_finder'); - - if ($params->get('show_autosuggest', 1)) - { - // Get the suggestions. - $model = $this->getModel('Suggestions'); - $return = $model->getItems(); - } - - // Check the data. - if (empty($return)) - { - $return = array(); - } - - return $return; - } + /** + * Method to find search query suggestions. Uses awesomplete + * + * @return void + * + * @since 3.4 + */ + public function suggest() + { + $app = $this->app; + $app->mimeType = 'application/json'; + + // Ensure caching is disabled as it depends on the query param in the model + $app->allowCache(false); + + $suggestions = $this->getSuggestions(); + + // Send the response. + $app->setHeader('Content-Type', $app->mimeType . '; charset=' . $app->charSet); + $app->sendHeaders(); + echo '{ "suggestions": ' . json_encode($suggestions) . ' }'; + } + + /** + * Method to find search query suggestions for OpenSearch + * + * @return void + * + * @since 4.0.0 + */ + public function opensearchsuggest() + { + $app = $this->app; + $app->mimeType = 'application/json'; + $result = array(); + $result[] = $app->input->request->get('q', '', 'string'); + + $result[] = $this->getSuggestions(); + + // Ensure caching is disabled as it depends on the query param in the model + $app->allowCache(false); + + // Send the response. + $app->setHeader('Content-Type', $app->mimeType . '; charset=' . $app->charSet); + $app->sendHeaders(); + echo json_encode($result); + } + + /** + * Method to retrieve the data from the database + * + * @return array The suggested words + * + * @since 3.4 + */ + protected function getSuggestions() + { + $return = array(); + + $params = ComponentHelper::getParams('com_finder'); + + if ($params->get('show_autosuggest', 1)) { + // Get the suggestions. + $model = $this->getModel('Suggestions'); + $return = $model->getItems(); + } + + // Check the data. + if (empty($return)) { + $return = array(); + } + + return $return; + } } diff --git a/components/com_finder/src/Helper/FinderHelper.php b/components/com_finder/src/Helper/FinderHelper.php index e32f6cd026804..8c896fba9111f 100644 --- a/components/com_finder/src/Helper/FinderHelper.php +++ b/components/com_finder/src/Helper/FinderHelper.php @@ -1,4 +1,5 @@ get('gather_search_statistics', 0)) - { - return; - } + /** + * Method to log searches to the database + * + * @param Query $searchquery The search query + * @param integer $resultCount The number of results for this search + * + * @return void + * + * @since 4.0.0 + */ + public static function logSearch(Query $searchquery, $resultCount = 0) + { + if (!ComponentHelper::getParams('com_finder')->get('gather_search_statistics', 0)) { + return; + } - if (trim($searchquery->input) == '' && !$searchquery->empty) - { - return; - } + if (trim($searchquery->input) == '' && !$searchquery->empty) { + return; + } - // Initialise our variables - $db = Factory::getDbo(); - $query = $db->getQuery(true); + // Initialise our variables + $db = Factory::getDbo(); + $query = $db->getQuery(true); - // Sanitise the term for the database - $temp = unserialize(serialize($searchquery)); - $temp->input = trim(strtolower($searchquery->input)); - $entry = new \stdClass; - $entry->searchterm = $temp->input; - $entry->query = serialize($temp); - $entry->md5sum = md5($entry->query); - $entry->hits = 1; - $entry->results = $resultCount; + // Sanitise the term for the database + $temp = unserialize(serialize($searchquery)); + $temp->input = trim(strtolower($searchquery->input)); + $entry = new \stdClass(); + $entry->searchterm = $temp->input; + $entry->query = serialize($temp); + $entry->md5sum = md5($entry->query); + $entry->hits = 1; + $entry->results = $resultCount; - // Query the table to determine if the term has been searched previously - $query->select($db->quoteName('hits')) - ->from($db->quoteName('#__finder_logging')) - ->where($db->quoteName('md5sum') . ' = ' . $db->quote($entry->md5sum)); - $db->setQuery($query); - $hits = (int) $db->loadResult(); + // Query the table to determine if the term has been searched previously + $query->select($db->quoteName('hits')) + ->from($db->quoteName('#__finder_logging')) + ->where($db->quoteName('md5sum') . ' = ' . $db->quote($entry->md5sum)); + $db->setQuery($query); + $hits = (int) $db->loadResult(); - // Reset the $query object - $query->clear(); + // Reset the $query object + $query->clear(); - // Update the table based on the results - if ($hits) - { - $query->update($db->quoteName('#__finder_logging')) - ->set('hits = (hits + 1)') - ->where($db->quoteName('md5sum') . ' = ' . $db->quote($entry->md5sum)); - $db->setQuery($query); - $db->execute(); - } - else - { - $query->insert($db->quoteName('#__finder_logging')) - ->columns( - [ - $db->quoteName('searchterm'), - $db->quoteName('query'), - $db->quoteName('md5sum'), - $db->quoteName('hits'), - $db->quoteName('results'), - ] - ) - ->values('?, ?, ?, ?, ?') - ->bind(1, $entry->searchterm) - ->bind(2, $entry->query, ParameterType::LARGE_OBJECT) - ->bind(3, $entry->md5sum) - ->bind(4, $entry->hits, ParameterType::INTEGER) - ->bind(5, $entry->results, ParameterType::INTEGER); - $db->setQuery($query); - $db->execute(); - } - } + // Update the table based on the results + if ($hits) { + $query->update($db->quoteName('#__finder_logging')) + ->set('hits = (hits + 1)') + ->where($db->quoteName('md5sum') . ' = ' . $db->quote($entry->md5sum)); + $db->setQuery($query); + $db->execute(); + } else { + $query->insert($db->quoteName('#__finder_logging')) + ->columns( + [ + $db->quoteName('searchterm'), + $db->quoteName('query'), + $db->quoteName('md5sum'), + $db->quoteName('hits'), + $db->quoteName('results'), + ] + ) + ->values('?, ?, ?, ?, ?') + ->bind(1, $entry->searchterm) + ->bind(2, $entry->query, ParameterType::LARGE_OBJECT) + ->bind(3, $entry->md5sum) + ->bind(4, $entry->hits, ParameterType::INTEGER) + ->bind(5, $entry->results, ParameterType::INTEGER); + $db->setQuery($query); + $db->execute(); + } + } } diff --git a/components/com_finder/src/Helper/RouteHelper.php b/components/com_finder/src/Helper/RouteHelper.php index 275df8c470c62..192ca84d92090 100644 --- a/components/com_finder/src/Helper/RouteHelper.php +++ b/components/com_finder/src/Helper/RouteHelper.php @@ -1,4 +1,5 @@ 'search', 'q' => $q, 'f' => $f); - $item = self::getItemid($query); - - // Get the base route. - $uri = clone Uri::getInstance('index.php?option=com_finder&view=search'); - - // Add the pre-defined search filter if present. - if ($f !== null) - { - $uri->setVar('f', $f); - } - - // Add the search query string if present. - if ($q !== null) - { - $uri->setVar('q', $q); - } - - // Add the menu item id if present. - if ($item !== null) - { - $uri->setVar('Itemid', $item); - } - - return $uri->toString(array('path', 'query')); - } - - /** - * Method to get the route for an advanced search page. - * - * @param integer $f The search filter id. [optional] - * @param string $q The search query string. [optional] - * - * @return string The advanced search route. - * - * @since 2.5 - */ - public static function getAdvancedRoute($f = null, $q = null) - { - // Get the menu item id. - $query = array('view' => 'advanced', 'q' => $q, 'f' => $f); - $item = self::getItemid($query); - - // Get the base route. - $uri = clone Uri::getInstance('index.php?option=com_finder&view=advanced'); - - // Add the pre-defined search filter if present. - if ($q !== null) - { - $uri->setVar('f', $f); - } - - // Add the search query string if present. - if ($q !== null) - { - $uri->setVar('q', $q); - } - - // Add the menu item id if present. - if ($item !== null) - { - $uri->setVar('Itemid', $item); - } - - return $uri->toString(array('path', 'query')); - } - - /** - * Method to get the most appropriate menu item for the route based on the - * supplied query needles. - * - * @param array $query An array of URL parameters. - * - * @return mixed An integer on success, null otherwise. - * - * @since 2.5 - */ - public static function getItemid($query) - { - static $items, $active; - - // Get the menu items for com_finder. - if (!$items || !$active) - { - $app = Factory::getApplication(); - $com = ComponentHelper::getComponent('com_finder'); - $menu = $app->getMenu(); - $active = $menu->getActive(); - $items = $menu->getItems('component_id', $com->id); - $items = is_array($items) ? $items : array(); - } - - // Try to match the active view and filter. - if ($active && @$active->query['view'] == @$query['view'] && @$active->query['f'] == @$query['f']) - { - return $active->id; - } - - // Try to match the view, query, and filter. - foreach ($items as $item) - { - if (@$item->query['view'] == @$query['view'] && @$item->query['q'] == @$query['q'] && @$item->query['f'] == @$query['f']) - { - return $item->id; - } - } - - // Try to match the view and filter. - foreach ($items as $item) - { - if (@$item->query['view'] == @$query['view'] && @$item->query['f'] == @$query['f']) - { - return $item->id; - } - } - - // Try to match the view. - foreach ($items as $item) - { - if (@$item->query['view'] == @$query['view']) - { - return $item->id; - } - } - - return null; - } + /** + * Method to get the route for a search page. + * + * @param integer $f The search filter id. [optional] + * @param string $q The search query string. [optional] + * + * @return string The search route. + * + * @since 2.5 + */ + public static function getSearchRoute($f = null, $q = null) + { + // Get the menu item id. + $query = array('view' => 'search', 'q' => $q, 'f' => $f); + $item = self::getItemid($query); + + // Get the base route. + $uri = clone Uri::getInstance('index.php?option=com_finder&view=search'); + + // Add the pre-defined search filter if present. + if ($f !== null) { + $uri->setVar('f', $f); + } + + // Add the search query string if present. + if ($q !== null) { + $uri->setVar('q', $q); + } + + // Add the menu item id if present. + if ($item !== null) { + $uri->setVar('Itemid', $item); + } + + return $uri->toString(array('path', 'query')); + } + + /** + * Method to get the route for an advanced search page. + * + * @param integer $f The search filter id. [optional] + * @param string $q The search query string. [optional] + * + * @return string The advanced search route. + * + * @since 2.5 + */ + public static function getAdvancedRoute($f = null, $q = null) + { + // Get the menu item id. + $query = array('view' => 'advanced', 'q' => $q, 'f' => $f); + $item = self::getItemid($query); + + // Get the base route. + $uri = clone Uri::getInstance('index.php?option=com_finder&view=advanced'); + + // Add the pre-defined search filter if present. + if ($q !== null) { + $uri->setVar('f', $f); + } + + // Add the search query string if present. + if ($q !== null) { + $uri->setVar('q', $q); + } + + // Add the menu item id if present. + if ($item !== null) { + $uri->setVar('Itemid', $item); + } + + return $uri->toString(array('path', 'query')); + } + + /** + * Method to get the most appropriate menu item for the route based on the + * supplied query needles. + * + * @param array $query An array of URL parameters. + * + * @return mixed An integer on success, null otherwise. + * + * @since 2.5 + */ + public static function getItemid($query) + { + static $items, $active; + + // Get the menu items for com_finder. + if (!$items || !$active) { + $app = Factory::getApplication(); + $com = ComponentHelper::getComponent('com_finder'); + $menu = $app->getMenu(); + $active = $menu->getActive(); + $items = $menu->getItems('component_id', $com->id); + $items = is_array($items) ? $items : array(); + } + + // Try to match the active view and filter. + if ($active && @$active->query['view'] == @$query['view'] && @$active->query['f'] == @$query['f']) { + return $active->id; + } + + // Try to match the view, query, and filter. + foreach ($items as $item) { + if (@$item->query['view'] == @$query['view'] && @$item->query['q'] == @$query['q'] && @$item->query['f'] == @$query['f']) { + return $item->id; + } + } + + // Try to match the view and filter. + foreach ($items as $item) { + if (@$item->query['view'] == @$query['view'] && @$item->query['f'] == @$query['f']) { + return $item->id; + } + } + + // Try to match the view. + foreach ($items as $item) { + if (@$item->query['view'] == @$query['view']) { + return $item->id; + } + } + + return null; + } } diff --git a/components/com_finder/src/Model/SearchModel.php b/components/com_finder/src/Model/SearchModel.php index cc248493436dd..b52ebb3e628ba 100644 --- a/components/com_finder/src/Model/SearchModel.php +++ b/components/com_finder/src/Model/SearchModel.php @@ -1,4 +1,5 @@ $row) - { - // Build the result object. - if (is_resource($row->object)) - { - $result = unserialize(stream_get_contents($row->object)); - } - else - { - $result = unserialize($row->object); - } - - $result->cleanURL = $result->route; - - // Add the result back to the stack. - $results[] = $result; - } - - // Return the results. - return $results; - } - - /** - * Method to get the query object. - * - * @return Query A query object. - * - * @since 2.5 - */ - public function getQuery() - { - // Return the query object. - return $this->searchquery; - } - - /** - * Method to build a database query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery A database query. - * - * @since 2.5 - */ - protected function getListQuery() - { - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select the required fields from the table. - $query->select( - $this->getState( - 'list.select', - 'l.link_id, l.object' - ) - ); - - $query->from('#__finder_links AS l'); - - $user = Factory::getUser(); - $groups = $this->getState('user.groups', $user->getAuthorisedViewLevels()); - $query->whereIn($db->quoteName('l.access'), $groups) - ->where('l.state = 1') - ->where('l.published = 1'); - - // Get the current date, minus seconds. - $nowDate = $db->quote(substr_replace(Factory::getDate()->toSql(), '00', -2)); - - // Add the publish up and publish down filters. - $query->where('(l.publish_start_date IS NULL OR l.publish_start_date <= ' . $nowDate . ')') - ->where('(l.publish_end_date IS NULL OR l.publish_end_date >= ' . $nowDate . ')'); - - $query->group('l.link_id'); - $query->group('l.object'); - - /* - * Add the taxonomy filters to the query. We have to join the taxonomy - * map table for each group so that we can use AND clauses across - * groups. Within each group there can be an array of values that will - * use OR clauses. - */ - if (!empty($this->searchquery->filters)) - { - // Convert the associative array to a numerically indexed array. - $groups = array_values($this->searchquery->filters); - $taxonomies = call_user_func_array('array_merge', array_values($this->searchquery->filters)); - - $query->join('INNER', $db->quoteName('#__finder_taxonomy_map') . ' AS t ON t.link_id = l.link_id') - ->where('t.node_id IN (' . implode(',', array_unique($taxonomies)) . ')'); - - // Iterate through each taxonomy group. - for ($i = 0, $c = count($groups); $i < $c; $i++) - { - $query->having('SUM(CASE WHEN t.node_id IN (' . implode(',', $groups[$i]) . ') THEN 1 ELSE 0 END) > 0'); - } - } - - // Add the start date filter to the query. - if (!empty($this->searchquery->date1)) - { - // Escape the date. - $date1 = $db->quote($this->searchquery->date1); - - // Add the appropriate WHERE condition. - if ($this->searchquery->when1 === 'before') - { - $query->where($db->quoteName('l.start_date') . ' <= ' . $date1); - } - elseif ($this->searchquery->when1 === 'after') - { - $query->where($db->quoteName('l.start_date') . ' >= ' . $date1); - } - else - { - $query->where($db->quoteName('l.start_date') . ' = ' . $date1); - } - } - - // Add the end date filter to the query. - if (!empty($this->searchquery->date2)) - { - // Escape the date. - $date2 = $db->quote($this->searchquery->date2); - - // Add the appropriate WHERE condition. - if ($this->searchquery->when2 === 'before') - { - $query->where($db->quoteName('l.start_date') . ' <= ' . $date2); - } - elseif ($this->searchquery->when2 === 'after') - { - $query->where($db->quoteName('l.start_date') . ' >= ' . $date2); - } - else - { - $query->where($db->quoteName('l.start_date') . ' = ' . $date2); - } - } - - // Filter by language - if ($this->getState('filter.language')) - { - $query->where('l.language IN (' . $db->quote(Factory::getLanguage()->getTag()) . ', ' . $db->quote('*') . ')'); - } - - // Get the result ordering and direction. - $ordering = $this->getState('list.ordering', 'm.weight'); - $direction = $this->getState('list.direction', 'DESC'); - - /* - * If we are ordering by relevance we have to add up the relevance - * scores that are contained in the ordering field. - */ - if ($ordering === 'm.weight') - { - // Get the base query and add the ordering information. - $query->select('SUM(' . $db->escape($ordering) . ') AS ordering'); - } - /* - * If we are not ordering by relevance, we just have to add - * the unique items to the set. - */ - else - { - // Get the base query and add the ordering information. - $query->select($db->escape($ordering) . ' AS ordering'); - } - - $query->order('ordering ' . $db->escape($direction)); - - /* - * If there are no optional or required search terms in the query, we - * can get the results in one relatively simple database query. - */ - if (empty($this->includedTerms) && $this->searchquery->empty && $this->searchquery->input == '') - { - // Return the results. - return $query; - } - - /* - * If there are no optional or required search terms in the query and - * empty searches are not allowed, we return an empty query. - * If the search term is not empty and empty searches are allowed, - * but no terms were found, we return an empty query as well. - */ - if (empty($this->includedTerms) - && (!$this->searchquery->empty || ($this->searchquery->empty && $this->searchquery->input != ''))) - { - // Since we need to return a query, we simplify this one. - $query->clear('join') - ->clear('where') - ->clear('bounded') - ->clear('having') - ->clear('group') - ->where('false'); - - return $query; - } - - $included = call_user_func_array('array_merge', array_values($this->includedTerms)); - $query->join('INNER', $db->quoteName('#__finder_links_terms') . ' AS m ON m.link_id = l.link_id') - ->where('m.term_id IN (' . implode(',', $included) . ')'); - - // Check if there are any excluded terms to deal with. - if (count($this->excludedTerms)) - { - $query2 = $db->getQuery(true); - $query2->select('e.link_id') - ->from($db->quoteName('#__finder_links_terms', 'e')) - ->where('e.term_id IN (' . implode(',', $this->excludedTerms) . ')'); - $query->where('l.link_id NOT IN (' . $query2 . ')'); - } - - /* - * The query contains required search terms. - */ - if (count($this->requiredTerms)) - { - foreach ($this->requiredTerms as $terms) - { - if (count($terms)) - { - $query->having('SUM(CASE WHEN m.term_id IN (' . implode(',', $terms) . ') THEN 1 ELSE 0 END) > 0'); - } - else - { - $query->where('false'); - break; - } - } - } - - return $query; - } - - /** - * Method to get a store id based on model the configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id An identifier string to generate the store id. [optional] - * @param boolean $page True to store the data paged, false to store all data. [optional] - * - * @return string A store id. - * - * @since 2.5 - */ - protected function getStoreId($id = '', $page = true) - { - // Get the query object. - $query = $this->getQuery(); - - // Add the search query state. - $id .= ':' . $query->input; - $id .= ':' . $query->language; - $id .= ':' . $query->filter; - $id .= ':' . serialize($query->filters); - $id .= ':' . $query->date1; - $id .= ':' . $query->date2; - $id .= ':' . $query->when1; - $id .= ':' . $query->when2; - - if ($page) - { - // Add the list state for page specific data. - $id .= ':' . $this->getState('list.start'); - $id .= ':' . $this->getState('list.limit'); - $id .= ':' . $this->getState('list.ordering'); - $id .= ':' . $this->getState('list.direction'); - } - - return parent::getStoreId($id); - } - - /** - * Method to auto-populate the model state. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. [optional] - * @param string $direction An optional direction. [optional] - * - * @return void - * - * @since 2.5 - */ - protected function populateState($ordering = null, $direction = null) - { - // Get the configuration options. - $app = Factory::getApplication(); - $input = $app->input; - $params = $app->getParams(); - $user = Factory::getUser(); - $language = Factory::getLanguage(); - - $this->setState('filter.language', Multilanguage::isEnabled()); - - $request = $input->request; - $options = array(); - - // Get the empty query setting. - $options['empty'] = $params->get('allow_empty_query', 0); - - // Get the static taxonomy filters. - $options['filter'] = $request->getInt('f', $params->get('f', '')); - - // Get the dynamic taxonomy filters. - $options['filters'] = $request->get('t', $params->get('t', array()), 'array'); - - // Get the query string. - $options['input'] = $request->getString('q', $params->get('q', '')); - - // Get the query language. - $options['language'] = $request->getCmd('l', $params->get('l', $language->getTag())); - - // Set the word match mode - $options['word_match'] = $params->get('word_match', 'exact'); - - // Get the start date and start date modifier filters. - $options['date1'] = $request->getString('d1', $params->get('d1', '')); - $options['when1'] = $request->getString('w1', $params->get('w1', '')); - - // Get the end date and end date modifier filters. - $options['date2'] = $request->getString('d2', $params->get('d2', '')); - $options['when2'] = $request->getString('w2', $params->get('w2', '')); - - // Load the query object. - $this->searchquery = new Query($options, $this->getDatabase()); - - // Load the query token data. - $this->excludedTerms = $this->searchquery->getExcludedTermIds(); - $this->includedTerms = $this->searchquery->getIncludedTermIds(); - $this->requiredTerms = $this->searchquery->getRequiredTermIds(); - - // Load the list state. - $this->setState('list.start', $input->get('limitstart', 0, 'uint')); - $this->setState('list.limit', $input->get('limit', $params->get('list_limit', $app->get('list_limit', 20)), 'uint')); - - /* - * Load the sort ordering. - * Currently this is 'hard' coded via menu item parameter but may not satisfy a users need. - * More flexibility was way more user friendly. So we allow the user to pass a custom value - * from the pool of fields that are indexed like the 'title' field. - * Also, we allow this parameter to be passed in either case (lower/upper). - */ - $order = $input->getWord('o', $params->get('sort_order', 'relevance')); - $order = StringHelper::strtolower($order); - $this->setState('list.raworder', $order); - - switch ($order) - { - case 'date': - $this->setState('list.ordering', 'l.start_date'); - break; - - case 'price': - $this->setState('list.ordering', 'l.list_price'); - break; - - case ($order === 'relevance' && !empty($this->includedTerms)) : - $this->setState('list.ordering', 'm.weight'); - break; - - case 'title': - $this->setState('list.ordering', 'l.title'); - break; - - default: - $this->setState('list.ordering', 'l.link_id'); - $this->setState('list.raworder'); - break; - } - - /* - * Load the sort direction. - * Currently this is 'hard' coded via menu item parameter but may not satisfy a users need. - * More flexibility was way more user friendly. So we allow to be inverted. - * Also, we allow this parameter to be passed in either case (lower/upper). - */ - $dirn = $input->getWord('od', $params->get('sort_direction', 'desc')); - $dirn = StringHelper::strtolower($dirn); - - switch ($dirn) - { - case 'asc': - $this->setState('list.direction', 'ASC'); - break; - - default: - $this->setState('list.direction', 'DESC'); - break; - } - - // Set the match limit. - $this->setState('match.limit', 1000); - - // Load the parameters. - $this->setState('params', $params); - - // Load the user state. - $this->setState('user.id', (int) $user->get('id')); - $this->setState('user.groups', $user->getAuthorisedViewLevels()); - } + /** + * Context string for the model type + * + * @var string + * @since 2.5 + */ + protected $context = 'com_finder.search'; + + /** + * The query object is an instance of Query which contains and + * models the entire search query including the text input; static and + * dynamic taxonomy filters; date filters; etc. + * + * @var Query + * @since 2.5 + */ + protected $searchquery; + + /** + * An array of all excluded terms ids. + * + * @var array + * @since 2.5 + */ + protected $excludedTerms = array(); + + /** + * An array of all included terms ids. + * + * @var array + * @since 2.5 + */ + protected $includedTerms = array(); + + /** + * An array of all required terms ids. + * + * @var array + * @since 2.5 + */ + protected $requiredTerms = array(); + + /** + * Method to get the results of the query. + * + * @return array An array of Result objects. + * + * @since 2.5 + * @throws \Exception on database error. + */ + public function getItems() + { + $items = parent::getItems(); + + // Check the data. + if (empty($items)) { + return null; + } + + $results = array(); + + // Convert the rows to result objects. + foreach ($items as $rk => $row) { + // Build the result object. + if (is_resource($row->object)) { + $result = unserialize(stream_get_contents($row->object)); + } else { + $result = unserialize($row->object); + } + + $result->cleanURL = $result->route; + + // Add the result back to the stack. + $results[] = $result; + } + + // Return the results. + return $results; + } + + /** + * Method to get the query object. + * + * @return Query A query object. + * + * @since 2.5 + */ + public function getQuery() + { + // Return the query object. + return $this->searchquery; + } + + /** + * Method to build a database query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery A database query. + * + * @since 2.5 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + $this->getState( + 'list.select', + 'l.link_id, l.object' + ) + ); + + $query->from('#__finder_links AS l'); + + $user = Factory::getUser(); + $groups = $this->getState('user.groups', $user->getAuthorisedViewLevels()); + $query->whereIn($db->quoteName('l.access'), $groups) + ->where('l.state = 1') + ->where('l.published = 1'); + + // Get the current date, minus seconds. + $nowDate = $db->quote(substr_replace(Factory::getDate()->toSql(), '00', -2)); + + // Add the publish up and publish down filters. + $query->where('(l.publish_start_date IS NULL OR l.publish_start_date <= ' . $nowDate . ')') + ->where('(l.publish_end_date IS NULL OR l.publish_end_date >= ' . $nowDate . ')'); + + $query->group('l.link_id'); + $query->group('l.object'); + + /* + * Add the taxonomy filters to the query. We have to join the taxonomy + * map table for each group so that we can use AND clauses across + * groups. Within each group there can be an array of values that will + * use OR clauses. + */ + if (!empty($this->searchquery->filters)) { + // Convert the associative array to a numerically indexed array. + $groups = array_values($this->searchquery->filters); + $taxonomies = call_user_func_array('array_merge', array_values($this->searchquery->filters)); + + $query->join('INNER', $db->quoteName('#__finder_taxonomy_map') . ' AS t ON t.link_id = l.link_id') + ->where('t.node_id IN (' . implode(',', array_unique($taxonomies)) . ')'); + + // Iterate through each taxonomy group. + for ($i = 0, $c = count($groups); $i < $c; $i++) { + $query->having('SUM(CASE WHEN t.node_id IN (' . implode(',', $groups[$i]) . ') THEN 1 ELSE 0 END) > 0'); + } + } + + // Add the start date filter to the query. + if (!empty($this->searchquery->date1)) { + // Escape the date. + $date1 = $db->quote($this->searchquery->date1); + + // Add the appropriate WHERE condition. + if ($this->searchquery->when1 === 'before') { + $query->where($db->quoteName('l.start_date') . ' <= ' . $date1); + } elseif ($this->searchquery->when1 === 'after') { + $query->where($db->quoteName('l.start_date') . ' >= ' . $date1); + } else { + $query->where($db->quoteName('l.start_date') . ' = ' . $date1); + } + } + + // Add the end date filter to the query. + if (!empty($this->searchquery->date2)) { + // Escape the date. + $date2 = $db->quote($this->searchquery->date2); + + // Add the appropriate WHERE condition. + if ($this->searchquery->when2 === 'before') { + $query->where($db->quoteName('l.start_date') . ' <= ' . $date2); + } elseif ($this->searchquery->when2 === 'after') { + $query->where($db->quoteName('l.start_date') . ' >= ' . $date2); + } else { + $query->where($db->quoteName('l.start_date') . ' = ' . $date2); + } + } + + // Filter by language + if ($this->getState('filter.language')) { + $query->where('l.language IN (' . $db->quote(Factory::getLanguage()->getTag()) . ', ' . $db->quote('*') . ')'); + } + + // Get the result ordering and direction. + $ordering = $this->getState('list.ordering', 'm.weight'); + $direction = $this->getState('list.direction', 'DESC'); + + /* + * If we are ordering by relevance we have to add up the relevance + * scores that are contained in the ordering field. + */ + if ($ordering === 'm.weight') { + // Get the base query and add the ordering information. + $query->select('SUM(' . $db->escape($ordering) . ') AS ordering'); + } else { + /** + * If we are not ordering by relevance, we just have to add + * the unique items to the set. + */ + // Get the base query and add the ordering information. + $query->select($db->escape($ordering) . ' AS ordering'); + } + + $query->order('ordering ' . $db->escape($direction)); + + /* + * If there are no optional or required search terms in the query, we + * can get the results in one relatively simple database query. + */ + if (empty($this->includedTerms) && $this->searchquery->empty && $this->searchquery->input == '') { + // Return the results. + return $query; + } + + /* + * If there are no optional or required search terms in the query and + * empty searches are not allowed, we return an empty query. + * If the search term is not empty and empty searches are allowed, + * but no terms were found, we return an empty query as well. + */ + if ( + empty($this->includedTerms) + && (!$this->searchquery->empty || ($this->searchquery->empty && $this->searchquery->input != '')) + ) { + // Since we need to return a query, we simplify this one. + $query->clear('join') + ->clear('where') + ->clear('bounded') + ->clear('having') + ->clear('group') + ->where('false'); + + return $query; + } + + $included = call_user_func_array('array_merge', array_values($this->includedTerms)); + $query->join('INNER', $db->quoteName('#__finder_links_terms') . ' AS m ON m.link_id = l.link_id') + ->where('m.term_id IN (' . implode(',', $included) . ')'); + + // Check if there are any excluded terms to deal with. + if (count($this->excludedTerms)) { + $query2 = $db->getQuery(true); + $query2->select('e.link_id') + ->from($db->quoteName('#__finder_links_terms', 'e')) + ->where('e.term_id IN (' . implode(',', $this->excludedTerms) . ')'); + $query->where('l.link_id NOT IN (' . $query2 . ')'); + } + + /* + * The query contains required search terms. + */ + if (count($this->requiredTerms)) { + foreach ($this->requiredTerms as $terms) { + if (count($terms)) { + $query->having('SUM(CASE WHEN m.term_id IN (' . implode(',', $terms) . ') THEN 1 ELSE 0 END) > 0'); + } else { + $query->where('false'); + break; + } + } + } + + return $query; + } + + /** + * Method to get a store id based on model the configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id An identifier string to generate the store id. [optional] + * @param boolean $page True to store the data paged, false to store all data. [optional] + * + * @return string A store id. + * + * @since 2.5 + */ + protected function getStoreId($id = '', $page = true) + { + // Get the query object. + $query = $this->getQuery(); + + // Add the search query state. + $id .= ':' . $query->input; + $id .= ':' . $query->language; + $id .= ':' . $query->filter; + $id .= ':' . serialize($query->filters); + $id .= ':' . $query->date1; + $id .= ':' . $query->date2; + $id .= ':' . $query->when1; + $id .= ':' . $query->when2; + + if ($page) { + // Add the list state for page specific data. + $id .= ':' . $this->getState('list.start'); + $id .= ':' . $this->getState('list.limit'); + $id .= ':' . $this->getState('list.ordering'); + $id .= ':' . $this->getState('list.direction'); + } + + return parent::getStoreId($id); + } + + /** + * Method to auto-populate the model state. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. [optional] + * @param string $direction An optional direction. [optional] + * + * @return void + * + * @since 2.5 + */ + protected function populateState($ordering = null, $direction = null) + { + // Get the configuration options. + $app = Factory::getApplication(); + $input = $app->input; + $params = $app->getParams(); + $user = Factory::getUser(); + $language = Factory::getLanguage(); + + $this->setState('filter.language', Multilanguage::isEnabled()); + + $request = $input->request; + $options = array(); + + // Get the empty query setting. + $options['empty'] = $params->get('allow_empty_query', 0); + + // Get the static taxonomy filters. + $options['filter'] = $request->getInt('f', $params->get('f', '')); + + // Get the dynamic taxonomy filters. + $options['filters'] = $request->get('t', $params->get('t', array()), 'array'); + + // Get the query string. + $options['input'] = $request->getString('q', $params->get('q', '')); + + // Get the query language. + $options['language'] = $request->getCmd('l', $params->get('l', $language->getTag())); + + // Set the word match mode + $options['word_match'] = $params->get('word_match', 'exact'); + + // Get the start date and start date modifier filters. + $options['date1'] = $request->getString('d1', $params->get('d1', '')); + $options['when1'] = $request->getString('w1', $params->get('w1', '')); + + // Get the end date and end date modifier filters. + $options['date2'] = $request->getString('d2', $params->get('d2', '')); + $options['when2'] = $request->getString('w2', $params->get('w2', '')); + + // Load the query object. + $this->searchquery = new Query($options, $this->getDatabase()); + + // Load the query token data. + $this->excludedTerms = $this->searchquery->getExcludedTermIds(); + $this->includedTerms = $this->searchquery->getIncludedTermIds(); + $this->requiredTerms = $this->searchquery->getRequiredTermIds(); + + // Load the list state. + $this->setState('list.start', $input->get('limitstart', 0, 'uint')); + $this->setState('list.limit', $input->get('limit', $params->get('list_limit', $app->get('list_limit', 20)), 'uint')); + + /* + * Load the sort ordering. + * Currently this is 'hard' coded via menu item parameter but may not satisfy a users need. + * More flexibility was way more user friendly. So we allow the user to pass a custom value + * from the pool of fields that are indexed like the 'title' field. + * Also, we allow this parameter to be passed in either case (lower/upper). + */ + $order = $input->getWord('o', $params->get('sort_order', 'relevance')); + $order = StringHelper::strtolower($order); + $this->setState('list.raworder', $order); + + switch ($order) { + case 'date': + $this->setState('list.ordering', 'l.start_date'); + break; + + case 'price': + $this->setState('list.ordering', 'l.list_price'); + break; + + case ($order === 'relevance' && !empty($this->includedTerms)): + $this->setState('list.ordering', 'm.weight'); + break; + + case 'title': + $this->setState('list.ordering', 'l.title'); + break; + + default: + $this->setState('list.ordering', 'l.link_id'); + $this->setState('list.raworder'); + break; + } + + /* + * Load the sort direction. + * Currently this is 'hard' coded via menu item parameter but may not satisfy a users need. + * More flexibility was way more user friendly. So we allow to be inverted. + * Also, we allow this parameter to be passed in either case (lower/upper). + */ + $dirn = $input->getWord('od', $params->get('sort_direction', 'desc')); + $dirn = StringHelper::strtolower($dirn); + + switch ($dirn) { + case 'asc': + $this->setState('list.direction', 'ASC'); + break; + + default: + $this->setState('list.direction', 'DESC'); + break; + } + + // Set the match limit. + $this->setState('match.limit', 1000); + + // Load the parameters. + $this->setState('params', $params); + + // Load the user state. + $this->setState('user.id', (int) $user->get('id')); + $this->setState('user.groups', $user->getAuthorisedViewLevels()); + } } diff --git a/components/com_finder/src/Model/SuggestionsModel.php b/components/com_finder/src/Model/SuggestionsModel.php index b0232e3d13655..44cb95e6d71ad 100644 --- a/components/com_finder/src/Model/SuggestionsModel.php +++ b/components/com_finder/src/Model/SuggestionsModel.php @@ -1,4 +1,5 @@ $v) - { - $items[$k] = $v->term; - } - - return $items; - } - - /** - * Method to build a database query to load the list data. - * - * @return DatabaseQuery A database query - * - * @since 2.5 - */ - protected function getListQuery() - { - $user = Factory::getUser(); - $groups = ArrayHelper::toInteger($user->getAuthorisedViewLevels()); - $lang = Helper::getPrimaryLanguage($this->getState('language')); - - // Create a new query object. - $db = $this->getDatabase(); - $termIdQuery = $db->getQuery(true); - $termQuery = $db->getQuery(true); - - // Limit term count to a reasonable number of results to reduce main query join size - $termIdQuery->select('ti.term_id') - ->from($db->quoteName('#__finder_terms', 'ti')) - ->where('ti.term LIKE ' . $db->quote($db->escape(StringHelper::strtolower($this->getState('input')), true) . '%', false)) - ->where('ti.common = 0') - ->where('ti.language IN (' . $db->quote($lang) . ', ' . $db->quote('*') . ')') - ->order('ti.links DESC') - ->order('ti.weight DESC'); - - $termIds = $db->setQuery($termIdQuery, 0, 100)->loadColumn(); - - // Early return on term mismatch - if (!count($termIds)) - { - return $termIdQuery; - } - - // Select required fields - $termQuery->select('DISTINCT(t.term)') - ->from($db->quoteName('#__finder_terms', 't')) - ->whereIn('t.term_id', $termIds) - ->order('t.links DESC') - ->order('t.weight DESC'); - - // Join mapping table for term <-> link relation - $mappingTable = $db->quoteName('#__finder_links_terms', 'tm'); - $termQuery->join('INNER', $mappingTable . ' ON tm.term_id = t.term_id'); - - // Join links table - $termQuery->join('INNER', $db->quoteName('#__finder_links', 'l') . ' ON (tm.link_id = l.link_id)') - ->where('l.access IN (' . implode(',', $groups) . ')') - ->where('l.state = 1') - ->where('l.published = 1'); - - return $termQuery; - } - - /** - * Method to get a store id based on model the configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id An identifier string to generate the store id. [optional] - * - * @return string A store id. - * - * @since 2.5 - */ - protected function getStoreId($id = '') - { - // Add the search query state. - $id .= ':' . $this->getState('input'); - $id .= ':' . $this->getState('language'); - - // Add the list state. - $id .= ':' . $this->getState('list.start'); - $id .= ':' . $this->getState('list.limit'); - - return parent::getStoreId($id); - } - - /** - * Method to auto-populate the model state. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 2.5 - */ - protected function populateState($ordering = null, $direction = null) - { - // Get the configuration options. - $app = Factory::getApplication(); - $input = $app->input; - $params = ComponentHelper::getParams('com_finder'); - $user = Factory::getUser(); - - // Get the query input. - $this->setState('input', $input->request->get('q', '', 'string')); - - // Set the query language - if (Multilanguage::isEnabled()) - { - $lang = Factory::getLanguage()->getTag(); - } - else - { - $lang = Helper::getDefaultLanguage(); - } - - $this->setState('language', $lang); - - // Load the list state. - $this->setState('list.start', 0); - $this->setState('list.limit', 10); - - // Load the parameters. - $this->setState('params', $params); - - // Load the user state. - $this->setState('user.id', (int) $user->get('id')); - } + /** + * Context string for the model type. + * + * @var string + * @since 2.5 + */ + protected $context = 'com_finder.suggestions'; + + /** + * Method to get an array of data items. + * + * @return array An array of data items. + * + * @since 2.5 + */ + public function getItems() + { + // Get the items. + $items = parent::getItems(); + + // Convert them to a simple array. + foreach ($items as $k => $v) { + $items[$k] = $v->term; + } + + return $items; + } + + /** + * Method to build a database query to load the list data. + * + * @return DatabaseQuery A database query + * + * @since 2.5 + */ + protected function getListQuery() + { + $user = Factory::getUser(); + $groups = ArrayHelper::toInteger($user->getAuthorisedViewLevels()); + $lang = Helper::getPrimaryLanguage($this->getState('language')); + + // Create a new query object. + $db = $this->getDatabase(); + $termIdQuery = $db->getQuery(true); + $termQuery = $db->getQuery(true); + + // Limit term count to a reasonable number of results to reduce main query join size + $termIdQuery->select('ti.term_id') + ->from($db->quoteName('#__finder_terms', 'ti')) + ->where('ti.term LIKE ' . $db->quote($db->escape(StringHelper::strtolower($this->getState('input')), true) . '%', false)) + ->where('ti.common = 0') + ->where('ti.language IN (' . $db->quote($lang) . ', ' . $db->quote('*') . ')') + ->order('ti.links DESC') + ->order('ti.weight DESC'); + + $termIds = $db->setQuery($termIdQuery, 0, 100)->loadColumn(); + + // Early return on term mismatch + if (!count($termIds)) { + return $termIdQuery; + } + + // Select required fields + $termQuery->select('DISTINCT(t.term)') + ->from($db->quoteName('#__finder_terms', 't')) + ->whereIn('t.term_id', $termIds) + ->order('t.links DESC') + ->order('t.weight DESC'); + + // Join mapping table for term <-> link relation + $mappingTable = $db->quoteName('#__finder_links_terms', 'tm'); + $termQuery->join('INNER', $mappingTable . ' ON tm.term_id = t.term_id'); + + // Join links table + $termQuery->join('INNER', $db->quoteName('#__finder_links', 'l') . ' ON (tm.link_id = l.link_id)') + ->where('l.access IN (' . implode(',', $groups) . ')') + ->where('l.state = 1') + ->where('l.published = 1'); + + return $termQuery; + } + + /** + * Method to get a store id based on model the configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id An identifier string to generate the store id. [optional] + * + * @return string A store id. + * + * @since 2.5 + */ + protected function getStoreId($id = '') + { + // Add the search query state. + $id .= ':' . $this->getState('input'); + $id .= ':' . $this->getState('language'); + + // Add the list state. + $id .= ':' . $this->getState('list.start'); + $id .= ':' . $this->getState('list.limit'); + + return parent::getStoreId($id); + } + + /** + * Method to auto-populate the model state. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 2.5 + */ + protected function populateState($ordering = null, $direction = null) + { + // Get the configuration options. + $app = Factory::getApplication(); + $input = $app->input; + $params = ComponentHelper::getParams('com_finder'); + $user = Factory::getUser(); + + // Get the query input. + $this->setState('input', $input->request->get('q', '', 'string')); + + // Set the query language + if (Multilanguage::isEnabled()) { + $lang = Factory::getLanguage()->getTag(); + } else { + $lang = Helper::getDefaultLanguage(); + } + + $this->setState('language', $lang); + + // Load the list state. + $this->setState('list.start', 0); + $this->setState('list.limit', 10); + + // Load the parameters. + $this->setState('params', $params); + + // Load the user state. + $this->setState('user.id', (int) $user->get('id')); + } } diff --git a/components/com_finder/src/Service/Router.php b/components/com_finder/src/Service/Router.php index 024fa4b3b8d88..2a90a09f7c366 100644 --- a/components/com_finder/src/Service/Router.php +++ b/components/com_finder/src/Service/Router.php @@ -1,4 +1,5 @@ registerView($search); + /** + * Finder Component router constructor + * + * @param SiteApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + */ + public function __construct(SiteApplication $app, AbstractMenu $menu) + { + $search = new RouterViewConfiguration('search'); + $this->registerView($search); - parent::__construct($app, $menu); + parent::__construct($app, $menu); - $this->attachRule(new MenuRules($this)); - $this->attachRule(new StandardRules($this)); - $this->attachRule(new NomenuRules($this)); - } + $this->attachRule(new MenuRules($this)); + $this->attachRule(new StandardRules($this)); + $this->attachRule(new NomenuRules($this)); + } } diff --git a/components/com_finder/src/View/Search/FeedView.php b/components/com_finder/src/View/Search/FeedView.php index 4e642595d4517..6cfd3b390a401 100644 --- a/components/com_finder/src/View/Search/FeedView.php +++ b/components/com_finder/src/View/Search/FeedView.php @@ -1,4 +1,5 @@ input->set('limit', $app->get('feed_limit')); + // Adjust the list limit to the feed limit. + $app->input->set('limit', $app->get('feed_limit')); - // Get view data. - $state = $this->get('State'); - $params = $state->get('params'); - $query = $this->get('Query'); - $results = $this->get('Items'); - $total = $this->get('Total'); + // Get view data. + $state = $this->get('State'); + $params = $state->get('params'); + $query = $this->get('Query'); + $results = $this->get('Items'); + $total = $this->get('Total'); - // Push out the query data. - $explained = HTMLHelper::_('query.explained', $query); + // Push out the query data. + $explained = HTMLHelper::_('query.explained', $query); - // Set the document title. - $this->setDocumentTitle($params->get('page_title', '')); + // Set the document title. + $this->setDocumentTitle($params->get('page_title', '')); - // Configure the document description. - if (!empty($explained)) - { - $this->document->setDescription(html_entity_decode(strip_tags($explained), ENT_QUOTES, 'UTF-8')); - } + // Configure the document description. + if (!empty($explained)) { + $this->document->setDescription(html_entity_decode(strip_tags($explained), ENT_QUOTES, 'UTF-8')); + } - // Set the document link. - $this->document->link = Route::_($query->toUri()); + // Set the document link. + $this->document->link = Route::_($query->toUri()); - // If we don't have any results, we are done. - if (empty($results)) - { - return; - } + // If we don't have any results, we are done. + if (empty($results)) { + return; + } - // Convert the results to feed entries. - foreach ($results as $result) - { - // Convert the result to a feed entry. - $item = new FeedItem; - $item->title = $result->title; - $item->link = Route::_($result->route); - $item->description = $result->description; + // Convert the results to feed entries. + foreach ($results as $result) { + // Convert the result to a feed entry. + $item = new FeedItem(); + $item->title = $result->title; + $item->link = Route::_($result->route); + $item->description = $result->description; - // Use Unix date to cope for non-english languages - $item->date = (int) $result->start_date ? HTMLHelper::_('date', $result->start_date, 'U') : $result->indexdate; + // Use Unix date to cope for non-english languages + $item->date = (int) $result->start_date ? HTMLHelper::_('date', $result->start_date, 'U') : $result->indexdate; - // Loads item info into RSS array - $this->document->addItem($item); - } - } + // Loads item info into RSS array + $this->document->addItem($item); + } + } } diff --git a/components/com_finder/src/View/Search/HtmlView.php b/components/com_finder/src/View/Search/HtmlView.php index a7494962e7da8..4117cc6f5532f 100644 --- a/components/com_finder/src/View/Search/HtmlView.php +++ b/components/com_finder/src/View/Search/HtmlView.php @@ -1,4 +1,5 @@ params = $app->getParams(); - - // Get view data. - $this->state = $this->get('State'); - $this->query = $this->get('Query'); - \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderQuery') : null; - $this->results = $this->get('Items'); - \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderResults') : null; - $this->total = $this->get('Total'); - \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderTotal') : null; - $this->pagination = $this->get('Pagination'); - \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderPagination') : null; - - // Flag indicates to not add limitstart=0 to URL - $this->pagination->hideEmptyLimitstart = true; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Configure the pathway. - if (!empty($this->query->input)) - { - $app->getPathway()->addItem($this->escape($this->query->input)); - } - - // Check for a double quote in the query string. - if (strpos($this->query->input, '"')) - { - $router = $this->getSiteRouter(); - - // Fix the q variable in the URL. - if ($router->getVar('q') !== $this->query->input) - { - $router->setVar('q', $this->query->input); - } - } - - // Run an event on each result item - if (is_array($this->results)) - { - // Import Finder plugins - PluginHelper::importPlugin('finder'); - - foreach ($this->results as $result) - { - $app->triggerEvent('onFinderResult', array(&$result, &$this->query)); - } - } - - // Log the search - FinderHelper::logSearch($this->query, $this->total); - - // Push out the query data. - $this->suggested = HTMLHelper::_('query.suggested', $this->query); - $this->explained = HTMLHelper::_('query.explained', $this->query); - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); - - // Check for layout override only if this is not the active menu item - // If it is the active menu item, then the view and category id will match - $active = $app->getMenu()->getActive(); - - if (isset($active->query['layout'])) - { - // We need to set the layout in case this is an alternative menu item (with an alternative layout) - $this->setLayout($active->query['layout']); - } - - $this->prepareDocument(); - - \JDEBUG ? Profiler::getInstance('Application')->mark('beforeFinderLayout') : null; - - parent::display($tpl); - - \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderLayout') : null; - } - - /** - * Method to get hidden input fields for a get form so that control variables - * are not lost upon form submission - * - * @return string A string of hidden input form fields - * - * @since 2.5 - */ - protected function getFields() - { - $fields = null; - - // Get the URI. - $uri = Uri::getInstance(Route::_($this->query->toUri())); - $uri->delVar('q'); - $uri->delVar('o'); - $uri->delVar('t'); - $uri->delVar('d1'); - $uri->delVar('d2'); - $uri->delVar('w1'); - $uri->delVar('w2'); - $elements = $uri->getQuery(true); - - // Create hidden input elements for each part of the URI. - foreach ($elements as $n => $v) - { - if (is_scalar($v)) - { - $fields .= ''; - } - } - - return $fields; - } - - /** - * Method to get the layout file for a search result object. - * - * @param string $layout The layout file to check. [optional] - * - * @return string The layout file to use. - * - * @since 2.5 - */ - protected function getLayoutFile($layout = null) - { - // Create and sanitize the file name. - $file = $this->_layout . '_' . preg_replace('/[^A-Z0-9_\.-]/i', '', $layout); - - // Check if the file exists. - $filetofind = $this->_createFileName('template', array('name' => $file)); - $exists = Path::find($this->_path['template'], $filetofind); - - return ($exists ? $layout : 'result'); - } - - /** - * Prepares the document - * - * @return void - * - * @since 2.5 - */ - protected function prepareDocument() - { - $app = Factory::getApplication(); - - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = $app->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_FINDER_DEFAULT_PAGE_TITLE')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($layout = $this->params->get('article_layout')) - { - $this->setLayout($layout); - } - - // Configure the document meta-description. - if (!empty($this->explained)) - { - $explained = $this->escape(html_entity_decode(strip_tags($this->explained), ENT_QUOTES, 'UTF-8')); - $this->document->setDescription($explained); - } - elseif ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - - // Check for OpenSearch - if ($this->params->get('opensearch', 1)) - { - $ostitle = $this->params->get('opensearch_name', - Text::_('COM_FINDER_OPENSEARCH_NAME') . ' ' . $app->get('sitename') - ); - $this->document->addHeadLink( - Uri::getInstance()->toString(array('scheme', 'host', 'port')) . Route::_('index.php?option=com_finder&view=search&format=opensearch'), - 'search', 'rel', array('title' => $ostitle, 'type' => 'application/opensearchdescription+xml') - ); - } - - // Add feed link to the document head. - if ($this->params->get('show_feed_link', 1) == 1) - { - // Add the RSS link. - $props = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); - $route = Route::_($this->query->toUri() . '&format=feed&type=rss'); - $this->document->addHeadLink($route, 'alternate', 'rel', $props); - - // Add the ATOM link. - $props = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); - $route = Route::_($this->query->toUri() . '&format=feed&type=atom'); - $this->document->addHeadLink($route, 'alternate', 'rel', $props); - } - } + use SiteRouterAwareTrait; + + /** + * The query indexer object + * + * @var Query + * + * @since 4.0.0 + */ + protected $query; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + */ + protected $params = null; + + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + */ + protected $state; + + /** + * The logged in user + * + * @var \Joomla\CMS\User\User|null + */ + protected $user = null; + + /** + * The suggested search query + * + * @var string|false + * + * @since 4.0.0 + */ + protected $suggested = false; + + /** + * The explained (human-readable) search query + * + * @var string|null + * + * @since 4.0.0 + */ + protected $explained = null; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * An array of results + * + * @var array + * + * @since 3.8.0 + */ + protected $results; + + /** + * The total number of items + * + * @var integer + * + * @since 3.8.0 + */ + protected $total; + + /** + * The pagination object + * + * @var Pagination + * + * @since 3.8.0 + */ + protected $pagination; + + /** + * Method to display the view. + * + * @param string $tpl A template file to load. [optional] + * + * @return void + * + * @since 2.5 + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $this->params = $app->getParams(); + + // Get view data. + $this->state = $this->get('State'); + $this->query = $this->get('Query'); + \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderQuery') : null; + $this->results = $this->get('Items'); + \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderResults') : null; + $this->total = $this->get('Total'); + \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderTotal') : null; + $this->pagination = $this->get('Pagination'); + \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderPagination') : null; + + // Flag indicates to not add limitstart=0 to URL + $this->pagination->hideEmptyLimitstart = true; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Configure the pathway. + if (!empty($this->query->input)) { + $app->getPathway()->addItem($this->escape($this->query->input)); + } + + // Check for a double quote in the query string. + if (strpos($this->query->input, '"')) { + $router = $this->getSiteRouter(); + + // Fix the q variable in the URL. + if ($router->getVar('q') !== $this->query->input) { + $router->setVar('q', $this->query->input); + } + } + + // Run an event on each result item + if (is_array($this->results)) { + // Import Finder plugins + PluginHelper::importPlugin('finder'); + + foreach ($this->results as $result) { + $app->triggerEvent('onFinderResult', array(&$result, &$this->query)); + } + } + + // Log the search + FinderHelper::logSearch($this->query, $this->total); + + // Push out the query data. + $this->suggested = HTMLHelper::_('query.suggested', $this->query); + $this->explained = HTMLHelper::_('query.explained', $this->query); + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); + + // Check for layout override only if this is not the active menu item + // If it is the active menu item, then the view and category id will match + $active = $app->getMenu()->getActive(); + + if (isset($active->query['layout'])) { + // We need to set the layout in case this is an alternative menu item (with an alternative layout) + $this->setLayout($active->query['layout']); + } + + $this->prepareDocument(); + + \JDEBUG ? Profiler::getInstance('Application')->mark('beforeFinderLayout') : null; + + parent::display($tpl); + + \JDEBUG ? Profiler::getInstance('Application')->mark('afterFinderLayout') : null; + } + + /** + * Method to get hidden input fields for a get form so that control variables + * are not lost upon form submission + * + * @return string A string of hidden input form fields + * + * @since 2.5 + */ + protected function getFields() + { + $fields = null; + + // Get the URI. + $uri = Uri::getInstance(Route::_($this->query->toUri())); + $uri->delVar('q'); + $uri->delVar('o'); + $uri->delVar('t'); + $uri->delVar('d1'); + $uri->delVar('d2'); + $uri->delVar('w1'); + $uri->delVar('w2'); + $elements = $uri->getQuery(true); + + // Create hidden input elements for each part of the URI. + foreach ($elements as $n => $v) { + if (is_scalar($v)) { + $fields .= ''; + } + } + + return $fields; + } + + /** + * Method to get the layout file for a search result object. + * + * @param string $layout The layout file to check. [optional] + * + * @return string The layout file to use. + * + * @since 2.5 + */ + protected function getLayoutFile($layout = null) + { + // Create and sanitize the file name. + $file = $this->_layout . '_' . preg_replace('/[^A-Z0-9_\.-]/i', '', $layout); + + // Check if the file exists. + $filetofind = $this->_createFileName('template', array('name' => $file)); + $exists = Path::find($this->_path['template'], $filetofind); + + return ($exists ? $layout : 'result'); + } + + /** + * Prepares the document + * + * @return void + * + * @since 2.5 + */ + protected function prepareDocument() + { + $app = Factory::getApplication(); + + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = $app->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_FINDER_DEFAULT_PAGE_TITLE')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($layout = $this->params->get('article_layout')) { + $this->setLayout($layout); + } + + // Configure the document meta-description. + if (!empty($this->explained)) { + $explained = $this->escape(html_entity_decode(strip_tags($this->explained), ENT_QUOTES, 'UTF-8')); + $this->document->setDescription($explained); + } elseif ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + + // Check for OpenSearch + if ($this->params->get('opensearch', 1)) { + $ostitle = $this->params->get( + 'opensearch_name', + Text::_('COM_FINDER_OPENSEARCH_NAME') . ' ' . $app->get('sitename') + ); + $this->document->addHeadLink( + Uri::getInstance()->toString(array('scheme', 'host', 'port')) . Route::_('index.php?option=com_finder&view=search&format=opensearch'), + 'search', + 'rel', + array('title' => $ostitle, 'type' => 'application/opensearchdescription+xml') + ); + } + + // Add feed link to the document head. + if ($this->params->get('show_feed_link', 1) == 1) { + // Add the RSS link. + $props = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); + $route = Route::_($this->query->toUri() . '&format=feed&type=rss'); + $this->document->addHeadLink($route, 'alternate', 'rel', $props); + + // Add the ATOM link. + $props = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); + $route = Route::_($this->query->toUri() . '&format=feed&type=atom'); + $this->document->addHeadLink($route, 'alternate', 'rel', $props); + } + } } diff --git a/components/com_finder/src/View/Search/OpensearchView.php b/components/com_finder/src/View/Search/OpensearchView.php index b8eac3f9de54a..3ade1d7c16021 100644 --- a/components/com_finder/src/View/Search/OpensearchView.php +++ b/components/com_finder/src/View/Search/OpensearchView.php @@ -1,4 +1,5 @@ document->setShortName($params->get('opensearch_name', $app->get('sitename'))); - $this->document->setDescription($params->get('opensearch_description', $app->get('MetaDesc'))); + $params = ComponentHelper::getParams('com_finder'); + $this->document->setShortName($params->get('opensearch_name', $app->get('sitename'))); + $this->document->setDescription($params->get('opensearch_description', $app->get('MetaDesc'))); - // Prevent any output when OpenSearch Support is disabled - if (!$params->get('opensearch', 1)) - { - return; - } + // Prevent any output when OpenSearch Support is disabled + if (!$params->get('opensearch', 1)) { + return; + } - // Add the URL for the search - $searchUri = 'index.php?option=com_finder&view=search&q={searchTerms}'; - $suggestionsUri = 'index.php?option=com_finder&task=suggestions.opensearchsuggest&format=json&q={searchTerms}'; - $baseUrl = Uri::getInstance()->toString(array('host', 'port', 'scheme')); - $active = $app->getMenu()->getActive(); + // Add the URL for the search + $searchUri = 'index.php?option=com_finder&view=search&q={searchTerms}'; + $suggestionsUri = 'index.php?option=com_finder&task=suggestions.opensearchsuggest&format=json&q={searchTerms}'; + $baseUrl = Uri::getInstance()->toString(array('host', 'port', 'scheme')); + $active = $app->getMenu()->getActive(); - if ($active->component == 'com_finder') - { - $searchUri .= '&Itemid=' . $active->id; - $suggestionsUri .= '&Itemid=' . $active->id; - } + if ($active->component == 'com_finder') { + $searchUri .= '&Itemid=' . $active->id; + $suggestionsUri .= '&Itemid=' . $active->id; + } - // Add the HTML result view - $htmlSearch = new OpensearchUrl; - $htmlSearch->template = $baseUrl . Route::_($searchUri, false); - $this->document->addUrl($htmlSearch); + // Add the HTML result view + $htmlSearch = new OpensearchUrl(); + $htmlSearch->template = $baseUrl . Route::_($searchUri, false); + $this->document->addUrl($htmlSearch); - // Add the RSS result view - $htmlSearch = new OpensearchUrl; - $htmlSearch->template = $baseUrl . Route::_($searchUri . '&format=feed&type=rss', false); - $htmlSearch->type = 'application/rss+xml'; - $this->document->addUrl($htmlSearch); + // Add the RSS result view + $htmlSearch = new OpensearchUrl(); + $htmlSearch->template = $baseUrl . Route::_($searchUri . '&format=feed&type=rss', false); + $htmlSearch->type = 'application/rss+xml'; + $this->document->addUrl($htmlSearch); - // Add the Atom result view - $htmlSearch = new OpensearchUrl; - $htmlSearch->template = $baseUrl . Route::_($searchUri . '&format=feed&type=atom', false); - $htmlSearch->type = 'application/atom+xml'; - $this->document->addUrl($htmlSearch); + // Add the Atom result view + $htmlSearch = new OpensearchUrl(); + $htmlSearch->template = $baseUrl . Route::_($searchUri . '&format=feed&type=atom', false); + $htmlSearch->type = 'application/atom+xml'; + $this->document->addUrl($htmlSearch); - // Add suggestions URL - if ($params->get('show_autosuggest', 1)) - { - $htmlSearch = new OpensearchUrl; - $htmlSearch->template = $baseUrl . Route::_($suggestionsUri, false); - $htmlSearch->type = 'application/x-suggestions+json'; - $this->document->addUrl($htmlSearch); - } - } + // Add suggestions URL + if ($params->get('show_autosuggest', 1)) { + $htmlSearch = new OpensearchUrl(); + $htmlSearch->template = $baseUrl . Route::_($suggestionsUri, false); + $htmlSearch->type = 'application/x-suggestions+json'; + $this->document->addUrl($htmlSearch); + } + } } diff --git a/components/com_finder/tmpl/search/default.php b/components/com_finder/tmpl/search/default.php index c83d4b72b5adf..419f1443c9e95 100644 --- a/components/com_finder/tmpl/search/default.php +++ b/components/com_finder/tmpl/search/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager() - ->useStyle('com_finder.finder') - ->useScript('com_finder.finder'); + ->useStyle('com_finder.finder') + ->useScript('com_finder.finder'); ?>
    - params->get('show_page_heading')) : ?> -

    - escape($this->params->get('page_heading'))) : ?> - escape($this->params->get('page_heading')); ?> - - escape($this->params->get('page_title')); ?> - -

    - -
    - loadTemplate('form'); ?> -
    - - query->search === true) : ?> -
    - loadTemplate('results'); ?> -
    - + params->get('show_page_heading')) : ?> +

    + escape($this->params->get('page_heading'))) : ?> + escape($this->params->get('page_heading')); ?> + + escape($this->params->get('page_title')); ?> + +

    + +
    + loadTemplate('form'); ?> +
    + + query->search === true) : ?> +
    + loadTemplate('results'); ?> +
    +
    diff --git a/components/com_finder/tmpl/search/default_form.php b/components/com_finder/tmpl/search/default_form.php index cfba0b5a9b912..5c7677391d849 100644 --- a/components/com_finder/tmpl/search/default_form.php +++ b/components/com_finder/tmpl/search/default_form.php @@ -1,4 +1,5 @@ params->get('show_autosuggest', 1)) -{ - $this->document->getWebAssetManager()->usePreset('awesomplete'); - $this->document->addScriptOptions('finder-search', array('url' => Route::_('index.php?option=com_finder&task=suggestions.suggest&format=json&tmpl=component', false))); +if ($this->params->get('show_autosuggest', 1)) { + $this->document->getWebAssetManager()->usePreset('awesomplete'); + $this->document->addScriptOptions('finder-search', array('url' => Route::_('index.php?option=com_finder&task=suggestions.suggest&format=json&tmpl=component', false))); } ?>
    - getFields(); ?> - + getFields(); ?> + - params->get('show_advanced', 1)) : ?> -
    - - - - params->get('show_advanced_tips', 1)) : ?> -
    -
    - - - - - params->get('tuplecount', 1) > 1) : ?> - - - -
    -
    - -
    - query, $this->params); ?> -
    -
    - + params->get('show_advanced', 1)) : ?> +
    + + + + params->get('show_advanced_tips', 1)) : ?> +
    +
    + + + + + params->get('tuplecount', 1) > 1) : ?> + + + +
    +
    + +
    + query, $this->params); ?> +
    +
    +
    diff --git a/components/com_finder/tmpl/search/default_result.php b/components/com_finder/tmpl/search/default_result.php index 8338a98121872..c04d30a3b8e58 100644 --- a/components/com_finder/tmpl/search/default_result.php +++ b/components/com_finder/tmpl/search/default_result.php @@ -1,4 +1,5 @@ getIdentity(); $show_description = $this->params->get('show_description', 1); -if ($show_description) -{ - // Calculate number of characters to display around the result - $term_length = StringHelper::strlen($this->query->input); - $desc_length = $this->params->get('description_length', 255); - $pad_length = $term_length < $desc_length ? (int) floor(($desc_length - $term_length) / 2) : 0; +if ($show_description) { + // Calculate number of characters to display around the result + $term_length = StringHelper::strlen($this->query->input); + $desc_length = $this->params->get('description_length', 255); + $pad_length = $term_length < $desc_length ? (int) floor(($desc_length - $term_length) / 2) : 0; - // Make sure we highlight term both in introtext and fulltext - $full_description = $this->result->description; - if (!empty($this->result->summary) && !empty($this->result->body)) - { - $full_description = Helper::parse($this->result->summary . $this->result->body); - } + // Make sure we highlight term both in introtext and fulltext + $full_description = $this->result->description; + if (!empty($this->result->summary) && !empty($this->result->body)) { + $full_description = Helper::parse($this->result->summary . $this->result->body); + } - // Find the position of the search term - $pos = $term_length ? StringHelper::strpos(StringHelper::strtolower($full_description), StringHelper::strtolower($this->query->input)) : false; + // Find the position of the search term + $pos = $term_length ? StringHelper::strpos(StringHelper::strtolower($full_description), StringHelper::strtolower($this->query->input)) : false; - // Find a potential start point - $start = ($pos && $pos > $pad_length) ? $pos - $pad_length : 0; + // Find a potential start point + $start = ($pos && $pos > $pad_length) ? $pos - $pad_length : 0; - // Find a space between $start and $pos, start right after it. - $space = StringHelper::strpos($full_description, ' ', $start > 0 ? $start - 1 : 0); - $start = ($space && $space < $pos) ? $space + 1 : $start; + // Find a space between $start and $pos, start right after it. + $space = StringHelper::strpos($full_description, ' ', $start > 0 ? $start - 1 : 0); + $start = ($space && $space < $pos) ? $space + 1 : $start; - $description = HTMLHelper::_('string.truncate', StringHelper::substr($full_description, $start), $desc_length, true); + $description = HTMLHelper::_('string.truncate', StringHelper::substr($full_description, $start), $desc_length, true); } $showImage = $this->params->get('show_image', 0); $imageClass = $this->params->get('image_class', ''); $extraAttr = []; -if ($showImage && !empty($this->result->imageUrl) && $imageClass !== '') -{ - $extraAttr['class'] = $imageClass; +if ($showImage && !empty($this->result->imageUrl) && $imageClass !== '') { + $extraAttr['class'] = $imageClass; } $icon = ''; -if (!empty($this->result->mime)) -{ - $icon = ' '; +if (!empty($this->result->mime)) { + $icon = ' '; } $show_url = ''; -if ($this->params->get('show_url', 1)) -{ - $show_url = '' . $this->baseUrl . Route::_($this->result->cleanURL) . ''; +if ($this->params->get('show_url', 1)) { + $show_url = '' . $this->baseUrl . Route::_($this->result->cleanURL) . ''; } ?>
  • - result->imageUrl)) : ?> -
    - params->get('link_image') && $this->result->route) : ?> - - result->imageUrl, $this->result->imageAlt, $extraAttr); ?> - - - result->imageUrl, $this->result->imageAlt, $extraAttr); ?> - -
    - -

    - result->route) : ?> - result->route), - '' . $icon . $this->result->title . '' . $show_url, - [ - 'class' => 'result__title-link' - ] - ); ?> - - result->title; ?> - -

    - -

    - result->start_date && $this->params->get('show_date', 1)) : ?> - - - -

    - - result->getTaxonomy(); ?> - params->get('show_taxonomy', 1)) : ?> -
      - $taxonomy) : ?> - - state == 1 && in_array($branch->access, $user->getAuthorisedViewLevels())) : ?> - - - state == 1 && in_array($node->access, $user->getAuthorisedViewLevels())) : ?> - title; ?> - - - -
    • - : -
    • - - - -
    - + result->imageUrl)) : ?> +
    + params->get('link_image') && $this->result->route) : ?> + + result->imageUrl, $this->result->imageAlt, $extraAttr); ?> + + + result->imageUrl, $this->result->imageAlt, $extraAttr); ?> + +
    + +

    + result->route) : ?> + result->route), + '' . $icon . $this->result->title . '' . $show_url, + [ + 'class' => 'result__title-link' + ] + ); ?> + + result->title; ?> + +

    + +

    + result->start_date && $this->params->get('show_date', 1)) : ?> + + + +

    + + result->getTaxonomy(); ?> + params->get('show_taxonomy', 1)) : ?> +
      + $taxonomy) : ?> + + state == 1 && in_array($branch->access, $user->getAuthorisedViewLevels())) : ?> + + + state == 1 && in_array($node->access, $user->getAuthorisedViewLevels())) : ?> + title; ?> + + + +
    • + : +
    • + + + +
    +
  • diff --git a/components/com_finder/tmpl/search/default_results.php b/components/com_finder/tmpl/search/default_results.php index 691fe7224b3b0..7e283441d5a9b 100644 --- a/components/com_finder/tmpl/search/default_results.php +++ b/components/com_finder/tmpl/search/default_results.php @@ -1,4 +1,5 @@ suggested && $this->params->get('show_suggested_query', 1)) || ($this->explained && $this->params->get('show_explained_query', 1))) : ?> -
    - - suggested && $this->params->get('show_suggested_query', 1)) : ?> - - query->toUri()); ?> - setVar('q', $this->suggested); ?> - - toString(array('path', 'query'))); ?> - ' . $this->escape($this->suggested) . ''; ?> - - explained && $this->params->get('show_explained_query', 1)) : ?> - -

    - total, $this->explained); ?> -

    - -
    +
    + + suggested && $this->params->get('show_suggested_query', 1)) : ?> + + query->toUri()); ?> + setVar('q', $this->suggested); ?> + + toString(array('path', 'query'))); ?> + ' . $this->escape($this->suggested) . ''; ?> + + explained && $this->params->get('show_explained_query', 1)) : ?> + +

    + total, $this->explained); ?> +

    + +
    total === 0) || ($this->total === null)) : ?> -
    -

    - getLanguageFilter() ? '_MULTILANG' : ''; ?> -

    escape($this->query->input)); ?>

    -
    - - +
    +

    + getLanguageFilter() ? '_MULTILANG' : ''; ?> +

    escape($this->query->input)); ?>

    +
    + + query->highlight) && $this->params->get('highlight_terms', 1)) : ?> - document->getWebAssetManager()->useScript('highlight'); - $this->document->addScriptOptions( - 'highlight', - [[ - 'class' => 'js-highlight', - 'highLight' => $this->query->highlight, - ]] - ); - ?> + document->getWebAssetManager()->useScript('highlight'); + $this->document->addScriptOptions( + 'highlight', + [[ + 'class' => 'js-highlight', + 'highLight' => $this->query->highlight, + ]] + ); + ?>
      - baseUrl = Uri::getInstance()->toString(array('scheme', 'host', 'port')); ?> - results as $i => $result) : ?> - result = &$result; ?> - result->counter = $i + 1; ?> - getLayoutFile($this->result->layout); ?> - loadTemplate($layout); ?> - + baseUrl = Uri::getInstance()->toString(array('scheme', 'host', 'port')); ?> + results as $i => $result) : ?> + result = &$result; ?> + result->counter = $i + 1; ?> + getLayoutFile($this->result->layout); ?> + loadTemplate($layout); ?> +
    - params->get('show_pagination', 1) > 0) : ?> -
    - pagination->getPagesLinks(); ?> -
    - - params->get('show_pagination_results', 1) > 0) : ?> -
    - - pagination->limitstart + 1; ?> - pagination->total; ?> - pagination->limit * $this->pagination->pagesCurrent; ?> - $total ? $total : $limit); ?> - -
    - + params->get('show_pagination', 1) > 0) : ?> +
    + pagination->getPagesLinks(); ?> +
    + + params->get('show_pagination_results', 1) > 0) : ?> +
    + + pagination->limitstart + 1; ?> + pagination->total; ?> + pagination->limit * $this->pagination->pagesCurrent; ?> + $total ? $total : $limit); ?> + +
    +
    diff --git a/components/com_media/src/Dispatcher/Dispatcher.php b/components/com_media/src/Dispatcher/Dispatcher.php index 893d134777743..964c118ee516b 100644 --- a/components/com_media/src/Dispatcher/Dispatcher.php +++ b/components/com_media/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->getLanguage()->load('', JPATH_ADMINISTRATOR); - $this->app->getLanguage()->load($this->option, JPATH_ADMINISTRATOR); + /** + * Load the language + * + * @since 4.0.0 + * + * @return void + */ + protected function loadLanguage() + { + // Load the administrator languages needed for the media manager + $this->app->getLanguage()->load('', JPATH_ADMINISTRATOR); + $this->app->getLanguage()->load($this->option, JPATH_ADMINISTRATOR); - parent::loadLanguage(); - } + parent::loadLanguage(); + } - /** - * Method to check component access permission - * - * @since 4.0.0 - * - * @return void - */ - protected function checkAccess() - { - $user = $this->app->getIdentity(); + /** + * Method to check component access permission + * + * @since 4.0.0 + * + * @return void + */ + protected function checkAccess() + { + $user = $this->app->getIdentity(); - // Access check - if (!$user->authorise('core.manage', 'com_media') - && !$user->authorise('core.create', 'com_media')) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + // Access check + if ( + !$user->authorise('core.manage', 'com_media') + && !$user->authorise('core.create', 'com_media') + ) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } - /** - * 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 4.0.0 - */ - public function getController(string $name, string $client = '', array $config = array()): BaseController - { - $config['base_path'] = JPATH_ADMINISTRATOR . '/components/com_media'; + /** + * 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 4.0.0 + */ + public function getController(string $name, string $client = '', array $config = array()): BaseController + { + $config['base_path'] = JPATH_ADMINISTRATOR . '/components/com_media'; - // Force to load the admin controller - return parent::getController($name, 'Administrator', $config); - } + // Force to load the admin controller + return parent::getController($name, 'Administrator', $config); + } } diff --git a/components/com_menus/layouts/joomla/searchtools/default.php b/components/com_menus/layouts/joomla/searchtools/default.php index 456ea9279fe11..0d7cd07e2d764 100644 --- a/components/com_menus/layouts/joomla/searchtools/default.php +++ b/components/com_menus/layouts/joomla/searchtools/default.php @@ -1,4 +1,5 @@ filterForm) && !empty($data['view']->filterForm)) -{ - // Checks if a selector (e.g. client_id) exists. - if ($selectorField = $data['view']->filterForm->getField($selectorFieldName)) - { - $showSelector = $selectorField->getAttribute('filtermode', '') === 'selector' ? true : $showSelector; - - // Checks if a selector should be shown in the current layout. - if (isset($data['view']->layout)) - { - $showSelector = $selectorField->getAttribute('layout', 'default') != $data['view']->layout ? false : $showSelector; - } - - // Unset the selector field from active filters group. - unset($data['view']->activeFilters[$selectorFieldName]); - } - - if ($data['view'] instanceof \Joomla\Component\Menus\Administrator\View\Items\HtmlView) : - unset($data['view']->activeFilters['client_id']); - endif; - - // Checks if the filters button should exist. - $filters = $data['view']->filterForm->getGroup('filter'); - $showFilterButton = isset($filters['filter_search']) && count($filters) === 1 ? false : true; - - // Checks if it should show the be hidden. - $hideActiveFilters = empty($data['view']->activeFilters); - - // Check if the no results message should appear. - if (isset($data['view']->total) && (int) $data['view']->total === 0) - { - $noResults = $data['view']->filterForm->getFieldAttribute('search', 'noresults', '', 'filter'); - if (!empty($noResults)) - { - $noResultsText = Text::_($noResults); - } - } +if (isset($data['view']->filterForm) && !empty($data['view']->filterForm)) { + // Checks if a selector (e.g. client_id) exists. + if ($selectorField = $data['view']->filterForm->getField($selectorFieldName)) { + $showSelector = $selectorField->getAttribute('filtermode', '') === 'selector' ? true : $showSelector; + + // Checks if a selector should be shown in the current layout. + if (isset($data['view']->layout)) { + $showSelector = $selectorField->getAttribute('layout', 'default') != $data['view']->layout ? false : $showSelector; + } + + // Unset the selector field from active filters group. + unset($data['view']->activeFilters[$selectorFieldName]); + } + + if ($data['view'] instanceof \Joomla\Component\Menus\Administrator\View\Items\HtmlView) : + unset($data['view']->activeFilters['client_id']); + endif; + + // Checks if the filters button should exist. + $filters = $data['view']->filterForm->getGroup('filter'); + $showFilterButton = isset($filters['filter_search']) && count($filters) === 1 ? false : true; + + // Checks if it should show the be hidden. + $hideActiveFilters = empty($data['view']->activeFilters); + + // Check if the no results message should appear. + if (isset($data['view']->total) && (int) $data['view']->total === 0) { + $noResults = $data['view']->filterForm->getFieldAttribute('search', 'noresults', '', 'filter'); + if (!empty($noResults)) { + $noResultsText = Text::_($noResults); + } + } } // Set some basic options. $customOptions = array( - 'filtersHidden' => isset($data['options']['filtersHidden']) && $data['options']['filtersHidden'] ? $data['options']['filtersHidden'] : $hideActiveFilters, - 'filterButton' => isset($data['options']['filterButton']) && $data['options']['filterButton'] ? $data['options']['filterButton'] : $showFilterButton, - 'defaultLimit' => $data['options']['defaultLimit'] ?? Factory::getApplication()->get('list_limit', 20), - 'searchFieldSelector' => '#filter_search', - 'selectorFieldName' => $selectorFieldName, - 'showSelector' => $showSelector, - 'orderFieldSelector' => '#list_fullordering', - 'showNoResults' => !empty($noResultsText), - 'noResultsText' => !empty($noResultsText) ? $noResultsText : '', - 'formSelector' => !empty($data['options']['formSelector']) ? $data['options']['formSelector'] : '#adminForm', + 'filtersHidden' => isset($data['options']['filtersHidden']) && $data['options']['filtersHidden'] ? $data['options']['filtersHidden'] : $hideActiveFilters, + 'filterButton' => isset($data['options']['filterButton']) && $data['options']['filterButton'] ? $data['options']['filterButton'] : $showFilterButton, + 'defaultLimit' => $data['options']['defaultLimit'] ?? Factory::getApplication()->get('list_limit', 20), + 'searchFieldSelector' => '#filter_search', + 'selectorFieldName' => $selectorFieldName, + 'showSelector' => $showSelector, + 'orderFieldSelector' => '#list_fullordering', + 'showNoResults' => !empty($noResultsText), + 'noResultsText' => !empty($noResultsText) ? $noResultsText : '', + 'formSelector' => !empty($data['options']['formSelector']) ? $data['options']['formSelector'] : '#adminForm', ); // Merge custom options in the options array. @@ -89,39 +85,39 @@ HTMLHelper::_('searchtools.form', $data['options']['formSelector'], $data['options']); ?> - sublayout('noitems', $data); ?> + sublayout('noitems', $data); ?> diff --git a/components/com_menus/src/Dispatcher/Dispatcher.php b/components/com_menus/src/Dispatcher/Dispatcher.php index 4a87d0ac39111..7273b065286b5 100644 --- a/components/com_menus/src/Dispatcher/Dispatcher.php +++ b/components/com_menus/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->getLanguage()->load('com_menus', JPATH_ADMINISTRATOR); - } + /** + * Load the language + * + * @since 4.0.0 + * + * @return void + */ + protected function loadLanguage() + { + $this->app->getLanguage()->load('com_menus', JPATH_ADMINISTRATOR); + } - /** - * Dispatch a controller task. Redirecting the user if appropriate. - * - * @return void - * - * @since 4.0.0 - */ - public function checkAccess() - { - parent::checkAccess(); + /** + * Dispatch a controller task. Redirecting the user if appropriate. + * + * @return void + * + * @since 4.0.0 + */ + public function checkAccess() + { + parent::checkAccess(); - if ($this->input->get('view') !== 'items' - || $this->input->get('layout') !== 'modal' - || !$this->app->getIdentity()->authorise('core.create', 'com_menus')) - { - throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); - } - } + if ( + $this->input->get('view') !== 'items' + || $this->input->get('layout') !== 'modal' + || !$this->app->getIdentity()->authorise('core.create', 'com_menus') + ) { + throw new NotAllowed($this->app->getLanguage()->_('JERROR_ALERTNOAUTHOR'), 403); + } + } - /** - * 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 \Joomla\CMS\MVC\Controller\BaseController - * - * @since 4.0.0 - */ - public function getController(string $name, string $client = '', array $config = array()): BaseController - { - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - $client = 'Administrator'; + /** + * 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 \Joomla\CMS\MVC\Controller\BaseController + * + * @since 4.0.0 + */ + public function getController(string $name, string $client = '', array $config = array()): BaseController + { + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + $client = 'Administrator'; - return parent::getController($name, $client, $config); - } + return parent::getController($name, $client, $config); + } } diff --git a/components/com_modules/src/Controller/DisplayController.php b/components/com_modules/src/Controller/DisplayController.php index e0f008346b87a..2e22c3707bd56 100644 --- a/components/com_modules/src/Controller/DisplayController.php +++ b/components/com_modules/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input = Factory::getApplication()->input; + /** + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The Application for the dispatcher + * @param Input|null $input The Input object for the request + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + $this->input = Factory::getApplication()->input; - // Modules frontpage Editor Module proxying. - if ($this->input->get('view') === 'modules' && $this->input->get('layout') === 'modal') - { - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - } + // Modules frontpage Editor Module proxying. + if ($this->input->get('view') === 'modules' && $this->input->get('layout') === 'modal') { + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + } - parent::__construct($config, $factory, $app, $input); - } + parent::__construct($config, $factory, $app, $input); + } } diff --git a/components/com_modules/src/Dispatcher/Dispatcher.php b/components/com_modules/src/Dispatcher/Dispatcher.php index daead79a2b2d8..b4d4e84beeb25 100644 --- a/components/com_modules/src/Dispatcher/Dispatcher.php +++ b/components/com_modules/src/Dispatcher/Dispatcher.php @@ -1,4 +1,5 @@ app->getLanguage()->load('com_modules', JPATH_ADMINISTRATOR); - } + /** + * Load the language + * + * @since 4.0.0 + * + * @return void + */ + protected function loadLanguage() + { + $this->app->getLanguage()->load('com_modules', JPATH_ADMINISTRATOR); + } - /** - * Dispatch a controller task. Redirecting the user if appropriate. - * - * @return void - * - * @since 4.0.0 - */ - public function checkAccess() - { - parent::checkAccess(); + /** + * Dispatch a controller task. Redirecting the user if appropriate. + * + * @return void + * + * @since 4.0.0 + */ + public function checkAccess() + { + parent::checkAccess(); - if ($this->input->get('view') === 'modules' - && $this->input->get('layout') === 'modal' - && !$this->app->getIdentity()->authorise('core.create', 'com_modules')) - { - throw new NotAllowed; - } - } + if ( + $this->input->get('view') === 'modules' + && $this->input->get('layout') === 'modal' + && !$this->app->getIdentity()->authorise('core.create', 'com_modules') + ) { + throw new NotAllowed(); + } + } - /** - * 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 \Joomla\CMS\MVC\Controller\BaseController - * - * @since 4.0.0 - */ - public function getController(string $name, string $client = '', array $config = array()): BaseController - { - if ($this->input->get('task') === 'orderPosition') - { - $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; - $client = 'Administrator'; - } + /** + * 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 \Joomla\CMS\MVC\Controller\BaseController + * + * @since 4.0.0 + */ + public function getController(string $name, string $client = '', array $config = array()): BaseController + { + if ($this->input->get('task') === 'orderPosition') { + $config['base_path'] = JPATH_COMPONENT_ADMINISTRATOR; + $client = 'Administrator'; + } - return parent::getController($name, $client, $config); - } + return parent::getController($name, $client, $config); + } } diff --git a/components/com_newsfeeds/helpers/route.php b/components/com_newsfeeds/helpers/route.php index 6ca8fecb9eae3..536053380a7b5 100644 --- a/components/com_newsfeeds/helpers/route.php +++ b/components/com_newsfeeds/helpers/route.php @@ -1,13 +1,14 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt - */ -defined('_JEXEC') or die; + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + */ use Joomla\Component\Newsfeeds\Site\Helper\RouteHelper; diff --git a/components/com_newsfeeds/src/Controller/DisplayController.php b/components/com_newsfeeds/src/Controller/DisplayController.php index 16de07e9e822d..aab32dc96d99b 100644 --- a/components/com_newsfeeds/src/Controller/DisplayController.php +++ b/components/com_newsfeeds/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'categories'); - $this->input->set('view', $vName); - - if ($this->app->getIdentity()->get('id') || ($this->input->getMethod() === 'POST' && $vName === 'category' )) - { - $cachable = false; - } - - $safeurlparams = array('id' => 'INT', 'limit' => 'UINT', 'limitstart' => 'UINT', - 'filter_order' => 'CMD', 'filter_order_Dir' => 'CMD', 'lang' => 'CMD'); - - return parent::display($cachable, $safeurlparams); - } + /** + * Method to show a newsfeeds view + * + * @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()}. + * + * @return static This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $cachable = true; + + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'categories'); + $this->input->set('view', $vName); + + if ($this->app->getIdentity()->get('id') || ($this->input->getMethod() === 'POST' && $vName === 'category' )) { + $cachable = false; + } + + $safeurlparams = array('id' => 'INT', 'limit' => 'UINT', 'limitstart' => 'UINT', + 'filter_order' => 'CMD', 'filter_order_Dir' => 'CMD', 'lang' => 'CMD'); + + return parent::display($cachable, $safeurlparams); + } } diff --git a/components/com_newsfeeds/src/Helper/AssociationHelper.php b/components/com_newsfeeds/src/Helper/AssociationHelper.php index c751582630221..9e052289e2f2a 100644 --- a/components/com_newsfeeds/src/Helper/AssociationHelper.php +++ b/components/com_newsfeeds/src/Helper/AssociationHelper.php @@ -1,4 +1,5 @@ input; - $view = $view ?? $jinput->get('view'); - $id = empty($id) ? $jinput->getInt('id') : $id; - - if ($view === 'newsfeed') - { - if ($id) - { - $associations = Associations::getAssociations('com_newsfeeds', '#__newsfeeds', 'com_newsfeeds.item', $id); - - $return = array(); - - foreach ($associations as $tag => $item) - { - $return[$tag] = RouteHelper::getNewsfeedRoute($item->id, (int) $item->catid, $item->language); - } - - return $return; - } - } - - if ($view === 'category' || $view === 'categories') - { - return self::getCategoryAssociations($id, 'com_newsfeeds'); - } - - return array(); - } + /** + * Method to get the associations for a given item + * + * @param integer $id Id of the item + * @param string $view Name of the view + * + * @return array Array of associations for the item + * + * @since 3.0 + */ + public static function getAssociations($id = 0, $view = null) + { + $jinput = Factory::getApplication()->input; + $view = $view ?? $jinput->get('view'); + $id = empty($id) ? $jinput->getInt('id') : $id; + + if ($view === 'newsfeed') { + if ($id) { + $associations = Associations::getAssociations('com_newsfeeds', '#__newsfeeds', 'com_newsfeeds.item', $id); + + $return = array(); + + foreach ($associations as $tag => $item) { + $return[$tag] = RouteHelper::getNewsfeedRoute($item->id, (int) $item->catid, $item->language); + } + + return $return; + } + } + + if ($view === 'category' || $view === 'categories') { + return self::getCategoryAssociations($id, 'com_newsfeeds'); + } + + return array(); + } } diff --git a/components/com_newsfeeds/src/Helper/RouteHelper.php b/components/com_newsfeeds/src/Helper/RouteHelper.php index 6f3be804299f0..f520b5b892a7c 100644 --- a/components/com_newsfeeds/src/Helper/RouteHelper.php +++ b/components/com_newsfeeds/src/Helper/RouteHelper.php @@ -1,4 +1,5 @@ 1) - { - $link .= '&catid=' . $catid; - } + if ((int) $catid > 1) { + $link .= '&catid=' . $catid; + } - if ($language && $language !== '*' && Multilanguage::isEnabled()) - { - $link .= '&lang=' . $language; - } + if ($language && $language !== '*' && Multilanguage::isEnabled()) { + $link .= '&lang=' . $language; + } - return $link; - } + return $link; + } - /** - * getCategoryRoute - * - * @param int $catid category id - * @param int $language language - * - * @return string - */ - public static function getCategoryRoute($catid, $language = 0) - { - if ($catid instanceof CategoryNode) - { - $id = $catid->id; - } - else - { - $id = (int) $catid; - } + /** + * getCategoryRoute + * + * @param int $catid category id + * @param int $language language + * + * @return string + */ + public static function getCategoryRoute($catid, $language = 0) + { + if ($catid instanceof CategoryNode) { + $id = $catid->id; + } else { + $id = (int) $catid; + } - if ($id < 1) - { - $link = ''; - } - else - { - // Create the link - $link = 'index.php?option=com_newsfeeds&view=category&id=' . $id; + if ($id < 1) { + $link = ''; + } else { + // Create the link + $link = 'index.php?option=com_newsfeeds&view=category&id=' . $id; - if ($language && $language !== '*' && Multilanguage::isEnabled()) - { - $link .= '&lang=' . $language; - } - } + if ($language && $language !== '*' && Multilanguage::isEnabled()) { + $link .= '&lang=' . $language; + } + } - return $link; - } + return $link; + } } diff --git a/components/com_newsfeeds/src/Model/CategoriesModel.php b/components/com_newsfeeds/src/Model/CategoriesModel.php index 964ab8b39b618..740fdb7630fe8 100644 --- a/components/com_newsfeeds/src/Model/CategoriesModel.php +++ b/components/com_newsfeeds/src/Model/CategoriesModel.php @@ -1,4 +1,5 @@ setState('filter.extension', $this->_extension); - - // Get the parent id if defined. - $parentId = $app->input->getInt('id'); - $this->setState('filter.parentId', $parentId); - - $params = $app->getParams(); - $this->setState('params', $params); - - $this->setState('filter.published', 1); - $this->setState('filter.access', true); - } - - /** - * Method to get a store id based on model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id A prefix for the store id. - * - * @return string A store id. - */ - protected function getStoreId($id = '') - { - // Compile the store id. - $id .= ':' . $this->getState('filter.extension'); - $id .= ':' . $this->getState('filter.published'); - $id .= ':' . $this->getState('filter.access'); - $id .= ':' . $this->getState('filter.parentId'); - - return parent::getStoreId($id); - } - - /** - * redefine the function and add some properties to make the styling easier - * - * @return mixed An array of data items on success, false on failure. - */ - public function getItems() - { - if ($this->_items === null) - { - $app = Factory::getApplication(); - $menu = $app->getMenu(); - $active = $menu->getActive(); - - if ($active) - { - $params = $active->getParams(); - } - else - { - $params = new Registry; - } - - $options = array(); - $options['countItems'] = $params->get('show_cat_items_cat', 1) || !$params->get('show_empty_categories_cat', 0); - $categories = Categories::getInstance('Newsfeeds', $options); - $this->_parent = $categories->get($this->getState('filter.parentId', 'root')); - - if (is_object($this->_parent)) - { - $this->_items = $this->_parent->getChildren(); - } - else - { - $this->_items = false; - } - } - - return $this->_items; - } - - /** - * get the Parent - * - * @return null - */ - public function getParent() - { - if (!is_object($this->_parent)) - { - $this->getItems(); - } - - return $this->_parent; - } + /** + * Model context string. + * + * @var string + */ + public $_context = 'com_newsfeeds.categories'; + + /** + * The category context (allows other extensions to derived from this model). + * + * @var string + */ + protected $_extension = 'com_newsfeeds'; + + /** + * Parent category of the current one + * + * @var CategoryNode|null + */ + private $_parent = null; + + /** + * Array of child-categories + * + * @var CategoryNode[]|null + */ + private $_items = null; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field + * @param string $direction An optional direction [asc|desc] + * + * @return void + * + * @throws \Exception + * + * @since 1.6 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + $this->setState('filter.extension', $this->_extension); + + // Get the parent id if defined. + $parentId = $app->input->getInt('id'); + $this->setState('filter.parentId', $parentId); + + $params = $app->getParams(); + $this->setState('params', $params); + + $this->setState('filter.published', 1); + $this->setState('filter.access', true); + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.extension'); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . $this->getState('filter.access'); + $id .= ':' . $this->getState('filter.parentId'); + + return parent::getStoreId($id); + } + + /** + * redefine the function and add some properties to make the styling easier + * + * @return mixed An array of data items on success, false on failure. + */ + public function getItems() + { + if ($this->_items === null) { + $app = Factory::getApplication(); + $menu = $app->getMenu(); + $active = $menu->getActive(); + + if ($active) { + $params = $active->getParams(); + } else { + $params = new Registry(); + } + + $options = array(); + $options['countItems'] = $params->get('show_cat_items_cat', 1) || !$params->get('show_empty_categories_cat', 0); + $categories = Categories::getInstance('Newsfeeds', $options); + $this->_parent = $categories->get($this->getState('filter.parentId', 'root')); + + if (is_object($this->_parent)) { + $this->_items = $this->_parent->getChildren(); + } else { + $this->_items = false; + } + } + + return $this->_items; + } + + /** + * get the Parent + * + * @return null + */ + public function getParent() + { + if (!is_object($this->_parent)) { + $this->getItems(); + } + + return $this->_parent; + } } diff --git a/components/com_newsfeeds/src/Model/CategoryModel.php b/components/com_newsfeeds/src/Model/CategoryModel.php index 6ba828b2d4cd0..393d927800b4d 100644 --- a/components/com_newsfeeds/src/Model/CategoryModel.php +++ b/components/com_newsfeeds/src/Model/CategoryModel.php @@ -1,4 +1,5 @@ _params)) - { - $item->params = new Registry($item->params); - } - - // Some contexts may not use tags data at all, so we allow callers to disable loading tag data - if ($this->getState('load_tags', true)) - { - $item->tags = new TagsHelper; - $taggedItems[$item->id] = $item; - } - } - - // Load tags of all items. - if ($taggedItems) - { - $tagsHelper = new TagsHelper; - $itemIds = \array_keys($taggedItems); - - foreach ($tagsHelper->getMultipleItemTags('com_newsfeeds.newsfeed', $itemIds) as $id => $tags) - { - $taggedItems[$id]->tags->itemTags = $tags; - } - } - - return $items; - } - - /** - * Method to build an SQL query to load the list data. - * - * @return \Joomla\Database\DatabaseQuery An SQL query - * - * @since 1.6 - */ - protected function getListQuery() - { - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - - // Create a new query object. - $db = $this->getDatabase(); - - /** @var \Joomla\Database\DatabaseQuery $query */ - $query = $db->getQuery(true); - - // Select required fields from the categories. - $query->select($this->getState('list.select', $db->quoteName('a') . '.*')) - ->from($db->quoteName('#__newsfeeds', 'a')) - ->whereIn($db->quoteName('a.access'), $groups); - - // Filter by category. - if ($categoryId = (int) $this->getState('category.id')) - { - $query->where($db->quoteName('a.catid') . ' = :categoryId') - ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) - ->whereIn($db->quoteName('c.access'), $groups) - ->bind(':categoryId', $categoryId, ParameterType::INTEGER); - } - - // Filter by state - $state = $this->getState('filter.published'); - - if (is_numeric($state)) - { - $state = (int) $state; - $query->where($db->quoteName('a.published') . ' = :state') - ->bind(':state', $state, ParameterType::INTEGER); - } - else - { - $query->where($db->quoteName('a.published') . ' IN (0,1,2)'); - } - - // Filter by start and end dates. - if ($this->getState('filter.publish_date')) - { - $nowDate = Factory::getDate()->toSql(); - - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_up') . ' IS NULL', - $db->quoteName('a.publish_up') . ' <= :nowDate1', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_down') . ' IS NULL', - $db->quoteName('a.publish_down') . ' >= :nowDate2', - ], - 'OR' - ) - ->bind([':nowDate1', ':nowDate2'], $nowDate); - } - - // Filter by search in title - if ($search = $this->getState('list.filter')) - { - $search = '%' . $search . '%'; - $query->where($db->quoteName('a.name') . ' LIKE :search') - ->bind(':search', $search); - } - - // Filter by language - if ($this->getState('filter.language')) - { - $query->whereIn($db->quoteName('a.language'), [Factory::getApplication()->getLanguage()->getTag(), '*'], ParameterType::STRING); - } - - // Add the list ordering clause. - $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); - - return $query; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field - * @param string $direction An optional direction [asc|desc] - * - * @return void - * - * @since 1.6 - * - * @throws \Exception - */ - protected function populateState($ordering = null, $direction = null) - { - $app = Factory::getApplication(); - $params = ComponentHelper::getParams('com_newsfeeds'); - - // List state information - $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->get('list_limit'), 'uint'); - $this->setState('list.limit', $limit); - - $limitstart = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.start', $limitstart); - - // Optional filter text - $this->setState('list.filter', $app->input->getString('filter-search')); - - $orderCol = $app->input->get('filter_order', 'ordering'); - - if (!in_array($orderCol, $this->filter_fields)) - { - $orderCol = 'ordering'; - } - - $this->setState('list.ordering', $orderCol); - - $listOrder = $app->input->get('filter_order_Dir', 'ASC'); - - if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) - { - $listOrder = 'ASC'; - } - - $this->setState('list.direction', $listOrder); - - $id = $app->input->get('id', 0, 'int'); - $this->setState('category.id', $id); - - $user = Factory::getUser(); - - if ((!$user->authorise('core.edit.state', 'com_newsfeeds')) && (!$user->authorise('core.edit', 'com_newsfeeds'))) - { - // Limit to published for people who can't edit or edit.state. - $this->setState('filter.published', 1); - - // Filter by start and end dates. - $this->setState('filter.publish_date', true); - } - - $this->setState('filter.language', Multilanguage::isEnabled()); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Method to get category data for the current category - * - * @return object - * - * @since 1.5 - */ - public function getCategory() - { - if (!is_object($this->_item)) - { - $app = Factory::getApplication(); - $menu = $app->getMenu(); - $active = $menu->getActive(); - - if ($active) - { - $params = $active->getParams(); - } - else - { - $params = new Registry; - } - - $options = array(); - $options['countItems'] = $params->get('show_cat_items', 1) || $params->get('show_empty_categories', 0); - $categories = Categories::getInstance('Newsfeeds', $options); - $this->_item = $categories->get($this->getState('category.id', 'root')); - - if (is_object($this->_item)) - { - $this->_children = $this->_item->getChildren(); - $this->_parent = false; - - if ($this->_item->getParent()) - { - $this->_parent = $this->_item->getParent(); - } - - $this->_rightsibling = $this->_item->getSibling(); - $this->_leftsibling = $this->_item->getSibling(false); - } - else - { - $this->_children = false; - $this->_parent = false; - } - } - - return $this->_item; - } - - /** - * Get the parent category. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function getParent() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_parent; - } - - /** - * Get the sibling (adjacent) categories. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function &getLeftSibling() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_leftsibling; - } - - /** - * Get the sibling (adjacent) categories. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function &getRightSibling() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_rightsibling; - } - - /** - * Get the child categories. - * - * @return mixed An array of categories or false if an error occurs. - */ - public function &getChildren() - { - if (!is_object($this->_item)) - { - $this->getCategory(); - } - - return $this->_children; - } - - /** - * Increment the hit counter for the category. - * - * @param int $pk Optional primary key of the category to increment. - * - * @return boolean True if successful; false otherwise and internal error set. - */ - public function hit($pk = 0) - { - $input = Factory::getApplication()->input; - $hitcount = $input->getInt('hitcount', 1); - - if ($hitcount) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('category.id'); - $table = Table::getInstance('Category', 'JTable'); - $table->hit($pk); - } - - return true; - } + /** + * Category items data + * + * @var array + */ + protected $_item; + + /** + * Array of newsfeeds in the category + * + * @var \stdClass[] + */ + protected $_articles; + + /** + * Category left and right of this one + * + * @var CategoryNode[]|null + */ + protected $_siblings; + + /** + * Array of child-categories + * + * @var CategoryNode[]|null + */ + protected $_children; + + /** + * Parent category of the current one + * + * @var CategoryNode|null + */ + protected $_parent; + + /** + * The category that applies. + * + * @var object + */ + protected $_category; + + /** + * The list of other newsfeed categories. + * + * @var array + */ + protected $_categories; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'id', 'a.id', + 'name', 'a.name', + 'numarticles', 'a.numarticles', + 'link', 'a.link', + 'ordering', 'a.ordering', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to get a list of items. + * + * @return mixed An array of objects on success, false on failure. + */ + public function getItems() + { + // Invoke the parent getItems method to get the main list + $items = parent::getItems(); + + $taggedItems = []; + + // Convert the params field into an object, saving original in _params + foreach ($items as $item) { + if (!isset($this->_params)) { + $item->params = new Registry($item->params); + } + + // Some contexts may not use tags data at all, so we allow callers to disable loading tag data + if ($this->getState('load_tags', true)) { + $item->tags = new TagsHelper(); + $taggedItems[$item->id] = $item; + } + } + + // Load tags of all items. + if ($taggedItems) { + $tagsHelper = new TagsHelper(); + $itemIds = \array_keys($taggedItems); + + foreach ($tagsHelper->getMultipleItemTags('com_newsfeeds.newsfeed', $itemIds) as $id => $tags) { + $taggedItems[$id]->tags->itemTags = $tags; + } + } + + return $items; + } + + /** + * Method to build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery An SQL query + * + * @since 1.6 + */ + protected function getListQuery() + { + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + + // Create a new query object. + $db = $this->getDatabase(); + + /** @var \Joomla\Database\DatabaseQuery $query */ + $query = $db->getQuery(true); + + // Select required fields from the categories. + $query->select($this->getState('list.select', $db->quoteName('a') . '.*')) + ->from($db->quoteName('#__newsfeeds', 'a')) + ->whereIn($db->quoteName('a.access'), $groups); + + // Filter by category. + if ($categoryId = (int) $this->getState('category.id')) { + $query->where($db->quoteName('a.catid') . ' = :categoryId') + ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) + ->whereIn($db->quoteName('c.access'), $groups) + ->bind(':categoryId', $categoryId, ParameterType::INTEGER); + } + + // Filter by state + $state = $this->getState('filter.published'); + + if (is_numeric($state)) { + $state = (int) $state; + $query->where($db->quoteName('a.published') . ' = :state') + ->bind(':state', $state, ParameterType::INTEGER); + } else { + $query->where($db->quoteName('a.published') . ' IN (0,1,2)'); + } + + // Filter by start and end dates. + if ($this->getState('filter.publish_date')) { + $nowDate = Factory::getDate()->toSql(); + + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_up') . ' IS NULL', + $db->quoteName('a.publish_up') . ' <= :nowDate1', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_down') . ' IS NULL', + $db->quoteName('a.publish_down') . ' >= :nowDate2', + ], + 'OR' + ) + ->bind([':nowDate1', ':nowDate2'], $nowDate); + } + + // Filter by search in title + if ($search = $this->getState('list.filter')) { + $search = '%' . $search . '%'; + $query->where($db->quoteName('a.name') . ' LIKE :search') + ->bind(':search', $search); + } + + // Filter by language + if ($this->getState('filter.language')) { + $query->whereIn($db->quoteName('a.language'), [Factory::getApplication()->getLanguage()->getTag(), '*'], ParameterType::STRING); + } + + // Add the list ordering clause. + $query->order($db->escape($this->getState('list.ordering', 'a.ordering')) . ' ' . $db->escape($this->getState('list.direction', 'ASC'))); + + return $query; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field + * @param string $direction An optional direction [asc|desc] + * + * @return void + * + * @since 1.6 + * + * @throws \Exception + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + $params = ComponentHelper::getParams('com_newsfeeds'); + + // List state information + $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->get('list_limit'), 'uint'); + $this->setState('list.limit', $limit); + + $limitstart = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.start', $limitstart); + + // Optional filter text + $this->setState('list.filter', $app->input->getString('filter-search')); + + $orderCol = $app->input->get('filter_order', 'ordering'); + + if (!in_array($orderCol, $this->filter_fields)) { + $orderCol = 'ordering'; + } + + $this->setState('list.ordering', $orderCol); + + $listOrder = $app->input->get('filter_order_Dir', 'ASC'); + + if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) { + $listOrder = 'ASC'; + } + + $this->setState('list.direction', $listOrder); + + $id = $app->input->get('id', 0, 'int'); + $this->setState('category.id', $id); + + $user = Factory::getUser(); + + if ((!$user->authorise('core.edit.state', 'com_newsfeeds')) && (!$user->authorise('core.edit', 'com_newsfeeds'))) { + // Limit to published for people who can't edit or edit.state. + $this->setState('filter.published', 1); + + // Filter by start and end dates. + $this->setState('filter.publish_date', true); + } + + $this->setState('filter.language', Multilanguage::isEnabled()); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Method to get category data for the current category + * + * @return object + * + * @since 1.5 + */ + public function getCategory() + { + if (!is_object($this->_item)) { + $app = Factory::getApplication(); + $menu = $app->getMenu(); + $active = $menu->getActive(); + + if ($active) { + $params = $active->getParams(); + } else { + $params = new Registry(); + } + + $options = array(); + $options['countItems'] = $params->get('show_cat_items', 1) || $params->get('show_empty_categories', 0); + $categories = Categories::getInstance('Newsfeeds', $options); + $this->_item = $categories->get($this->getState('category.id', 'root')); + + if (is_object($this->_item)) { + $this->_children = $this->_item->getChildren(); + $this->_parent = false; + + if ($this->_item->getParent()) { + $this->_parent = $this->_item->getParent(); + } + + $this->_rightsibling = $this->_item->getSibling(); + $this->_leftsibling = $this->_item->getSibling(false); + } else { + $this->_children = false; + $this->_parent = false; + } + } + + return $this->_item; + } + + /** + * Get the parent category. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function getParent() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_parent; + } + + /** + * Get the sibling (adjacent) categories. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function &getLeftSibling() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_leftsibling; + } + + /** + * Get the sibling (adjacent) categories. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function &getRightSibling() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_rightsibling; + } + + /** + * Get the child categories. + * + * @return mixed An array of categories or false if an error occurs. + */ + public function &getChildren() + { + if (!is_object($this->_item)) { + $this->getCategory(); + } + + return $this->_children; + } + + /** + * Increment the hit counter for the category. + * + * @param int $pk Optional primary key of the category to increment. + * + * @return boolean True if successful; false otherwise and internal error set. + */ + public function hit($pk = 0) + { + $input = Factory::getApplication()->input; + $hitcount = $input->getInt('hitcount', 1); + + if ($hitcount) { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('category.id'); + $table = Table::getInstance('Category', 'JTable'); + $table->hit($pk); + } + + return true; + } } diff --git a/components/com_newsfeeds/src/Model/NewsfeedModel.php b/components/com_newsfeeds/src/Model/NewsfeedModel.php index f63e671d6a450..d31a247d3315a 100644 --- a/components/com_newsfeeds/src/Model/NewsfeedModel.php +++ b/components/com_newsfeeds/src/Model/NewsfeedModel.php @@ -1,4 +1,5 @@ input->getInt('id'); - $this->setState('newsfeed.id', $pk); - - $offset = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.offset', $offset); - - // Load the parameters. - $params = $app->getParams(); - $this->setState('params', $params); - - $user = Factory::getUser(); - - if ((!$user->authorise('core.edit.state', 'com_newsfeeds')) && (!$user->authorise('core.edit', 'com_newsfeeds'))) - { - $this->setState('filter.published', 1); - $this->setState('filter.archived', 2); - } - } - - /** - * Method to get newsfeed data. - * - * @param integer $pk The id of the newsfeed. - * - * @return mixed Menu item data object on success, false on failure. - * - * @since 1.6 - */ - public function &getItem($pk = null) - { - $pk = (int) $pk ?: (int) $this->getState('newsfeed.id'); - - if ($this->_item === null) - { - $this->_item = array(); - } - - if (!isset($this->_item[$pk])) - { - try - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select( - [ - $this->getState('item.select', $db->quoteName('a') . '.*'), - $db->quoteName('c.title', 'category_title'), - $db->quoteName('c.alias', 'category_alias'), - $db->quoteName('c.access', 'category_access'), - $db->quoteName('u.name', 'author'), - $db->quoteName('parent.title', 'parent_title'), - $db->quoteName('parent.id', 'parent_id'), - $db->quoteName('parent.path', 'parent_route'), - $db->quoteName('parent.alias', 'parent_alias'), - ] - ) - ->from($db->quoteName('#__newsfeeds', 'a')) - ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) - ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('a.created_by')) - ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')) - ->where($db->quoteName('a.id') . ' = :id') - ->bind(':id', $pk, ParameterType::INTEGER); - - // Filter by published state. - $published = $this->getState('filter.published'); - $archived = $this->getState('filter.archived'); - - if (is_numeric($published)) - { - // Filter by start and end dates. - $nowDate = Factory::getDate()->toSql(); - - $published = (int) $published; - $archived = (int) $archived; - - $query->extendWhere( - 'AND', - [ - $db->quoteName('a.published') . ' = :published1', - $db->quoteName('a.published') . ' = :archived1', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_up') . ' IS NULL', - $db->quoteName('a.publish_up') . ' <= :nowDate1', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('a.publish_down') . ' IS NULL', - $db->quoteName('a.publish_down') . ' >= :nowDate2', - ], - 'OR' - ) - ->extendWhere( - 'AND', - [ - $db->quoteName('c.published') . ' = :published2', - $db->quoteName('c.published') . ' = :archived2', - ], - 'OR' - ) - ->bind([':published1', ':published2'], $published, ParameterType::INTEGER) - ->bind([':archived1', ':archived2'], $archived, ParameterType::INTEGER) - ->bind([':nowDate1', ':nowDate2'], $nowDate); - } - - $db->setQuery($query); - - $data = $db->loadObject(); - - if ($data === null) - { - throw new \Exception(Text::_('COM_NEWSFEEDS_ERROR_FEED_NOT_FOUND'), 404); - } - - // Check for published state if filter set. - - if ((is_numeric($published) || is_numeric($archived)) && $data->published != $published && $data->published != $archived) - { - throw new \Exception(Text::_('COM_NEWSFEEDS_ERROR_FEED_NOT_FOUND'), 404); - } - - // Convert parameter fields to objects. - $registry = new Registry($data->params); - $data->params = clone $this->getState('params'); - $data->params->merge($registry); - - $data->metadata = new Registry($data->metadata); - - // Compute access permissions. - - if ($access = $this->getState('filter.access')) - { - // If the access filter has been set, we already know this user can view. - $data->params->set('access-view', true); - } - else - { - // If no access filter is set, the layout takes some responsibility for display of limited information. - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - $data->params->set('access-view', in_array($data->access, $groups) && in_array($data->category_access, $groups)); - } - - $this->_item[$pk] = $data; - } - catch (\Exception $e) - { - $this->setError($e); - $this->_item[$pk] = false; - } - } - - return $this->_item[$pk]; - } - - /** - * Increment the hit counter for the newsfeed. - * - * @param int $pk Optional primary key of the item to increment. - * - * @return boolean True if successful; false otherwise and internal error set. - * - * @since 3.0 - */ - public function hit($pk = 0) - { - $input = Factory::getApplication()->input; - $hitcount = $input->getInt('hitcount', 1); - - if ($hitcount) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('newsfeed.id'); - - $table = $this->getTable('Newsfeed', 'Administrator'); - $table->hit($pk); - } - - return true; - } + /** + * Model context string. + * + * @var string + * @since 1.6 + */ + protected $_context = 'com_newsfeeds.newsfeed'; + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + */ + protected function populateState() + { + $app = Factory::getApplication(); + + // Load state from the request. + $pk = $app->input->getInt('id'); + $this->setState('newsfeed.id', $pk); + + $offset = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.offset', $offset); + + // Load the parameters. + $params = $app->getParams(); + $this->setState('params', $params); + + $user = Factory::getUser(); + + if ((!$user->authorise('core.edit.state', 'com_newsfeeds')) && (!$user->authorise('core.edit', 'com_newsfeeds'))) { + $this->setState('filter.published', 1); + $this->setState('filter.archived', 2); + } + } + + /** + * Method to get newsfeed data. + * + * @param integer $pk The id of the newsfeed. + * + * @return mixed Menu item data object on success, false on failure. + * + * @since 1.6 + */ + public function &getItem($pk = null) + { + $pk = (int) $pk ?: (int) $this->getState('newsfeed.id'); + + if ($this->_item === null) { + $this->_item = array(); + } + + if (!isset($this->_item[$pk])) { + try { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select( + [ + $this->getState('item.select', $db->quoteName('a') . '.*'), + $db->quoteName('c.title', 'category_title'), + $db->quoteName('c.alias', 'category_alias'), + $db->quoteName('c.access', 'category_access'), + $db->quoteName('u.name', 'author'), + $db->quoteName('parent.title', 'parent_title'), + $db->quoteName('parent.id', 'parent_id'), + $db->quoteName('parent.path', 'parent_route'), + $db->quoteName('parent.alias', 'parent_alias'), + ] + ) + ->from($db->quoteName('#__newsfeeds', 'a')) + ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('a.catid')) + ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('a.created_by')) + ->join('LEFT', $db->quoteName('#__categories', 'parent'), $db->quoteName('parent.id') . ' = ' . $db->quoteName('c.parent_id')) + ->where($db->quoteName('a.id') . ' = :id') + ->bind(':id', $pk, ParameterType::INTEGER); + + // Filter by published state. + $published = $this->getState('filter.published'); + $archived = $this->getState('filter.archived'); + + if (is_numeric($published)) { + // Filter by start and end dates. + $nowDate = Factory::getDate()->toSql(); + + $published = (int) $published; + $archived = (int) $archived; + + $query->extendWhere( + 'AND', + [ + $db->quoteName('a.published') . ' = :published1', + $db->quoteName('a.published') . ' = :archived1', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_up') . ' IS NULL', + $db->quoteName('a.publish_up') . ' <= :nowDate1', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('a.publish_down') . ' IS NULL', + $db->quoteName('a.publish_down') . ' >= :nowDate2', + ], + 'OR' + ) + ->extendWhere( + 'AND', + [ + $db->quoteName('c.published') . ' = :published2', + $db->quoteName('c.published') . ' = :archived2', + ], + 'OR' + ) + ->bind([':published1', ':published2'], $published, ParameterType::INTEGER) + ->bind([':archived1', ':archived2'], $archived, ParameterType::INTEGER) + ->bind([':nowDate1', ':nowDate2'], $nowDate); + } + + $db->setQuery($query); + + $data = $db->loadObject(); + + if ($data === null) { + throw new \Exception(Text::_('COM_NEWSFEEDS_ERROR_FEED_NOT_FOUND'), 404); + } + + // Check for published state if filter set. + + if ((is_numeric($published) || is_numeric($archived)) && $data->published != $published && $data->published != $archived) { + throw new \Exception(Text::_('COM_NEWSFEEDS_ERROR_FEED_NOT_FOUND'), 404); + } + + // Convert parameter fields to objects. + $registry = new Registry($data->params); + $data->params = clone $this->getState('params'); + $data->params->merge($registry); + + $data->metadata = new Registry($data->metadata); + + // Compute access permissions. + + if ($access = $this->getState('filter.access')) { + // If the access filter has been set, we already know this user can view. + $data->params->set('access-view', true); + } else { + // If no access filter is set, the layout takes some responsibility for display of limited information. + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + $data->params->set('access-view', in_array($data->access, $groups) && in_array($data->category_access, $groups)); + } + + $this->_item[$pk] = $data; + } catch (\Exception $e) { + $this->setError($e); + $this->_item[$pk] = false; + } + } + + return $this->_item[$pk]; + } + + /** + * Increment the hit counter for the newsfeed. + * + * @param int $pk Optional primary key of the item to increment. + * + * @return boolean True if successful; false otherwise and internal error set. + * + * @since 3.0 + */ + public function hit($pk = 0) + { + $input = Factory::getApplication()->input; + $hitcount = $input->getInt('hitcount', 1); + + if ($hitcount) { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('newsfeed.id'); + + $table = $this->getTable('Newsfeed', 'Administrator'); + $table->hit($pk); + } + + return true; + } } diff --git a/components/com_newsfeeds/src/Service/Category.php b/components/com_newsfeeds/src/Service/Category.php index 2c2ae1c0a1b04..63158c8e3a7d6 100644 --- a/components/com_newsfeeds/src/Service/Category.php +++ b/components/com_newsfeeds/src/Service/Category.php @@ -1,4 +1,5 @@ categoryFactory = $categoryFactory; - $this->db = $db; - - $params = ComponentHelper::getParams('com_newsfeeds'); - $this->noIDs = (bool) $params->get('sef_ids'); - $categories = new RouterViewConfiguration('categories'); - $categories->setKey('id'); - $this->registerView($categories); - $category = new RouterViewConfiguration('category'); - $category->setKey('id')->setParent($categories, 'catid')->setNestable(); - $this->registerView($category); - $newsfeed = new RouterViewConfiguration('newsfeed'); - $newsfeed->setKey('id')->setParent($category, 'catid'); - $this->registerView($newsfeed); - - parent::__construct($app, $menu); - - $this->attachRule(new MenuRules($this)); - $this->attachRule(new StandardRules($this)); - $this->attachRule(new NomenuRules($this)); - } - - /** - * Method to get the segment(s) for a category - * - * @param string $id ID of the category to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getCategorySegment($id, $query) - { - $category = $this->getCategories()->get($id); - - if ($category) - { - $path = array_reverse($category->getPath(), true); - $path[0] = '1:root'; - - if ($this->noIDs) - { - foreach ($path as &$segment) - { - list($id, $segment) = explode(':', $segment, 2); - } - } - - return $path; - } - - return array(); - } - - /** - * Method to get the segment(s) for a category - * - * @param string $id ID of the category to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getCategoriesSegment($id, $query) - { - return $this->getCategorySegment($id, $query); - } - - /** - * Method to get the segment(s) for a newsfeed - * - * @param string $id ID of the newsfeed to retrieve the segments for - * @param array $query The request that is built right now - * - * @return array|string The segments of this item - */ - public function getNewsfeedSegment($id, $query) - { - if (!strpos($id, ':')) - { - $id = (int) $id; - $dbquery = $this->db->getQuery(true); - $dbquery->select($this->db->quoteName('alias')) - ->from($this->db->quoteName('#__newsfeeds')) - ->where($this->db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - $this->db->setQuery($dbquery); - - $id .= ':' . $this->db->loadResult(); - } - - if ($this->noIDs) - { - list($void, $segment) = explode(':', $id, 2); - - return array($void => $segment); - } - - return array((int) $id => $id); - } - - /** - * Method to get the id for a category - * - * @param string $segment Segment to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getCategoryId($segment, $query) - { - if (isset($query['id'])) - { - $category = $this->getCategories(['access' => false])->get($query['id']); - - if ($category) - { - foreach ($category->getChildren() as $child) - { - if ($this->noIDs) - { - if ($child->alias === $segment) - { - return $child->id; - } - } - else - { - if ($child->id == (int) $segment) - { - return $child->id; - } - } - } - } - } - - return false; - } - - /** - * Method to get the segment(s) for a category - * - * @param string $segment Segment to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getCategoriesId($segment, $query) - { - return $this->getCategoryId($segment, $query); - } - - /** - * Method to get the segment(s) for a newsfeed - * - * @param string $segment Segment of the newsfeed to retrieve the ID for - * @param array $query The request that is parsed right now - * - * @return mixed The id of this item or false - */ - public function getNewsfeedId($segment, $query) - { - if ($this->noIDs) - { - $dbquery = $this->db->getQuery(true); - $dbquery->select($this->db->quoteName('id')) - ->from($this->db->quoteName('#__newsfeeds')) - ->where( - [ - $this->db->quoteName('alias') . ' = :segment', - $this->db->quoteName('catid') . ' = :id', - ] - ) - ->bind(':segment', $segment) - ->bind(':id', $query['id'], ParameterType::INTEGER); - $this->db->setQuery($dbquery); - - return (int) $this->db->loadResult(); - } - - return (int) $segment; - } - - /** - * Method to get categories from cache - * - * @param array $options The options for retrieving categories - * - * @return CategoryInterface The object containing categories - * - * @since 4.0.0 - */ - private function getCategories(array $options = []): CategoryInterface - { - $key = serialize($options); - - if (!isset($this->categoryCache[$key])) - { - $this->categoryCache[$key] = $this->categoryFactory->createCategory($options); - } - - return $this->categoryCache[$key]; - } + /** + * Flag to remove IDs + * + * @var boolean + */ + protected $noIDs = false; + + /** + * The category factory + * + * @var CategoryFactoryInterface + * + * @since 4.0.0 + */ + private $categoryFactory; + + /** + * The category cache + * + * @var array + * + * @since 4.0.0 + */ + private $categoryCache = []; + + /** + * The db + * + * @var DatabaseInterface + * + * @since 4.0.0 + */ + private $db; + + /** + * Newsfeeds Component router constructor + * + * @param SiteApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + * @param CategoryFactoryInterface $categoryFactory The category object + * @param DatabaseInterface $db The database object + */ + public function __construct(SiteApplication $app, AbstractMenu $menu, CategoryFactoryInterface $categoryFactory, DatabaseInterface $db) + { + $this->categoryFactory = $categoryFactory; + $this->db = $db; + + $params = ComponentHelper::getParams('com_newsfeeds'); + $this->noIDs = (bool) $params->get('sef_ids'); + $categories = new RouterViewConfiguration('categories'); + $categories->setKey('id'); + $this->registerView($categories); + $category = new RouterViewConfiguration('category'); + $category->setKey('id')->setParent($categories, 'catid')->setNestable(); + $this->registerView($category); + $newsfeed = new RouterViewConfiguration('newsfeed'); + $newsfeed->setKey('id')->setParent($category, 'catid'); + $this->registerView($newsfeed); + + parent::__construct($app, $menu); + + $this->attachRule(new MenuRules($this)); + $this->attachRule(new StandardRules($this)); + $this->attachRule(new NomenuRules($this)); + } + + /** + * Method to get the segment(s) for a category + * + * @param string $id ID of the category to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getCategorySegment($id, $query) + { + $category = $this->getCategories()->get($id); + + if ($category) { + $path = array_reverse($category->getPath(), true); + $path[0] = '1:root'; + + if ($this->noIDs) { + foreach ($path as &$segment) { + list($id, $segment) = explode(':', $segment, 2); + } + } + + return $path; + } + + return array(); + } + + /** + * Method to get the segment(s) for a category + * + * @param string $id ID of the category to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getCategoriesSegment($id, $query) + { + return $this->getCategorySegment($id, $query); + } + + /** + * Method to get the segment(s) for a newsfeed + * + * @param string $id ID of the newsfeed to retrieve the segments for + * @param array $query The request that is built right now + * + * @return array|string The segments of this item + */ + public function getNewsfeedSegment($id, $query) + { + if (!strpos($id, ':')) { + $id = (int) $id; + $dbquery = $this->db->getQuery(true); + $dbquery->select($this->db->quoteName('alias')) + ->from($this->db->quoteName('#__newsfeeds')) + ->where($this->db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + $this->db->setQuery($dbquery); + + $id .= ':' . $this->db->loadResult(); + } + + if ($this->noIDs) { + list($void, $segment) = explode(':', $id, 2); + + return array($void => $segment); + } + + return array((int) $id => $id); + } + + /** + * Method to get the id for a category + * + * @param string $segment Segment to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getCategoryId($segment, $query) + { + if (isset($query['id'])) { + $category = $this->getCategories(['access' => false])->get($query['id']); + + if ($category) { + foreach ($category->getChildren() as $child) { + if ($this->noIDs) { + if ($child->alias === $segment) { + return $child->id; + } + } else { + if ($child->id == (int) $segment) { + return $child->id; + } + } + } + } + } + + return false; + } + + /** + * Method to get the segment(s) for a category + * + * @param string $segment Segment to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getCategoriesId($segment, $query) + { + return $this->getCategoryId($segment, $query); + } + + /** + * Method to get the segment(s) for a newsfeed + * + * @param string $segment Segment of the newsfeed to retrieve the ID for + * @param array $query The request that is parsed right now + * + * @return mixed The id of this item or false + */ + public function getNewsfeedId($segment, $query) + { + if ($this->noIDs) { + $dbquery = $this->db->getQuery(true); + $dbquery->select($this->db->quoteName('id')) + ->from($this->db->quoteName('#__newsfeeds')) + ->where( + [ + $this->db->quoteName('alias') . ' = :segment', + $this->db->quoteName('catid') . ' = :id', + ] + ) + ->bind(':segment', $segment) + ->bind(':id', $query['id'], ParameterType::INTEGER); + $this->db->setQuery($dbquery); + + return (int) $this->db->loadResult(); + } + + return (int) $segment; + } + + /** + * Method to get categories from cache + * + * @param array $options The options for retrieving categories + * + * @return CategoryInterface The object containing categories + * + * @since 4.0.0 + */ + private function getCategories(array $options = []): CategoryInterface + { + $key = serialize($options); + + if (!isset($this->categoryCache[$key])) { + $this->categoryCache[$key] = $this->categoryFactory->createCategory($options); + } + + return $this->categoryCache[$key]; + } } diff --git a/components/com_newsfeeds/src/View/Categories/HtmlView.php b/components/com_newsfeeds/src/View/Categories/HtmlView.php index e8076475251c2..fc13f59bf40b3 100644 --- a/components/com_newsfeeds/src/View/Categories/HtmlView.php +++ b/components/com_newsfeeds/src/View/Categories/HtmlView.php @@ -1,4 +1,5 @@ commonCategoryDisplay(); - - // Flag indicates to not add limitstart=0 to URL - $this->pagination->hideEmptyLimitstart = true; - - // Prepare the data. - // Compute the newsfeed slug. - foreach ($this->items as $item) - { - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - $temp = $item->params; - $item->params = clone $this->params; - $item->params->merge($temp); - } - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - */ - protected function prepareDocument() - { - parent::prepareDocument(); - - $menu = $this->menu; - $id = (int) @$menu->query['id']; - - if ($menu && (!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] === 'newsfeed' - || $id != $this->category->id)) - { - $path = array(array('title' => $this->category->title, 'link' => '')); - $category = $this->category->getParent(); - - while ((!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] === 'newsfeed' - || $id != $category->id) && $category->id > 1) - { - $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)); - $category = $category->getParent(); - } - - $path = array_reverse($path); - - foreach ($path as $item) - { - $this->pathway->addItem($item['title'], $item['link']); - } - } - } + /** + * @var string Default title to use for page title + * @since 3.2 + */ + protected $defaultPageTitle = 'COM_NEWSFEEDS_DEFAULT_PAGE_TITLE'; + + /** + * @var string The name of the extension for the category + * @since 3.2 + */ + protected $extension = 'com_newsfeeds'; + + /** + * @var string The name of the view to link individual items to + * @since 3.2 + */ + protected $viewName = 'newsfeed'; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->commonCategoryDisplay(); + + // Flag indicates to not add limitstart=0 to URL + $this->pagination->hideEmptyLimitstart = true; + + // Prepare the data. + // Compute the newsfeed slug. + foreach ($this->items as $item) { + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + $temp = $item->params; + $item->params = clone $this->params; + $item->params->merge($temp); + } + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + */ + protected function prepareDocument() + { + parent::prepareDocument(); + + $menu = $this->menu; + $id = (int) @$menu->query['id']; + + if ( + $menu && (!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] === 'newsfeed' + || $id != $this->category->id) + ) { + $path = array(array('title' => $this->category->title, 'link' => '')); + $category = $this->category->getParent(); + + while ( + (!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] === 'newsfeed' + || $id != $category->id) && $category->id > 1 + ) { + $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id, $category->language)); + $category = $category->getParent(); + } + + $path = array_reverse($path); + + foreach ($path as $item) { + $this->pathway->addItem($item['title'], $item['link']); + } + } + } } diff --git a/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php b/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php index 92675cc012365..22a0a13190251 100644 --- a/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php +++ b/components/com_newsfeeds/src/View/Newsfeed/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser(); - - // Get view related request variables. - $print = $app->input->getBool('print'); - - // Get model data. - $state = $this->get('State'); - $item = $this->get('Item'); - - // Check for errors. - // @TODO: Maybe this could go into ComponentHelper::raiseErrors($this->get('Errors')) - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Add router helpers. - $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; - $item->catslug = $item->category_alias ? ($item->catid . ':' . $item->category_alias) : $item->catid; - $item->parent_slug = $item->category_alias ? ($item->parent_id . ':' . $item->parent_alias) : $item->parent_id; - - // Merge newsfeed params. If this is single-newsfeed view, menu params override newsfeed params - // Otherwise, newsfeed params override menu item params - $params = $state->get('params'); - $newsfeed_params = clone $item->params; - $active = $app->getMenu()->getActive(); - $temp = clone $params; - - // Check to see which parameters should take priority - if ($active) - { - $currentLink = $active->link; - - // If the current view is the active item and a newsfeed view for this feed, then the menu item params take priority - if (strpos($currentLink, 'view=newsfeed') && strpos($currentLink, '&id=' . (string) $item->id)) - { - // $item->params are the newsfeed params, $temp are the menu item params - // Merge so that the menu item params take priority - $newsfeed_params->merge($temp); - $item->params = $newsfeed_params; - - // Load layout from active query (in case it is an alternative menu item) - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - } - else - { - // Current view is not a single newsfeed, so the newsfeed params take priority here - // Merge the menu item params with the newsfeed params so that the newsfeed params take priority - $temp->merge($newsfeed_params); - $item->params = $temp; - - // Check for alternative layouts (since we are not in a single-newsfeed menu item) - if ($layout = $item->params->get('newsfeed_layout')) - { - $this->setLayout($layout); - } - } - } - else - { - // Merge so that newsfeed params take priority - $temp->merge($newsfeed_params); - $item->params = $temp; - - // Check for alternative layouts (since we are not in a single-newsfeed menu item) - if ($layout = $item->params->get('newsfeed_layout')) - { - $this->setLayout($layout); - } - } - - // Check the access to the newsfeed - $levels = $user->getAuthorisedViewLevels(); - - if (!in_array($item->access, $levels) || (in_array($item->access, $levels) && (!in_array($item->category_access, $levels)))) - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $app->setHeader('status', 403, true); - - return; - } - - // Get the current menu item - $params = $app->getParams(); - - $params->merge($item->params); - - try - { - $feed = new FeedFactory; - $this->rssDoc = $feed->getFeed($item->link); - } - catch (\InvalidArgumentException $e) - { - $msg = Text::_('COM_NEWSFEEDS_ERRORS_FEED_NOT_RETRIEVED'); - } - catch (\RuntimeException $e) - { - $msg = Text::_('COM_NEWSFEEDS_ERRORS_FEED_NOT_RETRIEVED'); - } - - if (empty($this->rssDoc)) - { - $msg = Text::_('COM_NEWSFEEDS_ERRORS_FEED_NOT_RETRIEVED'); - } - - $feed_display_order = $params->get('feed_display_order', 'des'); - - if ($feed_display_order === 'asc') - { - $this->rssDoc->reverseItems(); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - - $this->params = $params; - $this->state = $state; - $this->item = $item; - $this->user = $user; - - if (!empty($msg)) - { - $this->msg = $msg; - } - - $this->print = $print; - - $item->tags = new TagsHelper; - $item->tags->getItemTags('com_newsfeeds.newsfeed', $item->id); - - // Increment the hit counter of the newsfeed. - $model = $this->getModel(); - $model->hit(); - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - * - * @since 1.6 - */ - protected function _prepareDocument() - { - $app = Factory::getApplication(); - $pathway = $app->getPathway(); - - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = $app->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_NEWSFEEDS_DEFAULT_PAGE_TITLE')); - } - - $title = $this->params->get('page_title', ''); - - $id = (int) @$menu->query['id']; - - // If the menu item does not concern this newsfeed - if ($menu && (!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] !== 'newsfeed' - || $id != $this->item->id)) - { - // If this is not a single newsfeed menu item, set the page title to the newsfeed title - if ($this->item->name) - { - $title = $this->item->name; - } - - $path = array(array('title' => $this->item->name, 'link' => '')); - $category = Categories::getInstance('Newsfeeds')->get($this->item->catid); - - while ((!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] === 'newsfeed' - || $id != $category->id) && $category->id > 1) - { - $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id)); - $category = $category->getParent(); - } - - $path = array_reverse($path); - - foreach ($path as $item) - { - $pathway->addItem($item['title'], $item['link']); - } - } - - if (empty($title)) - { - $title = $this->item->name; - } - - $this->setDocumentTitle($title); - - if ($this->item->metadesc) - { - $this->document->setDescription($this->item->metadesc); - } - elseif ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - - if ($app->get('MetaAuthor') == '1') - { - $this->document->setMetaData('author', $this->item->author); - } - - $mdata = $this->item->metadata->toArray(); - - foreach ($mdata as $k => $v) - { - if ($v) - { - $this->document->setMetaData($k, $v); - } - } - } + /** + * The model state + * + * @var object + * + * @since 1.6 + */ + protected $state; + + /** + * The newsfeed item + * + * @var object + * + * @since 1.6 + */ + protected $item; + + /** + * UNUSED? + * + * @var boolean + * + * @since 1.6 + */ + protected $print; + + /** + * The current user instance + * + * @var \Joomla\CMS\User\User|null + * + * @since 4.0.0 + */ + protected $user = null; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 4.0.0 + */ + protected $params; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.6 + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $user = $this->getCurrentUser(); + + // Get view related request variables. + $print = $app->input->getBool('print'); + + // Get model data. + $state = $this->get('State'); + $item = $this->get('Item'); + + // Check for errors. + // @TODO: Maybe this could go into ComponentHelper::raiseErrors($this->get('Errors')) + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Add router helpers. + $item->slug = $item->alias ? ($item->id . ':' . $item->alias) : $item->id; + $item->catslug = $item->category_alias ? ($item->catid . ':' . $item->category_alias) : $item->catid; + $item->parent_slug = $item->category_alias ? ($item->parent_id . ':' . $item->parent_alias) : $item->parent_id; + + // Merge newsfeed params. If this is single-newsfeed view, menu params override newsfeed params + // Otherwise, newsfeed params override menu item params + $params = $state->get('params'); + $newsfeed_params = clone $item->params; + $active = $app->getMenu()->getActive(); + $temp = clone $params; + + // Check to see which parameters should take priority + if ($active) { + $currentLink = $active->link; + + // If the current view is the active item and a newsfeed view for this feed, then the menu item params take priority + if (strpos($currentLink, 'view=newsfeed') && strpos($currentLink, '&id=' . (string) $item->id)) { + // $item->params are the newsfeed params, $temp are the menu item params + // Merge so that the menu item params take priority + $newsfeed_params->merge($temp); + $item->params = $newsfeed_params; + + // Load layout from active query (in case it is an alternative menu item) + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } + } else { + // Current view is not a single newsfeed, so the newsfeed params take priority here + // Merge the menu item params with the newsfeed params so that the newsfeed params take priority + $temp->merge($newsfeed_params); + $item->params = $temp; + + // Check for alternative layouts (since we are not in a single-newsfeed menu item) + if ($layout = $item->params->get('newsfeed_layout')) { + $this->setLayout($layout); + } + } + } else { + // Merge so that newsfeed params take priority + $temp->merge($newsfeed_params); + $item->params = $temp; + + // Check for alternative layouts (since we are not in a single-newsfeed menu item) + if ($layout = $item->params->get('newsfeed_layout')) { + $this->setLayout($layout); + } + } + + // Check the access to the newsfeed + $levels = $user->getAuthorisedViewLevels(); + + if (!in_array($item->access, $levels) || (in_array($item->access, $levels) && (!in_array($item->category_access, $levels)))) { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $app->setHeader('status', 403, true); + + return; + } + + // Get the current menu item + $params = $app->getParams(); + + $params->merge($item->params); + + try { + $feed = new FeedFactory(); + $this->rssDoc = $feed->getFeed($item->link); + } catch (\InvalidArgumentException $e) { + $msg = Text::_('COM_NEWSFEEDS_ERRORS_FEED_NOT_RETRIEVED'); + } catch (\RuntimeException $e) { + $msg = Text::_('COM_NEWSFEEDS_ERRORS_FEED_NOT_RETRIEVED'); + } + + if (empty($this->rssDoc)) { + $msg = Text::_('COM_NEWSFEEDS_ERRORS_FEED_NOT_RETRIEVED'); + } + + $feed_display_order = $params->get('feed_display_order', 'des'); + + if ($feed_display_order === 'asc') { + $this->rssDoc->reverseItems(); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + + $this->params = $params; + $this->state = $state; + $this->item = $item; + $this->user = $user; + + if (!empty($msg)) { + $this->msg = $msg; + } + + $this->print = $print; + + $item->tags = new TagsHelper(); + $item->tags->getItemTags('com_newsfeeds.newsfeed', $item->id); + + // Increment the hit counter of the newsfeed. + $model = $this->getModel(); + $model->hit(); + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + * + * @since 1.6 + */ + protected function _prepareDocument() + { + $app = Factory::getApplication(); + $pathway = $app->getPathway(); + + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = $app->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_NEWSFEEDS_DEFAULT_PAGE_TITLE')); + } + + $title = $this->params->get('page_title', ''); + + $id = (int) @$menu->query['id']; + + // If the menu item does not concern this newsfeed + if ( + $menu && (!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] !== 'newsfeed' + || $id != $this->item->id) + ) { + // If this is not a single newsfeed menu item, set the page title to the newsfeed title + if ($this->item->name) { + $title = $this->item->name; + } + + $path = array(array('title' => $this->item->name, 'link' => '')); + $category = Categories::getInstance('Newsfeeds')->get($this->item->catid); + + while ( + (!isset($menu->query['option']) || $menu->query['option'] !== 'com_newsfeeds' || $menu->query['view'] === 'newsfeed' + || $id != $category->id) && $category->id > 1 + ) { + $path[] = array('title' => $category->title, 'link' => RouteHelper::getCategoryRoute($category->id)); + $category = $category->getParent(); + } + + $path = array_reverse($path); + + foreach ($path as $item) { + $pathway->addItem($item['title'], $item['link']); + } + } + + if (empty($title)) { + $title = $this->item->name; + } + + $this->setDocumentTitle($title); + + if ($this->item->metadesc) { + $this->document->setDescription($this->item->metadesc); + } elseif ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + + if ($app->get('MetaAuthor') == '1') { + $this->document->setMetaData('author', $this->item->author); + } + + $mdata = $this->item->metadata->toArray(); + + foreach ($mdata as $k => $v) { + if ($v) { + $this->document->setMetaData($k, $v); + } + } + } } diff --git a/components/com_newsfeeds/tmpl/categories/default.php b/components/com_newsfeeds/tmpl/categories/default.php index 1a90ac23d72b0..3335f6061d218 100644 --- a/components/com_newsfeeds/tmpl/categories/default.php +++ b/components/com_newsfeeds/tmpl/categories/default.php @@ -1,4 +1,5 @@
    - - loadTemplate('items'); ?> + + loadTemplate('items'); ?>
    diff --git a/components/com_newsfeeds/tmpl/categories/default_items.php b/components/com_newsfeeds/tmpl/categories/default_items.php index a762475053450..0b3d6aff68a7f 100644 --- a/components/com_newsfeeds/tmpl/categories/default_items.php +++ b/components/com_newsfeeds/tmpl/categories/default_items.php @@ -1,4 +1,5 @@ maxLevelcat != 0 && count($this->items[$this->parent->id]) > 0) : ?> - items[$this->parent->id] as $id => $item) : ?> - params->get('show_empty_categories_cat') || $item->numitems || count($item->getChildren())) : ?> -
    - - params->get('show_subcat_desc_cat') == 1) : ?> - description) : ?> -
    - description, '', 'com_newsfeeds.categories'); ?> -
    - - - getChildren()) > 0 && $this->maxLevelcat > 1) : ?> -
    - items[$item->id] = $item->getChildren(); ?> - parent = $item; ?> - maxLevelcat--; ?> - loadTemplate('items'); ?> - parent = $item->getParent(); ?> - maxLevelcat++; ?> -
    - -
    - - + items[$this->parent->id] as $id => $item) : ?> + params->get('show_empty_categories_cat') || $item->numitems || count($item->getChildren())) : ?> +
    + + params->get('show_subcat_desc_cat') == 1) : ?> + description) : ?> +
    + description, '', 'com_newsfeeds.categories'); ?> +
    + + + getChildren()) > 0 && $this->maxLevelcat > 1) : ?> +
    + items[$item->id] = $item->getChildren(); ?> + parent = $item; ?> + maxLevelcat--; ?> + loadTemplate('items'); ?> + parent = $item->getParent(); ?> + maxLevelcat++; ?> +
    + +
    + + diff --git a/components/com_newsfeeds/tmpl/category/default.php b/components/com_newsfeeds/tmpl/category/default.php index 12ce5fafcdcb7..f3852a4e7ee20 100644 --- a/components/com_newsfeeds/tmpl/category/default.php +++ b/components/com_newsfeeds/tmpl/category/default.php @@ -1,4 +1,5 @@ params->get('show_page_heading') ? 'h2' : 'h1'; ?>
    - params->get('show_page_heading')) : ?> -

    - escape($this->params->get('page_heading')); ?> -

    - - params->get('show_category_title', 1)) : ?> - <> - category->title, '', 'com_newsfeeds.category.title'); ?> - > - - params->get('show_tags', 1) && !empty($this->category->tags->itemTags)) : ?> - category->tagLayout = new FileLayout('joomla.content.tags'); ?> - category->tagLayout->render($this->category->tags->itemTags); ?> - - params->get('show_description', 1) || $this->params->def('show_description_image', 1)) : ?> -
    - params->get('show_description_image') && $this->category->getParams()->get('image')) : ?> - $this->category->getParams()->get('image'), - 'alt' => empty($this->category->getParams()->get('image_alt')) && empty($this->category->getParams()->get('image_alt_empty')) ? false : $this->category->getParams()->get('image_alt'), - ] - ); ?> - - params->get('show_description') && $this->category->description) : ?> - category->description, '', 'com_newsfeeds.category'); ?> - -
    -
    - - loadTemplate('items'); ?> - maxLevel != 0 && !empty($this->children[$this->category->id])) : ?> -
    -

    - -

    - loadTemplate('children'); ?> -
    - + params->get('show_page_heading')) : ?> +

    + escape($this->params->get('page_heading')); ?> +

    + + params->get('show_category_title', 1)) : ?> + <> + category->title, '', 'com_newsfeeds.category.title'); ?> + > + + params->get('show_tags', 1) && !empty($this->category->tags->itemTags)) : ?> + category->tagLayout = new FileLayout('joomla.content.tags'); ?> + category->tagLayout->render($this->category->tags->itemTags); ?> + + params->get('show_description', 1) || $this->params->def('show_description_image', 1)) : ?> +
    + params->get('show_description_image') && $this->category->getParams()->get('image')) : ?> + $this->category->getParams()->get('image'), + 'alt' => empty($this->category->getParams()->get('image_alt')) && empty($this->category->getParams()->get('image_alt_empty')) ? false : $this->category->getParams()->get('image_alt'), + ] + ); ?> + + params->get('show_description') && $this->category->description) : ?> + category->description, '', 'com_newsfeeds.category'); ?> + +
    +
    + + loadTemplate('items'); ?> + maxLevel != 0 && !empty($this->children[$this->category->id])) : ?> +
    +

    + +

    + loadTemplate('children'); ?> +
    +
    diff --git a/components/com_newsfeeds/tmpl/category/default_children.php b/components/com_newsfeeds/tmpl/category/default_children.php index 7c7e074964c60..09b81be97867d 100644 --- a/components/com_newsfeeds/tmpl/category/default_children.php +++ b/components/com_newsfeeds/tmpl/category/default_children.php @@ -1,4 +1,5 @@ maxLevel != 0 && count($this->children[$this->category->id]) > 0) : ?> - +
    - items)) : ?> -

    - -
    - params->get('filter_field') !== 'hide' || $this->params->get('show_pagination_limit')) : ?> -
    - params->get('filter_field') !== 'hide' && $this->params->get('filter_field') == '1') : ?> -
    - - -
    - - params->get('show_pagination_limit')) : ?> -
    - - pagination->getLimitBox(); ?> -
    - -
    - -
      - items as $item) : ?> -
    • - params->get('show_articles')) : ?> - - numarticles); ?> - - - - - - published == 0) : ?> - - - - -
      - params->get('show_link')) : ?> - link); ?> - - - - - -
      - -
    • - -
    - - items)) : ?> - params->def('show_pagination', 2) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> -
    - params->def('show_pagination_results', 1)) : ?> -

    - pagination->getPagesCounter(); ?> -

    - - pagination->getPagesLinks(); ?> -
    - - -
    - + items)) : ?> +

    + +
    + params->get('filter_field') !== 'hide' || $this->params->get('show_pagination_limit')) : ?> +
    + params->get('filter_field') !== 'hide' && $this->params->get('filter_field') == '1') : ?> +
    + + +
    + + params->get('show_pagination_limit')) : ?> +
    + + pagination->getLimitBox(); ?> +
    + +
    + +
      + items as $item) : ?> +
    • + params->get('show_articles')) : ?> + + numarticles); ?> + + + + + + published == 0) : ?> + + + + +
      + params->get('show_link')) : ?> + link); ?> + + + + + +
      + +
    • + +
    + + items)) : ?> + params->def('show_pagination', 2) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> +
    + params->def('show_pagination_results', 1)) : ?> +

    + pagination->getPagesCounter(); ?> +

    + + pagination->getPagesLinks(); ?> +
    + + +
    +
    diff --git a/components/com_newsfeeds/tmpl/newsfeed/default.php b/components/com_newsfeeds/tmpl/newsfeed/default.php index 99cf1315060ea..6ee290eec2e7d 100644 --- a/components/com_newsfeeds/tmpl/newsfeed/default.php +++ b/components/com_newsfeeds/tmpl/newsfeed/default.php @@ -1,4 +1,5 @@ msg)) : ?> - msg; ?> + msg; ?> - - item->rtl; ?> - - isRtl(); ?> - - - - - - - - - - - - - - item->images); ?> -
    - params->get('display_num')) : ?> -

    - escape($this->params->get('page_heading')); ?> -

    - -

    - item->published == 0) : ?> - - - - item->name); ?> - -

    + + item->rtl; ?> + + isRtl(); ?> + + + + + + + + + + + + + + item->images); ?> +
    + params->get('display_num')) : ?> +

    + escape($this->params->get('page_heading')); ?> +

    + +

    + item->published == 0) : ?> + + + + item->name); ?> + +

    - params->get('show_tags', 1)) : ?> - item->tagLayout = new FileLayout('joomla.content.tags'); ?> - item->tagLayout->render($this->item->tags->itemTags); ?> - + params->get('show_tags', 1)) : ?> + item->tagLayout = new FileLayout('joomla.content.tags'); ?> + item->tagLayout->render($this->item->tags->itemTags); ?> + - - image_first) && !empty($images->image_first)) : ?> - float_first) ? $this->params->get('float_first') : $images->float_first; ?> -
    -
    - $images->image_first, - 'alt' => empty($images->image_first_alt) && empty($images->image_first_alt_empty) ? false : $images->image_first_alt, - ] - ); ?> - image_first_caption) : ?> -
    escape($images->image_first_caption); ?>
    - -
    -
    - + + image_first) && !empty($images->image_first)) : ?> + float_first) ? $this->params->get('float_first') : $images->float_first; ?> +
    +
    + $images->image_first, + 'alt' => empty($images->image_first_alt) && empty($images->image_first_alt_empty) ? false : $images->image_first_alt, + ] + ); ?> + image_first_caption) : ?> +
    escape($images->image_first_caption); ?>
    + +
    +
    + - image_second) and !empty($images->image_second)) : ?> - float_second) ? $this->params->get('float_second') : $images->float_second; ?> -
    -
    - $images->image_second, - 'alt' => empty($images->image_second_alt) && empty($images->image_second_alt_empty) ? false : $images->image_second_alt, - ] - ); ?> - image_second_caption) : ?> -
    escape($images->image_second_caption); ?>
    - -
    -
    - - - item->description; ?> - + image_second) and !empty($images->image_second)) : ?> + float_second) ? $this->params->get('float_second') : $images->float_second; ?> +
    +
    + $images->image_second, + 'alt' => empty($images->image_second_alt) && empty($images->image_second_alt_empty) ? false : $images->image_second_alt, + ] + ); ?> + image_second_caption) : ?> +
    escape($images->image_second_caption); ?>
    + +
    +
    + + + item->description; ?> + - params->get('show_feed_description')) : ?> -
    - rssDoc->description); ?> -
    - + params->get('show_feed_description')) : ?> +
    + rssDoc->description); ?> +
    + - - rssDoc->image && $this->params->get('show_feed_image')) : ?> -
    - $this->rssDoc->image->uri, - 'alt' => $this->rssDoc->image->title, - ] - ); ?> -
    - + + rssDoc->image && $this->params->get('show_feed_image')) : ?> +
    + $this->rssDoc->image->uri, + 'alt' => $this->rssDoc->image->title, + ] + ); ?> +
    + - - rssDoc[0])) : ?> -
      - item->numarticles; $i++) : ?> - rssDoc[$i])) : ?> - - - rssDoc[$i]->uri || !$this->rssDoc[$i]->isPermaLink ? trim($this->rssDoc[$i]->uri) : trim($this->rssDoc[$i]->guid); ?> - item->link : $uri; ?> - rssDoc[$i]->content !== '' ? trim($this->rssDoc[$i]->content) : ''; ?> -
    1. - - - - - + + rssDoc[0])) : ?> +
        + item->numarticles; $i++) : ?> + rssDoc[$i])) : ?> + + + rssDoc[$i]->uri || !$this->rssDoc[$i]->isPermaLink ? trim($this->rssDoc[$i]->uri) : trim($this->rssDoc[$i]->guid); ?> + item->link : $uri; ?> + rssDoc[$i]->content !== '' ? trim($this->rssDoc[$i]->content) : ''; ?> +
      1. + + + + + - params->get('show_item_description') && $text !== '') : ?> -
        - params->get('show_feed_image', 0) == 0) : ?> - - - params->get('feed_character_count')); ?> - -
        - -
      2. - -
      - -
    + params->get('show_item_description') && $text !== '') : ?> +
    + params->get('show_feed_image', 0) == 0) : ?> + + + params->get('feed_character_count')); ?> + +
    + + + + + +
    diff --git a/components/com_privacy/src/Controller/DisplayController.php b/components/com_privacy/src/Controller/DisplayController.php index a428c60f856c7..77221fdb9b1cf 100644 --- a/components/com_privacy/src/Controller/DisplayController.php +++ b/components/com_privacy/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', $this->default_view); - - // Submitting information requests and confirmation through the frontend is restricted to authenticated users at this time - if (in_array($view, ['confirm', 'request']) && $this->app->getIdentity()->guest) - { - $this->setRedirect( - Route::_('index.php?option=com_users&view=login&return=' . base64_encode('index.php?option=com_privacy&view=' . $view), false) - ); - - return $this; - } - - // Set a Referrer-Policy header for views which require it - if (in_array($view, ['confirm', 'remind'])) - { - $this->app->setHeader('Referrer-Policy', 'no-referrer', true); - } - - return parent::display($cachable, $urlparams); - } + /** + * Method to display a view. + * + * @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()}. + * + * @return $this + * + * @since 3.9.0 + */ + public function display($cachable = false, $urlparams = []) + { + $view = $this->input->get('view', $this->default_view); + + // Submitting information requests and confirmation through the frontend is restricted to authenticated users at this time + if (in_array($view, ['confirm', 'request']) && $this->app->getIdentity()->guest) { + $this->setRedirect( + Route::_('index.php?option=com_users&view=login&return=' . base64_encode('index.php?option=com_privacy&view=' . $view), false) + ); + + return $this; + } + + // Set a Referrer-Policy header for views which require it + if (in_array($view, ['confirm', 'remind'])) { + $this->app->setHeader('Referrer-Policy', 'no-referrer', true); + } + + return parent::display($cachable, $urlparams); + } } diff --git a/components/com_privacy/src/Controller/RequestController.php b/components/com_privacy/src/Controller/RequestController.php index 8147528aa6b66..2dfdc6dab0675 100644 --- a/components/com_privacy/src/Controller/RequestController.php +++ b/components/com_privacy/src/Controller/RequestController.php @@ -1,4 +1,5 @@ checkToken('post'); - - /** @var ConfirmModel $model */ - $model = $this->getModel('Confirm', 'Site'); - $data = $this->input->post->get('jform', [], 'array'); - - $return = $model->confirmRequest($data); - - // Check for a hard error. - if ($return instanceof \Exception) - { - // Get the error message to display. - if (Factory::getApplication()->get('error_reporting')) - { - $message = $return->getMessage(); - } - else - { - $message = Text::_('COM_PRIVACY_ERROR_CONFIRMING_REQUEST'); - } - - // Go back to the confirm form. - $this->setRedirect(Route::_('index.php?option=com_privacy&view=confirm', false), $message, 'error'); - - return false; - } - elseif ($return === false) - { - // Confirm failed. - // Go back to the confirm form. - $message = Text::sprintf('COM_PRIVACY_ERROR_CONFIRMING_REQUEST_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_privacy&view=confirm', false), $message, 'notice'); - - return false; - } - else - { - // Confirm succeeded. - $this->setRedirect(Route::_(Uri::root()), Text::_('COM_PRIVACY_CONFIRM_REQUEST_SUCCEEDED'), 'info'); - - return true; - } - } - - /** - * Method to submit an information request. - * - * @return boolean - * - * @since 3.9.0 - */ - public function submit() - { - // Check the request token. - $this->checkToken('post'); - - /** @var RequestModel $model */ - $model = $this->getModel('Request', 'Site'); - $data = $this->input->post->get('jform', [], 'array'); - - $return = $model->createRequest($data); - - // Check for a hard error. - if ($return instanceof \Exception) - { - // Get the error message to display. - if (Factory::getApplication()->get('error_reporting')) - { - $message = $return->getMessage(); - } - else - { - $message = Text::_('COM_PRIVACY_ERROR_CREATING_REQUEST'); - } - - // Go back to the confirm form. - $this->setRedirect(Route::_('index.php?option=com_privacy&view=request', false), $message, 'error'); - - return false; - } - elseif ($return === false) - { - // Confirm failed. - // Go back to the confirm form. - $message = Text::sprintf('COM_PRIVACY_ERROR_CREATING_REQUEST_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_privacy&view=request', false), $message, 'notice'); - - return false; - } - else - { - // Confirm succeeded. - $this->setRedirect(Route::_(Uri::root()), Text::_('COM_PRIVACY_CREATE_REQUEST_SUCCEEDED'), 'info'); - - return true; - } - } - - /** - * Method to extend the privacy consent. - * - * @return boolean - * - * @since 3.9.0 - */ - public function remind() - { - // Check the request token. - $this->checkToken('post'); - - /** @var ConfirmModel $model */ - $model = $this->getModel('Remind', 'Site'); - $data = $this->input->post->get('jform', [], 'array'); - - $return = $model->remindRequest($data); - - // Check for a hard error. - if ($return instanceof \Exception) - { - // Get the error message to display. - if (Factory::getApplication()->get('error_reporting')) - { - $message = $return->getMessage(); - } - else - { - $message = Text::_('COM_PRIVACY_ERROR_REMIND_REQUEST'); - } - - // Go back to the confirm form. - $this->setRedirect(Route::_('index.php?option=com_privacy&view=remind', false), $message, 'error'); - - return false; - } - elseif ($return === false) - { - // Confirm failed. - // Go back to the confirm form. - $message = Text::sprintf('COM_PRIVACY_ERROR_CONFIRMING_REMIND_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_privacy&view=remind', false), $message, 'notice'); - - return false; - } - else - { - // Confirm succeeded. - $this->setRedirect(Route::_(Uri::root()), Text::_('COM_PRIVACY_CONFIRM_REMIND_SUCCEEDED'), 'info'); - - return true; - } - } + /** + * Method to confirm the information request. + * + * @return boolean + * + * @since 3.9.0 + */ + public function confirm() + { + // Check the request token. + $this->checkToken('post'); + + /** @var ConfirmModel $model */ + $model = $this->getModel('Confirm', 'Site'); + $data = $this->input->post->get('jform', [], 'array'); + + $return = $model->confirmRequest($data); + + // Check for a hard error. + if ($return instanceof \Exception) { + // Get the error message to display. + if (Factory::getApplication()->get('error_reporting')) { + $message = $return->getMessage(); + } else { + $message = Text::_('COM_PRIVACY_ERROR_CONFIRMING_REQUEST'); + } + + // Go back to the confirm form. + $this->setRedirect(Route::_('index.php?option=com_privacy&view=confirm', false), $message, 'error'); + + return false; + } elseif ($return === false) { + // Confirm failed. + // Go back to the confirm form. + $message = Text::sprintf('COM_PRIVACY_ERROR_CONFIRMING_REQUEST_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_privacy&view=confirm', false), $message, 'notice'); + + return false; + } else { + // Confirm succeeded. + $this->setRedirect(Route::_(Uri::root()), Text::_('COM_PRIVACY_CONFIRM_REQUEST_SUCCEEDED'), 'info'); + + return true; + } + } + + /** + * Method to submit an information request. + * + * @return boolean + * + * @since 3.9.0 + */ + public function submit() + { + // Check the request token. + $this->checkToken('post'); + + /** @var RequestModel $model */ + $model = $this->getModel('Request', 'Site'); + $data = $this->input->post->get('jform', [], 'array'); + + $return = $model->createRequest($data); + + // Check for a hard error. + if ($return instanceof \Exception) { + // Get the error message to display. + if (Factory::getApplication()->get('error_reporting')) { + $message = $return->getMessage(); + } else { + $message = Text::_('COM_PRIVACY_ERROR_CREATING_REQUEST'); + } + + // Go back to the confirm form. + $this->setRedirect(Route::_('index.php?option=com_privacy&view=request', false), $message, 'error'); + + return false; + } elseif ($return === false) { + // Confirm failed. + // Go back to the confirm form. + $message = Text::sprintf('COM_PRIVACY_ERROR_CREATING_REQUEST_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_privacy&view=request', false), $message, 'notice'); + + return false; + } else { + // Confirm succeeded. + $this->setRedirect(Route::_(Uri::root()), Text::_('COM_PRIVACY_CREATE_REQUEST_SUCCEEDED'), 'info'); + + return true; + } + } + + /** + * Method to extend the privacy consent. + * + * @return boolean + * + * @since 3.9.0 + */ + public function remind() + { + // Check the request token. + $this->checkToken('post'); + + /** @var ConfirmModel $model */ + $model = $this->getModel('Remind', 'Site'); + $data = $this->input->post->get('jform', [], 'array'); + + $return = $model->remindRequest($data); + + // Check for a hard error. + if ($return instanceof \Exception) { + // Get the error message to display. + if (Factory::getApplication()->get('error_reporting')) { + $message = $return->getMessage(); + } else { + $message = Text::_('COM_PRIVACY_ERROR_REMIND_REQUEST'); + } + + // Go back to the confirm form. + $this->setRedirect(Route::_('index.php?option=com_privacy&view=remind', false), $message, 'error'); + + return false; + } elseif ($return === false) { + // Confirm failed. + // Go back to the confirm form. + $message = Text::sprintf('COM_PRIVACY_ERROR_CONFIRMING_REMIND_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_privacy&view=remind', false), $message, 'notice'); + + return false; + } else { + // Confirm succeeded. + $this->setRedirect(Route::_(Uri::root()), Text::_('COM_PRIVACY_CONFIRM_REMIND_SUCCEEDED'), 'info'); + + return true; + } + } } diff --git a/components/com_privacy/src/Model/ConfirmModel.php b/components/com_privacy/src/Model/ConfirmModel.php index d47cb8fc1a03b..562eb8085f53f 100644 --- a/components/com_privacy/src/Model/ConfirmModel.php +++ b/components/com_privacy/src/Model/ConfirmModel.php @@ -1,4 +1,5 @@ getForm(); - - // Check for an error. - if ($form instanceof \Exception) - { - return $form; - } - - // Filter and validate the form data. - $data = $form->filter($data); - $return = $form->validate($data); - - // Check for an error. - if ($return instanceof \Exception) - { - return $return; - } - - // Check the validation results. - if ($return === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $formError) - { - $this->setError($formError->getMessage()); - } - - return false; - } - - // Get the user email address - $email = Factory::getUser()->email; - - // Search for the information request - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load(['email' => $email, 'status' => 0])) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REQUESTS')); - - return false; - } - - // A request can only be confirmed if it is in a pending status and has a confirmation token - if ($table->status != '0' || !$table->confirm_token || $table->confirm_token_created_at === null) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REQUESTS')); - - return false; - } - - // A request can only be confirmed if the token is less than 24 hours old - $confirmTokenCreatedAt = new Date($table->confirm_token_created_at); - $confirmTokenCreatedAt->add(new \DateInterval('P1D')); - - $now = new Date('now'); - - if ($now > $confirmTokenCreatedAt) - { - // Invalidate the request - $table->status = -1; - $table->confirm_token = ''; - $table->confirm_token_created_at = null; - - try - { - $table->store(); - } - catch (ExecutionFailureException $exception) - { - // The error will be logged in the database API, we just need to catch it here to not let things fatal out - } - - $this->setError(Text::_('COM_PRIVACY_ERROR_CONFIRM_TOKEN_EXPIRED')); - - return false; - } - - // Verify the token - if (!UserHelper::verifyPassword($data['confirm_token'], $table->confirm_token)) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REQUESTS')); - - return false; - } - - // Everything is good to go, transition the request to confirmed - $saved = $this->save( - [ - 'id' => $table->id, - 'status' => 1, - 'confirm_token' => '', - ] - ); - - if (!$saved) - { - // Error was set by the save method - return false; - } - - // Push a notification to the site's super users, deliberately ignoring if this process fails so the below message goes out - /** @var MessageModel $messageModel */ - $messageModel = Factory::getApplication()->bootComponent('com_messages')->getMVCFactory()->createModel('Message', 'Administrator'); - - $messageModel->notifySuperUsers( - Text::_('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CONFIRMED_REQUEST_SUBJECT'), - Text::sprintf('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CONFIRMED_REQUEST_MESSAGE', $table->email) - ); - - $message = [ - 'action' => 'request-confirmed', - 'subjectemail' => $table->email, - 'id' => $table->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_CONFIRMED_REQUEST', 'com_privacy.request'); - - return true; - } - - /** - * Method for getting the form from the model. - * - * @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 Form|boolean A Form object on success, false on failure - * - * @since 3.9.0 - */ - public function getForm($data = [], $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_privacy.confirm', 'confirm', ['control' => 'jform']); - - if (empty($form)) - { - return false; - } - - $input = Factory::getApplication()->input; - - if ($input->getMethod() === 'GET') - { - $form->setValue('confirm_token', '', $input->get->getAlnum('confirm_token')); - } - - return $form; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @since 3.9.0 - * @throws \Exception - */ - public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState() - { - // Get the application object. - $params = Factory::getApplication()->getParams('com_privacy'); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Method to fetch an instance of the action log model. - * - * @return ActionlogModel - * - * @since 4.0.0 - */ - private function getActionlogModel(): ActionlogModel - { - return Factory::getApplication()->bootComponent('com_actionlogs') - ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); - } + /** + * Confirms the information request. + * + * @param array $data The data expected for the form. + * + * @return mixed Exception | boolean + * + * @since 3.9.0 + */ + public function confirmRequest($data) + { + // Get the form. + $form = $this->getForm(); + + // Check for an error. + if ($form instanceof \Exception) { + return $form; + } + + // Filter and validate the form data. + $data = $form->filter($data); + $return = $form->validate($data); + + // Check for an error. + if ($return instanceof \Exception) { + return $return; + } + + // Check the validation results. + if ($return === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $formError) { + $this->setError($formError->getMessage()); + } + + return false; + } + + // Get the user email address + $email = Factory::getUser()->email; + + // Search for the information request + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load(['email' => $email, 'status' => 0])) { + $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REQUESTS')); + + return false; + } + + // A request can only be confirmed if it is in a pending status and has a confirmation token + if ($table->status != '0' || !$table->confirm_token || $table->confirm_token_created_at === null) { + $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REQUESTS')); + + return false; + } + + // A request can only be confirmed if the token is less than 24 hours old + $confirmTokenCreatedAt = new Date($table->confirm_token_created_at); + $confirmTokenCreatedAt->add(new \DateInterval('P1D')); + + $now = new Date('now'); + + if ($now > $confirmTokenCreatedAt) { + // Invalidate the request + $table->status = -1; + $table->confirm_token = ''; + $table->confirm_token_created_at = null; + + try { + $table->store(); + } catch (ExecutionFailureException $exception) { + // The error will be logged in the database API, we just need to catch it here to not let things fatal out + } + + $this->setError(Text::_('COM_PRIVACY_ERROR_CONFIRM_TOKEN_EXPIRED')); + + return false; + } + + // Verify the token + if (!UserHelper::verifyPassword($data['confirm_token'], $table->confirm_token)) { + $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REQUESTS')); + + return false; + } + + // Everything is good to go, transition the request to confirmed + $saved = $this->save( + [ + 'id' => $table->id, + 'status' => 1, + 'confirm_token' => '', + ] + ); + + if (!$saved) { + // Error was set by the save method + return false; + } + + // Push a notification to the site's super users, deliberately ignoring if this process fails so the below message goes out + /** @var MessageModel $messageModel */ + $messageModel = Factory::getApplication()->bootComponent('com_messages')->getMVCFactory()->createModel('Message', 'Administrator'); + + $messageModel->notifySuperUsers( + Text::_('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CONFIRMED_REQUEST_SUBJECT'), + Text::sprintf('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CONFIRMED_REQUEST_MESSAGE', $table->email) + ); + + $message = [ + 'action' => 'request-confirmed', + 'subjectemail' => $table->email, + 'id' => $table->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_CONFIRMED_REQUEST', 'com_privacy.request'); + + return true; + } + + /** + * Method for getting the form from the model. + * + * @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 Form|boolean A Form object on success, false on failure + * + * @since 3.9.0 + */ + public function getForm($data = [], $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_privacy.confirm', 'confirm', ['control' => 'jform']); + + if (empty($form)) { + return false; + } + + $input = Factory::getApplication()->input; + + if ($input->getMethod() === 'GET') { + $form->setValue('confirm_token', '', $input->get->getAlnum('confirm_token')); + } + + return $form; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @since 3.9.0 + * @throws \Exception + */ + public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState() + { + // Get the application object. + $params = Factory::getApplication()->getParams('com_privacy'); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Method to fetch an instance of the action log model. + * + * @return ActionlogModel + * + * @since 4.0.0 + */ + private function getActionlogModel(): ActionlogModel + { + return Factory::getApplication()->bootComponent('com_actionlogs') + ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); + } } diff --git a/components/com_privacy/src/Model/RemindModel.php b/components/com_privacy/src/Model/RemindModel.php index 68b88eee6f29a..1123161d8a46b 100644 --- a/components/com_privacy/src/Model/RemindModel.php +++ b/components/com_privacy/src/Model/RemindModel.php @@ -1,4 +1,5 @@ getForm(); - $data['email'] = PunycodeHelper::emailToPunycode($data['email']); - - // Check for an error. - if ($form instanceof \Exception) - { - return $form; - } - - // Filter and validate the form data. - $data = $form->filter($data); - $return = $form->validate($data); - - // Check for an error. - if ($return instanceof \Exception) - { - return $return; - } - - // Check the validation results. - if ($return === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $formError) - { - $this->setError($formError->getMessage()); - } - - return false; - } - - /** @var ConsentTable $table */ - $table = $this->getTable(); - - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName(['r.id', 'r.user_id', 'r.token'])); - $query->from($db->quoteName('#__privacy_consents', 'r')); - $query->join('LEFT', $db->quoteName('#__users', 'u'), - $db->quoteName('u.id') . ' = ' . $db->quoteName('r.user_id') - ); - $query->where($db->quoteName('u.email') . ' = :email') - ->bind(':email', $data['email']); - $query->where($db->quoteName('r.remind') . ' = 1'); - $db->setQuery($query); - - try - { - $remind = $db->loadObject(); - } - catch (ExecutionFailureException $e) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REMIND')); - - return false; - } - - if (!$remind) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REMIND')); - - return false; - } - - // Verify the token - if (!UserHelper::verifyPassword($data['remind_token'], $remind->token)) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_NO_REMIND_REQUESTS')); - - return false; - } - - // Everything is good to go, transition the request to extended - $saved = $this->save( - [ - 'id' => $remind->id, - 'remind' => 0, - 'token' => '', - 'created' => Factory::getDate()->toSql(), - ] - ); - - if (!$saved) - { - // Error was set by the save method - return false; - } - - return true; - } - - /** - * Method for getting the form from the model. - * - * @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 Form|boolean A Form object on success, false on failure - * - * @since 3.9.0 - */ - public function getForm($data = [], $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_privacy.remind', 'remind', ['control' => 'jform']); - - if (empty($form)) - { - return false; - } - - $input = Factory::getApplication()->input; - - if ($input->getMethod() === 'GET') - { - $form->setValue('remind_token', '', $input->get->getAlnum('remind_token')); - } - - return $form; - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @throws \Exception - * @since 3.9.0 - */ - public function getTable($name = 'Consent', $prefix = 'Administrator', $options = []) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState() - { - // Get the application object. - $params = Factory::getApplication()->getParams('com_privacy'); - - // Load the parameters. - $this->setState('params', $params); - } + /** + * Confirms the remind request. + * + * @param array $data The data expected for the form. + * + * @return mixed \Exception | JException | boolean + * + * @since 3.9.0 + */ + public function remindRequest($data) + { + // Get the form. + $form = $this->getForm(); + $data['email'] = PunycodeHelper::emailToPunycode($data['email']); + + // Check for an error. + if ($form instanceof \Exception) { + return $form; + } + + // Filter and validate the form data. + $data = $form->filter($data); + $return = $form->validate($data); + + // Check for an error. + if ($return instanceof \Exception) { + return $return; + } + + // Check the validation results. + if ($return === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $formError) { + $this->setError($formError->getMessage()); + } + + return false; + } + + /** @var ConsentTable $table */ + $table = $this->getTable(); + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['r.id', 'r.user_id', 'r.token'])); + $query->from($db->quoteName('#__privacy_consents', 'r')); + $query->join( + 'LEFT', + $db->quoteName('#__users', 'u'), + $db->quoteName('u.id') . ' = ' . $db->quoteName('r.user_id') + ); + $query->where($db->quoteName('u.email') . ' = :email') + ->bind(':email', $data['email']); + $query->where($db->quoteName('r.remind') . ' = 1'); + $db->setQuery($query); + + try { + $remind = $db->loadObject(); + } catch (ExecutionFailureException $e) { + $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REMIND')); + + return false; + } + + if (!$remind) { + $this->setError(Text::_('COM_PRIVACY_ERROR_NO_PENDING_REMIND')); + + return false; + } + + // Verify the token + if (!UserHelper::verifyPassword($data['remind_token'], $remind->token)) { + $this->setError(Text::_('COM_PRIVACY_ERROR_NO_REMIND_REQUESTS')); + + return false; + } + + // Everything is good to go, transition the request to extended + $saved = $this->save( + [ + 'id' => $remind->id, + 'remind' => 0, + 'token' => '', + 'created' => Factory::getDate()->toSql(), + ] + ); + + if (!$saved) { + // Error was set by the save method + return false; + } + + return true; + } + + /** + * Method for getting the form from the model. + * + * @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 Form|boolean A Form object on success, false on failure + * + * @since 3.9.0 + */ + public function getForm($data = [], $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_privacy.remind', 'remind', ['control' => 'jform']); + + if (empty($form)) { + return false; + } + + $input = Factory::getApplication()->input; + + if ($input->getMethod() === 'GET') { + $form->setValue('remind_token', '', $input->get->getAlnum('remind_token')); + } + + return $form; + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @throws \Exception + * @since 3.9.0 + */ + public function getTable($name = 'Consent', $prefix = 'Administrator', $options = []) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState() + { + // Get the application object. + $params = Factory::getApplication()->getParams('com_privacy'); + + // Load the parameters. + $this->setState('params', $params); + } } diff --git a/components/com_privacy/src/Model/RequestModel.php b/components/com_privacy/src/Model/RequestModel.php index cebcccca630a8..176827ea455e2 100644 --- a/components/com_privacy/src/Model/RequestModel.php +++ b/components/com_privacy/src/Model/RequestModel.php @@ -1,4 +1,5 @@ get('mailonline', 1)) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_CREATE_REQUEST_WHEN_SENDMAIL_DISABLED')); - - return false; - } - - // Get the form. - $form = $this->getForm(); - - // Check for an error. - if ($form instanceof \Exception) - { - return $form; - } - - // Filter and validate the form data. - $data = $form->filter($data); - $return = $form->validate($data); - - // Check for an error. - if ($return instanceof \Exception) - { - return $return; - } - - // Check the validation results. - if ($return === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $formError) - { - $this->setError($formError->getMessage()); - } - - return false; - } - - $data['email'] = Factory::getUser()->email; - - // Search for an open information request matching the email and type - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('COUNT(id)') - ->from($db->quoteName('#__privacy_requests')) - ->where($db->quoteName('email') . ' = :email') - ->where($db->quoteName('request_type') . ' = :requesttype') - ->whereIn($db->quoteName('status'), [0, 1]) - ->bind(':email', $data['email']) - ->bind(':requesttype', $data['request_type']); - - try - { - $result = (int) $db->setQuery($query)->loadResult(); - } - catch (ExecutionFailureException $exception) - { - // Can't check for existing requests, so don't create a new one - $this->setError(Text::_('COM_PRIVACY_ERROR_CHECKING_FOR_EXISTING_REQUESTS')); - - return false; - } - - if ($result > 0) - { - $this->setError(Text::_('COM_PRIVACY_ERROR_PENDING_REQUEST_OPEN')); - - return false; - } - - // Everything is good to go, create the request - $token = ApplicationHelper::getHash(UserHelper::genRandomPassword()); - $hashedToken = UserHelper::hashPassword($token); - - $data['confirm_token'] = $hashedToken; - $data['confirm_token_created_at'] = Factory::getDate()->toSql(); - - if (!$this->save($data)) - { - // The save function will set the error message, so just return here - return false; - } - - // Push a notification to the site's super users, deliberately ignoring if this process fails so the below message goes out - /** @var MessageModel $messageModel */ - $messageModel = $app->bootComponent('com_messages')->getMVCFactory()->createModel('Message', 'Administrator'); - - $messageModel->notifySuperUsers( - Text::_('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CREATED_REQUEST_SUBJECT'), - Text::sprintf('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CREATED_REQUEST_MESSAGE', $data['email']) - ); - - // The mailer can be set to either throw Exceptions or return boolean false, account for both - try - { - $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; - - $templateData = [ - 'sitename' => $app->get('sitename'), - 'url' => Uri::root(), - 'tokenurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm&confirm_token=' . $token, false, $linkMode, true), - 'formurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm', false, $linkMode, true), - 'token' => $token, - ]; - - switch ($data['request_type']) - { - case 'export': - $mailer = new MailTemplate('com_privacy.notification.export', $app->getLanguage()->getTag()); - - break; - - case 'remove': - $mailer = new MailTemplate('com_privacy.notification.remove', $app->getLanguage()->getTag()); - - break; - - default: - $this->setError(Text::_('COM_PRIVACY_ERROR_UNKNOWN_REQUEST_TYPE')); - - return false; - } - - $mailer->addTemplateData($templateData); - $mailer->addRecipient($data['email']); - - $mailer->send(); - - /** @var RequestTable $table */ - $table = $this->getTable(); - - if (!$table->load($this->getState($this->getName() . '.id'))) - { - $this->setError($table->getError()); - - return false; - } - - // Log the request's creation - $message = [ - 'action' => 'request-created', - 'requesttype' => $table->request_type, - 'subjectemail' => $table->email, - 'id' => $table->id, - 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, - ]; - - $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_CREATED_REQUEST', 'com_privacy.request'); - - // The email sent and the record is saved, everything is good to go from here - return true; - } - catch (MailDisabledException | phpmailerException $exception) - { - $this->setError($exception->getMessage()); - - return false; - } - } - - /** - * Method for getting the form from the model. - * - * @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 Form|boolean A Form object on success, false on failure - * - * @since 3.9.0 - */ - public function getForm($data = [], $loadData = true) - { - return $this->loadForm('com_privacy.request', 'request', ['control' => 'jform']); - } - - /** - * Method to get a table object, load it if necessary. - * - * @param string $name The table name. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $options Configuration array for model. Optional. - * - * @return Table A Table object - * - * @throws \Exception - * @since 3.9.0 - */ - public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) - { - return parent::getTable($name, $prefix, $options); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 3.9.0 - */ - protected function populateState() - { - // Get the application object. - $params = Factory::getApplication()->getParams('com_privacy'); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Method to fetch an instance of the action log model. - * - * @return ActionlogModel - * - * @since 4.0.0 - */ - private function getActionlogModel(): ActionlogModel - { - return Factory::getApplication()->bootComponent('com_actionlogs') - ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); - } + /** + * Creates an information request. + * + * @param array $data The data expected for the form. + * + * @return mixed Exception | boolean + * + * @since 3.9.0 + */ + public function createRequest($data) + { + $app = Factory::getApplication(); + + // Creating requests requires the site's email sending be enabled + if (!$app->get('mailonline', 1)) { + $this->setError(Text::_('COM_PRIVACY_ERROR_CANNOT_CREATE_REQUEST_WHEN_SENDMAIL_DISABLED')); + + return false; + } + + // Get the form. + $form = $this->getForm(); + + // Check for an error. + if ($form instanceof \Exception) { + return $form; + } + + // Filter and validate the form data. + $data = $form->filter($data); + $return = $form->validate($data); + + // Check for an error. + if ($return instanceof \Exception) { + return $return; + } + + // Check the validation results. + if ($return === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $formError) { + $this->setError($formError->getMessage()); + } + + return false; + } + + $data['email'] = Factory::getUser()->email; + + // Search for an open information request matching the email and type + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('COUNT(id)') + ->from($db->quoteName('#__privacy_requests')) + ->where($db->quoteName('email') . ' = :email') + ->where($db->quoteName('request_type') . ' = :requesttype') + ->whereIn($db->quoteName('status'), [0, 1]) + ->bind(':email', $data['email']) + ->bind(':requesttype', $data['request_type']); + + try { + $result = (int) $db->setQuery($query)->loadResult(); + } catch (ExecutionFailureException $exception) { + // Can't check for existing requests, so don't create a new one + $this->setError(Text::_('COM_PRIVACY_ERROR_CHECKING_FOR_EXISTING_REQUESTS')); + + return false; + } + + if ($result > 0) { + $this->setError(Text::_('COM_PRIVACY_ERROR_PENDING_REQUEST_OPEN')); + + return false; + } + + // Everything is good to go, create the request + $token = ApplicationHelper::getHash(UserHelper::genRandomPassword()); + $hashedToken = UserHelper::hashPassword($token); + + $data['confirm_token'] = $hashedToken; + $data['confirm_token_created_at'] = Factory::getDate()->toSql(); + + if (!$this->save($data)) { + // The save function will set the error message, so just return here + return false; + } + + // Push a notification to the site's super users, deliberately ignoring if this process fails so the below message goes out + /** @var MessageModel $messageModel */ + $messageModel = $app->bootComponent('com_messages')->getMVCFactory()->createModel('Message', 'Administrator'); + + $messageModel->notifySuperUsers( + Text::_('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CREATED_REQUEST_SUBJECT'), + Text::sprintf('COM_PRIVACY_ADMIN_NOTIFICATION_USER_CREATED_REQUEST_MESSAGE', $data['email']) + ); + + // The mailer can be set to either throw Exceptions or return boolean false, account for both + try { + $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; + + $templateData = [ + 'sitename' => $app->get('sitename'), + 'url' => Uri::root(), + 'tokenurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm&confirm_token=' . $token, false, $linkMode, true), + 'formurl' => Route::link('site', 'index.php?option=com_privacy&view=confirm', false, $linkMode, true), + 'token' => $token, + ]; + + switch ($data['request_type']) { + case 'export': + $mailer = new MailTemplate('com_privacy.notification.export', $app->getLanguage()->getTag()); + + break; + + case 'remove': + $mailer = new MailTemplate('com_privacy.notification.remove', $app->getLanguage()->getTag()); + + break; + + default: + $this->setError(Text::_('COM_PRIVACY_ERROR_UNKNOWN_REQUEST_TYPE')); + + return false; + } + + $mailer->addTemplateData($templateData); + $mailer->addRecipient($data['email']); + + $mailer->send(); + + /** @var RequestTable $table */ + $table = $this->getTable(); + + if (!$table->load($this->getState($this->getName() . '.id'))) { + $this->setError($table->getError()); + + return false; + } + + // Log the request's creation + $message = [ + 'action' => 'request-created', + 'requesttype' => $table->request_type, + 'subjectemail' => $table->email, + 'id' => $table->id, + 'itemlink' => 'index.php?option=com_privacy&view=request&id=' . $table->id, + ]; + + $this->getActionlogModel()->addLog([$message], 'COM_PRIVACY_ACTION_LOG_CREATED_REQUEST', 'com_privacy.request'); + + // The email sent and the record is saved, everything is good to go from here + return true; + } catch (MailDisabledException | phpmailerException $exception) { + $this->setError($exception->getMessage()); + + return false; + } + } + + /** + * Method for getting the form from the model. + * + * @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 Form|boolean A Form object on success, false on failure + * + * @since 3.9.0 + */ + public function getForm($data = [], $loadData = true) + { + return $this->loadForm('com_privacy.request', 'request', ['control' => 'jform']); + } + + /** + * Method to get a table object, load it if necessary. + * + * @param string $name The table name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $options Configuration array for model. Optional. + * + * @return Table A Table object + * + * @throws \Exception + * @since 3.9.0 + */ + public function getTable($name = 'Request', $prefix = 'Administrator', $options = []) + { + return parent::getTable($name, $prefix, $options); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 3.9.0 + */ + protected function populateState() + { + // Get the application object. + $params = Factory::getApplication()->getParams('com_privacy'); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Method to fetch an instance of the action log model. + * + * @return ActionlogModel + * + * @since 4.0.0 + */ + private function getActionlogModel(): ActionlogModel + { + return Factory::getApplication()->bootComponent('com_actionlogs') + ->getMVCFactory()->createModel('Actionlog', 'Administrator', ['ignore_request' => true]); + } } diff --git a/components/com_privacy/src/Service/Router.php b/components/com_privacy/src/Service/Router.php index 6b6a7ddbfbb5c..cce81693be7d1 100644 --- a/components/com_privacy/src/Service/Router.php +++ b/components/com_privacy/src/Service/Router.php @@ -1,4 +1,5 @@ registerView(new RouterViewConfiguration('confirm')); - $this->registerView(new RouterViewConfiguration('request')); - $this->registerView(new RouterViewConfiguration('remind')); + /** + * Privacy Component router constructor + * + * @param CMSApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + * + * @since 3.9.0 + */ + public function __construct($app = null, $menu = null) + { + $this->registerView(new RouterViewConfiguration('confirm')); + $this->registerView(new RouterViewConfiguration('request')); + $this->registerView(new RouterViewConfiguration('remind')); - parent::__construct($app, $menu); + parent::__construct($app, $menu); - $this->attachRule(new MenuRules($this)); - $this->attachRule(new StandardRules($this)); - $this->attachRule(new NomenuRules($this)); - } + $this->attachRule(new MenuRules($this)); + $this->attachRule(new StandardRules($this)); + $this->attachRule(new NomenuRules($this)); + } } diff --git a/components/com_privacy/src/View/Confirm/HtmlView.php b/components/com_privacy/src/View/Confirm/HtmlView.php index 1d3fc2fef02df..09c9eb00c981d 100644 --- a/components/com_privacy/src/View/Confirm/HtmlView.php +++ b/components/com_privacy/src/View/Confirm/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->state = $this->get('State'); - $this->params = $this->state->params; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 3.9.0 - */ - protected function prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_PRIVACY_VIEW_CONFIRM_PAGE_TITLE')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The form object + * + * @var Form + * @since 3.9.0 + */ + protected $form; + + /** + * The CSS class suffix to append to the view container + * + * @var string + * @since 3.9.0 + */ + protected $pageclass_sfx; + + /** + * The view parameters + * + * @var Registry + * @since 3.9.0 + */ + protected $params; + + /** + * The state information + * + * @var CMSObject + * @since 3.9.0 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see BaseHtmlView::loadTemplate() + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + // Initialise variables. + $this->form = $this->get('Form'); + $this->state = $this->get('State'); + $this->params = $this->state->params; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 3.9.0 + */ + protected function prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_PRIVACY_VIEW_CONFIRM_PAGE_TITLE')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_privacy/src/View/Remind/HtmlView.php b/components/com_privacy/src/View/Remind/HtmlView.php index 2664844f35ac2..7b2bed9579bf6 100644 --- a/components/com_privacy/src/View/Remind/HtmlView.php +++ b/components/com_privacy/src/View/Remind/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->state = $this->get('State'); - $this->params = $this->state->params; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 3.9.0 - */ - protected function prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_PRIVACY_VIEW_REMIND_PAGE_TITLE')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The form object + * + * @var Form + * @since 3.9.0 + */ + protected $form; + + /** + * The CSS class suffix to append to the view container + * + * @var string + * @since 3.9.0 + */ + protected $pageclass_sfx; + + /** + * The view parameters + * + * @var Registry + * @since 3.9.0 + */ + protected $params; + + /** + * The state information + * + * @var CMSObject + * @since 3.9.0 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see BaseHtmlView::loadTemplate() + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + // Initialise variables. + $this->form = $this->get('Form'); + $this->state = $this->get('State'); + $this->params = $this->state->params; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 3.9.0 + */ + protected function prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_PRIVACY_VIEW_REMIND_PAGE_TITLE')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_privacy/src/View/Request/HtmlView.php b/components/com_privacy/src/View/Request/HtmlView.php index 23c97e2ac7bae..4a5e9c2b0f7ca 100644 --- a/components/com_privacy/src/View/Request/HtmlView.php +++ b/components/com_privacy/src/View/Request/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->state = $this->get('State'); - $this->params = $this->state->params; - $this->sendMailEnabled = (bool) Factory::getApplication()->get('mailonline', 1); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 3.9.0 - */ - protected function prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_PRIVACY_VIEW_REQUEST_PAGE_TITLE')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The form object + * + * @var Form + * @since 3.9.0 + */ + protected $form; + + /** + * The CSS class suffix to append to the view container + * + * @var string + * @since 3.9.0 + */ + protected $pageclass_sfx; + + /** + * The view parameters + * + * @var Registry + * @since 3.9.0 + */ + protected $params; + + /** + * Flag indicating the site supports sending email + * + * @var boolean + * @since 3.9.0 + */ + protected $sendMailEnabled; + + /** + * The state information + * + * @var CMSObject + * @since 3.9.0 + */ + protected $state; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @see BaseHtmlView::loadTemplate() + * @since 3.9.0 + * @throws \Exception + */ + public function display($tpl = null) + { + // Initialise variables. + $this->form = $this->get('Form'); + $this->state = $this->get('State'); + $this->params = $this->state->params; + $this->sendMailEnabled = (bool) Factory::getApplication()->get('mailonline', 1); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 3.9.0 + */ + protected function prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_PRIVACY_VIEW_REQUEST_PAGE_TITLE')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_privacy/tmpl/confirm/default.php b/components/com_privacy/tmpl/confirm/default.php index 2f80026cadb9b..7a74c0c893ac5 100644 --- a/components/com_privacy/tmpl/confirm/default.php +++ b/components/com_privacy/tmpl/confirm/default.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - -
    - form->getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - -
    -
    - -
    -
    - -
    + params->get('show_page_heading')) : ?> + + +
    + form->getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + +
    +
    + +
    +
    + +
    diff --git a/components/com_privacy/tmpl/remind/default.php b/components/com_privacy/tmpl/remind/default.php index aa311d365ca9c..6e501f6f23c86 100644 --- a/components/com_privacy/tmpl/remind/default.php +++ b/components/com_privacy/tmpl/remind/default.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - -
    - form->getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - -
    -
    - -
    -
    - -
    + params->get('show_page_heading')) : ?> + + +
    + form->getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + +
    +
    + +
    +
    + +
    diff --git a/components/com_privacy/tmpl/request/default.php b/components/com_privacy/tmpl/request/default.php index 44afcbb8d0a6d..d203a87d107de 100644 --- a/components/com_privacy/tmpl/request/default.php +++ b/components/com_privacy/tmpl/request/default.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - - sendMailEnabled) : ?> -
    - form->getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - -
    -
    - -
    -
    - -
    - -
    - - -
    - + params->get('show_page_heading')) : ?> + + + sendMailEnabled) : ?> +
    + form->getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + +
    +
    + +
    +
    + +
    + +
    + + +
    +
    diff --git a/components/com_tags/helpers/route.php b/components/com_tags/helpers/route.php index 55cb605e086d5..39436980b1110 100644 --- a/components/com_tags/helpers/route.php +++ b/components/com_tags/helpers/route.php @@ -1,13 +1,14 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt - */ -defined('_JEXEC') or die; + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + */ use Joomla\Component\Tags\Site\Helper\RouteHelper; diff --git a/components/com_tags/src/Controller/DisplayController.php b/components/com_tags/src/Controller/DisplayController.php index 562624c249b15..a1b6d7ea37cd8 100644 --- a/components/com_tags/src/Controller/DisplayController.php +++ b/components/com_tags/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ app->getIdentity(); - - // Set the default view name and format from the Request. - $vName = $this->input->get('view', 'tags'); - $this->input->set('view', $vName); - - if ($user->get('id') || ($this->input->getMethod() === 'POST' && $vName === 'tags')) - { - $cachable = false; - } - - $safeurlparams = array( - 'id' => 'ARRAY', - 'type' => 'ARRAY', - 'limit' => 'UINT', - 'limitstart' => 'UINT', - 'filter_order' => 'CMD', - 'filter_order_Dir' => 'CMD', - 'lang' => 'CMD' - ); - - return parent::display($cachable, $safeurlparams); - } + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param mixed|boolean $urlparams An array of safe URL parameters and their + * variable types, for valid values see {@link \JFilterInput::clean()}. + * + * @return static This object to support chaining. + * + * @since 3.1 + */ + public function display($cachable = false, $urlparams = false) + { + $user = $this->app->getIdentity(); + + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'tags'); + $this->input->set('view', $vName); + + if ($user->get('id') || ($this->input->getMethod() === 'POST' && $vName === 'tags')) { + $cachable = false; + } + + $safeurlparams = array( + 'id' => 'ARRAY', + 'type' => 'ARRAY', + 'limit' => 'UINT', + 'limitstart' => 'UINT', + 'filter_order' => 'CMD', + 'filter_order_Dir' => 'CMD', + 'lang' => 'CMD' + ); + + return parent::display($cachable, $safeurlparams); + } } diff --git a/components/com_tags/src/Controller/TagsController.php b/components/com_tags/src/Controller/TagsController.php index 293227bc7dbe5..39695682b3e5f 100644 --- a/components/com_tags/src/Controller/TagsController.php +++ b/components/com_tags/src/Controller/TagsController.php @@ -1,4 +1,5 @@ app->getIdentity(); - - // Receive request data - $filters = array( - 'like' => trim($this->input->get('like', null, 'string')), - 'title' => trim($this->input->get('title', null, 'string')), - 'flanguage' => $this->input->get('flanguage', null, 'word'), - 'published' => $this->input->get('published', 1, 'int'), - 'parent_id' => $this->input->get('parent_id', 0, 'int'), - 'access' => $user->getAuthorisedViewLevels(), - ); - - if ((!$user->authorise('core.edit.state', 'com_tags')) && (!$user->authorise('core.edit', 'com_tags'))) - { - // Filter on published for those who do not have edit or edit.state rights. - $filters['published'] = 1; - } - - $results = TagsHelper::searchTags($filters); - - if ($results) - { - // Output a JSON object - echo json_encode($results); - } - - $this->app->close(); - } + /** + * Method to search tags with AJAX + * + * @return void + */ + public function searchAjax() + { + $user = $this->app->getIdentity(); + + // Receive request data + $filters = array( + 'like' => trim($this->input->get('like', null, 'string')), + 'title' => trim($this->input->get('title', null, 'string')), + 'flanguage' => $this->input->get('flanguage', null, 'word'), + 'published' => $this->input->get('published', 1, 'int'), + 'parent_id' => $this->input->get('parent_id', 0, 'int'), + 'access' => $user->getAuthorisedViewLevels(), + ); + + if ((!$user->authorise('core.edit.state', 'com_tags')) && (!$user->authorise('core.edit', 'com_tags'))) { + // Filter on published for those who do not have edit or edit.state rights. + $filters['published'] = 1; + } + + $results = TagsHelper::searchTags($filters); + + if ($results) { + // Output a JSON object + echo json_encode($results); + } + + $this->app->close(); + } } diff --git a/components/com_tags/src/Helper/RouteHelper.php b/components/com_tags/src/Helper/RouteHelper.php index 99354fcb481bd..2cbd8f7cc56f1 100644 --- a/components/com_tags/src/Helper/RouteHelper.php +++ b/components/com_tags/src/Helper/RouteHelper.php @@ -1,4 +1,5 @@ getRoute($contentItemId, $typeAlias, $link, $language, $contentCatId); - } - - return $link; - } - - /** - * Tries to load the router for the component and calls it. Otherwise calls getRoute. - * - * @param integer $id The ID of the tag - * - * @return string URL link to pass to the router - * - * @since 3.1 - * @throws Exception - * @deprecated 5.0.0 Use getComponentTagRoute() instead - */ - public static function getTagRoute($id) - { - @trigger_error('This function is replaced by the getComponentTagRoute()', E_USER_DEPRECATED); - - return self::getComponentTagRoute($id); - } - - /** - * Tries to load the router for the component and calls it. Otherwise calls getRoute. - * - * @param string $id The ID of the tag in the format TAG_ID:TAG_ALIAS - * @param string $language The language of the tag - * - * @return string URL link to pass to the router - * - * @since 4.2.0 - * @throws Exception - */ - public static function getComponentTagRoute(string $id, string $language = '*'): string - { - $needles = [ - 'tag' => [(int) $id], - 'language' => $language, - ]; - - if ($id < 1) - { - $link = ''; - } - else - { - $link = 'index.php?option=com_tags&view=tag&id=' . $id; - - if ($item = self::_findItem($needles)) - { - $link .= '&Itemid=' . $item; - } - else - { - $needles = [ - 'tags' => [1, 0], - 'language' => $language, - ]; - - if ($item = self::_findItem($needles)) - { - $link .= '&Itemid=' . $item; - } - } - } - - return $link; - } - - /** - * Tries to load the router for the tags view. - * - * @return string URL link to pass to the router - * - * @since 3.7 - * @throws Exception - * @deprecated 5.0.0 - */ - public static function getTagsRoute() - { - @trigger_error('This function is replaced by the getComponentTagsRoute()', E_USER_DEPRECATED); - - return self::getComponentTagsRoute(); - } - - /** - * Tries to load the router for the tags view. - * - * @param string $language The language of the tag - * - * @return string URL link to pass to the router - * - * @since 4.2.0 - * @throws Exception - */ - public static function getComponentTagsRoute(string $language = '*'): string - { - $needles = [ - 'tags' => [0], - 'language' => $language, - ]; - - $link = 'index.php?option=com_tags&view=tags'; - - if ($item = self::_findItem($needles)) - { - $link .= '&Itemid=' . $item; - } - - return $link; - } - - /** - * Find Item static function - * - * @param array $needles Array used to get the language value - * - * @return null - * - * @throws Exception - */ - protected static function _findItem($needles = null) - { - $menus = AbstractMenu::getInstance('site'); - $language = $needles['language'] ?? '*'; - - // Prepare the reverse lookup array. - if (self::$lookup === null) - { - self::$lookup = array(); - - $component = ComponentHelper::getComponent('com_tags'); - $items = $menus->getItems('component_id', $component->id); - - if ($items) - { - foreach ($items as $item) - { - if (isset($item->query, $item->query['view'])) - { - $lang = ($item->language != '' ? $item->language : '*'); - - if (!isset(self::$lookup[$lang])) - { - self::$lookup[$lang] = array(); - } - - $view = $item->query['view']; - - if (!isset(self::$lookup[$lang][$view])) - { - self::$lookup[$lang][$view] = array(); - } - - // Only match menu items that list one tag - if (isset($item->query['id']) && is_array($item->query['id'])) - { - foreach ($item->query['id'] as $position => $tagId) - { - if (!isset(self::$lookup[$lang][$view][$item->query['id'][$position]]) || count($item->query['id']) == 1) - { - self::$lookup[$lang][$view][$item->query['id'][$position]] = $item->id; - } - } - } - elseif ($view == 'tags') - { - self::$lookup[$lang]['tags'][] = $item->id; - } - } - } - } - } - - if ($needles) - { - foreach ($needles as $view => $ids) - { - if (isset(self::$lookup[$language][$view])) - { - foreach ($ids as $id) - { - if (isset(self::$lookup[$language][$view][(int) $id])) - { - return self::$lookup[$language][$view][(int) $id]; - } - } - } - } - } - else - { - $active = $menus->getActive(); - - if ($active) - { - return $active->id; - } - } - - return null; - } + /** + * Lookup-table for menu items + * + * @var array + */ + protected static $lookup; + + /** + * Tries to load the router for the component and calls it. Otherwise uses getTagRoute. + * + * @param integer $contentItemId Component item id + * @param string $contentItemAlias Component item alias + * @param integer $contentCatId Component item category id + * @param string $language Component item language + * @param string $typeAlias Component type alias + * @param string $routerName Component router + * + * @return string URL link to pass to the router + * + * @since 3.1 + */ + public static function getItemRoute($contentItemId, $contentItemAlias, $contentCatId, $language, $typeAlias, $routerName) + { + $link = ''; + $explodedAlias = explode('.', $typeAlias); + $explodedRouter = explode('::', $routerName); + + if (file_exists($routerFile = JPATH_BASE . '/components/' . $explodedAlias[0] . '/helpers/route.php')) { + \JLoader::register($explodedRouter[0], $routerFile); + $routerClass = $explodedRouter[0]; + $routerMethod = $explodedRouter[1]; + + if (class_exists($routerClass) && method_exists($routerClass, $routerMethod)) { + if ($routerMethod === 'getCategoryRoute') { + $link = $routerClass::$routerMethod($contentItemId, $language); + } else { + $link = $routerClass::$routerMethod($contentItemId . ':' . $contentItemAlias, $contentCatId, $language); + } + } + } + + if ($link === '') { + // Create a fallback link in case we can't find the component router + $router = new CMSRouteHelper(); + $link = $router->getRoute($contentItemId, $typeAlias, $link, $language, $contentCatId); + } + + return $link; + } + + /** + * Tries to load the router for the component and calls it. Otherwise calls getRoute. + * + * @param integer $id The ID of the tag + * + * @return string URL link to pass to the router + * + * @since 3.1 + * @throws Exception + * @deprecated 5.0.0 Use getComponentTagRoute() instead + */ + public static function getTagRoute($id) + { + @trigger_error('This function is replaced by the getComponentTagRoute()', E_USER_DEPRECATED); + + return self::getComponentTagRoute($id); + } + + /** + * Tries to load the router for the component and calls it. Otherwise calls getRoute. + * + * @param string $id The ID of the tag in the format TAG_ID:TAG_ALIAS + * @param string $language The language of the tag + * + * @return string URL link to pass to the router + * + * @since 4.2.0 + * @throws Exception + */ + public static function getComponentTagRoute(string $id, string $language = '*'): string + { + $needles = [ + 'tag' => [(int) $id], + 'language' => $language, + ]; + + if ($id < 1) { + $link = ''; + } else { + $link = 'index.php?option=com_tags&view=tag&id=' . $id; + + if ($item = self::_findItem($needles)) { + $link .= '&Itemid=' . $item; + } else { + $needles = [ + 'tags' => [1, 0], + 'language' => $language, + ]; + + if ($item = self::_findItem($needles)) { + $link .= '&Itemid=' . $item; + } + } + } + + return $link; + } + + /** + * Tries to load the router for the tags view. + * + * @return string URL link to pass to the router + * + * @since 3.7 + * @throws Exception + * @deprecated 5.0.0 + */ + public static function getTagsRoute() + { + @trigger_error('This function is replaced by the getComponentTagsRoute()', E_USER_DEPRECATED); + + return self::getComponentTagsRoute(); + } + + /** + * Tries to load the router for the tags view. + * + * @param string $language The language of the tag + * + * @return string URL link to pass to the router + * + * @since 4.2.0 + * @throws Exception + */ + public static function getComponentTagsRoute(string $language = '*'): string + { + $needles = [ + 'tags' => [0], + 'language' => $language, + ]; + + $link = 'index.php?option=com_tags&view=tags'; + + if ($item = self::_findItem($needles)) { + $link .= '&Itemid=' . $item; + } + + return $link; + } + + /** + * Find Item static function + * + * @param array $needles Array used to get the language value + * + * @return null + * + * @throws Exception + */ + protected static function _findItem($needles = null) + { + $menus = AbstractMenu::getInstance('site'); + $language = $needles['language'] ?? '*'; + + // Prepare the reverse lookup array. + if (self::$lookup === null) { + self::$lookup = array(); + + $component = ComponentHelper::getComponent('com_tags'); + $items = $menus->getItems('component_id', $component->id); + + if ($items) { + foreach ($items as $item) { + if (isset($item->query, $item->query['view'])) { + $lang = ($item->language != '' ? $item->language : '*'); + + if (!isset(self::$lookup[$lang])) { + self::$lookup[$lang] = array(); + } + + $view = $item->query['view']; + + if (!isset(self::$lookup[$lang][$view])) { + self::$lookup[$lang][$view] = array(); + } + + // Only match menu items that list one tag + if (isset($item->query['id']) && is_array($item->query['id'])) { + foreach ($item->query['id'] as $position => $tagId) { + if (!isset(self::$lookup[$lang][$view][$item->query['id'][$position]]) || count($item->query['id']) == 1) { + self::$lookup[$lang][$view][$item->query['id'][$position]] = $item->id; + } + } + } elseif ($view == 'tags') { + self::$lookup[$lang]['tags'][] = $item->id; + } + } + } + } + } + + if ($needles) { + foreach ($needles as $view => $ids) { + if (isset(self::$lookup[$language][$view])) { + foreach ($ids as $id) { + if (isset(self::$lookup[$language][$view][(int) $id])) { + return self::$lookup[$language][$view][(int) $id]; + } + } + } + } + } else { + $active = $menus->getActive(); + + if ($active) { + return $active->id; + } + } + + return null; + } } diff --git a/components/com_tags/src/Model/TagModel.php b/components/com_tags/src/Model/TagModel.php index af2c4ad3a6415..2c49b9fd266f2 100644 --- a/components/com_tags/src/Model/TagModel.php +++ b/components/com_tags/src/Model/TagModel.php @@ -1,4 +1,5 @@ link = RouteHelper::getItemRoute( - $item->content_item_id, - $item->core_alias, - $item->core_catid, - $item->core_language, - $item->type_alias, - $item->router - ); - - // Get display date - switch ($this->state->params->get('tag_list_show_date')) - { - case 'modified': - $item->displayDate = $item->core_modified_time; - break; - - case 'created': - $item->displayDate = $item->core_created_time; - break; - - default: - $item->displayDate = ($item->core_publish_up == 0) ? $item->core_created_time : $item->core_publish_up; - break; - } - } - } - - return $items; - } - - /** - * Method to build an SQL query to load the list data of all items with a given tag. - * - * @return string An SQL query - * - * @since 3.1 - */ - protected function getListQuery() - { - $tagId = $this->getState('tag.id') ? : ''; - - $typesr = $this->getState('tag.typesr'); - $orderByOption = $this->getState('list.ordering', 'c.core_title'); - $includeChildren = $this->state->params->get('include_children', 0); - $orderDir = $this->getState('list.direction', 'ASC'); - $matchAll = $this->getState('params')->get('return_any_or_all', 1); - $language = $this->getState('tag.language'); - $stateFilter = $this->getState('tag.state'); - - // Optionally filter on language - if (empty($language)) - { - $language = ComponentHelper::getParams('com_tags')->get('tag_list_language_filter', 'all'); - } - - $query = (new TagsHelper)->getTagItemsQuery($tagId, $typesr, $includeChildren, $orderByOption, $orderDir, $matchAll, $language, $stateFilter); - - if ($this->state->get('list.filter')) - { - $db = $this->getDatabase(); - $query->where($db->quoteName('c.core_title') . ' LIKE ' . $db->quote('%' . $this->state->get('list.filter') . '%')); - } - - return $query; - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @param string $ordering An optional ordering field. - * @param string $direction An optional direction (asc|desc). - * - * @return void - * - * @since 3.1 - */ - protected function populateState($ordering = 'c.core_title', $direction = 'ASC') - { - $app = Factory::getApplication(); - - // Load the parameters. - $params = $app->isClient('administrator') ? ComponentHelper::getParams('com_tags') : $app->getParams(); - - $this->setState('params', $params); - - // Load state from the request. - $ids = (array) $app->input->get('id', array(), 'string'); - - if (count($ids) == 1) - { - $ids = explode(',', $ids[0]); - } - - $ids = ArrayHelper::toInteger($ids); - - // Remove zero values resulting from bad input - $ids = array_filter($ids); - - $pkString = implode(',', $ids); - - $this->setState('tag.id', $pkString); - - // Get the selected list of types from the request. If none are specified all are used. - $typesr = $app->input->get('types', array(), 'array'); - - if ($typesr) - { - // Implode is needed because the array can contain a string with a coma separated list of ids - $typesr = implode(',', $typesr); - - // Sanitise - $typesr = explode(',', $typesr); - $typesr = ArrayHelper::toInteger($typesr); - - $this->setState('tag.typesr', $typesr); - } - - $language = $app->input->getString('tag_list_language_filter'); - $this->setState('tag.language', $language); - - // List state information - $format = $app->input->getWord('format'); - - if ($format === 'feed') - { - $limit = $app->get('feed_limit'); - } - else - { - $limit = $params->get('display_num', $app->get('list_limit', 20)); - $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $limit, 'uint'); - } - - $this->setState('list.limit', $limit); - - $offset = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.start', $offset); - - $itemid = $pkString . ':' . $app->input->get('Itemid', 0, 'int'); - $orderCol = $app->getUserStateFromRequest('com_tags.tag.list.' . $itemid . '.filter_order', 'filter_order', '', 'string'); - $orderCol = !$orderCol ? $this->state->params->get('tag_list_orderby', 'c.core_title') : $orderCol; - - if (!in_array($orderCol, $this->filter_fields)) - { - $orderCol = 'c.core_title'; - } - - $this->setState('list.ordering', $orderCol); - - $listOrder = $app->getUserStateFromRequest('com_tags.tag.list.' . $itemid . '.filter_order_direction', 'filter_order_Dir', '', 'string'); - $listOrder = !$listOrder ? $this->state->params->get('tag_list_orderby_direction', 'ASC') : $listOrder; - - if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) - { - $listOrder = 'ASC'; - } - - $this->setState('list.direction', $listOrder); - - $this->setState('tag.state', 1); - - // Optional filter text - $filterSearch = $app->getUserStateFromRequest('com_tags.tag.list.' . $itemid . '.filter_search', 'filter-search', '', 'string'); - $this->setState('list.filter', $filterSearch); - } - - /** - * Method to get tag data for the current tag or tags - * - * @param integer $pk An optional ID - * - * @return array - * - * @since 3.1 - */ - public function getItem($pk = null) - { - if (!isset($this->item)) - { - $this->item = []; - - if (empty($pk)) - { - $pk = $this->getState('tag.id'); - } - - // Get a level row instance. - /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ - $table = $this->getTable(); - - $idsArray = explode(',', $pk); - - // Attempt to load the rows into an array. - foreach ($idsArray as $id) - { - try - { - $table->load($id); - - // Check published state. - if ($published = $this->getState('tag.state')) - { - if ($table->published != $published) - { - continue; - } - } - - if (!in_array($table->access, Factory::getUser()->getAuthorisedViewLevels())) - { - continue; - } - - // Convert the Table to a clean CMSObject. - $properties = $table->getProperties(1); - $this->item[] = ArrayHelper::toObject($properties, CMSObject::class); - } - catch (\RuntimeException $e) - { - $this->setError($e->getMessage()); - - return false; - } - } - } - - if (!$this->item) - { - throw new \Exception(Text::_('COM_TAGS_TAG_NOT_FOUND'), 404); - } - - return $this->item; - } - - /** - * Increment the hit counter. - * - * @param integer $pk Optional primary key of the article to increment. - * - * @return boolean True if successful; false otherwise and internal error set. - * - * @since 3.2 - */ - public function hit($pk = 0) - { - $input = Factory::getApplication()->input; - $hitcount = $input->getInt('hitcount', 1); - - if ($hitcount) - { - $pk = (!empty($pk)) ? $pk : (int) $this->getState('tag.id'); - - /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ - $table = $this->getTable(); - $table->hit($pk); - - // Load the table data for later - $table->load($pk); - - if (!$table->hasPrimaryKey()) - { - throw new \Exception(Text::_('COM_TAGS_TAG_NOT_FOUND'), 404); - } - } - - return true; - } + /** + * The tags that apply. + * + * @var object + * @since 3.1 + */ + protected $tag = null; + + /** + * The list of items associated with the tags. + * + * @var array + * @since 3.1 + */ + protected $items = null; + + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * @param MVCFactoryInterface $factory The factory. + * + * @since 1.6 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null) + { + if (empty($config['filter_fields'])) { + $config['filter_fields'] = array( + 'core_content_id', 'c.core_content_id', + 'core_title', 'c.core_title', + 'core_type_alias', 'c.core_type_alias', + 'core_checked_out_user_id', 'c.core_checked_out_user_id', + 'core_checked_out_time', 'c.core_checked_out_time', + 'core_catid', 'c.core_catid', + 'core_state', 'c.core_state', + 'core_access', 'c.core_access', + 'core_created_user_id', 'c.core_created_user_id', + 'core_created_time', 'c.core_created_time', + 'core_modified_time', 'c.core_modified_time', + 'core_ordering', 'c.core_ordering', + 'core_featured', 'c.core_featured', + 'core_language', 'c.core_language', + 'core_hits', 'c.core_hits', + 'core_publish_up', 'c.core_publish_up', + 'core_publish_down', 'c.core_publish_down', + 'core_images', 'c.core_images', + 'core_urls', 'c.core_urls', + 'match_count', + ); + } + + parent::__construct($config, $factory); + } + + /** + * Method to get a list of items for a list of tags. + * + * @return mixed An array of objects on success, false on failure. + * + * @since 3.1 + */ + public function getItems() + { + // Invoke the parent getItems method to get the main list + $items = parent::getItems(); + + if (!empty($items)) { + foreach ($items as $item) { + $item->link = RouteHelper::getItemRoute( + $item->content_item_id, + $item->core_alias, + $item->core_catid, + $item->core_language, + $item->type_alias, + $item->router + ); + + // Get display date + switch ($this->state->params->get('tag_list_show_date')) { + case 'modified': + $item->displayDate = $item->core_modified_time; + break; + + case 'created': + $item->displayDate = $item->core_created_time; + break; + + default: + $item->displayDate = ($item->core_publish_up == 0) ? $item->core_created_time : $item->core_publish_up; + break; + } + } + } + + return $items; + } + + /** + * Method to build an SQL query to load the list data of all items with a given tag. + * + * @return string An SQL query + * + * @since 3.1 + */ + protected function getListQuery() + { + $tagId = $this->getState('tag.id') ? : ''; + + $typesr = $this->getState('tag.typesr'); + $orderByOption = $this->getState('list.ordering', 'c.core_title'); + $includeChildren = $this->state->params->get('include_children', 0); + $orderDir = $this->getState('list.direction', 'ASC'); + $matchAll = $this->getState('params')->get('return_any_or_all', 1); + $language = $this->getState('tag.language'); + $stateFilter = $this->getState('tag.state'); + + // Optionally filter on language + if (empty($language)) { + $language = ComponentHelper::getParams('com_tags')->get('tag_list_language_filter', 'all'); + } + + $query = (new TagsHelper())->getTagItemsQuery($tagId, $typesr, $includeChildren, $orderByOption, $orderDir, $matchAll, $language, $stateFilter); + + if ($this->state->get('list.filter')) { + $db = $this->getDatabase(); + $query->where($db->quoteName('c.core_title') . ' LIKE ' . $db->quote('%' . $this->state->get('list.filter') . '%')); + } + + return $query; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 3.1 + */ + protected function populateState($ordering = 'c.core_title', $direction = 'ASC') + { + $app = Factory::getApplication(); + + // Load the parameters. + $params = $app->isClient('administrator') ? ComponentHelper::getParams('com_tags') : $app->getParams(); + + $this->setState('params', $params); + + // Load state from the request. + $ids = (array) $app->input->get('id', array(), 'string'); + + if (count($ids) == 1) { + $ids = explode(',', $ids[0]); + } + + $ids = ArrayHelper::toInteger($ids); + + // Remove zero values resulting from bad input + $ids = array_filter($ids); + + $pkString = implode(',', $ids); + + $this->setState('tag.id', $pkString); + + // Get the selected list of types from the request. If none are specified all are used. + $typesr = $app->input->get('types', array(), 'array'); + + if ($typesr) { + // Implode is needed because the array can contain a string with a coma separated list of ids + $typesr = implode(',', $typesr); + + // Sanitise + $typesr = explode(',', $typesr); + $typesr = ArrayHelper::toInteger($typesr); + + $this->setState('tag.typesr', $typesr); + } + + $language = $app->input->getString('tag_list_language_filter'); + $this->setState('tag.language', $language); + + // List state information + $format = $app->input->getWord('format'); + + if ($format === 'feed') { + $limit = $app->get('feed_limit'); + } else { + $limit = $params->get('display_num', $app->get('list_limit', 20)); + $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $limit, 'uint'); + } + + $this->setState('list.limit', $limit); + + $offset = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.start', $offset); + + $itemid = $pkString . ':' . $app->input->get('Itemid', 0, 'int'); + $orderCol = $app->getUserStateFromRequest('com_tags.tag.list.' . $itemid . '.filter_order', 'filter_order', '', 'string'); + $orderCol = !$orderCol ? $this->state->params->get('tag_list_orderby', 'c.core_title') : $orderCol; + + if (!in_array($orderCol, $this->filter_fields)) { + $orderCol = 'c.core_title'; + } + + $this->setState('list.ordering', $orderCol); + + $listOrder = $app->getUserStateFromRequest('com_tags.tag.list.' . $itemid . '.filter_order_direction', 'filter_order_Dir', '', 'string'); + $listOrder = !$listOrder ? $this->state->params->get('tag_list_orderby_direction', 'ASC') : $listOrder; + + if (!in_array(strtoupper($listOrder), array('ASC', 'DESC', ''))) { + $listOrder = 'ASC'; + } + + $this->setState('list.direction', $listOrder); + + $this->setState('tag.state', 1); + + // Optional filter text + $filterSearch = $app->getUserStateFromRequest('com_tags.tag.list.' . $itemid . '.filter_search', 'filter-search', '', 'string'); + $this->setState('list.filter', $filterSearch); + } + + /** + * Method to get tag data for the current tag or tags + * + * @param integer $pk An optional ID + * + * @return array + * + * @since 3.1 + */ + public function getItem($pk = null) + { + if (!isset($this->item)) { + $this->item = []; + + if (empty($pk)) { + $pk = $this->getState('tag.id'); + } + + // Get a level row instance. + /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ + $table = $this->getTable(); + + $idsArray = explode(',', $pk); + + // Attempt to load the rows into an array. + foreach ($idsArray as $id) { + try { + $table->load($id); + + // Check published state. + if ($published = $this->getState('tag.state')) { + if ($table->published != $published) { + continue; + } + } + + if (!in_array($table->access, Factory::getUser()->getAuthorisedViewLevels())) { + continue; + } + + // Convert the Table to a clean CMSObject. + $properties = $table->getProperties(1); + $this->item[] = ArrayHelper::toObject($properties, CMSObject::class); + } catch (\RuntimeException $e) { + $this->setError($e->getMessage()); + + return false; + } + } + } + + if (!$this->item) { + throw new \Exception(Text::_('COM_TAGS_TAG_NOT_FOUND'), 404); + } + + return $this->item; + } + + /** + * Increment the hit counter. + * + * @param integer $pk Optional primary key of the article to increment. + * + * @return boolean True if successful; false otherwise and internal error set. + * + * @since 3.2 + */ + public function hit($pk = 0) + { + $input = Factory::getApplication()->input; + $hitcount = $input->getInt('hitcount', 1); + + if ($hitcount) { + $pk = (!empty($pk)) ? $pk : (int) $this->getState('tag.id'); + + /** @var \Joomla\Component\Tags\Administrator\Table\TagTable $table */ + $table = $this->getTable(); + $table->hit($pk); + + // Load the table data for later + $table->load($pk); + + if (!$table->hasPrimaryKey()) { + throw new \Exception(Text::_('COM_TAGS_TAG_NOT_FOUND'), 404); + } + } + + return true; + } } diff --git a/components/com_tags/src/Model/TagsModel.php b/components/com_tags/src/Model/TagsModel.php index 06d7486bbdcbd..691adb0743a15 100644 --- a/components/com_tags/src/Model/TagsModel.php +++ b/components/com_tags/src/Model/TagsModel.php @@ -1,4 +1,5 @@ input->getInt('parent_id'); - $this->setState('tag.parent_id', $pid); - - $language = $app->input->getString('tag_list_language_filter'); - $this->setState('tag.language', $language); - - $offset = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.offset', $offset); - $app = Factory::getApplication(); - - $params = $app->getParams(); - $this->setState('params', $params); - - $this->setState('list.limit', $params->get('maximum', 200)); - - $this->setState('filter.published', 1); - $this->setState('filter.access', true); - - $user = Factory::getUser(); - - if ((!$user->authorise('core.edit.state', 'com_tags')) && (!$user->authorise('core.edit', 'com_tags'))) - { - $this->setState('filter.published', 1); - } - - // Optional filter text - $itemid = $pid . ':' . $app->input->getInt('Itemid', 0); - $filterSearch = $app->getUserStateFromRequest('com_tags.tags.list.' . $itemid . '.filter_search', 'filter-search', '', 'string'); - $this->setState('list.filter', $filterSearch); - } - - /** - * Method to build an SQL query to load the list data. - * - * @return string An SQL query - * - * @since 1.6 - */ - protected function getListQuery() - { - $app = Factory::getApplication(); - $user = Factory::getUser(); - $groups = $user->getAuthorisedViewLevels(); - $pid = (int) $this->getState('tag.parent_id'); - $orderby = $this->state->params->get('all_tags_orderby', 'title'); - $published = (int) $this->state->params->get('published', 1); - $orderDirection = $this->state->params->get('all_tags_orderby_direction', 'ASC'); - $language = $this->getState('tag.language'); - - // Create a new query object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select required fields from the tags. - $query->select('a.*, u.name as created_by_user_name, u.email') - ->from($db->quoteName('#__tags', 'a')) - ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('a.created_user_id') . ' = ' . $db->quoteName('u.id')) - ->whereIn($db->quoteName('a.access'), $groups); - - if (!empty($pid)) - { - $query->where($db->quoteName('a.parent_id') . ' = :pid') - ->bind(':pid', $pid, ParameterType::INTEGER); - } - - // Exclude the root. - $query->where($db->quoteName('a.parent_id') . ' <> 0'); - - // Optionally filter on language - if (empty($language)) - { - $language = ComponentHelper::getParams('com_tags')->get('tag_list_language_filter', 'all'); - } - - if ($language !== 'all') - { - if ($language === 'current_language') - { - $language = ContentHelper::getCurrentLanguage(); - } - - $query->whereIn($db->quoteName('language'), [$language, '*'], ParameterType::STRING); - } - - // List state information - $format = $app->input->getWord('format'); - - if ($format === 'feed') - { - $limit = $app->get('feed_limit'); - } - else - { - if ($this->state->params->get('show_pagination_limit')) - { - $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->get('list_limit'), 'uint'); - } - else - { - $limit = $this->state->params->get('maximum', 20); - } - } - - $this->setState('list.limit', $limit); - - $offset = $app->input->get('limitstart', 0, 'uint'); - $this->setState('list.start', $offset); - - // Optionally filter on entered value - if ($this->state->get('list.filter')) - { - $title = '%' . $this->state->get('list.filter') . '%'; - $query->where($db->quoteName('a.title') . ' LIKE :title') - ->bind(':title', $title); - } - - $query->where($db->quoteName('a.published') . ' = :published') - ->bind(':published', $published, ParameterType::INTEGER); - - $query->order($db->quoteName($orderby) . ' ' . $orderDirection . ', a.title ASC'); - - return $query; - } + /** + * Model context string. + * + * @var string + * @since 3.1 + */ + public $_context = 'com_tags.tags'; + + /** + * Method to auto-populate the model state. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @note Calling getState in this method will result in recursion. + * + * @since 3.1 + */ + protected function populateState($ordering = null, $direction = null) + { + $app = Factory::getApplication(); + + // Load state from the request. + $pid = $app->input->getInt('parent_id'); + $this->setState('tag.parent_id', $pid); + + $language = $app->input->getString('tag_list_language_filter'); + $this->setState('tag.language', $language); + + $offset = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.offset', $offset); + $app = Factory::getApplication(); + + $params = $app->getParams(); + $this->setState('params', $params); + + $this->setState('list.limit', $params->get('maximum', 200)); + + $this->setState('filter.published', 1); + $this->setState('filter.access', true); + + $user = Factory::getUser(); + + if ((!$user->authorise('core.edit.state', 'com_tags')) && (!$user->authorise('core.edit', 'com_tags'))) { + $this->setState('filter.published', 1); + } + + // Optional filter text + $itemid = $pid . ':' . $app->input->getInt('Itemid', 0); + $filterSearch = $app->getUserStateFromRequest('com_tags.tags.list.' . $itemid . '.filter_search', 'filter-search', '', 'string'); + $this->setState('list.filter', $filterSearch); + } + + /** + * Method to build an SQL query to load the list data. + * + * @return string An SQL query + * + * @since 1.6 + */ + protected function getListQuery() + { + $app = Factory::getApplication(); + $user = Factory::getUser(); + $groups = $user->getAuthorisedViewLevels(); + $pid = (int) $this->getState('tag.parent_id'); + $orderby = $this->state->params->get('all_tags_orderby', 'title'); + $published = (int) $this->state->params->get('published', 1); + $orderDirection = $this->state->params->get('all_tags_orderby_direction', 'ASC'); + $language = $this->getState('tag.language'); + + // Create a new query object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select required fields from the tags. + $query->select('a.*, u.name as created_by_user_name, u.email') + ->from($db->quoteName('#__tags', 'a')) + ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('a.created_user_id') . ' = ' . $db->quoteName('u.id')) + ->whereIn($db->quoteName('a.access'), $groups); + + if (!empty($pid)) { + $query->where($db->quoteName('a.parent_id') . ' = :pid') + ->bind(':pid', $pid, ParameterType::INTEGER); + } + + // Exclude the root. + $query->where($db->quoteName('a.parent_id') . ' <> 0'); + + // Optionally filter on language + if (empty($language)) { + $language = ComponentHelper::getParams('com_tags')->get('tag_list_language_filter', 'all'); + } + + if ($language !== 'all') { + if ($language === 'current_language') { + $language = ContentHelper::getCurrentLanguage(); + } + + $query->whereIn($db->quoteName('language'), [$language, '*'], ParameterType::STRING); + } + + // List state information + $format = $app->input->getWord('format'); + + if ($format === 'feed') { + $limit = $app->get('feed_limit'); + } else { + if ($this->state->params->get('show_pagination_limit')) { + $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->get('list_limit'), 'uint'); + } else { + $limit = $this->state->params->get('maximum', 20); + } + } + + $this->setState('list.limit', $limit); + + $offset = $app->input->get('limitstart', 0, 'uint'); + $this->setState('list.start', $offset); + + // Optionally filter on entered value + if ($this->state->get('list.filter')) { + $title = '%' . $this->state->get('list.filter') . '%'; + $query->where($db->quoteName('a.title') . ' LIKE :title') + ->bind(':title', $title); + } + + $query->where($db->quoteName('a.published') . ' = :published') + ->bind(':published', $published, ParameterType::INTEGER); + + $query->order($db->quoteName($orderby) . ' ' . $orderDirection . ', a.title ASC'); + + return $query; + } } diff --git a/components/com_tags/src/Service/Router.php b/components/com_tags/src/Service/Router.php index d840c208f8a59..85fe9a41a3a10 100644 --- a/components/com_tags/src/Service/Router.php +++ b/components/com_tags/src/Service/Router.php @@ -1,4 +1,5 @@ db = $db; - - parent::__construct($app, $menu); - } - - /** - * Build the route for the com_tags component - * - * @param array &$query An array of URL arguments - * - * @return array The URL arguments to use to assemble the subsequent URL. - * - * @since 3.3 - */ - public function build(&$query) - { - $segments = array(); - - // Get a menu item based on Itemid or currently active - - // We need a menu item. Either the one specified in the query, or the current active one if none specified - if (empty($query['Itemid'])) - { - $menuItem = $this->menu->getActive(); - } - else - { - $menuItem = $this->menu->getItem($query['Itemid']); - } - - $mView = empty($menuItem->query['view']) ? null : $menuItem->query['view']; - $mId = empty($menuItem->query['id']) ? null : $menuItem->query['id']; - - if (is_array($mId)) - { - $mId = ArrayHelper::toInteger($mId); - } - - $view = ''; - - if (isset($query['view'])) - { - $view = $query['view']; - - if (empty($query['Itemid'])) - { - $segments[] = $view; - } - - unset($query['view']); - } - - // Are we dealing with a tag that is attached to a menu item? - if ($mView == $view && isset($query['id']) && $mId == $query['id']) - { - unset($query['id']); - - return $segments; - } - - if ($view === 'tag') - { - $notActiveTag = is_array($mId) ? (count($mId) > 1 || $mId[0] != (int) $query['id']) : ($mId != (int) $query['id']); - - if ($notActiveTag || $mView != $view) - { - // ID in com_tags can be either an integer, a string or an array of IDs - $id = is_array($query['id']) ? implode(',', $query['id']) : $query['id']; - $segments[] = $id; - } - - unset($query['id']); - } - - if (isset($query['layout'])) - { - if ((!empty($query['Itemid']) && isset($menuItem->query['layout']) - && $query['layout'] == $menuItem->query['layout']) - || $query['layout'] === 'default') - { - unset($query['layout']); - } - } - - $total = count($segments); - - for ($i = 0; $i < $total; $i++) - { - $segments[$i] = str_replace(':', '-', $segments[$i]); - $position = strpos($segments[$i], '-'); - - if ($position) - { - // Remove id from segment - $segments[$i] = substr($segments[$i], $position + 1); - } - } - - return $segments; - } - - /** - * Parse the segments of a URL. - * - * @param array &$segments The segments of the URL to parse. - * - * @return array The URL attributes to be used by the application. - * - * @since 3.3 - */ - public function parse(&$segments) - { - $total = count($segments); - $vars = array(); - - for ($i = 0; $i < $total; $i++) - { - $segments[$i] = preg_replace('/-/', ':', $segments[$i], 1); - } - - // Get the active menu item. - $item = $this->menu->getActive(); - - // Count route segments - $count = count($segments); - - // Standard routing for tags. - if (!isset($item)) - { - $vars['view'] = $segments[0]; - $vars['id'] = $this->fixSegment($segments[$count - 1]); - unset($segments[0]); - unset($segments[$count - 1]); - - return $vars; - } - - $vars['id'] = $this->fixSegment($segments[0]); - $vars['view'] = 'tag'; - unset($segments[0]); - - return $vars; - } - - /** - * Try to add missing id to segment - * - * @param string $segment One piece of segment of the URL to parse - * - * @return string The segment with founded id - * - * @since 3.7 - */ - protected function fixSegment($segment) - { - // Try to find tag id - $alias = str_replace(':', '-', $segment); - - $query = $this->db->getQuery(true) - ->select($this->db->quoteName('id')) - ->from($this->db->quoteName('#__tags')) - ->where($this->db->quoteName('alias') . ' = :alias') - ->bind(':alias', $alias); - - $id = $this->db->setQuery($query)->loadResult(); - - if ($id) - { - $segment = "$id:$alias"; - } - - return $segment; - } + /** + * The db + * + * @var DatabaseInterface + * + * @since 4.0.0 + */ + private $db; + + /** + * Tags Component router constructor + * + * @param SiteApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + * @param CategoryFactoryInterface $categoryFactory The category object + * @param DatabaseInterface $db The database object + * + * @since 4.0.0 + */ + public function __construct(SiteApplication $app, AbstractMenu $menu, ?CategoryFactoryInterface $categoryFactory, DatabaseInterface $db) + { + $this->db = $db; + + parent::__construct($app, $menu); + } + + /** + * Build the route for the com_tags component + * + * @param array &$query An array of URL arguments + * + * @return array The URL arguments to use to assemble the subsequent URL. + * + * @since 3.3 + */ + public function build(&$query) + { + $segments = array(); + + // Get a menu item based on Itemid or currently active + + // We need a menu item. Either the one specified in the query, or the current active one if none specified + if (empty($query['Itemid'])) { + $menuItem = $this->menu->getActive(); + } else { + $menuItem = $this->menu->getItem($query['Itemid']); + } + + $mView = empty($menuItem->query['view']) ? null : $menuItem->query['view']; + $mId = empty($menuItem->query['id']) ? null : $menuItem->query['id']; + + if (is_array($mId)) { + $mId = ArrayHelper::toInteger($mId); + } + + $view = ''; + + if (isset($query['view'])) { + $view = $query['view']; + + if (empty($query['Itemid'])) { + $segments[] = $view; + } + + unset($query['view']); + } + + // Are we dealing with a tag that is attached to a menu item? + if ($mView == $view && isset($query['id']) && $mId == $query['id']) { + unset($query['id']); + + return $segments; + } + + if ($view === 'tag') { + $notActiveTag = is_array($mId) ? (count($mId) > 1 || $mId[0] != (int) $query['id']) : ($mId != (int) $query['id']); + + if ($notActiveTag || $mView != $view) { + // ID in com_tags can be either an integer, a string or an array of IDs + $id = is_array($query['id']) ? implode(',', $query['id']) : $query['id']; + $segments[] = $id; + } + + unset($query['id']); + } + + if (isset($query['layout'])) { + if ( + (!empty($query['Itemid']) && isset($menuItem->query['layout']) + && $query['layout'] == $menuItem->query['layout']) + || $query['layout'] === 'default' + ) { + unset($query['layout']); + } + } + + $total = count($segments); + + for ($i = 0; $i < $total; $i++) { + $segments[$i] = str_replace(':', '-', $segments[$i]); + $position = strpos($segments[$i], '-'); + + if ($position) { + // Remove id from segment + $segments[$i] = substr($segments[$i], $position + 1); + } + } + + return $segments; + } + + /** + * Parse the segments of a URL. + * + * @param array &$segments The segments of the URL to parse. + * + * @return array The URL attributes to be used by the application. + * + * @since 3.3 + */ + public function parse(&$segments) + { + $total = count($segments); + $vars = array(); + + for ($i = 0; $i < $total; $i++) { + $segments[$i] = preg_replace('/-/', ':', $segments[$i], 1); + } + + // Get the active menu item. + $item = $this->menu->getActive(); + + // Count route segments + $count = count($segments); + + // Standard routing for tags. + if (!isset($item)) { + $vars['view'] = $segments[0]; + $vars['id'] = $this->fixSegment($segments[$count - 1]); + unset($segments[0]); + unset($segments[$count - 1]); + + return $vars; + } + + $vars['id'] = $this->fixSegment($segments[0]); + $vars['view'] = 'tag'; + unset($segments[0]); + + return $vars; + } + + /** + * Try to add missing id to segment + * + * @param string $segment One piece of segment of the URL to parse + * + * @return string The segment with founded id + * + * @since 3.7 + */ + protected function fixSegment($segment) + { + // Try to find tag id + $alias = str_replace(':', '-', $segment); + + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('id')) + ->from($this->db->quoteName('#__tags')) + ->where($this->db->quoteName('alias') . ' = :alias') + ->bind(':alias', $alias); + + $id = $this->db->setQuery($query)->loadResult(); + + if ($id) { + $segment = "$id:$alias"; + } + + return $segment; + } } diff --git a/components/com_tags/src/View/Tag/FeedView.php b/components/com_tags/src/View/Tag/FeedView.php index 7fa154cd44916..0b62a96e1c49e 100644 --- a/components/com_tags/src/View/Tag/FeedView.php +++ b/components/com_tags/src/View/Tag/FeedView.php @@ -1,4 +1,5 @@ input->get('id', array(), 'int'); - $i = 0; - $tagIds = ''; - - // Remove zero values resulting from input filter - $ids = array_filter($ids); - - foreach ($ids as $id) - { - if ($i !== 0) - { - $tagIds .= '&'; - } - - $tagIds .= 'id[' . $i . ']=' . $id; - - $i++; - } - - $this->document->link = Route::_('index.php?option=com_tags&view=tag&' . $tagIds); - - $app->input->set('limit', $app->get('feed_limit')); - $siteEmail = $app->get('mailfrom'); - $fromName = $app->get('fromname'); - $feedEmail = $app->get('feed_email', 'none'); - - $this->document->editor = $fromName; - - if ($feedEmail !== 'none') - { - $this->document->editorEmail = $siteEmail; - } - - // Get some data from the model - $items = $this->get('Items'); - - if ($items !== false) - { - foreach ($items as $item) - { - // Strip HTML from feed item title - $title = $this->escape($item->core_title); - $title = html_entity_decode($title, ENT_COMPAT, 'UTF-8'); - - // Strip HTML from feed item description text - $description = $item->core_body; - $author = $item->core_created_by_alias ?: $item->author; - $date = ($item->displayDate ? date('r', strtotime($item->displayDate)) : ''); - - // Load individual item creator class - $feeditem = new FeedItem; - $feeditem->title = $title; - $feeditem->link = Route::_($item->link); - $feeditem->description = $description; - $feeditem->date = $date; - $feeditem->category = $title; - $feeditem->author = $author; - - if ($feedEmail === 'site') - { - $item->authorEmail = $siteEmail; - } - elseif ($feedEmail === 'author') - { - $item->authorEmail = $item->author_email; - } - - // Loads item info into RSS array - $this->document->addItem($feeditem); - } - } - } + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $ids = (array) $app->input->get('id', array(), 'int'); + $i = 0; + $tagIds = ''; + + // Remove zero values resulting from input filter + $ids = array_filter($ids); + + foreach ($ids as $id) { + if ($i !== 0) { + $tagIds .= '&'; + } + + $tagIds .= 'id[' . $i . ']=' . $id; + + $i++; + } + + $this->document->link = Route::_('index.php?option=com_tags&view=tag&' . $tagIds); + + $app->input->set('limit', $app->get('feed_limit')); + $siteEmail = $app->get('mailfrom'); + $fromName = $app->get('fromname'); + $feedEmail = $app->get('feed_email', 'none'); + + $this->document->editor = $fromName; + + if ($feedEmail !== 'none') { + $this->document->editorEmail = $siteEmail; + } + + // Get some data from the model + $items = $this->get('Items'); + + if ($items !== false) { + foreach ($items as $item) { + // Strip HTML from feed item title + $title = $this->escape($item->core_title); + $title = html_entity_decode($title, ENT_COMPAT, 'UTF-8'); + + // Strip HTML from feed item description text + $description = $item->core_body; + $author = $item->core_created_by_alias ?: $item->author; + $date = ($item->displayDate ? date('r', strtotime($item->displayDate)) : ''); + + // Load individual item creator class + $feeditem = new FeedItem(); + $feeditem->title = $title; + $feeditem->link = Route::_($item->link); + $feeditem->description = $description; + $feeditem->date = $date; + $feeditem->category = $title; + $feeditem->author = $author; + + if ($feedEmail === 'site') { + $item->authorEmail = $siteEmail; + } elseif ($feedEmail === 'author') { + $item->authorEmail = $item->author_email; + } + + // Loads item info into RSS array + $this->document->addItem($feeditem); + } + } + } } diff --git a/components/com_tags/src/View/Tag/HtmlView.php b/components/com_tags/src/View/Tag/HtmlView.php index c700751e65fea..c0528ca1af341 100644 --- a/components/com_tags/src/View/Tag/HtmlView.php +++ b/components/com_tags/src/View/Tag/HtmlView.php @@ -1,4 +1,5 @@ getParams(); - - // Get some data from the models - $state = $this->get('State'); - $items = $this->get('Items'); - $item = $this->get('Item'); - $children = $this->get('Children'); - $parent = $this->get('Parent'); - $pagination = $this->get('Pagination'); - - // Flag indicates to not add limitstart=0 to URL - $pagination->hideEmptyLimitstart = true; - - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check whether access level allows access. - // @TODO: Should already be computed in $item->params->get('access-view') - $user = $this->getCurrentUser(); - $groups = $user->getAuthorisedViewLevels(); - - foreach ($item as $itemElement) - { - if (!in_array($itemElement->access, $groups)) - { - unset($itemElement); - } - - // Prepare the data. - if (!empty($itemElement)) - { - $temp = new Registry($itemElement->params); - $itemElement->params = clone $params; - $itemElement->params->merge($temp); - $itemElement->params = (array) json_decode($itemElement->params); - $itemElement->metadata = new Registry($itemElement->metadata); - } - } - - if ($items !== false) - { - PluginHelper::importPlugin('content'); - - foreach ($items as $itemElement) - { - $itemElement->event = new \stdClass; - - // For some plugins. - !empty($itemElement->core_body) ? $itemElement->text = $itemElement->core_body : $itemElement->text = null; - - $itemElement->core_params = new Registry($itemElement->core_params); - - Factory::getApplication()->triggerEvent('onContentPrepare', ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0]); - - $results = Factory::getApplication()->triggerEvent('onContentAfterTitle', - ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0] - ); - $itemElement->event->afterDisplayTitle = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentBeforeDisplay', - ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0] - ); - $itemElement->event->beforeDisplayContent = trim(implode("\n", $results)); - - $results = Factory::getApplication()->triggerEvent('onContentAfterDisplay', - ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0] - ); - $itemElement->event->afterDisplayContent = trim(implode("\n", $results)); - - // Write the results back into the body - if (!empty($itemElement->core_body)) - { - $itemElement->core_body = $itemElement->text; - } - - // Categories store the images differently so lets re-map it so the display is correct - if ($itemElement->type_alias === 'com_content.category') - { - $itemElement->core_images = json_encode( - array( - 'image_intro' => $itemElement->core_params->get('image', ''), - 'image_intro_alt' => $itemElement->core_params->get('image_alt', '') - ) - ); - } - } - } - - $this->state = $state; - $this->items = $items; - $this->children = $children; - $this->parent = $parent; - $this->pagination = $pagination; - $this->user = $user; - $this->item = $item; - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - - // Merge tag params. If this is single-tag view, menu params override tag params - // Otherwise, article params override menu item params - $this->params = $this->state->get('params'); - $active = $app->getMenu()->getActive(); - $temp = clone $this->params; - - // Convert item params to a Registry object - $item[0]->params = new Registry($item[0]->params); - - // Check to see which parameters should take priority - if ($active) - { - $currentLink = $active->link; - - // If the current view is the active item and a tag view for one tag, then the menu item params take priority - if (strpos($currentLink, 'view=tag') && strpos($currentLink, '&id[0]=' . (string) $item[0]->id)) - { - // $item[0]->params are the tag params, $temp are the menu item params - // Merge so that the menu item params take priority - $item[0]->params->merge($temp); - - // Load layout from active query (in case it is an alternative menu item) - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - } - else - { - // Current menuitem is not a single tag view, so the tag params take priority. - // Merge the menu item params with the tag params so that the tag params take priority - $temp->merge($item[0]->params); - $item[0]->params = $temp; - - // Check for alternative layouts (since we are not in a single-article menu item) - // Single-article menu item layout takes priority over alt layout for an article - if ($layout = $item[0]->params->get('tag_layout')) - { - $this->setLayout($layout); - } - } - } - else - { - // Merge so that item params take priority - $temp->merge($item[0]->params); - $item[0]->params = $temp; - - // Check for alternative layouts (since we are not in a single-tag menu item) - // Single-tag menu item layout takes priority over alt layout for an article - if ($layout = $item[0]->params->get('tag_layout')) - { - $this->setLayout($layout); - } - } - - // Increment the hit counter - $model = $this->getModel(); - $model->hit(); - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - */ - protected function _prepareDocument() - { - $app = Factory::getApplication(); - $menu = $app->getMenu()->getActive(); - $this->tags_title = $this->getTagsTitle(); - $pathway = $app->getPathway(); - $title = ''; - - // Highest priority for "Browser Page Title". - if ($menu) - { - $title = $menu->getParams()->get('page_title', ''); - } - - if ($this->tags_title) - { - $this->params->def('page_heading', $this->tags_title); - $title = $title ?: $this->tags_title; - } - elseif ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - $title = $title ?: $this->params->get('page_title', $menu->title); - } - - $this->setDocumentTitle($title); - $pathway->addItem($title); - - foreach ($this->item as $itemElement) - { - if ($itemElement->metadesc) - { - $this->document->setDescription($itemElement->metadesc); - } - elseif ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } - - if (count($this->item) === 1) - { - foreach ($this->item[0]->metadata->toArray() as $k => $v) - { - if ($v) - { - $this->document->setMetaData($k, $v); - } - } - } - - if ($this->params->get('show_feed_link', 1) == 1) - { - $link = '&format=feed&limitstart='; - $attribs = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); - $this->document->addHeadLink(Route::_($link . '&type=rss'), 'alternate', 'rel', $attribs); - $attribs = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); - $this->document->addHeadLink(Route::_($link . '&type=atom'), 'alternate', 'rel', $attribs); - } - } - - /** - * Creates the tags title for the output - * - * @return string - * - * @since 3.1 - */ - protected function getTagsTitle() - { - $tags_title = array(); - - if (!empty($this->item)) - { - $user = $this->getCurrentUser(); - $groups = $user->getAuthorisedViewLevels(); - - foreach ($this->item as $item) - { - if (in_array($item->access, $groups)) - { - $tags_title[] = $item->title; - } - } - } - - return implode(' ', $tags_title); - } + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 3.1 + */ + protected $state; + + /** + * List of items associated with the tag + * + * @var \stdClass[]|false + * + * @since 3.1 + */ + protected $items; + + /** + * Tag data for the current tag or tags (on success, false on failure) + * + * @var \Joomla\CMS\Object\CMSObject|boolean + * + * @since 3.1 + */ + protected $item; + + /** + * UNUSED + * + * @var null + * + * @since 3.1 + */ + protected $children; + + /** + * UNUSED + * + * @var null + * + * @since 3.1 + */ + protected $parent; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * + * @since 3.1 + */ + protected $pagination; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * + * @since 3.1 + */ + protected $params; + + /** + * Array of tags title + * + * @var array + * + * @since 3.1 + */ + protected $tags_title; + + /** + * The page class suffix + * + * @var string + * + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The logged in user + * + * @var User|null + * + * @since 4.0.0 + */ + protected $user = null; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 3.1 + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $params = $app->getParams(); + + // Get some data from the models + $state = $this->get('State'); + $items = $this->get('Items'); + $item = $this->get('Item'); + $children = $this->get('Children'); + $parent = $this->get('Parent'); + $pagination = $this->get('Pagination'); + + // Flag indicates to not add limitstart=0 to URL + $pagination->hideEmptyLimitstart = true; + + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check whether access level allows access. + // @TODO: Should already be computed in $item->params->get('access-view') + $user = $this->getCurrentUser(); + $groups = $user->getAuthorisedViewLevels(); + + foreach ($item as $itemElement) { + if (!in_array($itemElement->access, $groups)) { + unset($itemElement); + } + + // Prepare the data. + if (!empty($itemElement)) { + $temp = new Registry($itemElement->params); + $itemElement->params = clone $params; + $itemElement->params->merge($temp); + $itemElement->params = (array) json_decode($itemElement->params); + $itemElement->metadata = new Registry($itemElement->metadata); + } + } + + if ($items !== false) { + PluginHelper::importPlugin('content'); + + foreach ($items as $itemElement) { + $itemElement->event = new \stdClass(); + + // For some plugins. + !empty($itemElement->core_body) ? $itemElement->text = $itemElement->core_body : $itemElement->text = null; + + $itemElement->core_params = new Registry($itemElement->core_params); + + Factory::getApplication()->triggerEvent('onContentPrepare', ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0]); + + $results = Factory::getApplication()->triggerEvent( + 'onContentAfterTitle', + ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0] + ); + $itemElement->event->afterDisplayTitle = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent( + 'onContentBeforeDisplay', + ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0] + ); + $itemElement->event->beforeDisplayContent = trim(implode("\n", $results)); + + $results = Factory::getApplication()->triggerEvent( + 'onContentAfterDisplay', + ['com_tags.tag', &$itemElement, &$itemElement->core_params, 0] + ); + $itemElement->event->afterDisplayContent = trim(implode("\n", $results)); + + // Write the results back into the body + if (!empty($itemElement->core_body)) { + $itemElement->core_body = $itemElement->text; + } + + // Categories store the images differently so lets re-map it so the display is correct + if ($itemElement->type_alias === 'com_content.category') { + $itemElement->core_images = json_encode( + array( + 'image_intro' => $itemElement->core_params->get('image', ''), + 'image_intro_alt' => $itemElement->core_params->get('image_alt', '') + ) + ); + } + } + } + + $this->state = $state; + $this->items = $items; + $this->children = $children; + $this->parent = $parent; + $this->pagination = $pagination; + $this->user = $user; + $this->item = $item; + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + + // Merge tag params. If this is single-tag view, menu params override tag params + // Otherwise, article params override menu item params + $this->params = $this->state->get('params'); + $active = $app->getMenu()->getActive(); + $temp = clone $this->params; + + // Convert item params to a Registry object + $item[0]->params = new Registry($item[0]->params); + + // Check to see which parameters should take priority + if ($active) { + $currentLink = $active->link; + + // If the current view is the active item and a tag view for one tag, then the menu item params take priority + if (strpos($currentLink, 'view=tag') && strpos($currentLink, '&id[0]=' . (string) $item[0]->id)) { + // $item[0]->params are the tag params, $temp are the menu item params + // Merge so that the menu item params take priority + $item[0]->params->merge($temp); + + // Load layout from active query (in case it is an alternative menu item) + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } + } else { + // Current menuitem is not a single tag view, so the tag params take priority. + // Merge the menu item params with the tag params so that the tag params take priority + $temp->merge($item[0]->params); + $item[0]->params = $temp; + + // Check for alternative layouts (since we are not in a single-article menu item) + // Single-article menu item layout takes priority over alt layout for an article + if ($layout = $item[0]->params->get('tag_layout')) { + $this->setLayout($layout); + } + } + } else { + // Merge so that item params take priority + $temp->merge($item[0]->params); + $item[0]->params = $temp; + + // Check for alternative layouts (since we are not in a single-tag menu item) + // Single-tag menu item layout takes priority over alt layout for an article + if ($layout = $item[0]->params->get('tag_layout')) { + $this->setLayout($layout); + } + } + + // Increment the hit counter + $model = $this->getModel(); + $model->hit(); + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + */ + protected function _prepareDocument() + { + $app = Factory::getApplication(); + $menu = $app->getMenu()->getActive(); + $this->tags_title = $this->getTagsTitle(); + $pathway = $app->getPathway(); + $title = ''; + + // Highest priority for "Browser Page Title". + if ($menu) { + $title = $menu->getParams()->get('page_title', ''); + } + + if ($this->tags_title) { + $this->params->def('page_heading', $this->tags_title); + $title = $title ?: $this->tags_title; + } elseif ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + $title = $title ?: $this->params->get('page_title', $menu->title); + } + + $this->setDocumentTitle($title); + $pathway->addItem($title); + + foreach ($this->item as $itemElement) { + if ($itemElement->metadesc) { + $this->document->setDescription($itemElement->metadesc); + } elseif ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } + + if (count($this->item) === 1) { + foreach ($this->item[0]->metadata->toArray() as $k => $v) { + if ($v) { + $this->document->setMetaData($k, $v); + } + } + } + + if ($this->params->get('show_feed_link', 1) == 1) { + $link = '&format=feed&limitstart='; + $attribs = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); + $this->document->addHeadLink(Route::_($link . '&type=rss'), 'alternate', 'rel', $attribs); + $attribs = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); + $this->document->addHeadLink(Route::_($link . '&type=atom'), 'alternate', 'rel', $attribs); + } + } + + /** + * Creates the tags title for the output + * + * @return string + * + * @since 3.1 + */ + protected function getTagsTitle() + { + $tags_title = array(); + + if (!empty($this->item)) { + $user = $this->getCurrentUser(); + $groups = $user->getAuthorisedViewLevels(); + + foreach ($this->item as $item) { + if (in_array($item->access, $groups)) { + $tags_title[] = $item->title; + } + } + } + + return implode(' ', $tags_title); + } } diff --git a/components/com_tags/src/View/Tags/FeedView.php b/components/com_tags/src/View/Tags/FeedView.php index 48fdbb3866b89..8992f9e27760b 100644 --- a/components/com_tags/src/View/Tags/FeedView.php +++ b/components/com_tags/src/View/Tags/FeedView.php @@ -1,4 +1,5 @@ document->link = Route::_('index.php?option=com_tags&view=tags'); + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + */ + public function display($tpl = null) + { + $app = Factory::getApplication(); + $this->document->link = Route::_('index.php?option=com_tags&view=tags'); - $app->input->set('limit', $app->get('feed_limit')); - $siteEmail = $app->get('mailfrom'); - $fromName = $app->get('fromname'); - $feedEmail = $app->get('feed_email', 'none'); + $app->input->set('limit', $app->get('feed_limit')); + $siteEmail = $app->get('mailfrom'); + $fromName = $app->get('fromname'); + $feedEmail = $app->get('feed_email', 'none'); - $this->document->editor = $fromName; + $this->document->editor = $fromName; - if ($feedEmail !== 'none') - { - $this->document->editorEmail = $siteEmail; - } + if ($feedEmail !== 'none') { + $this->document->editorEmail = $siteEmail; + } - // Get some data from the model - $items = $this->get('Items'); + // Get some data from the model + $items = $this->get('Items'); - foreach ($items as $item) - { - // Strip HTML from feed item title - $title = $this->escape($item->title); - $title = html_entity_decode($title, ENT_COMPAT, 'UTF-8'); + foreach ($items as $item) { + // Strip HTML from feed item title + $title = $this->escape($item->title); + $title = html_entity_decode($title, ENT_COMPAT, 'UTF-8'); - // Strip HTML from feed item description text - $description = $item->description; - $author = $item->created_by_alias ?: $item->created_by_user_name; - $date = $item->created_time ? date('r', strtotime($item->created_time)) : ''; + // Strip HTML from feed item description text + $description = $item->description; + $author = $item->created_by_alias ?: $item->created_by_user_name; + $date = $item->created_time ? date('r', strtotime($item->created_time)) : ''; - // Load individual item creator class - $feeditem = new FeedItem; - $feeditem->title = $title; - $feeditem->link = '/index.php?option=com_tags&view=tag&id=' . (int) $item->id; - $feeditem->description = $description; - $feeditem->date = $date; - $feeditem->category = 'All Tags'; - $feeditem->author = $author; + // Load individual item creator class + $feeditem = new FeedItem(); + $feeditem->title = $title; + $feeditem->link = '/index.php?option=com_tags&view=tag&id=' . (int) $item->id; + $feeditem->description = $description; + $feeditem->date = $date; + $feeditem->category = 'All Tags'; + $feeditem->author = $author; - if ($feedEmail === 'site') - { - $feeditem->authorEmail = $siteEmail; - } + if ($feedEmail === 'site') { + $feeditem->authorEmail = $siteEmail; + } - if ($feedEmail === 'author') - { - $feeditem->authorEmail = $item->email; - } + if ($feedEmail === 'author') { + $feeditem->authorEmail = $item->email; + } - // Loads item info into RSS array - $this->document->addItem($feeditem); - } - } + // Loads item info into RSS array + $this->document->addItem($feeditem); + } + } } diff --git a/components/com_tags/src/View/Tags/HtmlView.php b/components/com_tags/src/View/Tags/HtmlView.php index 4797969d18291..8efd823e1ca95 100644 --- a/components/com_tags/src/View/Tags/HtmlView.php +++ b/components/com_tags/src/View/Tags/HtmlView.php @@ -1,4 +1,5 @@ state = $this->get('State'); - $this->items = $this->get('Items'); - $this->pagination = $this->get('Pagination'); - $this->params = $this->state->get('params'); - $this->user = $this->getCurrentUser(); - - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Flag indicates to not add limitstart=0 to URL - $this->pagination->hideEmptyLimitstart = true; - - if (!empty($this->items)) - { - foreach ($this->items as $itemElement) - { - // Prepare the data. - $temp = new Registry($itemElement->params); - $itemElement->params = clone $this->params; - $itemElement->params->merge($temp); - $itemElement->params = (array) json_decode($itemElement->params); - } - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); - - $active = Factory::getApplication()->getMenu()->getActive(); - - // Load layout from active query (in case it is an alternative menu item) - if ($active && isset($active->query['option']) && $active->query['option'] === 'com_tags' && $active->query['view'] === 'tags') - { - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - } - else - { - // Load default All Tags layout from component - if ($layout = $this->params->get('tags_layout')) - { - $this->setLayout($layout); - } - } - - $this->_prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - */ - protected function _prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_TAGS_DEFAULT_PAGE_TITLE')); - } - - // Set metadata for all tags menu item - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - - // Respect configuration Sitename Before/After for TITLE in views All Tags. - $this->setDocumentTitle($this->document->getTitle()); - - // Add alternative feed link - if ($this->params->get('show_feed_link', 1) == 1) - { - $link = '&format=feed&limitstart='; - $attribs = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); - $this->document->addHeadLink(Route::_($link . '&type=rss'), 'alternate', 'rel', $attribs); - $attribs = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); - $this->document->addHeadLink(Route::_($link . '&type=atom'), 'alternate', 'rel', $attribs); - } - } + /** + * The model state + * + * @var \Joomla\CMS\Object\CMSObject + * + * @since 3.1 + */ + protected $state; + + /** + * The list of tags + * + * @var array|false + * @since 3.1 + */ + protected $items; + + /** + * The pagination object + * + * @var \Joomla\CMS\Pagination\Pagination + * @since 3.1 + */ + protected $pagination; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * @since 3.1 + */ + protected $params = null; + + /** + * The page class suffix + * + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The logged in user + * + * @var \Joomla\CMS\User\User|null + * @since 4.0.0 + */ + protected $user = null; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return mixed A string if successful, otherwise an Error object. + */ + public function display($tpl = null) + { + // Get some data from the models + $this->state = $this->get('State'); + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->params = $this->state->get('params'); + $this->user = $this->getCurrentUser(); + + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Flag indicates to not add limitstart=0 to URL + $this->pagination->hideEmptyLimitstart = true; + + if (!empty($this->items)) { + foreach ($this->items as $itemElement) { + // Prepare the data. + $temp = new Registry($itemElement->params); + $itemElement->params = clone $this->params; + $itemElement->params->merge($temp); + $itemElement->params = (array) json_decode($itemElement->params); + } + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); + + $active = Factory::getApplication()->getMenu()->getActive(); + + // Load layout from active query (in case it is an alternative menu item) + if ($active && isset($active->query['option']) && $active->query['option'] === 'com_tags' && $active->query['view'] === 'tags') { + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } + } else { + // Load default All Tags layout from component + if ($layout = $this->params->get('tags_layout')) { + $this->setLayout($layout); + } + } + + $this->_prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + */ + protected function _prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_TAGS_DEFAULT_PAGE_TITLE')); + } + + // Set metadata for all tags menu item + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + + // Respect configuration Sitename Before/After for TITLE in views All Tags. + $this->setDocumentTitle($this->document->getTitle()); + + // Add alternative feed link + if ($this->params->get('show_feed_link', 1) == 1) { + $link = '&format=feed&limitstart='; + $attribs = array('type' => 'application/rss+xml', 'title' => 'RSS 2.0'); + $this->document->addHeadLink(Route::_($link . '&type=rss'), 'alternate', 'rel', $attribs); + $attribs = array('type' => 'application/atom+xml', 'title' => 'Atom 1.0'); + $this->document->addHeadLink(Route::_($link . '&type=atom'), 'alternate', 'rel', $attribs); + } + } } diff --git a/components/com_tags/tmpl/tag/default.php b/components/com_tags/tmpl/tag/default.php index 5e17225a6ed76..fabeaa5114f56 100644 --- a/components/com_tags/tmpl/tag/default.php +++ b/components/com_tags/tmpl/tag/default.php @@ -1,4 +1,5 @@ - params->get('show_page_heading')) : ?> -

    - escape($this->params->get('page_heading')); ?> -

    - + params->get('show_page_heading')) : ?> +

    + escape($this->params->get('page_heading')); ?> +

    + - params->get('show_tag_title', 1)) : ?> - <> - tags_title, '', 'com_tag.tag'); ?> - > - + params->get('show_tag_title', 1)) : ?> + <> + tags_title, '', 'com_tag.tag'); ?> + > + - - item) === 1 && ($this->params->get('tag_list_show_tag_image', 1) || $this->params->get('tag_list_show_tag_description', 1))) : ?> -
    - item[0]->images); ?> - params->get('tag_list_show_tag_image', 1) == 1 && !empty($images->image_fulltext)) : ?> - image_fulltext, $images->image_fulltext_alt); ?> - - params->get('tag_list_show_tag_description') == 1 && $this->item[0]->description) : ?> - item[0]->description, '', 'com_tags.tag'); ?> - -
    - + + item) === 1 && ($this->params->get('tag_list_show_tag_image', 1) || $this->params->get('tag_list_show_tag_description', 1))) : ?> +
    + item[0]->images); ?> + params->get('tag_list_show_tag_image', 1) == 1 && !empty($images->image_fulltext)) : ?> + image_fulltext, $images->image_fulltext_alt); ?> + + params->get('tag_list_show_tag_description') == 1 && $this->item[0]->description) : ?> + item[0]->description, '', 'com_tags.tag'); ?> + +
    + - - params->get('tag_list_show_tag_description', 1) || $this->params->get('show_description_image', 1)) : ?> - params->get('show_description_image', 1) == 1 && $this->params->get('tag_list_image')) : ?> - params->get('tag_list_image'), empty($this->params->get('tag_list_image_alt')) && empty($this->params->get('tag_list_image_alt_empty')) ? false : $this->params->get('tag_list_image_alt')); ?> - - params->get('tag_list_description', '') > '') : ?> - params->get('tag_list_description'), '', 'com_tags.tag'); ?> - - - loadTemplate('items'); ?> + + params->get('tag_list_show_tag_description', 1) || $this->params->get('show_description_image', 1)) : ?> + params->get('show_description_image', 1) == 1 && $this->params->get('tag_list_image')) : ?> + params->get('tag_list_image'), empty($this->params->get('tag_list_image_alt')) && empty($this->params->get('tag_list_image_alt_empty')) ? false : $this->params->get('tag_list_image_alt')); ?> + + params->get('tag_list_description', '') > '') : ?> + params->get('tag_list_description'), '', 'com_tags.tag'); ?> + + + loadTemplate('items'); ?> - params->def('show_pagination', 1) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> -
    - params->def('show_pagination_results', 1)) : ?> -

    - pagination->getPagesCounter(); ?> -

    - - pagination->getPagesLinks(); ?> -
    - + params->def('show_pagination', 1) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> +
    + params->def('show_pagination_results', 1)) : ?> +

    + pagination->getPagesCounter(); ?> +

    + + pagination->getPagesLinks(); ?> +
    + diff --git a/components/com_tags/tmpl/tag/default_items.php b/components/com_tags/tmpl/tag/default_items.php index ce51281cc7649..62e5185845fa2 100644 --- a/components/com_tags/tmpl/tag/default_items.php +++ b/components/com_tags/tmpl/tag/default_items.php @@ -1,4 +1,5 @@ authorise('core.edit.state', 'com_tags'); ?>
    -
    - params->get('filter_field') || $this->params->get('show_pagination_limit')) : ?> - params->get('filter_field')) : ?> -
    - - - - -
    - - params->get('show_pagination_limit')) : ?> -
    - - pagination->getLimitBox(); ?> -
    - + + params->get('filter_field') || $this->params->get('show_pagination_limit')) : ?> + params->get('filter_field')) : ?> +
    + + + + +
    + + params->get('show_pagination_limit')) : ?> +
    + + pagination->getLimitBox(); ?> +
    + - - - -
    + + + + - items)) : ?> -
    - - -
    - -
      - items as $i => $item) : ?> - core_state == 0) : ?> -
    • - -
    • - - type_alias === 'com_users.category') || ($item->type_alias === 'com_banners.category')) : ?> -

      - escape($item->core_title); ?> -

      - -

      - - escape($item->core_title); ?> - -

      - - - event->afterDisplayTitle; ?> - core_images); ?> - params->get('tag_list_show_item_image', 1) == 1 && !empty($images->image_intro)) : ?> - - image_intro, $images->image_intro_alt); ?> - - - params->get('tag_list_show_item_description', 1)) : ?> - - event->beforeDisplayContent; ?> - - core_body, $this->params->get('tag_list_item_maximum_characters')); ?> - - - event->afterDisplayContent; ?> - -
    • - -
    - + items)) : ?> +
    + + +
    + +
      + items as $i => $item) : ?> + core_state == 0) : ?> +
    • + +
    • + + type_alias === 'com_users.category') || ($item->type_alias === 'com_banners.category')) : ?> +

      + escape($item->core_title); ?> +

      + +

      + + escape($item->core_title); ?> + +

      + + + event->afterDisplayTitle; ?> + core_images); ?> + params->get('tag_list_show_item_image', 1) == 1 && !empty($images->image_intro)) : ?> + + image_intro, $images->image_intro_alt); ?> + + + params->get('tag_list_show_item_description', 1)) : ?> + + event->beforeDisplayContent; ?> + + core_body, $this->params->get('tag_list_item_maximum_characters')); ?> + + + event->afterDisplayContent; ?> + +
    • + +
    +
    diff --git a/components/com_tags/tmpl/tag/list.php b/components/com_tags/tmpl/tag/list.php index 05bbafc2a2394..c60afcfaa107b 100644 --- a/components/com_tags/tmpl/tag/list.php +++ b/components/com_tags/tmpl/tag/list.php @@ -1,4 +1,5 @@ - params->get('show_page_heading')) : ?> -

    - escape($this->params->get('page_heading')); ?> -

    - - - params->get('show_tag_title', 1)) : ?> - <> - tags_title, '', 'com_tag.tag'); ?> - > - - - - item) === 1 && ($this->params->get('tag_list_show_tag_image', 1) || $this->params->get('tag_list_show_tag_description', 1))) : ?> -
    - item[0]->images); ?> - params->get('tag_list_show_tag_image', 1) == 1 && !empty($images->image_fulltext)) : ?> - image_fulltext, ''); ?> - - params->get('tag_list_show_tag_description') == 1 && $this->item[0]->description) : ?> - item[0]->description, '', 'com_tags.tag'); ?> - -
    - - - - params->get('tag_list_show_tag_description', 1) || $this->params->get('show_description_image', 1)) : ?> - params->get('show_description_image', 1) == 1 && $this->params->get('tag_list_image')) : ?> - params->get('tag_list_image'), empty($this->params->get('tag_list_image_alt')) && empty($this->params->get('tag_list_image_alt_empty')) ? false : $this->params->get('tag_list_image_alt')); ?> - - params->get('tag_list_description', '') > '') : ?> - params->get('tag_list_description'), '', 'com_tags.tag'); ?> - - - loadTemplate('items'); ?> + params->get('show_page_heading')) : ?> +

    + escape($this->params->get('page_heading')); ?> +

    + + + params->get('show_tag_title', 1)) : ?> + <> + tags_title, '', 'com_tag.tag'); ?> + > + + + + item) === 1 && ($this->params->get('tag_list_show_tag_image', 1) || $this->params->get('tag_list_show_tag_description', 1))) : ?> +
    + item[0]->images); ?> + params->get('tag_list_show_tag_image', 1) == 1 && !empty($images->image_fulltext)) : ?> + image_fulltext, ''); ?> + + params->get('tag_list_show_tag_description') == 1 && $this->item[0]->description) : ?> + item[0]->description, '', 'com_tags.tag'); ?> + +
    + + + + params->get('tag_list_show_tag_description', 1) || $this->params->get('show_description_image', 1)) : ?> + params->get('show_description_image', 1) == 1 && $this->params->get('tag_list_image')) : ?> + params->get('tag_list_image'), empty($this->params->get('tag_list_image_alt')) && empty($this->params->get('tag_list_image_alt_empty')) ? false : $this->params->get('tag_list_image_alt')); ?> + + params->get('tag_list_description', '') > '') : ?> + params->get('tag_list_description'), '', 'com_tags.tag'); ?> + + + loadTemplate('items'); ?> diff --git a/components/com_tags/tmpl/tag/list_items.php b/components/com_tags/tmpl/tag/list_items.php index 3d84e9fb793db..777091a953cad 100644 --- a/components/com_tags/tmpl/tag/list_items.php +++ b/components/com_tags/tmpl/tag/list_items.php @@ -1,4 +1,5 @@ escape($this->state->get('list.direction')); ?>
    -
    - params->get('filter_field')) : ?> -
    - - - - -
    - - params->get('show_pagination_limit')) : ?> -
    - - pagination->getLimitBox(); ?> -
    - + + params->get('filter_field')) : ?> +
    + + + + +
    + + params->get('show_pagination_limit')) : ?> +
    + + pagination->getLimitBox(); ?> +
    + - items)) : ?> -
    - - -
    - - - params->get('show_headings')) : ?> - - - - params->get('tag_list_show_date')) : ?> - - - - - - - items as $i => $item) : ?> - core_state == 0) : ?> - - - - - - params->get('tag_list_show_date')) : ?> - - - - - -
    - - - - - - - - - -
    - type_alias === 'com_users.category') || ($item->type_alias === 'com_banners.category')) : ?> - escape($item->core_title); ?> - - - escape($item->core_title); ?> - - - core_state == 0) : ?> - - - - - - displayDate, - $this->escape($this->params->get('date_format', Text::_('DATE_FORMAT_LC3'))) - ); ?> -
    - + items)) : ?> +
    + + +
    + + + params->get('show_headings')) : ?> + + + + params->get('tag_list_show_date')) : ?> + + + + + + + items as $i => $item) : ?> + core_state == 0) : ?> + + + + + + params->get('tag_list_show_date')) : ?> + + + + + +
    + + + + + + + + + +
    + type_alias === 'com_users.category') || ($item->type_alias === 'com_banners.category')) : ?> + escape($item->core_title); ?> + + + escape($item->core_title); ?> + + + core_state == 0) : ?> + + + + + + displayDate, + $this->escape($this->params->get('date_format', Text::_('DATE_FORMAT_LC3'))) + ); ?> +
    + - - params->def('show_pagination', 2) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> -
    - params->def('show_pagination_results', 1)) : ?> -

    - pagination->getPagesCounter(); ?> -

    - - pagination->getPagesLinks(); ?> -
    - - - - - -
    + + params->def('show_pagination', 2) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> +
    + params->def('show_pagination_results', 1)) : ?> +

    + pagination->getPagesCounter(); ?> +

    + + pagination->getPagesLinks(); ?> +
    + + + + + +
    diff --git a/components/com_tags/tmpl/tags/default.php b/components/com_tags/tmpl/tags/default.php index fe98a64e1b1c6..c45171f41202f 100644 --- a/components/com_tags/tmpl/tags/default.php +++ b/components/com_tags/tmpl/tags/default.php @@ -1,4 +1,5 @@ params->get('all_tags_description_image'); ?>
    - params->get('show_page_heading')) : ?> -

    - escape($this->params->get('page_heading')); ?> -

    - - params->get('all_tags_show_description_image') && !empty($descriptionImage)) : ?> -
    - params->get('all_tags_description_image_alt')) && empty($this->params->get('all_tags_description_image_alt_empty')) ? false : $this->params->get('all_tags_description_image_alt')); ?> -
    - - -
    - -
    - - loadTemplate('items'); ?> + params->get('show_page_heading')) : ?> +

    + escape($this->params->get('page_heading')); ?> +

    + + params->get('all_tags_show_description_image') && !empty($descriptionImage)) : ?> +
    + params->get('all_tags_description_image_alt')) && empty($this->params->get('all_tags_description_image_alt_empty')) ? false : $this->params->get('all_tags_description_image_alt')); ?> +
    + + +
    + +
    + + loadTemplate('items'); ?>
    diff --git a/components/com_tags/tmpl/tags/default_items.php b/components/com_tags/tmpl/tags/default_items.php index a54d944d9cd54..4d115e68e2af4 100644 --- a/components/com_tags/tmpl/tags/default_items.php +++ b/components/com_tags/tmpl/tags/default_items.php @@ -1,4 +1,5 @@ params->get('tag_columns', 1); // Avoid division by 0 and negative columns. -if ($columns < 1) -{ - $columns = 1; +if ($columns < 1) { + $columns = 1; } $bsspans = floor(12 / $columns); -if ($bsspans < 1) -{ - $bsspans = 1; +if ($bsspans < 1) { + $bsspans = 1; } $bscolumns = min($columns, floor(12 / $bsspans)); @@ -48,110 +47,110 @@ ?>
    -
    - params->get('filter_field') || $this->params->get('show_pagination_limit')) : ?> - params->get('filter_field')) : ?> -
    - - - - -
    - - params->get('show_pagination_limit')) : ?> -
    - - pagination->getLimitBox(); ?> -
    - - - - - -
    - - items == false || $n === 0) : ?> -
    - - -
    - - items as $i => $item) : ?> - -
      - - -
    • - access)) && in_array($item->access, $this->user->getAuthorisedViewLevels())) : ?> -

      - - escape($item->title); ?> - -

      - - - params->get('all_tags_show_tag_image') && !empty($item->images)) : ?> - images); ?> - - image_intro)) : ?> - float_intro) ? $this->params->get('float_intro') : $images->float_intro; ?> -
      - - image_intro_caption) : ?> - image_intro_caption; ?> - - - image_intro, $images->image_intro_alt, $imageOptions); ?> -
      - -
      - - - params->get('all_tags_show_tag_description', 1) && !empty($item->description)) || $this->params->get('all_tags_show_tag_hits')) : ?> -
      - params->get('all_tags_show_tag_description', 1) && !empty($item->description)) : ?> - - description, $this->params->get('all_tags_tag_maximum_characters')); ?> - - - params->get('all_tags_show_tag_hits')) : ?> - - hits); ?> - - -
      - -
    • - - -
    - - - - - - - items)) : ?> - params->def('show_pagination', 2) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> -
    - params->def('show_pagination_results', 1)) : ?> -

    - pagination->getPagesCounter(); ?> -

    - - pagination->getPagesLinks(); ?> -
    - - +
    + params->get('filter_field') || $this->params->get('show_pagination_limit')) : ?> + params->get('filter_field')) : ?> +
    + + + + +
    + + params->get('show_pagination_limit')) : ?> +
    + + pagination->getLimitBox(); ?> +
    + + + + + +
    + + items == false || $n === 0) : ?> +
    + + +
    + + items as $i => $item) : ?> + +
      + + +
    • + access)) && in_array($item->access, $this->user->getAuthorisedViewLevels())) : ?> +

      + + escape($item->title); ?> + +

      + + + params->get('all_tags_show_tag_image') && !empty($item->images)) : ?> + images); ?> + + image_intro)) : ?> + float_intro) ? $this->params->get('float_intro') : $images->float_intro; ?> +
      + + image_intro_caption) : ?> + image_intro_caption; ?> + + + image_intro, $images->image_intro_alt, $imageOptions); ?> +
      + +
      + + + params->get('all_tags_show_tag_description', 1) && !empty($item->description)) || $this->params->get('all_tags_show_tag_hits')) : ?> +
      + params->get('all_tags_show_tag_description', 1) && !empty($item->description)) : ?> + + description, $this->params->get('all_tags_tag_maximum_characters')); ?> + + + params->get('all_tags_show_tag_hits')) : ?> + + hits); ?> + + +
      + +
    • + + +
    + + + + + + + items)) : ?> + params->def('show_pagination', 2) == 1 || ($this->params->get('show_pagination') == 2)) && ($this->pagination->pagesTotal > 1)) : ?> +
    + params->def('show_pagination_results', 1)) : ?> +

    + pagination->getPagesCounter(); ?> +

    + + pagination->getPagesLinks(); ?> +
    + +
    diff --git a/components/com_users/src/Controller/CallbackController.php b/components/com_users/src/Controller/CallbackController.php index de8508032f249..39fb085c16be4 100644 --- a/components/com_users/src/Controller/CallbackController.php +++ b/components/com_users/src/Controller/CallbackController.php @@ -1,4 +1,5 @@ getCode() !== 403) - { - throw $e; - } + /** + * Execute a task by triggering a Method in the derived class. + * + * @param string $task The task to perform. + * + * @return mixed The value returned by the called Method. + * + * @throws \Exception + * @since 4.2.0 + */ + public function execute($task) + { + try { + return parent::execute($task); + } catch (\Exception $e) { + if ($e->getCode() !== 403) { + throw $e; + } - if ($this->app->getIdentity()->guest) - { - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + if ($this->app->getIdentity()->guest) { + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - return null; - } - } + return null; + } + } - return null; - } + return null; + } } diff --git a/components/com_users/src/Controller/DisplayController.php b/components/com_users/src/Controller/DisplayController.php index d5ed593109557..ab6574dd5122f 100644 --- a/components/com_users/src/Controller/DisplayController.php +++ b/components/com_users/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ app->getDocument(); - - // Set the default view name and format from the Request. - $vName = $this->input->getCmd('view', 'login'); - $vFormat = $document->getType(); - $lName = $this->input->getCmd('layout', 'default'); - - if ($view = $this->getView($vName, $vFormat)) - { - // Do any specific processing by view. - switch ($vName) - { - case 'registration': - // If the user is already logged in, redirect to the profile page. - $user = $this->app->getIdentity(); - - if ($user->get('guest') != 1) - { - // Redirect to profile page. - $this->setRedirect(Route::_('index.php?option=com_users&view=profile', false)); - - return; - } - - // Check if user registration is enabled - if (ComponentHelper::getParams('com_users')->get('allowUserRegistration') == 0) - { - // Registration is disabled - Redirect to login page. - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - - return; - } - - // The user is a guest, load the registration model and show the registration page. - $model = $this->getModel('Registration'); - break; - - // Handle view specific models. - case 'profile': - - // If the user is a guest, redirect to the login page. - $user = $this->app->getIdentity(); - - if ($user->get('guest') == 1) - { - // Redirect to login page. - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - - return; - } - - $model = $this->getModel($vName); - break; - - // Handle the default views. - case 'login': - $model = $this->getModel($vName); - break; - - case 'remind': - case 'reset': - // If the user is already logged in, redirect to the profile page. - $user = $this->app->getIdentity(); - - if ($user->get('guest') != 1) - { - // Redirect to profile page. - $this->setRedirect(Route::_('index.php?option=com_users&view=profile', false)); - - return; - } - - $model = $this->getModel($vName); - break; - - case 'captive': - case 'methods': - case 'method': - $controller = $this->factory->createController($vName, 'Site', [], $this->app, $this->input); - $task = $this->input->get('task', ''); - - return $controller->execute($task); - - break; - - default: - $model = $this->getModel('Login'); - break; - } - - // Make sure we don't send a referer - if (in_array($vName, array('remind', 'reset'))) - { - $this->app->setHeader('Referrer-Policy', 'no-referrer', true); - } - - // Push the model into the view (as default). - $view->setModel($model, true); - $view->setLayout($lName); - - // Push document object into the view. - $view->document = $document; - - $view->display(); - } - } + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached + * @param array|boolean $urlparams An array of safe URL parameters and their variable types, + * for valid values see {@link \Joomla\CMS\Filter\InputFilter::clean()}. + * + * @return void + * + * @since 1.5 + * @throws \Exception + */ + public function display($cachable = false, $urlparams = false) + { + // Get the document object. + $document = $this->app->getDocument(); + + // Set the default view name and format from the Request. + $vName = $this->input->getCmd('view', 'login'); + $vFormat = $document->getType(); + $lName = $this->input->getCmd('layout', 'default'); + + if ($view = $this->getView($vName, $vFormat)) { + // Do any specific processing by view. + switch ($vName) { + case 'registration': + // If the user is already logged in, redirect to the profile page. + $user = $this->app->getIdentity(); + + if ($user->get('guest') != 1) { + // Redirect to profile page. + $this->setRedirect(Route::_('index.php?option=com_users&view=profile', false)); + + return; + } + + // Check if user registration is enabled + if (ComponentHelper::getParams('com_users')->get('allowUserRegistration') == 0) { + // Registration is disabled - Redirect to login page. + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + + return; + } + + // The user is a guest, load the registration model and show the registration page. + $model = $this->getModel('Registration'); + break; + + // Handle view specific models. + case 'profile': + // If the user is a guest, redirect to the login page. + $user = $this->app->getIdentity(); + + if ($user->get('guest') == 1) { + // Redirect to login page. + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + + return; + } + + $model = $this->getModel($vName); + break; + + // Handle the default views. + case 'login': + $model = $this->getModel($vName); + break; + + case 'remind': + case 'reset': + // If the user is already logged in, redirect to the profile page. + $user = $this->app->getIdentity(); + + if ($user->get('guest') != 1) { + // Redirect to profile page. + $this->setRedirect(Route::_('index.php?option=com_users&view=profile', false)); + + return; + } + + $model = $this->getModel($vName); + break; + + case 'captive': + case 'methods': + case 'method': + $controller = $this->factory->createController($vName, 'Site', [], $this->app, $this->input); + $task = $this->input->get('task', ''); + + return $controller->execute($task); + + break; + + default: + $model = $this->getModel('Login'); + break; + } + + // Make sure we don't send a referer + if (in_array($vName, array('remind', 'reset'))) { + $this->app->setHeader('Referrer-Policy', 'no-referrer', true); + } + + // Push the model into the view (as default). + $view->setModel($model, true); + $view->setLayout($lName); + + // Push document object into the view. + $view->document = $document; + + $view->display(); + } + } } diff --git a/components/com_users/src/Controller/MethodController.php b/components/com_users/src/Controller/MethodController.php index 59a0a7bee255e..282b2d7cb1d8c 100644 --- a/components/com_users/src/Controller/MethodController.php +++ b/components/com_users/src/Controller/MethodController.php @@ -1,4 +1,5 @@ getCode() !== 403) - { - throw $e; - } + /** + * Execute a task by triggering a Method in the derived class. + * + * @param string $task The task to perform. + * + * @return mixed The value returned by the called Method. + * + * @throws \Exception + * @since 4.2.0 + */ + public function execute($task) + { + try { + return parent::execute($task); + } catch (\Exception $e) { + if ($e->getCode() !== 403) { + throw $e; + } - if ($this->app->getIdentity()->guest) - { - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + if ($this->app->getIdentity()->guest) { + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - return null; - } - } + return null; + } + } - return null; - } + return null; + } } diff --git a/components/com_users/src/Controller/MethodsController.php b/components/com_users/src/Controller/MethodsController.php index 8e742c1534071..5f1cfbdef819b 100644 --- a/components/com_users/src/Controller/MethodsController.php +++ b/components/com_users/src/Controller/MethodsController.php @@ -1,4 +1,5 @@ getCode() !== 403) - { - throw $e; - } + /** + * Execute a task by triggering a Method in the derived class. + * + * @param string $task The task to perform. + * + * @return mixed The value returned by the called Method. + * + * @throws \Exception + * @since 4.2.0 + */ + public function execute($task) + { + try { + return parent::execute($task); + } catch (\Exception $e) { + if ($e->getCode() !== 403) { + throw $e; + } - if ($this->app->getIdentity()->guest) - { - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + if ($this->app->getIdentity()->guest) { + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - return null; - } - } + return null; + } + } - return null; - } + return null; + } } diff --git a/components/com_users/src/Controller/ProfileController.php b/components/com_users/src/Controller/ProfileController.php index e53fd9ef1d5c8..25e87eb0d8058 100644 --- a/components/com_users/src/Controller/ProfileController.php +++ b/components/com_users/src/Controller/ProfileController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Users\Site\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Users\Site\Controller; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Controller\BaseController; use Joomla\CMS\Router\Route; use Joomla\CMS\Uri\Uri; - /** * Profile controller class for Users. * @@ -23,215 +22,201 @@ */ class ProfileController extends BaseController { - /** - * Method to check out a user for editing and redirect to the edit form. - * - * @return boolean - * - * @since 1.6 - */ - public function edit() - { - $app = $this->app; - $user = $this->app->getIdentity(); - $loginUserId = (int) $user->get('id'); - - // Get the current user id. - $userId = $this->input->getInt('user_id'); - - // Check if the user is trying to edit another users profile. - if ($userId != $loginUserId) - { - $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $app->setHeader('status', 403, true); - - return false; - } - - $cookieLogin = $user->get('cookieLogin'); - - // Check if the user logged in with a cookie - if (!empty($cookieLogin)) - { - // If so, the user must login to edit the password and other data. - $app->enqueueMessage(Text::_('JGLOBAL_REMEMBER_MUST_LOGIN'), 'message'); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - - return false; - } - - // Set the user id for the user to edit in the session. - $app->setUserState('com_users.edit.profile.id', $userId); - - // Redirect to the edit screen. - $this->setRedirect(Route::_('index.php?option=com_users&view=profile&layout=edit', false)); - - return true; - } - - /** - * Method to save a user's profile data. - * - * @return void|boolean - * - * @since 1.6 - * @throws \Exception - */ - public function save() - { - // Check for request forgeries. - $this->checkToken(); - - $app = $this->app; - - /** @var \Joomla\Component\Users\Site\Model\ProfileModel $model */ - $model = $this->getModel('Profile', 'Site'); - $user = $this->app->getIdentity(); - $userId = (int) $user->get('id'); - - // Get the user data. - $requestData = $app->input->post->get('jform', array(), 'array'); - - // Force the ID to this user. - $requestData['id'] = $userId; - - // Validate the posted data. - $form = $model->getForm(); - - if (!$form) - { - throw new \Exception($model->getError(), 500); - } - - // Send an object which can be modified through the plugin event - $objData = (object) $requestData; - $app->triggerEvent( - 'onContentNormaliseRequestData', - array('com_users.user', $objData, $form) - ); - $requestData = (array) $objData; - - // Validate the posted data. - $data = $model->validate($form, $requestData); - - // Check for errors. - if ($data === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $app->enqueueMessage($errors[$i]->getMessage(), 'warning'); - } - else - { - $app->enqueueMessage($errors[$i], 'warning'); - } - } - - // Unset the passwords. - unset($requestData['password1'], $requestData['password2']); - - // Save the data in the session. - $app->setUserState('com_users.edit.profile.data', $requestData); - - // Redirect back to the edit screen. - $userId = (int) $app->getUserState('com_users.edit.profile.id'); - $this->setRedirect(Route::_('index.php?option=com_users&view=profile&layout=edit&user_id=' . $userId, false)); - - return false; - } - - // Attempt to save the data. - $return = $model->save($data); - - // Check for errors. - if ($return === false) - { - // Save the data in the session. - $app->setUserState('com_users.edit.profile.data', $data); - - // Redirect back to the edit screen. - $userId = (int) $app->getUserState('com_users.edit.profile.id'); - $this->setMessage(Text::sprintf('COM_USERS_PROFILE_SAVE_FAILED', $model->getError()), 'warning'); - $this->setRedirect(Route::_('index.php?option=com_users&view=profile&layout=edit&user_id=' . $userId, false)); - - return false; - } - - // Redirect the user and adjust session state based on the chosen task. - switch ($this->getTask()) - { - case 'apply': - // Check out the profile. - $app->setUserState('com_users.edit.profile.id', $return); - - // Redirect back to the edit screen. - $this->setMessage(Text::_('COM_USERS_PROFILE_SAVE_SUCCESS')); - - $redirect = $app->getUserState('com_users.edit.profile.redirect'); - - // Don't redirect to an external URL. - if (!Uri::isInternal($redirect)) - { - $redirect = null; - } - - if (!$redirect) - { - $redirect = 'index.php?option=com_users&view=profile&layout=edit&hidemainmenu=1'; - } - - $this->setRedirect(Route::_($redirect, false)); - break; - - default: - // Clear the profile id from the session. - $app->setUserState('com_users.edit.profile.id', null); - - $redirect = $app->getUserState('com_users.edit.profile.redirect'); - - // Don't redirect to an external URL. - if (!Uri::isInternal($redirect)) - { - $redirect = null; - } - - if (!$redirect) - { - $redirect = 'index.php?option=com_users&view=profile&user_id=' . $return; - } - - // Redirect to the list screen. - $this->setMessage(Text::_('COM_USERS_PROFILE_SAVE_SUCCESS')); - $this->setRedirect(Route::_($redirect, false)); - break; - } - - // Flush the data from the session. - $app->setUserState('com_users.edit.profile.data', null); - } - - /** - * Method to cancel an edit. - * - * @return void - * - * @since 4.0.0 - */ - public function cancel() - { - // Check for request forgeries. - $this->checkToken(); - - // Flush the data from the session. - $this->app->setUserState('com_users.edit.profile', null); - - // Redirect to user profile. - $this->setRedirect(Route::_('index.php?option=com_users&view=profile', false)); - } + /** + * Method to check out a user for editing and redirect to the edit form. + * + * @return boolean + * + * @since 1.6 + */ + public function edit() + { + $app = $this->app; + $user = $this->app->getIdentity(); + $loginUserId = (int) $user->get('id'); + + // Get the current user id. + $userId = $this->input->getInt('user_id'); + + // Check if the user is trying to edit another users profile. + if ($userId != $loginUserId) { + $app->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $app->setHeader('status', 403, true); + + return false; + } + + $cookieLogin = $user->get('cookieLogin'); + + // Check if the user logged in with a cookie + if (!empty($cookieLogin)) { + // If so, the user must login to edit the password and other data. + $app->enqueueMessage(Text::_('JGLOBAL_REMEMBER_MUST_LOGIN'), 'message'); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + + return false; + } + + // Set the user id for the user to edit in the session. + $app->setUserState('com_users.edit.profile.id', $userId); + + // Redirect to the edit screen. + $this->setRedirect(Route::_('index.php?option=com_users&view=profile&layout=edit', false)); + + return true; + } + + /** + * Method to save a user's profile data. + * + * @return void|boolean + * + * @since 1.6 + * @throws \Exception + */ + public function save() + { + // Check for request forgeries. + $this->checkToken(); + + $app = $this->app; + + /** @var \Joomla\Component\Users\Site\Model\ProfileModel $model */ + $model = $this->getModel('Profile', 'Site'); + $user = $this->app->getIdentity(); + $userId = (int) $user->get('id'); + + // Get the user data. + $requestData = $app->input->post->get('jform', array(), 'array'); + + // Force the ID to this user. + $requestData['id'] = $userId; + + // Validate the posted data. + $form = $model->getForm(); + + if (!$form) { + throw new \Exception($model->getError(), 500); + } + + // Send an object which can be modified through the plugin event + $objData = (object) $requestData; + $app->triggerEvent( + 'onContentNormaliseRequestData', + array('com_users.user', $objData, $form) + ); + $requestData = (array) $objData; + + // Validate the posted data. + $data = $model->validate($form, $requestData); + + // Check for errors. + if ($data === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $app->enqueueMessage($errors[$i], 'warning'); + } + } + + // Unset the passwords. + unset($requestData['password1'], $requestData['password2']); + + // Save the data in the session. + $app->setUserState('com_users.edit.profile.data', $requestData); + + // Redirect back to the edit screen. + $userId = (int) $app->getUserState('com_users.edit.profile.id'); + $this->setRedirect(Route::_('index.php?option=com_users&view=profile&layout=edit&user_id=' . $userId, false)); + + return false; + } + + // Attempt to save the data. + $return = $model->save($data); + + // Check for errors. + if ($return === false) { + // Save the data in the session. + $app->setUserState('com_users.edit.profile.data', $data); + + // Redirect back to the edit screen. + $userId = (int) $app->getUserState('com_users.edit.profile.id'); + $this->setMessage(Text::sprintf('COM_USERS_PROFILE_SAVE_FAILED', $model->getError()), 'warning'); + $this->setRedirect(Route::_('index.php?option=com_users&view=profile&layout=edit&user_id=' . $userId, false)); + + return false; + } + + // Redirect the user and adjust session state based on the chosen task. + switch ($this->getTask()) { + case 'apply': + // Check out the profile. + $app->setUserState('com_users.edit.profile.id', $return); + + // Redirect back to the edit screen. + $this->setMessage(Text::_('COM_USERS_PROFILE_SAVE_SUCCESS')); + + $redirect = $app->getUserState('com_users.edit.profile.redirect'); + + // Don't redirect to an external URL. + if (!Uri::isInternal($redirect)) { + $redirect = null; + } + + if (!$redirect) { + $redirect = 'index.php?option=com_users&view=profile&layout=edit&hidemainmenu=1'; + } + + $this->setRedirect(Route::_($redirect, false)); + break; + + default: + // Clear the profile id from the session. + $app->setUserState('com_users.edit.profile.id', null); + + $redirect = $app->getUserState('com_users.edit.profile.redirect'); + + // Don't redirect to an external URL. + if (!Uri::isInternal($redirect)) { + $redirect = null; + } + + if (!$redirect) { + $redirect = 'index.php?option=com_users&view=profile&user_id=' . $return; + } + + // Redirect to the list screen. + $this->setMessage(Text::_('COM_USERS_PROFILE_SAVE_SUCCESS')); + $this->setRedirect(Route::_($redirect, false)); + break; + } + + // Flush the data from the session. + $app->setUserState('com_users.edit.profile.data', null); + } + + /** + * Method to cancel an edit. + * + * @return void + * + * @since 4.0.0 + */ + public function cancel() + { + // Check for request forgeries. + $this->checkToken(); + + // Flush the data from the session. + $this->app->setUserState('com_users.edit.profile', null); + + // Redirect to user profile. + $this->setRedirect(Route::_('index.php?option=com_users&view=profile', false)); + } } diff --git a/components/com_users/src/Controller/RegistrationController.php b/components/com_users/src/Controller/RegistrationController.php index 7328f0df01bac..51bb51c0bd3a7 100644 --- a/components/com_users/src/Controller/RegistrationController.php +++ b/components/com_users/src/Controller/RegistrationController.php @@ -1,4 +1,5 @@ app->getIdentity(); - $input = $this->input; - $uParams = ComponentHelper::getParams('com_users'); - - // Check for admin activation. Don't allow non-super-admin to delete a super admin - if ($uParams->get('useractivation') != 2 && $user->get('id')) - { - $this->setRedirect('index.php'); - - return true; - } - - // If user registration or account activation is disabled, throw a 403. - if ($uParams->get('useractivation') == 0 || $uParams->get('allowUserRegistration') == 0) - { - throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); - } - - /** @var \Joomla\Component\Users\Site\Model\RegistrationModel $model */ - $model = $this->getModel('Registration', 'Site'); - $token = $input->getAlnum('token'); - - // Check that the token is in a valid format. - if ($token === null || strlen($token) !== 32) - { - throw new \Exception(Text::_('JINVALID_TOKEN'), 403); - } - - // Get the User ID - $userIdToActivate = $model->getUserIdFromToken($token); - - if (!$userIdToActivate) - { - $this->setMessage(Text::_('COM_USERS_ACTIVATION_TOKEN_NOT_FOUND')); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - - return false; - } - - // Get the user we want to activate - $userToActivate = Factory::getUser($userIdToActivate); - - // Admin activation is on and admin is activating the account - if (($uParams->get('useractivation') == 2) && $userToActivate->getParam('activate', 0)) - { - // If a user admin is not logged in, redirect them to the login page with an error message - if (!$user->authorise('core.create', 'com_users') || !$user->authorise('core.manage', 'com_users')) - { - $activationUrl = 'index.php?option=com_users&task=registration.activate&token=' . $token; - $loginUrl = 'index.php?option=com_users&view=login&return=' . base64_encode($activationUrl); - - // In case we still run into this in the second step the user does not have the right permissions - $message = Text::_('COM_USERS_REGISTRATION_ACL_ADMIN_ACTIVATION_PERMISSIONS'); - - // When we are not logged in we should login - if ($user->guest) - { - $message = Text::_('COM_USERS_REGISTRATION_ACL_ADMIN_ACTIVATION'); - } - - $this->setMessage($message); - $this->setRedirect(Route::_($loginUrl, false)); - - return false; - } - } - - // Attempt to activate the user. - $return = $model->activate($token); - - // Check for errors. - if ($return === false) - { - // Redirect back to the home page. - $this->setMessage(Text::sprintf('COM_USERS_REGISTRATION_SAVE_FAILED', $model->getError()), 'error'); - $this->setRedirect('index.php'); - - return false; - } - - $useractivation = $uParams->get('useractivation'); - - // Redirect to the login screen. - if ($useractivation == 0) - { - $this->setMessage(Text::_('COM_USERS_REGISTRATION_SAVE_SUCCESS')); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - } - elseif ($useractivation == 1) - { - $this->setMessage(Text::_('COM_USERS_REGISTRATION_ACTIVATE_SUCCESS')); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - } - elseif ($return->getParam('activate')) - { - $this->setMessage(Text::_('COM_USERS_REGISTRATION_VERIFY_SUCCESS')); - $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); - } - else - { - $this->setMessage(Text::_('COM_USERS_REGISTRATION_ADMINACTIVATE_SUCCESS')); - $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); - } - - return true; - } - - /** - * Method to register a user. - * - * @return boolean True on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function register() - { - // Check for request forgeries. - $this->checkToken(); - - // If registration is disabled - Redirect to login page. - if (ComponentHelper::getParams('com_users')->get('allowUserRegistration') == 0) - { - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - - return false; - } - - $app = $this->app; - - /** @var \Joomla\Component\Users\Site\Model\RegistrationModel $model */ - $model = $this->getModel('Registration', 'Site'); - - // Get the user data. - $requestData = $this->input->post->get('jform', array(), 'array'); - - // Validate the posted data. - $form = $model->getForm(); - - if (!$form) - { - throw new \Exception($model->getError(), 500); - } - - $data = $model->validate($form, $requestData); - - // Check for validation errors. - if ($data === false) - { - // Get the validation messages. - $errors = $model->getErrors(); - - // Push up to three validation messages out to the user. - for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) - { - if ($errors[$i] instanceof \Exception) - { - $app->enqueueMessage($errors[$i]->getMessage(), 'error'); - } - else - { - $app->enqueueMessage($errors[$i], 'error'); - } - } - - // Save the data in the session. - $app->setUserState('com_users.registration.data', $requestData); - - // Redirect back to the registration screen. - $this->setRedirect(Route::_('index.php?option=com_users&view=registration', false)); - - return false; - } - - // Attempt to save the data. - $return = $model->register($data); - - // Check for errors. - if ($return === false) - { - // Save the data in the session. - $app->setUserState('com_users.registration.data', $data); - - // Redirect back to the edit screen. - $this->setMessage($model->getError(), 'error'); - $this->setRedirect(Route::_('index.php?option=com_users&view=registration', false)); - - return false; - } - - // Flush the data from the session. - $app->setUserState('com_users.registration.data', null); - - // Redirect to the profile screen. - if ($return === 'adminactivate') - { - $this->setMessage(Text::_('COM_USERS_REGISTRATION_COMPLETE_VERIFY')); - $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); - } - elseif ($return === 'useractivate') - { - $this->setMessage(Text::_('COM_USERS_REGISTRATION_COMPLETE_ACTIVATE')); - $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); - } - else - { - $this->setMessage(Text::_('COM_USERS_REGISTRATION_SAVE_SUCCESS')); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); - } - - return true; - } + /** + * Method to activate a user. + * + * @return boolean True on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function activate() + { + $user = $this->app->getIdentity(); + $input = $this->input; + $uParams = ComponentHelper::getParams('com_users'); + + // Check for admin activation. Don't allow non-super-admin to delete a super admin + if ($uParams->get('useractivation') != 2 && $user->get('id')) { + $this->setRedirect('index.php'); + + return true; + } + + // If user registration or account activation is disabled, throw a 403. + if ($uParams->get('useractivation') == 0 || $uParams->get('allowUserRegistration') == 0) { + throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403); + } + + /** @var \Joomla\Component\Users\Site\Model\RegistrationModel $model */ + $model = $this->getModel('Registration', 'Site'); + $token = $input->getAlnum('token'); + + // Check that the token is in a valid format. + if ($token === null || strlen($token) !== 32) { + throw new \Exception(Text::_('JINVALID_TOKEN'), 403); + } + + // Get the User ID + $userIdToActivate = $model->getUserIdFromToken($token); + + if (!$userIdToActivate) { + $this->setMessage(Text::_('COM_USERS_ACTIVATION_TOKEN_NOT_FOUND')); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + + return false; + } + + // Get the user we want to activate + $userToActivate = Factory::getUser($userIdToActivate); + + // Admin activation is on and admin is activating the account + if (($uParams->get('useractivation') == 2) && $userToActivate->getParam('activate', 0)) { + // If a user admin is not logged in, redirect them to the login page with an error message + if (!$user->authorise('core.create', 'com_users') || !$user->authorise('core.manage', 'com_users')) { + $activationUrl = 'index.php?option=com_users&task=registration.activate&token=' . $token; + $loginUrl = 'index.php?option=com_users&view=login&return=' . base64_encode($activationUrl); + + // In case we still run into this in the second step the user does not have the right permissions + $message = Text::_('COM_USERS_REGISTRATION_ACL_ADMIN_ACTIVATION_PERMISSIONS'); + + // When we are not logged in we should login + if ($user->guest) { + $message = Text::_('COM_USERS_REGISTRATION_ACL_ADMIN_ACTIVATION'); + } + + $this->setMessage($message); + $this->setRedirect(Route::_($loginUrl, false)); + + return false; + } + } + + // Attempt to activate the user. + $return = $model->activate($token); + + // Check for errors. + if ($return === false) { + // Redirect back to the home page. + $this->setMessage(Text::sprintf('COM_USERS_REGISTRATION_SAVE_FAILED', $model->getError()), 'error'); + $this->setRedirect('index.php'); + + return false; + } + + $useractivation = $uParams->get('useractivation'); + + // Redirect to the login screen. + if ($useractivation == 0) { + $this->setMessage(Text::_('COM_USERS_REGISTRATION_SAVE_SUCCESS')); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + } elseif ($useractivation == 1) { + $this->setMessage(Text::_('COM_USERS_REGISTRATION_ACTIVATE_SUCCESS')); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + } elseif ($return->getParam('activate')) { + $this->setMessage(Text::_('COM_USERS_REGISTRATION_VERIFY_SUCCESS')); + $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); + } else { + $this->setMessage(Text::_('COM_USERS_REGISTRATION_ADMINACTIVATE_SUCCESS')); + $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); + } + + return true; + } + + /** + * Method to register a user. + * + * @return boolean True on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function register() + { + // Check for request forgeries. + $this->checkToken(); + + // If registration is disabled - Redirect to login page. + if (ComponentHelper::getParams('com_users')->get('allowUserRegistration') == 0) { + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + + return false; + } + + $app = $this->app; + + /** @var \Joomla\Component\Users\Site\Model\RegistrationModel $model */ + $model = $this->getModel('Registration', 'Site'); + + // Get the user data. + $requestData = $this->input->post->get('jform', array(), 'array'); + + // Validate the posted data. + $form = $model->getForm(); + + if (!$form) { + throw new \Exception($model->getError(), 500); + } + + $data = $model->validate($form, $requestData); + + // Check for validation errors. + if ($data === false) { + // Get the validation messages. + $errors = $model->getErrors(); + + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof \Exception) { + $app->enqueueMessage($errors[$i]->getMessage(), 'error'); + } else { + $app->enqueueMessage($errors[$i], 'error'); + } + } + + // Save the data in the session. + $app->setUserState('com_users.registration.data', $requestData); + + // Redirect back to the registration screen. + $this->setRedirect(Route::_('index.php?option=com_users&view=registration', false)); + + return false; + } + + // Attempt to save the data. + $return = $model->register($data); + + // Check for errors. + if ($return === false) { + // Save the data in the session. + $app->setUserState('com_users.registration.data', $data); + + // Redirect back to the edit screen. + $this->setMessage($model->getError(), 'error'); + $this->setRedirect(Route::_('index.php?option=com_users&view=registration', false)); + + return false; + } + + // Flush the data from the session. + $app->setUserState('com_users.registration.data', null); + + // Redirect to the profile screen. + if ($return === 'adminactivate') { + $this->setMessage(Text::_('COM_USERS_REGISTRATION_COMPLETE_VERIFY')); + $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); + } elseif ($return === 'useractivate') { + $this->setMessage(Text::_('COM_USERS_REGISTRATION_COMPLETE_ACTIVATE')); + $this->setRedirect(Route::_('index.php?option=com_users&view=registration&layout=complete', false)); + } else { + $this->setMessage(Text::_('COM_USERS_REGISTRATION_SAVE_SUCCESS')); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false)); + } + + return true; + } } diff --git a/components/com_users/src/Controller/RemindController.php b/components/com_users/src/Controller/RemindController.php index f3d4aff83ceba..876de98296037 100644 --- a/components/com_users/src/Controller/RemindController.php +++ b/components/com_users/src/Controller/RemindController.php @@ -1,4 +1,5 @@ checkToken('post'); - - /** @var \Joomla\Component\Users\Site\Model\RemindModel $model */ - $model = $this->getModel('Remind', 'Site'); - $data = $this->input->post->get('jform', array(), 'array'); - - // Submit the password reset request. - $return = $model->processRemindRequest($data); - - // Check for a hard error. - if ($return == false && JDEBUG) - { - // The request failed. - // Go back to the request form. - $message = Text::sprintf('COM_USERS_REMIND_REQUEST_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_users&view=remind', false), $message, 'notice'); - - return false; - } - - // To not expose if the user exists or not we send a generic message. - $message = Text::_('COM_USERS_REMIND_REQUEST'); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false), $message, 'notice'); - - return true; - } + /** + * Method to request a username reminder. + * + * @return boolean + * + * @since 1.6 + */ + public function remind() + { + // Check the request token. + $this->checkToken('post'); + + /** @var \Joomla\Component\Users\Site\Model\RemindModel $model */ + $model = $this->getModel('Remind', 'Site'); + $data = $this->input->post->get('jform', array(), 'array'); + + // Submit the password reset request. + $return = $model->processRemindRequest($data); + + // Check for a hard error. + if ($return == false && JDEBUG) { + // The request failed. + // Go back to the request form. + $message = Text::sprintf('COM_USERS_REMIND_REQUEST_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_users&view=remind', false), $message, 'notice'); + + return false; + } + + // To not expose if the user exists or not we send a generic message. + $message = Text::_('COM_USERS_REMIND_REQUEST'); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false), $message, 'notice'); + + return true; + } } diff --git a/components/com_users/src/Controller/ResetController.php b/components/com_users/src/Controller/ResetController.php index f856676509ce6..0d5606f54b023 100644 --- a/components/com_users/src/Controller/ResetController.php +++ b/components/com_users/src/Controller/ResetController.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt */ -namespace Joomla\Component\Users\Site\Controller; -\defined('_JEXEC') or die; +namespace Joomla\Component\Users\Site\Controller; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Controller\BaseController; @@ -21,177 +21,155 @@ */ class ResetController extends BaseController { - /** - * Method to request a password reset. - * - * @return boolean - * - * @since 1.6 - */ - public function request() - { - // Check the request token. - $this->checkToken('post'); - - $app = $this->app; - - /** @var \Joomla\Component\Users\Site\Model\ResetModel $model */ - $model = $this->getModel('Reset', 'Site'); - $data = $this->input->post->get('jform', array(), 'array'); - - // Submit the password reset request. - $return = $model->processResetRequest($data); - - // Check for a hard error. - if ($return instanceof \Exception && JDEBUG) - { - // Get the error message to display. - if ($app->get('error_reporting')) - { - $message = $return->getMessage(); - } - else - { - $message = Text::_('COM_USERS_RESET_REQUEST_ERROR'); - } - - // Go back to the request form. - $this->setRedirect(Route::_('index.php?option=com_users&view=reset', false), $message, 'error'); - - return false; - } - elseif ($return === false && JDEBUG) - { - // The request failed. - // Go back to the request form. - $message = Text::sprintf('COM_USERS_RESET_REQUEST_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_users&view=reset', false), $message, 'notice'); - - return false; - } - - // To not expose if the user exists or not we send a generic message. - $message = Text::_('COM_USERS_RESET_REQUEST'); - $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=confirm', false), $message, 'notice'); - - return true; - } - - /** - * Method to confirm the password request. - * - * @return boolean - * - * @access public - * @since 1.6 - */ - public function confirm() - { - // Check the request token. - $this->checkToken('request'); - - $app = $this->app; - - /** @var \Joomla\Component\Users\Site\Model\ResetModel $model */ - $model = $this->getModel('Reset', 'Site'); - $data = $this->input->get('jform', array(), 'array'); - - // Confirm the password reset request. - $return = $model->processResetConfirm($data); - - // Check for a hard error. - if ($return instanceof \Exception) - { - // Get the error message to display. - if ($app->get('error_reporting')) - { - $message = $return->getMessage(); - } - else - { - $message = Text::_('COM_USERS_RESET_CONFIRM_ERROR'); - } - - // Go back to the confirm form. - $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=confirm', false), $message, 'error'); - - return false; - } - elseif ($return === false) - { - // Confirm failed. - // Go back to the confirm form. - $message = Text::sprintf('COM_USERS_RESET_CONFIRM_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=confirm', false), $message, 'notice'); - - return false; - } - else - { - // Confirm succeeded. - // Proceed to step three. - $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=complete', false)); - - return true; - } - } - - /** - * Method to complete the password reset process. - * - * @return boolean - * - * @since 1.6 - */ - public function complete() - { - // Check for request forgeries - $this->checkToken('post'); - - $app = $this->app; - - /** @var \Joomla\Component\Users\Site\Model\ResetModel $model */ - $model = $this->getModel('Reset', 'Site'); - $data = $this->input->post->get('jform', array(), 'array'); - - // Complete the password reset request. - $return = $model->processResetComplete($data); - - // Check for a hard error. - if ($return instanceof \Exception) - { - // Get the error message to display. - if ($app->get('error_reporting')) - { - $message = $return->getMessage(); - } - else - { - $message = Text::_('COM_USERS_RESET_COMPLETE_ERROR'); - } - - // Go back to the complete form. - $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=complete', false), $message, 'error'); - - return false; - } - elseif ($return === false) - { - // Complete failed. - // Go back to the complete form. - $message = Text::sprintf('COM_USERS_RESET_COMPLETE_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=complete', false), $message, 'notice'); - - return false; - } - else - { - // Complete succeeded. - // Proceed to the login form. - $message = Text::_('COM_USERS_RESET_COMPLETE_SUCCESS'); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false), $message); - - return true; - } - } + /** + * Method to request a password reset. + * + * @return boolean + * + * @since 1.6 + */ + public function request() + { + // Check the request token. + $this->checkToken('post'); + + $app = $this->app; + + /** @var \Joomla\Component\Users\Site\Model\ResetModel $model */ + $model = $this->getModel('Reset', 'Site'); + $data = $this->input->post->get('jform', array(), 'array'); + + // Submit the password reset request. + $return = $model->processResetRequest($data); + + // Check for a hard error. + if ($return instanceof \Exception && JDEBUG) { + // Get the error message to display. + if ($app->get('error_reporting')) { + $message = $return->getMessage(); + } else { + $message = Text::_('COM_USERS_RESET_REQUEST_ERROR'); + } + + // Go back to the request form. + $this->setRedirect(Route::_('index.php?option=com_users&view=reset', false), $message, 'error'); + + return false; + } elseif ($return === false && JDEBUG) { + // The request failed. + // Go back to the request form. + $message = Text::sprintf('COM_USERS_RESET_REQUEST_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_users&view=reset', false), $message, 'notice'); + + return false; + } + + // To not expose if the user exists or not we send a generic message. + $message = Text::_('COM_USERS_RESET_REQUEST'); + $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=confirm', false), $message, 'notice'); + + return true; + } + + /** + * Method to confirm the password request. + * + * @return boolean + * + * @access public + * @since 1.6 + */ + public function confirm() + { + // Check the request token. + $this->checkToken('request'); + + $app = $this->app; + + /** @var \Joomla\Component\Users\Site\Model\ResetModel $model */ + $model = $this->getModel('Reset', 'Site'); + $data = $this->input->get('jform', array(), 'array'); + + // Confirm the password reset request. + $return = $model->processResetConfirm($data); + + // Check for a hard error. + if ($return instanceof \Exception) { + // Get the error message to display. + if ($app->get('error_reporting')) { + $message = $return->getMessage(); + } else { + $message = Text::_('COM_USERS_RESET_CONFIRM_ERROR'); + } + + // Go back to the confirm form. + $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=confirm', false), $message, 'error'); + + return false; + } elseif ($return === false) { + // Confirm failed. + // Go back to the confirm form. + $message = Text::sprintf('COM_USERS_RESET_CONFIRM_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=confirm', false), $message, 'notice'); + + return false; + } else { + // Confirm succeeded. + // Proceed to step three. + $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=complete', false)); + + return true; + } + } + + /** + * Method to complete the password reset process. + * + * @return boolean + * + * @since 1.6 + */ + public function complete() + { + // Check for request forgeries + $this->checkToken('post'); + + $app = $this->app; + + /** @var \Joomla\Component\Users\Site\Model\ResetModel $model */ + $model = $this->getModel('Reset', 'Site'); + $data = $this->input->post->get('jform', array(), 'array'); + + // Complete the password reset request. + $return = $model->processResetComplete($data); + + // Check for a hard error. + if ($return instanceof \Exception) { + // Get the error message to display. + if ($app->get('error_reporting')) { + $message = $return->getMessage(); + } else { + $message = Text::_('COM_USERS_RESET_COMPLETE_ERROR'); + } + + // Go back to the complete form. + $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=complete', false), $message, 'error'); + + return false; + } elseif ($return === false) { + // Complete failed. + // Go back to the complete form. + $message = Text::sprintf('COM_USERS_RESET_COMPLETE_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_users&view=reset&layout=complete', false), $message, 'notice'); + + return false; + } else { + // Complete succeeded. + // Proceed to the login form. + $message = Text::_('COM_USERS_RESET_COMPLETE_SUCCESS'); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false), $message); + + return true; + } + } } diff --git a/components/com_users/src/Controller/UserController.php b/components/com_users/src/Controller/UserController.php index 0239d4c8833ed..3daee6a820eb6 100644 --- a/components/com_users/src/Controller/UserController.php +++ b/components/com_users/src/Controller/UserController.php @@ -1,4 +1,5 @@ checkToken('post'); - - $input = $this->input->getInputForRequestMethod(); - - // Populate the data array: - $data = array(); - - $data['return'] = base64_decode($input->get('return', '', 'BASE64')); - $data['username'] = $input->get('username', '', 'USERNAME'); - $data['password'] = $input->get('password', '', 'RAW'); - $data['secretkey'] = $input->get('secretkey', '', 'RAW'); - - // Check for a simple menu item id - if (is_numeric($data['return'])) - { - $language = $this->getModel('Login', 'Site')->getMenuLanguage($data['return']); - $data['return'] = 'index.php?Itemid=' . $data['return'] . ($language !== '*' ? '&lang=' . $language : ''); - } - // Don't redirect to an external URL. - elseif (!Uri::isInternal($data['return'])) - { - $data['return'] = ''; - } - - // Set the return URL if empty. - if (empty($data['return'])) - { - $data['return'] = 'index.php?option=com_users&view=profile'; - } - - // Set the return URL in the user state to allow modification by plugins - $this->app->setUserState('users.login.form.return', $data['return']); - - // Get the log in options. - $options = array(); - $options['remember'] = $this->input->getBool('remember', false); - $options['return'] = $data['return']; - - // Get the log in credentials. - $credentials = array(); - $credentials['username'] = $data['username']; - $credentials['password'] = $data['password']; - $credentials['secretkey'] = $data['secretkey']; - - // Perform the log in. - if (true !== $this->app->login($credentials, $options)) - { - // Login failed ! - // Clear user name, password and secret key before sending the login form back to the user. - $data['remember'] = (int) $options['remember']; - $data['username'] = ''; - $data['password'] = ''; - $data['secretkey'] = ''; - $this->app->setUserState('users.login.form.data', $data); - $this->app->redirect(Route::_('index.php?option=com_users&view=login', false)); - } - - // Success - if ($options['remember'] == true) - { - $this->app->setUserState('rememberLogin', true); - } - - $this->app->setUserState('users.login.form.data', array()); - $this->app->redirect(Route::_($this->app->getUserState('users.login.form.return'), false)); - } - - /** - * Method to log out a user. - * - * @return void - * - * @since 1.6 - */ - public function logout() - { - $this->checkToken('request'); - - $app = $this->app; - - // Prepare the logout options. - $options = array( - 'clientid' => $app->get('shared_session', '0') ? null : 0, - ); - - // Perform the log out. - $error = $app->logout(null, $options); - $input = $app->input->getInputForRequestMethod(); - - // Check if the log out succeeded. - if ($error instanceof \Exception) - { - $app->redirect(Route::_('index.php?option=com_users&view=login', false)); - } - - // Get the return URL from the request and validate that it is internal. - $return = $input->get('return', '', 'BASE64'); - $return = base64_decode($return); - - // Check for a simple menu item id - if (is_numeric($return)) - { - $language = $this->getModel('Login', 'Site')->getMenuLanguage($return); - $return = 'index.php?Itemid=' . $return . ($language !== '*' ? '&lang=' . $language : ''); - } - elseif (!Uri::isInternal($return)) - { - $return = ''; - } - - // In case redirect url is not set, redirect user to homepage - if (empty($return)) - { - $return = Uri::root(); - } - - // Redirect the user. - $app->redirect(Route::_($return, false)); - } - - /** - * Method to logout directly and redirect to page. - * - * @return void - * - * @since 3.5 - */ - public function menulogout() - { - // Get the ItemID of the page to redirect after logout - $app = $this->app; - $active = $app->getMenu()->getActive(); - $itemid = $active ? $active->getParams()->get('logout') : 0; - - // Get the language of the page when multilang is on - if (Multilanguage::isEnabled()) - { - if ($itemid) - { - $language = $this->getModel('Login', 'Site')->getMenuLanguage($itemid); - - // URL to redirect after logout - $url = 'index.php?Itemid=' . $itemid . ($language !== '*' ? '&lang=' . $language : ''); - } - else - { - // Logout is set to default. Get the home page ItemID - $lang_code = $app->input->cookie->getString(ApplicationHelper::getHash('language')); - $item = $app->getMenu()->getDefault($lang_code); - $itemid = $item->id; - - // Redirect to Home page after logout - $url = 'index.php?Itemid=' . $itemid; - } - } - else - { - // URL to redirect after logout, default page if no ItemID is set - $url = $itemid ? 'index.php?Itemid=' . $itemid : Uri::root(); - } - - // Logout and redirect - $this->setRedirect('index.php?option=com_users&task=user.logout&' . Session::getFormToken() . '=1&return=' . base64_encode($url)); - } - - /** - * Method to request a username reminder. - * - * @return boolean - * - * @since 1.6 - */ - public function remind() - { - // Check the request token. - $this->checkToken('post'); - - $app = $this->app; - - /** @var \Joomla\Component\Users\Site\Model\RemindModel $model */ - $model = $this->getModel('Remind', 'Site'); - $data = $this->input->post->get('jform', array(), 'array'); - - // Submit the username remind request. - $return = $model->processRemindRequest($data); - - // Check for a hard error. - if ($return instanceof \Exception) - { - // Get the error message to display. - $message = $app->get('error_reporting') - ? $return->getMessage() - : Text::_('COM_USERS_REMIND_REQUEST_ERROR'); - - // Go back to the complete form. - $this->setRedirect(Route::_('index.php?option=com_users&view=remind', false), $message, 'error'); - - return false; - } - - if ($return === false) - { - // Go back to the complete form. - $message = Text::sprintf('COM_USERS_REMIND_REQUEST_FAILED', $model->getError()); - $this->setRedirect(Route::_('index.php?option=com_users&view=remind', false), $message, 'notice'); - - return false; - } - - // Proceed to the login form. - $message = Text::_('COM_USERS_REMIND_REQUEST_SUCCESS'); - $this->setRedirect(Route::_('index.php?option=com_users&view=login', false), $message); - - return true; - } - - /** - * Method to resend a user. - * - * @return void - * - * @since 1.6 - */ - public function resend() - { - // Check for request forgeries - // $this->checkToken('post'); - } + /** + * Method to log in a user. + * + * @return void + * + * @since 1.6 + */ + public function login() + { + $this->checkToken('post'); + + $input = $this->input->getInputForRequestMethod(); + + // Populate the data array: + $data = array(); + + $data['return'] = base64_decode($input->get('return', '', 'BASE64')); + $data['username'] = $input->get('username', '', 'USERNAME'); + $data['password'] = $input->get('password', '', 'RAW'); + $data['secretkey'] = $input->get('secretkey', '', 'RAW'); + + // Check for a simple menu item id + if (is_numeric($data['return'])) { + $language = $this->getModel('Login', 'Site')->getMenuLanguage($data['return']); + $data['return'] = 'index.php?Itemid=' . $data['return'] . ($language !== '*' ? '&lang=' . $language : ''); + } elseif (!Uri::isInternal($data['return'])) { + // Don't redirect to an external URL. + $data['return'] = ''; + } + + // Set the return URL if empty. + if (empty($data['return'])) { + $data['return'] = 'index.php?option=com_users&view=profile'; + } + + // Set the return URL in the user state to allow modification by plugins + $this->app->setUserState('users.login.form.return', $data['return']); + + // Get the log in options. + $options = array(); + $options['remember'] = $this->input->getBool('remember', false); + $options['return'] = $data['return']; + + // Get the log in credentials. + $credentials = array(); + $credentials['username'] = $data['username']; + $credentials['password'] = $data['password']; + $credentials['secretkey'] = $data['secretkey']; + + // Perform the log in. + if (true !== $this->app->login($credentials, $options)) { + // Login failed ! + // Clear user name, password and secret key before sending the login form back to the user. + $data['remember'] = (int) $options['remember']; + $data['username'] = ''; + $data['password'] = ''; + $data['secretkey'] = ''; + $this->app->setUserState('users.login.form.data', $data); + $this->app->redirect(Route::_('index.php?option=com_users&view=login', false)); + } + + // Success + if ($options['remember'] == true) { + $this->app->setUserState('rememberLogin', true); + } + + $this->app->setUserState('users.login.form.data', array()); + $this->app->redirect(Route::_($this->app->getUserState('users.login.form.return'), false)); + } + + /** + * Method to log out a user. + * + * @return void + * + * @since 1.6 + */ + public function logout() + { + $this->checkToken('request'); + + $app = $this->app; + + // Prepare the logout options. + $options = array( + 'clientid' => $app->get('shared_session', '0') ? null : 0, + ); + + // Perform the log out. + $error = $app->logout(null, $options); + $input = $app->input->getInputForRequestMethod(); + + // Check if the log out succeeded. + if ($error instanceof \Exception) { + $app->redirect(Route::_('index.php?option=com_users&view=login', false)); + } + + // Get the return URL from the request and validate that it is internal. + $return = $input->get('return', '', 'BASE64'); + $return = base64_decode($return); + + // Check for a simple menu item id + if (is_numeric($return)) { + $language = $this->getModel('Login', 'Site')->getMenuLanguage($return); + $return = 'index.php?Itemid=' . $return . ($language !== '*' ? '&lang=' . $language : ''); + } elseif (!Uri::isInternal($return)) { + $return = ''; + } + + // In case redirect url is not set, redirect user to homepage + if (empty($return)) { + $return = Uri::root(); + } + + // Redirect the user. + $app->redirect(Route::_($return, false)); + } + + /** + * Method to logout directly and redirect to page. + * + * @return void + * + * @since 3.5 + */ + public function menulogout() + { + // Get the ItemID of the page to redirect after logout + $app = $this->app; + $active = $app->getMenu()->getActive(); + $itemid = $active ? $active->getParams()->get('logout') : 0; + + // Get the language of the page when multilang is on + if (Multilanguage::isEnabled()) { + if ($itemid) { + $language = $this->getModel('Login', 'Site')->getMenuLanguage($itemid); + + // URL to redirect after logout + $url = 'index.php?Itemid=' . $itemid . ($language !== '*' ? '&lang=' . $language : ''); + } else { + // Logout is set to default. Get the home page ItemID + $lang_code = $app->input->cookie->getString(ApplicationHelper::getHash('language')); + $item = $app->getMenu()->getDefault($lang_code); + $itemid = $item->id; + + // Redirect to Home page after logout + $url = 'index.php?Itemid=' . $itemid; + } + } else { + // URL to redirect after logout, default page if no ItemID is set + $url = $itemid ? 'index.php?Itemid=' . $itemid : Uri::root(); + } + + // Logout and redirect + $this->setRedirect('index.php?option=com_users&task=user.logout&' . Session::getFormToken() . '=1&return=' . base64_encode($url)); + } + + /** + * Method to request a username reminder. + * + * @return boolean + * + * @since 1.6 + */ + public function remind() + { + // Check the request token. + $this->checkToken('post'); + + $app = $this->app; + + /** @var \Joomla\Component\Users\Site\Model\RemindModel $model */ + $model = $this->getModel('Remind', 'Site'); + $data = $this->input->post->get('jform', array(), 'array'); + + // Submit the username remind request. + $return = $model->processRemindRequest($data); + + // Check for a hard error. + if ($return instanceof \Exception) { + // Get the error message to display. + $message = $app->get('error_reporting') + ? $return->getMessage() + : Text::_('COM_USERS_REMIND_REQUEST_ERROR'); + + // Go back to the complete form. + $this->setRedirect(Route::_('index.php?option=com_users&view=remind', false), $message, 'error'); + + return false; + } + + if ($return === false) { + // Go back to the complete form. + $message = Text::sprintf('COM_USERS_REMIND_REQUEST_FAILED', $model->getError()); + $this->setRedirect(Route::_('index.php?option=com_users&view=remind', false), $message, 'notice'); + + return false; + } + + // Proceed to the login form. + $message = Text::_('COM_USERS_REMIND_REQUEST_SUCCESS'); + $this->setRedirect(Route::_('index.php?option=com_users&view=login', false), $message); + + return true; + } + + /** + * Method to resend a user. + * + * @return void + * + * @since 1.6 + */ + public function resend() + { + // Check for request forgeries + // $this->checkToken('post'); + } } diff --git a/components/com_users/src/Model/BackupcodesModel.php b/components/com_users/src/Model/BackupcodesModel.php index 0f84826b7940d..a0ad57313a908 100644 --- a/components/com_users/src/Model/BackupcodesModel.php +++ b/components/com_users/src/Model/BackupcodesModel.php @@ -1,4 +1,5 @@ loadForm('com_users.login', 'login', array('load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return array The default data is an empty array. - * - * @since 1.6 - * @throws \Exception - */ - protected function loadFormData() - { - // Check the session for previously entered login form data. - $app = Factory::getApplication(); - $data = $app->getUserState('users.login.form.data', array()); - - $input = $app->input->getInputForRequestMethod(); - - // Check for return URL from the request first - if ($return = $input->get('return', '', 'BASE64')) - { - $data['return'] = base64_decode($return); - - if (!Uri::isInternal($data['return'])) - { - $data['return'] = ''; - } - } - - $app->setUserState('users.login.form.data', $data); - - $this->preprocessData('com_users.login', $data); - - return $data; - } - - /** - * Method to auto-populate the model state. - * - * Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function populateState() - { - // Get the application object. - $params = Factory::getApplication()->getParams('com_users'); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Override Joomla\CMS\MVC\Model\AdminModel::preprocessForm to ensure the correct plugin group is loaded. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'user') - { - parent::preprocessForm($form, $data, $group); - } - - /** - * Returns the language for the given menu id. - * - * @param int $id The menu id - * - * @return string - * - * @since 4.2.0 - */ - public function getMenuLanguage(int $id): string - { - if (!Multilanguage::isEnabled()) - { - return ''; - } - - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('language')) - ->from($db->quoteName('#__menu')) - ->where($db->quoteName('client_id') . ' = 0') - ->where($db->quoteName('id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - - $db->setQuery($query); - - try - { - return $db->loadResult(); - } - catch (\RuntimeException $e) - { - return ''; - } - } + /** + * Method to get the login form. + * + * The base form is loaded from XML and then an event is fired + * for users plugins to extend the form with extra fields. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.login', 'login', array('load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return array The default data is an empty array. + * + * @since 1.6 + * @throws \Exception + */ + protected function loadFormData() + { + // Check the session for previously entered login form data. + $app = Factory::getApplication(); + $data = $app->getUserState('users.login.form.data', array()); + + $input = $app->input->getInputForRequestMethod(); + + // Check for return URL from the request first + if ($return = $input->get('return', '', 'BASE64')) { + $data['return'] = base64_decode($return); + + if (!Uri::isInternal($data['return'])) { + $data['return'] = ''; + } + } + + $app->setUserState('users.login.form.data', $data); + + $this->preprocessData('com_users.login', $data); + + return $data; + } + + /** + * Method to auto-populate the model state. + * + * Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function populateState() + { + // Get the application object. + $params = Factory::getApplication()->getParams('com_users'); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Override Joomla\CMS\MVC\Model\AdminModel::preprocessForm to ensure the correct plugin group is loaded. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'user') + { + parent::preprocessForm($form, $data, $group); + } + + /** + * Returns the language for the given menu id. + * + * @param int $id The menu id + * + * @return string + * + * @since 4.2.0 + */ + public function getMenuLanguage(int $id): string + { + if (!Multilanguage::isEnabled()) { + return ''; + } + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('language')) + ->from($db->quoteName('#__menu')) + ->where($db->quoteName('client_id') . ' = 0') + ->where($db->quoteName('id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + $db->setQuery($query); + + try { + return $db->loadResult(); + } catch (\RuntimeException $e) { + return ''; + } + } } diff --git a/components/com_users/src/Model/MethodModel.php b/components/com_users/src/Model/MethodModel.php index 867155b060f40..516784da2854c 100644 --- a/components/com_users/src/Model/MethodModel.php +++ b/components/com_users/src/Model/MethodModel.php @@ -1,4 +1,5 @@ array('validate' => 'user') - ), $config - ); - - parent::__construct($config, $factory, $formFactory); - } - - /** - * Method to get the profile form data. - * - * The base form data is loaded and then an event is fired - * for users plugins to extend the data. - * - * @return User - * - * @since 1.6 - * @throws \Exception - */ - public function getData() - { - if ($this->data === null) - { - $userId = $this->getState('user.id'); - - // Initialise the table with Joomla\CMS\User\User. - $this->data = new User($userId); - - // Set the base user data. - $this->data->email1 = $this->data->get('email'); - - // Override the base user data with any data in the session. - $temp = (array) Factory::getApplication()->getUserState('com_users.edit.profile.data', array()); - - foreach ($temp as $k => $v) - { - $this->data->$k = $v; - } - - // Unset the passwords. - unset($this->data->password1, $this->data->password2); - - $registry = new Registry($this->data->params); - $this->data->params = $registry->toArray(); - } - - return $this->data; - } - - /** - * Method to get the profile form. - * - * The base form is loaded from XML and then an event is fired - * for users plugins to extend the form with extra fields. - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form|bool A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_users.profile', 'profile', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // Check for username compliance and parameter set - $isUsernameCompliant = true; - $username = $loadData ? $form->getValue('username') : $this->loadFormData()->username; - - if ($username) - { - $isUsernameCompliant = !(preg_match('#[<>"\'%;()&\\\\]|\\.\\./#', $username) || strlen(utf8_decode($username)) < 2 - || trim($username) !== $username); - } - - $this->setState('user.username.compliant', $isUsernameCompliant); - - if ($isUsernameCompliant && !ComponentHelper::getParams('com_users')->get('change_login_name')) - { - $form->setFieldAttribute('username', 'class', ''); - $form->setFieldAttribute('username', 'filter', ''); - $form->setFieldAttribute('username', 'description', 'COM_USERS_PROFILE_NOCHANGE_USERNAME_DESC'); - $form->setFieldAttribute('username', 'validate', ''); - $form->setFieldAttribute('username', 'message', ''); - $form->setFieldAttribute('username', 'readonly', 'true'); - $form->setFieldAttribute('username', 'required', 'false'); - } - - // When multilanguage is set, a user's default site language should also be a Content Language - if (Multilanguage::isEnabled()) - { - $form->setFieldAttribute('language', 'type', 'frontend_language', 'params'); - } - - // If the user needs to change their password, mark the password fields as required - if (Factory::getUser()->requireReset) - { - $form->setFieldAttribute('password1', 'required', 'true'); - $form->setFieldAttribute('password2', 'required', 'true'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - $data = $this->getData(); - - $this->preprocessData('com_users.profile', $data, 'user'); - - return $data; - } - - /** - * Override preprocessForm to load the user plugin group instead of content. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @throws \Exception if there is an error in the form event. - * - * @since 1.6 - */ - protected function preprocessForm(Form $form, $data, $group = 'user') - { - if (ComponentHelper::getParams('com_users')->get('frontend_userparams')) - { - $form->loadFile('frontend', false); - - if (Factory::getUser()->authorise('core.login.admin')) - { - $form->loadFile('frontend_admin', false); - } - } - - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function populateState() - { - // Get the application object. - $params = Factory::getApplication()->getParams('com_users'); - - // Get the user id. - $userId = Factory::getApplication()->getUserState('com_users.edit.profile.id'); - $userId = !empty($userId) ? $userId : (int) Factory::getUser()->get('id'); - - // Set the user id. - $this->setState('user.id', $userId); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Method to save the form data. - * - * @param array $data The form data. - * - * @return mixed The user id on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function save($data) - { - $userId = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('user.id'); - - $user = new User($userId); - - // Prepare the data for the user object. - $data['email'] = PunycodeHelper::emailToPunycode($data['email1']); - $data['password'] = $data['password1']; - - // Unset the username if it should not be overwritten - $isUsernameCompliant = $this->getState('user.username.compliant'); - - if ($isUsernameCompliant && !ComponentHelper::getParams('com_users')->get('change_login_name')) - { - unset($data['username']); - } - - // Unset block and sendEmail so they do not get overwritten - unset($data['block'], $data['sendEmail']); - - // Bind the data. - if (!$user->bind($data)) - { - $this->setError($user->getError()); - - return false; - } - - // Load the users plugin group. - PluginHelper::importPlugin('user'); - - // Retrieve the user groups so they don't get overwritten - unset($user->groups); - $user->groups = Access::getGroupsByUser($user->id, false); - - // Store the data. - if (!$user->save()) - { - $this->setError($user->getError()); - - return false; - } - - // Destroy all active sessions for the user after changing the password - if ($data['password']) - { - UserHelper::destroyUserSessions($user->id, true); - } - - return $user->id; - } - - /** - * Gets the configuration forms for all two-factor authentication methods - * in an array. - * - * @param integer $userId The user ID to load the forms for (optional) - * - * @return array - * - * @since 3.2 - * @deprecated 4.2.0 Will be removed in 5.0. - */ - public function getTwofactorform($userId = null) - { - return []; - } - - /** - * No longer used - * - * @param integer $userId Ignored - * - * @return \stdClass - * - * @since 3.2 - * @deprecated 4.2.0 Will be removed in 5.0 - */ - public function getOtpConfig($userId = null) - { - @trigger_error( - sprintf( - '%s() is deprecated. Use \Joomla\Component\Users\Administrator\Helper\Mfa::getUserMfaRecords() instead.', - __METHOD__ - ), - E_USER_DEPRECATED - ); - - /** @var UserModel $model */ - $model = $this->bootComponent('com_users') - ->getMVCFactory()->createModel('User', 'Administrator'); - - return $model->getOtpConfig(); - } + /** + * @var object The user profile data. + * @since 1.6 + */ + protected $data; + + /** + * Constructor. + * + * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). + * @param MVCFactoryInterface $factory The factory. + * @param FormFactoryInterface $formFactory The form factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, FormFactoryInterface $formFactory = null) + { + $config = array_merge( + array( + 'events_map' => array('validate' => 'user') + ), + $config + ); + + parent::__construct($config, $factory, $formFactory); + } + + /** + * Method to get the profile form data. + * + * The base form data is loaded and then an event is fired + * for users plugins to extend the data. + * + * @return User + * + * @since 1.6 + * @throws \Exception + */ + public function getData() + { + if ($this->data === null) { + $userId = $this->getState('user.id'); + + // Initialise the table with Joomla\CMS\User\User. + $this->data = new User($userId); + + // Set the base user data. + $this->data->email1 = $this->data->get('email'); + + // Override the base user data with any data in the session. + $temp = (array) Factory::getApplication()->getUserState('com_users.edit.profile.data', array()); + + foreach ($temp as $k => $v) { + $this->data->$k = $v; + } + + // Unset the passwords. + unset($this->data->password1, $this->data->password2); + + $registry = new Registry($this->data->params); + $this->data->params = $registry->toArray(); + } + + return $this->data; + } + + /** + * Method to get the profile form. + * + * The base form is loaded from XML and then an event is fired + * for users plugins to extend the form with extra fields. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.profile', 'profile', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // Check for username compliance and parameter set + $isUsernameCompliant = true; + $username = $loadData ? $form->getValue('username') : $this->loadFormData()->username; + + if ($username) { + $isUsernameCompliant = !(preg_match('#[<>"\'%;()&\\\\]|\\.\\./#', $username) || strlen(utf8_decode($username)) < 2 + || trim($username) !== $username); + } + + $this->setState('user.username.compliant', $isUsernameCompliant); + + if ($isUsernameCompliant && !ComponentHelper::getParams('com_users')->get('change_login_name')) { + $form->setFieldAttribute('username', 'class', ''); + $form->setFieldAttribute('username', 'filter', ''); + $form->setFieldAttribute('username', 'description', 'COM_USERS_PROFILE_NOCHANGE_USERNAME_DESC'); + $form->setFieldAttribute('username', 'validate', ''); + $form->setFieldAttribute('username', 'message', ''); + $form->setFieldAttribute('username', 'readonly', 'true'); + $form->setFieldAttribute('username', 'required', 'false'); + } + + // When multilanguage is set, a user's default site language should also be a Content Language + if (Multilanguage::isEnabled()) { + $form->setFieldAttribute('language', 'type', 'frontend_language', 'params'); + } + + // If the user needs to change their password, mark the password fields as required + if (Factory::getUser()->requireReset) { + $form->setFieldAttribute('password1', 'required', 'true'); + $form->setFieldAttribute('password2', 'required', 'true'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + $data = $this->getData(); + + $this->preprocessData('com_users.profile', $data, 'user'); + + return $data; + } + + /** + * Override preprocessForm to load the user plugin group instead of content. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @throws \Exception if there is an error in the form event. + * + * @since 1.6 + */ + protected function preprocessForm(Form $form, $data, $group = 'user') + { + if (ComponentHelper::getParams('com_users')->get('frontend_userparams')) { + $form->loadFile('frontend', false); + + if (Factory::getUser()->authorise('core.login.admin')) { + $form->loadFile('frontend_admin', false); + } + } + + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function populateState() + { + // Get the application object. + $params = Factory::getApplication()->getParams('com_users'); + + // Get the user id. + $userId = Factory::getApplication()->getUserState('com_users.edit.profile.id'); + $userId = !empty($userId) ? $userId : (int) Factory::getUser()->get('id'); + + // Set the user id. + $this->setState('user.id', $userId); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Method to save the form data. + * + * @param array $data The form data. + * + * @return mixed The user id on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function save($data) + { + $userId = (!empty($data['id'])) ? $data['id'] : (int) $this->getState('user.id'); + + $user = new User($userId); + + // Prepare the data for the user object. + $data['email'] = PunycodeHelper::emailToPunycode($data['email1']); + $data['password'] = $data['password1']; + + // Unset the username if it should not be overwritten + $isUsernameCompliant = $this->getState('user.username.compliant'); + + if ($isUsernameCompliant && !ComponentHelper::getParams('com_users')->get('change_login_name')) { + unset($data['username']); + } + + // Unset block and sendEmail so they do not get overwritten + unset($data['block'], $data['sendEmail']); + + // Bind the data. + if (!$user->bind($data)) { + $this->setError($user->getError()); + + return false; + } + + // Load the users plugin group. + PluginHelper::importPlugin('user'); + + // Retrieve the user groups so they don't get overwritten + unset($user->groups); + $user->groups = Access::getGroupsByUser($user->id, false); + + // Store the data. + if (!$user->save()) { + $this->setError($user->getError()); + + return false; + } + + // Destroy all active sessions for the user after changing the password + if ($data['password']) { + UserHelper::destroyUserSessions($user->id, true); + } + + return $user->id; + } + + /** + * Gets the configuration forms for all two-factor authentication methods + * in an array. + * + * @param integer $userId The user ID to load the forms for (optional) + * + * @return array + * + * @since 3.2 + * @deprecated 4.2.0 Will be removed in 5.0. + */ + public function getTwofactorform($userId = null) + { + return []; + } + + /** + * No longer used + * + * @param integer $userId Ignored + * + * @return \stdClass + * + * @since 3.2 + * @deprecated 4.2.0 Will be removed in 5.0 + */ + public function getOtpConfig($userId = null) + { + @trigger_error( + sprintf( + '%s() is deprecated. Use \Joomla\Component\Users\Administrator\Helper\Mfa::getUserMfaRecords() instead.', + __METHOD__ + ), + E_USER_DEPRECATED + ); + + /** @var UserModel $model */ + $model = $this->bootComponent('com_users') + ->getMVCFactory()->createModel('User', 'Administrator'); + + return $model->getOtpConfig(); + } } diff --git a/components/com_users/src/Model/RegistrationModel.php b/components/com_users/src/Model/RegistrationModel.php index 1f8d331584e1a..fe15de52560ac 100644 --- a/components/com_users/src/Model/RegistrationModel.php +++ b/components/com_users/src/Model/RegistrationModel.php @@ -1,4 +1,5 @@ array('validate' => 'user') - ), $config - ); - - parent::__construct($config, $factory, $formFactory); - } - - /** - * Method to get the user ID from the given token - * - * @param string $token The activation token. - * - * @return mixed False on failure, id of the user on success - * - * @since 3.8.13 - */ - public function getUserIdFromToken($token) - { - $db = $this->getDatabase(); - - // Get the user id based on the token. - $query = $db->getQuery(true); - $query->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('activation') . ' = :activation') - ->where($db->quoteName('block') . ' = 1') - ->where($db->quoteName('lastvisitDate') . ' IS NULL') - ->bind(':activation', $token); - $db->setQuery($query); - - try - { - return (int) $db->loadResult(); - } - catch (\RuntimeException $e) - { - $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); - - return false; - } - } - - /** - * Method to activate a user account. - * - * @param string $token The activation token. - * - * @return mixed False on failure, user object on success. - * - * @since 1.6 - */ - public function activate($token) - { - $app = Factory::getApplication(); - $userParams = ComponentHelper::getParams('com_users'); - $userId = $this->getUserIdFromToken($token); - - // Check for a valid user id. - if (!$userId) - { - $this->setError(Text::_('COM_USERS_ACTIVATION_TOKEN_NOT_FOUND')); - - return false; - } - - // Load the users plugin group. - PluginHelper::importPlugin('user'); - - // Activate the user. - $user = Factory::getUser($userId); - - // Admin activation is on and user is verifying their email - if (($userParams->get('useractivation') == 2) && !$user->getParam('activate', 0)) - { - $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; - - // Compile the admin notification mail values. - $data = $user->getProperties(); - $data['activation'] = ApplicationHelper::getHash(UserHelper::genRandomPassword()); - $user->set('activation', $data['activation']); - $data['siteurl'] = Uri::base(); - $data['activate'] = Route::link( - 'site', - 'index.php?option=com_users&task=registration.activate&token=' . $data['activation'], - false, - $linkMode, - true - ); - - $data['fromname'] = $app->get('fromname'); - $data['mailfrom'] = $app->get('mailfrom'); - $data['sitename'] = $app->get('sitename'); - $user->setParam('activate', 1); - - // Get all admin users - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName(array('name', 'email', 'sendEmail', 'id'))) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('sendEmail') . ' = 1') - ->where($db->quoteName('block') . ' = 0'); - - $db->setQuery($query); - - try - { - $rows = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); - - return false; - } - - // Send mail to all users with users creating permissions and receiving system emails - foreach ($rows as $row) - { - $usercreator = Factory::getUser($row->id); - - if ($usercreator->authorise('core.create', 'com_users') && $usercreator->authorise('core.manage', 'com_users')) - { - try - { - $mailer = new MailTemplate('com_users.registration.admin.verification_request', $app->getLanguage()->getTag()); - $mailer->addTemplateData($data); - $mailer->addRecipient($row->email); - $return = $mailer->send(); - } - catch (\Exception $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $return = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $return = false; - } - } - - // Check for an error. - if ($return !== true) - { - $this->setError(Text::_('COM_USERS_REGISTRATION_ACTIVATION_NOTIFY_SEND_MAIL_FAILED')); - - return false; - } - } - } - } - // Admin activation is on and admin is activating the account - elseif (($userParams->get('useractivation') == 2) && $user->getParam('activate', 0)) - { - $user->set('activation', ''); - $user->set('block', '0'); - - // Compile the user activated notification mail values. - $data = $user->getProperties(); - $user->setParam('activate', 0); - $data['fromname'] = $app->get('fromname'); - $data['mailfrom'] = $app->get('mailfrom'); - $data['sitename'] = $app->get('sitename'); - $data['siteurl'] = Uri::base(); - $mailer = new MailTemplate('com_users.registration.user.admin_activated', $app->getLanguage()->getTag()); - $mailer->addTemplateData($data); - $mailer->addRecipient($data['email']); - - try - { - $return = $mailer->send(); - } - catch (\Exception $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $return = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $return = false; - } - } - - // Check for an error. - if ($return !== true) - { - $this->setError(Text::_('COM_USERS_REGISTRATION_ACTIVATION_NOTIFY_SEND_MAIL_FAILED')); - - return false; - } - } - else - { - $user->set('activation', ''); - $user->set('block', '0'); - } - - // Store the user object. - if (!$user->save()) - { - $this->setError(Text::sprintf('COM_USERS_REGISTRATION_ACTIVATION_SAVE_FAILED', $user->getError())); - - return false; - } - - return $user; - } - - /** - * Method to get the registration form data. - * - * The base form data is loaded and then an event is fired - * for users plugins to extend the data. - * - * @return mixed Data object on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function getData() - { - if ($this->data === null) - { - $this->data = new \stdClass; - $app = Factory::getApplication(); - $params = ComponentHelper::getParams('com_users'); - - // Override the base user data with any data in the session. - $temp = (array) $app->getUserState('com_users.registration.data', array()); - - // Don't load the data in this getForm call, or we'll call ourself - $form = $this->getForm(array(), false); - - foreach ($temp as $k => $v) - { - // Here we could have a grouped field, let's check it - if (is_array($v)) - { - $this->data->$k = new \stdClass; - - foreach ($v as $key => $val) - { - if ($form->getField($key, $k) !== false) - { - $this->data->$k->$key = $val; - } - } - } - // Only merge the field if it exists in the form. - elseif ($form->getField($k) !== false) - { - $this->data->$k = $v; - } - } - - // Get the groups the user should be added to after registration. - $this->data->groups = array(); - - // Get the default new user group, guest or public group if not specified. - $system = $params->get('new_usertype', $params->get('guest_usergroup', 1)); - - $this->data->groups[] = $system; - - // Unset the passwords. - unset($this->data->password1, $this->data->password2); - - // Get the dispatcher and load the users plugins. - PluginHelper::importPlugin('user'); - - // Trigger the data preparation event. - Factory::getApplication()->triggerEvent('onContentPrepareData', array('com_users.registration', $this->data)); - } - - return $this->data; - } - - /** - * Method to get the registration form. - * - * The base form is loaded from XML and then an event is fired - * for users plugins to extend the form with extra fields. - * - * @param array $data An optional array of data for the form to interrogate. - * @param boolean $loadData True if the form is to load its own data (default case), false if not. - * - * @return Form A Form object on success, false on failure - * - * @since 1.6 - */ - public function getForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_users.registration', 'registration', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - // When multilanguage is set, a user's default site language should also be a Content Language - if (Multilanguage::isEnabled()) - { - $form->setFieldAttribute('language', 'type', 'frontend_language', 'params'); - } - - return $form; - } - - /** - * Method to get the data that should be injected in the form. - * - * @return mixed The data for the form. - * - * @since 1.6 - */ - protected function loadFormData() - { - $data = $this->getData(); - - if (Multilanguage::isEnabled() && empty($data->language)) - { - $data->language = Factory::getLanguage()->getTag(); - } - - $this->preprocessData('com_users.registration', $data); - - return $data; - } - - /** - * Override preprocessForm to load the user plugin group instead of content. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @since 1.6 - * @throws \Exception if there is an error in the form event. - */ - protected function preprocessForm(Form $form, $data, $group = 'user') - { - $userParams = ComponentHelper::getParams('com_users'); - - // Add the choice for site language at registration time - if ($userParams->get('site_language') == 1 && $userParams->get('frontend_userparams') == 1) - { - $form->loadFile('sitelang', false); - } - - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function populateState() - { - // Get the application object. - $app = Factory::getApplication(); - $params = $app->getParams('com_users'); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Method to save the form data. - * - * @param array $temp The form data. - * - * @return mixed The user id on success, false on failure. - * - * @since 1.6 - * @throws \Exception - */ - public function register($temp) - { - $params = ComponentHelper::getParams('com_users'); - - // Initialise the table with Joomla\CMS\User\User. - $user = new User; - $data = (array) $this->getData(); - - // Merge in the registration data. - foreach ($temp as $k => $v) - { - $data[$k] = $v; - } - - // Prepare the data for the user object. - $data['email'] = PunycodeHelper::emailToPunycode($data['email1']); - $data['password'] = $data['password1']; - $useractivation = $params->get('useractivation'); - $sendpassword = $params->get('sendpassword', 1); - - // Check if the user needs to activate their account. - if (($useractivation == 1) || ($useractivation == 2)) - { - $data['activation'] = ApplicationHelper::getHash(UserHelper::genRandomPassword()); - $data['block'] = 1; - } - - // Bind the data. - if (!$user->bind($data)) - { - $this->setError($user->getError()); - - return false; - } - - // Load the users plugin group. - PluginHelper::importPlugin('user'); - - // Store the data. - if (!$user->save()) - { - $this->setError(Text::sprintf('COM_USERS_REGISTRATION_SAVE_FAILED', $user->getError())); - - return false; - } - - $app = Factory::getApplication(); - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Compile the notification mail values. - $data = $user->getProperties(); - $data['fromname'] = $app->get('fromname'); - $data['mailfrom'] = $app->get('mailfrom'); - $data['sitename'] = $app->get('sitename'); - $data['siteurl'] = Uri::root(); - - // Handle account activation/confirmation emails. - if ($useractivation == 2) - { - // Set the link to confirm the user email. - $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; - - $data['activate'] = Route::link( - 'site', - 'index.php?option=com_users&task=registration.activate&token=' . $data['activation'], - false, - $linkMode, - true - ); - - $mailtemplate = 'com_users.registration.user.admin_activation'; - } - elseif ($useractivation == 1) - { - // Set the link to activate the user account. - $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; - - $data['activate'] = Route::link( - 'site', - 'index.php?option=com_users&task=registration.activate&token=' . $data['activation'], - false, - $linkMode, - true - ); - - $mailtemplate = 'com_users.registration.user.self_activation'; - } - else - { - $mailtemplate = 'com_users.registration.user.registration_mail'; - } - - if ($sendpassword) - { - $mailtemplate .= '_w_pw'; - } - - // Try to send the registration email. - try - { - $mailer = new MailTemplate($mailtemplate, $app->getLanguage()->getTag()); - $mailer->addTemplateData($data); - $mailer->addRecipient($data['email']); - $return = $mailer->send(); - } - catch (\Exception $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $return = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $this->setError(Text::_('COM_MESSAGES_ERROR_MAIL_FAILED')); - - $return = false; - } - } - - // Send mail to all users with user creating permissions and receiving system emails - if (($params->get('useractivation') < 2) && ($params->get('mail_to_admin') == 1)) - { - // Get all admin users - $query->clear() - ->select($db->quoteName(array('name', 'email', 'sendEmail', 'id'))) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('sendEmail') . ' = 1') - ->where($db->quoteName('block') . ' = 0'); - - $db->setQuery($query); - - try - { - $rows = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); - - return false; - } - - // Send mail to all superadministrators id - foreach ($rows as $row) - { - $usercreator = Factory::getUser($row->id); - - if (!$usercreator->authorise('core.create', 'com_users') || !$usercreator->authorise('core.manage', 'com_users')) - { - continue; - } - - try - { - $mailer = new MailTemplate('com_users.registration.admin.new_notification', $app->getLanguage()->getTag()); - $mailer->addTemplateData($data); - $mailer->addRecipient($row->email); - $return = $mailer->send(); - } - catch (\Exception $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $return = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $return = false; - } - } - - // Check for an error. - if ($return !== true) - { - $this->setError(Text::_('COM_USERS_REGISTRATION_ACTIVATION_NOTIFY_SEND_MAIL_FAILED')); - - return false; - } - } - } - - // Check for an error. - if ($return !== true) - { - $this->setError(Text::_('COM_USERS_REGISTRATION_SEND_MAIL_FAILED')); - - // Send a system message to administrators receiving system mails - $db = $this->getDatabase(); - $query->clear() - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('block') . ' = 0') - ->where($db->quoteName('sendEmail') . ' = 1'); - $db->setQuery($query); - - try - { - $userids = $db->loadColumn(); - } - catch (\RuntimeException $e) - { - $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); - - return false; - } - - if (count($userids) > 0) - { - $jdate = new Date; - $dateToSql = $jdate->toSql(); - $subject = Text::_('COM_USERS_MAIL_SEND_FAILURE_SUBJECT'); - $message = Text::sprintf('COM_USERS_MAIL_SEND_FAILURE_BODY', $data['username']); - - // Build the query to add the messages - foreach ($userids as $userid) - { - $values = [ - ':user_id_from', - ':user_id_to', - ':date_time', - ':subject', - ':message', - ]; - $query->clear() - ->insert($db->quoteName('#__messages')) - ->columns($db->quoteName(['user_id_from', 'user_id_to', 'date_time', 'subject', 'message'])) - ->values(implode(',', $values)); - $query->bind(':user_id_from', $userid, ParameterType::INTEGER) - ->bind(':user_id_to', $userid, ParameterType::INTEGER) - ->bind(':date_time', $dateToSql) - ->bind(':subject', $subject) - ->bind(':message', $message); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); - - return false; - } - } - } - - return false; - } - - if ($useractivation == 1) - { - return 'useractivate'; - } - elseif ($useractivation == 2) - { - return 'adminactivate'; - } - else - { - return $user->id; - } - } + /** + * @var object The user registration data. + * @since 1.6 + */ + protected $data; + + /** + * Constructor. + * + * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). + * @param MVCFactoryInterface $factory The factory. + * @param FormFactoryInterface $formFactory The form factory. + * + * @see \Joomla\CMS\MVC\Model\BaseDatabaseModel + * @since 3.2 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, FormFactoryInterface $formFactory = null) + { + $config = array_merge( + array( + 'events_map' => array('validate' => 'user') + ), + $config + ); + + parent::__construct($config, $factory, $formFactory); + } + + /** + * Method to get the user ID from the given token + * + * @param string $token The activation token. + * + * @return mixed False on failure, id of the user on success + * + * @since 3.8.13 + */ + public function getUserIdFromToken($token) + { + $db = $this->getDatabase(); + + // Get the user id based on the token. + $query = $db->getQuery(true); + $query->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('activation') . ' = :activation') + ->where($db->quoteName('block') . ' = 1') + ->where($db->quoteName('lastvisitDate') . ' IS NULL') + ->bind(':activation', $token); + $db->setQuery($query); + + try { + return (int) $db->loadResult(); + } catch (\RuntimeException $e) { + $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); + + return false; + } + } + + /** + * Method to activate a user account. + * + * @param string $token The activation token. + * + * @return mixed False on failure, user object on success. + * + * @since 1.6 + */ + public function activate($token) + { + $app = Factory::getApplication(); + $userParams = ComponentHelper::getParams('com_users'); + $userId = $this->getUserIdFromToken($token); + + // Check for a valid user id. + if (!$userId) { + $this->setError(Text::_('COM_USERS_ACTIVATION_TOKEN_NOT_FOUND')); + + return false; + } + + // Load the users plugin group. + PluginHelper::importPlugin('user'); + + // Activate the user. + $user = Factory::getUser($userId); + + // Admin activation is on and user is verifying their email + if (($userParams->get('useractivation') == 2) && !$user->getParam('activate', 0)) { + $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; + + // Compile the admin notification mail values. + $data = $user->getProperties(); + $data['activation'] = ApplicationHelper::getHash(UserHelper::genRandomPassword()); + $user->set('activation', $data['activation']); + $data['siteurl'] = Uri::base(); + $data['activate'] = Route::link( + 'site', + 'index.php?option=com_users&task=registration.activate&token=' . $data['activation'], + false, + $linkMode, + true + ); + + $data['fromname'] = $app->get('fromname'); + $data['mailfrom'] = $app->get('mailfrom'); + $data['sitename'] = $app->get('sitename'); + $user->setParam('activate', 1); + + // Get all admin users + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(array('name', 'email', 'sendEmail', 'id'))) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('sendEmail') . ' = 1') + ->where($db->quoteName('block') . ' = 0'); + + $db->setQuery($query); + + try { + $rows = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); + + return false; + } + + // Send mail to all users with users creating permissions and receiving system emails + foreach ($rows as $row) { + $usercreator = Factory::getUser($row->id); + + if ($usercreator->authorise('core.create', 'com_users') && $usercreator->authorise('core.manage', 'com_users')) { + try { + $mailer = new MailTemplate('com_users.registration.admin.verification_request', $app->getLanguage()->getTag()); + $mailer->addTemplateData($data); + $mailer->addRecipient($row->email); + $return = $mailer->send(); + } catch (\Exception $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $return = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $return = false; + } + } + + // Check for an error. + if ($return !== true) { + $this->setError(Text::_('COM_USERS_REGISTRATION_ACTIVATION_NOTIFY_SEND_MAIL_FAILED')); + + return false; + } + } + } + } elseif (($userParams->get('useractivation') == 2) && $user->getParam('activate', 0)) { + // Admin activation is on and admin is activating the account + $user->set('activation', ''); + $user->set('block', '0'); + + // Compile the user activated notification mail values. + $data = $user->getProperties(); + $user->setParam('activate', 0); + $data['fromname'] = $app->get('fromname'); + $data['mailfrom'] = $app->get('mailfrom'); + $data['sitename'] = $app->get('sitename'); + $data['siteurl'] = Uri::base(); + $mailer = new MailTemplate('com_users.registration.user.admin_activated', $app->getLanguage()->getTag()); + $mailer->addTemplateData($data); + $mailer->addRecipient($data['email']); + + try { + $return = $mailer->send(); + } catch (\Exception $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $return = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $return = false; + } + } + + // Check for an error. + if ($return !== true) { + $this->setError(Text::_('COM_USERS_REGISTRATION_ACTIVATION_NOTIFY_SEND_MAIL_FAILED')); + + return false; + } + } else { + $user->set('activation', ''); + $user->set('block', '0'); + } + + // Store the user object. + if (!$user->save()) { + $this->setError(Text::sprintf('COM_USERS_REGISTRATION_ACTIVATION_SAVE_FAILED', $user->getError())); + + return false; + } + + return $user; + } + + /** + * Method to get the registration form data. + * + * The base form data is loaded and then an event is fired + * for users plugins to extend the data. + * + * @return mixed Data object on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function getData() + { + if ($this->data === null) { + $this->data = new \stdClass(); + $app = Factory::getApplication(); + $params = ComponentHelper::getParams('com_users'); + + // Override the base user data with any data in the session. + $temp = (array) $app->getUserState('com_users.registration.data', array()); + + // Don't load the data in this getForm call, or we'll call ourself + $form = $this->getForm(array(), false); + + foreach ($temp as $k => $v) { + // Here we could have a grouped field, let's check it + if (is_array($v)) { + $this->data->$k = new \stdClass(); + + foreach ($v as $key => $val) { + if ($form->getField($key, $k) !== false) { + $this->data->$k->$key = $val; + } + } + } elseif ($form->getField($k) !== false) { + // Only merge the field if it exists in the form. + $this->data->$k = $v; + } + } + + // Get the groups the user should be added to after registration. + $this->data->groups = array(); + + // Get the default new user group, guest or public group if not specified. + $system = $params->get('new_usertype', $params->get('guest_usergroup', 1)); + + $this->data->groups[] = $system; + + // Unset the passwords. + unset($this->data->password1, $this->data->password2); + + // Get the dispatcher and load the users plugins. + PluginHelper::importPlugin('user'); + + // Trigger the data preparation event. + Factory::getApplication()->triggerEvent('onContentPrepareData', array('com_users.registration', $this->data)); + } + + return $this->data; + } + + /** + * Method to get the registration form. + * + * The base form is loaded from XML and then an event is fired + * for users plugins to extend the form with extra fields. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.registration', 'registration', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + // When multilanguage is set, a user's default site language should also be a Content Language + if (Multilanguage::isEnabled()) { + $form->setFieldAttribute('language', 'type', 'frontend_language', 'params'); + } + + return $form; + } + + /** + * Method to get the data that should be injected in the form. + * + * @return mixed The data for the form. + * + * @since 1.6 + */ + protected function loadFormData() + { + $data = $this->getData(); + + if (Multilanguage::isEnabled() && empty($data->language)) { + $data->language = Factory::getLanguage()->getTag(); + } + + $this->preprocessData('com_users.registration', $data); + + return $data; + } + + /** + * Override preprocessForm to load the user plugin group instead of content. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @since 1.6 + * @throws \Exception if there is an error in the form event. + */ + protected function preprocessForm(Form $form, $data, $group = 'user') + { + $userParams = ComponentHelper::getParams('com_users'); + + // Add the choice for site language at registration time + if ($userParams->get('site_language') == 1 && $userParams->get('frontend_userparams') == 1) { + $form->loadFile('sitelang', false); + } + + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function populateState() + { + // Get the application object. + $app = Factory::getApplication(); + $params = $app->getParams('com_users'); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Method to save the form data. + * + * @param array $temp The form data. + * + * @return mixed The user id on success, false on failure. + * + * @since 1.6 + * @throws \Exception + */ + public function register($temp) + { + $params = ComponentHelper::getParams('com_users'); + + // Initialise the table with Joomla\CMS\User\User. + $user = new User(); + $data = (array) $this->getData(); + + // Merge in the registration data. + foreach ($temp as $k => $v) { + $data[$k] = $v; + } + + // Prepare the data for the user object. + $data['email'] = PunycodeHelper::emailToPunycode($data['email1']); + $data['password'] = $data['password1']; + $useractivation = $params->get('useractivation'); + $sendpassword = $params->get('sendpassword', 1); + + // Check if the user needs to activate their account. + if (($useractivation == 1) || ($useractivation == 2)) { + $data['activation'] = ApplicationHelper::getHash(UserHelper::genRandomPassword()); + $data['block'] = 1; + } + + // Bind the data. + if (!$user->bind($data)) { + $this->setError($user->getError()); + + return false; + } + + // Load the users plugin group. + PluginHelper::importPlugin('user'); + + // Store the data. + if (!$user->save()) { + $this->setError(Text::sprintf('COM_USERS_REGISTRATION_SAVE_FAILED', $user->getError())); + + return false; + } + + $app = Factory::getApplication(); + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Compile the notification mail values. + $data = $user->getProperties(); + $data['fromname'] = $app->get('fromname'); + $data['mailfrom'] = $app->get('mailfrom'); + $data['sitename'] = $app->get('sitename'); + $data['siteurl'] = Uri::root(); + + // Handle account activation/confirmation emails. + if ($useractivation == 2) { + // Set the link to confirm the user email. + $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; + + $data['activate'] = Route::link( + 'site', + 'index.php?option=com_users&task=registration.activate&token=' . $data['activation'], + false, + $linkMode, + true + ); + + $mailtemplate = 'com_users.registration.user.admin_activation'; + } elseif ($useractivation == 1) { + // Set the link to activate the user account. + $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; + + $data['activate'] = Route::link( + 'site', + 'index.php?option=com_users&task=registration.activate&token=' . $data['activation'], + false, + $linkMode, + true + ); + + $mailtemplate = 'com_users.registration.user.self_activation'; + } else { + $mailtemplate = 'com_users.registration.user.registration_mail'; + } + + if ($sendpassword) { + $mailtemplate .= '_w_pw'; + } + + // Try to send the registration email. + try { + $mailer = new MailTemplate($mailtemplate, $app->getLanguage()->getTag()); + $mailer->addTemplateData($data); + $mailer->addRecipient($data['email']); + $return = $mailer->send(); + } catch (\Exception $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $return = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $this->setError(Text::_('COM_MESSAGES_ERROR_MAIL_FAILED')); + + $return = false; + } + } + + // Send mail to all users with user creating permissions and receiving system emails + if (($params->get('useractivation') < 2) && ($params->get('mail_to_admin') == 1)) { + // Get all admin users + $query->clear() + ->select($db->quoteName(array('name', 'email', 'sendEmail', 'id'))) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('sendEmail') . ' = 1') + ->where($db->quoteName('block') . ' = 0'); + + $db->setQuery($query); + + try { + $rows = $db->loadObjectList(); + } catch (\RuntimeException $e) { + $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); + + return false; + } + + // Send mail to all superadministrators id + foreach ($rows as $row) { + $usercreator = Factory::getUser($row->id); + + if (!$usercreator->authorise('core.create', 'com_users') || !$usercreator->authorise('core.manage', 'com_users')) { + continue; + } + + try { + $mailer = new MailTemplate('com_users.registration.admin.new_notification', $app->getLanguage()->getTag()); + $mailer->addTemplateData($data); + $mailer->addRecipient($row->email); + $return = $mailer->send(); + } catch (\Exception $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $return = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $return = false; + } + } + + // Check for an error. + if ($return !== true) { + $this->setError(Text::_('COM_USERS_REGISTRATION_ACTIVATION_NOTIFY_SEND_MAIL_FAILED')); + + return false; + } + } + } + + // Check for an error. + if ($return !== true) { + $this->setError(Text::_('COM_USERS_REGISTRATION_SEND_MAIL_FAILED')); + + // Send a system message to administrators receiving system mails + $db = $this->getDatabase(); + $query->clear() + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('block') . ' = 0') + ->where($db->quoteName('sendEmail') . ' = 1'); + $db->setQuery($query); + + try { + $userids = $db->loadColumn(); + } catch (\RuntimeException $e) { + $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); + + return false; + } + + if (count($userids) > 0) { + $jdate = new Date(); + $dateToSql = $jdate->toSql(); + $subject = Text::_('COM_USERS_MAIL_SEND_FAILURE_SUBJECT'); + $message = Text::sprintf('COM_USERS_MAIL_SEND_FAILURE_BODY', $data['username']); + + // Build the query to add the messages + foreach ($userids as $userid) { + $values = [ + ':user_id_from', + ':user_id_to', + ':date_time', + ':subject', + ':message', + ]; + $query->clear() + ->insert($db->quoteName('#__messages')) + ->columns($db->quoteName(['user_id_from', 'user_id_to', 'date_time', 'subject', 'message'])) + ->values(implode(',', $values)); + $query->bind(':user_id_from', $userid, ParameterType::INTEGER) + ->bind(':user_id_to', $userid, ParameterType::INTEGER) + ->bind(':date_time', $dateToSql) + ->bind(':subject', $subject) + ->bind(':message', $message); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); + + return false; + } + } + } + + return false; + } + + if ($useractivation == 1) { + return 'useractivate'; + } elseif ($useractivation == 2) { + return 'adminactivate'; + } else { + return $user->id; + } + } } diff --git a/components/com_users/src/Model/RemindModel.php b/components/com_users/src/Model/RemindModel.php index 9fad52db00f4d..ca742f3f3ea8a 100644 --- a/components/com_users/src/Model/RemindModel.php +++ b/components/com_users/src/Model/RemindModel.php @@ -1,4 +1,5 @@ loadForm('com_users.remind', 'remind', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Override preprocessForm to load the user plugin group instead of content. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @throws \Exception if there is an error in the form event. - * - * @since 1.6 - */ - protected function preprocessForm(Form $form, $data, $group = 'user') - { - parent::preprocessForm($form, $data, 'user'); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - * - * @throws \Exception - */ - protected function populateState() - { - // Get the application object. - $app = Factory::getApplication(); - $params = $app->getParams('com_users'); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Send the remind username email - * - * @param array $data Array with the data received from the form - * - * @return boolean - * - * @since 1.6 - */ - public function processRemindRequest($data) - { - // Get the form. - $form = $this->getForm(); - $data['email'] = PunycodeHelper::emailToPunycode($data['email']); - - // Check for an error. - if (empty($form)) - { - return false; - } - - // Validate the data. - $data = $this->validate($form, $data); - - // Check for an error. - if ($data instanceof \Exception) - { - return false; - } - - // Check the validation results. - if ($data === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $formError) - { - $this->setError($formError->getMessage()); - } - - return false; - } - - // Find the user id for the given email address. - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__users')) - ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') - ->bind(':email', $data['email']); - - // Get the user id. - $db->setQuery($query); - - try - { - $user = $db->loadObject(); - } - catch (\RuntimeException $e) - { - $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); - - return false; - } - - // Check for a user. - if (empty($user)) - { - $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); - - return false; - } - - // Make sure the user isn't blocked. - if ($user->block) - { - $this->setError(Text::_('COM_USERS_USER_BLOCKED')); - - return false; - } - - $app = Factory::getApplication(); - - // Assemble the login link. - $link = 'index.php?option=com_users&view=login'; - $mode = $app->get('force_ssl', 0) == 2 ? 1 : (-1); - - // Put together the email template data. - $data = ArrayHelper::fromObject($user); - $data['sitename'] = $app->get('sitename'); - $data['link_text'] = Route::_($link, false, $mode); - $data['link_html'] = Route::_($link, true, $mode); - - $mailer = new MailTemplate('com_users.reminder', $app->getLanguage()->getTag()); - $mailer->addTemplateData($data); - $mailer->addRecipient($user->email, $user->name); - - // Try to send the password reset request email. - try - { - $return = $mailer->send(); - } - catch (\Exception $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $return = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $return = false; - } - } - - // Check for an error. - if ($return !== true) - { - $this->setError(Text::_('COM_USERS_MAIL_FAILED')); - - return false; - } - - Factory::getApplication()->triggerEvent('onUserAfterRemind', array($user)); - - return true; - } + /** + * Method to get the username remind request form. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|bool A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.remind', 'remind', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Override preprocessForm to load the user plugin group instead of content. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @throws \Exception if there is an error in the form event. + * + * @since 1.6 + */ + protected function preprocessForm(Form $form, $data, $group = 'user') + { + parent::preprocessForm($form, $data, 'user'); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + * + * @throws \Exception + */ + protected function populateState() + { + // Get the application object. + $app = Factory::getApplication(); + $params = $app->getParams('com_users'); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Send the remind username email + * + * @param array $data Array with the data received from the form + * + * @return boolean + * + * @since 1.6 + */ + public function processRemindRequest($data) + { + // Get the form. + $form = $this->getForm(); + $data['email'] = PunycodeHelper::emailToPunycode($data['email']); + + // Check for an error. + if (empty($form)) { + return false; + } + + // Validate the data. + $data = $this->validate($form, $data); + + // Check for an error. + if ($data instanceof \Exception) { + return false; + } + + // Check the validation results. + if ($data === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $formError) { + $this->setError($formError->getMessage()); + } + + return false; + } + + // Find the user id for the given email address. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__users')) + ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') + ->bind(':email', $data['email']); + + // Get the user id. + $db->setQuery($query); + + try { + $user = $db->loadObject(); + } catch (\RuntimeException $e) { + $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); + + return false; + } + + // Check for a user. + if (empty($user)) { + $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); + + return false; + } + + // Make sure the user isn't blocked. + if ($user->block) { + $this->setError(Text::_('COM_USERS_USER_BLOCKED')); + + return false; + } + + $app = Factory::getApplication(); + + // Assemble the login link. + $link = 'index.php?option=com_users&view=login'; + $mode = $app->get('force_ssl', 0) == 2 ? 1 : (-1); + + // Put together the email template data. + $data = ArrayHelper::fromObject($user); + $data['sitename'] = $app->get('sitename'); + $data['link_text'] = Route::_($link, false, $mode); + $data['link_html'] = Route::_($link, true, $mode); + + $mailer = new MailTemplate('com_users.reminder', $app->getLanguage()->getTag()); + $mailer->addTemplateData($data); + $mailer->addRecipient($user->email, $user->name); + + // Try to send the password reset request email. + try { + $return = $mailer->send(); + } catch (\Exception $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $return = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $return = false; + } + } + + // Check for an error. + if ($return !== true) { + $this->setError(Text::_('COM_USERS_MAIL_FAILED')); + + return false; + } + + Factory::getApplication()->triggerEvent('onUserAfterRemind', array($user)); + + return true; + } } diff --git a/components/com_users/src/Model/ResetModel.php b/components/com_users/src/Model/ResetModel.php index 53d8bed74b8f0..d9c356494873e 100644 --- a/components/com_users/src/Model/ResetModel.php +++ b/components/com_users/src/Model/ResetModel.php @@ -1,4 +1,5 @@ loadForm('com_users.reset_request', 'reset_request', array('control' => 'jform', 'load_data' => $loadData)); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the password reset complete form. - * - * @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 Form A Form object on success, false on failure - * - * @since 1.6 - */ - public function getResetCompleteForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_users.reset_complete', 'reset_complete', $options = array('control' => 'jform')); - - if (empty($form)) - { - return false; - } - - return $form; - } - - /** - * Method to get the password reset confirm form. - * - * @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 Form A Form object on success, false on failure - * - * @since 1.6 - * @throws \Exception - */ - public function getResetConfirmForm($data = array(), $loadData = true) - { - // Get the form. - $form = $this->loadForm('com_users.reset_confirm', 'reset_confirm', $options = array('control' => 'jform')); - - if (empty($form)) - { - return false; - } - else - { - $form->setValue('token', '', Factory::getApplication()->input->get('token')); - } - - return $form; - } - - /** - * Override preprocessForm to load the user plugin group instead of content. - * - * @param Form $form A Form object. - * @param mixed $data The data expected for the form. - * @param string $group The name of the plugin group to import (defaults to "content"). - * - * @return void - * - * @throws \Exception if there is an error in the form event. - * - * @since 1.6 - */ - protected function preprocessForm(Form $form, $data, $group = 'user') - { - parent::preprocessForm($form, $data, $group); - } - - /** - * Method to auto-populate the model state. - * - * Note. Calling getState in this method will result in recursion. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function populateState() - { - // Get the application object. - $params = Factory::getApplication()->getParams('com_users'); - - // Load the parameters. - $this->setState('params', $params); - } - - /** - * Save the new password after reset is done - * - * @param array $data The data expected for the form. - * - * @return mixed \Exception | boolean - * - * @since 1.6 - * @throws \Exception - */ - public function processResetComplete($data) - { - // Get the form. - $form = $this->getResetCompleteForm(); - - // Check for an error. - if ($form instanceof \Exception) - { - return $form; - } - - // Filter and validate the form data. - $data = $form->filter($data); - $return = $form->validate($data); - - // Check for an error. - if ($return instanceof \Exception) - { - return $return; - } - - // Check the validation results. - if ($return === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $formError) - { - $this->setError($formError->getMessage()); - } - - return false; - } - - // Get the token and user id from the confirmation process. - $app = Factory::getApplication(); - $token = $app->getUserState('com_users.reset.token', null); - $userId = $app->getUserState('com_users.reset.user', null); - - // Check the token and user id. - if (empty($token) || empty($userId)) - { - return new \Exception(Text::_('COM_USERS_RESET_COMPLETE_TOKENS_MISSING'), 403); - } - - // Get the user object. - $user = User::getInstance($userId); - - // Check for a user and that the tokens match. - if (empty($user) || $user->activation !== $token) - { - $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); - - return false; - } - - // Make sure the user isn't blocked. - if ($user->block) - { - $this->setError(Text::_('COM_USERS_USER_BLOCKED')); - - return false; - } - - // Check if the user is reusing the current password if required to reset their password - if ($user->requireReset == 1 && UserHelper::verifyPassword($data['password1'], $user->password)) - { - $this->setError(Text::_('JLIB_USER_ERROR_CANNOT_REUSE_PASSWORD')); - - return false; - } - - // Prepare user data. - $data['password'] = $data['password1']; - $data['activation'] = ''; - - // Update the user object. - if (!$user->bind($data)) - { - return new \Exception($user->getError(), 500); - } - - // Save the user to the database. - if (!$user->save(true)) - { - return new \Exception(Text::sprintf('COM_USERS_USER_SAVE_FAILED', $user->getError()), 500); - } - - // Destroy all active sessions for the user - UserHelper::destroyUserSessions($user->id); - - // Flush the user data from the session. - $app->setUserState('com_users.reset.token', null); - $app->setUserState('com_users.reset.user', null); - - return true; - } - - /** - * Receive the reset password request - * - * @param array $data The data expected for the form. - * - * @return mixed \Exception | boolean - * - * @since 1.6 - * @throws \Exception - */ - public function processResetConfirm($data) - { - // Get the form. - $form = $this->getResetConfirmForm(); - - // Check for an error. - if ($form instanceof \Exception) - { - return $form; - } - - // Filter and validate the form data. - $data = $form->filter($data); - $return = $form->validate($data); - - // Check for an error. - if ($return instanceof \Exception) - { - return $return; - } - - // Check the validation results. - if ($return === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $formError) - { - $this->setError($formError->getMessage()); - } - - return false; - } - - // Find the user id for the given token. - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName(['activation', 'id', 'block'])) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('username') . ' = :username') - ->bind(':username', $data['username']); - - // Get the user id. - $db->setQuery($query); - - try - { - $user = $db->loadObject(); - } - catch (\RuntimeException $e) - { - return new \Exception(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage()), 500); - } - - // Check for a user. - if (empty($user)) - { - $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); - - return false; - } - - if (!$user->activation) - { - $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); - - return false; - } - - // Verify the token - if (!UserHelper::verifyPassword($data['token'], $user->activation)) - { - $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); - - return false; - } - - // Make sure the user isn't blocked. - if ($user->block) - { - $this->setError(Text::_('COM_USERS_USER_BLOCKED')); - - return false; - } - - // Push the user data into the session. - $app = Factory::getApplication(); - $app->setUserState('com_users.reset.token', $user->activation); - $app->setUserState('com_users.reset.user', $user->id); - - return true; - } - - /** - * Method to start the password reset process. - * - * @param array $data The data expected for the form. - * - * @return mixed \Exception | boolean - * - * @since 1.6 - * @throws \Exception - */ - public function processResetRequest($data) - { - $app = Factory::getApplication(); - - // Get the form. - $form = $this->getForm(); - - $data['email'] = PunycodeHelper::emailToPunycode($data['email']); - - // Check for an error. - if ($form instanceof \Exception) - { - return $form; - } - - // Filter and validate the form data. - $data = $form->filter($data); - $return = $form->validate($data); - - // Check for an error. - if ($return instanceof \Exception) - { - return $return; - } - - // Check the validation results. - if ($return === false) - { - // Get the validation messages from the form. - foreach ($form->getErrors() as $formError) - { - $this->setError($formError->getMessage()); - } - - return false; - } - - // Find the user id for the given email address. - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') - ->bind(':email', $data['email']); - - // Get the user object. - $db->setQuery($query); - - try - { - $userId = $db->loadResult(); - } - catch (\RuntimeException $e) - { - $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); - - return false; - } - - // Check for a user. - if (empty($userId)) - { - $this->setError(Text::_('COM_USERS_INVALID_EMAIL')); - - return false; - } - - // Get the user object. - $user = User::getInstance($userId); - - // Make sure the user isn't blocked. - if ($user->block) - { - $this->setError(Text::_('COM_USERS_USER_BLOCKED')); - - return false; - } - - // Make sure the user isn't a Super Admin. - if ($user->authorise('core.admin')) - { - $this->setError(Text::_('COM_USERS_REMIND_SUPERADMIN_ERROR')); - - return false; - } - - // Make sure the user has not exceeded the reset limit - if (!$this->checkResetLimit($user)) - { - $resetLimit = (int) Factory::getApplication()->getParams()->get('reset_time'); - $this->setError(Text::plural('COM_USERS_REMIND_LIMIT_ERROR_N_HOURS', $resetLimit)); - - return false; - } - - // Set the confirmation token. - $token = ApplicationHelper::getHash(UserHelper::genRandomPassword()); - $hashedToken = UserHelper::hashPassword($token); - - $user->activation = $hashedToken; - - // Save the user to the database. - if (!$user->save(true)) - { - return new \Exception(Text::sprintf('COM_USERS_USER_SAVE_FAILED', $user->getError()), 500); - } - - // Assemble the password reset confirmation link. - $mode = $app->get('force_ssl', 0) == 2 ? 1 : (-1); - $link = 'index.php?option=com_users&view=reset&layout=confirm&token=' . $token; - - // Put together the email template data. - $data = $user->getProperties(); - $data['sitename'] = $app->get('sitename'); - $data['link_text'] = Route::_($link, false, $mode); - $data['link_html'] = Route::_($link, true, $mode); - $data['token'] = $token; - - $mailer = new MailTemplate('com_users.password_reset', $app->getLanguage()->getTag()); - $mailer->addTemplateData($data); - $mailer->addRecipient($user->email, $user->name); - - // Try to send the password reset request email. - try - { - $return = $mailer->send(); - } - catch (\Exception $exception) - { - try - { - Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); - - $return = false; - } - catch (\RuntimeException $exception) - { - Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); - - $return = false; - } - } - - // Check for an error. - if ($return !== true) - { - return new \Exception(Text::_('COM_USERS_MAIL_FAILED'), 500); - } - else - { - return true; - } - } - - /** - * Method to check if user reset limit has been exceeded within the allowed time period. - * - * @param User $user User doing the password reset - * - * @return boolean true if user can do the reset, false if limit exceeded - * - * @since 2.5 - * @throws \Exception - */ - public function checkResetLimit($user) - { - $params = Factory::getApplication()->getParams(); - $maxCount = (int) $params->get('reset_count'); - $resetHours = (int) $params->get('reset_time'); - $result = true; - - $lastResetTime = strtotime($user->lastResetTime) ?: 0; - $hoursSinceLastReset = (strtotime(Factory::getDate()->toSql()) - $lastResetTime) / 3600; - - if ($hoursSinceLastReset > $resetHours) - { - // If it's been long enough, start a new reset count - $user->lastResetTime = Factory::getDate()->toSql(); - $user->resetCount = 1; - } - elseif ($user->resetCount < $maxCount) - { - // If we are under the max count, just increment the counter - ++$user->resetCount; - } - else - { - // At this point, we know we have exceeded the maximum resets for the time period - $result = false; - } - - return $result; - } + /** + * Method to get the password reset request form. + * + * The base form is loaded from XML and then an event is fired + * for users plugins to extend the form with extra fields. + * + * @param array $data An optional array of data for the form to interrogate. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.reset_request', 'reset_request', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the password reset complete form. + * + * @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 Form A Form object on success, false on failure + * + * @since 1.6 + */ + public function getResetCompleteForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.reset_complete', 'reset_complete', $options = array('control' => 'jform')); + + if (empty($form)) { + return false; + } + + return $form; + } + + /** + * Method to get the password reset confirm form. + * + * @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 Form A Form object on success, false on failure + * + * @since 1.6 + * @throws \Exception + */ + public function getResetConfirmForm($data = array(), $loadData = true) + { + // Get the form. + $form = $this->loadForm('com_users.reset_confirm', 'reset_confirm', $options = array('control' => 'jform')); + + if (empty($form)) { + return false; + } else { + $form->setValue('token', '', Factory::getApplication()->input->get('token')); + } + + return $form; + } + + /** + * Override preprocessForm to load the user plugin group instead of content. + * + * @param Form $form A Form object. + * @param mixed $data The data expected for the form. + * @param string $group The name of the plugin group to import (defaults to "content"). + * + * @return void + * + * @throws \Exception if there is an error in the form event. + * + * @since 1.6 + */ + protected function preprocessForm(Form $form, $data, $group = 'user') + { + parent::preprocessForm($form, $data, $group); + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function populateState() + { + // Get the application object. + $params = Factory::getApplication()->getParams('com_users'); + + // Load the parameters. + $this->setState('params', $params); + } + + /** + * Save the new password after reset is done + * + * @param array $data The data expected for the form. + * + * @return mixed \Exception | boolean + * + * @since 1.6 + * @throws \Exception + */ + public function processResetComplete($data) + { + // Get the form. + $form = $this->getResetCompleteForm(); + + // Check for an error. + if ($form instanceof \Exception) { + return $form; + } + + // Filter and validate the form data. + $data = $form->filter($data); + $return = $form->validate($data); + + // Check for an error. + if ($return instanceof \Exception) { + return $return; + } + + // Check the validation results. + if ($return === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $formError) { + $this->setError($formError->getMessage()); + } + + return false; + } + + // Get the token and user id from the confirmation process. + $app = Factory::getApplication(); + $token = $app->getUserState('com_users.reset.token', null); + $userId = $app->getUserState('com_users.reset.user', null); + + // Check the token and user id. + if (empty($token) || empty($userId)) { + return new \Exception(Text::_('COM_USERS_RESET_COMPLETE_TOKENS_MISSING'), 403); + } + + // Get the user object. + $user = User::getInstance($userId); + + // Check for a user and that the tokens match. + if (empty($user) || $user->activation !== $token) { + $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); + + return false; + } + + // Make sure the user isn't blocked. + if ($user->block) { + $this->setError(Text::_('COM_USERS_USER_BLOCKED')); + + return false; + } + + // Check if the user is reusing the current password if required to reset their password + if ($user->requireReset == 1 && UserHelper::verifyPassword($data['password1'], $user->password)) { + $this->setError(Text::_('JLIB_USER_ERROR_CANNOT_REUSE_PASSWORD')); + + return false; + } + + // Prepare user data. + $data['password'] = $data['password1']; + $data['activation'] = ''; + + // Update the user object. + if (!$user->bind($data)) { + return new \Exception($user->getError(), 500); + } + + // Save the user to the database. + if (!$user->save(true)) { + return new \Exception(Text::sprintf('COM_USERS_USER_SAVE_FAILED', $user->getError()), 500); + } + + // Destroy all active sessions for the user + UserHelper::destroyUserSessions($user->id); + + // Flush the user data from the session. + $app->setUserState('com_users.reset.token', null); + $app->setUserState('com_users.reset.user', null); + + return true; + } + + /** + * Receive the reset password request + * + * @param array $data The data expected for the form. + * + * @return mixed \Exception | boolean + * + * @since 1.6 + * @throws \Exception + */ + public function processResetConfirm($data) + { + // Get the form. + $form = $this->getResetConfirmForm(); + + // Check for an error. + if ($form instanceof \Exception) { + return $form; + } + + // Filter and validate the form data. + $data = $form->filter($data); + $return = $form->validate($data); + + // Check for an error. + if ($return instanceof \Exception) { + return $return; + } + + // Check the validation results. + if ($return === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $formError) { + $this->setError($formError->getMessage()); + } + + return false; + } + + // Find the user id for the given token. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['activation', 'id', 'block'])) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('username') . ' = :username') + ->bind(':username', $data['username']); + + // Get the user id. + $db->setQuery($query); + + try { + $user = $db->loadObject(); + } catch (\RuntimeException $e) { + return new \Exception(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage()), 500); + } + + // Check for a user. + if (empty($user)) { + $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); + + return false; + } + + if (!$user->activation) { + $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); + + return false; + } + + // Verify the token + if (!UserHelper::verifyPassword($data['token'], $user->activation)) { + $this->setError(Text::_('COM_USERS_USER_NOT_FOUND')); + + return false; + } + + // Make sure the user isn't blocked. + if ($user->block) { + $this->setError(Text::_('COM_USERS_USER_BLOCKED')); + + return false; + } + + // Push the user data into the session. + $app = Factory::getApplication(); + $app->setUserState('com_users.reset.token', $user->activation); + $app->setUserState('com_users.reset.user', $user->id); + + return true; + } + + /** + * Method to start the password reset process. + * + * @param array $data The data expected for the form. + * + * @return mixed \Exception | boolean + * + * @since 1.6 + * @throws \Exception + */ + public function processResetRequest($data) + { + $app = Factory::getApplication(); + + // Get the form. + $form = $this->getForm(); + + $data['email'] = PunycodeHelper::emailToPunycode($data['email']); + + // Check for an error. + if ($form instanceof \Exception) { + return $form; + } + + // Filter and validate the form data. + $data = $form->filter($data); + $return = $form->validate($data); + + // Check for an error. + if ($return instanceof \Exception) { + return $return; + } + + // Check the validation results. + if ($return === false) { + // Get the validation messages from the form. + foreach ($form->getErrors() as $formError) { + $this->setError($formError->getMessage()); + } + + return false; + } + + // Find the user id for the given email address. + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where('LOWER(' . $db->quoteName('email') . ') = LOWER(:email)') + ->bind(':email', $data['email']); + + // Get the user object. + $db->setQuery($query); + + try { + $userId = $db->loadResult(); + } catch (\RuntimeException $e) { + $this->setError(Text::sprintf('COM_USERS_DATABASE_ERROR', $e->getMessage())); + + return false; + } + + // Check for a user. + if (empty($userId)) { + $this->setError(Text::_('COM_USERS_INVALID_EMAIL')); + + return false; + } + + // Get the user object. + $user = User::getInstance($userId); + + // Make sure the user isn't blocked. + if ($user->block) { + $this->setError(Text::_('COM_USERS_USER_BLOCKED')); + + return false; + } + + // Make sure the user isn't a Super Admin. + if ($user->authorise('core.admin')) { + $this->setError(Text::_('COM_USERS_REMIND_SUPERADMIN_ERROR')); + + return false; + } + + // Make sure the user has not exceeded the reset limit + if (!$this->checkResetLimit($user)) { + $resetLimit = (int) Factory::getApplication()->getParams()->get('reset_time'); + $this->setError(Text::plural('COM_USERS_REMIND_LIMIT_ERROR_N_HOURS', $resetLimit)); + + return false; + } + + // Set the confirmation token. + $token = ApplicationHelper::getHash(UserHelper::genRandomPassword()); + $hashedToken = UserHelper::hashPassword($token); + + $user->activation = $hashedToken; + + // Save the user to the database. + if (!$user->save(true)) { + return new \Exception(Text::sprintf('COM_USERS_USER_SAVE_FAILED', $user->getError()), 500); + } + + // Assemble the password reset confirmation link. + $mode = $app->get('force_ssl', 0) == 2 ? 1 : (-1); + $link = 'index.php?option=com_users&view=reset&layout=confirm&token=' . $token; + + // Put together the email template data. + $data = $user->getProperties(); + $data['sitename'] = $app->get('sitename'); + $data['link_text'] = Route::_($link, false, $mode); + $data['link_html'] = Route::_($link, true, $mode); + $data['token'] = $token; + + $mailer = new MailTemplate('com_users.password_reset', $app->getLanguage()->getTag()); + $mailer->addTemplateData($data); + $mailer->addRecipient($user->email, $user->name); + + // Try to send the password reset request email. + try { + $return = $mailer->send(); + } catch (\Exception $exception) { + try { + Log::add(Text::_($exception->getMessage()), Log::WARNING, 'jerror'); + + $return = false; + } catch (\RuntimeException $exception) { + Factory::getApplication()->enqueueMessage(Text::_($exception->errorMessage()), 'warning'); + + $return = false; + } + } + + // Check for an error. + if ($return !== true) { + return new \Exception(Text::_('COM_USERS_MAIL_FAILED'), 500); + } else { + return true; + } + } + + /** + * Method to check if user reset limit has been exceeded within the allowed time period. + * + * @param User $user User doing the password reset + * + * @return boolean true if user can do the reset, false if limit exceeded + * + * @since 2.5 + * @throws \Exception + */ + public function checkResetLimit($user) + { + $params = Factory::getApplication()->getParams(); + $maxCount = (int) $params->get('reset_count'); + $resetHours = (int) $params->get('reset_time'); + $result = true; + + $lastResetTime = strtotime($user->lastResetTime) ?: 0; + $hoursSinceLastReset = (strtotime(Factory::getDate()->toSql()) - $lastResetTime) / 3600; + + if ($hoursSinceLastReset > $resetHours) { + // If it's been long enough, start a new reset count + $user->lastResetTime = Factory::getDate()->toSql(); + $user->resetCount = 1; + } elseif ($user->resetCount < $maxCount) { + // If we are under the max count, just increment the counter + ++$user->resetCount; + } else { + // At this point, we know we have exceeded the maximum resets for the time period + $result = false; + } + + return $result; + } } diff --git a/components/com_users/src/Rule/LoginUniqueFieldRule.php b/components/com_users/src/Rule/LoginUniqueFieldRule.php index 04310e7dc23ab..f095e4e177ee7 100644 --- a/components/com_users/src/Rule/LoginUniqueFieldRule.php +++ b/components/com_users/src/Rule/LoginUniqueFieldRule.php @@ -1,4 +1,5 @@ ` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. - * @param Form $form The form object for which the field is being tested. - * - * @return boolean True if the value is valid, false otherwise. - * - * @since 3.6 - */ - public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) - { - $loginRedirectUrl = $input['params']->login_redirect_url; - $loginRedirectMenuitem = $input['params']->login_redirect_menuitem; - - if ($form === null) - { - throw new \InvalidArgumentException(sprintf('The value for $form must not be null in %s', get_class($this))); - } - - if ($input === null) - { - throw new \InvalidArgumentException(sprintf('The value for $input must not be null in %s', get_class($this))); - } - - // Test the input values for login. - if ($loginRedirectUrl != '' && $loginRedirectMenuitem != '') - { - return false; - } - - return true; - } + /** + * Method to test if two fields have a value in order to use only one field. + * To use this rule, the form + * XML needs a validate attribute of loginuniquefield and a field attribute + * that is equal to the field to test against. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form $form The form object for which the field is being tested. + * + * @return boolean True if the value is valid, false otherwise. + * + * @since 3.6 + */ + public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + $loginRedirectUrl = $input['params']->login_redirect_url; + $loginRedirectMenuitem = $input['params']->login_redirect_menuitem; + + if ($form === null) { + throw new \InvalidArgumentException(sprintf('The value for $form must not be null in %s', get_class($this))); + } + + if ($input === null) { + throw new \InvalidArgumentException(sprintf('The value for $input must not be null in %s', get_class($this))); + } + + // Test the input values for login. + if ($loginRedirectUrl != '' && $loginRedirectMenuitem != '') { + return false; + } + + return true; + } } diff --git a/components/com_users/src/Rule/LogoutUniqueFieldRule.php b/components/com_users/src/Rule/LogoutUniqueFieldRule.php index 89dd4eaf95fd7..d621f79d7b84f 100644 --- a/components/com_users/src/Rule/LogoutUniqueFieldRule.php +++ b/components/com_users/src/Rule/LogoutUniqueFieldRule.php @@ -1,4 +1,5 @@ ` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. - * @param Form $form The form object for which the field is being tested. - * - * @return boolean True if the value is valid, false otherwise. - * - * @since 3.6 - */ - public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) - { - $logoutRedirectUrl = $input['params']->logout_redirect_url; - $logoutRedirectMenuitem = $input['params']->logout_redirect_menuitem; - - if ($form === null) - { - throw new \InvalidArgumentException(sprintf('The value for $form must not be null in %s', get_class($this))); - } - - if ($input === null) - { - throw new \InvalidArgumentException(sprintf('The value for $input must not be null in %s', get_class($this))); - } - - // Test the input values for logout. - if ($logoutRedirectUrl != '' && $logoutRedirectMenuitem != '') - { - return false; - } - - return true; - } + /** + * Method to test if two fields have a value in order to use only one field. + * To use this rule, the form + * XML needs a validate attribute of logoutuniquefield and a field attribute + * that is equal to the field to test against. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form $form The form object for which the field is being tested. + * + * @return boolean True if the value is valid, false otherwise. + * + * @since 3.6 + */ + public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + $logoutRedirectUrl = $input['params']->logout_redirect_url; + $logoutRedirectMenuitem = $input['params']->logout_redirect_menuitem; + + if ($form === null) { + throw new \InvalidArgumentException(sprintf('The value for $form must not be null in %s', get_class($this))); + } + + if ($input === null) { + throw new \InvalidArgumentException(sprintf('The value for $input must not be null in %s', get_class($this))); + } + + // Test the input values for logout. + if ($logoutRedirectUrl != '' && $logoutRedirectMenuitem != '') { + return false; + } + + return true; + } } diff --git a/components/com_users/src/Service/Router.php b/components/com_users/src/Service/Router.php index 43a85cf4ebe86..d15ee4cf15bda 100644 --- a/components/com_users/src/Service/Router.php +++ b/components/com_users/src/Service/Router.php @@ -1,4 +1,5 @@ registerView(new RouterViewConfiguration('login')); - $profile = new RouterViewConfiguration('profile'); - $profile->addLayout('edit'); - $this->registerView($profile); - $this->registerView(new RouterViewConfiguration('registration')); - $this->registerView(new RouterViewConfiguration('remind')); - $this->registerView(new RouterViewConfiguration('reset')); - $this->registerView(new RouterViewConfiguration('callback')); - $this->registerView(new RouterViewConfiguration('captive')); - $this->registerView(new RouterViewConfiguration('methods')); + /** + * Users Component router constructor + * + * @param SiteApplication $app The application object + * @param AbstractMenu $menu The menu object to work with + */ + public function __construct(SiteApplication $app, AbstractMenu $menu) + { + $this->registerView(new RouterViewConfiguration('login')); + $profile = new RouterViewConfiguration('profile'); + $profile->addLayout('edit'); + $this->registerView($profile); + $this->registerView(new RouterViewConfiguration('registration')); + $this->registerView(new RouterViewConfiguration('remind')); + $this->registerView(new RouterViewConfiguration('reset')); + $this->registerView(new RouterViewConfiguration('callback')); + $this->registerView(new RouterViewConfiguration('captive')); + $this->registerView(new RouterViewConfiguration('methods')); - $method = new RouterViewConfiguration('method'); - $method->setKey('id'); - $this->registerView($method); + $method = new RouterViewConfiguration('method'); + $method->setKey('id'); + $this->registerView($method); - parent::__construct($app, $menu); + parent::__construct($app, $menu); - $this->attachRule(new MenuRules($this)); - $this->attachRule(new StandardRules($this)); - $this->attachRule(new NomenuRules($this)); - } + $this->attachRule(new MenuRules($this)); + $this->attachRule(new StandardRules($this)); + $this->attachRule(new NomenuRules($this)); + } - /** - * Get the method ID from a URL segment - * - * @param string $segment The URL segment - * @param array $query The URL query parameters - * - * @return integer - * @since 4.2.0 - */ - public function getMethodId($segment, $query) - { - return (int) $segment; - } + /** + * Get the method ID from a URL segment + * + * @param string $segment The URL segment + * @param array $query The URL query parameters + * + * @return integer + * @since 4.2.0 + */ + public function getMethodId($segment, $query) + { + return (int) $segment; + } - /** - * Get a segment from a method ID - * - * @param integer $id The method ID - * @param array $query The URL query parameters - * - * @return int[] - * @since 4.2.0 - */ - public function getMethodSegment($id, $query) - { - return [$id => (int) $id]; - } + /** + * Get a segment from a method ID + * + * @param integer $id The method ID + * @param array $query The URL query parameters + * + * @return int[] + * @since 4.2.0 + */ + public function getMethodSegment($id, $query) + { + return [$id => (int) $id]; + } } diff --git a/components/com_users/src/View/Captive/HtmlView.php b/components/com_users/src/View/Captive/HtmlView.php index d43d325412bf1..d6caf754d4c35 100644 --- a/components/com_users/src/View/Captive/HtmlView.php +++ b/components/com_users/src/View/Captive/HtmlView.php @@ -1,4 +1,5 @@ user = $this->getCurrentUser(); - $this->form = $this->get('Form'); - $this->state = $this->get('State'); - $this->params = $this->state->get('params'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check for layout override - $active = Factory::getApplication()->getMenu()->getActive(); - - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - - $this->extraButtons = AuthenticationHelper::getLoginButtons('com-users-login__form'); - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function prepareDocument() - { - $login = $this->getCurrentUser()->get('guest') ? true : false; - - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', $login ? Text::_('JLOGIN') : Text::_('JLOGOUT')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + */ + protected $params; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The logged in user + * + * @var User + */ + protected $user; + + /** + * The page class suffix + * + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * No longer used + * + * @var boolean + * @since 4.0.0 + * @deprecated 4.2.0 Will be removed in 5.0. + */ + protected $tfa = false; + + /** + * Additional buttons to show on the login page + * + * @var array + * @since 4.0.0 + */ + protected $extraButtons = []; + + /** + * Method to display the view. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.5 + * @throws \Exception + */ + public function display($tpl = null) + { + // Get the view data. + $this->user = $this->getCurrentUser(); + $this->form = $this->get('Form'); + $this->state = $this->get('State'); + $this->params = $this->state->get('params'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check for layout override + $active = Factory::getApplication()->getMenu()->getActive(); + + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } + + $this->extraButtons = AuthenticationHelper::getLoginButtons('com-users-login__form'); + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function prepareDocument() + { + $login = $this->getCurrentUser()->get('guest') ? true : false; + + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', $login ? Text::_('JLOGIN') : Text::_('JLOGOUT')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_users/src/View/Method/HtmlView.php b/components/com_users/src/View/Method/HtmlView.php index c5e837fb01bf6..e1722c59dd406 100644 --- a/components/com_users/src/View/Method/HtmlView.php +++ b/components/com_users/src/View/Method/HtmlView.php @@ -1,4 +1,5 @@ getCurrentUser(); - - // Get the view data. - $this->data = $this->get('Data'); - $this->form = $this->getModel()->getForm(new CMSObject(['id' => $user->id])); - $this->state = $this->get('State'); - $this->params = $this->state->get('params'); - $this->mfaConfigurationUI = Mfa::getConfigurationInterface($user); - $this->db = Factory::getDbo(); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // View also takes responsibility for checking if the user logged in with remember me. - $cookieLogin = $user->get('cookieLogin'); - - if (!empty($cookieLogin)) - { - // If so, the user must login to edit the password and other data. - // What should happen here? Should we force a logout which destroys the cookies? - $app = Factory::getApplication(); - $app->enqueueMessage(Text::_('JGLOBAL_REMEMBER_MUST_LOGIN'), 'message'); - $app->redirect(Route::_('index.php?option=com_users&view=login', false)); - - return false; - } - - // Check if a user was found. - if (!$this->data->id) - { - throw new \Exception(Text::_('JERROR_USERS_PROFILE_NOT_FOUND'), 404); - } - - PluginHelper::importPlugin('content'); - $this->data->text = ''; - Factory::getApplication()->triggerEvent('onContentPrepare', array ('com_users.user', &$this->data, &$this->data->params, 0)); - unset($this->data->text); - - // Check for layout from menu item. - $query = Factory::getApplication()->getMenu()->getActive()->query; - - if (isset($query['layout']) && isset($query['option']) && $query['option'] === 'com_users' - && isset($query['view']) && $query['view'] === 'profile') - { - $this->setLayout($query['layout']); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $this->getCurrentUser()->name)); - } - else - { - $this->params->def('page_heading', Text::_('COM_USERS_PROFILE')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * Profile form data for the user + * + * @var User + */ + protected $data; + + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + */ + protected $params; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * An instance of DatabaseDriver. + * + * @var DatabaseDriver + * @since 3.6.3 + * + * @deprecated 5.0 Will be removed without replacement + */ + protected $db; + + /** + * The page class suffix + * + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The Multi-factor Authentication configuration interface for the user. + * + * @var string|null + * @since 4.2.0 + */ + protected $mfaConfigurationUI; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void|boolean + * + * @since 1.6 + * @throws \Exception + */ + public function display($tpl = null) + { + $user = $this->getCurrentUser(); + + // Get the view data. + $this->data = $this->get('Data'); + $this->form = $this->getModel()->getForm(new CMSObject(['id' => $user->id])); + $this->state = $this->get('State'); + $this->params = $this->state->get('params'); + $this->mfaConfigurationUI = Mfa::getConfigurationInterface($user); + $this->db = Factory::getDbo(); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // View also takes responsibility for checking if the user logged in with remember me. + $cookieLogin = $user->get('cookieLogin'); + + if (!empty($cookieLogin)) { + // If so, the user must login to edit the password and other data. + // What should happen here? Should we force a logout which destroys the cookies? + $app = Factory::getApplication(); + $app->enqueueMessage(Text::_('JGLOBAL_REMEMBER_MUST_LOGIN'), 'message'); + $app->redirect(Route::_('index.php?option=com_users&view=login', false)); + + return false; + } + + // Check if a user was found. + if (!$this->data->id) { + throw new \Exception(Text::_('JERROR_USERS_PROFILE_NOT_FOUND'), 404); + } + + PluginHelper::importPlugin('content'); + $this->data->text = ''; + Factory::getApplication()->triggerEvent('onContentPrepare', array ('com_users.user', &$this->data, &$this->data->params, 0)); + unset($this->data->text); + + // Check for layout from menu item. + $query = Factory::getApplication()->getMenu()->getActive()->query; + + if ( + isset($query['layout']) && isset($query['option']) && $query['option'] === 'com_users' + && isset($query['view']) && $query['view'] === 'profile' + ) { + $this->setLayout($query['layout']); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', '')); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $this->getCurrentUser()->name)); + } else { + $this->params->def('page_heading', Text::_('COM_USERS_PROFILE')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_users/src/View/Registration/HtmlView.php b/components/com_users/src/View/Registration/HtmlView.php index 98741cff3f6ab..cb4791fe0d515 100644 --- a/components/com_users/src/View/Registration/HtmlView.php +++ b/components/com_users/src/View/Registration/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->data = $this->get('Data'); - $this->state = $this->get('State'); - $this->params = $this->state->get('params'); - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check for layout override - $active = Factory::getApplication()->getMenu()->getActive(); - - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_USERS_REGISTRATION')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * Registration form data + * + * @var \stdClass|false + */ + protected $data; + + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + */ + protected $params; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The HtmlDocument instance + * + * @var HtmlDocument + */ + public $document; + + /** + * The page class suffix + * + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * Method to display the view. + * + * @param string $tpl The template file to include + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + public function display($tpl = null) + { + // Get the view data. + $this->form = $this->get('Form'); + $this->data = $this->get('Data'); + $this->state = $this->get('State'); + $this->params = $this->state->get('params'); + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check for layout override + $active = Factory::getApplication()->getMenu()->getActive(); + + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_USERS_REGISTRATION')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_users/src/View/Remind/HtmlView.php b/components/com_users/src/View/Remind/HtmlView.php index 8855d9d1c80e9..be4b879c6fb82 100644 --- a/components/com_users/src/View/Remind/HtmlView.php +++ b/components/com_users/src/View/Remind/HtmlView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); - $this->state = $this->get('State'); - $this->params = $this->state->params; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Check for layout override - $active = Factory::getApplication()->getMenu()->getActive(); - - if (isset($active->query['layout'])) - { - $this->setLayout($active->query['layout']); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_USERS_REMIND')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + */ + protected $params; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The page class suffix + * + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * Method to display the view. + * + * @param string $tpl The template file to include + * + * @return mixed + * + * @since 1.5 + * @throws \Exception + */ + public function display($tpl = null) + { + // Get the view data. + $this->form = $this->get('Form'); + $this->state = $this->get('State'); + $this->params = $this->state->params; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Check for layout override + $active = Factory::getApplication()->getMenu()->getActive(); + + if (isset($active->query['layout'])) { + $this->setLayout($active->query['layout']); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_USERS_REMIND')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_users/src/View/Reset/HtmlView.php b/components/com_users/src/View/Reset/HtmlView.php index c5266911f93ed..bf644b3946759 100644 --- a/components/com_users/src/View/Reset/HtmlView.php +++ b/components/com_users/src/View/Reset/HtmlView.php @@ -1,4 +1,5 @@ getLayout(); - - // Check that the name is valid - has an associated model. - if (!in_array($name, array('confirm', 'complete'))) - { - $name = 'default'; - } - - if ('default' === $name) - { - $formname = 'Form'; - } - else - { - $formname = ucfirst($this->_name) . ucfirst($name) . 'Form'; - } - - // Get the view data. - $this->form = $this->get($formname); - $this->state = $this->get('State'); - $this->params = $this->state->params; - - // Check for errors. - if (count($errors = $this->get('Errors'))) - { - throw new GenericDataException(implode("\n", $errors), 500); - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); - - $this->prepareDocument(); - - parent::display($tpl); - } - - /** - * Prepares the document. - * - * @return void - * - * @since 1.6 - * @throws \Exception - */ - protected function prepareDocument() - { - // Because the application sets a default page title, - // we need to get it from the menu item itself - $menu = Factory::getApplication()->getMenu()->getActive(); - - if ($menu) - { - $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); - } - else - { - $this->params->def('page_heading', Text::_('COM_USERS_RESET')); - } - - $this->setDocumentTitle($this->params->get('page_title', '')); - - if ($this->params->get('menu-meta_description')) - { - $this->document->setDescription($this->params->get('menu-meta_description')); - } - - if ($this->params->get('robots')) - { - $this->document->setMetaData('robots', $this->params->get('robots')); - } - } + /** + * The Form object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + */ + protected $params; + + /** + * The model state + * + * @var CMSObject + */ + protected $state; + + /** + * The page class suffix + * + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * Method to display the view. + * + * @param string $tpl The template file to include + * + * @return mixed + * + * @since 1.5 + */ + public function display($tpl = null) + { + // This name will be used to get the model + $name = $this->getLayout(); + + // Check that the name is valid - has an associated model. + if (!in_array($name, array('confirm', 'complete'))) { + $name = 'default'; + } + + if ('default' === $name) { + $formname = 'Form'; + } else { + $formname = ucfirst($this->_name) . ucfirst($name) . 'Form'; + } + + // Get the view data. + $this->form = $this->get($formname); + $this->state = $this->get('State'); + $this->params = $this->state->params; + + // Check for errors. + if (count($errors = $this->get('Errors'))) { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($this->params->get('pageclass_sfx', ''), ENT_COMPAT, 'UTF-8'); + + $this->prepareDocument(); + + parent::display($tpl); + } + + /** + * Prepares the document. + * + * @return void + * + * @since 1.6 + * @throws \Exception + */ + protected function prepareDocument() + { + // Because the application sets a default page title, + // we need to get it from the menu item itself + $menu = Factory::getApplication()->getMenu()->getActive(); + + if ($menu) { + $this->params->def('page_heading', $this->params->get('page_title', $menu->title)); + } else { + $this->params->def('page_heading', Text::_('COM_USERS_RESET')); + } + + $this->setDocumentTitle($this->params->get('page_title', '')); + + if ($this->params->get('menu-meta_description')) { + $this->document->setDescription($this->params->get('menu-meta_description')); + } + + if ($this->params->get('robots')) { + $this->document->setMetaData('robots', $this->params->get('robots')); + } + } } diff --git a/components/com_users/tmpl/login/default.php b/components/com_users/tmpl/login/default.php index a40eba9805255..0fbcc008e8855 100644 --- a/components/com_users/tmpl/login/default.php +++ b/components/com_users/tmpl/login/default.php @@ -1,4 +1,5 @@ user->get('cookieLogin'); -if (!empty($cookieLogin) || $this->user->get('guest')) -{ - // The user is not logged in or needs to provide a password. - echo $this->loadTemplate('login'); -} -else -{ - // The user is already logged in. - echo $this->loadTemplate('logout'); +if (!empty($cookieLogin) || $this->user->get('guest')) { + // The user is not logged in or needs to provide a password. + echo $this->loadTemplate('login'); +} else { + // The user is already logged in. + echo $this->loadTemplate('logout'); } diff --git a/components/com_users/tmpl/login/default_login.php b/components/com_users/tmpl/login/default_login.php index 8219bafdea841..ec60bda63bee5 100644 --- a/components/com_users/tmpl/login/default_login.php +++ b/components/com_users/tmpl/login/default_login.php @@ -1,4 +1,5 @@ diff --git a/components/com_users/tmpl/login/default_logout.php b/components/com_users/tmpl/login/default_logout.php index 0f199854dd435..099ab56f5cb1e 100644 --- a/components/com_users/tmpl/login/default_logout.php +++ b/components/com_users/tmpl/login/default_logout.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - - - params->get('logoutdescription_show') == 1 && str_replace(' ', '', $this->params->get('logout_description')) != '')|| $this->params->get('logout_image') != '') : ?> -
    - - - params->get('logoutdescription_show') == 1) : ?> - params->get('logout_description'); ?> - - - params->get('logout_image') != '') : ?> - params->get('logout_image'), empty($this->params->get('logout_image_alt')) && empty($this->params->get('logout_image_alt_empty')) ? false : $this->params->get('logout_image_alt'), ['class' => 'com-users-logout__image thumbnail float-end logout-image']); ?> - - - params->get('logoutdescription_show') == 1 && str_replace(' ', '', $this->params->get('logout_description')) != '')|| $this->params->get('logout_image') != '') : ?> -
    - - -
    -
    -
    - -
    -
    - params->get('logout_redirect_url')) : ?> - - - - - -
    + params->get('show_page_heading')) : ?> + + + + params->get('logoutdescription_show') == 1 && str_replace(' ', '', $this->params->get('logout_description')) != '') || $this->params->get('logout_image') != '') : ?> +
    + + + params->get('logoutdescription_show') == 1) : ?> + params->get('logout_description'); ?> + + + params->get('logout_image') != '') : ?> + params->get('logout_image'), empty($this->params->get('logout_image_alt')) && empty($this->params->get('logout_image_alt_empty')) ? false : $this->params->get('logout_image_alt'), ['class' => 'com-users-logout__image thumbnail float-end logout-image']); ?> + + + params->get('logoutdescription_show') == 1 && str_replace(' ', '', $this->params->get('logout_description')) != '') || $this->params->get('logout_image') != '') : ?> +
    + + +
    +
    +
    + +
    +
    + params->get('logout_redirect_url')) : ?> + + + + + +
    diff --git a/components/com_users/tmpl/profile/default.php b/components/com_users/tmpl/profile/default.php index e6b2af7c92a82..a534e5b458e82 100644 --- a/components/com_users/tmpl/profile/default.php +++ b/components/com_users/tmpl/profile/default.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - + params->get('show_page_heading')) : ?> + + - id == $this->data->id) : ?> - - + id == $this->data->id) : ?> + + - loadTemplate('core'); ?> - loadTemplate('params'); ?> - loadTemplate('custom'); ?> + loadTemplate('core'); ?> + loadTemplate('params'); ?> + loadTemplate('custom'); ?>
    diff --git a/components/com_users/tmpl/profile/default_core.php b/components/com_users/tmpl/profile/default_core.php index 01d085e257bf1..066094aaf0db3 100644 --- a/components/com_users/tmpl/profile/default_core.php +++ b/components/com_users/tmpl/profile/default_core.php @@ -1,4 +1,5 @@
    - - - -
    -
    - -
    -
    - escape($this->data->name); ?> -
    -
    - -
    -
    - escape($this->data->username); ?> -
    -
    - -
    -
    - data->registerDate, Text::_('DATE_FORMAT_LC1')); ?> -
    -
    - -
    - data->lastvisitDate !== null) : ?> -
    - data->lastvisitDate, Text::_('DATE_FORMAT_LC1')); ?> -
    - -
    - -
    - -
    + + + +
    +
    + +
    +
    + escape($this->data->name); ?> +
    +
    + +
    +
    + escape($this->data->username); ?> +
    +
    + +
    +
    + data->registerDate, Text::_('DATE_FORMAT_LC1')); ?> +
    +
    + +
    + data->lastvisitDate !== null) : ?> +
    + data->lastvisitDate, Text::_('DATE_FORMAT_LC1')); ?> +
    + +
    + +
    + +
    diff --git a/components/com_users/tmpl/profile/default_custom.php b/components/com_users/tmpl/profile/default_custom.php index 5106b09f5ef2f..6bf103ecd34b1 100644 --- a/components/com_users/tmpl/profile/default_custom.php +++ b/components/com_users/tmpl/profile/default_custom.php @@ -1,4 +1,5 @@ form->getFieldsets(); -if (isset($fieldsets['core'])) -{ - unset($fieldsets['core']); +if (isset($fieldsets['core'])) { + unset($fieldsets['core']); } -if (isset($fieldsets['params'])) -{ - unset($fieldsets['params']); +if (isset($fieldsets['params'])) { + unset($fieldsets['params']); } $tmp = $this->data->jcfields ?? array(); $customFields = array(); -foreach ($tmp as $customField) -{ - $customFields[$customField->name] = $customField; +foreach ($tmp as $customField) { + $customFields[$customField->name] = $customField; } ?> $fieldset) : ?> - form->getFieldset($group); ?> - -
    - label) && ($legend = trim(Text::_($fieldset->label))) !== '') : ?> - - - description) && trim($fieldset->description)) : ?> -

    escape(Text::_($fieldset->description)); ?>

    - -
    - - hidden && $field->type !== 'Spacer') : ?> -
    - title; ?> -
    -
    - fieldname, $customFields)) : ?> - fieldname]->value) ? $customFields[$field->fieldname]->value : Text::_('COM_USERS_PROFILE_VALUE_NOT_FOUND'); ?> - id)) : ?> - id, $field->value); ?> - fieldname)) : ?> - fieldname, $field->value); ?> - type)) : ?> - type, $field->value); ?> - - value); ?> - -
    - - -
    -
    - + form->getFieldset($group); ?> + +
    + label) && ($legend = trim(Text::_($fieldset->label))) !== '') : ?> + + + description) && trim($fieldset->description)) : ?> +

    escape(Text::_($fieldset->description)); ?>

    + +
    + + hidden && $field->type !== 'Spacer') : ?> +
    + title; ?> +
    +
    + fieldname, $customFields)) : ?> + fieldname]->value) ? $customFields[$field->fieldname]->value : Text::_('COM_USERS_PROFILE_VALUE_NOT_FOUND'); ?> + id)) : ?> + id, $field->value); ?> + fieldname)) : ?> + fieldname, $field->value); ?> + type)) : ?> + type, $field->value); ?> + + value); ?> + +
    + + +
    +
    + diff --git a/components/com_users/tmpl/profile/default_params.php b/components/com_users/tmpl/profile/default_params.php index 80aa06bffe944..f7906946fdfd0 100644 --- a/components/com_users/tmpl/profile/default_params.php +++ b/components/com_users/tmpl/profile/default_params.php @@ -1,4 +1,5 @@ form->getFieldset('params'); ?> -
    - -
    - - hidden) : ?> -
    - title; ?> -
    -
    - id)) : ?> - id, $field->value); ?> - fieldname)) : ?> - fieldname, $field->value); ?> - type)) : ?> - type, $field->value); ?> - - value); ?> - -
    - - -
    -
    +
    + +
    + + hidden) : ?> +
    + title; ?> +
    +
    + id)) : ?> + id, $field->value); ?> + fieldname)) : ?> + fieldname, $field->value); ?> + type)) : ?> + type, $field->value); ?> + + value); ?> + +
    + + +
    +
    diff --git a/components/com_users/tmpl/profile/edit.php b/components/com_users/tmpl/profile/edit.php index e0a9abcb0a651..357a4f52d426f 100644 --- a/components/com_users/tmpl/profile/edit.php +++ b/components/com_users/tmpl/profile/edit.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); $wa->useScript('keepalive') - ->useScript('form.validate'); + ->useScript('form.validate'); ?>
    - params->get('show_page_heading')) : ?> - - + params->get('show_page_heading')) : ?> + + -
    - - form->getFieldsets() as $group => $fieldset) : ?> - form->getFieldset($group); ?> - -
    - - label)) : ?> - - label); ?> - - - description) && trim($fieldset->description)) : ?> -

    - escape(Text::_($fieldset->description)); ?> -

    - - - - renderField(); ?> - -
    - - + + + form->getFieldsets() as $group => $fieldset) : ?> + form->getFieldset($group); ?> + +
    + + label)) : ?> + + label); ?> + + + description) && trim($fieldset->description)) : ?> +

    + escape(Text::_($fieldset->description)); ?> +

    + + + + renderField(); ?> + +
    + + - mfaConfigurationUI): ?> -
    - - mfaConfigurationUI ?> -
    - + mfaConfigurationUI) : ?> +
    + + mfaConfigurationUI ?> +
    + -
    -
    - - - -
    -
    - -
    +
    +
    + + + +
    +
    + +
    diff --git a/components/com_users/tmpl/registration/complete.php b/components/com_users/tmpl/registration/complete.php index 36ab26988cb59..15c5efc41667f 100644 --- a/components/com_users/tmpl/registration/complete.php +++ b/components/com_users/tmpl/registration/complete.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> -

    - escape($this->params->get('page_heading')); ?> -

    - + params->get('show_page_heading')) : ?> +

    + escape($this->params->get('page_heading')); ?> +

    +
    diff --git a/components/com_users/tmpl/registration/default.php b/components/com_users/tmpl/registration/default.php index e9b3093bc07eb..79fcbd67faa4f 100644 --- a/components/com_users/tmpl/registration/default.php +++ b/components/com_users/tmpl/registration/default.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - + params->get('show_page_heading')) : ?> + + -
    - - form->getFieldsets() as $fieldset) : ?> - form->getFieldset($fieldset->name); ?> - -
    - - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - - -
    -
    - - - -
    -
    - -
    +
    + + form->getFieldsets() as $fieldset) : ?> + form->getFieldset($fieldset->name); ?> + +
    + + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + + +
    +
    + + + +
    +
    + +
    diff --git a/components/com_users/tmpl/remind/default.php b/components/com_users/tmpl/remind/default.php index cff4f4dce489b..e1b0ecf11df72 100644 --- a/components/com_users/tmpl/remind/default.php +++ b/components/com_users/tmpl/remind/default.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - -
    - form->getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - -
    -
    - -
    -
    - -
    + params->get('show_page_heading')) : ?> + + +
    + form->getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + +
    +
    + +
    +
    + +
    diff --git a/components/com_users/tmpl/reset/complete.php b/components/com_users/tmpl/reset/complete.php index 463c6b06131bf..9ca759f0645ac 100644 --- a/components/com_users/tmpl/reset/complete.php +++ b/components/com_users/tmpl/reset/complete.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - -
    - form->getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - -
    -
    - -
    -
    - -
    + params->get('show_page_heading')) : ?> + + +
    + form->getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + +
    +
    + +
    +
    + +
    diff --git a/components/com_users/tmpl/reset/confirm.php b/components/com_users/tmpl/reset/confirm.php index b77b17bbb5611..c1bf30e65a7fc 100644 --- a/components/com_users/tmpl/reset/confirm.php +++ b/components/com_users/tmpl/reset/confirm.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - -
    - form->getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - -
    -
    - -
    -
    - -
    + params->get('show_page_heading')) : ?> + + +
    + form->getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + +
    +
    + +
    +
    + +
    diff --git a/components/com_users/tmpl/reset/default.php b/components/com_users/tmpl/reset/default.php index d56e2181a0cce..4c8687ec76ea3 100644 --- a/components/com_users/tmpl/reset/default.php +++ b/components/com_users/tmpl/reset/default.php @@ -1,4 +1,5 @@
    - params->get('show_page_heading')) : ?> - - -
    - form->getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - form->renderFieldset($fieldset->name); ?> -
    - -
    -
    - -
    -
    - -
    + params->get('show_page_heading')) : ?> + + +
    + form->getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + form->renderFieldset($fieldset->name); ?> +
    + +
    +
    + +
    +
    + +
    diff --git a/components/com_wrapper/src/Controller/DisplayController.php b/components/com_wrapper/src/Controller/DisplayController.php index 2062abda7cc17..3c28bf44a17b4 100644 --- a/components/com_wrapper/src/Controller/DisplayController.php +++ b/components/com_wrapper/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ input->get('view', 'wrapper'); - $this->input->set('view', $vName); + // Set the default view name and format from the Request. + $vName = $this->input->get('view', 'wrapper'); + $this->input->set('view', $vName); - return parent::display($cachable, array('Itemid' => 'INT')); - } + return parent::display($cachable, array('Itemid' => 'INT')); + } } diff --git a/components/com_wrapper/src/Service/Router.php b/components/com_wrapper/src/Service/Router.php index 32b3c111cad2a..1e300a43dacc5 100644 --- a/components/com_wrapper/src/Service/Router.php +++ b/components/com_wrapper/src/Service/Router.php @@ -1,4 +1,5 @@ 'wrapper'); - } + /** + * Parse the segments of a URL. + * + * @param array $segments The segments of the URL to parse. + * + * @return array The URL attributes to be used by the application. + * + * @since 3.3 + */ + public function parse(&$segments) + { + return array('view' => 'wrapper'); + } } diff --git a/components/com_wrapper/src/View/Wrapper/HtmlView.php b/components/com_wrapper/src/View/Wrapper/HtmlView.php index 6e5b41fe5ab93..0ac8774f89670 100644 --- a/components/com_wrapper/src/View/Wrapper/HtmlView.php +++ b/components/com_wrapper/src/View/Wrapper/HtmlView.php @@ -1,4 +1,5 @@ getParams(); - - // Because the application sets a default page title, we need to get it - // right from the menu item itself - - $this->setDocumentTitle($params->get('page_title', '')); - - if ($params->get('menu-meta_description')) - { - $this->document->setDescription($params->get('menu-meta_description')); - } - - if ($params->get('robots')) - { - $this->document->setMetaData('robots', $params->get('robots')); - } - - $wrapper = new \stdClass; - - // Auto height control - if ($params->def('height_auto')) - { - $wrapper->load = 'onload="iFrameHeight(this)"'; - } - else - { - $wrapper->load = ''; - } - - $url = $params->def('url', ''); - - if ($params->def('add_scheme', 1)) - { - // Adds 'http://' or 'https://' if none is set - if (strpos($url, '//') === 0) - { - // URL without scheme in component. Prepend current scheme. - $wrapper->url = Uri::getInstance()->toString(array('scheme')) . substr($url, 2); - } - elseif (strpos($url, '/') === 0) - { - // Relative URL in component. Use scheme + host + port. - $wrapper->url = Uri::getInstance()->toString(array('scheme', 'host', 'port')) . $url; - } - elseif (strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0) - { - // URL doesn't start with either 'http://' or 'https://'. Add current scheme. - $wrapper->url = Uri::getInstance()->toString(array('scheme')) . $url; - } - else - { - // URL starts with either 'http://' or 'https://'. Do not change it. - $wrapper->url = $url; - } - } - else - { - $wrapper->url = $url; - } - - // Escape strings for HTML output - $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); - $this->params = &$params; - $this->wrapper = &$wrapper; - - parent::display($tpl); - } + /** + * The page class suffix + * + * @var string + * @since 4.0.0 + */ + protected $pageclass_sfx = ''; + + /** + * The page parameters + * + * @var \Joomla\Registry\Registry|null + * @since 4.0.0 + */ + protected $params = null; + + /** + * The page parameters + * + * @var \stdClass + * @since 4.0.0 + */ + protected $wrapper = null; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 1.5 + */ + public function display($tpl = null) + { + $params = Factory::getApplication()->getParams(); + + // Because the application sets a default page title, we need to get it + // right from the menu item itself + + $this->setDocumentTitle($params->get('page_title', '')); + + if ($params->get('menu-meta_description')) { + $this->document->setDescription($params->get('menu-meta_description')); + } + + if ($params->get('robots')) { + $this->document->setMetaData('robots', $params->get('robots')); + } + + $wrapper = new \stdClass(); + + // Auto height control + if ($params->def('height_auto')) { + $wrapper->load = 'onload="iFrameHeight(this)"'; + } else { + $wrapper->load = ''; + } + + $url = $params->def('url', ''); + + if ($params->def('add_scheme', 1)) { + // Adds 'http://' or 'https://' if none is set + if (strpos($url, '//') === 0) { + // URL without scheme in component. Prepend current scheme. + $wrapper->url = Uri::getInstance()->toString(array('scheme')) . substr($url, 2); + } elseif (strpos($url, '/') === 0) { + // Relative URL in component. Use scheme + host + port. + $wrapper->url = Uri::getInstance()->toString(array('scheme', 'host', 'port')) . $url; + } elseif (strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0) { + // URL doesn't start with either 'http://' or 'https://'. Add current scheme. + $wrapper->url = Uri::getInstance()->toString(array('scheme')) . $url; + } else { + // URL starts with either 'http://' or 'https://'. Do not change it. + $wrapper->url = $url; + } + } else { + $wrapper->url = $url; + } + + // Escape strings for HTML output + $this->pageclass_sfx = htmlspecialchars($params->get('pageclass_sfx', '')); + $this->params = &$params; + $this->wrapper = &$wrapper; + + parent::display($tpl); + } } diff --git a/components/com_wrapper/tmpl/wrapper/default.php b/components/com_wrapper/tmpl/wrapper/default.php index 73fdd91bf0eb3..5e69c5bf06acf 100644 --- a/components/com_wrapper/tmpl/wrapper/default.php +++ b/components/com_wrapper/tmpl/wrapper/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager() - ->registerAndUseScript('com_wrapper.iframe', 'com_wrapper/iframe-height.min.js', [], ['defer' => true]); + ->registerAndUseScript('com_wrapper.iframe', 'com_wrapper/iframe-height.min.js', [], ['defer' => true]); ?>
    - params->get('show_page_heading')) : ?> - - - + params->get('show_page_heading')) : ?> + + +
    diff --git a/includes/app.php b/includes/app.php index 15fdbddf75e80..b50005044fb99 100644 --- a/includes/app.php +++ b/includes/app.php @@ -1,4 +1,5 @@ alias('session.web', 'session.web.site') - ->alias('session', 'session.web.site') - ->alias('JSession', 'session.web.site') - ->alias(\Joomla\CMS\Session\Session::class, 'session.web.site') - ->alias(\Joomla\Session\Session::class, 'session.web.site') - ->alias(\Joomla\Session\SessionInterface::class, 'session.web.site'); + ->alias('session', 'session.web.site') + ->alias('JSession', 'session.web.site') + ->alias(\Joomla\CMS\Session\Session::class, 'session.web.site') + ->alias(\Joomla\Session\Session::class, 'session.web.site') + ->alias(\Joomla\Session\SessionInterface::class, 'session.web.site'); // Instantiate the application. $app = $container->get(\Joomla\CMS\Application\SiteApplication::class); diff --git a/includes/defines.php b/includes/defines.php index 1b89ecbd003aa..a0d6d924b5516 100644 --- a/includes/defines.php +++ b/includes/defines.php @@ -1,4 +1,5 @@ isInDevelopmentState()))) -{ - if (file_exists(JPATH_INSTALLATION . '/index.php')) - { - header('Location: ' . substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], 'index.php')) . 'installation/index.php'); - - exit; - } - else - { - echo 'No configuration file found and no installation code available. Exiting...'; - - exit; - } +if ( + !file_exists(JPATH_CONFIGURATION . '/configuration.php') + || (filesize(JPATH_CONFIGURATION . '/configuration.php') < 10) + || (file_exists(JPATH_INSTALLATION . '/index.php') && (false === (new Version())->isInDevelopmentState())) +) { + if (file_exists(JPATH_INSTALLATION . '/index.php')) { + header('Location: ' . substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], 'index.php')) . 'installation/index.php'); + + exit; + } else { + echo 'No configuration file found and no installation code available. Exiting...'; + + exit; + } } // Pre-Load configuration. Don't remove the Output Buffering due to BOM issues, see JCode 26026 @@ -39,68 +38,61 @@ ob_end_clean(); // System configuration. -$config = new JConfig; +$config = new JConfig(); // Set the error_reporting, and adjust a global Error Handler -switch ($config->error_reporting) -{ - case 'default': - case '-1': - - break; +switch ($config->error_reporting) { + case 'default': + case '-1': + break; - case 'none': - case '0': - error_reporting(0); + case 'none': + case '0': + error_reporting(0); - break; + break; - case 'simple': - error_reporting(E_ERROR | E_WARNING | E_PARSE); - ini_set('display_errors', 1); + case 'simple': + error_reporting(E_ERROR | E_WARNING | E_PARSE); + ini_set('display_errors', 1); - break; + break; - case 'maximum': - case 'development': // <= Stays for backward compatibility, @TODO: can be removed in 5.0 - error_reporting(E_ALL); - ini_set('display_errors', 1); + case 'maximum': + case 'development': // <= Stays for backward compatibility, @TODO: can be removed in 5.0 + error_reporting(E_ALL); + ini_set('display_errors', 1); - break; + break; - default: - error_reporting($config->error_reporting); - ini_set('display_errors', 1); + default: + error_reporting($config->error_reporting); + ini_set('display_errors', 1); - break; + break; } -if (!defined('JDEBUG')) -{ - define('JDEBUG', $config->debug); +if (!defined('JDEBUG')) { + define('JDEBUG', $config->debug); } // Check deprecation logging -if (empty($config->log_deprecated)) -{ - // Reset handler for E_USER_DEPRECATED - set_error_handler(null, E_USER_DEPRECATED); -} -else -{ - // Make sure handler for E_USER_DEPRECATED is registered - set_error_handler(['Joomla\CMS\Exception\ExceptionHandler', 'handleUserDeprecatedErrors'], E_USER_DEPRECATED); +if (empty($config->log_deprecated)) { + // Reset handler for E_USER_DEPRECATED + set_error_handler(null, E_USER_DEPRECATED); +} else { + // Make sure handler for E_USER_DEPRECATED is registered + set_error_handler(['Joomla\CMS\Exception\ExceptionHandler', 'handleUserDeprecatedErrors'], E_USER_DEPRECATED); } -if (JDEBUG || $config->error_reporting === 'maximum') -{ - // Set new Exception handler with debug enabled - $errorHandler->setExceptionHandler( - [ - new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), - 'renderException' - ] - ); +if (JDEBUG || $config->error_reporting === 'maximum') { + // Set new Exception handler with debug enabled + $errorHandler->setExceptionHandler( + [ + new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), + 'renderException' + ] + ); } /** @@ -109,15 +101,12 @@ * We need to do this as high up the stack as we can, as the default in \Joomla\Utilities\IpHelper is to * $allowIpOverride = true which is the wrong default for a generic site NOT behind a trusted proxy/load balancer. */ -if (property_exists($config, 'behind_loadbalancer') && $config->behind_loadbalancer == 1) -{ - // If Joomla is configured to be behind a trusted proxy/load balancer, allow HTTP Headers to override the REMOTE_ADDR - IpHelper::setAllowIpOverrides(true); -} -else -{ - // We disable the allowing of IP overriding using headers by default. - IpHelper::setAllowIpOverrides(false); +if (property_exists($config, 'behind_loadbalancer') && $config->behind_loadbalancer == 1) { + // If Joomla is configured to be behind a trusted proxy/load balancer, allow HTTP Headers to override the REMOTE_ADDR + IpHelper::setAllowIpOverrides(true); +} else { + // We disable the allowing of IP overriding using headers by default. + IpHelper::setAllowIpOverrides(false); } unset($config); diff --git a/index.php b/index.php index c2b6893a435f6..59acdd25df0d2 100644 --- a/index.php +++ b/index.php @@ -1,4 +1,5 @@ '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}', - 'text_file' => 'error.php' - ], - \Joomla\CMS\Log\Log::ALL, - ['error'] - ); +if (is_writable(JPATH_ADMINISTRATOR . '/logs')) { + \Joomla\CMS\Log\Log::addLogger( + [ + 'format' => '{DATE}\t{TIME}\t{LEVEL}\t{CODE}\t{MESSAGE}', + 'text_file' => 'error.php' + ], + \Joomla\CMS\Log\Log::ALL, + ['error'] + ); } // Register the Installation application @@ -45,7 +44,7 @@ // Get the dependency injection container $container = \Joomla\CMS\Factory::getContainer(); -$container->registerServiceProvider(new \Joomla\CMS\Installation\Service\Provider\Application); +$container->registerServiceProvider(new \Joomla\CMS\Installation\Service\Provider\Application()); /* * Alias the session service keys to the web session service as that is the primary session backend for this application @@ -55,11 +54,11 @@ * deprecated to be removed when the class name alias is removed as well. */ $container->alias('session.web', 'session.web.installation') - ->alias('session', 'session.web.installation') - ->alias('JSession', 'session.web.installation') - ->alias(\Joomla\CMS\Session\Session::class, 'session.web.installation') - ->alias(\Joomla\Session\Session::class, 'session.web.installation') - ->alias(\Joomla\Session\SessionInterface::class, 'session.web.installation'); + ->alias('session', 'session.web.installation') + ->alias('JSession', 'session.web.installation') + ->alias(\Joomla\CMS\Session\Session::class, 'session.web.installation') + ->alias(\Joomla\Session\Session::class, 'session.web.installation') + ->alias(\Joomla\Session\SessionInterface::class, 'session.web.installation'); // Instantiate and execute the application $container->get(\Joomla\CMS\Installation\Application\InstallationApplication::class)->execute(); diff --git a/installation/includes/defines.php b/installation/includes/defines.php index e3c481ed1a173..9ae71a84dc859 100644 --- a/installation/includes/defines.php +++ b/installation/includes/defines.php @@ -1,4 +1,5 @@ 10) - && !file_exists(JPATH_INSTALLATION . '/index.php')) -{ - header('Location: ../index.php'); - exit(); +if ( + file_exists(JPATH_CONFIGURATION . '/configuration.php') + && (filesize(JPATH_CONFIGURATION . '/configuration.php') > 10) + && !file_exists(JPATH_INSTALLATION . '/index.php') +) { + header('Location: ../index.php'); + exit(); } // Import the Joomla Platform. require_once JPATH_LIBRARIES . '/bootstrap.php'; // If debug mode enabled, set new Exception handler with debug enabled. -if (JDEBUG) -{ - $errorHandler->setExceptionHandler( - [ - new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), - 'renderException' - ] - ); +if (JDEBUG) { + $errorHandler->setExceptionHandler( + [ + new \Symfony\Component\ErrorHandler\ErrorHandler(null, true), + 'renderException' + ] + ); } diff --git a/installation/index.php b/installation/index.php index dc7ebdce1b082..25c1eeeb53e5e 100644 --- a/installation/index.php +++ b/installation/index.php @@ -1,4 +1,5 @@ name = 'installation'; - - // Register the client ID. - $this->clientId = 2; - - // Run the parent constructor. - parent::__construct($input, $config, $client, $container); - - // Store the debug value to config based on the JDEBUG flag. - $this->config->set('debug', JDEBUG); - - // Register the config to Factory. - Factory::$config = $this->config; - - // Set the root in the URI one level up. - $parts = explode('/', Uri::base(true)); - array_pop($parts); - Uri::root(null, implode('/', $parts)); - } - - /** - * After the session has been started we need to populate it with some default values. - * - * @param SessionEvent $event Session event being triggered - * - * @return void - * - * @since 4.0.0 - */ - public function afterSessionStart(SessionEvent $event) - { - $session = $event->getSession(); - - if ($session->isNew()) - { - $session->set('registry', new Registry('session')); - } - } - - /** - * Method to display errors in language parsing. - * - * @return string Language debug output. - * - * @since 3.1 - */ - public function debugLanguage() - { - if ($this->getDocument()->getType() != 'html') - { - return ''; - } - - $lang = Factory::getLanguage(); - $output = '

    ' . Text::_('JDEBUG_LANGUAGE_FILES_IN_ERROR') . '

    '; - - $errorfiles = $lang->getErrorFiles(); - - if (count($errorfiles)) - { - $output .= '
      '; - - foreach ($errorfiles as $error) - { - $output .= "
    • $error
    • "; - } - - $output .= '
    '; - } - else - { - $output .= '
    ' . Text::_('JNONE') . '
    '; - } - - $output .= '

    ' . Text::_('JDEBUG_LANGUAGE_UNTRANSLATED_STRING') . '

    '; - $output .= '
    ';
    -		$orphans = $lang->getOrphans();
    -
    -		if (count($orphans))
    -		{
    -			ksort($orphans, SORT_STRING);
    -
    -			$guesses = array();
    -
    -			foreach ($orphans as $key => $occurrence)
    -			{
    -				$guess = str_replace('_', ' ', $key);
    -
    -				$parts = explode(' ', $guess);
    -
    -				if (count($parts) > 1)
    -				{
    -					array_shift($parts);
    -					$guess = implode(' ', $parts);
    -				}
    -
    -				$guess = trim($guess);
    -
    -				$key = strtoupper(trim($key));
    -				$key = preg_replace('#\s+#', '_', $key);
    -				$key = preg_replace('#\W#', '', $key);
    -
    -				// Prepare the text.
    -				$guesses[] = $key . '="' . $guess . '"';
    -			}
    -
    -			$output .= implode("\n", $guesses);
    -		}
    -		else
    -		{
    -			$output .= '
    ' . Text::_('JNONE') . '
    '; - } - - $output .= '
    '; - - return $output; - } - - /** - * Dispatch the application. - * - * @return void - * - * @since 3.1 - */ - public function dispatch() - { - // Load the document to the API. - $this->loadDocument(); - - // Set up the params - $document = $this->getDocument(); - - // Register the document object with Factory. - Factory::$document = $document; - - // Define component path. - \define('JPATH_COMPONENT', JPATH_BASE); - \define('JPATH_COMPONENT_SITE', JPATH_SITE); - \define('JPATH_COMPONENT_ADMINISTRATOR', JPATH_ADMINISTRATOR); - - // Execute the task. - ob_start(); - $this->executeController(); - $contents = ob_get_clean(); - - // If debug language is set, append its output to the contents. - if ($this->config->get('debug_lang')) - { - $contents .= $this->debugLanguage(); - } - - // Set the content on the document - $this->getDocument()->setBuffer($contents, 'component'); - - // Set the document title - $document->setTitle(Text::_('INSTL_PAGE_TITLE')); - } - - /** - * Method to run the Web application routines. - * - * @return void - * - * @since 3.1 - */ - protected function doExecute() - { - // Ensure we load the namespace loader - $this->createExtensionNamespaceMap(); - - // Initialise the application. - $this->initialiseApp(); - - // Dispatch the application. - $this->dispatch(); - } - - /** - * Execute the application. - * - * @return void - * - * @since 4.0.0 - */ - public function execute() - { - try - { - // Perform application routines. - $this->doExecute(); - - // If we have an application document object, render it. - if ($this->document instanceof Document) - { - // Render the application output. - $this->render(); - } - - // If gzip compression is enabled in configuration and the server is compliant, compress the output. - if ($this->get('gzip') && !ini_get('zlib.output_compression') && (ini_get('output_handler') != 'ob_gzhandler')) - { - $this->compress(); - } - } - catch (\Throwable $throwable) - { - ExceptionHandler::render($throwable); - } - - // Send the application response. - $this->respond(); - } - - /** - * Method to load a PHP configuration class file based on convention and return the instantiated data object. You - * will extend this method in child classes to provide configuration data from whatever data source is relevant - * for your specific application. - * - * @param string $file The path and filename of the configuration file. If not provided, configuration.php - * in JPATH_BASE will be used. - * @param string $class The class name to instantiate. - * - * @return mixed Either an array or object to be loaded into the configuration object. - * - * @since 1.7.3 - * @throws \RuntimeException - */ - protected function fetchConfigurationData($file = '', $class = 'JConfig') - { - return array(); - } - - /** - * Executed a controller from the input task. - * - * @return void - * - * @since 4.0.0 - */ - private function executeController() - { - $task = $this->input->getCmd('task', ''); - - // The name of the controller - $controllerName = 'display'; - - // Parse task in format controller.task - if ($task !== '') - { - list($controllerName, $task) = explode('.', $task, 2); - } - - $factory = new MVCFactory('Joomla\\CMS'); - $factory->setDatabase($this->getContainer()->get(DatabaseInterface::class)); - - // Create the instance - $controller = $factory->createController($controllerName, 'Installation', [], $this, $this->input); - - // Execute the task - $controller->execute($task); - } - - /** - * Returns the language code and help URL set in the localise.xml file. - * Used for forcing a particular language in localised releases. - * - * @return mixed False on failure, array on success. - * - * @since 3.1 - */ - public function getLocalise() - { - $xml = simplexml_load_file(JPATH_INSTALLATION . '/localise.xml'); - - if (!$xml) - { - return false; - } - - // Check that it's a localise file. - if ($xml->getName() !== 'localise') - { - return false; - } - - $ret = array(); - - $ret['language'] = (string) $xml->forceLang; - $ret['debug'] = (string) $xml->debug; - $ret['sampledata'] = (string) $xml->sampledata; - - return $ret; - } - - /** - * Returns the installed language files in the administrative and frontend area. - * - * @param DatabaseInterface|null $db Database driver. - * - * @return array Array with installed language packs in admin and site area. - * - * @since 3.1 - */ - public function getLocaliseAdmin(DatabaseInterface $db = null) - { - $langfiles = array(); - - // If db connection, fetch them from the database. - if ($db) - { - foreach (LanguageHelper::getInstalledLanguages() as $clientId => $language) - { - $clientName = $clientId === 0 ? 'site' : 'admin'; - - foreach ($language as $languageCode => $lang) - { - $langfiles[$clientName][] = $lang->element; - } - } - } - // Read the folder names in the site and admin area. - else - { - $langfiles['site'] = Folder::folders(LanguageHelper::getLanguagePath(JPATH_SITE)); - $langfiles['admin'] = Folder::folders(LanguageHelper::getLanguagePath(JPATH_ADMINISTRATOR)); - } - - return $langfiles; - } - - /** - * Gets the name of the current template. - * - * @param boolean $params True to return the template parameters - * - * @return string|\stdClass The name of the template. - * - * @since 3.1 - */ - public function getTemplate($params = false) - { - if ($params) - { - $template = new \stdClass; - $template->template = 'template'; - $template->params = new Registry; - $template->inheritable = 0; - $template->parent = ''; - - return $template; - } - - return 'template'; - } - - /** - * Initialise the application. - * - * @param array $options An optional associative array of configuration settings. - * - * @return void - * - * @since 3.1 - */ - protected function initialiseApp($options = array()) - { - // Get the localisation information provided in the localise.xml file. - $forced = $this->getLocalise(); - - // Check the request data for the language. - if (empty($options['language'])) - { - $requestLang = $this->input->getCmd('lang', null); - - if ($requestLang !== null) - { - $options['language'] = $requestLang; - } - } - - // Check the session for the language. - if (empty($options['language'])) - { - $sessionOptions = $this->getSession()->get('setup.options'); - - if (isset($sessionOptions['language'])) - { - $options['language'] = $sessionOptions['language']; - } - } - - // This could be a first-time visit - try to determine what the client accepts. - if (empty($options['language'])) - { - if (!empty($forced['language'])) - { - $options['language'] = $forced['language']; - } - else - { - $options['language'] = LanguageHelper::detectLanguage(); - - if (empty($options['language'])) - { - $options['language'] = 'en-GB'; - } - } - } - - // Give the user English. - if (empty($options['language'])) - { - $options['language'] = 'en-GB'; - } - - // Set the official helpurl. - $options['helpurl'] = 'https://help.joomla.org/proxy?keyref=Help{major}{minor}:{keyref}&lang={langcode}'; - - // Store helpurl in the session. - $this->getSession()->set('setup.helpurl', $options['helpurl']); - - // Set the language in the class. - $this->config->set('language', $options['language']); - $this->config->set('debug_lang', $forced['debug']); - $this->config->set('sampledata', $forced['sampledata']); - $this->config->set('helpurl', $options['helpurl']); - } - - /** - * Allows the application to load a custom or default document. - * - * The logic and options for creating this object are adequately generic for default cases - * but for many applications it will make sense to override this method and create a document, - * if required, based on more specific needs. - * - * @param Document|null $document An optional document object. If omitted, the factory document is created. - * - * @return InstallationApplication This method is chainable. - * - * @since 3.2 - */ - public function loadDocument(Document $document = null) - { - if ($document === null) - { - $lang = Factory::getLanguage(); - $type = $this->input->get('format', 'html', 'word'); - $date = new Date('now'); - - $attributes = array( - 'charset' => 'utf-8', - 'lineend' => 'unix', - 'tab' => "\t", - 'language' => $lang->getTag(), - 'direction' => $lang->isRtl() ? 'rtl' : 'ltr', - 'mediaversion' => md5($date->format('YmdHi')), - ); - - $document = $this->getContainer()->get(FactoryInterface::class)->createDocument($type, $attributes); - - // Register the instance to Factory. - Factory::$document = $document; - } - - $this->document = $document; - - return $this; - } - - /** - * Rendering is the process of pushing the document buffers into the template - * placeholders, retrieving data from the document and pushing it into - * the application response buffer. - * - * @return void - * - * @since 3.1 - */ - public function render() - { - $options = []; - - if ($this->document instanceof HtmlDocument) - { - $file = $this->input->getCmd('tmpl', 'index'); - - $options = [ - 'template' => 'template', - 'file' => $file . '.php', - 'directory' => JPATH_THEMES, - 'params' => '{}', - "templateInherits" => '' - ]; - } - - // Parse the document. - $this->document->parse($options); - - // Render the document. - $data = $this->document->render($this->get('cache_enabled'), $options); - - // Set the application output data. - $this->setBody($data); - } - - /** - * Set configuration values. - * - * @param array $vars Array of configuration values - * @param string $namespace The namespace - * - * @return void - * - * @since 3.1 - */ - public function setCfg(array $vars = array(), $namespace = 'config') - { - $this->config->loadArray($vars, $namespace); - } - - /** - * Returns the application \JMenu object. - * - * @param string|null $name The name of the application/client. - * @param array $options An optional associative array of configuration settings. - * - * @return null - * - * @since 3.2 - */ - public function getMenu($name = null, $options = array()) - { - return null; - } + use \Joomla\CMS\Application\ExtensionNamespaceMapper; + + /** + * Class constructor. + * + * @param Input|null $input An optional argument to provide dependency injection for the application's input + * object. If the argument is a JInput object that object will become the + * application's input object, otherwise a default input object is created. + * @param Registry|null $config An optional argument to provide dependency injection for the application's + * config object. If the argument is a Registry object that object will become + * the application's config object, otherwise a default config object is created. + * @param WebClient|null $client An optional argument to provide dependency injection for the application's + * client object. If the argument is a WebClient object that object will become the + * application's client object, otherwise a default client object is created. + * @param Container|null $container Dependency injection container. + * + * @since 3.1 + */ + public function __construct(Input $input = null, Registry $config = null, WebClient $client = null, Container $container = null) + { + // Register the application name. + $this->name = 'installation'; + + // Register the client ID. + $this->clientId = 2; + + // Run the parent constructor. + parent::__construct($input, $config, $client, $container); + + // Store the debug value to config based on the JDEBUG flag. + $this->config->set('debug', JDEBUG); + + // Register the config to Factory. + Factory::$config = $this->config; + + // Set the root in the URI one level up. + $parts = explode('/', Uri::base(true)); + array_pop($parts); + Uri::root(null, implode('/', $parts)); + } + + /** + * After the session has been started we need to populate it with some default values. + * + * @param SessionEvent $event Session event being triggered + * + * @return void + * + * @since 4.0.0 + */ + public function afterSessionStart(SessionEvent $event) + { + $session = $event->getSession(); + + if ($session->isNew()) { + $session->set('registry', new Registry('session')); + } + } + + /** + * Method to display errors in language parsing. + * + * @return string Language debug output. + * + * @since 3.1 + */ + public function debugLanguage() + { + if ($this->getDocument()->getType() != 'html') { + return ''; + } + + $lang = Factory::getLanguage(); + $output = '

    ' . Text::_('JDEBUG_LANGUAGE_FILES_IN_ERROR') . '

    '; + + $errorfiles = $lang->getErrorFiles(); + + if (count($errorfiles)) { + $output .= '
      '; + + foreach ($errorfiles as $error) { + $output .= "
    • $error
    • "; + } + + $output .= '
    '; + } else { + $output .= '
    ' . Text::_('JNONE') . '
    '; + } + + $output .= '

    ' . Text::_('JDEBUG_LANGUAGE_UNTRANSLATED_STRING') . '

    '; + $output .= '
    ';
    +        $orphans = $lang->getOrphans();
    +
    +        if (count($orphans)) {
    +            ksort($orphans, SORT_STRING);
    +
    +            $guesses = array();
    +
    +            foreach ($orphans as $key => $occurrence) {
    +                $guess = str_replace('_', ' ', $key);
    +
    +                $parts = explode(' ', $guess);
    +
    +                if (count($parts) > 1) {
    +                    array_shift($parts);
    +                    $guess = implode(' ', $parts);
    +                }
    +
    +                $guess = trim($guess);
    +
    +                $key = strtoupper(trim($key));
    +                $key = preg_replace('#\s+#', '_', $key);
    +                $key = preg_replace('#\W#', '', $key);
    +
    +                // Prepare the text.
    +                $guesses[] = $key . '="' . $guess . '"';
    +            }
    +
    +            $output .= implode("\n", $guesses);
    +        } else {
    +            $output .= '
    ' . Text::_('JNONE') . '
    '; + } + + $output .= '
    '; + + return $output; + } + + /** + * Dispatch the application. + * + * @return void + * + * @since 3.1 + */ + public function dispatch() + { + // Load the document to the API. + $this->loadDocument(); + + // Set up the params + $document = $this->getDocument(); + + // Register the document object with Factory. + Factory::$document = $document; + + // Define component path. + \define('JPATH_COMPONENT', JPATH_BASE); + \define('JPATH_COMPONENT_SITE', JPATH_SITE); + \define('JPATH_COMPONENT_ADMINISTRATOR', JPATH_ADMINISTRATOR); + + // Execute the task. + ob_start(); + $this->executeController(); + $contents = ob_get_clean(); + + // If debug language is set, append its output to the contents. + if ($this->config->get('debug_lang')) { + $contents .= $this->debugLanguage(); + } + + // Set the content on the document + $this->getDocument()->setBuffer($contents, 'component'); + + // Set the document title + $document->setTitle(Text::_('INSTL_PAGE_TITLE')); + } + + /** + * Method to run the Web application routines. + * + * @return void + * + * @since 3.1 + */ + protected function doExecute() + { + // Ensure we load the namespace loader + $this->createExtensionNamespaceMap(); + + // Initialise the application. + $this->initialiseApp(); + + // Dispatch the application. + $this->dispatch(); + } + + /** + * Execute the application. + * + * @return void + * + * @since 4.0.0 + */ + public function execute() + { + try { + // Perform application routines. + $this->doExecute(); + + // If we have an application document object, render it. + if ($this->document instanceof Document) { + // Render the application output. + $this->render(); + } + + // If gzip compression is enabled in configuration and the server is compliant, compress the output. + if ($this->get('gzip') && !ini_get('zlib.output_compression') && (ini_get('output_handler') != 'ob_gzhandler')) { + $this->compress(); + } + } catch (\Throwable $throwable) { + ExceptionHandler::render($throwable); + } + + // Send the application response. + $this->respond(); + } + + /** + * Method to load a PHP configuration class file based on convention and return the instantiated data object. You + * will extend this method in child classes to provide configuration data from whatever data source is relevant + * for your specific application. + * + * @param string $file The path and filename of the configuration file. If not provided, configuration.php + * in JPATH_BASE will be used. + * @param string $class The class name to instantiate. + * + * @return mixed Either an array or object to be loaded into the configuration object. + * + * @since 1.7.3 + * @throws \RuntimeException + */ + protected function fetchConfigurationData($file = '', $class = 'JConfig') + { + return array(); + } + + /** + * Executed a controller from the input task. + * + * @return void + * + * @since 4.0.0 + */ + private function executeController() + { + $task = $this->input->getCmd('task', ''); + + // The name of the controller + $controllerName = 'display'; + + // Parse task in format controller.task + if ($task !== '') { + list($controllerName, $task) = explode('.', $task, 2); + } + + $factory = new MVCFactory('Joomla\\CMS'); + $factory->setDatabase($this->getContainer()->get(DatabaseInterface::class)); + + // Create the instance + $controller = $factory->createController($controllerName, 'Installation', [], $this, $this->input); + + // Execute the task + $controller->execute($task); + } + + /** + * Returns the language code and help URL set in the localise.xml file. + * Used for forcing a particular language in localised releases. + * + * @return mixed False on failure, array on success. + * + * @since 3.1 + */ + public function getLocalise() + { + $xml = simplexml_load_file(JPATH_INSTALLATION . '/localise.xml'); + + if (!$xml) { + return false; + } + + // Check that it's a localise file. + if ($xml->getName() !== 'localise') { + return false; + } + + $ret = array(); + + $ret['language'] = (string) $xml->forceLang; + $ret['debug'] = (string) $xml->debug; + $ret['sampledata'] = (string) $xml->sampledata; + + return $ret; + } + + /** + * Returns the installed language files in the administrative and frontend area. + * + * @param DatabaseInterface|null $db Database driver. + * + * @return array Array with installed language packs in admin and site area. + * + * @since 3.1 + */ + public function getLocaliseAdmin(DatabaseInterface $db = null) + { + $langfiles = array(); + + // If db connection, fetch them from the database. + if ($db) { + foreach (LanguageHelper::getInstalledLanguages() as $clientId => $language) { + $clientName = $clientId === 0 ? 'site' : 'admin'; + + foreach ($language as $languageCode => $lang) { + $langfiles[$clientName][] = $lang->element; + } + } + } else { + // Read the folder names in the site and admin area. + $langfiles['site'] = Folder::folders(LanguageHelper::getLanguagePath(JPATH_SITE)); + $langfiles['admin'] = Folder::folders(LanguageHelper::getLanguagePath(JPATH_ADMINISTRATOR)); + } + + return $langfiles; + } + + /** + * Gets the name of the current template. + * + * @param boolean $params True to return the template parameters + * + * @return string|\stdClass The name of the template. + * + * @since 3.1 + */ + public function getTemplate($params = false) + { + if ($params) { + $template = new \stdClass(); + $template->template = 'template'; + $template->params = new Registry(); + $template->inheritable = 0; + $template->parent = ''; + + return $template; + } + + return 'template'; + } + + /** + * Initialise the application. + * + * @param array $options An optional associative array of configuration settings. + * + * @return void + * + * @since 3.1 + */ + protected function initialiseApp($options = array()) + { + // Get the localisation information provided in the localise.xml file. + $forced = $this->getLocalise(); + + // Check the request data for the language. + if (empty($options['language'])) { + $requestLang = $this->input->getCmd('lang', null); + + if ($requestLang !== null) { + $options['language'] = $requestLang; + } + } + + // Check the session for the language. + if (empty($options['language'])) { + $sessionOptions = $this->getSession()->get('setup.options'); + + if (isset($sessionOptions['language'])) { + $options['language'] = $sessionOptions['language']; + } + } + + // This could be a first-time visit - try to determine what the client accepts. + if (empty($options['language'])) { + if (!empty($forced['language'])) { + $options['language'] = $forced['language']; + } else { + $options['language'] = LanguageHelper::detectLanguage(); + + if (empty($options['language'])) { + $options['language'] = 'en-GB'; + } + } + } + + // Give the user English. + if (empty($options['language'])) { + $options['language'] = 'en-GB'; + } + + // Set the official helpurl. + $options['helpurl'] = 'https://help.joomla.org/proxy?keyref=Help{major}{minor}:{keyref}&lang={langcode}'; + + // Store helpurl in the session. + $this->getSession()->set('setup.helpurl', $options['helpurl']); + + // Set the language in the class. + $this->config->set('language', $options['language']); + $this->config->set('debug_lang', $forced['debug']); + $this->config->set('sampledata', $forced['sampledata']); + $this->config->set('helpurl', $options['helpurl']); + } + + /** + * Allows the application to load a custom or default document. + * + * The logic and options for creating this object are adequately generic for default cases + * but for many applications it will make sense to override this method and create a document, + * if required, based on more specific needs. + * + * @param Document|null $document An optional document object. If omitted, the factory document is created. + * + * @return InstallationApplication This method is chainable. + * + * @since 3.2 + */ + public function loadDocument(Document $document = null) + { + if ($document === null) { + $lang = Factory::getLanguage(); + $type = $this->input->get('format', 'html', 'word'); + $date = new Date('now'); + + $attributes = array( + 'charset' => 'utf-8', + 'lineend' => 'unix', + 'tab' => "\t", + 'language' => $lang->getTag(), + 'direction' => $lang->isRtl() ? 'rtl' : 'ltr', + 'mediaversion' => md5($date->format('YmdHi')), + ); + + $document = $this->getContainer()->get(FactoryInterface::class)->createDocument($type, $attributes); + + // Register the instance to Factory. + Factory::$document = $document; + } + + $this->document = $document; + + return $this; + } + + /** + * Rendering is the process of pushing the document buffers into the template + * placeholders, retrieving data from the document and pushing it into + * the application response buffer. + * + * @return void + * + * @since 3.1 + */ + public function render() + { + $options = []; + + if ($this->document instanceof HtmlDocument) { + $file = $this->input->getCmd('tmpl', 'index'); + + $options = [ + 'template' => 'template', + 'file' => $file . '.php', + 'directory' => JPATH_THEMES, + 'params' => '{}', + "templateInherits" => '' + ]; + } + + // Parse the document. + $this->document->parse($options); + + // Render the document. + $data = $this->document->render($this->get('cache_enabled'), $options); + + // Set the application output data. + $this->setBody($data); + } + + /** + * Set configuration values. + * + * @param array $vars Array of configuration values + * @param string $namespace The namespace + * + * @return void + * + * @since 3.1 + */ + public function setCfg(array $vars = array(), $namespace = 'config') + { + $this->config->loadArray($vars, $namespace); + } + + /** + * Returns the application \JMenu object. + * + * @param string|null $name The name of the application/client. + * @param array $options An optional associative array of configuration settings. + * + * @return null + * + * @since 3.2 + */ + public function getMenu($name = null, $options = array()) + { + return null; + } } diff --git a/installation/src/Controller/DisplayController.php b/installation/src/Controller/DisplayController.php index 4968a764ab14a..0fe3ef766a8ac 100644 --- a/installation/src/Controller/DisplayController.php +++ b/installation/src/Controller/DisplayController.php @@ -1,4 +1,5 @@ app; - - $defaultView = 'setup'; - - // If the app has already been installed, default to the remove view - if (file_exists(JPATH_CONFIGURATION . '/configuration.php') - && filesize(JPATH_CONFIGURATION . '/configuration.php') > 10 - && file_exists(JPATH_INSTALLATION . '/index.php')) - { - $defaultView = 'remove'; - } - - /** @var \Joomla\CMS\Installation\Model\ChecksModel $model */ - $model = $this->getModel('Checks'); - - $vName = $this->input->getWord('view', $defaultView); - - if (!$model->getPhpOptionsSufficient() && $defaultView !== 'remove') - { - if ($vName !== 'preinstall') - { - $app->redirect('index.php?view=preinstall'); - } - - $vName = 'preinstall'; - } - else - { - if ($vName === 'preinstall') - { - $app->redirect('index.php?view=setup'); - } - - if ($vName === 'remove' && !file_exists(JPATH_CONFIGURATION . '/configuration.php')) - { - $app->redirect('index.php?view=setup'); - } - - if ($vName !== $defaultView && !$model->getOptions() && $defaultView !== 'remove') - { - $app->redirect('index.php'); - } - } - - $this->input->set('view', $vName); - - return parent::display($cachable, $urlparams); - } - - /** - * Method to get a reference to the current view and load it if necessary. - * - * @param string $name The view name. Optional, defaults to the controller name. - * @param string $type The view type. Optional. - * @param string $prefix The class prefix. Optional. - * @param array $config Configuration array for view. Optional. - * - * @return AbstractView Reference to the view or an error. - * - * @since 3.0 - * @throws \Exception - */ - public function getView($name = '', $type = '', $prefix = '', $config = array()) - { - $view = parent::getView($name, $type, $prefix, $config); - - if ($view instanceof AbstractView) - { - // Set some models, used by various views - $view->setModel($this->getModel('Checks')); - $view->setModel($this->getModel('Languages')); - } - - return $view; - } + /** + * Method to display a view. + * + * @param boolean $cachable If true, the view output will be cached. + * @param boolean $urlparams An array of safe URL parameters and their variable types, for valid values see {@link JFilterInput::clean()}. + * + * @return \Joomla\CMS\MVC\Controller\BaseController This object to support chaining. + * + * @since 1.5 + */ + public function display($cachable = false, $urlparams = false) + { + $app = $this->app; + + $defaultView = 'setup'; + + // If the app has already been installed, default to the remove view + if ( + file_exists(JPATH_CONFIGURATION . '/configuration.php') + && filesize(JPATH_CONFIGURATION . '/configuration.php') > 10 + && file_exists(JPATH_INSTALLATION . '/index.php') + ) { + $defaultView = 'remove'; + } + + /** @var \Joomla\CMS\Installation\Model\ChecksModel $model */ + $model = $this->getModel('Checks'); + + $vName = $this->input->getWord('view', $defaultView); + + if (!$model->getPhpOptionsSufficient() && $defaultView !== 'remove') { + if ($vName !== 'preinstall') { + $app->redirect('index.php?view=preinstall'); + } + + $vName = 'preinstall'; + } else { + if ($vName === 'preinstall') { + $app->redirect('index.php?view=setup'); + } + + if ($vName === 'remove' && !file_exists(JPATH_CONFIGURATION . '/configuration.php')) { + $app->redirect('index.php?view=setup'); + } + + if ($vName !== $defaultView && !$model->getOptions() && $defaultView !== 'remove') { + $app->redirect('index.php'); + } + } + + $this->input->set('view', $vName); + + return parent::display($cachable, $urlparams); + } + + /** + * Method to get a reference to the current view and load it if necessary. + * + * @param string $name The view name. Optional, defaults to the controller name. + * @param string $type The view type. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config Configuration array for view. Optional. + * + * @return AbstractView Reference to the view or an error. + * + * @since 3.0 + * @throws \Exception + */ + public function getView($name = '', $type = '', $prefix = '', $config = array()) + { + $view = parent::getView($name, $type, $prefix, $config); + + if ($view instanceof AbstractView) { + // Set some models, used by various views + $view->setModel($this->getModel('Checks')); + $view->setModel($this->getModel('Languages')); + } + + return $view; + } } diff --git a/installation/src/Controller/InstallationController.php b/installation/src/Controller/InstallationController.php index 87ea82c249c50..f57f82d3d17ea 100644 --- a/installation/src/Controller/InstallationController.php +++ b/installation/src/Controller/InstallationController.php @@ -1,4 +1,5 @@ registerTask('populate1', 'populate'); - $this->registerTask('populate2', 'populate'); - $this->registerTask('populate3', 'populate'); - $this->registerTask('custom1', 'populate'); - $this->registerTask('custom2', 'populate'); - $this->registerTask('removeFolder', 'delete'); - } - - /** - * Database check task. - * - * @return void - * - * @since 4.0.0 - */ - public function dbcheck() - { - $this->checkValidToken(); - - // Redirect to the page. - $r = new \stdClass; - $r->view = 'setup'; - - // Check the form - /** @var \Joomla\CMS\Installation\Model\SetupModel $model */ - $model = $this->getModel('Setup'); - - if ($model->checkForm('setup') === false) - { - $this->app->enqueueMessage(Text::_('INSTL_DATABASE_VALIDATION_ERROR'), 'error'); - $r->validated = false; - $this->sendJsonResponse($r); - - return; - } - - $r->validated = $model->validateDbConnection(); - - $this->sendJsonResponse($r); - } - - /** - * Create DB task. - * - * @return void - * - * @since 4.0.0 - */ - public function create() - { - $this->checkValidToken(); - - $r = new \stdClass; - - /** @var \Joomla\CMS\Installation\Model\DatabaseModel $databaseModel */ - $databaseModel = $this->getModel('Database'); - - // Create Db - try - { - $dbCreated = $databaseModel->createDatabase(); - } - catch (\RuntimeException $e) - { - $this->app->enqueueMessage($e->getMessage(), 'error'); - - $dbCreated = false; - } - - if (!$dbCreated) - { - $r->view = 'setup'; - } - else - { - if (!$databaseModel->handleOldDatabase()) - { - $r->view = 'setup'; - } - } - - $this->sendJsonResponse($r); - } - - /** - * Populate the database. - * - * @return void - * - * @since 4.0.0 - */ - public function populate() - { - $this->checkValidToken(); - $step = $this->getTask(); - /** @var \Joomla\CMS\Installation\Model\DatabaseModel $model */ - $model = $this->getModel('Database'); - - $r = new \stdClass; - $db = $model->initialise(); - $files = [ - 'populate1' => 'base', - 'populate2' => 'supports', - 'populate3' => 'extensions', - 'custom1' => 'localise', - 'custom2' => 'custom' - ]; - - $schema = $files[$step]; - $serverType = $db->getServerType(); - - if (in_array($step, ['custom1', 'custom2']) && !is_file('sql/' . $serverType . '/' . $schema . '.sql')) - { - $this->sendJsonResponse($r); - - return; - } - - if (!isset($files[$step])) - { - $r->view = 'setup'; - Factory::getApplication()->enqueueMessage(Text::_('INSTL_SAMPLE_DATA_NOT_FOUND'), 'error'); - $this->sendJsonResponse($r); - } - - // Attempt to populate the database with the given file. - if (!$model->createTables($schema)) - { - $r->view = 'setup'; - } - - $this->sendJsonResponse($r); - } - - /** - * Config task. - * - * @return void - * - * @since 4.0.0 - */ - public function config() - { - $this->checkValidToken(); - - /** @var \Joomla\CMS\Installation\Model\SetupModel $setUpModel */ - $setUpModel = $this->getModel('Setup'); - - // Get the options from the session - $options = $setUpModel->getOptions(); - - $r = new \stdClass; - $r->view = 'remove'; - - /** @var \Joomla\CMS\Installation\Model\ConfigurationModel $configurationModel */ - $configurationModel = $this->getModel('Configuration'); - - // Attempt to setup the configuration. - if (!$configurationModel->setup($options)) - { - $r->view = 'setup'; - } - - $this->sendJsonResponse($r); - } - - /** - * Languages task. - * - * @return void - * - * @since 4.0.0 - */ - public function languages() - { - $this->checkValidToken(); - - // Get array of selected languages - $lids = (array) $this->input->get('cid', [], 'int'); - - // Remove zero values resulting from input filter - $lids = array_filter($lids); - - if (empty($lids)) - { - // No languages have been selected - $this->app->enqueueMessage(Text::_('INSTL_LANGUAGES_NO_LANGUAGE_SELECTED'), 'warning'); - } - else - { - // Get the languages model. - /** @var \Joomla\CMS\Installation\Model\LanguagesModel $model */ - $model = $this->getModel('Languages'); - - // Install selected languages - $model->install($lids); - } - - // Redirect to the page. - $r = new \stdClass; - $r->view = 'remove'; - - $this->sendJsonResponse($r); - } - - /** - * Delete installation folder task. - * - * @return void - * - * @since 4.0.0 - */ - public function delete() - { - $this->checkValidToken(); - - /** @var \Joomla\CMS\Installation\Model\CleanupModel $model */ - $model = $this->getModel('Cleanup'); - - if (!$model->deleteInstallationFolder()) - { - // We can't send a response with sendJsonResponse because our installation classes might not now exist - $error = [ - 'token' => Session::getFormToken(true), - 'error' => true, - 'data' => [ - 'view' => 'remove' - ], - 'messages' => [ - 'warning' => [ - Text::sprintf('INSTL_COMPLETE_ERROR_FOLDER_DELETE', 'installation') - ] - ] - ]; - - echo json_encode($error); - - return; - } - - $this->app->getSession()->destroy(); - - // We can't send a response with sendJsonResponse because our installation classes now do not exist - echo json_encode(['error' => false]); - } + /** + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface|null $factory The factory. + * @param CMSApplication|null $app The Application for the dispatcher + * @param \Joomla\CMS\Input\Input|null $input The Input object. + * + * @since 3.0 + */ + public function __construct($config = [], MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + $this->registerTask('populate1', 'populate'); + $this->registerTask('populate2', 'populate'); + $this->registerTask('populate3', 'populate'); + $this->registerTask('custom1', 'populate'); + $this->registerTask('custom2', 'populate'); + $this->registerTask('removeFolder', 'delete'); + } + + /** + * Database check task. + * + * @return void + * + * @since 4.0.0 + */ + public function dbcheck() + { + $this->checkValidToken(); + + // Redirect to the page. + $r = new \stdClass(); + $r->view = 'setup'; + + // Check the form + /** @var \Joomla\CMS\Installation\Model\SetupModel $model */ + $model = $this->getModel('Setup'); + + if ($model->checkForm('setup') === false) { + $this->app->enqueueMessage(Text::_('INSTL_DATABASE_VALIDATION_ERROR'), 'error'); + $r->validated = false; + $this->sendJsonResponse($r); + + return; + } + + $r->validated = $model->validateDbConnection(); + + $this->sendJsonResponse($r); + } + + /** + * Create DB task. + * + * @return void + * + * @since 4.0.0 + */ + public function create() + { + $this->checkValidToken(); + + $r = new \stdClass(); + + /** @var \Joomla\CMS\Installation\Model\DatabaseModel $databaseModel */ + $databaseModel = $this->getModel('Database'); + + // Create Db + try { + $dbCreated = $databaseModel->createDatabase(); + } catch (\RuntimeException $e) { + $this->app->enqueueMessage($e->getMessage(), 'error'); + + $dbCreated = false; + } + + if (!$dbCreated) { + $r->view = 'setup'; + } else { + if (!$databaseModel->handleOldDatabase()) { + $r->view = 'setup'; + } + } + + $this->sendJsonResponse($r); + } + + /** + * Populate the database. + * + * @return void + * + * @since 4.0.0 + */ + public function populate() + { + $this->checkValidToken(); + $step = $this->getTask(); + /** @var \Joomla\CMS\Installation\Model\DatabaseModel $model */ + $model = $this->getModel('Database'); + + $r = new \stdClass(); + $db = $model->initialise(); + $files = [ + 'populate1' => 'base', + 'populate2' => 'supports', + 'populate3' => 'extensions', + 'custom1' => 'localise', + 'custom2' => 'custom' + ]; + + $schema = $files[$step]; + $serverType = $db->getServerType(); + + if (in_array($step, ['custom1', 'custom2']) && !is_file('sql/' . $serverType . '/' . $schema . '.sql')) { + $this->sendJsonResponse($r); + + return; + } + + if (!isset($files[$step])) { + $r->view = 'setup'; + Factory::getApplication()->enqueueMessage(Text::_('INSTL_SAMPLE_DATA_NOT_FOUND'), 'error'); + $this->sendJsonResponse($r); + } + + // Attempt to populate the database with the given file. + if (!$model->createTables($schema)) { + $r->view = 'setup'; + } + + $this->sendJsonResponse($r); + } + + /** + * Config task. + * + * @return void + * + * @since 4.0.0 + */ + public function config() + { + $this->checkValidToken(); + + /** @var \Joomla\CMS\Installation\Model\SetupModel $setUpModel */ + $setUpModel = $this->getModel('Setup'); + + // Get the options from the session + $options = $setUpModel->getOptions(); + + $r = new \stdClass(); + $r->view = 'remove'; + + /** @var \Joomla\CMS\Installation\Model\ConfigurationModel $configurationModel */ + $configurationModel = $this->getModel('Configuration'); + + // Attempt to setup the configuration. + if (!$configurationModel->setup($options)) { + $r->view = 'setup'; + } + + $this->sendJsonResponse($r); + } + + /** + * Languages task. + * + * @return void + * + * @since 4.0.0 + */ + public function languages() + { + $this->checkValidToken(); + + // Get array of selected languages + $lids = (array) $this->input->get('cid', [], 'int'); + + // Remove zero values resulting from input filter + $lids = array_filter($lids); + + if (empty($lids)) { + // No languages have been selected + $this->app->enqueueMessage(Text::_('INSTL_LANGUAGES_NO_LANGUAGE_SELECTED'), 'warning'); + } else { + // Get the languages model. + /** @var \Joomla\CMS\Installation\Model\LanguagesModel $model */ + $model = $this->getModel('Languages'); + + // Install selected languages + $model->install($lids); + } + + // Redirect to the page. + $r = new \stdClass(); + $r->view = 'remove'; + + $this->sendJsonResponse($r); + } + + /** + * Delete installation folder task. + * + * @return void + * + * @since 4.0.0 + */ + public function delete() + { + $this->checkValidToken(); + + /** @var \Joomla\CMS\Installation\Model\CleanupModel $model */ + $model = $this->getModel('Cleanup'); + + if (!$model->deleteInstallationFolder()) { + // We can't send a response with sendJsonResponse because our installation classes might not now exist + $error = [ + 'token' => Session::getFormToken(true), + 'error' => true, + 'data' => [ + 'view' => 'remove' + ], + 'messages' => [ + 'warning' => [ + Text::sprintf('INSTL_COMPLETE_ERROR_FOLDER_DELETE', 'installation') + ] + ] + ]; + + echo json_encode($error); + + return; + } + + $this->app->getSession()->destroy(); + + // We can't send a response with sendJsonResponse because our installation classes now do not exist + echo json_encode(['error' => false]); + } } diff --git a/installation/src/Controller/JSONController.php b/installation/src/Controller/JSONController.php index f288c83d8d764..c35e0d36ddc70 100644 --- a/installation/src/Controller/JSONController.php +++ b/installation/src/Controller/JSONController.php @@ -1,4 +1,5 @@ app->mimeType = 'application/json'; + /** + * Method to send a JSON response. The data parameter + * can be an Exception object for when an error has occurred or + * a JsonResponse for a good response. + * + * @param mixed $response JsonResponse on success, Exception on failure. + * + * @return void + * + * @since 4.0.0 + */ + protected function sendJsonResponse($response) + { + $this->app->mimeType = 'application/json'; - // Very crude workaround to give an error message when JSON is disabled - if (!function_exists('json_encode') || !function_exists('json_decode')) - { - $this->app->setHeader('status', 500); - echo '{"token":"' . Session::getFormToken(true) . '","lang":"' . Factory::getLanguage()->getTag() - . '","error":true,"header":"' . Text::_('INSTL_HEADER_ERROR') . '","message":"' . Text::_('INSTL_WARNJSON') . '"}'; + // Very crude workaround to give an error message when JSON is disabled + if (!function_exists('json_encode') || !function_exists('json_decode')) { + $this->app->setHeader('status', 500); + echo '{"token":"' . Session::getFormToken(true) . '","lang":"' . Factory::getLanguage()->getTag() + . '","error":true,"header":"' . Text::_('INSTL_HEADER_ERROR') . '","message":"' . Text::_('INSTL_WARNJSON') . '"}'; - return; - } + return; + } - // Check if we need to send an error code. - if ($response instanceof \Exception) - { - // Send the appropriate error code response. - $this->app->setHeader('status', $response->getCode(), true); - } + // Check if we need to send an error code. + if ($response instanceof \Exception) { + // Send the appropriate error code response. + $this->app->setHeader('status', $response->getCode(), true); + } - // Send the JSON response. - echo json_encode(new JsonResponse($response)); - } + // Send the JSON response. + echo json_encode(new JsonResponse($response)); + } - /** - * Checks for a form token, if it is invalid a JSON response with the error code 403 is sent. - * - * @return void - * - * @since 4.0.0 - * @see Session::checkToken() - */ - public function checkValidToken() - { - // Check for request forgeries. - if (!Session::checkToken()) - { - $this->sendJsonResponse(new \Exception(Text::_('JINVALID_TOKEN_NOTICE'), 403)); + /** + * Checks for a form token, if it is invalid a JSON response with the error code 403 is sent. + * + * @return void + * + * @since 4.0.0 + * @see Session::checkToken() + */ + public function checkValidToken() + { + // Check for request forgeries. + if (!Session::checkToken()) { + $this->sendJsonResponse(new \Exception(Text::_('JINVALID_TOKEN_NOTICE'), 403)); - $this->app->close(); - } - } + $this->app->close(); + } + } } diff --git a/installation/src/Controller/LanguageController.php b/installation/src/Controller/LanguageController.php index 031363d399f60..24c4534d6545b 100644 --- a/installation/src/Controller/LanguageController.php +++ b/installation/src/Controller/LanguageController.php @@ -1,4 +1,5 @@ checkValidToken(); - - // Check for potentially unwritable session - $session = $this->app->getSession(); - - if ($session->isNew()) - { - $this->sendJsonResponse(new \Exception(Text::_('INSTL_COOKIES_NOT_ENABLED'), 500)); - } - - /** @var SetupModel $model */ - $model = $this->getModel('Setup'); - - // Get the posted values from the request and validate them. - $data = $this->input->post->get('jform', [], 'array'); - $return = $model->validate($data, 'language'); - - $r = new \stdClass; - - // Check for validation errors. - if ($return === false) - { - /* - * The validate method enqueued all messages for us, so we just need to - * redirect back to the site setup screen. - */ - $r->view = $this->input->getWord('view', 'setup'); - $this->sendJsonResponse($r); - } - - // Store the options in the session. - $model->storeOptions($return); - - // Setup language - Factory::$language = Language::getInstance($return['language']); - - // Redirect to the page. - $r->view = $this->input->getWord('view', 'setup'); - - $this->sendJsonResponse($r); - } - - /** - * Sets the default language. - * - * @return void - * - * @since 4.0.0 - */ - public function setdefault() - { - $this->checkValidToken(); - - $app = $this->app; - - /** @var \Joomla\CMS\Installation\Model\LanguagesModel $model */ - $model = $this->getModel('Languages'); - - // Check for request forgeries in the administrator language - $admin_lang = $this->input->getString('administratorlang', false); - - // Check that the string is an ISO Language Code avoiding any injection. - if (!preg_match('/^[a-z]{2}(\-[A-Z]{2})?$/', $admin_lang)) - { - $admin_lang = 'en-GB'; - } - - // Attempt to set the default administrator language - if (!$model->setDefault($admin_lang, 'administrator')) - { - // Create an error response message. - $this->app->enqueueMessage(Text::_('INSTL_DEFAULTLANGUAGE_ADMIN_COULDNT_SET_DEFAULT'), 'error'); - } - else - { - // Create a response body. - $app->enqueueMessage(Text::sprintf('INSTL_DEFAULTLANGUAGE_ADMIN_SET_DEFAULT', $admin_lang), 'message'); - } - - // Check for request forgeries in the site language - $frontend_lang = $this->input->getString('frontendlang', false); - - // Check that the string is an ISO Language Code avoiding any injection. - if (!preg_match('/^[a-z]{2}(\-[A-Z]{2})?$/', $frontend_lang)) - { - $frontend_lang = 'en-GB'; - } - - // Attempt to set the default site language - if (!$model->setDefault($frontend_lang, 'site')) - { - // Create an error response message. - $app->enqueueMessage(Text::_('INSTL_DEFAULTLANGUAGE_FRONTEND_COULDNT_SET_DEFAULT'), 'error'); - } - else - { - // Create a response body. - $app->enqueueMessage(Text::sprintf('INSTL_DEFAULTLANGUAGE_FRONTEND_SET_DEFAULT', $frontend_lang), 'message'); - } - - $r = new \stdClass; - - // Redirect to the final page. - $r->view = 'remove'; - $this->sendJsonResponse($r); - } + /** + * Sets the language. + * + * @return void + * + * @since 4.0.0 + */ + public function set() + { + $this->checkValidToken(); + + // Check for potentially unwritable session + $session = $this->app->getSession(); + + if ($session->isNew()) { + $this->sendJsonResponse(new \Exception(Text::_('INSTL_COOKIES_NOT_ENABLED'), 500)); + } + + /** @var SetupModel $model */ + $model = $this->getModel('Setup'); + + // Get the posted values from the request and validate them. + $data = $this->input->post->get('jform', [], 'array'); + $return = $model->validate($data, 'language'); + + $r = new \stdClass(); + + // Check for validation errors. + if ($return === false) { + /* + * The validate method enqueued all messages for us, so we just need to + * redirect back to the site setup screen. + */ + $r->view = $this->input->getWord('view', 'setup'); + $this->sendJsonResponse($r); + } + + // Store the options in the session. + $model->storeOptions($return); + + // Setup language + Factory::$language = Language::getInstance($return['language']); + + // Redirect to the page. + $r->view = $this->input->getWord('view', 'setup'); + + $this->sendJsonResponse($r); + } + + /** + * Sets the default language. + * + * @return void + * + * @since 4.0.0 + */ + public function setdefault() + { + $this->checkValidToken(); + + $app = $this->app; + + /** @var \Joomla\CMS\Installation\Model\LanguagesModel $model */ + $model = $this->getModel('Languages'); + + // Check for request forgeries in the administrator language + $admin_lang = $this->input->getString('administratorlang', false); + + // Check that the string is an ISO Language Code avoiding any injection. + if (!preg_match('/^[a-z]{2}(\-[A-Z]{2})?$/', $admin_lang)) { + $admin_lang = 'en-GB'; + } + + // Attempt to set the default administrator language + if (!$model->setDefault($admin_lang, 'administrator')) { + // Create an error response message. + $this->app->enqueueMessage(Text::_('INSTL_DEFAULTLANGUAGE_ADMIN_COULDNT_SET_DEFAULT'), 'error'); + } else { + // Create a response body. + $app->enqueueMessage(Text::sprintf('INSTL_DEFAULTLANGUAGE_ADMIN_SET_DEFAULT', $admin_lang), 'message'); + } + + // Check for request forgeries in the site language + $frontend_lang = $this->input->getString('frontendlang', false); + + // Check that the string is an ISO Language Code avoiding any injection. + if (!preg_match('/^[a-z]{2}(\-[A-Z]{2})?$/', $frontend_lang)) { + $frontend_lang = 'en-GB'; + } + + // Attempt to set the default site language + if (!$model->setDefault($frontend_lang, 'site')) { + // Create an error response message. + $app->enqueueMessage(Text::_('INSTL_DEFAULTLANGUAGE_FRONTEND_COULDNT_SET_DEFAULT'), 'error'); + } else { + // Create a response body. + $app->enqueueMessage(Text::sprintf('INSTL_DEFAULTLANGUAGE_FRONTEND_SET_DEFAULT', $frontend_lang), 'message'); + } + + $r = new \stdClass(); + + // Redirect to the final page. + $r->view = 'remove'; + $this->sendJsonResponse($r); + } } diff --git a/installation/src/Error/Renderer/JsonRenderer.php b/installation/src/Error/Renderer/JsonRenderer.php index 72c0438eec900..e4dd9c622b547 100644 --- a/installation/src/Error/Renderer/JsonRenderer.php +++ b/installation/src/Error/Renderer/JsonRenderer.php @@ -1,4 +1,5 @@ ` tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string $group The field name group control value. This acts as as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * - * @return boolean True on success. - * - * @since 4.2.0 - */ - public function setup(\SimpleXMLElement $element, $value, $group = null) - { - $value = $this->getNativeLanguage(); - - return parent::setup($element, $value, $group); - } - - /** - * Method to get the field options. - * - * @return array The field option objects. - * - * @since 1.6 - */ - protected function getOptions() - { - $native = $this->getNativeLanguage(); - - // Get the list of available languages. - $options = LanguageHelper::createLanguageList($native); - - // Fix wrongly set parentheses in RTL languages - if (Factory::getLanguage()->isRtl()) - { - foreach ($options as &$option) - { - $option['text'] .= '‎'; - } - } - - if (!$options || $options instanceof \Exception) - { - $options = array(); - } - // Sort languages by name - else - { - usort($options, array($this, '_sortLanguages')); - } - - // Merge any additional options in the XML definition. - $options = array_merge(parent::getOptions(), $options); - - return $options; - } - - /** - * Method to sort languages by name. - * - * @param array $a The first value to determine sort - * @param array $b The second value to determine sort - * - * @return integer - * - * @since 3.1 - */ - protected function _sortLanguages($a, $b) - { - return strcmp($a['text'], $b['text']); - } - - /** - * Determinate the native language to select - * - * @return string The native language to use - * - * @since 4.2.0 - */ - protected function getNativeLanguage() - { - static $native; - - if (isset($native)) - { - return $native; - } - - $app = Factory::getApplication(); - - // Detect the native language. - $native = LanguageHelper::detectLanguage(); - - if (empty($native)) - { - $native = 'en-GB'; - } - - // Get a forced language if it exists. - $forced = $app->getLocalise(); - - if (!empty($forced['language'])) - { - $native = $forced['language']; - } - - // If a language is already set in the session, use this instead - $model = new SetupModel; - $options = $model->getOptions(); - - if (isset($options['language'])) - { - $native = $options['language']; - } - - return $native; - } + /** + * The form field type. + * + * @var string + * @since 1.6 + */ + protected $type = 'Language'; + + /** + * Method to attach a Form object to the field. + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the `` tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string $group The field name group control value. This acts as as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * + * @return boolean True on success. + * + * @since 4.2.0 + */ + public function setup(\SimpleXMLElement $element, $value, $group = null) + { + $value = $this->getNativeLanguage(); + + return parent::setup($element, $value, $group); + } + + /** + * Method to get the field options. + * + * @return array The field option objects. + * + * @since 1.6 + */ + protected function getOptions() + { + $native = $this->getNativeLanguage(); + + // Get the list of available languages. + $options = LanguageHelper::createLanguageList($native); + + // Fix wrongly set parentheses in RTL languages + if (Factory::getLanguage()->isRtl()) { + foreach ($options as &$option) { + $option['text'] .= '‎'; + } + } + + if (!$options || $options instanceof \Exception) { + $options = array(); + } else { + // Sort languages by name + usort($options, array($this, '_sortLanguages')); + } + + // Merge any additional options in the XML definition. + $options = array_merge(parent::getOptions(), $options); + + return $options; + } + + /** + * Method to sort languages by name. + * + * @param array $a The first value to determine sort + * @param array $b The second value to determine sort + * + * @return integer + * + * @since 3.1 + */ + protected function _sortLanguages($a, $b) + { + return strcmp($a['text'], $b['text']); + } + + /** + * Determinate the native language to select + * + * @return string The native language to use + * + * @since 4.2.0 + */ + protected function getNativeLanguage() + { + static $native; + + if (isset($native)) { + return $native; + } + + $app = Factory::getApplication(); + + // Detect the native language. + $native = LanguageHelper::detectLanguage(); + + if (empty($native)) { + $native = 'en-GB'; + } + + // Get a forced language if it exists. + $forced = $app->getLocalise(); + + if (!empty($forced['language'])) { + $native = $forced['language']; + } + + // If a language is already set in the session, use this instead + $model = new SetupModel(); + $options = $model->getOptions(); + + if (isset($options['language'])) { + $native = $options['language']; + } + + return $native; + } } diff --git a/installation/src/Form/Field/Installation/PrefixField.php b/installation/src/Form/Field/Installation/PrefixField.php index 272150ee895db..1516a4cb7c792 100644 --- a/installation/src/Form/Field/Installation/PrefixField.php +++ b/installation/src/Form/Field/Installation/PrefixField.php @@ -1,4 +1,5 @@ element['size'] ? abs((int) $this->element['size']) : 5; - $maxLength = $this->element['maxlength'] ? ' maxlength="' . (int) $this->element['maxlength'] . '"' : ''; - $class = $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; - $readonly = (string) $this->element['readonly'] === 'true' ? ' readonly="readonly"' : ''; - $disabled = (string) $this->element['disabled'] === 'true' ? ' disabled="disabled"' : ''; + /** + * Method to get the field input markup. + * + * @return string The field input markup. + * + * @since 1.6 + */ + protected function getInput() + { + // Initialize some field attributes. + $size = $this->element['size'] ? abs((int) $this->element['size']) : 5; + $maxLength = $this->element['maxlength'] ? ' maxlength="' . (int) $this->element['maxlength'] . '"' : ''; + $class = $this->element['class'] ? ' class="' . (string) $this->element['class'] . '"' : ''; + $readonly = (string) $this->element['readonly'] === 'true' ? ' readonly="readonly"' : ''; + $disabled = (string) $this->element['disabled'] === 'true' ? ' disabled="disabled"' : ''; - // Make sure somebody doesn't put in a too large prefix size value. - if ($size > 10) - { - $size = 10; - } + // Make sure somebody doesn't put in a too large prefix size value. + if ($size > 10) { + $size = 10; + } - // If a prefix is already set, use it instead. - $session = Factory::getSession()->get('setup.options', array()); + // If a prefix is already set, use it instead. + $session = Factory::getSession()->get('setup.options', array()); - if (empty($session['db_prefix'])) - { - // Create the random prefix. - $prefix = ''; - $chars = range('a', 'z'); - $numbers = range(0, 9); + if (empty($session['db_prefix'])) { + // Create the random prefix. + $prefix = ''; + $chars = range('a', 'z'); + $numbers = range(0, 9); - // We want the fist character to be a random letter. - shuffle($chars); - $prefix .= $chars[0]; + // We want the fist character to be a random letter. + shuffle($chars); + $prefix .= $chars[0]; - // Next we combine the numbers and characters to get the other characters. - $symbols = array_merge($numbers, $chars); - shuffle($symbols); + // Next we combine the numbers and characters to get the other characters. + $symbols = array_merge($numbers, $chars); + shuffle($symbols); - for ($i = 0, $j = $size - 1; $i < $j; ++$i) - { - $prefix .= $symbols[$i]; - } + for ($i = 0, $j = $size - 1; $i < $j; ++$i) { + $prefix .= $symbols[$i]; + } - // Add in the underscore. - $prefix .= '_'; - } - else - { - $prefix = $session['db_prefix']; - } + // Add in the underscore. + $prefix .= '_'; + } else { + $prefix = $session['db_prefix']; + } - // Initialize JavaScript field attributes. - $onchange = $this->element['onchange'] ? ' onchange="' . (string) $this->element['onchange'] . '"' : ''; + // Initialize JavaScript field attributes. + $onchange = $this->element['onchange'] ? ' onchange="' . (string) $this->element['onchange'] . '"' : ''; - return ''; - } + return ''; + } } diff --git a/installation/src/Form/Rule/PrefixRule.php b/installation/src/Form/Rule/PrefixRule.php index 6bb6e98c4a660..252cbd313f15c 100644 --- a/installation/src/Form/Rule/PrefixRule.php +++ b/installation/src/Form/Rule/PrefixRule.php @@ -1,4 +1,5 @@ tag for the form field object. - * @param mixed $value The form field value to validate. - * @param string|null $group The field name group control value. This acts as an array container for the field. - * For example if the field has name="foo" and the group value is set to "bar" then the - * full field name would end up being "bar[foo]". - * @param Registry|null $input An optional Registry object with the entire data set to validate against the entire form. - * @param Form|null $form The form object for which the field is being tested. - * - * @return boolean True if the value is valid, false otherwise. - */ - public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) - { - $filterInput = InputFilter::getInstance(); + /** + * Method to test a username + * + * @param \SimpleXMLElement $element The SimpleXMLElement object representing the tag for the form field object. + * @param mixed $value The form field value to validate. + * @param string|null $group The field name group control value. This acts as an array container for the field. + * For example if the field has name="foo" and the group value is set to "bar" then the + * full field name would end up being "bar[foo]". + * @param Registry|null $input An optional Registry object with the entire data set to validate against the entire form. + * @param Form|null $form The form object for which the field is being tested. + * + * @return boolean True if the value is valid, false otherwise. + */ + public function test(\SimpleXMLElement $element, $value, $group = null, Registry $input = null, Form $form = null) + { + $filterInput = InputFilter::getInstance(); - if (preg_match('#[<>"\'%;()&\\\\]|\\.\\./#', $value) || strlen(utf8_decode($value)) < 2 - || $filterInput->clean($value, 'TRIM') !== $value - || strlen(utf8_decode($value)) > $element['size']) - { - return false; - } + if ( + preg_match('#[<>"\'%;()&\\\\]|\\.\\./#', $value) || strlen(utf8_decode($value)) < 2 + || $filterInput->clean($value, 'TRIM') !== $value + || strlen(utf8_decode($value)) > $element['size'] + ) { + return false; + } - return true; - } + return true; + } } diff --git a/installation/src/Helper/DatabaseHelper.php b/installation/src/Helper/DatabaseHelper.php index 5ffe9c658ec54..e8b3aaa07fe09 100644 --- a/installation/src/Helper/DatabaseHelper.php +++ b/installation/src/Helper/DatabaseHelper.php @@ -1,4 +1,5 @@ $driver, - 'host' => $host, - 'user' => $user, - 'password' => $password, - 'database' => $database, - 'prefix' => $prefix, - 'select' => $select, - ]; - - if (!empty($ssl['dbencryption'])) - { - $options['ssl'] = [ - 'enable' => true, - 'verify_server_cert' => (bool) $ssl['dbsslverifyservercert'], - ]; - - foreach (['cipher', 'ca', 'key', 'cert'] as $value) - { - $confVal = trim($ssl['dbssl' . $value]); - - if ($confVal !== '') - { - $options['ssl'][$value] = $confVal; - } - } - } - - // Enable utf8mb4 connections for mysql adapters - if (strtolower($driver) === 'mysqli') - { - $options['utf8mb4'] = true; - } - - if (strtolower($driver) === 'mysql') - { - $options['charset'] = 'utf8mb4'; - } - - // Get a database object. - $db = DatabaseDriver::getInstance($options); - } - - return $db; - } - - /** - * Convert encryption options to array. - * - * @param \stdClass $options The session options - * - * @return array The encryption settings - * - * @since 4.0.0 - */ - public static function getEncryptionSettings($options) - { - return [ - 'dbencryption' => $options->db_encryption, - 'dbsslverifyservercert' => $options->db_sslverifyservercert, - 'dbsslkey' => $options->db_sslkey, - 'dbsslcert' => $options->db_sslcert, - 'dbsslca' => $options->db_sslca, - 'dbsslcipher' => $options->db_sslcipher, - ]; - } - - /** - * Get the minimum required database server version. - * - * @param DatabaseDriver $db Database object - * @param \stdClass $options The session options - * - * @return string The minimum required database server version. - * - * @since 4.0.0 - */ - public static function getMinimumServerVersion($db, $options) - { - // Get minimum database version required by the database driver - $minDbVersionRequired = $db->getMinimum(); - - // Get minimum database version required by the CMS - if (in_array($options->db_type, ['mysql', 'mysqli'])) - { - if ($db->isMariaDb()) - { - $minDbVersionCms = self::$dbMinimumMariaDb; - } - else - { - $minDbVersionCms = self::$dbMinimumMySql; - } - } - else - { - $minDbVersionCms = self::$dbMinimumPostgreSql; - } - - // Use most restrictive, i.e. largest minimum database version requirement - if (version_compare($minDbVersionCms, $minDbVersionRequired) > 0) - { - $minDbVersionRequired = $minDbVersionCms; - } - - return $minDbVersionRequired; - } - - /** - * Validate and clean up database connection parameters. - * - * @param \stdClass $options The session options - * - * @return string|boolean A string with the translated error message if - * validation error, otherwise false. - * - * @since 4.0.0 - */ - public static function validateConnectionParameters($options) - { - // Ensure a database type was selected. - if (empty($options->db_type)) - { - return Text::_('INSTL_DATABASE_INVALID_TYPE'); - } - - // Ensure that a hostname and user name were input. - if (empty($options->db_host) || empty($options->db_user)) - { - return Text::_('INSTL_DATABASE_INVALID_DB_DETAILS'); - } - - // Ensure that a database name is given. - if (empty($options->db_name)) - { - return Text::_('INSTL_DATABASE_EMPTY_NAME'); - } - - // Validate length of database name. - if (strlen($options->db_name) > 64) - { - return Text::_('INSTL_DATABASE_NAME_TOO_LONG'); - } - - // Validate database table prefix. - if (empty($options->db_prefix) || !preg_match('#^[a-zA-Z]+[a-zA-Z0-9_]*$#', $options->db_prefix)) - { - return Text::_('INSTL_DATABASE_PREFIX_MSG'); - } - - // Validate length of database table prefix. - if (strlen($options->db_prefix) > 15) - { - return Text::_('INSTL_DATABASE_FIX_TOO_LONG'); - } - - // Validate database name. - if (in_array($options->db_type, ['pgsql', 'postgresql']) && !preg_match('#^[a-zA-Z_][0-9a-zA-Z_$]*$#', $options->db_name)) - { - return Text::_('INSTL_DATABASE_NAME_MSG_POSTGRES'); - } - - if (in_array($options->db_type, ['mysql', 'mysqli']) && preg_match('#[\\\\\/]#', $options->db_name)) - { - return Text::_('INSTL_DATABASE_NAME_MSG_MYSQL'); - } - - // Workaround for UPPERCASE table prefix for postgresql - if (in_array($options->db_type, ['pgsql', 'postgresql'])) - { - if (strtolower($options->db_prefix) != $options->db_prefix) - { - return Text::_('INSTL_DATABASE_FIX_LOWERCASE'); - } - } - - // Validate and clean up database connection encryption options - $optionsChanged = false; - - if ($options->db_encryption === 0) - { - // Reset unused options - if (!empty($options->db_sslkey)) - { - $options->db_sslkey = ''; - $optionsChanged = true; - } - - if (!empty($options->db_sslcert)) - { - $options->db_sslcert = ''; - $optionsChanged = true; - } - - if ($options->db_sslverifyservercert) - { - $options->db_sslverifyservercert = false; - $optionsChanged = true; - } - - if (!empty($options->db_sslca)) - { - $options->db_sslca = ''; - $optionsChanged = true; - } - - if (!empty($options->db_sslcipher)) - { - $options->db_sslcipher = ''; - $optionsChanged = true; - } - } - else - { - // Check localhost - if (strtolower($options->db_host) === 'localhost') - { - return Text::_('INSTL_DATABASE_ENCRYPTION_MSG_LOCALHOST'); - } - - // Check CA file and folder depending on database type if server certificate verification - if ($options->db_sslverifyservercert) - { - if (empty($options->db_sslca)) - { - return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_EMPTY', Text::_('INSTL_DATABASE_ENCRYPTION_CA_LABEL')); - } - - if (!File::exists(Path::clean($options->db_sslca))) - { - return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_BAD', Text::_('INSTL_DATABASE_ENCRYPTION_CA_LABEL')); - } - } - else - { - // Reset unused option - if (!empty($options->db_sslca)) - { - $options->db_sslca = ''; - $optionsChanged = true; - } - } - - // Check key and certificate if two-way encryption - if ($options->db_encryption === 2) - { - if (empty($options->db_sslkey)) - { - return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_EMPTY', Text::_('INSTL_DATABASE_ENCRYPTION_KEY_LABEL')); - } - - if (!File::exists(Path::clean($options->db_sslkey))) - { - return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_BAD', Text::_('INSTL_DATABASE_ENCRYPTION_KEY_LABEL')); - } - - if (empty($options->db_sslcert)) - { - return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_EMPTY', Text::_('INSTL_DATABASE_ENCRYPTION_CERT_LABEL')); - } - - if (!File::exists(Path::clean($options->db_sslcert))) - { - return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_BAD', Text::_('INSTL_DATABASE_ENCRYPTION_CERT_LABEL')); - } - } - else - { - // Reset unused options - if (!empty($options->db_sslkey)) - { - $options->db_sslkey = ''; - $optionsChanged = true; - } - - if (!empty($options->db_sslcert)) - { - $options->db_sslcert = ''; - $optionsChanged = true; - } - } - } - - // Save options to session data if changed - if ($optionsChanged) - { - $optsArr = ArrayHelper::fromObject($options); - Factory::getSession()->set('setup.options', $optsArr); - } - - return false; - } - - /** - * Security check for remote db hosts - * - * @param \stdClass $options The session options - * - * @return boolean True if passed, otherwise false. - * - * @since 4.0.0 - */ - public static function checkRemoteDbHost($options) - { - // Security check for remote db hosts: Check env var if disabled - $shouldCheckLocalhost = getenv('JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK') !== '1'; - - // Per default allowed DB hosts: localhost / 127.0.0.1 / ::1 (optionally with port) - $localhost = '/^(((localhost|127\.0\.0\.1|\[\:\:1\])(\:[1-9]{1}[0-9]{0,4})?)|(\:\:1))$/'; - - // Check the security file if the db_host is not localhost / 127.0.0.1 / ::1 - if ($shouldCheckLocalhost && preg_match($localhost, $options->db_host) !== 1) - { - $remoteDbFileTestsPassed = Factory::getSession()->get('remoteDbFileTestsPassed', false); - - // When all checks have been passed we don't need to do this here again. - if ($remoteDbFileTestsPassed === false) - { - $generalRemoteDatabaseMessage = Text::sprintf( - 'INSTL_DATABASE_HOST_IS_NOT_LOCALHOST_GENERAL_MESSAGE', - 'https://docs.joomla.org/Special:MyLanguage/J3.x:Secured_procedure_for_installing_Joomla_with_a_remote_database' - ); - - $remoteDbFile = Factory::getSession()->get('remoteDbFile', false); - - if ($remoteDbFile === false) - { - // Add the general message - Factory::getApplication()->enqueueMessage($generalRemoteDatabaseMessage, 'warning'); - - // This is the file you need to remove if you want to use a remote database - $remoteDbFile = '_Joomla' . UserHelper::genRandomPassword(21) . '.txt'; - Factory::getSession()->set('remoteDbFile', $remoteDbFile); - - // Get the path - $remoteDbPath = JPATH_INSTALLATION . '/' . $remoteDbFile; - - // When the path is not writable the user needs to create the file manually - if (!File::write($remoteDbPath, '')) - { - // Request to create the file manually - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'INSTL_DATABASE_HOST_IS_NOT_LOCALHOST_CREATE_FILE', - $remoteDbFile, - 'installation', - Text::_('INSTL_INSTALL_JOOMLA') - ), - 'notice' - ); - - Factory::getSession()->set('remoteDbFileUnwritable', true); - - return false; - } - - // Save the file name to the session - Factory::getSession()->set('remoteDbFileWrittenByJoomla', true); - - // Request to delete that file - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'INSTL_DATABASE_HOST_IS_NOT_LOCALHOST_DELETE_FILE', - $remoteDbFile, - 'installation', - Text::_('INSTL_INSTALL_JOOMLA') - ), - 'notice' - ); - - return false; - } - - if (Factory::getSession()->get('remoteDbFileWrittenByJoomla', false) === true - && File::exists(JPATH_INSTALLATION . '/' . $remoteDbFile)) - { - // Add the general message - Factory::getApplication()->enqueueMessage($generalRemoteDatabaseMessage, 'warning'); - - // Request to delete the file - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'INSTL_DATABASE_HOST_IS_NOT_LOCALHOST_DELETE_FILE', - $remoteDbFile, - 'installation', - Text::_('INSTL_INSTALL_JOOMLA') - ), - 'notice' - ); - - return false; - } - - if (Factory::getSession()->get('remoteDbFileUnwritable', false) === true && !File::exists(JPATH_INSTALLATION . '/' . $remoteDbFile)) - { - // Add the general message - Factory::getApplication()->enqueueMessage($generalRemoteDatabaseMessage, 'warning'); - - // Request to create the file manually - Factory::getApplication()->enqueueMessage( - Text::sprintf( - 'INSTL_DATABASE_HOST_IS_NOT_LOCALHOST_CREATE_FILE', - $remoteDbFile, - 'installation', - Text::_('INSTL_INSTALL_JOOMLA') - ), - 'notice' - ); - - return false; - } - - // All tests for this session passed set it to the session - Factory::getSession()->set('remoteDbFileTestsPassed', true); - } - } - - return true; - } - - /** - * Check database server parameters after connection - * - * @param DatabaseDriver $db Database object - * @param \stdClass $options The session options - * - * @return string|boolean A string with the translated error message if - * some server parameter is not ok, otherwise false. - * - * @since 4.0.0 - */ - public static function checkDbServerParameters($db, $options) - { - $dbVersion = $db->getVersion(); - - // Get required database version - $minDbVersionRequired = self::getMinimumServerVersion($db, $options); - - // Check minimum database version - if (version_compare($dbVersion, $minDbVersionRequired) < 0) - { - if (in_array($options->db_type, ['mysql', 'mysqli']) && $db->isMariaDb()) - { - $errorMessage = Text::sprintf( - 'INSTL_DATABASE_INVALID_MARIADB_VERSION', - $minDbVersionRequired, - $dbVersion - ); - } - else - { - $errorMessage = Text::sprintf( - 'INSTL_DATABASE_INVALID_' . strtoupper($options->db_type) . '_VERSION', - $minDbVersionRequired, - $dbVersion - ); - } - - return $errorMessage; - } - - // Check database connection encryption - if ($options->db_encryption !== 0 && empty($db->getConnectionEncryption())) - { - if ($db->isConnectionEncryptionSupported()) - { - $errorMessage = Text::_('INSTL_DATABASE_ENCRYPTION_MSG_CONN_NOT_ENCRYPT'); - } - else - { - $errorMessage = Text::_('INSTL_DATABASE_ENCRYPTION_MSG_SRV_NOT_SUPPORTS'); - } - - return $errorMessage; - } - - return false; - } + /** + * The minimum database server version for MariaDB databases as required by the CMS. + * This is not necessarily equal to what the database driver requires. + * + * @var string + * @since 4.0.0 + */ + protected static $dbMinimumMariaDb = '10.1'; + + /** + * The minimum database server version for MySQL databases as required by the CMS. + * This is not necessarily equal to what the database driver requires. + * + * @var string + * @since 4.0.0 + */ + protected static $dbMinimumMySql = '5.6'; + + /** + * The minimum database server version for PostgreSQL databases as required by the CMS. + * This is not necessarily equal to what the database driver requires. + * + * @var string + * @since 4.0.0 + */ + protected static $dbMinimumPostgreSql = '11.0'; + + /** + * Method to get a database driver. + * + * @param string $driver The database driver to use. + * @param string $host The hostname to connect on. + * @param string $user The user name to connect with. + * @param string $password The password to use for connection authentication. + * @param string $database The database to use. + * @param string $prefix The table prefix to use. + * @param boolean $select True if the database should be selected. + * @param array $ssl Database TLS connection options. + * + * @return DatabaseInterface + * + * @since 1.6 + */ + public static function getDbo($driver, $host, $user, $password, $database, $prefix, $select = true, array $ssl = []) + { + static $db; + + if (!$db) { + // Build the connection options array. + $options = [ + 'driver' => $driver, + 'host' => $host, + 'user' => $user, + 'password' => $password, + 'database' => $database, + 'prefix' => $prefix, + 'select' => $select, + ]; + + if (!empty($ssl['dbencryption'])) { + $options['ssl'] = [ + 'enable' => true, + 'verify_server_cert' => (bool) $ssl['dbsslverifyservercert'], + ]; + + foreach (['cipher', 'ca', 'key', 'cert'] as $value) { + $confVal = trim($ssl['dbssl' . $value]); + + if ($confVal !== '') { + $options['ssl'][$value] = $confVal; + } + } + } + + // Enable utf8mb4 connections for mysql adapters + if (strtolower($driver) === 'mysqli') { + $options['utf8mb4'] = true; + } + + if (strtolower($driver) === 'mysql') { + $options['charset'] = 'utf8mb4'; + } + + // Get a database object. + $db = DatabaseDriver::getInstance($options); + } + + return $db; + } + + /** + * Convert encryption options to array. + * + * @param \stdClass $options The session options + * + * @return array The encryption settings + * + * @since 4.0.0 + */ + public static function getEncryptionSettings($options) + { + return [ + 'dbencryption' => $options->db_encryption, + 'dbsslverifyservercert' => $options->db_sslverifyservercert, + 'dbsslkey' => $options->db_sslkey, + 'dbsslcert' => $options->db_sslcert, + 'dbsslca' => $options->db_sslca, + 'dbsslcipher' => $options->db_sslcipher, + ]; + } + + /** + * Get the minimum required database server version. + * + * @param DatabaseDriver $db Database object + * @param \stdClass $options The session options + * + * @return string The minimum required database server version. + * + * @since 4.0.0 + */ + public static function getMinimumServerVersion($db, $options) + { + // Get minimum database version required by the database driver + $minDbVersionRequired = $db->getMinimum(); + + // Get minimum database version required by the CMS + if (in_array($options->db_type, ['mysql', 'mysqli'])) { + if ($db->isMariaDb()) { + $minDbVersionCms = self::$dbMinimumMariaDb; + } else { + $minDbVersionCms = self::$dbMinimumMySql; + } + } else { + $minDbVersionCms = self::$dbMinimumPostgreSql; + } + + // Use most restrictive, i.e. largest minimum database version requirement + if (version_compare($minDbVersionCms, $minDbVersionRequired) > 0) { + $minDbVersionRequired = $minDbVersionCms; + } + + return $minDbVersionRequired; + } + + /** + * Validate and clean up database connection parameters. + * + * @param \stdClass $options The session options + * + * @return string|boolean A string with the translated error message if + * validation error, otherwise false. + * + * @since 4.0.0 + */ + public static function validateConnectionParameters($options) + { + // Ensure a database type was selected. + if (empty($options->db_type)) { + return Text::_('INSTL_DATABASE_INVALID_TYPE'); + } + + // Ensure that a hostname and user name were input. + if (empty($options->db_host) || empty($options->db_user)) { + return Text::_('INSTL_DATABASE_INVALID_DB_DETAILS'); + } + + // Ensure that a database name is given. + if (empty($options->db_name)) { + return Text::_('INSTL_DATABASE_EMPTY_NAME'); + } + + // Validate length of database name. + if (strlen($options->db_name) > 64) { + return Text::_('INSTL_DATABASE_NAME_TOO_LONG'); + } + + // Validate database table prefix. + if (empty($options->db_prefix) || !preg_match('#^[a-zA-Z]+[a-zA-Z0-9_]*$#', $options->db_prefix)) { + return Text::_('INSTL_DATABASE_PREFIX_MSG'); + } + + // Validate length of database table prefix. + if (strlen($options->db_prefix) > 15) { + return Text::_('INSTL_DATABASE_FIX_TOO_LONG'); + } + + // Validate database name. + if (in_array($options->db_type, ['pgsql', 'postgresql']) && !preg_match('#^[a-zA-Z_][0-9a-zA-Z_$]*$#', $options->db_name)) { + return Text::_('INSTL_DATABASE_NAME_MSG_POSTGRES'); + } + + if (in_array($options->db_type, ['mysql', 'mysqli']) && preg_match('#[\\\\\/]#', $options->db_name)) { + return Text::_('INSTL_DATABASE_NAME_MSG_MYSQL'); + } + + // Workaround for UPPERCASE table prefix for postgresql + if (in_array($options->db_type, ['pgsql', 'postgresql'])) { + if (strtolower($options->db_prefix) != $options->db_prefix) { + return Text::_('INSTL_DATABASE_FIX_LOWERCASE'); + } + } + + // Validate and clean up database connection encryption options + $optionsChanged = false; + + if ($options->db_encryption === 0) { + // Reset unused options + if (!empty($options->db_sslkey)) { + $options->db_sslkey = ''; + $optionsChanged = true; + } + + if (!empty($options->db_sslcert)) { + $options->db_sslcert = ''; + $optionsChanged = true; + } + + if ($options->db_sslverifyservercert) { + $options->db_sslverifyservercert = false; + $optionsChanged = true; + } + + if (!empty($options->db_sslca)) { + $options->db_sslca = ''; + $optionsChanged = true; + } + + if (!empty($options->db_sslcipher)) { + $options->db_sslcipher = ''; + $optionsChanged = true; + } + } else { + // Check localhost + if (strtolower($options->db_host) === 'localhost') { + return Text::_('INSTL_DATABASE_ENCRYPTION_MSG_LOCALHOST'); + } + + // Check CA file and folder depending on database type if server certificate verification + if ($options->db_sslverifyservercert) { + if (empty($options->db_sslca)) { + return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_EMPTY', Text::_('INSTL_DATABASE_ENCRYPTION_CA_LABEL')); + } + + if (!File::exists(Path::clean($options->db_sslca))) { + return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_BAD', Text::_('INSTL_DATABASE_ENCRYPTION_CA_LABEL')); + } + } else { + // Reset unused option + if (!empty($options->db_sslca)) { + $options->db_sslca = ''; + $optionsChanged = true; + } + } + + // Check key and certificate if two-way encryption + if ($options->db_encryption === 2) { + if (empty($options->db_sslkey)) { + return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_EMPTY', Text::_('INSTL_DATABASE_ENCRYPTION_KEY_LABEL')); + } + + if (!File::exists(Path::clean($options->db_sslkey))) { + return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_BAD', Text::_('INSTL_DATABASE_ENCRYPTION_KEY_LABEL')); + } + + if (empty($options->db_sslcert)) { + return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_EMPTY', Text::_('INSTL_DATABASE_ENCRYPTION_CERT_LABEL')); + } + + if (!File::exists(Path::clean($options->db_sslcert))) { + return Text::sprintf('INSTL_DATABASE_ENCRYPTION_MSG_FILE_FIELD_BAD', Text::_('INSTL_DATABASE_ENCRYPTION_CERT_LABEL')); + } + } else { + // Reset unused options + if (!empty($options->db_sslkey)) { + $options->db_sslkey = ''; + $optionsChanged = true; + } + + if (!empty($options->db_sslcert)) { + $options->db_sslcert = ''; + $optionsChanged = true; + } + } + } + + // Save options to session data if changed + if ($optionsChanged) { + $optsArr = ArrayHelper::fromObject($options); + Factory::getSession()->set('setup.options', $optsArr); + } + + return false; + } + + /** + * Security check for remote db hosts + * + * @param \stdClass $options The session options + * + * @return boolean True if passed, otherwise false. + * + * @since 4.0.0 + */ + public static function checkRemoteDbHost($options) + { + // Security check for remote db hosts: Check env var if disabled + $shouldCheckLocalhost = getenv('JOOMLA_INSTALLATION_DISABLE_LOCALHOST_CHECK') !== '1'; + + // Per default allowed DB hosts: localhost / 127.0.0.1 / ::1 (optionally with port) + $localhost = '/^(((localhost|127\.0\.0\.1|\[\:\:1\])(\:[1-9]{1}[0-9]{0,4})?)|(\:\:1))$/'; + + // Check the security file if the db_host is not localhost / 127.0.0.1 / ::1 + if ($shouldCheckLocalhost && preg_match($localhost, $options->db_host) !== 1) { + $remoteDbFileTestsPassed = Factory::getSession()->get('remoteDbFileTestsPassed', false); + + // When all checks have been passed we don't need to do this here again. + if ($remoteDbFileTestsPassed === false) { + $generalRemoteDatabaseMessage = Text::sprintf( + 'INSTL_DATABASE_HOST_IS_NOT_LOCALHOST_GENERAL_MESSAGE', + 'https://docs.joomla.org/Special:MyLanguage/J3.x:Secured_procedure_for_installing_Joomla_with_a_remote_database' + ); + + $remoteDbFile = Factory::getSession()->get('remoteDbFile', false); + + if ($remoteDbFile === false) { + // Add the general message + Factory::getApplication()->enqueueMessage($generalRemoteDatabaseMessage, 'warning'); + + // This is the file you need to remove if you want to use a remote database + $remoteDbFile = '_Joomla' . UserHelper::genRandomPassword(21) . '.txt'; + Factory::getSession()->set('remoteDbFile', $remoteDbFile); + + // Get the path + $remoteDbPath = JPATH_INSTALLATION . '/' . $remoteDbFile; + + // When the path is not writable the user needs to create the file manually + if (!File::write($remoteDbPath, '')) { + // Request to create the file manually + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'INSTL_DATABASE_HOST_IS_NOT_LOCALHOST_CREATE_FILE', + $remoteDbFile, + 'installation', + Text::_('INSTL_INSTALL_JOOMLA') + ), + 'notice' + ); + + Factory::getSession()->set('remoteDbFileUnwritable', true); + + return false; + } + + // Save the file name to the session + Factory::getSession()->set('remoteDbFileWrittenByJoomla', true); + + // Request to delete that file + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'INSTL_DATABASE_HOST_IS_NOT_LOCALHOST_DELETE_FILE', + $remoteDbFile, + 'installation', + Text::_('INSTL_INSTALL_JOOMLA') + ), + 'notice' + ); + + return false; + } + + if ( + Factory::getSession()->get('remoteDbFileWrittenByJoomla', false) === true + && File::exists(JPATH_INSTALLATION . '/' . $remoteDbFile) + ) { + // Add the general message + Factory::getApplication()->enqueueMessage($generalRemoteDatabaseMessage, 'warning'); + + // Request to delete the file + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'INSTL_DATABASE_HOST_IS_NOT_LOCALHOST_DELETE_FILE', + $remoteDbFile, + 'installation', + Text::_('INSTL_INSTALL_JOOMLA') + ), + 'notice' + ); + + return false; + } + + if (Factory::getSession()->get('remoteDbFileUnwritable', false) === true && !File::exists(JPATH_INSTALLATION . '/' . $remoteDbFile)) { + // Add the general message + Factory::getApplication()->enqueueMessage($generalRemoteDatabaseMessage, 'warning'); + + // Request to create the file manually + Factory::getApplication()->enqueueMessage( + Text::sprintf( + 'INSTL_DATABASE_HOST_IS_NOT_LOCALHOST_CREATE_FILE', + $remoteDbFile, + 'installation', + Text::_('INSTL_INSTALL_JOOMLA') + ), + 'notice' + ); + + return false; + } + + // All tests for this session passed set it to the session + Factory::getSession()->set('remoteDbFileTestsPassed', true); + } + } + + return true; + } + + /** + * Check database server parameters after connection + * + * @param DatabaseDriver $db Database object + * @param \stdClass $options The session options + * + * @return string|boolean A string with the translated error message if + * some server parameter is not ok, otherwise false. + * + * @since 4.0.0 + */ + public static function checkDbServerParameters($db, $options) + { + $dbVersion = $db->getVersion(); + + // Get required database version + $minDbVersionRequired = self::getMinimumServerVersion($db, $options); + + // Check minimum database version + if (version_compare($dbVersion, $minDbVersionRequired) < 0) { + if (in_array($options->db_type, ['mysql', 'mysqli']) && $db->isMariaDb()) { + $errorMessage = Text::sprintf( + 'INSTL_DATABASE_INVALID_MARIADB_VERSION', + $minDbVersionRequired, + $dbVersion + ); + } else { + $errorMessage = Text::sprintf( + 'INSTL_DATABASE_INVALID_' . strtoupper($options->db_type) . '_VERSION', + $minDbVersionRequired, + $dbVersion + ); + } + + return $errorMessage; + } + + // Check database connection encryption + if ($options->db_encryption !== 0 && empty($db->getConnectionEncryption())) { + if ($db->isConnectionEncryptionSupported()) { + $errorMessage = Text::_('INSTL_DATABASE_ENCRYPTION_MSG_CONN_NOT_ENCRYPT'); + } else { + $errorMessage = Text::_('INSTL_DATABASE_ENCRYPTION_MSG_SRV_NOT_SUPPORTS'); + } + + return $errorMessage; + } + + return false; + } } diff --git a/installation/src/Model/BaseInstallationModel.php b/installation/src/Model/BaseInstallationModel.php index 1a1c7a756b372..3c0b4a4092de8 100644 --- a/installation/src/Model/BaseInstallationModel.php +++ b/installation/src/Model/BaseInstallationModel.php @@ -1,4 +1,5 @@ label = Text::_('INSTL_ZLIB_COMPRESSION_SUPPORT'); - $option->state = extension_loaded('zlib'); - $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_ZLIB_COMPRESSION_SUPPORT'); - $options[] = $option; - - // Check for XML support. - $option = new \stdClass; - $option->label = Text::_('INSTL_XML_SUPPORT'); - $option->state = extension_loaded('xml'); - $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_XML_SUPPORT'); - $options[] = $option; - - // Check for database support. - // We are satisfied if there is at least one database driver available. - $available = DatabaseDriver::getConnectors(); - $option = new \stdClass; - $option->label = Text::_('INSTL_DATABASE_SUPPORT'); - $option->label .= '
    (' . implode(', ', $available) . ')'; - $option->state = count($available); - $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_DATABASE_SUPPORT'); - $options[] = $option; - - // Check for mbstring options. - if (extension_loaded('mbstring')) - { - // Check for default MB language. - $option = new \stdClass; - $option->label = Text::_('INSTL_MB_LANGUAGE_IS_DEFAULT'); - $option->state = (strtolower(ini_get('mbstring.language')) == 'neutral'); - $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_MBLANG_NOTDEFAULT'); - $options[] = $option; - - // Check for MB function overload. - $option = new \stdClass; - $option->label = Text::_('INSTL_MB_STRING_OVERLOAD_OFF'); - $option->state = (ini_get('mbstring.func_overload') == 0); - $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_MBSTRING_OVERLOAD_OFF'); - $options[] = $option; - } - - // Check for a missing native parse_ini_file implementation. - $option = new \stdClass; - $option->label = Text::_('INSTL_PARSE_INI_FILE_AVAILABLE'); - $option->state = $this->getIniParserAvailability(); - $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_PARSE_INI_FILE_AVAILABLE'); - $options[] = $option; - - // Check for missing native json_encode / json_decode support. - $option = new \stdClass; - $option->label = Text::_('INSTL_JSON_SUPPORT_AVAILABLE'); - $option->state = function_exists('json_encode') && function_exists('json_decode'); - $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_JSON_SUPPORT_AVAILABLE'); - $options[] = $option; - - // Check for configuration file writable. - $writable = (is_writable(JPATH_CONFIGURATION . '/configuration.php') - || (!file_exists(JPATH_CONFIGURATION . '/configuration.php') && is_writable(JPATH_ROOT))); - - $option = new \stdClass; - $option->label = Text::sprintf('INSTL_WRITABLE', 'configuration.php'); - $option->state = $writable; - $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_NEEDSTOBEWRITABLE'); - $options[] = $option; - - return $options; - } - - /** - * Checks if all of the mandatory PHP options are met. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function getPhpOptionsSufficient() - { - $options = $this->getPhpOptions(); - - foreach ($options as $option) - { - if ($option->state === false) - { - $result = $option->state; - } - } - - return isset($result) ? false : true; - } - - /** - * Gets PHP Settings. - * - * @return array - * - * @since 3.1 - */ - public function getPhpSettings() - { - $settings = array(); - - // Check for display errors. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_DISPLAY_ERRORS'); - $setting->state = (bool) ini_get('display_errors'); - $setting->recommended = false; - $settings[] = $setting; - - // Check for file uploads. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_FILE_UPLOADS'); - $setting->state = (bool) ini_get('file_uploads'); - $setting->recommended = true; - $settings[] = $setting; - - // Check for output buffering. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_OUTPUT_BUFFERING'); - $setting->state = (int) ini_get('output_buffering') !== 0; - $setting->recommended = false; - $settings[] = $setting; - - // Check for session auto-start. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_SESSION_AUTO_START'); - $setting->state = (bool) ini_get('session.auto_start'); - $setting->recommended = false; - $settings[] = $setting; - - // Check for native ZIP support. - $setting = new \stdClass; - $setting->label = Text::_('INSTL_ZIP_SUPPORT_AVAILABLE'); - $setting->state = function_exists('zip_open') && function_exists('zip_read'); - $setting->recommended = true; - $settings[] = $setting; - - // Check for GD support - $setting = new \stdClass; - $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'GD'); - $setting->state = extension_loaded('gd'); - $setting->recommended = true; - $settings[] = $setting; - - // Check for iconv support - $setting = new \stdClass; - $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'iconv'); - $setting->state = function_exists('iconv'); - $setting->recommended = true; - $settings[] = $setting; - - // Check for intl support - $setting = new \stdClass; - $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'intl'); - $setting->state = function_exists('transliterator_transliterate'); - $setting->recommended = true; - $settings[] = $setting; - - return $settings; - } - - /** - * Get the current setup options from the session. - * - * @return array An array of options from the session. - * - * @since 3.1 - */ - public function getOptions() - { - if (!empty(Factory::getSession()->get('setup.options', array()))) - { - return Factory::getSession()->get('setup.options', array()); - } - } - - /** - * Method to get the form. - * - * @param string|null $view The view being processed. - * - * @return Form|boolean Form object on success, false on failure. - * - * @since 3.1 - */ - public function getForm($view = null) - { - if (!$view) - { - $view = Factory::getApplication()->input->getWord('view', 'setup'); - } - - // Get the form. - Form::addFormPath(JPATH_COMPONENT . '/forms'); - - try - { - $form = Form::getInstance('jform', $view, array('control' => 'jform')); - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - // Check the session for previously entered form data. - $data = (array) $this->getOptions(); - - // Bind the form data if present. - if (!empty($data)) - { - $form->bind($data); - } - - return $form; - } + /** + * Checks the availability of the parse_ini_file and parse_ini_string functions. + * + * @return boolean True if the method exists. + * + * @since 3.1 + */ + public function getIniParserAvailability() + { + $disabled_functions = ini_get('disable_functions'); + + if (!empty($disabled_functions)) { + // Attempt to detect them in the PHP INI disable_functions variable. + $disabled_functions = explode(',', trim($disabled_functions)); + $number_of_disabled_functions = count($disabled_functions); + + for ($i = 0, $l = $number_of_disabled_functions; $i < $l; $i++) { + $disabled_functions[$i] = trim($disabled_functions[$i]); + } + + $result = !in_array('parse_ini_string', $disabled_functions); + } else { + // Attempt to detect their existence; even pure PHP implementation of them will trigger a positive response, though. + $result = function_exists('parse_ini_string'); + } + + return $result; + } + + /** + * Gets PHP options. + * + * @return array Array of PHP config options + * + * @since 3.1 + */ + public function getPhpOptions() + { + $options = []; + + // Check for zlib support. + $option = new \stdClass(); + $option->label = Text::_('INSTL_ZLIB_COMPRESSION_SUPPORT'); + $option->state = extension_loaded('zlib'); + $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_ZLIB_COMPRESSION_SUPPORT'); + $options[] = $option; + + // Check for XML support. + $option = new \stdClass(); + $option->label = Text::_('INSTL_XML_SUPPORT'); + $option->state = extension_loaded('xml'); + $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_XML_SUPPORT'); + $options[] = $option; + + // Check for database support. + // We are satisfied if there is at least one database driver available. + $available = DatabaseDriver::getConnectors(); + $option = new \stdClass(); + $option->label = Text::_('INSTL_DATABASE_SUPPORT'); + $option->label .= '
    (' . implode(', ', $available) . ')'; + $option->state = count($available); + $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_DATABASE_SUPPORT'); + $options[] = $option; + + // Check for mbstring options. + if (extension_loaded('mbstring')) { + // Check for default MB language. + $option = new \stdClass(); + $option->label = Text::_('INSTL_MB_LANGUAGE_IS_DEFAULT'); + $option->state = (strtolower(ini_get('mbstring.language')) == 'neutral'); + $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_MBLANG_NOTDEFAULT'); + $options[] = $option; + + // Check for MB function overload. + $option = new \stdClass(); + $option->label = Text::_('INSTL_MB_STRING_OVERLOAD_OFF'); + $option->state = (ini_get('mbstring.func_overload') == 0); + $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_MBSTRING_OVERLOAD_OFF'); + $options[] = $option; + } + + // Check for a missing native parse_ini_file implementation. + $option = new \stdClass(); + $option->label = Text::_('INSTL_PARSE_INI_FILE_AVAILABLE'); + $option->state = $this->getIniParserAvailability(); + $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_PARSE_INI_FILE_AVAILABLE'); + $options[] = $option; + + // Check for missing native json_encode / json_decode support. + $option = new \stdClass(); + $option->label = Text::_('INSTL_JSON_SUPPORT_AVAILABLE'); + $option->state = function_exists('json_encode') && function_exists('json_decode'); + $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_JSON_SUPPORT_AVAILABLE'); + $options[] = $option; + + // Check for configuration file writable. + $writable = (is_writable(JPATH_CONFIGURATION . '/configuration.php') + || (!file_exists(JPATH_CONFIGURATION . '/configuration.php') && is_writable(JPATH_ROOT))); + + $option = new \stdClass(); + $option->label = Text::sprintf('INSTL_WRITABLE', 'configuration.php'); + $option->state = $writable; + $option->notice = $option->state ? null : Text::_('INSTL_NOTICE_NEEDSTOBEWRITABLE'); + $options[] = $option; + + return $options; + } + + /** + * Checks if all of the mandatory PHP options are met. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function getPhpOptionsSufficient() + { + $options = $this->getPhpOptions(); + + foreach ($options as $option) { + if ($option->state === false) { + $result = $option->state; + } + } + + return isset($result) ? false : true; + } + + /** + * Gets PHP Settings. + * + * @return array + * + * @since 3.1 + */ + public function getPhpSettings() + { + $settings = array(); + + // Check for display errors. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_DISPLAY_ERRORS'); + $setting->state = (bool) ini_get('display_errors'); + $setting->recommended = false; + $settings[] = $setting; + + // Check for file uploads. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_FILE_UPLOADS'); + $setting->state = (bool) ini_get('file_uploads'); + $setting->recommended = true; + $settings[] = $setting; + + // Check for output buffering. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_OUTPUT_BUFFERING'); + $setting->state = (int) ini_get('output_buffering') !== 0; + $setting->recommended = false; + $settings[] = $setting; + + // Check for session auto-start. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_SESSION_AUTO_START'); + $setting->state = (bool) ini_get('session.auto_start'); + $setting->recommended = false; + $settings[] = $setting; + + // Check for native ZIP support. + $setting = new \stdClass(); + $setting->label = Text::_('INSTL_ZIP_SUPPORT_AVAILABLE'); + $setting->state = function_exists('zip_open') && function_exists('zip_read'); + $setting->recommended = true; + $settings[] = $setting; + + // Check for GD support + $setting = new \stdClass(); + $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'GD'); + $setting->state = extension_loaded('gd'); + $setting->recommended = true; + $settings[] = $setting; + + // Check for iconv support + $setting = new \stdClass(); + $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'iconv'); + $setting->state = function_exists('iconv'); + $setting->recommended = true; + $settings[] = $setting; + + // Check for intl support + $setting = new \stdClass(); + $setting->label = Text::sprintf('INSTL_EXTENSION_AVAILABLE', 'intl'); + $setting->state = function_exists('transliterator_transliterate'); + $setting->recommended = true; + $settings[] = $setting; + + return $settings; + } + + /** + * Get the current setup options from the session. + * + * @return array An array of options from the session. + * + * @since 3.1 + */ + public function getOptions() + { + if (!empty(Factory::getSession()->get('setup.options', array()))) { + return Factory::getSession()->get('setup.options', array()); + } + } + + /** + * Method to get the form. + * + * @param string|null $view The view being processed. + * + * @return Form|boolean Form object on success, false on failure. + * + * @since 3.1 + */ + public function getForm($view = null) + { + if (!$view) { + $view = Factory::getApplication()->input->getWord('view', 'setup'); + } + + // Get the form. + Form::addFormPath(JPATH_COMPONENT . '/forms'); + + try { + $form = Form::getInstance('jform', $view, array('control' => 'jform')); + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + // Check the session for previously entered form data. + $data = (array) $this->getOptions(); + + // Bind the form data if present. + if (!empty($data)) { + $form->bind($data); + } + + return $form; + } } diff --git a/installation/src/Model/CleanupModel.php b/installation/src/Model/CleanupModel.php index 6065faf799924..7bbd64e49514f 100644 --- a/installation/src/Model/CleanupModel.php +++ b/installation/src/Model/CleanupModel.php @@ -1,4 +1,5 @@ db_type, - $options->db_host, - $options->db_user, - $options->db_pass_plain, - $options->db_name, - $options->db_prefix, - true, - DatabaseHelper::getEncryptionSettings($options) - ); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_ERROR_CONNECT_DB', $e->getMessage()), 'error'); - - return false; - } - - // Attempt to create the configuration. - if (!$this->createConfiguration($options)) - { - return false; - } - - $serverType = $db->getServerType(); - - // Attempt to update the table #__schema. - $pathPart = JPATH_ADMINISTRATOR . '/components/com_admin/sql/updates/' . $serverType . '/'; - - $files = Folder::files($pathPart, '\.sql$'); - - if (empty($files)) - { - Factory::getApplication()->enqueueMessage(Text::_('INSTL_ERROR_INITIALISE_SCHEMA'), 'error'); - - return false; - } - - $version = ''; - - foreach ($files as $file) - { - if (version_compare($version, File::stripExt($file)) < 0) - { - $version = File::stripExt($file); - } - } - - $query = $db->getQuery(true) - ->select('extension_id') - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('name') . ' = ' . $db->quote('files_joomla')); - $db->setQuery($query); - $eid = $db->loadResult(); - - $query->clear() - ->insert($db->quoteName('#__schemas')) - ->columns( - array( - $db->quoteName('extension_id'), - $db->quoteName('version_id') - ) - ) - ->values($eid . ', ' . $db->quote($version)); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - // Attempt to refresh manifest caches. - $query->clear() - ->select('*') - ->from('#__extensions'); - $db->setQuery($query); - - $return = true; - - try - { - $extensions = $db->loadObjectList(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - $return = false; - } - - // This is needed because the installer loads the extension table in constructor, needs to be refactored in 5.0 - Factory::$database = $db; - $installer = Installer::getInstance(); - - foreach ($extensions as $extension) - { - if (!$installer->refreshManifestCache($extension->extension_id)) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf('INSTL_DATABASE_COULD_NOT_REFRESH_MANIFEST_CACHE', $extension->name), - 'error' - ); - - return false; - } - } - - // Handle default backend language setting. This feature is available for localized versions of Joomla. - $languages = Factory::getApplication()->getLocaliseAdmin($db); - - if (in_array($options->language, $languages['admin']) || in_array($options->language, $languages['site'])) - { - // Build the language parameters for the language manager. - $params = array(); - - // Set default administrator/site language to sample data values. - $params['administrator'] = 'en-GB'; - $params['site'] = 'en-GB'; - - if (in_array($options->language, $languages['admin'])) - { - $params['administrator'] = $options->language; - } - - if (in_array($options->language, $languages['site'])) - { - $params['site'] = $options->language; - } - - $params = json_encode($params); - - // Update the language settings in the language manager. - $query->clear() - ->update($db->quoteName('#__extensions')) - ->set($db->quoteName('params') . ' = ' . $db->quote($params)) - ->where($db->quoteName('element') . ' = ' . $db->quote('com_languages')); - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - $return = false; - } - } - - // Attempt to create the root user. - if (!$this->createRootUser($options, $db)) - { - $this->deleteConfiguration(); - - return false; - } - - // Update the cms data user ids. - $this->updateUserIds($db); - - // Check for testing sampledata plugin. - $this->checkTestingSampledata($db); - - return $return; - } - - /** - * Retrieves the default user ID and sets it if necessary. - * - * @return integer The user ID. - * - * @since 3.1 - */ - public static function getUserId() - { - if (!self::$userId) - { - self::$userId = self::generateRandUserId(); - } - - return self::$userId; - } - - /** - * Generates the user ID. - * - * @return integer The user ID. - * - * @since 3.1 - */ - protected static function generateRandUserId() - { - $session = Factory::getSession(); - $randUserId = $session->get('randUserId'); - - if (empty($randUserId)) - { - // Create the ID for the root user only once and store in session. - $randUserId = mt_rand(1, 1000); - $session->set('randUserId', $randUserId); - } - - return $randUserId; - } - - /** - * Resets the user ID. - * - * @return void - * - * @since 3.1 - */ - public static function resetRandUserId() - { - self::$userId = 0; - - Factory::getSession()->set('randUserId', self::$userId); - } - - /** - * Method to update the user id of sql data content to the new rand user id. - * - * @param DatabaseDriver $db Database connector object $db*. - * - * @return void - * - * @since 3.6.1 - */ - protected function updateUserIds($db) - { - // Create the ID for the root user. - $userId = self::getUserId(); - - // Update all core tables created_by fields of the tables with the random user id. - $updatesArray = array( - '#__banners' => array('created_by', 'modified_by'), - '#__categories' => array('created_user_id', 'modified_user_id'), - '#__contact_details' => array('created_by', 'modified_by'), - '#__content' => array('created_by', 'modified_by'), - '#__fields' => array('created_user_id', 'modified_by'), - '#__finder_filters' => array('created_by', 'modified_by'), - '#__newsfeeds' => array('created_by', 'modified_by'), - '#__tags' => array('created_user_id', 'modified_user_id'), - '#__ucm_content' => array('core_created_user_id', 'core_modified_user_id'), - '#__history' => array('editor_user_id'), - '#__user_notes' => array('created_user_id', 'modified_user_id'), - '#__workflows' => array('created_by', 'modified_by'), - ); - - foreach ($updatesArray as $table => $fields) - { - foreach ($fields as $field) - { - $query = $db->getQuery(true) - ->update($db->quoteName($table)) - ->set($db->quoteName($field) . ' = ' . $db->quote($userId)) - ->where($db->quoteName($field) . ' != 0') - ->where($db->quoteName($field) . ' IS NOT NULL'); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - } - } - } - } - - /** - * Method to check for the testing sampledata plugin. - * - * @param DatabaseDriver $db Database connector object $db*. - * - * @return void - * - * @since 4.0.0 - */ - public function checkTestingSampledata($db) - { - $version = new Version; - - if (!$version->isInDevelopmentState() || !is_file(JPATH_PLUGINS . '/sampledata/testing/testing.php')) - { - return; - } - - $testingPlugin = new \stdClass; - $testingPlugin->extension_id = null; - $testingPlugin->name = 'plg_sampledata_testing'; - $testingPlugin->type = 'plugin'; - $testingPlugin->element = 'testing'; - $testingPlugin->folder = 'sampledata'; - $testingPlugin->client_id = 0; - $testingPlugin->enabled = 1; - $testingPlugin->access = 1; - $testingPlugin->manifest_cache = ''; - $testingPlugin->params = '{}'; - $testingPlugin->custom_data = ''; - - $db->insertObject('#__extensions', $testingPlugin, 'extension_id'); - - $installer = new Installer; - $installer->setDatabase($db); - - if (!$installer->refreshManifestCache($testingPlugin->extension_id)) - { - Factory::getApplication()->enqueueMessage( - Text::sprintf('INSTL_DATABASE_COULD_NOT_REFRESH_MANIFEST_CACHE', $testingPlugin->name), - 'error' - ); - } - } - - /** - * Method to create the configuration file - * - * @param \stdClass $options The session options - * - * @return boolean True on success - * - * @since 3.1 - */ - public function createConfiguration($options) - { - // Create a new registry to build the configuration options. - $registry = new Registry; - - // Site settings. - $registry->set('offline', false); - $registry->set('offline_message', Text::_('INSTL_STD_OFFLINE_MSG')); - $registry->set('display_offline_message', 1); - $registry->set('offline_image', ''); - $registry->set('sitename', $options->site_name); - $registry->set('editor', 'tinymce'); - $registry->set('captcha', '0'); - $registry->set('list_limit', 20); - $registry->set('access', 1); - - // Debug settings. - $registry->set('debug', false); - $registry->set('debug_lang', false); - $registry->set('debug_lang_const', true); - - // Database settings. - $registry->set('dbtype', $options->db_type); - $registry->set('host', $options->db_host); - $registry->set('user', $options->db_user); - $registry->set('password', $options->db_pass_plain); - $registry->set('db', $options->db_name); - $registry->set('dbprefix', $options->db_prefix); - $registry->set('dbencryption', $options->db_encryption); - $registry->set('dbsslverifyservercert', $options->db_sslverifyservercert); - $registry->set('dbsslkey', $options->db_sslkey); - $registry->set('dbsslcert', $options->db_sslcert); - $registry->set('dbsslca', $options->db_sslca); - $registry->set('dbsslcipher', $options->db_sslcipher); - - // Server settings. - $registry->set('force_ssl', 0); - $registry->set('live_site', ''); - $registry->set('secret', UserHelper::genRandomPassword(16)); - $registry->set('gzip', false); - $registry->set('error_reporting', 'default'); - $registry->set('helpurl', $options->helpurl); - - // Locale settings. - $registry->set('offset', 'UTC'); - - // Mail settings. - $registry->set('mailonline', true); - $registry->set('mailer', 'mail'); - $registry->set('mailfrom', $options->admin_email); - $registry->set('fromname', $options->site_name); - $registry->set('sendmail', '/usr/sbin/sendmail'); - $registry->set('smtpauth', false); - $registry->set('smtpuser', ''); - $registry->set('smtppass', ''); - $registry->set('smtphost', 'localhost'); - $registry->set('smtpsecure', 'none'); - $registry->set('smtpport', 25); - - // Cache settings. - $registry->set('caching', 0); - $registry->set('cache_handler', 'file'); - $registry->set('cachetime', 15); - $registry->set('cache_platformprefix', false); - - // Meta settings. - $registry->set('MetaDesc', ''); - $registry->set('MetaAuthor', true); - $registry->set('MetaVersion', false); - $registry->set('robots', ''); - - // SEO settings. - $registry->set('sef', true); - $registry->set('sef_rewrite', false); - $registry->set('sef_suffix', false); - $registry->set('unicodeslugs', false); - - // Feed settings. - $registry->set('feed_limit', 10); - $registry->set('feed_email', 'none'); - - $registry->set('log_path', JPATH_ADMINISTRATOR . '/logs'); - $registry->set('tmp_path', JPATH_ROOT . '/tmp'); - - // Session setting. - $registry->set('lifetime', 15); - $registry->set('session_handler', 'database'); - $registry->set('shared_session', false); - $registry->set('session_metadata', true); - - // Generate the configuration class string buffer. - $buffer = $registry->toString('PHP', array('class' => 'JConfig', 'closingtag' => false)); - - // Build the configuration file path. - $path = JPATH_CONFIGURATION . '/configuration.php'; - - // Determine if the configuration file path is writable. - if (file_exists($path)) - { - $canWrite = is_writable($path); - } - else - { - $canWrite = is_writable(JPATH_CONFIGURATION . '/'); - } - - /* - * If the file exists but isn't writable OR if the file doesn't exist and the parent directory - * is not writable the user needs to fix this. - */ - if ((file_exists($path) && !is_writable($path)) || (!file_exists($path) && !is_writable(dirname($path) . '/'))) - { - return false; - } - - // Get the session - $session = Factory::getSession(); - - if ($canWrite) - { - file_put_contents($path, $buffer); - $session->set('setup.config', null); - } - else - { - // If we cannot write the configuration.php, setup fails! - return false; - } - - return true; - } - - /** - * Method to create the root user for the site. - * - * @param object $options The session options. - * @param DatabaseDriver $db Database connector object $db*. - * - * @return boolean True on success. - * - * @since 3.1 - */ - private function createRootUser($options, $db) - { - $cryptpass = UserHelper::hashPassword($options->admin_password_plain); - - // Take the admin user id - we'll need to leave this in the session for sample data install later on. - $userId = self::getUserId(); - - // Create the admin user. - date_default_timezone_set('UTC'); - $installdate = date('Y-m-d H:i:s'); - - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('id') . ' = ' . $db->quote($userId)); - - $db->setQuery($query); - - try - { - $result = $db->loadResult(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - if ($result) - { - $query->clear() - ->update($db->quoteName('#__users')) - ->set($db->quoteName('name') . ' = ' . $db->quote(trim($options->admin_user))) - ->set($db->quoteName('username') . ' = ' . $db->quote(trim($options->admin_username))) - ->set($db->quoteName('email') . ' = ' . $db->quote($options->admin_email)) - ->set($db->quoteName('password') . ' = ' . $db->quote($cryptpass)) - ->set($db->quoteName('block') . ' = 0') - ->set($db->quoteName('sendEmail') . ' = 1') - ->set($db->quoteName('registerDate') . ' = ' . $db->quote($installdate)) - ->set($db->quoteName('lastvisitDate') . ' = NULL') - ->set($db->quoteName('activation') . ' = ' . $db->quote('0')) - ->set($db->quoteName('params') . ' = ' . $db->quote('')) - ->where($db->quoteName('id') . ' = ' . $db->quote($userId)); - } - else - { - $columns = array( - $db->quoteName('id'), - $db->quoteName('name'), - $db->quoteName('username'), - $db->quoteName('email'), - $db->quoteName('password'), - $db->quoteName('block'), - $db->quoteName('sendEmail'), - $db->quoteName('registerDate'), - $db->quoteName('lastvisitDate'), - $db->quoteName('activation'), - $db->quoteName('params') - ); - $query->clear() - ->insert('#__users', true) - ->columns($columns) - ->values( - $db->quote($userId) . ', ' . $db->quote(trim($options->admin_user)) . ', ' . $db->quote(trim($options->admin_username)) . ', ' . - $db->quote($options->admin_email) . ', ' . $db->quote($cryptpass) . ', ' . - $db->quote('0') . ', ' . $db->quote('1') . ', ' . $db->quote($installdate) . ', NULL, ' . - $db->quote('0') . ', ' . $db->quote('') - ); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - // Map the super user to the Super Users group - $query->clear() - ->select($db->quoteName('user_id')) - ->from($db->quoteName('#__user_usergroup_map')) - ->where($db->quoteName('user_id') . ' = ' . $db->quote($userId)); - - $db->setQuery($query); - - if ($db->loadResult()) - { - $query->clear() - ->update($db->quoteName('#__user_usergroup_map')) - ->set($db->quoteName('user_id') . ' = ' . $db->quote($userId)) - ->set($db->quoteName('group_id') . ' = 8'); - } - else - { - $query->clear() - ->insert($db->quoteName('#__user_usergroup_map'), false) - ->columns(array($db->quoteName('user_id'), $db->quoteName('group_id'))) - ->values($db->quote($userId) . ', 8'); - } - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - return true; - } - - /** - * Method to erase the configuration file. - * - * @return void - * - * @since 4.0.0 - */ - private function deleteConfiguration() - { - // The configuration file path. - $path = JPATH_CONFIGURATION . '/configuration.php'; - - if (file_exists($path)) - { - File::delete($path); - } - } + /** + * The generated user ID. + * + * @var integer + * @since 4.0.0 + */ + protected static $userId = 0; + + /** + * Method to setup the configuration file + * + * @param array $options The session options + * + * @return boolean True on success + * + * @since 3.1 + */ + public function setup($options) + { + // Get the options as an object for easier handling. + $options = ArrayHelper::toObject($options); + + // Get a database object. + try { + $db = DatabaseHelper::getDbo( + $options->db_type, + $options->db_host, + $options->db_user, + $options->db_pass_plain, + $options->db_name, + $options->db_prefix, + true, + DatabaseHelper::getEncryptionSettings($options) + ); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_ERROR_CONNECT_DB', $e->getMessage()), 'error'); + + return false; + } + + // Attempt to create the configuration. + if (!$this->createConfiguration($options)) { + return false; + } + + $serverType = $db->getServerType(); + + // Attempt to update the table #__schema. + $pathPart = JPATH_ADMINISTRATOR . '/components/com_admin/sql/updates/' . $serverType . '/'; + + $files = Folder::files($pathPart, '\.sql$'); + + if (empty($files)) { + Factory::getApplication()->enqueueMessage(Text::_('INSTL_ERROR_INITIALISE_SCHEMA'), 'error'); + + return false; + } + + $version = ''; + + foreach ($files as $file) { + if (version_compare($version, File::stripExt($file)) < 0) { + $version = File::stripExt($file); + } + } + + $query = $db->getQuery(true) + ->select('extension_id') + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('name') . ' = ' . $db->quote('files_joomla')); + $db->setQuery($query); + $eid = $db->loadResult(); + + $query->clear() + ->insert($db->quoteName('#__schemas')) + ->columns( + array( + $db->quoteName('extension_id'), + $db->quoteName('version_id') + ) + ) + ->values($eid . ', ' . $db->quote($version)); + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + // Attempt to refresh manifest caches. + $query->clear() + ->select('*') + ->from('#__extensions'); + $db->setQuery($query); + + $return = true; + + try { + $extensions = $db->loadObjectList(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + $return = false; + } + + // This is needed because the installer loads the extension table in constructor, needs to be refactored in 5.0 + Factory::$database = $db; + $installer = Installer::getInstance(); + + foreach ($extensions as $extension) { + if (!$installer->refreshManifestCache($extension->extension_id)) { + Factory::getApplication()->enqueueMessage( + Text::sprintf('INSTL_DATABASE_COULD_NOT_REFRESH_MANIFEST_CACHE', $extension->name), + 'error' + ); + + return false; + } + } + + // Handle default backend language setting. This feature is available for localized versions of Joomla. + $languages = Factory::getApplication()->getLocaliseAdmin($db); + + if (in_array($options->language, $languages['admin']) || in_array($options->language, $languages['site'])) { + // Build the language parameters for the language manager. + $params = array(); + + // Set default administrator/site language to sample data values. + $params['administrator'] = 'en-GB'; + $params['site'] = 'en-GB'; + + if (in_array($options->language, $languages['admin'])) { + $params['administrator'] = $options->language; + } + + if (in_array($options->language, $languages['site'])) { + $params['site'] = $options->language; + } + + $params = json_encode($params); + + // Update the language settings in the language manager. + $query->clear() + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('params') . ' = ' . $db->quote($params)) + ->where($db->quoteName('element') . ' = ' . $db->quote('com_languages')); + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + $return = false; + } + } + + // Attempt to create the root user. + if (!$this->createRootUser($options, $db)) { + $this->deleteConfiguration(); + + return false; + } + + // Update the cms data user ids. + $this->updateUserIds($db); + + // Check for testing sampledata plugin. + $this->checkTestingSampledata($db); + + return $return; + } + + /** + * Retrieves the default user ID and sets it if necessary. + * + * @return integer The user ID. + * + * @since 3.1 + */ + public static function getUserId() + { + if (!self::$userId) { + self::$userId = self::generateRandUserId(); + } + + return self::$userId; + } + + /** + * Generates the user ID. + * + * @return integer The user ID. + * + * @since 3.1 + */ + protected static function generateRandUserId() + { + $session = Factory::getSession(); + $randUserId = $session->get('randUserId'); + + if (empty($randUserId)) { + // Create the ID for the root user only once and store in session. + $randUserId = mt_rand(1, 1000); + $session->set('randUserId', $randUserId); + } + + return $randUserId; + } + + /** + * Resets the user ID. + * + * @return void + * + * @since 3.1 + */ + public static function resetRandUserId() + { + self::$userId = 0; + + Factory::getSession()->set('randUserId', self::$userId); + } + + /** + * Method to update the user id of sql data content to the new rand user id. + * + * @param DatabaseDriver $db Database connector object $db*. + * + * @return void + * + * @since 3.6.1 + */ + protected function updateUserIds($db) + { + // Create the ID for the root user. + $userId = self::getUserId(); + + // Update all core tables created_by fields of the tables with the random user id. + $updatesArray = array( + '#__banners' => array('created_by', 'modified_by'), + '#__categories' => array('created_user_id', 'modified_user_id'), + '#__contact_details' => array('created_by', 'modified_by'), + '#__content' => array('created_by', 'modified_by'), + '#__fields' => array('created_user_id', 'modified_by'), + '#__finder_filters' => array('created_by', 'modified_by'), + '#__newsfeeds' => array('created_by', 'modified_by'), + '#__tags' => array('created_user_id', 'modified_user_id'), + '#__ucm_content' => array('core_created_user_id', 'core_modified_user_id'), + '#__history' => array('editor_user_id'), + '#__user_notes' => array('created_user_id', 'modified_user_id'), + '#__workflows' => array('created_by', 'modified_by'), + ); + + foreach ($updatesArray as $table => $fields) { + foreach ($fields as $field) { + $query = $db->getQuery(true) + ->update($db->quoteName($table)) + ->set($db->quoteName($field) . ' = ' . $db->quote($userId)) + ->where($db->quoteName($field) . ' != 0') + ->where($db->quoteName($field) . ' IS NOT NULL'); + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + } + } + } + } + + /** + * Method to check for the testing sampledata plugin. + * + * @param DatabaseDriver $db Database connector object $db*. + * + * @return void + * + * @since 4.0.0 + */ + public function checkTestingSampledata($db) + { + $version = new Version(); + + if (!$version->isInDevelopmentState() || !is_file(JPATH_PLUGINS . '/sampledata/testing/testing.php')) { + return; + } + + $testingPlugin = new \stdClass(); + $testingPlugin->extension_id = null; + $testingPlugin->name = 'plg_sampledata_testing'; + $testingPlugin->type = 'plugin'; + $testingPlugin->element = 'testing'; + $testingPlugin->folder = 'sampledata'; + $testingPlugin->client_id = 0; + $testingPlugin->enabled = 1; + $testingPlugin->access = 1; + $testingPlugin->manifest_cache = ''; + $testingPlugin->params = '{}'; + $testingPlugin->custom_data = ''; + + $db->insertObject('#__extensions', $testingPlugin, 'extension_id'); + + $installer = new Installer(); + $installer->setDatabase($db); + + if (!$installer->refreshManifestCache($testingPlugin->extension_id)) { + Factory::getApplication()->enqueueMessage( + Text::sprintf('INSTL_DATABASE_COULD_NOT_REFRESH_MANIFEST_CACHE', $testingPlugin->name), + 'error' + ); + } + } + + /** + * Method to create the configuration file + * + * @param \stdClass $options The session options + * + * @return boolean True on success + * + * @since 3.1 + */ + public function createConfiguration($options) + { + // Create a new registry to build the configuration options. + $registry = new Registry(); + + // Site settings. + $registry->set('offline', false); + $registry->set('offline_message', Text::_('INSTL_STD_OFFLINE_MSG')); + $registry->set('display_offline_message', 1); + $registry->set('offline_image', ''); + $registry->set('sitename', $options->site_name); + $registry->set('editor', 'tinymce'); + $registry->set('captcha', '0'); + $registry->set('list_limit', 20); + $registry->set('access', 1); + + // Debug settings. + $registry->set('debug', false); + $registry->set('debug_lang', false); + $registry->set('debug_lang_const', true); + + // Database settings. + $registry->set('dbtype', $options->db_type); + $registry->set('host', $options->db_host); + $registry->set('user', $options->db_user); + $registry->set('password', $options->db_pass_plain); + $registry->set('db', $options->db_name); + $registry->set('dbprefix', $options->db_prefix); + $registry->set('dbencryption', $options->db_encryption); + $registry->set('dbsslverifyservercert', $options->db_sslverifyservercert); + $registry->set('dbsslkey', $options->db_sslkey); + $registry->set('dbsslcert', $options->db_sslcert); + $registry->set('dbsslca', $options->db_sslca); + $registry->set('dbsslcipher', $options->db_sslcipher); + + // Server settings. + $registry->set('force_ssl', 0); + $registry->set('live_site', ''); + $registry->set('secret', UserHelper::genRandomPassword(16)); + $registry->set('gzip', false); + $registry->set('error_reporting', 'default'); + $registry->set('helpurl', $options->helpurl); + + // Locale settings. + $registry->set('offset', 'UTC'); + + // Mail settings. + $registry->set('mailonline', true); + $registry->set('mailer', 'mail'); + $registry->set('mailfrom', $options->admin_email); + $registry->set('fromname', $options->site_name); + $registry->set('sendmail', '/usr/sbin/sendmail'); + $registry->set('smtpauth', false); + $registry->set('smtpuser', ''); + $registry->set('smtppass', ''); + $registry->set('smtphost', 'localhost'); + $registry->set('smtpsecure', 'none'); + $registry->set('smtpport', 25); + + // Cache settings. + $registry->set('caching', 0); + $registry->set('cache_handler', 'file'); + $registry->set('cachetime', 15); + $registry->set('cache_platformprefix', false); + + // Meta settings. + $registry->set('MetaDesc', ''); + $registry->set('MetaAuthor', true); + $registry->set('MetaVersion', false); + $registry->set('robots', ''); + + // SEO settings. + $registry->set('sef', true); + $registry->set('sef_rewrite', false); + $registry->set('sef_suffix', false); + $registry->set('unicodeslugs', false); + + // Feed settings. + $registry->set('feed_limit', 10); + $registry->set('feed_email', 'none'); + + $registry->set('log_path', JPATH_ADMINISTRATOR . '/logs'); + $registry->set('tmp_path', JPATH_ROOT . '/tmp'); + + // Session setting. + $registry->set('lifetime', 15); + $registry->set('session_handler', 'database'); + $registry->set('shared_session', false); + $registry->set('session_metadata', true); + + // Generate the configuration class string buffer. + $buffer = $registry->toString('PHP', array('class' => 'JConfig', 'closingtag' => false)); + + // Build the configuration file path. + $path = JPATH_CONFIGURATION . '/configuration.php'; + + // Determine if the configuration file path is writable. + if (file_exists($path)) { + $canWrite = is_writable($path); + } else { + $canWrite = is_writable(JPATH_CONFIGURATION . '/'); + } + + /* + * If the file exists but isn't writable OR if the file doesn't exist and the parent directory + * is not writable the user needs to fix this. + */ + if ((file_exists($path) && !is_writable($path)) || (!file_exists($path) && !is_writable(dirname($path) . '/'))) { + return false; + } + + // Get the session + $session = Factory::getSession(); + + if ($canWrite) { + file_put_contents($path, $buffer); + $session->set('setup.config', null); + } else { + // If we cannot write the configuration.php, setup fails! + return false; + } + + return true; + } + + /** + * Method to create the root user for the site. + * + * @param object $options The session options. + * @param DatabaseDriver $db Database connector object $db*. + * + * @return boolean True on success. + * + * @since 3.1 + */ + private function createRootUser($options, $db) + { + $cryptpass = UserHelper::hashPassword($options->admin_password_plain); + + // Take the admin user id - we'll need to leave this in the session for sample data install later on. + $userId = self::getUserId(); + + // Create the admin user. + date_default_timezone_set('UTC'); + $installdate = date('Y-m-d H:i:s'); + + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('id') . ' = ' . $db->quote($userId)); + + $db->setQuery($query); + + try { + $result = $db->loadResult(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + if ($result) { + $query->clear() + ->update($db->quoteName('#__users')) + ->set($db->quoteName('name') . ' = ' . $db->quote(trim($options->admin_user))) + ->set($db->quoteName('username') . ' = ' . $db->quote(trim($options->admin_username))) + ->set($db->quoteName('email') . ' = ' . $db->quote($options->admin_email)) + ->set($db->quoteName('password') . ' = ' . $db->quote($cryptpass)) + ->set($db->quoteName('block') . ' = 0') + ->set($db->quoteName('sendEmail') . ' = 1') + ->set($db->quoteName('registerDate') . ' = ' . $db->quote($installdate)) + ->set($db->quoteName('lastvisitDate') . ' = NULL') + ->set($db->quoteName('activation') . ' = ' . $db->quote('0')) + ->set($db->quoteName('params') . ' = ' . $db->quote('')) + ->where($db->quoteName('id') . ' = ' . $db->quote($userId)); + } else { + $columns = array( + $db->quoteName('id'), + $db->quoteName('name'), + $db->quoteName('username'), + $db->quoteName('email'), + $db->quoteName('password'), + $db->quoteName('block'), + $db->quoteName('sendEmail'), + $db->quoteName('registerDate'), + $db->quoteName('lastvisitDate'), + $db->quoteName('activation'), + $db->quoteName('params') + ); + $query->clear() + ->insert('#__users', true) + ->columns($columns) + ->values( + $db->quote($userId) . ', ' . $db->quote(trim($options->admin_user)) . ', ' . $db->quote(trim($options->admin_username)) . ', ' . + $db->quote($options->admin_email) . ', ' . $db->quote($cryptpass) . ', ' . + $db->quote('0') . ', ' . $db->quote('1') . ', ' . $db->quote($installdate) . ', NULL, ' . + $db->quote('0') . ', ' . $db->quote('') + ); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + // Map the super user to the Super Users group + $query->clear() + ->select($db->quoteName('user_id')) + ->from($db->quoteName('#__user_usergroup_map')) + ->where($db->quoteName('user_id') . ' = ' . $db->quote($userId)); + + $db->setQuery($query); + + if ($db->loadResult()) { + $query->clear() + ->update($db->quoteName('#__user_usergroup_map')) + ->set($db->quoteName('user_id') . ' = ' . $db->quote($userId)) + ->set($db->quoteName('group_id') . ' = 8'); + } else { + $query->clear() + ->insert($db->quoteName('#__user_usergroup_map'), false) + ->columns(array($db->quoteName('user_id'), $db->quoteName('group_id'))) + ->values($db->quote($userId) . ', 8'); + } + + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + return true; + } + + /** + * Method to erase the configuration file. + * + * @return void + * + * @since 4.0.0 + */ + private function deleteConfiguration() + { + // The configuration file path. + $path = JPATH_CONFIGURATION . '/configuration.php'; + + if (file_exists($path)) { + File::delete($path); + } + } } diff --git a/installation/src/Model/DatabaseModel.php b/installation/src/Model/DatabaseModel.php index 98e4827386b3d..7f6e87b21e836 100644 --- a/installation/src/Model/DatabaseModel.php +++ b/installation/src/Model/DatabaseModel.php @@ -1,4 +1,5 @@ get('setup.options', array()); - } - - /** - * Method to initialise the database. - * - * @param boolean $select Select the database when creating the connections. - * - * @return DatabaseInterface|boolean Database object on success, boolean false on failure - * - * @since 3.1 - */ - public function initialise($select = true) - { - $options = $this->getOptions(); - - // Get the options as an object for easier handling. - $options = ArrayHelper::toObject($options); - - // Load the backend language files so that the DB error messages work. - $lang = Factory::getLanguage(); - $currentLang = $lang->getTag(); - - // Load the selected language - if (LanguageHelper::exists($currentLang, JPATH_ADMINISTRATOR)) - { - $lang->load('joomla', JPATH_ADMINISTRATOR, $currentLang, true); - } - // Pre-load en-GB in case the chosen language files do not exist. - else - { - $lang->load('joomla', JPATH_ADMINISTRATOR, 'en-GB', true); - } - - // Validate and clean up connection parameters - $paramsCheck = DatabaseHelper::validateConnectionParameters($options); - - if ($paramsCheck) - { - Factory::getApplication()->enqueueMessage($paramsCheck, 'warning'); - - return false; - } - - // Security check for remote db hosts - if (!DatabaseHelper::checkRemoteDbHost($options)) - { - // Messages have been enqueued in the called function. - return false; - } - - // Get a database object. - try - { - return DatabaseHelper::getDbo( - $options->db_type, - $options->db_host, - $options->db_user, - $options->db_pass_plain, - $options->db_name, - $options->db_prefix, - $select, - DatabaseHelper::getEncryptionSettings($options) - ); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_DATABASE_COULD_NOT_CONNECT', $e->getMessage()), 'error'); - - return false; - } - } - - /** - * Method to create a new database. - * - * @return boolean - * - * @since 3.1 - * @throws \RuntimeException - */ - public function createDatabase() - { - $options = (object) $this->getOptions(); - - $db = $this->initialise(false); - - if ($db === false) - { - // Error messages are enqueued by the initialise function, we just need to tell the controller how to redirect - return false; - } - - // Check database version. - $type = $options->db_type; - - try - { - $db_version = $db->getVersion(); - } - catch (\RuntimeException $e) - { - /* - * We may get here if the database doesn't exist, if so then explain that to users instead of showing the database connector's error - * This only supports PDO PostgreSQL and the PDO MySQL drivers presently - * - * Error Messages: - * PDO MySQL: [1049] Unknown database 'database_name' - * PDO PostgreSQL: database "database_name" does not exist - */ - if ($type === 'mysql' && strpos($e->getMessage(), '[1049] Unknown database') === 42 - || $type === 'pgsql' && strpos($e->getMessage(), 'database "' . $options->db_name . '" does not exist')) - { - /* - * Now we're really getting insane here; we're going to try building a new JDatabaseDriver instance - * in order to trick the connection into creating the database - */ - if ($type === 'mysql') - { - // MySQL (PDO): Don't specify database name - $altDBoptions = array( - 'driver' => $options->db_type, - 'host' => $options->db_host, - 'user' => $options->db_user, - 'password' => $options->db_pass_plain, - 'prefix' => $options->db_prefix, - 'select' => false, - DatabaseHelper::getEncryptionSettings($options), - ); - } - else - { - // PostgreSQL (PDO): Use 'postgres' - $altDBoptions = array( - 'driver' => $options->db_type, - 'host' => $options->db_host, - 'user' => $options->db_user, - 'password' => $options->db_pass_plain, - 'database' => 'postgres', - 'prefix' => $options->db_prefix, - 'select' => false, - DatabaseHelper::getEncryptionSettings($options), - ); - } - - $altDB = DatabaseDriver::getInstance($altDBoptions); - - // Check database server parameters - $dbServerCheck = DatabaseHelper::checkDbServerParameters($altDB, $options); - - if ($dbServerCheck) - { - // Some server parameter is not ok - throw new \RuntimeException($dbServerCheck, 500, $e); - } - - // Try to create the database now using the alternate driver - try - { - $this->createDb($altDB, $options, $altDB->hasUtfSupport()); - } - catch (\RuntimeException $e) - { - // We did everything we could - throw new \RuntimeException(Text::_('INSTL_DATABASE_COULD_NOT_CREATE_DATABASE'), 500, $e); - } - - // If we got here, the database should have been successfully created, now try one more time to get the version - try - { - $db_version = $db->getVersion(); - } - catch (\RuntimeException $e) - { - // We did everything we could - throw new \RuntimeException(Text::sprintf('INSTL_DATABASE_COULD_NOT_CONNECT', $e->getMessage()), 500, $e); - } - } - // Anything getting into this part of the conditional either doesn't support manually creating the database or isn't that type of error - else - { - throw new \RuntimeException(Text::sprintf('INSTL_DATABASE_COULD_NOT_CONNECT', $e->getMessage()), 500, $e); - } - } - - // Check database server parameters - $dbServerCheck = DatabaseHelper::checkDbServerParameters($db, $options); - - if ($dbServerCheck) - { - // Some server parameter is not ok - throw new \RuntimeException($dbServerCheck, 500, $e); - } - - // @internal Check for spaces in beginning or end of name. - if (strlen(trim($options->db_name)) <> strlen($options->db_name)) - { - throw new \RuntimeException(Text::_('INSTL_DATABASE_NAME_INVALID_SPACES')); - } - - // @internal Check for asc(00) Null in name. - if (strpos($options->db_name, chr(00)) !== false) - { - throw new \RuntimeException(Text::_('INSTL_DATABASE_NAME_INVALID_CHAR')); - } - - // Get database's UTF support. - $utfSupport = $db->hasUtfSupport(); - - // Try to select the database. - try - { - $db->select($options->db_name); - } - catch (\RuntimeException $e) - { - // If the database could not be selected, attempt to create it and then select it. - if (!$this->createDb($db, $options, $utfSupport)) - { - throw new \RuntimeException(Text::sprintf('INSTL_DATABASE_ERROR_CREATE', $options->db_name), 500, $e); - } - - $db->select($options->db_name); - } - - // Set the character set to UTF-8 for pre-existing databases. - try - { - $db->alterDbCharacterSet($options->db_name); - } - catch (\RuntimeException $e) - { - // Continue Anyhow - } - - $options = (array) $options; - - // Remove *_errors value. - foreach ($options as $i => $option) - { - if (isset($i['1']) && $i['1'] == '*') - { - unset($options[$i]); - - break; - } - } - - $options = array_merge(['db_created' => 1], $options); - - Factory::getSession()->set('setup.options', $options); - - return true; - } - - /** - * Method to process the old database. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function handleOldDatabase() - { - $options = $this->getOptions(); - - if (!isset($options['db_created']) || !$options['db_created']) - { - return $this->createDatabase($options); - } - - // Get the options as an object for easier handling. - $options = ArrayHelper::toObject($options); - - if (!$db = $this->initialise()) - { - return false; - } - - // Set the character set to UTF-8 for pre-existing databases. - try - { - $db->alterDbCharacterSet($options->db_name); - } - catch (\RuntimeException $e) - { - // Continue Anyhow - } - - // Backup any old database. - if (!$this->backupDatabase($db, $options->db_prefix)) - { - return false; - } - - return true; - } - - /** - * Method to create the database tables. - * - * @param string $schema The SQL schema file to apply. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function createTables($schema) - { - if (!$db = $this->initialise()) - { - return false; - } - - $serverType = $db->getServerType(); - - // Set the appropriate schema script based on UTF-8 support. - $schemaFile = 'sql/' . $serverType . '/' . $schema . '.sql'; - - // Check if the schema is a valid file - if (!is_file($schemaFile)) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_ERROR_DB', Text::_('INSTL_DATABASE_NO_SCHEMA')), 'error'); - - return false; - } - - // Attempt to import the database schema. - if (!$this->populateDatabase($db, $schemaFile)) - { - return false; - } - - return true; - } - - /** - * Method to backup all tables in a database with a given prefix. - * - * @param DatabaseDriver $db JDatabaseDriver object. - * @param string $prefix Database table prefix. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function backupDatabase($db, $prefix) - { - $return = true; - $backup = 'bak_' . $prefix; - - // Get the tables in the database. - $tables = $db->getTableList(); - - if ($tables) - { - foreach ($tables as $table) - { - // If the table uses the given prefix, back it up. - if (strpos($table, $prefix) === 0) - { - // Backup table name. - $backupTable = str_replace($prefix, $backup, $table); - - // Drop the backup table. - try - { - $db->dropTable($backupTable, true); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_DATABASE_ERROR_BACKINGUP', $e->getMessage()), 'error'); - - $return = false; - } - - // Rename the current table to the backup table. - try - { - $db->renameTable($table, $backupTable, $backup, $prefix); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_DATABASE_ERROR_BACKINGUP', $e->getMessage()), 'error'); - - $return = false; - } - } - } - } - - return $return; - } - - /** - * Method to create a new database. - * - * @param DatabaseDriver $db Database object. - * @param CMSObject $options CMSObject coming from "initialise" function to pass user - * and database name to database driver. - * @param boolean $utf True if the database supports the UTF-8 character set. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function createDb($db, $options, $utf) - { - // Build the create database query. - try - { - // Run the create database query. - $db->createDatabase($options, $utf); - } - catch (\RuntimeException $e) - { - // If an error occurred return false. - return false; - } - - return true; - } - - /** - * Method to import a database schema from a file. - * - * @param \Joomla\Database\DatabaseInterface $db JDatabase object. - * @param string $schema Path to the schema file. - * - * @return boolean True on success. - * - * @since 3.1 - */ - public function populateDatabase($db, $schema) - { - $return = true; - - // Get the contents of the schema file. - if (!($buffer = file_get_contents($schema))) - { - Factory::getApplication()->enqueueMessage(Text::_('INSTL_SAMPLE_DATA_NOT_FOUND'), 'error'); - - return false; - } - - // Get an array of queries from the schema and process them. - $queries = $this->splitQueries($buffer); - - foreach ($queries as $query) - { - // Trim any whitespace. - $query = trim($query); - - // If the query isn't empty and is not a MySQL or PostgreSQL comment, execute it. - if (!empty($query) && ($query[0] != '#') && ($query[0] != '-')) - { - // Execute the query. - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - $return = false; - } - } - } - - return $return; - } - - /** - * Method to split up queries from a schema file into an array. - * - * @param string $query SQL schema. - * - * @return array Queries to perform. - * - * @since 3.1 - */ - protected function splitQueries($query) - { - $buffer = array(); - $queries = array(); - $in_string = false; - - // Trim any whitespace. - $query = trim($query); - - // Remove comment lines. - $query = preg_replace("/\n\#[^\n]*/", '', "\n" . $query); - - // Remove PostgreSQL comment lines. - $query = preg_replace("/\n\--[^\n]*/", '', "\n" . $query); - - // Find function. - $funct = explode('CREATE OR REPLACE FUNCTION', $query); - - // Save sql before function and parse it. - $query = $funct[0]; - - // Parse the schema file to break up queries. - for ($i = 0; $i < strlen($query) - 1; $i++) - { - if ($query[$i] == ';' && !$in_string) - { - $queries[] = substr($query, 0, $i); - $query = substr($query, $i + 1); - $i = 0; - } - - if ($in_string && ($query[$i] == $in_string) && $buffer[1] != "\\") - { - $in_string = false; - } - elseif (!$in_string && ($query[$i] == '"' || $query[$i] == "'") && (!isset($buffer[0]) || $buffer[0] != "\\")) - { - $in_string = $query[$i]; - } - - if (isset($buffer[1])) - { - $buffer[0] = $buffer[1]; - } - - $buffer[1] = $query[$i]; - } - - // If the is anything left over, add it to the queries. - if (!empty($query)) - { - $queries[] = $query; - } - - // Add function part as is. - for ($f = 1, $fMax = count($funct); $f < $fMax; $f++) - { - $queries[] = 'CREATE OR REPLACE FUNCTION ' . $funct[$f]; - } - - return $queries; - } + /** + * Get the current setup options from the session. + * + * @return array An array of options from the session. + * + * @since 4.0.0 + */ + public function getOptions() + { + return Factory::getSession()->get('setup.options', array()); + } + + /** + * Method to initialise the database. + * + * @param boolean $select Select the database when creating the connections. + * + * @return DatabaseInterface|boolean Database object on success, boolean false on failure + * + * @since 3.1 + */ + public function initialise($select = true) + { + $options = $this->getOptions(); + + // Get the options as an object for easier handling. + $options = ArrayHelper::toObject($options); + + // Load the backend language files so that the DB error messages work. + $lang = Factory::getLanguage(); + $currentLang = $lang->getTag(); + + // Load the selected language + if (LanguageHelper::exists($currentLang, JPATH_ADMINISTRATOR)) { + $lang->load('joomla', JPATH_ADMINISTRATOR, $currentLang, true); + } else { + // Pre-load en-GB in case the chosen language files do not exist. + $lang->load('joomla', JPATH_ADMINISTRATOR, 'en-GB', true); + } + + // Validate and clean up connection parameters + $paramsCheck = DatabaseHelper::validateConnectionParameters($options); + + if ($paramsCheck) { + Factory::getApplication()->enqueueMessage($paramsCheck, 'warning'); + + return false; + } + + // Security check for remote db hosts + if (!DatabaseHelper::checkRemoteDbHost($options)) { + // Messages have been enqueued in the called function. + return false; + } + + // Get a database object. + try { + return DatabaseHelper::getDbo( + $options->db_type, + $options->db_host, + $options->db_user, + $options->db_pass_plain, + $options->db_name, + $options->db_prefix, + $select, + DatabaseHelper::getEncryptionSettings($options) + ); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_DATABASE_COULD_NOT_CONNECT', $e->getMessage()), 'error'); + + return false; + } + } + + /** + * Method to create a new database. + * + * @return boolean + * + * @since 3.1 + * @throws \RuntimeException + */ + public function createDatabase() + { + $options = (object) $this->getOptions(); + + $db = $this->initialise(false); + + if ($db === false) { + // Error messages are enqueued by the initialise function, we just need to tell the controller how to redirect + return false; + } + + // Check database version. + $type = $options->db_type; + + try { + $db_version = $db->getVersion(); + } catch (\RuntimeException $e) { + /* + * We may get here if the database doesn't exist, if so then explain that to users instead of showing the database connector's error + * This only supports PDO PostgreSQL and the PDO MySQL drivers presently + * + * Error Messages: + * PDO MySQL: [1049] Unknown database 'database_name' + * PDO PostgreSQL: database "database_name" does not exist + */ + if ( + $type === 'mysql' && strpos($e->getMessage(), '[1049] Unknown database') === 42 + || $type === 'pgsql' && strpos($e->getMessage(), 'database "' . $options->db_name . '" does not exist') + ) { + /* + * Now we're really getting insane here; we're going to try building a new JDatabaseDriver instance + * in order to trick the connection into creating the database + */ + if ($type === 'mysql') { + // MySQL (PDO): Don't specify database name + $altDBoptions = array( + 'driver' => $options->db_type, + 'host' => $options->db_host, + 'user' => $options->db_user, + 'password' => $options->db_pass_plain, + 'prefix' => $options->db_prefix, + 'select' => false, + DatabaseHelper::getEncryptionSettings($options), + ); + } else { + // PostgreSQL (PDO): Use 'postgres' + $altDBoptions = array( + 'driver' => $options->db_type, + 'host' => $options->db_host, + 'user' => $options->db_user, + 'password' => $options->db_pass_plain, + 'database' => 'postgres', + 'prefix' => $options->db_prefix, + 'select' => false, + DatabaseHelper::getEncryptionSettings($options), + ); + } + + $altDB = DatabaseDriver::getInstance($altDBoptions); + + // Check database server parameters + $dbServerCheck = DatabaseHelper::checkDbServerParameters($altDB, $options); + + if ($dbServerCheck) { + // Some server parameter is not ok + throw new \RuntimeException($dbServerCheck, 500, $e); + } + + // Try to create the database now using the alternate driver + try { + $this->createDb($altDB, $options, $altDB->hasUtfSupport()); + } catch (\RuntimeException $e) { + // We did everything we could + throw new \RuntimeException(Text::_('INSTL_DATABASE_COULD_NOT_CREATE_DATABASE'), 500, $e); + } + + // If we got here, the database should have been successfully created, now try one more time to get the version + try { + $db_version = $db->getVersion(); + } catch (\RuntimeException $e) { + // We did everything we could + throw new \RuntimeException(Text::sprintf('INSTL_DATABASE_COULD_NOT_CONNECT', $e->getMessage()), 500, $e); + } + } else { + // Anything getting into this part of the conditional either doesn't support manually creating the database or isn't that type of error + throw new \RuntimeException(Text::sprintf('INSTL_DATABASE_COULD_NOT_CONNECT', $e->getMessage()), 500, $e); + } + } + + // Check database server parameters + $dbServerCheck = DatabaseHelper::checkDbServerParameters($db, $options); + + if ($dbServerCheck) { + // Some server parameter is not ok + throw new \RuntimeException($dbServerCheck, 500, $e); + } + + // @internal Check for spaces in beginning or end of name. + if (strlen(trim($options->db_name)) <> strlen($options->db_name)) { + throw new \RuntimeException(Text::_('INSTL_DATABASE_NAME_INVALID_SPACES')); + } + + // @internal Check for asc(00) Null in name. + if (strpos($options->db_name, chr(00)) !== false) { + throw new \RuntimeException(Text::_('INSTL_DATABASE_NAME_INVALID_CHAR')); + } + + // Get database's UTF support. + $utfSupport = $db->hasUtfSupport(); + + // Try to select the database. + try { + $db->select($options->db_name); + } catch (\RuntimeException $e) { + // If the database could not be selected, attempt to create it and then select it. + if (!$this->createDb($db, $options, $utfSupport)) { + throw new \RuntimeException(Text::sprintf('INSTL_DATABASE_ERROR_CREATE', $options->db_name), 500, $e); + } + + $db->select($options->db_name); + } + + // Set the character set to UTF-8 for pre-existing databases. + try { + $db->alterDbCharacterSet($options->db_name); + } catch (\RuntimeException $e) { + // Continue Anyhow + } + + $options = (array) $options; + + // Remove *_errors value. + foreach ($options as $i => $option) { + if (isset($i['1']) && $i['1'] == '*') { + unset($options[$i]); + + break; + } + } + + $options = array_merge(['db_created' => 1], $options); + + Factory::getSession()->set('setup.options', $options); + + return true; + } + + /** + * Method to process the old database. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function handleOldDatabase() + { + $options = $this->getOptions(); + + if (!isset($options['db_created']) || !$options['db_created']) { + return $this->createDatabase($options); + } + + // Get the options as an object for easier handling. + $options = ArrayHelper::toObject($options); + + if (!$db = $this->initialise()) { + return false; + } + + // Set the character set to UTF-8 for pre-existing databases. + try { + $db->alterDbCharacterSet($options->db_name); + } catch (\RuntimeException $e) { + // Continue Anyhow + } + + // Backup any old database. + if (!$this->backupDatabase($db, $options->db_prefix)) { + return false; + } + + return true; + } + + /** + * Method to create the database tables. + * + * @param string $schema The SQL schema file to apply. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function createTables($schema) + { + if (!$db = $this->initialise()) { + return false; + } + + $serverType = $db->getServerType(); + + // Set the appropriate schema script based on UTF-8 support. + $schemaFile = 'sql/' . $serverType . '/' . $schema . '.sql'; + + // Check if the schema is a valid file + if (!is_file($schemaFile)) { + Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_ERROR_DB', Text::_('INSTL_DATABASE_NO_SCHEMA')), 'error'); + + return false; + } + + // Attempt to import the database schema. + if (!$this->populateDatabase($db, $schemaFile)) { + return false; + } + + return true; + } + + /** + * Method to backup all tables in a database with a given prefix. + * + * @param DatabaseDriver $db JDatabaseDriver object. + * @param string $prefix Database table prefix. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function backupDatabase($db, $prefix) + { + $return = true; + $backup = 'bak_' . $prefix; + + // Get the tables in the database. + $tables = $db->getTableList(); + + if ($tables) { + foreach ($tables as $table) { + // If the table uses the given prefix, back it up. + if (strpos($table, $prefix) === 0) { + // Backup table name. + $backupTable = str_replace($prefix, $backup, $table); + + // Drop the backup table. + try { + $db->dropTable($backupTable, true); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_DATABASE_ERROR_BACKINGUP', $e->getMessage()), 'error'); + + $return = false; + } + + // Rename the current table to the backup table. + try { + $db->renameTable($table, $backupTable, $backup, $prefix); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_DATABASE_ERROR_BACKINGUP', $e->getMessage()), 'error'); + + $return = false; + } + } + } + } + + return $return; + } + + /** + * Method to create a new database. + * + * @param DatabaseDriver $db Database object. + * @param CMSObject $options CMSObject coming from "initialise" function to pass user + * and database name to database driver. + * @param boolean $utf True if the database supports the UTF-8 character set. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function createDb($db, $options, $utf) + { + // Build the create database query. + try { + // Run the create database query. + $db->createDatabase($options, $utf); + } catch (\RuntimeException $e) { + // If an error occurred return false. + return false; + } + + return true; + } + + /** + * Method to import a database schema from a file. + * + * @param \Joomla\Database\DatabaseInterface $db JDatabase object. + * @param string $schema Path to the schema file. + * + * @return boolean True on success. + * + * @since 3.1 + */ + public function populateDatabase($db, $schema) + { + $return = true; + + // Get the contents of the schema file. + if (!($buffer = file_get_contents($schema))) { + Factory::getApplication()->enqueueMessage(Text::_('INSTL_SAMPLE_DATA_NOT_FOUND'), 'error'); + + return false; + } + + // Get an array of queries from the schema and process them. + $queries = $this->splitQueries($buffer); + + foreach ($queries as $query) { + // Trim any whitespace. + $query = trim($query); + + // If the query isn't empty and is not a MySQL or PostgreSQL comment, execute it. + if (!empty($query) && ($query[0] != '#') && ($query[0] != '-')) { + // Execute the query. + $db->setQuery($query); + + try { + $db->execute(); + } catch (\RuntimeException $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + $return = false; + } + } + } + + return $return; + } + + /** + * Method to split up queries from a schema file into an array. + * + * @param string $query SQL schema. + * + * @return array Queries to perform. + * + * @since 3.1 + */ + protected function splitQueries($query) + { + $buffer = array(); + $queries = array(); + $in_string = false; + + // Trim any whitespace. + $query = trim($query); + + // Remove comment lines. + $query = preg_replace("/\n\#[^\n]*/", '', "\n" . $query); + + // Remove PostgreSQL comment lines. + $query = preg_replace("/\n\--[^\n]*/", '', "\n" . $query); + + // Find function. + $funct = explode('CREATE OR REPLACE FUNCTION', $query); + + // Save sql before function and parse it. + $query = $funct[0]; + + // Parse the schema file to break up queries. + for ($i = 0; $i < strlen($query) - 1; $i++) { + if ($query[$i] == ';' && !$in_string) { + $queries[] = substr($query, 0, $i); + $query = substr($query, $i + 1); + $i = 0; + } + + if ($in_string && ($query[$i] == $in_string) && $buffer[1] != "\\") { + $in_string = false; + } elseif (!$in_string && ($query[$i] == '"' || $query[$i] == "'") && (!isset($buffer[0]) || $buffer[0] != "\\")) { + $in_string = $query[$i]; + } + + if (isset($buffer[1])) { + $buffer[0] = $buffer[1]; + } + + $buffer[1] = $query[$i]; + } + + // If the is anything left over, add it to the queries. + if (!empty($query)) { + $queries[] = $query; + } + + // Add function part as is. + for ($f = 1, $fMax = count($funct); $f < $fMax; $f++) { + $queries[] = 'CREATE OR REPLACE FUNCTION ' . $funct[$f]; + } + + return $queries; + } } diff --git a/installation/src/Model/LanguagesModel.php b/installation/src/Model/LanguagesModel.php index 73af151cf7ddf..831f4e90dcbdd 100644 --- a/installation/src/Model/LanguagesModel.php +++ b/installation/src/Model/LanguagesModel.php @@ -1,4 +1,5 @@ setConfiguration(new Registry(new \JConfig)); - } - - parent::__construct(); - } - - /** - * Generate a list of language choices to install in the Joomla CMS. - * - * @return array - * - * @since 3.1 - */ - public function getItems() - { - // Get the extension_id of the en-GB package. - $db = $this->getDatabase(); - $extQuery = $db->getQuery(true); - - $extQuery->select($db->quoteName('extension_id')) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('package')) - ->where($db->quoteName('element') . ' = ' . $db->quote('pkg_en-GB')) - ->where($db->quoteName('client_id') . ' = 0'); - - $db->setQuery($extQuery); - - $extId = (int) $db->loadResult(); - - if ($extId) - { - $updater = Updater::getInstance(); - - /* - * The following function call uses the extension_id of the en-GB package. - * In #__update_sites_extensions you should have this extension_id linked - * to the Accredited Translations Repo. - */ - $updater->findUpdates(array($extId), 0); - - $query = $db->getQuery(true); - - // Select the required fields from the updates table. - $query->select($db->quoteName(array('update_id', 'name', 'element', 'version'))) - ->from($db->quoteName('#__updates')) - ->order($db->quoteName('name')); - - $db->setQuery($query); - $list = $db->loadObjectList(); - - if (!$list || $list instanceof \Exception) - { - $list = array(); - } - } - else - { - $list = array(); - } - - return $list; - } - - /** - * Method that installs in Joomla! the selected languages in the Languages View of the installer. - * - * @param array $lids List of the update_id value of the languages to install. - * - * @return boolean True if successful - */ - public function install($lids) - { - $app = Factory::getApplication(); - $installerBase = new Installer; - $installerBase->setDatabase($this->getDatabase()); - - // Loop through every selected language. - foreach ($lids as $id) - { - $installer = clone $installerBase; - - // Loads the update database object that represents the language. - $language = Table::getInstance('update'); - $language->load($id); - - // Get the URL to the XML manifest file of the selected language. - $remote_manifest = $this->getLanguageManifest($id); - - if (!$remote_manifest) - { - // Could not find the url, the information in the update server may be corrupt. - $message = Text::sprintf('INSTL_DEFAULTLANGUAGE_COULD_NOT_INSTALL_LANGUAGE', $language->name); - $message .= ' ' . Text::_('INSTL_DEFAULTLANGUAGE_TRY_LATER'); - - $app->enqueueMessage($message, 'warning'); - - continue; - } - - // Based on the language XML manifest get the URL of the package to download. - $package_url = $this->getPackageUrl($remote_manifest); - - if (!$package_url) - { - // Could not find the URL, maybe the URL is wrong in the update server, or there is no internet access. - $message = Text::sprintf('INSTL_DEFAULTLANGUAGE_COULD_NOT_INSTALL_LANGUAGE', $language->name); - $message .= ' ' . Text::_('INSTL_DEFAULTLANGUAGE_TRY_LATER'); - - $app->enqueueMessage($message, 'warning'); - - continue; - } - - // Download the package to the tmp folder. - $package = $this->downloadPackage($package_url); - - if (!$package) - { - $app->enqueueMessage(Text::sprintf('INSTL_DEFAULTLANGUAGE_COULD_NOT_DOWNLOAD_PACKAGE', $package_url), 'error'); - - continue; - } - - // Install the package. - if (!$installer->install($package['dir'])) - { - // There was an error installing the package. - $message = Text::sprintf('INSTL_DEFAULTLANGUAGE_COULD_NOT_INSTALL_LANGUAGE', $language->name); - $message .= ' ' . Text::_('INSTL_DEFAULTLANGUAGE_TRY_LATER'); - - $app->enqueueMessage($message, 'warning'); - - continue; - } - - // Cleanup the install files in tmp folder. - if (!is_file($package['packagefile'])) - { - $package['packagefile'] = $app->get('tmp_path') . '/' . $package['packagefile']; - } - - InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); - - // Delete the installed language from the list. - $language->delete($id); - } - - return true; - } - - /** - * Gets the manifest file of a selected language from a the language list in an update server. - * - * @param integer $uid The id of the language in the #__updates table. - * - * @return string - * - * @since 3.1 - */ - protected function getLanguageManifest($uid) - { - $instance = Table::getInstance('update'); - $instance->load($uid); - - return trim($instance->detailsurl); - } - - /** - * Finds the URL of the package to download. - * - * @param string $remoteManifest URL to the manifest XML file of the remote package. - * - * @return string|boolean - * - * @since 3.1 - */ - protected function getPackageUrl($remoteManifest) - { - $update = new Update; - $update->loadFromXml($remoteManifest); - - // Get the download url from the remote manifest - $downloadUrl = $update->get('downloadurl', false); - - // Check if the download url exist, otherwise return empty value - if ($downloadUrl === false) - { - return ''; - } - - return trim($downloadUrl->_data); - } - - /** - * Download a language package from a URL and unpack it in the tmp folder. - * - * @param string $url URL of the package. - * - * @return array|boolean Package details or false on failure. - * - * @since 3.1 - */ - protected function downloadPackage($url) - { - $app = Factory::getApplication(); - - // Download the package from the given URL. - $p_file = InstallerHelper::downloadPackage($url); - - // Was the package downloaded? - if (!$p_file) - { - $app->enqueueMessage(Text::_('INSTL_ERROR_INVALID_URL'), 'warning'); - - return false; - } - - // Unpack the downloaded package file. - return InstallerHelper::unpack($app->get('tmp_path') . '/' . $p_file); - } - - /** - * Get Languages item data for the Administrator. - * - * @return array - * - * @since 3.1 - */ - public function getInstalledlangsAdministrator() - { - return $this->getInstalledlangs('administrator'); - } - - /** - * Get Languages item data for the Frontend. - * - * @return array List of installed languages in the frontend application. - * - * @since 3.1 - */ - public function getInstalledlangsFrontend() - { - return $this->getInstalledlangs('site'); - } - - /** - * Get Languages item data. - * - * @param string $clientName Name of the cms client. - * - * @return array - * - * @since 3.1 - */ - protected function getInstalledlangs($clientName = 'administrator') - { - // Get information. - $path = $this->getPath(); - $client = $this->getClient($clientName); - $langlist = $this->getLanguageList($client->id); - - // Compute all the languages. - $data = array(); - - foreach ($langlist as $lang) - { - $file = $path . '/' . $lang . '/langmetadata.xml'; - - if (!is_file($file)) - { - $file = $path . '/' . $lang . '/' . $lang . '.xml'; - } - - $info = Installer::parseXMLInstallFile($file); - $row = new \stdClass; - $row->language = $lang; - - if (!is_array($info)) - { - continue; - } - - foreach ($info as $key => $value) - { - $row->$key = $value; - } - - // If current then set published. - $params = ComponentHelper::getParams('com_languages'); - - if ($params->get($client->name, 'en-GB') == $row->language) - { - $row->published = 1; - } - else - { - $row->published = 0; - } - - $row->checked_out = null; - $data[] = $row; - } - - usort($data, array($this, 'compareLanguages')); - - return $data; - } - - /** - * Get installed languages data. - * - * @param integer $clientId The client ID to retrieve data for. - * - * @return object The language data. - * - * @since 3.1 - */ - protected function getLanguageList($clientId = 1) - { - // Create a new db object. - $db = $this->getDatabase(); - $query = $db->getQuery(true); - - // Select field element from the extensions table. - $query->select($db->quoteName(array('element', 'name'))) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('type') . ' = ' . $db->quote('language')) - ->where($db->quoteName('state') . ' = 0') - ->where($db->quoteName('enabled') . ' = 1') - ->where($db->quoteName('client_id') . ' = ' . (int) $clientId); - - $db->setQuery($query); - - $this->langlist = $db->loadColumn(); - - return $this->langlist; - } - - /** - * Compare two languages in order to sort them. - * - * @param object $lang1 The first language. - * @param object $lang2 The second language. - * - * @return integer - * - * @since 3.1 - */ - protected function compareLanguages($lang1, $lang2) - { - return strcmp($lang1->name, $lang2->name); - } - - /** - * Get the languages folder path. - * - * @return string The path to the languages folders. - * - * @since 3.1 - */ - protected function getPath() - { - if ($this->path === null) - { - $client = $this->getClient(); - $this->path = LanguageHelper::getLanguagePath($client->path); - } - - return $this->path; - } - - /** - * Get the client object of Administrator or Frontend. - * - * @param string $client Name of the client object. - * - * @return object - * - * @since 3.1 - */ - protected function getClient($client = 'administrator') - { - $this->client = ApplicationHelper::getClientInfo($client, true); - - return $this->client; - } - - /** - * Set the default language. - * - * @param string $language The language to be set as default. - * @param string $clientName The name of the CMS client. - * - * @return boolean - * - * @since 3.1 - */ - public function setDefault($language, $clientName = 'administrator') - { - $client = $this->getClient($clientName); - - $params = ComponentHelper::getParams('com_languages'); - $params->set($client->name, $language); - - $table = Table::getInstance('extension'); - $id = $table->find(array('element' => 'com_languages')); - - // Load - if (!$table->load($id)) - { - Factory::getApplication()->enqueueMessage($table->getError(), 'warning'); - - return false; - } - - $table->params = (string) $params; - - // Pre-save checks. - if (!$table->check()) - { - Factory::getApplication()->enqueueMessage($table->getError(), 'warning'); - - return false; - } - - // Save the changes. - if (!$table->store()) - { - Factory::getApplication()->enqueueMessage($table->getError(), 'warning'); - - return false; - } - - return true; - } - - /** - * Get the current setup options from the session. - * - * @return array - * - * @since 3.1 - */ - public function getOptions() - { - return Factory::getSession()->get('setup.options', array()); - } - - /** - * Get the model form. - * - * @param string|null $view The view being processed. - * - * @return mixed JForm object on success, false on failure. - * - * @since 3.1 - */ - public function getForm($view = null) - { - if (!$view) - { - $view = Factory::getApplication()->input->getWord('view', 'defaultlanguage'); - } - - // Get the form. - Form::addFormPath(JPATH_COMPONENT . '/forms'); - Form::addFieldPath(JPATH_COMPONENT . '/model/fields'); - Form::addRulePath(JPATH_COMPONENT . '/model/rules'); - - try - { - $form = Form::getInstance('jform', $view, array('control' => 'jform')); - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - // Check the session for previously entered form data. - $data = (array) $this->getOptions(); - - // Bind the form data if present. - if (!empty($data)) - { - $form->bind($data); - } - - return $form; - } + use DatabaseAwareTrait; + + /** + * @var object Client object. + * @since 3.1 + */ + protected $client; + + /** + * @var array Languages description. + * @since 3.1 + */ + protected $data; + + /** + * @var string Language path. + * @since 3.1 + */ + protected $path; + + /** + * @var integer Total number of languages installed. + * @since 3.1 + */ + protected $langlist; + + /** + * @var integer Admin Id, author of all generated content. + * @since 3.1 + */ + protected $adminId; + + /** + * Constructor: Deletes the default installation config file and recreates it with the good config file. + * + * @since 3.1 + */ + public function __construct() + { + // Overrides application config and set the configuration.php file so tokens and database works. + if (file_exists(JPATH_BASE . '/configuration.php')) { + Factory::getApplication()->setConfiguration(new Registry(new \JConfig())); + } + + parent::__construct(); + } + + /** + * Generate a list of language choices to install in the Joomla CMS. + * + * @return array + * + * @since 3.1 + */ + public function getItems() + { + // Get the extension_id of the en-GB package. + $db = $this->getDatabase(); + $extQuery = $db->getQuery(true); + + $extQuery->select($db->quoteName('extension_id')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('package')) + ->where($db->quoteName('element') . ' = ' . $db->quote('pkg_en-GB')) + ->where($db->quoteName('client_id') . ' = 0'); + + $db->setQuery($extQuery); + + $extId = (int) $db->loadResult(); + + if ($extId) { + $updater = Updater::getInstance(); + + /* + * The following function call uses the extension_id of the en-GB package. + * In #__update_sites_extensions you should have this extension_id linked + * to the Accredited Translations Repo. + */ + $updater->findUpdates(array($extId), 0); + + $query = $db->getQuery(true); + + // Select the required fields from the updates table. + $query->select($db->quoteName(array('update_id', 'name', 'element', 'version'))) + ->from($db->quoteName('#__updates')) + ->order($db->quoteName('name')); + + $db->setQuery($query); + $list = $db->loadObjectList(); + + if (!$list || $list instanceof \Exception) { + $list = array(); + } + } else { + $list = array(); + } + + return $list; + } + + /** + * Method that installs in Joomla! the selected languages in the Languages View of the installer. + * + * @param array $lids List of the update_id value of the languages to install. + * + * @return boolean True if successful + */ + public function install($lids) + { + $app = Factory::getApplication(); + $installerBase = new Installer(); + $installerBase->setDatabase($this->getDatabase()); + + // Loop through every selected language. + foreach ($lids as $id) { + $installer = clone $installerBase; + + // Loads the update database object that represents the language. + $language = Table::getInstance('update'); + $language->load($id); + + // Get the URL to the XML manifest file of the selected language. + $remote_manifest = $this->getLanguageManifest($id); + + if (!$remote_manifest) { + // Could not find the url, the information in the update server may be corrupt. + $message = Text::sprintf('INSTL_DEFAULTLANGUAGE_COULD_NOT_INSTALL_LANGUAGE', $language->name); + $message .= ' ' . Text::_('INSTL_DEFAULTLANGUAGE_TRY_LATER'); + + $app->enqueueMessage($message, 'warning'); + + continue; + } + + // Based on the language XML manifest get the URL of the package to download. + $package_url = $this->getPackageUrl($remote_manifest); + + if (!$package_url) { + // Could not find the URL, maybe the URL is wrong in the update server, or there is no internet access. + $message = Text::sprintf('INSTL_DEFAULTLANGUAGE_COULD_NOT_INSTALL_LANGUAGE', $language->name); + $message .= ' ' . Text::_('INSTL_DEFAULTLANGUAGE_TRY_LATER'); + + $app->enqueueMessage($message, 'warning'); + + continue; + } + + // Download the package to the tmp folder. + $package = $this->downloadPackage($package_url); + + if (!$package) { + $app->enqueueMessage(Text::sprintf('INSTL_DEFAULTLANGUAGE_COULD_NOT_DOWNLOAD_PACKAGE', $package_url), 'error'); + + continue; + } + + // Install the package. + if (!$installer->install($package['dir'])) { + // There was an error installing the package. + $message = Text::sprintf('INSTL_DEFAULTLANGUAGE_COULD_NOT_INSTALL_LANGUAGE', $language->name); + $message .= ' ' . Text::_('INSTL_DEFAULTLANGUAGE_TRY_LATER'); + + $app->enqueueMessage($message, 'warning'); + + continue; + } + + // Cleanup the install files in tmp folder. + if (!is_file($package['packagefile'])) { + $package['packagefile'] = $app->get('tmp_path') . '/' . $package['packagefile']; + } + + InstallerHelper::cleanupInstall($package['packagefile'], $package['extractdir']); + + // Delete the installed language from the list. + $language->delete($id); + } + + return true; + } + + /** + * Gets the manifest file of a selected language from a the language list in an update server. + * + * @param integer $uid The id of the language in the #__updates table. + * + * @return string + * + * @since 3.1 + */ + protected function getLanguageManifest($uid) + { + $instance = Table::getInstance('update'); + $instance->load($uid); + + return trim($instance->detailsurl); + } + + /** + * Finds the URL of the package to download. + * + * @param string $remoteManifest URL to the manifest XML file of the remote package. + * + * @return string|boolean + * + * @since 3.1 + */ + protected function getPackageUrl($remoteManifest) + { + $update = new Update(); + $update->loadFromXml($remoteManifest); + + // Get the download url from the remote manifest + $downloadUrl = $update->get('downloadurl', false); + + // Check if the download url exist, otherwise return empty value + if ($downloadUrl === false) { + return ''; + } + + return trim($downloadUrl->_data); + } + + /** + * Download a language package from a URL and unpack it in the tmp folder. + * + * @param string $url URL of the package. + * + * @return array|boolean Package details or false on failure. + * + * @since 3.1 + */ + protected function downloadPackage($url) + { + $app = Factory::getApplication(); + + // Download the package from the given URL. + $p_file = InstallerHelper::downloadPackage($url); + + // Was the package downloaded? + if (!$p_file) { + $app->enqueueMessage(Text::_('INSTL_ERROR_INVALID_URL'), 'warning'); + + return false; + } + + // Unpack the downloaded package file. + return InstallerHelper::unpack($app->get('tmp_path') . '/' . $p_file); + } + + /** + * Get Languages item data for the Administrator. + * + * @return array + * + * @since 3.1 + */ + public function getInstalledlangsAdministrator() + { + return $this->getInstalledlangs('administrator'); + } + + /** + * Get Languages item data for the Frontend. + * + * @return array List of installed languages in the frontend application. + * + * @since 3.1 + */ + public function getInstalledlangsFrontend() + { + return $this->getInstalledlangs('site'); + } + + /** + * Get Languages item data. + * + * @param string $clientName Name of the cms client. + * + * @return array + * + * @since 3.1 + */ + protected function getInstalledlangs($clientName = 'administrator') + { + // Get information. + $path = $this->getPath(); + $client = $this->getClient($clientName); + $langlist = $this->getLanguageList($client->id); + + // Compute all the languages. + $data = array(); + + foreach ($langlist as $lang) { + $file = $path . '/' . $lang . '/langmetadata.xml'; + + if (!is_file($file)) { + $file = $path . '/' . $lang . '/' . $lang . '.xml'; + } + + $info = Installer::parseXMLInstallFile($file); + $row = new \stdClass(); + $row->language = $lang; + + if (!is_array($info)) { + continue; + } + + foreach ($info as $key => $value) { + $row->$key = $value; + } + + // If current then set published. + $params = ComponentHelper::getParams('com_languages'); + + if ($params->get($client->name, 'en-GB') == $row->language) { + $row->published = 1; + } else { + $row->published = 0; + } + + $row->checked_out = null; + $data[] = $row; + } + + usort($data, array($this, 'compareLanguages')); + + return $data; + } + + /** + * Get installed languages data. + * + * @param integer $clientId The client ID to retrieve data for. + * + * @return object The language data. + * + * @since 3.1 + */ + protected function getLanguageList($clientId = 1) + { + // Create a new db object. + $db = $this->getDatabase(); + $query = $db->getQuery(true); + + // Select field element from the extensions table. + $query->select($db->quoteName(array('element', 'name'))) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('language')) + ->where($db->quoteName('state') . ' = 0') + ->where($db->quoteName('enabled') . ' = 1') + ->where($db->quoteName('client_id') . ' = ' . (int) $clientId); + + $db->setQuery($query); + + $this->langlist = $db->loadColumn(); + + return $this->langlist; + } + + /** + * Compare two languages in order to sort them. + * + * @param object $lang1 The first language. + * @param object $lang2 The second language. + * + * @return integer + * + * @since 3.1 + */ + protected function compareLanguages($lang1, $lang2) + { + return strcmp($lang1->name, $lang2->name); + } + + /** + * Get the languages folder path. + * + * @return string The path to the languages folders. + * + * @since 3.1 + */ + protected function getPath() + { + if ($this->path === null) { + $client = $this->getClient(); + $this->path = LanguageHelper::getLanguagePath($client->path); + } + + return $this->path; + } + + /** + * Get the client object of Administrator or Frontend. + * + * @param string $client Name of the client object. + * + * @return object + * + * @since 3.1 + */ + protected function getClient($client = 'administrator') + { + $this->client = ApplicationHelper::getClientInfo($client, true); + + return $this->client; + } + + /** + * Set the default language. + * + * @param string $language The language to be set as default. + * @param string $clientName The name of the CMS client. + * + * @return boolean + * + * @since 3.1 + */ + public function setDefault($language, $clientName = 'administrator') + { + $client = $this->getClient($clientName); + + $params = ComponentHelper::getParams('com_languages'); + $params->set($client->name, $language); + + $table = Table::getInstance('extension'); + $id = $table->find(array('element' => 'com_languages')); + + // Load + if (!$table->load($id)) { + Factory::getApplication()->enqueueMessage($table->getError(), 'warning'); + + return false; + } + + $table->params = (string) $params; + + // Pre-save checks. + if (!$table->check()) { + Factory::getApplication()->enqueueMessage($table->getError(), 'warning'); + + return false; + } + + // Save the changes. + if (!$table->store()) { + Factory::getApplication()->enqueueMessage($table->getError(), 'warning'); + + return false; + } + + return true; + } + + /** + * Get the current setup options from the session. + * + * @return array + * + * @since 3.1 + */ + public function getOptions() + { + return Factory::getSession()->get('setup.options', array()); + } + + /** + * Get the model form. + * + * @param string|null $view The view being processed. + * + * @return mixed JForm object on success, false on failure. + * + * @since 3.1 + */ + public function getForm($view = null) + { + if (!$view) { + $view = Factory::getApplication()->input->getWord('view', 'defaultlanguage'); + } + + // Get the form. + Form::addFormPath(JPATH_COMPONENT . '/forms'); + Form::addFieldPath(JPATH_COMPONENT . '/model/fields'); + Form::addRulePath(JPATH_COMPONENT . '/model/rules'); + + try { + $form = Form::getInstance('jform', $view, array('control' => 'jform')); + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + // Check the session for previously entered form data. + $data = (array) $this->getOptions(); + + // Bind the form data if present. + if (!empty($data)) { + $form->bind($data); + } + + return $form; + } } diff --git a/installation/src/Model/SetupModel.php b/installation/src/Model/SetupModel.php index 7ea706f79c2f4..6664602f17262 100644 --- a/installation/src/Model/SetupModel.php +++ b/installation/src/Model/SetupModel.php @@ -1,4 +1,5 @@ get('setup.options', array()))) - { - return Factory::getSession()->get('setup.options', array()); - } - } - - /** - * Store the current setup options in the session. - * - * @param array $options The installation options. - * - * @return array An array of options from the session. - * - * @since 3.1 - */ - public function storeOptions($options) - { - // Get the current setup options from the session. - $old = (array) $this->getOptions(); - - // Ensure that we have language - if (!isset($options['language']) || empty($options['language'])) - { - $options['language'] = Factory::getLanguage()->getTag(); - } - - // Store passwords as a separate key that is not used in the forms - foreach (array('admin_password', 'db_pass') as $passwordField) - { - if (isset($options[$passwordField])) - { - $plainTextKey = $passwordField . '_plain'; - - $options[$plainTextKey] = $options[$passwordField]; - - unset($options[$passwordField]); - } - } - - // Get the session - $session = Factory::getSession(); - $options['helpurl'] = $session->get('setup.helpurl', null); - - // Merge the new setup options into the current ones and store in the session. - $options = array_merge($old, (array) $options); - $session->set('setup.options', $options); - - return $options; - } - - /** - * Method to get the form. - * - * @param string|null $view The view being processed. - * - * @return Form|boolean JForm object on success, false on failure. - * - * @since 3.1 - */ - public function getForm($view = null) - { - if (!$view) - { - $view = Factory::getApplication()->input->getWord('view', 'setup'); - } - - // Get the form. - Form::addFormPath(JPATH_COMPONENT . '/forms'); - - try - { - $form = Form::getInstance('jform', $view, array('control' => 'jform')); - } - catch (\Exception $e) - { - Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); - - return false; - } - - // Check the session for previously entered form data. - $data = (array) $this->getOptions(); - - // Bind the form data if present. - if (!empty($data)) - { - $form->bind($data); - } - - return $form; - } - - /** - * Method to check the form data. - * - * @param string $page The view being checked. - * - * @return array|boolean Array with the validated form data or boolean false on a validation failure. - * - * @since 3.1 - */ - public function checkForm($page = 'setup') - { - // Get the posted values from the request and validate them. - $data = Factory::getApplication()->input->post->get('jform', array(), 'array'); - $return = $this->validate($data, $page); - - // Attempt to save the data before validation. - $form = $this->getForm(); - $data = $form->filter($data); - - $this->storeOptions($data); - - // Check for validation errors. - if ($return === false) - { - return false; - } - - // Store the options in the session. - return $this->storeOptions($return); - } - - /** - * Generate a panel of language choices for the user to select their language. - * - * @return array - * - * @since 3.1 - */ - public function getLanguages() - { - // Detect the native language. - $native = LanguageHelper::detectLanguage(); - - if (empty($native)) - { - $native = 'en-GB'; - } - - // Get a forced language if it exists. - $forced = Factory::getApplication()->getLocalise(); - - if (!empty($forced['language'])) - { - $native = $forced['language']; - } - - // Get the list of available languages. - $list = LanguageHelper::createLanguageList($native); - - if (!$list || $list instanceof \Exception) - { - $list = array(); - } - - return $list; - } - - /** - * Method to validate the form data. - * - * @param array $data The form data. - * @param string|null $view The view. - * - * @return array|boolean Array of filtered data if valid, false otherwise. - * - * @since 3.1 - */ - public function validate($data, $view = null) - { - // Get the form. - $form = $this->getForm($view); - - // Check for an error. - if ($form === false) - { - return false; - } - - // Filter and validate the form data. - $data = $form->filter($data); - $return = $form->validate($data); - - // Check for an error. - if ($return instanceof \Exception) - { - Factory::getApplication()->enqueueMessage($return->getMessage(), 'warning'); - - return false; - } - - // Check the validation results. - if ($return === false) - { - // Get the validation messages from the form. - $messages = array_reverse($form->getErrors()); - - foreach ($messages as $message) - { - if ($message instanceof \Exception) - { - Factory::getApplication()->enqueueMessage($message->getMessage(), 'warning'); - } - else - { - Factory::getApplication()->enqueueMessage($message, 'warning'); - } - } - - return false; - } - - return $data; - } - - /** - * Method to validate the db connection properties. - * - * @return boolean - * - * @since 4.0.0 - * @throws \Exception - */ - public function validateDbConnection() - { - $options = $this->getOptions(); - - // Get the options as an object for easier handling. - $options = ArrayHelper::toObject($options); - - // Load the backend language files so that the DB error messages work. - $lang = Factory::getLanguage(); - $currentLang = $lang->getTag(); - - // Load the selected language - if (LanguageHelper::exists($currentLang, JPATH_ADMINISTRATOR)) - { - $lang->load('joomla', JPATH_ADMINISTRATOR, $currentLang, true); - } - // Pre-load en-GB in case the chosen language files do not exist. - else - { - $lang->load('joomla', JPATH_ADMINISTRATOR, 'en-GB', true); - } - - // Validate and clean up connection parameters - $paramsCheck = DatabaseHelper::validateConnectionParameters($options); - - if ($paramsCheck) - { - // Validation error: Enqueue the error message - Factory::getApplication()->enqueueMessage($paramsCheck, 'error'); - - return false; - } - - // Security check for remote db hosts - if (!DatabaseHelper::checkRemoteDbHost($options)) - { - // Messages have been enqueued in the called function. - return false; - } - - // Get a database object. - try - { - $db = DatabaseHelper::getDbo( - $options->db_type, - $options->db_host, - $options->db_user, - $options->db_pass_plain, - $options->db_name, - $options->db_prefix, - false, - DatabaseHelper::getEncryptionSettings($options) - ); - - $db->connect(); - } - catch (\RuntimeException $e) - { - if ($options->db_type === 'mysql' && strpos($e->getMessage(), '[1049] Unknown database') === 42 - || $options->db_type === 'pgsql' && strpos($e->getMessage(), 'database "' . $options->db_name . '" does not exist')) - { - // Database doesn't exist: Skip the below checks, they will be done later at database creation - return true; - } - - Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_DATABASE_COULD_NOT_CONNECT', $e->getMessage()), 'error'); - - return false; - } - - // Check database server parameters - $dbServerCheck = DatabaseHelper::checkDbServerParameters($db, $options); - - if ($dbServerCheck) - { - // Some server parameter is not ok: Enqueue the error message - Factory::getApplication()->enqueueMessage($dbServerCheck, 'error'); - - return false; - } - - return true; - } + /** + * Get the current setup options from the session. + * + * @return array An array of options from the session. + * + * @since 3.1 + */ + public function getOptions() + { + if (!empty(Factory::getSession()->get('setup.options', array()))) { + return Factory::getSession()->get('setup.options', array()); + } + } + + /** + * Store the current setup options in the session. + * + * @param array $options The installation options. + * + * @return array An array of options from the session. + * + * @since 3.1 + */ + public function storeOptions($options) + { + // Get the current setup options from the session. + $old = (array) $this->getOptions(); + + // Ensure that we have language + if (!isset($options['language']) || empty($options['language'])) { + $options['language'] = Factory::getLanguage()->getTag(); + } + + // Store passwords as a separate key that is not used in the forms + foreach (array('admin_password', 'db_pass') as $passwordField) { + if (isset($options[$passwordField])) { + $plainTextKey = $passwordField . '_plain'; + + $options[$plainTextKey] = $options[$passwordField]; + + unset($options[$passwordField]); + } + } + + // Get the session + $session = Factory::getSession(); + $options['helpurl'] = $session->get('setup.helpurl', null); + + // Merge the new setup options into the current ones and store in the session. + $options = array_merge($old, (array) $options); + $session->set('setup.options', $options); + + return $options; + } + + /** + * Method to get the form. + * + * @param string|null $view The view being processed. + * + * @return Form|boolean JForm object on success, false on failure. + * + * @since 3.1 + */ + public function getForm($view = null) + { + if (!$view) { + $view = Factory::getApplication()->input->getWord('view', 'setup'); + } + + // Get the form. + Form::addFormPath(JPATH_COMPONENT . '/forms'); + + try { + $form = Form::getInstance('jform', $view, array('control' => 'jform')); + } catch (\Exception $e) { + Factory::getApplication()->enqueueMessage($e->getMessage(), 'error'); + + return false; + } + + // Check the session for previously entered form data. + $data = (array) $this->getOptions(); + + // Bind the form data if present. + if (!empty($data)) { + $form->bind($data); + } + + return $form; + } + + /** + * Method to check the form data. + * + * @param string $page The view being checked. + * + * @return array|boolean Array with the validated form data or boolean false on a validation failure. + * + * @since 3.1 + */ + public function checkForm($page = 'setup') + { + // Get the posted values from the request and validate them. + $data = Factory::getApplication()->input->post->get('jform', array(), 'array'); + $return = $this->validate($data, $page); + + // Attempt to save the data before validation. + $form = $this->getForm(); + $data = $form->filter($data); + + $this->storeOptions($data); + + // Check for validation errors. + if ($return === false) { + return false; + } + + // Store the options in the session. + return $this->storeOptions($return); + } + + /** + * Generate a panel of language choices for the user to select their language. + * + * @return array + * + * @since 3.1 + */ + public function getLanguages() + { + // Detect the native language. + $native = LanguageHelper::detectLanguage(); + + if (empty($native)) { + $native = 'en-GB'; + } + + // Get a forced language if it exists. + $forced = Factory::getApplication()->getLocalise(); + + if (!empty($forced['language'])) { + $native = $forced['language']; + } + + // Get the list of available languages. + $list = LanguageHelper::createLanguageList($native); + + if (!$list || $list instanceof \Exception) { + $list = array(); + } + + return $list; + } + + /** + * Method to validate the form data. + * + * @param array $data The form data. + * @param string|null $view The view. + * + * @return array|boolean Array of filtered data if valid, false otherwise. + * + * @since 3.1 + */ + public function validate($data, $view = null) + { + // Get the form. + $form = $this->getForm($view); + + // Check for an error. + if ($form === false) { + return false; + } + + // Filter and validate the form data. + $data = $form->filter($data); + $return = $form->validate($data); + + // Check for an error. + if ($return instanceof \Exception) { + Factory::getApplication()->enqueueMessage($return->getMessage(), 'warning'); + + return false; + } + + // Check the validation results. + if ($return === false) { + // Get the validation messages from the form. + $messages = array_reverse($form->getErrors()); + + foreach ($messages as $message) { + if ($message instanceof \Exception) { + Factory::getApplication()->enqueueMessage($message->getMessage(), 'warning'); + } else { + Factory::getApplication()->enqueueMessage($message, 'warning'); + } + } + + return false; + } + + return $data; + } + + /** + * Method to validate the db connection properties. + * + * @return boolean + * + * @since 4.0.0 + * @throws \Exception + */ + public function validateDbConnection() + { + $options = $this->getOptions(); + + // Get the options as an object for easier handling. + $options = ArrayHelper::toObject($options); + + // Load the backend language files so that the DB error messages work. + $lang = Factory::getLanguage(); + $currentLang = $lang->getTag(); + + // Load the selected language + if (LanguageHelper::exists($currentLang, JPATH_ADMINISTRATOR)) { + $lang->load('joomla', JPATH_ADMINISTRATOR, $currentLang, true); + } else { + // Pre-load en-GB in case the chosen language files do not exist. + $lang->load('joomla', JPATH_ADMINISTRATOR, 'en-GB', true); + } + + // Validate and clean up connection parameters + $paramsCheck = DatabaseHelper::validateConnectionParameters($options); + + if ($paramsCheck) { + // Validation error: Enqueue the error message + Factory::getApplication()->enqueueMessage($paramsCheck, 'error'); + + return false; + } + + // Security check for remote db hosts + if (!DatabaseHelper::checkRemoteDbHost($options)) { + // Messages have been enqueued in the called function. + return false; + } + + // Get a database object. + try { + $db = DatabaseHelper::getDbo( + $options->db_type, + $options->db_host, + $options->db_user, + $options->db_pass_plain, + $options->db_name, + $options->db_prefix, + false, + DatabaseHelper::getEncryptionSettings($options) + ); + + $db->connect(); + } catch (\RuntimeException $e) { + if ( + $options->db_type === 'mysql' && strpos($e->getMessage(), '[1049] Unknown database') === 42 + || $options->db_type === 'pgsql' && strpos($e->getMessage(), 'database "' . $options->db_name . '" does not exist') + ) { + // Database doesn't exist: Skip the below checks, they will be done later at database creation + return true; + } + + Factory::getApplication()->enqueueMessage(Text::sprintf('INSTL_DATABASE_COULD_NOT_CONNECT', $e->getMessage()), 'error'); + + return false; + } + + // Check database server parameters + $dbServerCheck = DatabaseHelper::checkDbServerParameters($db, $options); + + if ($dbServerCheck) { + // Some server parameter is not ok: Enqueue the error message + Factory::getApplication()->enqueueMessage($dbServerCheck, 'error'); + + return false; + } + + return true; + } } diff --git a/installation/src/Response/JsonResponse.php b/installation/src/Response/JsonResponse.php index 5271d4e153799..441773618013c 100644 --- a/installation/src/Response/JsonResponse.php +++ b/installation/src/Response/JsonResponse.php @@ -1,4 +1,5 @@ token = Session::getFormToken(true); + /** + * Constructor for the JSON response + * + * @param mixed $data Exception if there is an error, otherwise, the session data + * + * @since 3.1 + */ + public function __construct($data) + { + // The old token is invalid so send a new one. + $this->token = Session::getFormToken(true); - // Get the language and send its tag along - $this->lang = Factory::getLanguage()->getTag(); + // Get the language and send its tag along + $this->lang = Factory::getLanguage()->getTag(); - // Get the message queue - $messages = Factory::getApplication()->getMessageQueue(); + // Get the message queue + $messages = Factory::getApplication()->getMessageQueue(); - // Build the sorted message list - if (is_array($messages) && count($messages)) - { - foreach ($messages as $msg) - { - if (isset($msg['type'], $msg['message'])) - { - $lists[$msg['type']][] = $msg['message']; - } - } - } + // Build the sorted message list + if (is_array($messages) && count($messages)) { + foreach ($messages as $msg) { + if (isset($msg['type'], $msg['message'])) { + $lists[$msg['type']][] = $msg['message']; + } + } + } - // If messages exist add them to the output - if (isset($lists) && is_array($lists)) - { - $this->messages = $lists; - } + // If messages exist add them to the output + if (isset($lists) && is_array($lists)) { + $this->messages = $lists; + } - // Check if we are dealing with an error. - if ($data instanceof \Throwable) - { - // Prepare the error response. - $this->error = true; - $this->header = Text::_('INSTL_HEADER_ERROR'); - $this->message = $data->getMessage(); - } - else - { - // Prepare the response data. - $this->error = false; - $this->data = $data; - } - } + // Check if we are dealing with an error. + if ($data instanceof \Throwable) { + // Prepare the error response. + $this->error = true; + $this->header = Text::_('INSTL_HEADER_ERROR'); + $this->message = $data->getMessage(); + } else { + // Prepare the response data. + $this->error = false; + $this->data = $data; + } + } } diff --git a/installation/src/Router/InstallationRouter.php b/installation/src/Router/InstallationRouter.php index 662eb898f4eca..13be5ac024e06 100644 --- a/installation/src/Router/InstallationRouter.php +++ b/installation/src/Router/InstallationRouter.php @@ -1,4 +1,5 @@ share( - InstallationApplication::class, - function (Container $container) - { - $app = new InstallationApplication(null, $container->get('config'), null, $container); + /** + * Registers the service provider with a DI container. + * + * @param Container $container The DI container. + * + * @return void + * + * @since 4.0.0 + */ + public function register(Container $container) + { + $container->share( + InstallationApplication::class, + function (Container $container) { + $app = new InstallationApplication(null, $container->get('config'), null, $container); - // The session service provider needs Factory::$application, set it if still null - if (Factory::$application === null) - { - Factory::$application = $app; - } + // The session service provider needs Factory::$application, set it if still null + if (Factory::$application === null) { + Factory::$application = $app; + } - $app->setDispatcher($container->get('Joomla\Event\DispatcherInterface')); - $app->setLogger($container->get(LoggerInterface::class)); - $app->setSession($container->get('Joomla\Session\SessionInterface')); + $app->setDispatcher($container->get('Joomla\Event\DispatcherInterface')); + $app->setLogger($container->get(LoggerInterface::class)); + $app->setSession($container->get('Joomla\Session\SessionInterface')); - return $app; - }, - true - ); + return $app; + }, + true + ); - // Inject a custom JSON error renderer - $container->share( - JsonRenderer::class, - function (Container $container) - { - return new \Joomla\CMS\Installation\Error\Renderer\JsonRenderer; - } - ); - } + // Inject a custom JSON error renderer + $container->share( + JsonRenderer::class, + function (Container $container) { + return new \Joomla\CMS\Installation\Error\Renderer\JsonRenderer(); + } + ); + } } diff --git a/installation/src/View/DefaultView.php b/installation/src/View/DefaultView.php index 127ecbe37a9b0..239a2f9d0edde 100644 --- a/installation/src/View/DefaultView.php +++ b/installation/src/View/DefaultView.php @@ -1,4 +1,5 @@ form = $this->get('Form'); + /** + * Execute and display a template script. + * + * @param string|null $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->form = $this->get('Form'); - parent::display($tpl); - } + parent::display($tpl); + } } diff --git a/installation/src/View/Error/HtmlView.php b/installation/src/View/Error/HtmlView.php index dbbfe15dfd15c..fa806b2b3c245 100644 --- a/installation/src/View/Error/HtmlView.php +++ b/installation/src/View/Error/HtmlView.php @@ -1,4 +1,5 @@ options = $this->get('PhpOptions'); + /** + * Execute and display a template script. + * + * @param string|null $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->options = $this->get('PhpOptions'); - parent::display($tpl); - } + parent::display($tpl); + } } diff --git a/installation/src/View/Remove/HtmlView.php b/installation/src/View/Remove/HtmlView.php index 53499f3721b5d..06e8a0fbce518 100644 --- a/installation/src/View/Remove/HtmlView.php +++ b/installation/src/View/Remove/HtmlView.php @@ -1,4 +1,5 @@ development = (new Version)->isInDevelopmentState(); + /** + * Execute and display a template script. + * + * @param string|null $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @since 4.0.0 + */ + public function display($tpl = null) + { + $this->development = (new Version())->isInDevelopmentState(); - $this->items = $this->get('Items', 'Languages'); + $this->items = $this->get('Items', 'Languages'); - $this->installed_languages = new \stdClass; - $this->installed_languages->administrator = $this->get('InstalledlangsAdministrator', 'Languages'); - $this->installed_languages->frontend = $this->get('InstalledlangsFrontend', 'Languages'); + $this->installed_languages = new \stdClass(); + $this->installed_languages->administrator = $this->get('InstalledlangsAdministrator', 'Languages'); + $this->installed_languages->frontend = $this->get('InstalledlangsFrontend', 'Languages'); - $this->phpoptions = $this->get('PhpOptions', 'Checks'); - $this->phpsettings = $this->get('PhpSettings', 'Checks'); + $this->phpoptions = $this->get('PhpOptions', 'Checks'); + $this->phpsettings = $this->get('PhpSettings', 'Checks'); - parent::display($tpl); - } + parent::display($tpl); + } } diff --git a/installation/src/View/Setup/HtmlView.php b/installation/src/View/Setup/HtmlView.php index fe0e3200aa996..12e662784a5e8 100644 --- a/installation/src/View/Setup/HtmlView.php +++ b/installation/src/View/Setup/HtmlView.php @@ -1,4 +1,5 @@ - * @license GNU General Public License version 2 or later; see LICENSE.txt + * @license GNU General Public License version 2 or later; see LICENSE.txt */ defined('_JEXEC') or die; @@ -14,13 +15,13 @@ /** @var \Joomla\CMS\Document\ErrorDocument $this */ // Add required assets $this->getWebAssetManager() - ->registerAndUseStyle('template.installation', 'template' . ($this->direction === 'rtl' ? '-rtl' : '') . '.css') - ->useScript('core') - ->registerAndUseScript('template.installation', 'installation/template/js/template.js', [], [], ['core']); + ->registerAndUseStyle('template.installation', 'template' . ($this->direction === 'rtl' ? '-rtl' : '') . '.css') + ->useScript('core') + ->registerAndUseScript('template.installation', 'installation/template/js/template.js', [], [], ['core']); $this->getWebAssetManager() - ->useStyle('webcomponent.joomla-alert') - ->useScript('messages'); + ->useStyle('webcomponent.joomla-alert') + ->useScript('messages'); // Add script options $this->addScriptOptions('system.installation', ['url' => Route::_('index.php')]); @@ -33,77 +34,77 @@ ?> - - - - - -
    - - - -
    - -
    - -
    -
    -
    -
    - -
    -
    -
    -
    - -
    -
    -

    -

    error->getCode(); ?> error->getMessage(), ENT_QUOTES, 'UTF-8'); ?>

    -
    -
    - debug) : ?> -
    - renderBacktrace(); ?> - - error->getPrevious()) : ?> - - _error here and in the loop as setError() assigns errors to this property and we need this for the backtrace to work correctly ?> - - setError($this->_error->getPrevious()); ?> - -

    -

    _error->getMessage(), ENT_QUOTES, 'UTF-8'); ?>

    - renderBacktrace(); ?> - setError($this->_error->getPrevious()); ?> - - - setError($this->error); ?> - -
    - -
    -
    -
    -
    - - -
    - + + + + + +
    + + + +
    + +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +

    +

    error->getCode(); ?> error->getMessage(), ENT_QUOTES, 'UTF-8'); ?>

    +
    +
    + debug) : ?> +
    + renderBacktrace(); ?> + + error->getPrevious()) : ?> + + _error here and in the loop as setError() assigns errors to this property and we need this for the backtrace to work correctly ?> + + setError($this->_error->getPrevious()); ?> + +

    +

    _error->getMessage(), ENT_QUOTES, 'UTF-8'); ?>

    + renderBacktrace(); ?> + setError($this->_error->getPrevious()); ?> + + + setError($this->error); ?> + +
    + +
    +
    +
    +
    + + +
    + diff --git a/installation/template/index.php b/installation/template/index.php index b670c113aa76d..75e34d7659f6c 100644 --- a/installation/template/index.php +++ b/installation/template/index.php @@ -1,9 +1,10 @@ - * @license GNU General Public License version 2 or later; see LICENSE.txt + * @license GNU General Public License version 2 or later; see LICENSE.txt */ defined('_JEXEC') or die; @@ -16,17 +17,17 @@ /** @var \Joomla\CMS\Document\HtmlDocument $this */ // Add required assets $this->getWebAssetManager() - ->registerAndUseStyle('template.installation', 'installation/template/css/template' . ($this->direction === 'rtl' ? '-rtl' : '') . '.css', ['version' => 'auto'], [], []) - ->useScript('core') - ->useScript('keepalive') - ->useScript('form.validate') - ->registerAndUseScript('template.installation', 'installation/template/js/template.js', ['version' => 'auto'], ['defer' => true], ['core', 'form.validate']); + ->registerAndUseStyle('template.installation', 'installation/template/css/template' . ($this->direction === 'rtl' ? '-rtl' : '') . '.css', ['version' => 'auto'], [], []) + ->useScript('core') + ->useScript('keepalive') + ->useScript('form.validate') + ->registerAndUseScript('template.installation', 'installation/template/js/template.js', ['version' => 'auto'], ['defer' => true], ['core', 'form.validate']); $this->getWebAssetManager() - ->useStyle('webcomponent.joomla-alert') - ->useScript('messages') - ->useScript('webcomponent.core-loader') - ->addInlineStyle(':root { + ->useStyle('webcomponent.joomla-alert') + ->useScript('messages') + ->useScript('webcomponent.core-loader') + ->addInlineStyle(':root { --hue: 214; --template-bg-light: #f0f4fb; --template-text-dark: #495057; @@ -61,65 +62,65 @@ ?> - - - - - - -
    - - - -
    -
    -
    -
    -
    - -
    - -
    - -
    -
    -
    - -
    -
    -
    -
    - -
    - + + + + + + +
    + + + +
    +
    +
    +
    +
    + +
    + +
    + +
    +
    +
    + +
    +
    +
    +
    + +
    + diff --git a/installation/tmpl/error/default.php b/installation/tmpl/error/default.php index 53e4239ea0e97..0bbde2a434c0f 100644 --- a/installation/tmpl/error/default.php +++ b/installation/tmpl/error/default.php @@ -1,4 +1,5 @@
    -
    -
    -
    -
    - -
    -
    - options as $option) : ?> - state === 'JNO' || $option->state === false) : ?> -
    -
    - -
    -
    - label; ?> -

    notice; ?>

    -
    -
    - - -
    -
    -
    -
    +
    +
    +
    +
    + +
    +
    + options as $option) : ?> + state === 'JNO' || $option->state === false) : ?> +
    +
    + +
    +
    + label; ?> +

    notice; ?>

    +
    +
    + + +
    +
    +
    +
    diff --git a/installation/tmpl/remove/default.php b/installation/tmpl/remove/default.php index 804a64d05a7fe..e9ebcdcdd7a14 100644 --- a/installation/tmpl/remove/default.php +++ b/installation/tmpl/remove/default.php @@ -1,4 +1,5 @@
    -
    - - - -
    -

    -
    - -
    -
    +
    + + + +
    +

    +
    + +
    +
    - installed_languages->administrator) > 1) : ?> -
    - -
    - -

    - - - - - - - - - - installed_languages->administrator as $i => $lang) : ?> - - - - - - - -
    - - - - - -
    - published) echo 'checked="checked"'; ?> - /> - - - - language; ?> -
    -

    - - - - - - - - - - installed_languages->frontend as $i => $lang) : ?> - - - - - - - -
    - - - - - -
    - published) echo 'checked="checked"'; ?> - /> - - - - language; ?> -
    - - -
    -
    + installed_languages->administrator) > 1) : ?> +
    + +
    + +

    + + + + + + + + + + installed_languages->administrator as $i => $lang) : ?> + + + + + + + +
    + + + + + +
    + published) { + echo 'checked="checked"'; + } ?> + /> + + + + language; ?> +
    +

    + + + + + + + + + + installed_languages->frontend as $i => $lang) : ?> + + + + + + + +
    + + + + + +
    + published) { + echo 'checked="checked"'; + } ?> + /> + + + + language; ?> +
    + + +
    +
    -
    -
    - - phpsettings as $setting) : ?> - state !== $setting->recommended) : ?> - - - - - - - - - - - - - - - phpsettings as $setting) : ?> - state !== $setting->recommended) : ?> - - - - - - - - -
    - -
    - - - - - -
    - label; ?> - - - recommended ? 'JON' : 'JOFF'); ?> - - - - state ? 'JON' : 'JOFF'); ?> - -
    +
    +
    + + phpsettings as $setting) : ?> + state !== $setting->recommended) : ?> + + + + + + + + + + + + + + + phpsettings as $setting) : ?> + state !== $setting->recommended) : ?> + + + + + + + + +
    + +
    + + + + + +
    + label; ?> + + + recommended ? 'JON' : 'JOFF'); ?> + + + + state ? 'JON' : 'JOFF'); ?> + +
    - - development) : ?> -
    - - -
    - - + + development) : ?> +
    + + +
    + + -
    - - -
    -
    -
    +
    + + +
    +
    +
    -
    - - - -
    - items) : ?> -

    -

    - - - - -

    -

    - -
    - -

    - - - - - - - - - - - - - getShortVersion()); ?> - items as $i => $language) : ?> - - element, $element); ?> - code = $element[1]; ?> - - - - - - - - -
    - - - - - - - -
    - - - - - code; ?> - - - version, 0, 3) != $version::MAJOR_VERSION . '.' . $version::MINOR_VERSION || substr($language->version, 0, 5) != $currentShortVersion) : ?> - version; ?> - - version; ?> - -
    - - -
    - - -
    -
    -
    -
    +
    + + + +
    + items) : ?> +

    +

    + + + + +

    +

    + +
    + +

    + + + + + + + + + + + + + getShortVersion()); ?> + items as $i => $language) : ?> + + element, $element); ?> + code = $element[1]; ?> + + + + + + + + +
    + + + + + + + +
    + + + + + code; ?> + + + version, 0, 3) != $version::MAJOR_VERSION . '.' . $version::MINOR_VERSION || substr($language->version, 0, 5) != $currentShortVersion) : ?> + version; ?> + + version; ?> + +
    + + +
    + + +
    +
    +
    +
    -
    - - - -
    -

    -
    -
    +
    + + + +
    +

    +
    +
    diff --git a/installation/tmpl/setup/default.php b/installation/tmpl/setup/default.php index 628163e1f8b01..2cdf7b4adc4ff 100644 --- a/installation/tmpl/setup/default.php +++ b/installation/tmpl/setup/default.php @@ -1,4 +1,5 @@
    -
    -
    - - - -
    -
    - form->renderField('language'); ?> -
    - - - -
    -
    -
    -
    -
    - - - -
    -
    - form->renderField('site_name'); ?> -
    -
    - -
    -
    -
    -
    - - - -
    -
    - form->renderField('admin_user'); ?> -
    -
    - form->renderField('admin_username'); ?> -
    -
    - form->renderField('admin_password'); ?> -
    -
    - form->renderField('admin_email'); ?> -
    -
    - -
    -
    -
    -
    - - - -
    -
    - form->renderField('db_type'); ?> -
    -
    - form->renderField('db_host'); ?> -
    -
    - form->renderField('db_user'); ?> -
    -
    - form->renderField('db_pass'); ?> -
    -
    - form->renderField('db_name'); ?> -
    -
    - form->renderField('db_prefix'); ?> -
    -
    - form->renderField('db_encryption'); ?> -
    -
    - form->renderField('db_sslkey'); ?> -
    -
    - form->renderField('db_sslcert'); ?> -
    -
    - form->renderField('db_sslverifyservercert'); ?> -
    -
    - form->renderField('db_sslca'); ?> -
    -
    - form->renderField('db_sslcipher'); ?> -
    -
    - form->getLabel('db_old'); ?> - form->getInput('db_old'); ?> -
    -
    - -
    -
    -
    + +
    + + + +
    +
    + form->renderField('language'); ?> +
    + + + +
    +
    +
    +
    +
    + + + +
    +
    + form->renderField('site_name'); ?> +
    +
    + +
    +
    +
    +
    + + + +
    +
    + form->renderField('admin_user'); ?> +
    +
    + form->renderField('admin_username'); ?> +
    +
    + form->renderField('admin_password'); ?> +
    +
    + form->renderField('admin_email'); ?> +
    +
    + +
    +
    +
    +
    + + + +
    +
    + form->renderField('db_type'); ?> +
    +
    + form->renderField('db_host'); ?> +
    +
    + form->renderField('db_user'); ?> +
    +
    + form->renderField('db_pass'); ?> +
    +
    + form->renderField('db_name'); ?> +
    +
    + form->renderField('db_prefix'); ?> +
    +
    + form->renderField('db_encryption'); ?> +
    +
    + form->renderField('db_sslkey'); ?> +
    +
    + form->renderField('db_sslcert'); ?> +
    +
    + form->renderField('db_sslverifyservercert'); ?> +
    +
    + form->renderField('db_sslca'); ?> +
    +
    + form->renderField('db_sslcipher'); ?> +
    +
    + form->getLabel('db_old'); ?> + form->getInput('db_old'); ?> +
    +
    + +
    +
    +
    - - -
    + + +
    diff --git a/language/en-GB/localise.php b/language/en-GB/localise.php index 8b46cfdc49ff1..fecaea3d20719 100644 --- a/language/en-GB/localise.php +++ b/language/en-GB/localise.php @@ -1,12 +1,17 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ -defined('_JEXEC') or die; + /** * en-GB localise class. @@ -15,28 +20,23 @@ */ abstract class En_GBLocalise { - /** - * Returns the potential suffixes for a specific number of items - * - * @param integer $count The number of items. - * - * @return array An array of potential suffixes. - * - * @since 1.6 - */ - public static function getPluralSuffixes($count) - { - if ($count == 0) - { - return array('0'); - } - elseif ($count == 1) - { - return array('ONE', '1'); - } - else - { - return array('OTHER', 'MORE'); - } - } + /** + * Returns the potential suffixes for a specific number of items + * + * @param integer $count The number of items. + * + * @return array An array of potential suffixes. + * + * @since 1.6 + */ + public static function getPluralSuffixes($count) + { + if ($count == 0) { + return array('0'); + } elseif ($count == 1) { + return array('ONE', '1'); + } else { + return array('OTHER', 'MORE'); + } + } } diff --git a/layouts/chromes/html5.php b/layouts/chromes/html5.php index 8979e47b6e9d9..092d469f5bc93 100644 --- a/layouts/chromes/html5.php +++ b/layouts/chromes/html5.php @@ -1,4 +1,5 @@ content === '') -{ - return; +if ((string) $module->content === '') { + return; } $moduleTag = htmlspecialchars($params->get('module_tag', 'div'), ENT_QUOTES, 'UTF-8'); @@ -31,27 +31,25 @@ $headerAttribs = []; // Only output a header class if one is set -if ($headerClass !== '') -{ - $headerAttribs['class'] = $headerClass; +if ($headerClass !== '') { + $headerAttribs['class'] = $headerClass; } // Only add aria if the moduleTag is not a div -if ($moduleTag !== 'div') -{ - if ($module->showtitle) : - $moduleAttribs['aria-labelledby'] = 'mod-' . $module->id; - $headerAttribs['id'] = 'mod-' . $module->id; - else: - $moduleAttribs['aria-label'] = $module->title; - endif; +if ($moduleTag !== 'div') { + if ($module->showtitle) : + $moduleAttribs['aria-labelledby'] = 'mod-' . $module->id; + $headerAttribs['id'] = 'mod-' . $module->id; + else : + $moduleAttribs['aria-label'] = $module->title; + endif; } $header = '<' . $headerTag . ' ' . ArrayHelper::toString($headerAttribs) . '>' . $module->title . ''; ?> < > - showtitle) : ?> - - - content; ?> + showtitle) : ?> + + + content; ?> > diff --git a/layouts/chromes/none.php b/layouts/chromes/none.php index aeab58cab272c..8c8c4077a34f1 100644 --- a/layouts/chromes/none.php +++ b/layouts/chromes/none.php @@ -1,4 +1,5 @@ getDocument() - ->getWebAssetManager() - ->registerAndUseStyle('layouts.chromes.outline', 'layouts/chromes/outline.css'); + ->getWebAssetManager() + ->registerAndUseStyle('layouts.chromes.outline', 'layouts/chromes/outline.css'); $module = $displayData['module']; ?>
    -
    -
    - position); ?> -
    -
    - style); ?> -
    -
    -
    - content; ?> -
    +
    +
    + position); ?> +
    +
    + style); ?> +
    +
    +
    + content; ?> +
    diff --git a/layouts/chromes/table.php b/layouts/chromes/table.php index 7d0fc8fe5b0e3..b19b8c1c2dbfe 100644 --- a/layouts/chromes/table.php +++ b/layouts/chromes/table.php @@ -1,4 +1,5 @@ - showtitle) : ?> - - - - - - - + class="moduletable get('moduleclass_sfx'), ENT_COMPAT, 'UTF-8'); ?>"> + showtitle) : ?> + + + + + + +
    - title; ?> -
    - content; ?> -
    + title; ?> +
    + content; ?> +
    diff --git a/layouts/joomla/button/action-button.php b/layouts/joomla/button/action-button.php index e3c630e7ca28e..6c90c112ff1b0 100644 --- a/layouts/joomla/button/action-button.php +++ b/layouts/joomla/button/action-button.php @@ -1,4 +1,5 @@ diff --git a/layouts/joomla/button/iconclass.php b/layouts/joomla/button/iconclass.php index 0697d8daa571d..ca0e74e7d3a51 100644 --- a/layouts/joomla/button/iconclass.php +++ b/layouts/joomla/button/iconclass.php @@ -1,4 +1,5 @@ -
    - - - - escape($options['title'])), - HTMLHelper::_('select.option', '-1', '--------', ['disable' => true]) - ]; +
    + + + + escape($options['title'])), + HTMLHelper::_('select.option', '-1', '--------', ['disable' => true]) + ]; - $transitions = array_merge($default, $options['transitions']); + $transitions = array_merge($default, $options['transitions']); - $attribs = [ - 'id' => 'transition-select_' . (int) $row ?? '', - 'list.attr' => [ - 'class' => 'form-select form-select-sm w-auto', - 'onchange' => "this.form.transition_id.value=this.value;Joomla.listItemTask('" . $checkboxName . $this->escape($row ?? '') . "', '" . $task . "')"] - ]; + $attribs = [ + 'id' => 'transition-select_' . (int) $row ?? '', + 'list.attr' => [ + 'class' => 'form-select form-select-sm w-auto', + 'onchange' => "this.form.transition_id.value=this.value;Joomla.listItemTask('" . $checkboxName . $this->escape($row ?? '') . "', '" . $task . "')"] + ]; - echo HTMLHelper::_('select.genericlist', $transitions, '', $attribs); - ?> -
    + echo HTMLHelper::_('select.genericlist', $transitions, '', $attribs); + ?> +
    diff --git a/layouts/joomla/content/associations.php b/layouts/joomla/content/associations.php index 57ebb493ea261..9104f74e96210 100644 --- a/layouts/joomla/content/associations.php +++ b/layouts/joomla/content/associations.php @@ -1,4 +1,5 @@ -
      - $item) : ?> - -
    • - -
    • - link)) : ?> -
    • - link; ?> -
    • - - -
    +
      + $item) : ?> + +
    • + +
    • + link)) : ?> +
    • + link; ?> +
    • + + +
    diff --git a/layouts/joomla/content/blog_style_default_item_title.php b/layouts/joomla/content/blog_style_default_item_title.php index b2a5ec482e854..7a3128e50f058 100644 --- a/layouts/joomla/content/blog_style_default_item_title.php +++ b/layouts/joomla/content/blog_style_default_item_title.php @@ -1,4 +1,5 @@ params->get('access-edit'); $currentDate = Factory::getDate()->format('Y-m-d H:i:s'); +$link = RouteHelper::getArticleRoute($displayData->slug, $displayData->catid, $displayData->language); ?> -state == 0 || $params->get('show_title') || ($params->get('show_author') && !empty($displayData->author ))) : ?> - +state == 0 || $params->get('show_title') || ($params->get('show_author') && !empty($displayData->author))) : ?> + diff --git a/layouts/joomla/content/categories_default.php b/layouts/joomla/content/categories_default.php index baab179cba025..6d1b30e84f9bd 100644 --- a/layouts/joomla/content/categories_default.php +++ b/layouts/joomla/content/categories_default.php @@ -1,4 +1,5 @@ params->get('show_page_heading')) : ?>

    - escape($displayData->params->get('page_heading')); ?> + escape($displayData->params->get('page_heading')); ?>

    params->get('show_base_description')) : ?> - - params->get('categories_description')) : ?> -
    - params->get('categories_description'), '', $displayData->get('extension') . '.categories'); ?> -
    - - - parent->description) : ?> -
    - parent->description, '', $displayData->parent->extension . '.categories'); ?> -
    - - + + params->get('categories_description')) : ?> +
    + params->get('categories_description'), '', $displayData->get('extension') . '.categories'); ?> +
    + + + parent->description) : ?> +
    + parent->description, '', $displayData->parent->extension . '.categories'); ?> +
    + + diff --git a/layouts/joomla/content/categories_default_items.php b/layouts/joomla/content/categories_default_items.php index 49e820c9b7c43..8cf8681b71a03 100644 --- a/layouts/joomla/content/categories_default_items.php +++ b/layouts/joomla/content/categories_default_items.php @@ -1,4 +1,5 @@ extension; $canEdit = $params->get('access-edit'); $className = substr($extension, 4); -$htag = $params->get('show_page_heading') ? 'h2' : 'h1'; +$htag = $params->get('show_page_heading') ? 'h2' : 'h1'; $app = Factory::getApplication(); @@ -44,59 +45,58 @@ * This will work for the core components but not necessarily for other components * that may have different pluralisation rules. */ -if (substr($className, -1) === 's') -{ - $className = rtrim($className, 's'); +if (substr($className, -1) === 's') { + $className = rtrim($className, 's'); } $tagsData = $category->tags->itemTags; ?>
    - get('show_page_heading')) : ?> -

    - escape($params->get('page_heading')); ?> -

    - + get('show_page_heading')) : ?> +

    + escape($params->get('page_heading')); ?> +

    + - get('show_category_title', 1)) : ?> - <> - title, '', $extension . '.category.title'); ?> - > - - + get('show_category_title', 1)) : ?> + <> + title, '', $extension . '.category.title'); ?> + > + + - get('show_cat_tags', 1)) : ?> - - + get('show_cat_tags', 1)) : ?> + + - get('show_description', 1) || $params->def('show_description_image', 1)) : ?> -
    - get('show_description_image') && $category->getParams()->get('image')) : ?> - $category->getParams()->get('image'), - 'alt' => empty($category->getParams()->get('image_alt')) && empty($category->getParams()->get('image_alt_empty')) ? false : $category->getParams()->get('image_alt'), - ] - ); ?> - - - get('show_description') && $category->description) : ?> - description, '', $extension . '.category.description'); ?> - - -
    - - loadTemplate($displayData->subtemplatename); ?> + get('show_description', 1) || $params->def('show_description_image', 1)) : ?> +
    + get('show_description_image') && $category->getParams()->get('image')) : ?> + $category->getParams()->get('image'), + 'alt' => empty($category->getParams()->get('image_alt')) && empty($category->getParams()->get('image_alt_empty')) ? false : $category->getParams()->get('image_alt'), + ] + ); ?> + + + get('show_description') && $category->description) : ?> + description, '', $extension . '.category.description'); ?> + + +
    + + loadTemplate($displayData->subtemplatename); ?> - maxLevel != 0 && $displayData->get('children')) : ?> -
    - get('show_category_heading_title_text', 1) == 1) : ?> -

    - -

    - - loadTemplate('children'); ?> -
    - + maxLevel != 0 && $displayData->get('children')) : ?> +
    + get('show_category_heading_title_text', 1) == 1) : ?> +

    + +

    + + loadTemplate('children'); ?> +
    +
    diff --git a/layouts/joomla/content/emptystate.php b/layouts/joomla/content/emptystate.php index d938227d0691e..e78d3a2752897 100644 --- a/layouts/joomla/content/emptystate.php +++ b/layouts/joomla/content/emptystate.php @@ -1,4 +1,5 @@ input->get('option')); +if (!$textPrefix) { + $textPrefix = strtoupper(Factory::getApplication()->input->get('option')); } $formURL = $displayData['formURL'] ?? ''; @@ -33,32 +33,32 @@
    -
    - -

    -
    -

    - -

    -
    - input->get('tmpl') !== 'component') : ?> - - - - - -
    -
    -
    +
    + +

    +
    +

    + +

    +
    + input->get('tmpl') !== 'component') : ?> + + + + + +
    +
    +
    - + - - - + + +
    diff --git a/layouts/joomla/content/emptystate_module.php b/layouts/joomla/content/emptystate_module.php index c89d4703d8d21..46be472e68f6d 100644 --- a/layouts/joomla/content/emptystate_module.php +++ b/layouts/joomla/content/emptystate_module.php @@ -1,4 +1,5 @@ getLanguage()->hasKey($moduleLangString) ? $moduleLangString : $componentLangString; +if (!$title) { + // Can we find a *_EMPTYSTATE_MODULE_TITLE translation, Else use the components *_EMPTYSTATE_TITLE string + $title = Factory::getApplication()->getLanguage()->hasKey($moduleLangString) ? $moduleLangString : $componentLangString; } ?>
    -

    - -

    +

    + +

    diff --git a/layouts/joomla/content/full_image.php b/layouts/joomla/content/full_image.php index 44b43401b96c8..83fcc57b4489e 100644 --- a/layouts/joomla/content/full_image.php +++ b/layouts/joomla/content/full_image.php @@ -1,4 +1,5 @@ params; $images = json_decode($displayData->images); -if (empty($images->image_fulltext)) -{ - return; +if (empty($images->image_fulltext)) { + return; } $imgclass = empty($images->float_fulltext) ? $params->get('float_fulltext') : $images->float_fulltext; $layoutAttr = [ - 'src' => $images->image_fulltext, - 'itemprop' => 'image', - 'alt' => empty($images->image_fulltext_alt) && empty($images->image_fulltext_alt_empty) ? false : $images->image_fulltext_alt, + 'src' => $images->image_fulltext, + 'itemprop' => 'image', + 'alt' => empty($images->image_fulltext_alt) && empty($images->image_fulltext_alt_empty) ? false : $images->image_fulltext_alt, ]; ?>
    - - image_fulltext_caption) && $images->image_fulltext_caption !== '') : ?> -
    escape($images->image_fulltext_caption); ?>
    - + + image_fulltext_caption) && $images->image_fulltext_caption !== '') : ?> +
    escape($images->image_fulltext_caption); ?>
    +
    diff --git a/layouts/joomla/content/icons.php b/layouts/joomla/content/icons.php index ba1d27136d28e..3369c65429dc4 100644 --- a/layouts/joomla/content/icons.php +++ b/layouts/joomla/content/icons.php @@ -1,4 +1,5 @@ -
    -
    -
    - -
    -
    -
    +
    +
    +
    + +
    +
    +
    diff --git a/layouts/joomla/content/icons/create.php b/layouts/joomla/content/icons/create.php index 74183d7b571f4..2a78ae4c1b034 100644 --- a/layouts/joomla/content/icons/create.php +++ b/layouts/joomla/content/icons/create.php @@ -1,4 +1,5 @@ get('show_icons')) : ?> - - + + - + diff --git a/layouts/joomla/content/icons/edit.php b/layouts/joomla/content/icons/edit.php index fc6702dbed57b..117b232208cb3 100644 --- a/layouts/joomla/content/icons/edit.php +++ b/layouts/joomla/content/icons/edit.php @@ -1,4 +1,5 @@ state ? 'edit' : 'eye-slash'; $currentDate = Factory::getDate()->format('Y-m-d H:i:s'); $isUnpublished = ($article->publish_up > $currentDate) - || !is_null($article->publish_down) && ($article->publish_down < $currentDate); + || !is_null($article->publish_down) && ($article->publish_down < $currentDate); -if ($isUnpublished) -{ - $icon = 'eye-slash'; +if ($isUnpublished) { + $icon = 'eye-slash'; } $aria_described = 'editarticle-' . (int) $article->id; ?> - + diff --git a/layouts/joomla/content/icons/edit_lock.php b/layouts/joomla/content/icons/edit_lock.php index 3b8def519104c..4ebd721fd36c7 100644 --- a/layouts/joomla/content/icons/edit_lock.php +++ b/layouts/joomla/content/icons/edit_lock.php @@ -1,4 +1,5 @@ id; -} -elseif (isset($displayData['contact'])) -{ - $contact = $displayData['contact']; - $aria_described = 'editcontact-' . (int) $contact->id; +if (isset($displayData['ariaDescribed'])) { + $aria_described = $displayData['ariaDescribed']; +} elseif (isset($displayData['article'])) { + $article = $displayData['article']; + $aria_described = 'editarticle-' . (int) $article->id; +} elseif (isset($displayData['contact'])) { + $contact = $displayData['contact']; + $aria_described = 'editcontact-' . (int) $contact->id; } $tooltip = $displayData['tooltip']; ?> - + diff --git a/layouts/joomla/content/info_block.php b/layouts/joomla/content/info_block.php index 402e1179595f2..5a2929993cf23 100644 --- a/layouts/joomla/content/info_block.php +++ b/layouts/joomla/content/info_block.php @@ -1,4 +1,5 @@ diff --git a/layouts/joomla/content/info_block/associations.php b/layouts/joomla/content/info_block/associations.php index 9732c557c3163..cfba66374ac34 100644 --- a/layouts/joomla/content/info_block/associations.php +++ b/layouts/joomla/content/info_block/associations.php @@ -1,4 +1,5 @@ associations)) : ?> -associations; ?> + associations; ?>
    - - - - params->get('flags', 1) && $association['language']->image) : ?> - image . '.gif', $association['language']->title_native, array('title' => $association['language']->title_native), true); ?> - - - lang_code); ?> - lang_code; ?> - title_native; ?> - - - + + + + params->get('flags', 1) && $association['language']->image) : ?> + image . '.gif', $association['language']->title_native, array('title' => $association['language']->title_native), true); ?> + + + lang_code); ?> + lang_code; ?> + title_native; ?> + + +
    diff --git a/layouts/joomla/content/info_block/author.php b/layouts/joomla/content/info_block/author.php index 5d744172c88ab..dcfaf6f851e6e 100644 --- a/layouts/joomla/content/info_block/author.php +++ b/layouts/joomla/content/info_block/author.php @@ -1,4 +1,5 @@ diff --git a/layouts/joomla/content/info_block/category.php b/layouts/joomla/content/info_block/category.php index 2803fe3c91196..4006b0f345fc1 100644 --- a/layouts/joomla/content/info_block/category.php +++ b/layouts/joomla/content/info_block/category.php @@ -1,4 +1,5 @@
    - 'icon-folder-open icon-fw']); ?> - escape($displayData['item']->category_title); ?> - get('link_category') && !empty($displayData['item']->catid)) : ?> - catid, $displayData['item']->category_language) - ) - . '" itemprop="genre">' . $title . ''; ?> - - - ' . $title . ''); ?> - + 'icon-folder-open icon-fw']); ?> + escape($displayData['item']->category_title); ?> + get('link_category') && !empty($displayData['item']->catid)) : ?> + catid, $displayData['item']->category_language) + ) + . '" itemprop="genre">' . $title . ''; ?> + + + ' . $title . ''); ?> +
    diff --git a/layouts/joomla/content/info_block/create_date.php b/layouts/joomla/content/info_block/create_date.php index 75cdc81ca6674..0fed237346ebc 100644 --- a/layouts/joomla/content/info_block/create_date.php +++ b/layouts/joomla/content/info_block/create_date.php @@ -1,4 +1,5 @@
    - - + +
    diff --git a/layouts/joomla/content/info_block/hits.php b/layouts/joomla/content/info_block/hits.php index 8d454f3460590..1ff76321b969b 100644 --- a/layouts/joomla/content/info_block/hits.php +++ b/layouts/joomla/content/info_block/hits.php @@ -1,4 +1,5 @@
    - - - hits); ?> + + + hits); ?>
    diff --git a/layouts/joomla/content/info_block/modify_date.php b/layouts/joomla/content/info_block/modify_date.php index 568b72b01ae3c..5c2aaf90ea1fc 100644 --- a/layouts/joomla/content/info_block/modify_date.php +++ b/layouts/joomla/content/info_block/modify_date.php @@ -1,4 +1,5 @@
    - - + +
    diff --git a/layouts/joomla/content/info_block/parent_category.php b/layouts/joomla/content/info_block/parent_category.php index 663a94674482d..39a3110625ce8 100644 --- a/layouts/joomla/content/info_block/parent_category.php +++ b/layouts/joomla/content/info_block/parent_category.php @@ -1,4 +1,5 @@
    - 'icon-folder icon-fw']); ?> - escape($displayData['item']->parent_title); ?> - get('link_parent_category') && !empty($displayData['item']->parent_id)) : ?> - parent_id, $displayData['item']->parent_language) - ) - . '" itemprop="genre">' . $title . ''; ?> - - - ' . $title . ''); ?> - + 'icon-folder icon-fw']); ?> + escape($displayData['item']->parent_title); ?> + get('link_parent_category') && !empty($displayData['item']->parent_id)) : ?> + parent_id, $displayData['item']->parent_language) + ) + . '" itemprop="genre">' . $title . ''; ?> + + + ' . $title . ''); ?> +
    diff --git a/layouts/joomla/content/info_block/publish_date.php b/layouts/joomla/content/info_block/publish_date.php index 8a528abb756c3..04fbb4492a13c 100644 --- a/layouts/joomla/content/info_block/publish_date.php +++ b/layouts/joomla/content/info_block/publish_date.php @@ -1,4 +1,5 @@
    - - + +
    diff --git a/layouts/joomla/content/intro_image.php b/layouts/joomla/content/intro_image.php index 9c71886774220..f7db3b42a9474 100644 --- a/layouts/joomla/content/intro_image.php +++ b/layouts/joomla/content/intro_image.php @@ -1,4 +1,5 @@ params; $images = json_decode($displayData->images); -if (empty($images->image_intro)) -{ - return; +if (empty($images->image_intro)) { + return; } $imgclass = empty($images->float_intro) ? $params->get('float_intro') : $images->float_intro; $layoutAttr = [ - 'src' => $images->image_intro, - 'alt' => empty($images->image_intro_alt) && empty($images->image_intro_alt_empty) ? false : $images->image_intro_alt, + 'src' => $images->image_intro, + 'alt' => empty($images->image_intro_alt) && empty($images->image_intro_alt_empty) ? false : $images->image_intro_alt, ]; ?>
    - get('link_intro_image') && ($params->get('access-view') || $params->get('show_noauth', '0') == '1')) : ?> - - - 'thumbnail'])); ?> - - image_intro_caption) && $images->image_intro_caption !== '') : ?> -
    escape($images->image_intro_caption); ?>
    - + get('link_intro_image') && ($params->get('access-view') || $params->get('show_noauth', '0') == '1')) : ?> + + + 'thumbnail'])); ?> + + image_intro_caption) && $images->image_intro_caption !== '') : ?> +
    escape($images->image_intro_caption); ?>
    +
    diff --git a/layouts/joomla/content/language.php b/layouts/joomla/content/language.php index b77ac4fd87b1e..90daa395e50f4 100644 --- a/layouts/joomla/content/language.php +++ b/layouts/joomla/content/language.php @@ -1,4 +1,5 @@ language === '*') -{ - echo Text::alt('JALL', 'language'); -} -elseif ($item->language_image) -{ - echo HTMLHelper::_('image', 'mod_languages/' . $item->language_image . '.gif', '', array('class' => 'me-1'), true) . htmlspecialchars($item->language_title, ENT_COMPAT, 'UTF-8'); -} -elseif ($item->language_title) -{ - echo htmlspecialchars($item->language_title, ENT_COMPAT, 'UTF-8'); -} -else -{ - echo Text::_('JUNDEFINED'); +if ($item->language === '*') { + echo Text::alt('JALL', 'language'); +} elseif ($item->language_image) { + echo HTMLHelper::_('image', 'mod_languages/' . $item->language_image . '.gif', '', array('class' => 'me-1'), true) . htmlspecialchars($item->language_title, ENT_COMPAT, 'UTF-8'); +} elseif ($item->language_title) { + echo htmlspecialchars($item->language_title, ENT_COMPAT, 'UTF-8'); +} else { + echo Text::_('JUNDEFINED'); } diff --git a/layouts/joomla/content/options_default.php b/layouts/joomla/content/options_default.php index 77f8f0a431233..9d0981f495d53 100644 --- a/layouts/joomla/content/options_default.php +++ b/layouts/joomla/content/options_default.php @@ -1,4 +1,5 @@
    - name; ?> - description)) : ?> -

    description; ?>

    - - fieldsname); ?> -
    - - form->getFieldset($fieldname) as $field) : ?> - - type === 'Spacer' ? ' field-spacer' : ''; ?> - showon) : ?> - useScript('showon'); ?> - showon, $field->formControl, $field->group)) . '\''; ?> - + name; ?> + description)) : ?> +

    description; ?>

    + + fieldsname); ?> +
    + + form->getFieldset($fieldname) as $field) : ?> + + type === 'Spacer' ? ' field-spacer' : ''; ?> + showon) : ?> + useScript('showon'); ?> + showon, $field->formControl, $field->group)) . '\''; ?> + - showlabel)) : ?> -
    > -
    input; ?>
    -
    - - renderField(); ?> - - - -
    + showlabel)) : ?> +
    > +
    input; ?>
    +
    + + renderField(); ?> + + + +
    diff --git a/layouts/joomla/content/readmore.php b/layouts/joomla/content/readmore.php index 7b4f89e0f1716..53127a969bdbf 100644 --- a/layouts/joomla/content/readmore.php +++ b/layouts/joomla/content/readmore.php @@ -1,4 +1,5 @@

    - get('access-view')) : ?> - - '; ?> - - - alternative_readmore) : ?> - - '; ?> - - get('show_readmore_title', 0) != 0) : ?> - title, $params->get('readmore_limit')); ?> - - - get('show_readmore_title', 0) == 0) : ?> - - '; ?> - - - - - '; ?> - title, $params->get('readmore_limit'))); ?> - - + get('access-view')) : ?> + + '; ?> + + + alternative_readmore) : ?> + + '; ?> + + get('show_readmore_title', 0) != 0) : ?> + title, $params->get('readmore_limit')); ?> + + + get('show_readmore_title', 0) == 0) : ?> + + '; ?> + + + + + '; ?> + title, $params->get('readmore_limit'))); ?> + +

    diff --git a/layouts/joomla/content/tags.php b/layouts/joomla/content/tags.php index d706485fca053..1bef066ea1a85 100644 --- a/layouts/joomla/content/tags.php +++ b/layouts/joomla/content/tags.php @@ -1,4 +1,5 @@ - + diff --git a/layouts/joomla/content/text_filters.php b/layouts/joomla/content/text_filters.php index 215b081f94771..3fc04eaae84db 100644 --- a/layouts/joomla/content/text_filters.php +++ b/layouts/joomla/content/text_filters.php @@ -1,4 +1,5 @@
    - name; ?> -
    - -
    -
    -
    - -
    -
    -
    - -
    -
    -
    - -
    -
    - fieldsname); ?> - - form->getFieldset($fieldname) as $field) : ?> -
    input; ?>
    - - + name; ?> +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    + fieldsname); ?> + + form->getFieldset($fieldname) as $field) : ?> +
    input; ?>
    + +
    diff --git a/layouts/joomla/edit/admin_modules.php b/layouts/joomla/edit/admin_modules.php index f7caf445c3e65..f5d84253ce2de 100644 --- a/layouts/joomla/edit/admin_modules.php +++ b/layouts/joomla/edit/admin_modules.php @@ -1,4 +1,5 @@ input; $fields = $displayData->get('fields') ?: array( - array('parent', 'parent_id'), - array('published', 'state', 'enabled'), - array('category', 'catid'), - 'featured', - 'sticky', - 'access', - 'language', - 'tags', - 'note', - 'version_note', + array('parent', 'parent_id'), + array('published', 'state', 'enabled'), + array('category', 'catid'), + 'featured', + 'sticky', + 'access', + 'language', + 'tags', + 'note', + 'version_note', ); $hiddenFields = $displayData->get('hidden_fields') ?: array(); -if (!ModuleHelper::isAdminMultilang()) -{ - $hiddenFields[] = 'language'; - $form->setFieldAttribute('language', 'default', '*'); +if (!ModuleHelper::isAdminMultilang()) { + $hiddenFields[] = 'language'; + $form->setFieldAttribute('language', 'default', '*'); } $html = array(); $html[] = '
    '; -foreach ($fields as $field) -{ - foreach ((array) $field as $f) - { - if ($form->getField($f)) - { - if (in_array($f, $hiddenFields)) - { - $form->setFieldAttribute($f, 'type', 'hidden'); - } - - $html[] = $form->renderField($f); - break; - } - } +foreach ($fields as $field) { + foreach ((array) $field as $f) { + if ($form->getField($f)) { + if (in_array($f, $hiddenFields)) { + $form->setFieldAttribute($f, 'type', 'hidden'); + } + + $html[] = $form->renderField($f); + break; + } + } } $html[] = '
    '; diff --git a/layouts/joomla/edit/associations.php b/layouts/joomla/edit/associations.php index 96e59f01d2057..423cc21beb674 100644 --- a/layouts/joomla/edit/associations.php +++ b/layouts/joomla/edit/associations.php @@ -1,4 +1,5 @@ getForm(); $options = array( - 'formControl' => $form->getFormControl(), - 'hidden' => (int) ($form->getValue('language', null, '*') === '*'), + 'formControl' => $form->getFormControl(), + 'hidden' => (int) ($form->getValue('language', null, '*') === '*'), ); // Load JavaScript message titles diff --git a/layouts/joomla/edit/fieldset.php b/layouts/joomla/edit/fieldset.php index ab45d78010c97..0a20a3beb8a0a 100644 --- a/layouts/joomla/edit/fieldset.php +++ b/layouts/joomla/edit/fieldset.php @@ -1,4 +1,5 @@ get('fieldset'); $fieldSet = $form->getFieldset($name); -if (empty($fieldSet)) -{ - return; +if (empty($fieldSet)) { + return; } $ignoreFields = $displayData->get('ignore_fields') ? : array(); $extraFields = $displayData->get('extra_fields') ? : array(); -if (!empty($displayData->showOptions) || $displayData->get('show_options', 1)) -{ - if (isset($extraFields[$name])) - { - foreach ($extraFields[$name] as $f) - { - if (in_array($f, $ignoreFields)) - { - continue; - } - if ($form->getField($f)) - { - $fieldSet[] = $form->getField($f); - } - } - } - - $html = array(); - - foreach ($fieldSet as $field) - { - $html[] = $field->renderField(); - } - - echo implode('', $html); -} -else -{ - $html = array(); - $html[] = ''; - - echo implode('', $html); +if (!empty($displayData->showOptions) || $displayData->get('show_options', 1)) { + if (isset($extraFields[$name])) { + foreach ($extraFields[$name] as $f) { + if (in_array($f, $ignoreFields)) { + continue; + } + if ($form->getField($f)) { + $fieldSet[] = $form->getField($f); + } + } + } + + $html = array(); + + foreach ($fieldSet as $field) { + $html[] = $field->renderField(); + } + + echo implode('', $html); +} else { + $html = array(); + $html[] = ''; + + echo implode('', $html); } diff --git a/layouts/joomla/edit/frontediting_modules.php b/layouts/joomla/edit/frontediting_modules.php index 14a95464a037b..373a2cde9ac4a 100644 --- a/layouts/joomla/edit/frontediting_modules.php +++ b/layouts/joomla/edit/frontediting_modules.php @@ -1,4 +1,5 @@ id; // If Module editing site -if ($parameters->get('redirect_edit', 'site') === 'site') -{ - $editUrl = Uri::base() . 'index.php?option=com_config&view=modules&id=' . (int) $mod->id . '&Itemid=' . $itemid . $redirectUri; - $target = '_self'; +if ($parameters->get('redirect_edit', 'site') === 'site') { + $editUrl = Uri::base() . 'index.php?option=com_config&view=modules&id=' . (int) $mod->id . '&Itemid=' . $itemid . $redirectUri; + $target = '_self'; } // Add link for editing the module $count = 0; $moduleHtml = preg_replace( - // Find first tag of module - '/^(\s*<(?:div|span|nav|ul|ol|h\d|section|aside|address|article|form) [^>]*>)/', - // Create and add the edit link and tooltip - '\\1 + // Find first tag of module + '/^(\s*<(?:div|span|nav|ul|ol|h\d|section|aside|address|article|form) [^>]*>)/', + // Create and add the edit link and tooltip + '\\1 ' . Text::_('JGLOBAL_EDIT') . ' ', - $moduleHtml, - 1, - $count + $moduleHtml, + 1, + $count ); // If menu editing is enabled and allowed and it's a menu module add link for editing -if ($menusEditing && $mod->module === 'mod_menu') -{ - // find the menu item id - $regex = '/\bitem-(\d+)\b/'; +if ($menusEditing && $mod->module === 'mod_menu') { + // find the menu item id + $regex = '/\bitem-(\d+)\b/'; - preg_match_all($regex, $moduleHtml, $menuItemids); - if ($menuItemids) - { - foreach ($menuItemids[1] as $menuItemid) - { - $menuitemEditUrl = Uri::base() . 'administrator/index.php?option=com_menus&view=item&client_id=0&layout=edit&id=' . (int) $menuItemid; - $moduleHtml = preg_replace( - // Find the link - '/()/', - // Create and add the edit link - '\\1 + preg_match_all($regex, $moduleHtml, $menuItemids); + if ($menuItemids) { + foreach ($menuItemids[1] as $menuItemid) { + $menuitemEditUrl = Uri::base() . 'administrator/index.php?option=com_menus&view=item&client_id=0&layout=edit&id=' . (int) $menuItemid; + $moduleHtml = preg_replace( + // Find the link + '/()/', + // Create and add the edit link + '\\1 ', - $moduleHtml - ); - } - } + $moduleHtml + ); + } + } } diff --git a/layouts/joomla/edit/global.php b/layouts/joomla/edit/global.php index 4cee42c8a6c24..35f72e68959da 100644 --- a/layouts/joomla/edit/global.php +++ b/layouts/joomla/edit/global.php @@ -1,4 +1,5 @@ input; $component = $input->getCmd('option', 'com_content'); -if ($component === 'com_categories') -{ - $extension = $input->getCmd('extension', 'com_content'); - $parts = explode('.', $extension); - $component = $parts[0]; +if ($component === 'com_categories') { + $extension = $input->getCmd('extension', 'com_content'); + $parts = explode('.', $extension); + $component = $parts[0]; } $saveHistory = ComponentHelper::getParams($component)->get('save_history', 0); $fields = $displayData->get('fields') ?: array( - 'transition', - array('parent', 'parent_id'), - array('published', 'state', 'enabled'), - array('category', 'catid'), - 'featured', - 'sticky', - 'access', - 'language', - 'tags', - 'note', - 'version_note', + 'transition', + array('parent', 'parent_id'), + array('published', 'state', 'enabled'), + array('category', 'catid'), + 'featured', + 'sticky', + 'access', + 'language', + 'tags', + 'note', + 'version_note', ); $hiddenFields = $displayData->get('hidden_fields') ?: array(); -if (!$saveHistory) -{ - $hiddenFields[] = 'version_note'; +if (!$saveHistory) { + $hiddenFields[] = 'version_note'; } -if (!Multilanguage::isEnabled()) -{ - $hiddenFields[] = 'language'; - $form->setFieldAttribute('language', 'default', '*'); +if (!Multilanguage::isEnabled()) { + $hiddenFields[] = 'language'; + $form->setFieldAttribute('language', 'default', '*'); } $html = array(); $html[] = '
    '; $html[] = '' . Text::_('JGLOBAL_FIELDSET_GLOBAL') . ''; -foreach ($fields as $field) -{ - foreach ((array) $field as $f) - { - if ($form->getField($f)) - { - if (in_array($f, $hiddenFields)) - { - $form->setFieldAttribute($f, 'type', 'hidden'); - } +foreach ($fields as $field) { + foreach ((array) $field as $f) { + if ($form->getField($f)) { + if (in_array($f, $hiddenFields)) { + $form->setFieldAttribute($f, 'type', 'hidden'); + } - $html[] = $form->renderField($f); - break; - } - } + $html[] = $form->renderField($f); + break; + } + } } $html[] = '
    '; diff --git a/layouts/joomla/edit/metadata.php b/layouts/joomla/edit/metadata.php index ee5be2a76db13..c786035096202 100644 --- a/layouts/joomla/edit/metadata.php +++ b/layouts/joomla/edit/metadata.php @@ -1,4 +1,5 @@ $fieldSet) : ?> - description) && trim($fieldSet->description)) : ?> -
    - - escape(Text::_($fieldSet->description)); ?> -
    - - - renderField('metadesc'); - echo $form->renderField('metakey'); - } - - foreach ($form->getFieldset($name) as $field) - { - if ($field->name !== 'jform[metadata][tags][]') - { - echo $field->renderField(); - } - } ?> + description) && trim($fieldSet->description)) : ?> +
    + + escape(Text::_($fieldSet->description)); ?> +
    + + + renderField('metadesc'); + echo $form->renderField('metakey'); + } + + foreach ($form->getFieldset($name) as $field) { + if ($field->name !== 'jform[metadata][tags][]') { + echo $field->renderField(); + } + } ?> diff --git a/layouts/joomla/edit/params.php b/layouts/joomla/edit/params.php index ae9cd0630bb37..dffef9fa7d1cb 100644 --- a/layouts/joomla/edit/params.php +++ b/layouts/joomla/edit/params.php @@ -1,4 +1,5 @@ getFieldsets(); $helper = $displayData->get('useCoreUI', false) ? 'uitab' : 'bootstrap'; -if (empty($fieldSets)) -{ - return; +if (empty($fieldSets)) { + return; } $ignoreFieldsets = $displayData->get('ignore_fieldsets') ?: array(); @@ -38,44 +38,38 @@ $configFieldsets = $displayData->get('configFieldsets') ?: array(); // Handle the hidden fieldsets when show_options is set false -if (!$displayData->get('show_options', 1)) -{ - // The HTML buffer - $html = array(); - - // Loop over the fieldsets - foreach ($fieldSets as $name => $fieldSet) - { - // Check if the fieldset should be ignored - if (in_array($name, $ignoreFieldsets, true)) - { - continue; - } - - // If it is a hidden fieldset, render the inputs - if (in_array($name, $hiddenFieldsets)) - { - // Loop over the fields - foreach ($form->getFieldset($name) as $field) - { - // Add only the input on the buffer - $html[] = $field->input; - } - - // Make sure the fieldset is not rendered twice - $ignoreFieldsets[] = $name; - } - - // Check if it is the correct fieldset to ignore - if (strpos($name, 'basic') === 0) - { - // Ignore only the fieldsets which are defined by the options not the custom fields ones - $ignoreFieldsets[] = $name; - } - } - - // Echo the hidden fieldsets - echo implode('', $html); +if (!$displayData->get('show_options', 1)) { + // The HTML buffer + $html = array(); + + // Loop over the fieldsets + foreach ($fieldSets as $name => $fieldSet) { + // Check if the fieldset should be ignored + if (in_array($name, $ignoreFieldsets, true)) { + continue; + } + + // If it is a hidden fieldset, render the inputs + if (in_array($name, $hiddenFieldsets)) { + // Loop over the fields + foreach ($form->getFieldset($name) as $field) { + // Add only the input on the buffer + $html[] = $field->input; + } + + // Make sure the fieldset is not rendered twice + $ignoreFieldsets[] = $name; + } + + // Check if it is the correct fieldset to ignore + if (strpos($name, 'basic') === 0) { + // Ignore only the fieldsets which are defined by the options not the custom fields ones + $ignoreFieldsets[] = $name; + } + } + + // Echo the hidden fieldsets + echo implode('', $html); } $opentab = false; @@ -83,131 +77,112 @@ $xml = $form->getXml(); // Loop again over the fieldsets -foreach ($fieldSets as $name => $fieldSet) -{ - // Ensure any fieldsets we don't want to show are skipped (including repeating formfield fieldsets) - if ((isset($fieldSet->repeat) && $fieldSet->repeat === true) - || in_array($name, $ignoreFieldsets) - || (!empty($configFieldsets) && in_array($name, $configFieldsets, true)) - || (!empty($hiddenFieldsets) && in_array($name, $hiddenFieldsets, true)) - ) - { - continue; - } - - // Determine the label - if (!empty($fieldSet->label)) - { - $label = Text::_($fieldSet->label); - } - else - { - $label = strtoupper('JGLOBAL_FIELDSET_' . $name); - if (Text::_($label) === $label) - { - $label = strtoupper($app->input->get('option') . '_' . $name . '_FIELDSET_LABEL'); - } - $label = Text::_($label); - } - - $hasChildren = $xml->xpath('//fieldset[@name="' . $name . '"]//fieldset[not(ancestor::field/form/*)]'); - $hasParent = $xml->xpath('//fieldset//fieldset[@name="' . $name . '"]'); - $isGrandchild = $xml->xpath('//fieldset//fieldset//fieldset[@name="' . $name . '"]'); - - if (!$isGrandchild && $hasParent) - { - echo '
    '; - echo '' . $label . ''; - - // Include the description when available - if (!empty($fieldSet->description)) - { - echo '
    '; - echo '' . Text::_('INFO') . ' '; - echo Text::_($fieldSet->description); - echo '
    '; - } - - echo '
    '; - } - // Tabs - elseif (!$hasParent) - { - if ($opentab) - { - if ($opentab > 1) - { - echo '
    '; - echo '
    '; - } - - // End previous tab - echo HTMLHelper::_($helper . '.endTab'); - } - - // Start the tab - echo HTMLHelper::_($helper . '.addTab', $tabName, 'attrib-' . $name, $label); - - $opentab = 1; - - // Directly add a fieldset if we have no children - if (!$hasChildren) - { - echo '
    '; - echo '' . $label . ''; - - // Include the description when available - if (!empty($fieldSet->description)) - { - echo '
    '; - echo '' . Text::_('INFO') . ' '; - echo Text::_($fieldSet->description); - echo '
    '; - } - - echo '
    '; - - $opentab = 2; - } - // Include the description when available - elseif (!empty($fieldSet->description)) - { - echo '
    '; - echo '' . Text::_('INFO') . ' '; - echo Text::_($fieldSet->description); - echo '
    '; - } - } - - // We're on the deepest level => output fields - if (!$hasChildren) - { - // The name of the fieldset to render - $displayData->fieldset = $name; - - // Force to show the options - $displayData->showOptions = true; - - // Render the fieldset - echo LayoutHelper::render('joomla.edit.fieldset', $displayData); - } - - // Close open fieldset - if (!$isGrandchild && $hasParent) - { - echo '
    '; - echo '
    '; - } +foreach ($fieldSets as $name => $fieldSet) { + // Ensure any fieldsets we don't want to show are skipped (including repeating formfield fieldsets) + if ( + (isset($fieldSet->repeat) && $fieldSet->repeat === true) + || in_array($name, $ignoreFieldsets) + || (!empty($configFieldsets) && in_array($name, $configFieldsets, true)) + || (!empty($hiddenFieldsets) && in_array($name, $hiddenFieldsets, true)) + ) { + continue; + } + + // Determine the label + if (!empty($fieldSet->label)) { + $label = Text::_($fieldSet->label); + } else { + $label = strtoupper('JGLOBAL_FIELDSET_' . $name); + if (Text::_($label) === $label) { + $label = strtoupper($app->input->get('option') . '_' . $name . '_FIELDSET_LABEL'); + } + $label = Text::_($label); + } + + $hasChildren = $xml->xpath('//fieldset[@name="' . $name . '"]//fieldset[not(ancestor::field/form/*)]'); + $hasParent = $xml->xpath('//fieldset//fieldset[@name="' . $name . '"]'); + $isGrandchild = $xml->xpath('//fieldset//fieldset//fieldset[@name="' . $name . '"]'); + + if (!$isGrandchild && $hasParent) { + echo '
    '; + echo '' . $label . ''; + + // Include the description when available + if (!empty($fieldSet->description)) { + echo '
    '; + echo '' . Text::_('INFO') . ' '; + echo Text::_($fieldSet->description); + echo '
    '; + } + + echo '
    '; + } elseif (!$hasParent) { + // Tabs + if ($opentab) { + if ($opentab > 1) { + echo '
    '; + echo '
    '; + } + + // End previous tab + echo HTMLHelper::_($helper . '.endTab'); + } + + // Start the tab + echo HTMLHelper::_($helper . '.addTab', $tabName, 'attrib-' . $name, $label); + + $opentab = 1; + + // Directly add a fieldset if we have no children + if (!$hasChildren) { + echo '
    '; + echo '' . $label . ''; + + // Include the description when available + if (!empty($fieldSet->description)) { + echo '
    '; + echo '' . Text::_('INFO') . ' '; + echo Text::_($fieldSet->description); + echo '
    '; + } + + echo '
    '; + + $opentab = 2; + } elseif (!empty($fieldSet->description)) { + // Include the description when available + echo '
    '; + echo '' . Text::_('INFO') . ' '; + echo Text::_($fieldSet->description); + echo '
    '; + } + } + + // We're on the deepest level => output fields + if (!$hasChildren) { + // The name of the fieldset to render + $displayData->fieldset = $name; + + // Force to show the options + $displayData->showOptions = true; + + // Render the fieldset + echo LayoutHelper::render('joomla.edit.fieldset', $displayData); + } + + // Close open fieldset + if (!$isGrandchild && $hasParent) { + echo '
    '; + echo '
    '; + } } -if ($opentab) -{ - if ($opentab > 1) - { - echo ''; - echo ''; - } +if ($opentab) { + if ($opentab > 1) { + echo ''; + echo ''; + } - // End previous tab - echo HTMLHelper::_($helper . '.endTab'); + // End previous tab + echo HTMLHelper::_($helper . '.endTab'); } diff --git a/layouts/joomla/edit/publishingdata.php b/layouts/joomla/edit/publishingdata.php index 452f393c62b90..9bbbce0b8ecc6 100644 --- a/layouts/joomla/edit/publishingdata.php +++ b/layouts/joomla/edit/publishingdata.php @@ -1,4 +1,5 @@ getForm(); $fields = $displayData->get('fields') ?: array( - 'publish_up', - 'publish_down', - 'featured_up', - 'featured_down', - array('created', 'created_time'), - array('created_by', 'created_user_id'), - 'created_by_alias', - array('modified', 'modified_time'), - array('modified_by', 'modified_user_id'), - 'version', - 'hits', - 'id' + 'publish_up', + 'publish_down', + 'featured_up', + 'featured_down', + array('created', 'created_time'), + array('created_by', 'created_user_id'), + 'created_by_alias', + array('modified', 'modified_time'), + array('modified_by', 'modified_user_id'), + 'version', + 'hits', + 'id' ); $hiddenFields = $displayData->get('hidden_fields') ?: array(); -foreach ($fields as $field) -{ - foreach ((array) $field as $f) - { - if ($form->getField($f)) - { - if (in_array($f, $hiddenFields)) - { - $form->setFieldAttribute($f, 'type', 'hidden'); - } +foreach ($fields as $field) { + foreach ((array) $field as $f) { + if ($form->getField($f)) { + if (in_array($f, $hiddenFields)) { + $form->setFieldAttribute($f, 'type', 'hidden'); + } - echo $form->renderField($f); - break; - } - } + echo $form->renderField($f); + break; + } + } } diff --git a/layouts/joomla/edit/title_alias.php b/layouts/joomla/edit/title_alias.php index a241cc4a8c8ea..1bcb121b11c27 100644 --- a/layouts/joomla/edit/title_alias.php +++ b/layouts/joomla/edit/title_alias.php @@ -1,4 +1,5 @@
    -
    - renderField($title) : ''; ?> -
    -
    - renderField('alias'); ?> -
    +
    + renderField($title) : ''; ?> +
    +
    + renderField('alias'); ?> +
    diff --git a/layouts/joomla/editors/buttons.php b/layouts/joomla/editors/buttons.php index 08db5c0d9a58c..3176e1dc44db2 100644 --- a/layouts/joomla/editors/buttons.php +++ b/layouts/joomla/editors/buttons.php @@ -1,4 +1,5 @@ diff --git a/layouts/joomla/editors/buttons/button.php b/layouts/joomla/editors/buttons/button.php index 49993ee41fb85..c7a0712f6d17d 100644 --- a/layouts/joomla/editors/buttons/button.php +++ b/layouts/joomla/editors/buttons/button.php @@ -1,4 +1,5 @@ get('name')) : - $class = 'btn btn-secondary'; - $class .= ($button->get('class')) ? ' ' . $button->get('class') : null; - $class .= ($button->get('modal')) ? ' modal-button' : null; - $href = '#' . strtolower($button->get('name')) . '_modal'; - $link = ($button->get('link')) ? Uri::base() . $button->get('link') : null; - $onclick = ($button->get('onclick')) ? ' onclick="' . $button->get('onclick') . '"' : ''; - $title = ($button->get('title')) ? $button->get('title') : $button->get('text'); - $icon = ($button->get('icon')) ? $button->get('icon') : $button->get('name'); -?> + $class = 'btn btn-secondary'; + $class .= ($button->get('class')) ? ' ' . $button->get('class') : null; + $class .= ($button->get('modal')) ? ' modal-button' : null; + $href = '#' . strtolower($button->get('name')) . '_modal'; + $link = ($button->get('link')) ? Uri::base() . $button->get('link') : null; + $onclick = ($button->get('onclick')) ? ' onclick="' . $button->get('onclick') . '"' : ''; + $title = ($button->get('title')) ? $button->get('title') : $button->get('text'); + $icon = ($button->get('icon')) ? $button->get('icon') : $button->get('name'); + ?> diff --git a/layouts/joomla/editors/buttons/modal.php b/layouts/joomla/editors/buttons/modal.php index 2ae1074d72af2..771de06474f0f 100644 --- a/layouts/joomla/editors/buttons/modal.php +++ b/layouts/joomla/editors/buttons/modal.php @@ -1,4 +1,5 @@ get('modal')) -{ - return; +if (!$button->get('modal')) { + return; } $class = ($button->get('class')) ? $button->get('class') : null; @@ -30,34 +30,30 @@ $confirm = ''; -if (is_array($button->get('options')) && isset($options['confirmText']) && isset($options['confirmCallback'])) -{ - $confirm = ''; +if (is_array($button->get('options')) && isset($options['confirmText']) && isset($options['confirmCallback'])) { + $confirm = ''; } -if (null !== $button->get('id')) -{ - $id = str_replace(' ', '', $button->get('id')); -} -else -{ - $id = strtolower($button->get('name')) . '_modal'; +if (null !== $button->get('id')) { + $id = str_replace(' ', '', $button->get('id')); +} else { + $id = strtolower($button->get('name')) . '_modal'; } // @todo: J4: Move Make buttons fullscreen on smaller devices per https://github.com/joomla/joomla-cms/pull/23091 // Create the modal echo HTMLHelper::_( - 'bootstrap.renderModal', - $id, - array( - 'url' => $link, - 'title' => $title, - 'height' => array_key_exists('height', $options) ? $options['height'] : '400px', - 'width' => array_key_exists('width', $options) ? $options['width'] : '800px', - 'bodyHeight' => array_key_exists('bodyHeight', $options) ? $options['bodyHeight'] : '70', - 'modalWidth' => array_key_exists('modalWidth', $options) ? $options['modalWidth'] : '80', - 'footer' => $confirm . '' - ) + 'bootstrap.renderModal', + $id, + array( + 'url' => $link, + 'title' => $title, + 'height' => array_key_exists('height', $options) ? $options['height'] : '400px', + 'width' => array_key_exists('width', $options) ? $options['width'] : '800px', + 'bodyHeight' => array_key_exists('bodyHeight', $options) ? $options['bodyHeight'] : '70', + 'modalWidth' => array_key_exists('modalWidth', $options) ? $options['modalWidth'] : '80', + 'footer' => $confirm . '' + ) ); diff --git a/layouts/joomla/error/backtrace.php b/layouts/joomla/error/backtrace.php index 415328df69f86..2742f143d2679 100644 --- a/layouts/joomla/error/backtrace.php +++ b/layouts/joomla/error/backtrace.php @@ -1,4 +1,5 @@ - - - + + + - - - - - + + + + + - $backtrace): ?> - - + $backtrace) : ?> + + - - - - - + + + + + - - - - - - - + + + + + + +
    - Call stack -
    + Call stack +
    - # - - Function - - Location -
    + # + + Function + + Location +
    - -
    + + - - - - + + + + - - -   -
    + + +   +
    diff --git a/layouts/joomla/form/field/calendar.php b/layouts/joomla/form/field/calendar.php index a977ef175d687..f81563ab5a821 100644 --- a/layouts/joomla/form/field/calendar.php +++ b/layouts/joomla/form/field/calendar.php @@ -1,4 +1,5 @@ format('Y-m-d H:i:s'); +if (strtoupper($value) === 'NOW') { + $value = Factory::getDate()->format('Y-m-d H:i:s'); } $readonly = isset($attributes['readonly']) && $attributes['readonly'] === 'readonly'; $disabled = isset($attributes['disabled']) && $attributes['disabled'] === 'disabled'; -if (is_array($attributes)) -{ - $attributes = ArrayHelper::toString($attributes); +if (is_array($attributes)) { + $attributes = ArrayHelper::toString($attributes); } $calendarAttrs = [ - 'data-inputfield' => $id, - 'data-button' => $id . '_btn', - 'data-date-format' => $format, - 'data-firstday' => empty($firstday) ? '' : $firstday, - 'data-weekend' => empty($weekend) ? '' : implode(',', $weekend), - 'data-today-btn' => $todaybutton, - 'data-week-numbers' => $weeknumbers, - 'data-show-time' => $showtime, - 'data-show-others' => $filltable, - 'data-time24' => $timeformat, - 'data-only-months-nav' => $singleheader, - 'data-min-year' => $minYear, - 'data-max-year' => $maxYear, - 'data-date-type' => strtolower($calendar), + 'data-inputfield' => $id, + 'data-button' => $id . '_btn', + 'data-date-format' => $format, + 'data-firstday' => empty($firstday) ? '' : $firstday, + 'data-weekend' => empty($weekend) ? '' : implode(',', $weekend), + 'data-today-btn' => $todaybutton, + 'data-week-numbers' => $weeknumbers, + 'data-show-time' => $showtime, + 'data-show-others' => $filltable, + 'data-time24' => $timeformat, + 'data-only-months-nav' => $singleheader, + 'data-min-year' => $minYear, + 'data-max-year' => $maxYear, + 'data-date-type' => strtolower($calendar), ]; $calendarAttrsStr = ArrayHelper::toString($calendarAttrs); // Add language strings $strings = [ - // Days - 'SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', - // Short days - 'SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', - // Months - 'JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE', 'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER', - // Short months - 'JANUARY_SHORT', 'FEBRUARY_SHORT', 'MARCH_SHORT', 'APRIL_SHORT', 'MAY_SHORT', 'JUNE_SHORT', - 'JULY_SHORT', 'AUGUST_SHORT', 'SEPTEMBER_SHORT', 'OCTOBER_SHORT', 'NOVEMBER_SHORT', 'DECEMBER_SHORT', - // Buttons - 'JCLOSE', 'JCLEAR', 'JLIB_HTML_BEHAVIOR_TODAY', - // Miscellaneous - 'JLIB_HTML_BEHAVIOR_WK', + // Days + 'SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', + // Short days + 'SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', + // Months + 'JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE', 'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER', + // Short months + 'JANUARY_SHORT', 'FEBRUARY_SHORT', 'MARCH_SHORT', 'APRIL_SHORT', 'MAY_SHORT', 'JUNE_SHORT', + 'JULY_SHORT', 'AUGUST_SHORT', 'SEPTEMBER_SHORT', 'OCTOBER_SHORT', 'NOVEMBER_SHORT', 'DECEMBER_SHORT', + // Buttons + 'JCLOSE', 'JCLEAR', 'JLIB_HTML_BEHAVIOR_TODAY', + // Miscellaneous + 'JLIB_HTML_BEHAVIOR_WK', ]; -foreach ($strings as $c) -{ - Text::script($c); +foreach ($strings as $c) { + Text::script($c); } // These are new strings. Make sure they exist. Can be generalised at later time: eg in 4.1 version. -if ($lang->hasKey('JLIB_HTML_BEHAVIOR_AM')) -{ - Text::script('JLIB_HTML_BEHAVIOR_AM'); +if ($lang->hasKey('JLIB_HTML_BEHAVIOR_AM')) { + Text::script('JLIB_HTML_BEHAVIOR_AM'); } -if ($lang->hasKey('JLIB_HTML_BEHAVIOR_PM')) -{ - Text::script('JLIB_HTML_BEHAVIOR_PM'); +if ($lang->hasKey('JLIB_HTML_BEHAVIOR_PM')) { + Text::script('JLIB_HTML_BEHAVIOR_PM'); } // Redefine locale/helper assets to use correct path, and load calendar assets $document->getWebAssetManager() - ->registerAndUseScript('field.calendar.helper', $helperPath, [], ['defer' => true]) - ->useStyle('field.calendar' . ($direction === 'rtl' ? '-rtl' : '')) - ->useScript('field.calendar'); + ->registerAndUseScript('field.calendar.helper', $helperPath, [], ['defer' => true]) + ->useStyle('field.calendar' . ($direction === 'rtl' ? '-rtl' : '')) + ->useScript('field.calendar'); ?>
    - -
    - - - - - - data-alt-value="" autocomplete="off"> - - -
    - + +
    + + + + + + data-alt-value="" autocomplete="off"> + + +
    +
    diff --git a/layouts/joomla/form/field/checkbox.php b/layouts/joomla/form/field/checkbox.php index 4134cdb9c5e0f..f4049fe630ed7 100644 --- a/layouts/joomla/form/field/checkbox.php +++ b/layouts/joomla/form/field/checkbox.php @@ -1,4 +1,5 @@
    - - > + + >
    diff --git a/layouts/joomla/form/field/checkboxes.php b/layouts/joomla/form/field/checkboxes.php index 58dd3878240f8..905b5238d73f1 100644 --- a/layouts/joomla/form/field/checkboxes.php +++ b/layouts/joomla/form/field/checkboxes.php @@ -1,4 +1,5 @@
    - - > - + + + > + - $option) : ?> - value, $checkedOptions, true) ? 'checked' : ''; + $option) : ?> + value, $checkedOptions, true) ? 'checked' : ''; - // In case there is no stored value, use the option's default state. - $checked = (!$hasValue && $option->checked) ? 'checked' : $checked; - $optionClass = !empty($option->class) ? 'class="form-check-input ' . $option->class . '"' : ' class="form-check-input"'; - $optionDisabled = !empty($option->disable) || $disabled ? 'disabled' : ''; + // In case there is no stored value, use the option's default state. + $checked = (!$hasValue && $option->checked) ? 'checked' : $checked; + $optionClass = !empty($option->class) ? 'class="form-check-input ' . $option->class . '"' : ' class="form-check-input"'; + $optionDisabled = !empty($option->disable) || $disabled ? 'disabled' : ''; - // Initialize some JavaScript option attributes. - $onclick = !empty($option->onclick) ? 'onclick="' . $option->onclick . '"' : ''; - $onchange = !empty($option->onchange) ? 'onchange="' . $option->onchange . '"' : ''; + // Initialize some JavaScript option attributes. + $onclick = !empty($option->onclick) ? 'onclick="' . $option->onclick . '"' : ''; + $onchange = !empty($option->onchange) ? 'onchange="' . $option->onchange . '"' : ''; - $oid = $id . $i; - $value = htmlspecialchars($option->value, ENT_COMPAT, 'UTF-8'); - $attributes = array_filter(array($checked, $optionClass, $optionDisabled, $onchange, $onclick)); - ?> -
    - - -
    - + $oid = $id . $i; + $value = htmlspecialchars($option->value, ENT_COMPAT, 'UTF-8'); + $attributes = array_filter(array($checked, $optionClass, $optionDisabled, $onchange, $onclick)); + ?> +
    + + +
    +
    diff --git a/layouts/joomla/form/field/color/advanced.php b/layouts/joomla/form/field/color/advanced.php index 468368671522d..db1a642557c7b 100644 --- a/layouts/joomla/form/field/color/advanced.php +++ b/layouts/joomla/form/field/color/advanced.php @@ -1,4 +1,5 @@ getDocument()->getWebAssetManager(); $wa->usePreset('minicolors') - ->useScript('field.color-adv'); + ->useScript('field.color-adv'); ?> /> diff --git a/layouts/joomla/form/field/color/simple.php b/layouts/joomla/form/field/color/simple.php index 0e043aff5e179..cfa1923581fde 100644 --- a/layouts/joomla/form/field/color/simple.php +++ b/layouts/joomla/form/field/color/simple.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useStyle('webcomponent.field-simple-color') - ->useScript('webcomponent.field-simple-color'); + ->useStyle('webcomponent.field-simple-color') + ->useScript('webcomponent.field-simple-color'); ?> - + diff --git a/layouts/joomla/form/field/color/slider.php b/layouts/joomla/form/field/color/slider.php index ec0c089be21cf..20baa206d53ac 100644 --- a/layouts/joomla/form/field/color/slider.php +++ b/layouts/joomla/form/field/color/slider.php @@ -1,4 +1,5 @@
    + > - - - > - - - - > - + + + > + + + + > + - - - - > - - - - - > - - - - - > - - - - - > - + + + + > + + + + + > + + + + + > + + + + + > +
    diff --git a/layouts/joomla/form/field/combo.php b/layouts/joomla/form/field/combo.php index 04dbe8bac182a..86f52cee67e07 100644 --- a/layouts/joomla/form/field/combo.php +++ b/layouts/joomla/form/field/combo.php @@ -1,4 +1,5 @@ text; +foreach ($options as $option) { + $val[] = $option->text; } ?> - data-list="" - + type="text" + name="" + id="" + value="" + + data-list="" + /> diff --git a/layouts/joomla/form/field/contenthistory.php b/layouts/joomla/form/field/contenthistory.php index 89b43ac2085a8..1091c6fb3a538 100644 --- a/layouts/joomla/form/field/contenthistory.php +++ b/layouts/joomla/form/field/contenthistory.php @@ -1,4 +1,5 @@ Route::_($link), - 'title' => $label, - 'height' => '100%', - 'width' => '100%', - 'modalWidth' => '80', - 'bodyHeight' => '60', - 'footer' => '' - ) + 'bootstrap.renderModal', + 'versionsModal', + array( + 'url' => Route::_($link), + 'title' => $label, + 'height' => '100%', + 'width' => '100%', + 'modalWidth' => '80', + 'bodyHeight' => '60', + 'footer' => '' + ) ); ?> diff --git a/layouts/joomla/form/field/email.php b/layouts/joomla/form/field/email.php index 254489393c144..2f9e75e09db4f 100644 --- a/layouts/joomla/form/field/email.php +++ b/layouts/joomla/form/field/email.php @@ -1,4 +1,5 @@ '; diff --git a/layouts/joomla/form/field/file.php b/layouts/joomla/form/field/file.php index d4f93a0110ad9..0120973249bfa 100644 --- a/layouts/joomla/form/field/file.php +++ b/layouts/joomla/form/field/file.php @@ -1,4 +1,5 @@ - - - - - - - - >
    - + name="" + id="" + + + + + + + + + >
    + diff --git a/layouts/joomla/form/field/groupedlist-fancy-select.php b/layouts/joomla/form/field/groupedlist-fancy-select.php index 9ce2f3b94fa8c..633ce7e030f42 100644 --- a/layouts/joomla/form/field/groupedlist-fancy-select.php +++ b/layouts/joomla/form/field/groupedlist-fancy-select.php @@ -1,4 +1,5 @@ escape($hint ?: Text::_('JGLOBAL_TYPE_OR_SELECT_SOME_OPTIONS')) . '" '; -if ($required) -{ - $attr .= ' required class="required"'; - $attr2 .= ' required'; +if ($required) { + $attr .= ' required class="required"'; + $attr2 .= ' required'; } // Create a read-only list (no name) with a hidden input to store the value. -if ($readonly) -{ - $html[] = HTMLHelper::_( - 'select.groupedlist', $groups, null, - array( - 'list.attr' => $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, - 'option.text.toHtml' => false, - ) - ); - - // E.g. form field type tag sends $this->value as array - if ($multiple && \is_array($value)) - { - if (!\count($value)) - { - $value[] = ''; - } - - foreach ($value as $val) - { - $html[] = ''; - } - } - else - { - $html[] = ''; - } -} - -// Create a regular list. -else -{ - $html[] = HTMLHelper::_( - 'select.groupedlist', $groups, $name, - array( - 'list.attr' => $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, - 'option.text.toHtml' => false, - ) - ); +if ($readonly) { + $html[] = HTMLHelper::_( + 'select.groupedlist', + $groups, + null, + array( + 'list.attr' => $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, + 'option.text.toHtml' => false, + ) + ); + + // E.g. form field type tag sends $this->value as array + if ($multiple && \is_array($value)) { + if (!\count($value)) { + $value[] = ''; + } + + foreach ($value as $val) { + $html[] = ''; + } + } else { + $html[] = ''; + } +} else { + // Create a regular list. + $html[] = HTMLHelper::_( + 'select.groupedlist', + $groups, + $name, + array( + 'list.attr' => $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, + 'option.text.toHtml' => false, + ) + ); } Text::script('JGLOBAL_SELECT_NO_RESULTS_MATCH'); Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); Factory::getApplication()->getDocument()->getWebAssetManager() - ->usePreset('choicesjs') - ->useScript('webcomponent.field-fancy-select'); + ->usePreset('choicesjs') + ->useScript('webcomponent.field-fancy-select'); ?> diff --git a/layouts/joomla/form/field/groupedlist.php b/layouts/joomla/form/field/groupedlist.php index f0f0eda4d909b..e781dde4eae14 100644 --- a/layouts/joomla/form/field/groupedlist.php +++ b/layouts/joomla/form/field/groupedlist.php @@ -1,4 +1,5 @@ $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, - 'option.text.toHtml' => false, - ) - ); - - // E.g. form field type tag sends $this->value as array - if ($multiple && \is_array($value)) - { - if (!\count($value)) - { - $value[] = ''; - } +if ($readonly) { + $html[] = HTMLHelper::_( + 'select.groupedlist', + $groups, + null, + array( + 'list.attr' => $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, + 'option.text.toHtml' => false, + ) + ); - foreach ($value as $val) - { - $html[] = ''; - } - } - else - { - $html[] = ''; - } -} + // E.g. form field type tag sends $this->value as array + if ($multiple && \is_array($value)) { + if (!\count($value)) { + $value[] = ''; + } -// Create a regular list. -else -{ - $html[] = HTMLHelper::_( - 'select.groupedlist', $groups, $name, - array( - 'list.attr' => $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, - 'option.text.toHtml' => false, - ) - ); + foreach ($value as $val) { + $html[] = ''; + } + } else { + $html[] = ''; + } +} else { + // Create a regular list. + $html[] = HTMLHelper::_( + 'select.groupedlist', + $groups, + $name, + array( + 'list.attr' => $attr, 'id' => $id, 'list.select' => $value, 'group.items' => null, 'option.key.toHtml' => false, + 'option.text.toHtml' => false, + ) + ); } echo implode($html); diff --git a/layouts/joomla/form/field/hidden.php b/layouts/joomla/form/field/hidden.php index 539e99ae8660e..88ba0160e8772 100644 --- a/layouts/joomla/form/field/hidden.php +++ b/layouts/joomla/form/field/hidden.php @@ -1,4 +1,5 @@ > + type="hidden" + name="" + id="" + value="" + > diff --git a/layouts/joomla/form/field/list-fancy-select.php b/layouts/joomla/form/field/list-fancy-select.php index 8820fe0fee473..89fba4525e3fa 100644 --- a/layouts/joomla/form/field/list-fancy-select.php +++ b/layouts/joomla/form/field/list-fancy-select.php @@ -1,4 +1,5 @@ escape($hint ?: Text::_('JGLOBAL_TYPE_OR_SELECT_SOME_OPTIONS')) . '" '; -if ($required) -{ - $attr .= ' required class="required"'; - $attr2 .= ' required'; +if ($required) { + $attr .= ' required class="required"'; + $attr2 .= ' required'; } // Create a read-only list (no name) with hidden input(s) to store the value(s). -if ($readonly) -{ - $html[] = HTMLHelper::_('select.genericlist', $options, '', trim($attr), 'value', 'text', $value, $id); - - // E.g. form field type tag sends $this->value as array - if ($multiple && is_array($value)) - { - if (!count($value)) - { - $value[] = ''; - } - - foreach ($value as $val) - { - $html[] = ''; - } - } - else - { - $html[] = ''; - } -} -else -// Create a regular list. +if ($readonly) { + $html[] = HTMLHelper::_('select.genericlist', $options, '', trim($attr), 'value', 'text', $value, $id); + + // E.g. form field type tag sends $this->value as array + if ($multiple && is_array($value)) { + if (!count($value)) { + $value[] = ''; + } + + foreach ($value as $val) { + $html[] = ''; + } + } else { + $html[] = ''; + } +} else // Create a regular list. { - $html[] = HTMLHelper::_('select.genericlist', $options, $name, trim($attr), 'value', 'text', $value, $id); + $html[] = HTMLHelper::_('select.genericlist', $options, $name, trim($attr), 'value', 'text', $value, $id); } Text::script('JGLOBAL_SELECT_NO_RESULTS_MATCH'); Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); Factory::getApplication()->getDocument()->getWebAssetManager() - ->usePreset('choicesjs') - ->useScript('webcomponent.field-fancy-select'); + ->usePreset('choicesjs') + ->useScript('webcomponent.field-fancy-select'); ?> diff --git a/layouts/joomla/form/field/list.php b/layouts/joomla/form/field/list.php index c827e314a99ac..99e6292257508 100644 --- a/layouts/joomla/form/field/list.php +++ b/layouts/joomla/form/field/list.php @@ -1,4 +1,5 @@ value as array - if ($multiple && is_array($value)) - { - if (!count($value)) - { - $value[] = ''; - } + // E.g. form field type tag sends $this->value as array + if ($multiple && is_array($value)) { + if (!count($value)) { + $value[] = ''; + } - foreach ($value as $val) - { - $html[] = ''; - } - } - else - { - $html[] = ''; - } -} -else -// Create a regular list passing the arguments in an array. + foreach ($value as $val) { + $html[] = ''; + } + } else { + $html[] = ''; + } +} else // Create a regular list passing the arguments in an array. { - $listoptions = array(); - $listoptions['option.key'] = 'value'; - $listoptions['option.text'] = 'text'; - $listoptions['list.select'] = $value; - $listoptions['id'] = $id; - $listoptions['list.translate'] = false; - $listoptions['option.attr'] = 'optionattr'; - $listoptions['list.attr'] = trim($attr); - $html[] = HTMLHelper::_('select.genericlist', $options, $name, $listoptions); + $listoptions = array(); + $listoptions['option.key'] = 'value'; + $listoptions['option.text'] = 'text'; + $listoptions['list.select'] = $value; + $listoptions['id'] = $id; + $listoptions['list.translate'] = false; + $listoptions['option.attr'] = 'optionattr'; + $listoptions['list.attr'] = trim($attr); + $html[] = HTMLHelper::_('select.genericlist', $options, $name, $listoptions); } echo implode($html); diff --git a/layouts/joomla/form/field/media.php b/layouts/joomla/form/field/media.php index 57fa34bde02c5..c62233c58b3af 100644 --- a/layouts/joomla/form/field/media.php +++ b/layouts/joomla/form/field/media.php @@ -1,4 +1,5 @@ 0) ? 'max-width:' . $width . 'px;' : ''; - $style .= ($height > 0) ? 'max-height:' . $height . 'px;' : ''; - - $imgattr = array( - 'id' => $id . '_preview', - 'class' => 'media-preview', - 'style' => $style, - ); - - $img = HTMLHelper::_('image', $src, Text::_('JLIB_FORM_MEDIA_PREVIEW_ALT'), $imgattr); - - $previewImg = '
    ' . $img . '
    '; - $previewImgEmpty = ''; - - $showPreview = 'static'; +if ($showPreview) { + $cleanValue = MediaHelper::getCleanMediaFieldValue($value); + + if ($cleanValue && file_exists(JPATH_ROOT . '/' . $cleanValue)) { + $src = Uri::root() . $value; + } else { + $src = ''; + } + + $width = $previewWidth; + $height = $previewHeight; + $style = ''; + $style .= ($width > 0) ? 'max-width:' . $width . 'px;' : ''; + $style .= ($height > 0) ? 'max-height:' . $height . 'px;' : ''; + + $imgattr = array( + 'id' => $id . '_preview', + 'class' => 'media-preview', + 'style' => $style, + ); + + $img = HTMLHelper::_('image', $src, Text::_('JLIB_FORM_MEDIA_PREVIEW_ALT'), $imgattr); + + $previewImg = '
    ' . $img . '
    '; + $previewImgEmpty = ''; + + $showPreview = 'static'; } // The url for the modal $url = ($readonly ? '' - : ($link ?: 'index.php?option=com_media&view=media&tmpl=component&mediatypes=' . $mediaTypes - . '&asset=' . $asset . '&author=' . $authorId) - . '&fieldid={field-media-id}&path=' . $folder); + : ($link ?: 'index.php?option=com_media&view=media&tmpl=component&mediatypes=' . $mediaTypes + . '&asset=' . $asset . '&author=' . $authorId) + . '&fieldid={field-media-id}&path=' . $folder); // Correctly route the url to ensure it's correctly using sef modes and subfolders $url = Route::_($url); @@ -143,63 +140,63 @@ Text::script('JLIB_FORM_MEDIA_PREVIEW_EMPTY', true); $modalHTML = HTMLHelper::_( - 'bootstrap.renderModal', - 'imageModal_' . $id, - [ - 'url' => $url, - 'title' => Text::_('JLIB_FORM_CHANGE_IMAGE'), - 'closeButton' => true, - 'height' => '100%', - 'width' => '100%', - 'modalWidth' => '80', - 'bodyHeight' => '60', - 'footer' => '' - . '', - ] + 'bootstrap.renderModal', + 'imageModal_' . $id, + [ + 'url' => $url, + 'title' => Text::_('JLIB_FORM_CHANGE_IMAGE'), + 'closeButton' => true, + 'height' => '100%', + 'width' => '100%', + 'modalWidth' => '80', + 'bodyHeight' => '60', + 'footer' => '' + . '', + ] ); $wam->useStyle('webcomponent.field-media') - ->useScript('webcomponent.field-media'); + ->useScript('webcomponent.field-media'); if (count($doc->getScriptOptions('media-picker')) === 0) { - $doc->addScriptOptions('media-picker', [ - 'images' => $imagesExt, - 'audios' => $audiosExt, - 'videos' => $videosExt, - 'documents' => $documentsExt, - ]); + $doc->addScriptOptions('media-picker', [ + 'images' => $imagesExt, + 'audios' => $audiosExt, + 'videos' => $videosExt, + 'documents' => $documentsExt, + ]); } ?> - base-path="" - root-folder="get('file_path', 'images'); ?>" - url="" - modal-container=".modal" - modal-width="100%" - modal-height="400px" - input=".field-media-input" - button-select=".button-select" - button-clear=".button-clear" - button-save-selected=".button-save-selected" - preview="static" - preview-container=".field-media-preview" - preview-width="" - preview-height="" - supported-extensions=" $imagesAllowedExt, 'audios' => $audiosAllowedExt, 'videos' => $videosAllowedExt, 'documents' => $documentsAllowedExt])); ?> + base-path="" + root-folder="get('file_path', 'images'); ?>" + url="" + modal-container=".modal" + modal-width="100%" + modal-height="400px" + input=".field-media-input" + button-select=".button-select" + button-clear=".button-clear" + button-save-selected=".button-save-selected" + preview="static" + preview-container=".field-media-preview" + preview-width="" + preview-height="" + supported-extensions=" $imagesAllowedExt, 'audios' => $audiosAllowedExt, 'videos' => $videosAllowedExt, 'documents' => $documentsAllowedExt])); ?> "> - - -
    - - -
    - -
    - > - - - - -
    + + +
    + + +
    + +
    + > + + + + +
    diff --git a/layouts/joomla/form/field/meter.php b/layouts/joomla/form/field/meter.php index 3e2a271b7bcaf..f6c112d8ef506 100644 --- a/layouts/joomla/form/field/meter.php +++ b/layouts/joomla/form/field/meter.php @@ -1,4 +1,5 @@
    -
    - style="width:%;">
    +
    + style="width:%;">
    diff --git a/layouts/joomla/form/field/moduleorder.php b/layouts/joomla/form/field/moduleorder.php index f71c8510d6270..95f23a7ba0720 100644 --- a/layouts/joomla/form/field/moduleorder.php +++ b/layouts/joomla/form/field/moduleorder.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useScript('webcomponent.field-module-order'); + ->useScript('webcomponent.field-module-order'); ?> > diff --git a/layouts/joomla/form/field/number.php b/layouts/joomla/form/field/number.php index d10d61f3d12ba..ccc24f22bd283 100644 --- a/layouts/joomla/form/field/number.php +++ b/layouts/joomla/form/field/number.php @@ -1,4 +1,5 @@ > + type="number" + inputmode="numeric" + name="" + id="" + value="" + > diff --git a/layouts/joomla/form/field/password.php b/layouts/joomla/form/field/password.php index a1bc52cd460b3..d128aacb00d04 100644 --- a/layouts/joomla/form/field/password.php +++ b/layouts/joomla/form/field/password.php @@ -1,4 +1,5 @@ getWebAssetManager(); -if ($meter) -{ - $wa->useScript('field.passwordstrength'); +if ($meter) { + $wa->useScript('field.passwordstrength'); - $class = 'js-password-strength ' . $class; + $class = 'js-password-strength ' . $class; - if ($forcePassword) - { - $class = $class . ' meteredPassword'; - } + if ($forcePassword) { + $class = $class . ' meteredPassword'; + } } $wa->useScript('field.passwordview'); @@ -75,92 +74,85 @@ Text::script('JSHOWPASSWORD'); Text::script('JHIDEPASSWORD'); -if ($lock) -{ - Text::script('JMODIFY'); - Text::script('JCANCEL'); +if ($lock) { + Text::script('JMODIFY'); + Text::script('JCANCEL'); - $disabled = true; - $hint = str_repeat('•', 10); - $value = ''; + $disabled = true; + $hint = str_repeat('•', 10); + $value = ''; } $ariaDescribedBy = $rules ? $name . '-rules ' : ''; $ariaDescribedBy .= !empty($description) ? (($id ?: $name) . '-desc') : ''; $attributes = array( - strlen($hint) ? 'placeholder="' . htmlspecialchars($hint, ENT_COMPAT, 'UTF-8') . '"' : '', - !empty($autocomplete) ? 'autocomplete="' . $autocomplete . '"' : '', - !empty($class) ? 'class="form-control ' . $class . '"' : 'class="form-control"', - !empty($ariaDescribedBy) ? 'aria-describedby="' . trim($ariaDescribedBy) . '"' : '', - $readonly ? 'readonly' : '', - $disabled ? 'disabled' : '', - !empty($size) ? 'size="' . $size . '"' : '', - !empty($maxLength) ? 'maxlength="' . $maxLength . '"' : '', - $required ? 'required' : '', - $autofocus ? 'autofocus' : '', - !empty($minLength) ? 'data-min-length="' . $minLength . '"' : '', - !empty($minIntegers) ? 'data-min-integers="' . $minIntegers . '"' : '', - !empty($minSymbols) ? 'data-min-symbols="' . $minSymbols . '"' : '', - !empty($minUppercase) ? 'data-min-uppercase="' . $minUppercase . '"' : '', - !empty($minLowercase) ? 'data-min-lowercase="' . $minLowercase . '"' : '', - !empty($forcePassword) ? 'data-min-force="' . $forcePassword . '"' : '', - $dataAttribute, + strlen($hint) ? 'placeholder="' . htmlspecialchars($hint, ENT_COMPAT, 'UTF-8') . '"' : '', + !empty($autocomplete) ? 'autocomplete="' . $autocomplete . '"' : '', + !empty($class) ? 'class="form-control ' . $class . '"' : 'class="form-control"', + !empty($ariaDescribedBy) ? 'aria-describedby="' . trim($ariaDescribedBy) . '"' : '', + $readonly ? 'readonly' : '', + $disabled ? 'disabled' : '', + !empty($size) ? 'size="' . $size . '"' : '', + !empty($maxLength) ? 'maxlength="' . $maxLength . '"' : '', + $required ? 'required' : '', + $autofocus ? 'autofocus' : '', + !empty($minLength) ? 'data-min-length="' . $minLength . '"' : '', + !empty($minIntegers) ? 'data-min-integers="' . $minIntegers . '"' : '', + !empty($minSymbols) ? 'data-min-symbols="' . $minSymbols . '"' : '', + !empty($minUppercase) ? 'data-min-uppercase="' . $minUppercase . '"' : '', + !empty($minLowercase) ? 'data-min-lowercase="' . $minLowercase . '"' : '', + !empty($forcePassword) ? 'data-min-force="' . $forcePassword . '"' : '', + $dataAttribute, ); -if ($rules) -{ - $requirements = []; - - if ($minLength) - { - $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_CHARACTERS', $minLength); - } - - if ($minIntegers) - { - $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_DIGITS', $minIntegers); - } - - if ($minSymbols) - { - $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_SYMBOLS', $minSymbols); - } - - if ($minUppercase) - { - $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_UPPERCASE', $minUppercase); - } - - if ($minLowercase) - { - $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_LOWERCASE', $minLowercase); - } +if ($rules) { + $requirements = []; + + if ($minLength) { + $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_CHARACTERS', $minLength); + } + + if ($minIntegers) { + $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_DIGITS', $minIntegers); + } + + if ($minSymbols) { + $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_SYMBOLS', $minSymbols); + } + + if ($minUppercase) { + $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_UPPERCASE', $minUppercase); + } + + if ($minLowercase) { + $requirements[] = Text::sprintf('JFIELD_PASSWORD_RULES_LOWERCASE', $minLowercase); + } } ?> -
    - -
    +
    + +
    -
    - > - - - - - -
    +
    + > + + + + + +
    diff --git a/layouts/joomla/form/field/radio/buttons.php b/layouts/joomla/form/field/radio/buttons.php index 96f36aa57e78c..faf1d93fdce28 100644 --- a/layouts/joomla/form/field/radio/buttons.php +++ b/layouts/joomla/form/field/radio/buttons.php @@ -1,4 +1,5 @@
    > - - - -
    - $option) : ?> - - disable) ? 'disabled' : ''; - $style = $disabled ? ' style="pointer-events: none"' : ''; + + + +
    + $option) : ?> + + disable) ? 'disabled' : ''; + $style = $disabled ? ' style="pointer-events: none"' : ''; - // Initialize some option attributes. - if ($isBtnYesNo) - { - // Set the button classes for the yes/no group - switch ($option->value) - { - case '0': - $btnClass = 'btn btn-outline-danger'; - break; - case '1': - $btnClass = 'btn btn-outline-success'; - break; - default: - $btnClass = 'btn btn-outline-secondary'; - break; - } - } + // Initialize some option attributes. + if ($isBtnYesNo) { + // Set the button classes for the yes/no group + switch ($option->value) { + case '0': + $btnClass = 'btn btn-outline-danger'; + break; + case '1': + $btnClass = 'btn btn-outline-success'; + break; + default: + $btnClass = 'btn btn-outline-secondary'; + break; + } + } - $optionClass = !empty($option->class) ? $option->class : $btnClass; - $optionClass = trim($optionClass . ' ' . $disabled); - $checked = ((string) $option->value === $value) ? 'checked="checked"' : ''; + $optionClass = !empty($option->class) ? $option->class : $btnClass; + $optionClass = trim($optionClass . ' ' . $disabled); + $checked = ((string) $option->value === $value) ? 'checked="checked"' : ''; - // Initialize some JavaScript option attributes. - $onclick = !empty($option->onclick) ? 'onclick="' . $option->onclick . '"' : ''; - $onchange = !empty($option->onchange) ? 'onchange="' . $option->onchange . '"' : ''; - $oid = $id . $i; - $ovalue = htmlspecialchars($option->value, ENT_COMPAT, 'UTF-8'); - $attributes = array_filter(array($checked, $disabled, ltrim($style), $onchange, $onclick)); - ?> - - - - > - - - -
    + // Initialize some JavaScript option attributes. + $onclick = !empty($option->onclick) ? 'onclick="' . $option->onclick . '"' : ''; + $onchange = !empty($option->onchange) ? 'onchange="' . $option->onchange . '"' : ''; + $oid = $id . $i; + $ovalue = htmlspecialchars($option->value, ENT_COMPAT, 'UTF-8'); + $attributes = array_filter(array($checked, $disabled, ltrim($style), $onchange, $onclick)); + ?> + + + + > + + + +
    diff --git a/layouts/joomla/form/field/radio/switcher.php b/layouts/joomla/form/field/radio/switcher.php index f1070fa6df77b..191a5a07850a1 100644 --- a/layouts/joomla/form/field/radio/switcher.php +++ b/layouts/joomla/form/field/radio/switcher.php @@ -1,4 +1,5 @@
    > - - - -
    - $option) : ?> - value == '0') - { - $value = '0'; - } + + + +
    + $option) : ?> + value == '0') { + $value = '0'; + } - // Initialize some option attributes. - $optionValue = (string) $option->value; - $optionId = $id . $i; - $attributes = $optionValue == $value ? 'checked class="active ' . $class . '"' : ($class ? 'class="' . $class . '"' : ''); - $attributes .= $optionValue != $value && $readonly || $disabled ? ' disabled' : ''; - ?> - escape($optionValue), $attributes); ?> - ' . $option->text . ''; ?> - - -
    + // Initialize some option attributes. + $optionValue = (string) $option->value; + $optionId = $id . $i; + $attributes = $optionValue == $value ? 'checked class="active ' . $class . '"' : ($class ? 'class="' . $class . '"' : ''); + $attributes .= $optionValue != $value && $readonly || $disabled ? ' disabled' : ''; + ?> + escape($optionValue), $attributes); ?> + ' . $option->text . ''; ?> + + +
    diff --git a/layouts/joomla/form/field/radiobasic.php b/layouts/joomla/form/field/radiobasic.php index e28959c7d8be4..45f1aa8abc3c9 100644 --- a/layouts/joomla/form/field/radiobasic.php +++ b/layouts/joomla/form/field/radiobasic.php @@ -1,4 +1,5 @@
    - - - > + + + + > - - $option) : ?> - value === $value) ? 'checked="checked"' : ''; - $optionClass = !empty($option->class) ? 'class="' . $option->class . '"' : ''; - $disabled = !empty($option->disable) || ($disabled && !$checked) ? 'disabled' : ''; + + $option) : ?> + value === $value) ? 'checked="checked"' : ''; + $optionClass = !empty($option->class) ? 'class="' . $option->class . '"' : ''; + $disabled = !empty($option->disable) || ($disabled && !$checked) ? 'disabled' : ''; - // Initialize some JavaScript option attributes. - $onclick = !empty($option->onclick) ? 'onclick="' . $option->onclick . '"' : ''; - $onchange = !empty($option->onchange) ? 'onchange="' . $option->onchange . '"' : ''; - $oid = $id . $i; - $ovalue = htmlspecialchars($option->value, ENT_COMPAT, 'UTF-8'); - $attributes = array_filter(array($checked, $optionClass, $disabled, $onchange, $onclick)); - ?> - - - -
    - -
    - - + // Initialize some JavaScript option attributes. + $onclick = !empty($option->onclick) ? 'onclick="' . $option->onclick . '"' : ''; + $onchange = !empty($option->onchange) ? 'onchange="' . $option->onchange . '"' : ''; + $oid = $id . $i; + $ovalue = htmlspecialchars($option->value, ENT_COMPAT, 'UTF-8'); + $attributes = array_filter(array($checked, $optionClass, $disabled, $onchange, $onclick)); + ?> + + + +
    + +
    + +
    diff --git a/layouts/joomla/form/field/range.php b/layouts/joomla/form/field/range.php index 3e07863d863c3..9e5f0a311ac1b 100644 --- a/layouts/joomla/form/field/range.php +++ b/layouts/joomla/form/field/range.php @@ -1,4 +1,5 @@ > + type="range" + name="" + id="" + value="" + > diff --git a/layouts/joomla/form/field/rules.php b/layouts/joomla/form/field/rules.php index 90df4535ad6e5..ee1c065ba3bd0 100644 --- a/layouts/joomla/form/field/rules.php +++ b/layouts/joomla/form/field/rules.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useStyle('webcomponent.field-permissions') - ->useScript('webcomponent.field-permissions') - ->useStyle('webcomponent.joomla-tab') - ->useScript('webcomponent.joomla-tab'); + ->useStyle('webcomponent.field-permissions') + ->useScript('webcomponent.field-permissions') + ->useStyle('webcomponent.joomla-tab') + ->useScript('webcomponent.joomla-tab'); // Load JavaScript message titles Text::script('ERROR'); @@ -79,171 +80,155 @@
    - - - -
    - -
    + + + +
    + +
    > - - - - value === 1 ? ' active' : ''; ?> - name=" $group->level + 1)), ENT_COMPAT, 'utf-8') . $group->text; ?>" id="permission-value; ?>"> - - - - - - - - - - - - - - value, 'core.admin'); ?> - - - - - - - - - -
    - - - - - -
    - - description)) : ?> - - - -
    -   - -
    -
    - - - value, $action->name, $assetId); - $inheritedGroupParentAssetRule = !empty($parentAssetId) ? Access::checkGroup($group->value, $action->name, $parentAssetId) : null; - $inheritedParentGroupRule = !empty($group->parent_id) ? Access::checkGroup($group->parent_id, $action->name, $assetId) : null; - - // Current group is a Super User group, so calculated setting is "Allowed (Super User)". - if ($isSuperUserGroup) - { - $result['class'] = 'badge bg-success'; - $result['text'] = '' . Text::_('JLIB_RULES_ALLOWED_ADMIN'); - } - else - { - // First get the real recursive calculated setting and add (Inherited) to it. - - // If recursive calculated setting is "Denied" or null. Calculated permission is "Not Allowed (Inherited)". - if ($inheritedGroupRule === null || $inheritedGroupRule === false) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_INHERITED'); - } - // If recursive calculated setting is "Allowed". Calculated permission is "Allowed (Inherited)". - else - { - $result['class'] = 'badge bg-success'; - $result['text'] = Text::_('JLIB_RULES_ALLOWED_INHERITED'); - } - - // Second part: Overwrite the calculated permissions labels if there is an explicit permission in the current group. - - /** - * @todo: incorrect info - * If a component has a permission that doesn't exists in global config (ex: frontend editing in com_modules) by default - * we get "Not Allowed (Inherited)" when we should get "Not Allowed (Default)". - */ - - // If there is an explicit permission "Not Allowed". Calculated permission is "Not Allowed". - if ($assetRule === false) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED'); - } - // If there is an explicit permission is "Allowed". Calculated permission is "Allowed". - elseif ($assetRule === true) - { - $result['class'] = 'badge bg-success'; - $result['text'] = Text::_('JLIB_RULES_ALLOWED'); - } - - // Third part: Overwrite the calculated permissions labels for special cases. - - // Global configuration with "Not Set" permission. Calculated permission is "Not Allowed (Default)". - if (empty($group->parent_id) && $isGlobalConfig === true && $assetRule === null) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_DEFAULT'); - } - - /** - * Component/Item with explicit "Denied" permission at parent Asset (Category, Component or Global config) configuration. - * Or some parent group has an explicit "Denied". - * Calculated permission is "Not Allowed (Locked)". - */ - elseif ($inheritedGroupParentAssetRule === false || $inheritedParentGroupRule === false) - { - $result['class'] = 'badge bg-danger'; - $result['text'] = ''. Text::_('JLIB_RULES_NOT_ALLOWED_LOCKED'); - } - } - ?> - -
    -
    - -
    + + + + value === 1 ? ' active' : ''; ?> + name=" $group->level + 1)), ENT_COMPAT, 'utf-8') . $group->text; ?>" id="permission-value; ?>"> + + + + + + + + + + + + + + value, 'core.admin'); ?> + + + + + + + + + +
    + + + + + +
    + + description)) : ?> + + + +
    +   + +
    +
    + + + value, $action->name, $assetId); + $inheritedGroupParentAssetRule = !empty($parentAssetId) ? Access::checkGroup($group->value, $action->name, $parentAssetId) : null; + $inheritedParentGroupRule = !empty($group->parent_id) ? Access::checkGroup($group->parent_id, $action->name, $assetId) : null; + + // Current group is a Super User group, so calculated setting is "Allowed (Super User)". + if ($isSuperUserGroup) { + $result['class'] = 'badge bg-success'; + $result['text'] = '' . Text::_('JLIB_RULES_ALLOWED_ADMIN'); + } else { + // First get the real recursive calculated setting and add (Inherited) to it. + + // If recursive calculated setting is "Denied" or null. Calculated permission is "Not Allowed (Inherited)". + if ($inheritedGroupRule === null || $inheritedGroupRule === false) { + $result['class'] = 'badge bg-danger'; + $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_INHERITED'); + } else { + // If recursive calculated setting is "Allowed". Calculated permission is "Allowed (Inherited)". + $result['class'] = 'badge bg-success'; + $result['text'] = Text::_('JLIB_RULES_ALLOWED_INHERITED'); + } + + // Second part: Overwrite the calculated permissions labels if there is an explicit permission in the current group. + + /** + * @todo: incorrect info + * If a component has a permission that doesn't exists in global config (ex: frontend editing in com_modules) by default + * we get "Not Allowed (Inherited)" when we should get "Not Allowed (Default)". + */ + + // If there is an explicit permission "Not Allowed". Calculated permission is "Not Allowed". + if ($assetRule === false) { + $result['class'] = 'badge bg-danger'; + $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED'); + } elseif ($assetRule === true) { + // If there is an explicit permission is "Allowed". Calculated permission is "Allowed". + $result['class'] = 'badge bg-success'; + $result['text'] = Text::_('JLIB_RULES_ALLOWED'); + } + + // Third part: Overwrite the calculated permissions labels for special cases. + + // Global configuration with "Not Set" permission. Calculated permission is "Not Allowed (Default)". + if (empty($group->parent_id) && $isGlobalConfig === true && $assetRule === null) { + $result['class'] = 'badge bg-danger'; + $result['text'] = Text::_('JLIB_RULES_NOT_ALLOWED_DEFAULT'); + } elseif ($inheritedGroupParentAssetRule === false || $inheritedParentGroupRule === false) { + /** + * Component/Item with explicit "Denied" permission at parent Asset (Category, Component or Global config) configuration. + * Or some parent group has an explicit "Denied". + * Calculated permission is "Not Allowed (Locked)". + */ + $result['class'] = 'badge bg-danger'; + $result['text'] = '' . Text::_('JLIB_RULES_NOT_ALLOWED_LOCKED'); + } + } + ?> + +
    +
    + +
    diff --git a/layouts/joomla/form/field/subform/default.php b/layouts/joomla/form/field/subform/default.php index 6afbd05d248cf..b93687284cdc3 100644 --- a/layouts/joomla/form/field/subform/default.php +++ b/layouts/joomla/form/field/subform/default.php @@ -1,4 +1,5 @@ getGroup('') as $field) : ?> - renderField(); ?> + renderField(); ?> diff --git a/layouts/joomla/form/field/subform/repeatable-table.php b/layouts/joomla/form/field/subform/repeatable-table.php index e2aa4a00d4010..7625282a9eebc 100644 --- a/layouts/joomla/form/field/subform/repeatable-table.php +++ b/layouts/joomla/form/field/subform/repeatable-table.php @@ -1,4 +1,5 @@ getDocument() - ->getWebAssetManager() - ->useScript('webcomponent.field-subform'); +if ($multiple) { + // Add script + Factory::getApplication() + ->getDocument() + ->getWebAssetManager() + ->useScript('webcomponent.field-subform'); } $class = $class ? ' ' . $class : ''; @@ -47,82 +47,77 @@ // Build heading $table_head = ''; -if (!empty($groupByFieldset)) -{ - foreach ($tmpl->getFieldsets() as $fieldset) { - $table_head .= '' . Text::_($fieldset->label); +if (!empty($groupByFieldset)) { + foreach ($tmpl->getFieldsets() as $fieldset) { + $table_head .= '' . Text::_($fieldset->label); - if ($fieldset->description) - { - $table_head .= ''; - } + if ($fieldset->description) { + $table_head .= ''; + } - $table_head .= ''; - } + $table_head .= ''; + } - $sublayout = 'section-byfieldsets'; -} -else -{ - foreach ($tmpl->getGroup('') as $field) { - $table_head .= '' . strip_tags($field->label); + $sublayout = 'section-byfieldsets'; +} else { + foreach ($tmpl->getGroup('') as $field) { + $table_head .= '' . strip_tags($field->label); - if ($field->description) - { - $table_head .= ''; - } + if ($field->description) { + $table_head .= ''; + } - $table_head .= ''; - } + $table_head .= ''; + } - $sublayout = 'section'; + $sublayout = 'section'; - // Label will not be shown for sections layout, so reset the margin left - Factory::getApplication() - ->getDocument() - ->addStyleDeclaration('.subform-table-sublayout-section .controls { margin-left: 0px }'); + // Label will not be shown for sections layout, so reset the margin left + Factory::getApplication() + ->getDocument() + ->addStyleDeclaration('.subform-table-sublayout-section .controls { margin-left: 0px }'); } ?>
    - -
    - - - - - - - - - - - - $form) : - echo $this->sublayout($sublayout, array('form' => $form, 'basegroup' => $fieldname, 'group' => $fieldname . $k, 'buttons' => $buttons)); - endforeach; - ?> - -
    - -
    - -
    - -
    - -
    -
    - - - -
    + +
    + + + + + + + + + + + + $form) : + echo $this->sublayout($sublayout, array('form' => $form, 'basegroup' => $fieldname, 'group' => $fieldname . $k, 'buttons' => $buttons)); + endforeach; + ?> + +
    + +
    + +
    + +
    + +
    +
    + + + +
    diff --git a/layouts/joomla/form/field/subform/repeatable-table/section-byfieldsets.php b/layouts/joomla/form/field/subform/repeatable-table/section-byfieldsets.php index ba2730f211656..a3ff486d387d1 100644 --- a/layouts/joomla/form/field/subform/repeatable-table/section-byfieldsets.php +++ b/layouts/joomla/form/field/subform/repeatable-table/section-byfieldsets.php @@ -1,4 +1,5 @@ - getFieldsets() as $fieldset) : ?> - - getFieldset($fieldset->name) as $field) : ?> - renderField(); ?> - - - - - -
    - - - - - - - - - -
    - - + getFieldsets() as $fieldset) : ?> + + getFieldset($fieldset->name) as $field) : ?> + renderField(); ?> + + + + + +
    + + + + + + + + + +
    + + diff --git a/layouts/joomla/form/field/subform/repeatable-table/section.php b/layouts/joomla/form/field/subform/repeatable-table/section.php index 3435f1d47403c..ccef5fc9cd47b 100644 --- a/layouts/joomla/form/field/subform/repeatable-table/section.php +++ b/layouts/joomla/form/field/subform/repeatable-table/section.php @@ -1,4 +1,5 @@ - getGroup('') as $field) : ?> - - renderField(array('hiddenLabel' => true, 'hiddenDescription' => true)); ?> - - - - -
    - - - - - - - - - -
    - - + getGroup('') as $field) : ?> + + renderField(array('hiddenLabel' => true, 'hiddenDescription' => true)); ?> + + + + +
    + + + + + + + + + +
    + + diff --git a/layouts/joomla/form/field/subform/repeatable.php b/layouts/joomla/form/field/subform/repeatable.php index 0d272e721aa41..a2504bbc3af36 100644 --- a/layouts/joomla/form/field/subform/repeatable.php +++ b/layouts/joomla/form/field/subform/repeatable.php @@ -1,4 +1,5 @@ getDocument() - ->getWebAssetManager() - ->useScript('webcomponent.field-subform'); +if ($multiple) { + // Add script + Factory::getApplication() + ->getDocument() + ->getWebAssetManager() + ->useScript('webcomponent.field-subform'); } $class = $class ? ' ' . $class : ''; @@ -48,27 +48,27 @@ ?>
    - - -
    -
    - -
    -
    - - $form) : - echo $this->sublayout($sublayout, array('form' => $form, 'basegroup' => $fieldname, 'group' => $fieldname . $k, 'buttons' => $buttons)); - endforeach; - ?> - - - -
    + + +
    +
    + +
    +
    + + $form) : + echo $this->sublayout($sublayout, array('form' => $form, 'basegroup' => $fieldname, 'group' => $fieldname . $k, 'buttons' => $buttons)); + endforeach; + ?> + + + +
    diff --git a/layouts/joomla/form/field/subform/repeatable/section-byfieldsets.php b/layouts/joomla/form/field/subform/repeatable/section-byfieldsets.php index 0abf0645bb01c..4710dc0b04540 100644 --- a/layouts/joomla/form/field/subform/repeatable/section-byfieldsets.php +++ b/layouts/joomla/form/field/subform/repeatable/section-byfieldsets.php @@ -1,4 +1,5 @@
    - -
    -
    - - - -
    -
    - -
    - getFieldsets() as $fieldset) : ?> -
    - label)) : ?> - label); ?> - - getFieldset($fieldset->name) as $field) : ?> - renderField(); ?> - -
    - -
    + +
    +
    + + + +
    +
    + +
    + getFieldsets() as $fieldset) : ?> +
    + label)) : ?> + label); ?> + + getFieldset($fieldset->name) as $field) : ?> + renderField(); ?> + +
    + +
    diff --git a/layouts/joomla/form/field/subform/repeatable/section.php b/layouts/joomla/form/field/subform/repeatable/section.php index e3693ca5d1ad2..7eb4826223d2c 100644 --- a/layouts/joomla/form/field/subform/repeatable/section.php +++ b/layouts/joomla/form/field/subform/repeatable/section.php @@ -1,4 +1,5 @@
    - -
    -
    - - - -
    -
    - + +
    +
    + + + +
    +
    + getGroup('') as $field) : ?> - renderField(); ?> + renderField(); ?>
    diff --git a/layouts/joomla/form/field/tag.php b/layouts/joomla/form/field/tag.php index 1c6de774d47be..e7a586c6e9bdf 100644 --- a/layouts/joomla/form/field/tag.php +++ b/layouts/joomla/form/field/tag.php @@ -1,4 +1,5 @@ escape($hint ?: Text::_('JGLOBAL_TYPE_OR_SELECT_SOME_TAGS')) . '" '; $attr2 .= $dataAttribute; -if ($allowCustom) -{ - $attr2 .= $allowCustom ? ' allow-custom' : ''; - $attr2 .= $allowCustom ? ' new-item-prefix="#new#"' : ''; +if ($allowCustom) { + $attr2 .= $allowCustom ? ' allow-custom' : ''; + $attr2 .= $allowCustom ? ' new-item-prefix="#new#"' : ''; } -if ($remoteSearch) -{ - $attr2 .= ' remote-search'; - $attr2 .= ' url="' . Uri::root(true) . '/index.php?option=com_tags&task=tags.searchAjax"'; - $attr2 .= ' term-key="like"'; - $attr2 .= ' min-term-length="' . $minTermLength . '"'; +if ($remoteSearch) { + $attr2 .= ' remote-search'; + $attr2 .= ' url="' . Uri::root(true) . '/index.php?option=com_tags&task=tags.searchAjax"'; + $attr2 .= ' term-key="like"'; + $attr2 .= ' min-term-length="' . $minTermLength . '"'; } -if ($required) -{ - $attr .= ' required class="required"'; - $attr2 .= ' required'; +if ($required) { + $attr .= ' required class="required"'; + $attr2 .= ' required'; } // Create a read-only list (no name) with hidden input(s) to store the value(s). -if ($readonly) -{ - $html[] = HTMLHelper::_('select.genericlist', $options, '', trim($attr), 'value', 'text', $value, $id); - - // E.g. form field type tag sends $this->value as array - if ($multiple && is_array($value)) - { - if (!count($value)) - { - $value[] = ''; - } - - foreach ($value as $val) - { - $html[] = ''; - } - } - else - { - $html[] = ''; - } -} -else -// Create a regular list. +if ($readonly) { + $html[] = HTMLHelper::_('select.genericlist', $options, '', trim($attr), 'value', 'text', $value, $id); + + // E.g. form field type tag sends $this->value as array + if ($multiple && is_array($value)) { + if (!count($value)) { + $value[] = ''; + } + + foreach ($value as $val) { + $html[] = ''; + } + } else { + $html[] = ''; + } +} else // Create a regular list. { - $html[] = HTMLHelper::_('select.genericlist', $options, $name, trim($attr), 'value', 'text', $value, $id); + $html[] = HTMLHelper::_('select.genericlist', $options, $name, trim($attr), 'value', 'text', $value, $id); } Text::script('JGLOBAL_SELECT_NO_RESULTS_MATCH'); Text::script('JGLOBAL_SELECT_PRESS_TO_SELECT'); Factory::getDocument()->getWebAssetManager() - ->usePreset('choicesjs') - ->useScript('webcomponent.field-fancy-select'); + ->usePreset('choicesjs') + ->useScript('webcomponent.field-fancy-select'); ?> diff --git a/layouts/joomla/form/field/tel.php b/layouts/joomla/form/field/tel.php index ddf0735ed522b..122b1e6dc83cf 100644 --- a/layouts/joomla/form/field/tel.php +++ b/layouts/joomla/form/field/tel.php @@ -1,4 +1,5 @@ - id="" - value="" - > + type="tel" + inputmode="tel" + name="" + + id="" + value="" + > diff --git a/layouts/joomla/form/field/text.php b/layouts/joomla/form/field/text.php index 300f2789ec8d5..a01f49309a8bf 100644 --- a/layouts/joomla/form/field/text.php +++ b/layouts/joomla/form/field/text.php @@ -1,4 +1,5 @@ ' . Text::_($addonBefore) . ''; @@ -88,33 +88,33 @@
    - - - + + + - - > + + > - - - + + +
    - - - value) : ?> - - - - - + + + value) : ?> + + + + + diff --git a/layouts/joomla/form/field/textarea.php b/layouts/joomla/form/field/textarea.php index dcbd3f994fd70..ceb57100957e8 100644 --- a/layouts/joomla/form/field/textarea.php +++ b/layouts/joomla/form/field/textarea.php @@ -1,4 +1,5 @@ getDocument()->getWebAssetManager(); - $wa->useScript('short-and-sweet'); +if ($charcounter) { + // Load the js file + /** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + $wa->useScript('short-and-sweet'); - // Set the css class to be used as the trigger - $charcounter = ' charcount'; - // Set the text - $counterlabel = 'data-counter-label="' . $this->escape(Text::_('JFIELD_META_DESCRIPTION_COUNTER')) . '"'; + // Set the css class to be used as the trigger + $charcounter = ' charcount'; + // Set the text + $counterlabel = 'data-counter-label="' . $this->escape(Text::_('JFIELD_META_DESCRIPTION_COUNTER')) . '"'; } $attributes = array( - $columns ?: '', - $rows ?: '', - !empty($class) ? 'class="form-control ' . $class . $charcounter . '"' : 'class="form-control' . $charcounter . '"', - !empty($description) ? 'aria-describedby="' . ($id ?: $name) . '-desc"' : '', - strlen($hint) ? 'placeholder="' . htmlspecialchars($hint, ENT_COMPAT, 'UTF-8') . '"' : '', - $disabled ? 'disabled' : '', - $readonly ? 'readonly' : '', - $onchange ? 'onchange="' . $onchange . '"' : '', - $onclick ? 'onclick="' . $onclick . '"' : '', - $required ? 'required' : '', - !empty($autocomplete) ? 'autocomplete="' . $autocomplete . '"' : '', - $autofocus ? 'autofocus' : '', - $spellcheck ? '' : 'spellcheck="false"', - $maxlength ?: '', - !empty($counterlabel) ? $counterlabel : '', - $dataAttribute, + $columns ?: '', + $rows ?: '', + !empty($class) ? 'class="form-control ' . $class . $charcounter . '"' : 'class="form-control' . $charcounter . '"', + !empty($description) ? 'aria-describedby="' . ($id ?: $name) . '-desc"' : '', + strlen($hint) ? 'placeholder="' . htmlspecialchars($hint, ENT_COMPAT, 'UTF-8') . '"' : '', + $disabled ? 'disabled' : '', + $readonly ? 'readonly' : '', + $onchange ? 'onchange="' . $onchange . '"' : '', + $onclick ? 'onclick="' . $onclick . '"' : '', + $required ? 'required' : '', + !empty($autocomplete) ? 'autocomplete="' . $autocomplete . '"' : '', + $autofocus ? 'autofocus' : '', + $spellcheck ? '' : 'spellcheck="false"', + $maxlength ?: '', + !empty($counterlabel) ? $counterlabel : '', + $dataAttribute, ); ?> diff --git a/layouts/joomla/tinymce/togglebutton.php b/layouts/joomla/tinymce/togglebutton.php index 6b19fc1efc18a..d963552b96c58 100644 --- a/layouts/joomla/tinymce/togglebutton.php +++ b/layouts/joomla/tinymce/togglebutton.php @@ -1,4 +1,5 @@
    -
    - -
    +
    + +
    diff --git a/layouts/joomla/toolbar/base.php b/layouts/joomla/toolbar/base.php index b27e92096da46..dfba1abbcd6e0 100644 --- a/layouts/joomla/toolbar/base.php +++ b/layouts/joomla/toolbar/base.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useScript('core') - ->useScript('webcomponent.toolbar-button'); + ->useScript('core') + ->useScript('webcomponent.toolbar-button'); extract($displayData, EXTR_OVERWRITE); @@ -47,35 +48,31 @@ $validate = !empty($formValidation) ? ' form-validation' : ''; $msgAttr = !empty($message) ? ' confirm-message="' . $this->escape($message) . '"' : ''; -if ($id === 'toolbar-help') -{ - $title = ' title="' . Text::_('JGLOBAL_OPENS_IN_A_NEW_WINDOW') . '"'; +if ($id === 'toolbar-help') { + $title = ' title="' . Text::_('JGLOBAL_OPENS_IN_A_NEW_WINDOW') . '"'; } -if (!empty($task)) -{ - $taskAttr = ' task="' . $task . '"'; -} -elseif (!empty($onclick)) -{ - $htmlAttributes .= ' onclick="' . $onclick . '"'; +if (!empty($task)) { + $taskAttr = ' task="' . $task . '"'; +} elseif (!empty($onclick)) { + $htmlAttributes .= ' onclick="' . $onclick . '"'; } $direction = Factory::getLanguage()->isRtl() ? 'dropdown-menu-end' : ''; ?> > < - class="" - - - > - - + class="" + + + > + + > - - + + diff --git a/layouts/joomla/toolbar/batch.php b/layouts/joomla/toolbar/batch.php index f9cd16ed06f71..a3d2cbc4604a2 100644 --- a/layouts/joomla/toolbar/batch.php +++ b/layouts/joomla/toolbar/batch.php @@ -1,4 +1,5 @@ type="button" onclick="if (document.adminForm.boxchecked.value==0){}else{document.getElementById('collapseModal').open(); return true;}" class="btn btn-primary"> - - + + diff --git a/layouts/joomla/toolbar/containerclose.php b/layouts/joomla/toolbar/containerclose.php index 8c687c4a992ef..aa9e8e5bb13cd 100644 --- a/layouts/joomla/toolbar/containerclose.php +++ b/layouts/joomla/toolbar/containerclose.php @@ -1,4 +1,5 @@ - - - - - - - - - + + + + + + + + + diff --git a/layouts/joomla/toolbar/iconclass.php b/layouts/joomla/toolbar/iconclass.php index 28a101b0c7b98..4a26e57453bf5 100644 --- a/layouts/joomla/toolbar/iconclass.php +++ b/layouts/joomla/toolbar/iconclass.php @@ -1,4 +1,5 @@ getDocument() - ->getWebAssetManager()->useScript('inlinehelp'); + ->getWebAssetManager()->useScript('inlinehelp'); echo LayoutHelper::render('joomla.toolbar.standard', $displayData); diff --git a/layouts/joomla/toolbar/link.php b/layouts/joomla/toolbar/link.php index ff89f87fba0c5..6ee810135850b 100644 --- a/layouts/joomla/toolbar/link.php +++ b/layouts/joomla/toolbar/link.php @@ -1,4 +1,5 @@ - - > - - - + + > + + + diff --git a/layouts/joomla/toolbar/popup.php b/layouts/joomla/toolbar/popup.php index 25dcd392453b1..3ee4f2198e441 100644 --- a/layouts/joomla/toolbar/popup.php +++ b/layouts/joomla/toolbar/popup.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useScript('core') - ->useScript('webcomponent.toolbar-button'); + ->useScript('core') + ->useScript('webcomponent.toolbar-button'); $tagName = $tagName ?? 'button'; @@ -43,12 +44,12 @@ ?> > < - value="" - class="" - - + value="" + class="" + + > - - + + > diff --git a/layouts/joomla/toolbar/separator.php b/layouts/joomla/toolbar/separator.php index dc5970af51b97..2adc1fc26e5be 100644 --- a/layouts/joomla/toolbar/separator.php +++ b/layouts/joomla/toolbar/separator.php @@ -1,4 +1,5 @@ - - - - - - + + + + + + diff --git a/layouts/joomla/toolbar/standard.php b/layouts/joomla/toolbar/standard.php index f7dbff435d94d..a7d18c83685b6 100644 --- a/layouts/joomla/toolbar/standard.php +++ b/layouts/joomla/toolbar/standard.php @@ -1,4 +1,5 @@ getWebAssetManager() - ->useScript('core') - ->useScript('webcomponent.toolbar-button'); + ->useScript('core') + ->useScript('webcomponent.toolbar-button'); $tagName = $tagName ?? 'button'; @@ -43,13 +44,10 @@ $validate = !empty($formValidation) ? ' form-validation' : ''; $msgAttr = !empty($message) ? ' confirm-message="' . $this->escape($message) . '"' : ''; -if (!empty($task)) -{ - $taskAttr = ' task="' . $task . '"'; -} -elseif (!empty($onclick)) -{ - $htmlAttributes .= ' onclick="' . $onclick . '"'; +if (!empty($task)) { + $taskAttr = ' task="' . $task . '"'; +} elseif (!empty($onclick)) { + $htmlAttributes .= ' onclick="' . $onclick . '"'; } ?> @@ -57,16 +55,16 @@ > - - + + < - class="" - - > - - + class="" + + > + + > diff --git a/layouts/joomla/toolbar/title.php b/layouts/joomla/toolbar/title.php index ad2c9a6075344..2f2fb21c0df33 100644 --- a/layouts/joomla/toolbar/title.php +++ b/layouts/joomla/toolbar/title.php @@ -1,4 +1,5 @@

    - $icon]); ?> - + $icon]); ?> +

    diff --git a/layouts/joomla/toolbar/versions.php b/layouts/joomla/toolbar/versions.php index 15d47b41d85b6..68a56dd768c38 100644 --- a/layouts/joomla/toolbar/versions.php +++ b/layouts/joomla/toolbar/versions.php @@ -1,4 +1,5 @@ getRegistry()->addExtensionRegistryFile('com_contenthistory'); $wa->useScript('core') - ->useScript('webcomponent.toolbar-button') - ->useScript('com_contenthistory.admin-history-versions'); + ->useScript('webcomponent.toolbar-button') + ->useScript('com_contenthistory.admin-history-versions'); echo HTMLHelper::_( - 'bootstrap.renderModal', - 'versionsModal', - array( - 'url' => 'index.php?' . http_build_query( - [ - 'option' => 'com_contenthistory', - 'view' => 'history', - 'layout' => 'modal', - 'tmpl' => 'component', - 'item_id' => $itemId, - Session::getFormToken() => 1 - ] - ), - 'title' => $title, - 'height' => '100%', - 'width' => '100%', - 'modalWidth' => '80', - 'bodyHeight' => '60', - 'footer' => '' - ) + 'bootstrap.renderModal', + 'versionsModal', + array( + 'url' => 'index.php?' . http_build_query( + [ + 'option' => 'com_contenthistory', + 'view' => 'history', + 'layout' => 'modal', + 'tmpl' => 'component', + 'item_id' => $itemId, + Session::getFormToken() => 1 + ] + ), + 'title' => $title, + 'height' => '100%', + 'width' => '100%', + 'modalWidth' => '80', + 'bodyHeight' => '60', + 'footer' => '' + ) ); ?> - + diff --git a/layouts/libraries/html/bootstrap/modal/body.php b/layouts/libraries/html/bootstrap/modal/body.php index 26dc032fcc01e..9bac638c53749 100644 --- a/layouts/libraries/html/bootstrap/modal/body.php +++ b/layouts/libraries/html/bootstrap/modal/body.php @@ -1,4 +1,5 @@ = 20 && $bodyHeight < 90) -{ - $bodyClass .= ' jviewport-height' . $bodyHeight; +if ($bodyHeight && $bodyHeight >= 20 && $bodyHeight < 90) { + $bodyClass .= ' jviewport-height' . $bodyHeight; } ?>
    - +
    diff --git a/layouts/libraries/html/bootstrap/modal/footer.php b/layouts/libraries/html/bootstrap/modal/footer.php index 78f11f45d95ec..8dd0f62c21669 100644 --- a/layouts/libraries/html/bootstrap/modal/footer.php +++ b/layouts/libraries/html/bootstrap/modal/footer.php @@ -1,4 +1,5 @@ diff --git a/layouts/libraries/html/bootstrap/modal/header.php b/layouts/libraries/html/bootstrap/modal/header.php index 61d7c7265f5e9..0b03945234a68 100644 --- a/layouts/libraries/html/bootstrap/modal/header.php +++ b/layouts/libraries/html/bootstrap/modal/header.php @@ -1,4 +1,5 @@ diff --git a/layouts/libraries/html/bootstrap/modal/iframe.php b/layouts/libraries/html/bootstrap/modal/iframe.php index 468b04ce65a05..db4911b45b9b9 100644 --- a/layouts/libraries/html/bootstrap/modal/iframe.php +++ b/layouts/libraries/html/bootstrap/modal/iframe.php @@ -1,4 +1,5 @@ 'iframe', - 'src' => $params['url'] + 'class' => 'iframe', + 'src' => $params['url'] ); -if (isset($params['title'])) -{ - $iframeAttributes['name'] = addslashes($params['title']); - $iframeAttributes['title'] = addslashes($params['title']); +if (isset($params['title'])) { + $iframeAttributes['name'] = addslashes($params['title']); + $iframeAttributes['title'] = addslashes($params['title']); } -if (isset($params['height'])) -{ - $iframeAttributes['height'] = $params['height']; +if (isset($params['height'])) { + $iframeAttributes['height'] = $params['height']; } -if (isset($params['width'])) -{ - $iframeAttributes['width'] = $params['width']; +if (isset($params['width'])) { + $iframeAttributes['width'] = $params['width']; } ?> diff --git a/layouts/libraries/html/bootstrap/modal/main.php b/layouts/libraries/html/bootstrap/modal/main.php index 3cdbabb0e9ade..2f0fe32b0ab92 100644 --- a/layouts/libraries/html/bootstrap/modal/main.php +++ b/layouts/libraries/html/bootstrap/modal/main.php @@ -1,4 +1,5 @@ 0 && $modalWidth <= 100) -{ - $modalDialogClass = ' jviewport-width' . $modalWidth; +if ($modalWidth && $modalWidth > 0 && $modalWidth <= 100) { + $modalDialogClass = ' jviewport-width' . $modalWidth; } $modalAttributes = array( - 'tabindex' => '-1', - 'class' => 'joomla-modal ' .implode(' ', $modalClasses) + 'tabindex' => '-1', + 'class' => 'joomla-modal ' . implode(' ', $modalClasses) ); -if (isset($params['backdrop'])) -{ - $modalAttributes['data-bs-backdrop'] = (is_bool($params['backdrop']) ? ($params['backdrop'] ? 'true' : 'false') : $params['backdrop']); +if (isset($params['backdrop'])) { + $modalAttributes['data-bs-backdrop'] = (is_bool($params['backdrop']) ? ($params['backdrop'] ? 'true' : 'false') : $params['backdrop']); } -if (isset($params['keyboard'])) -{ - $modalAttributes['data-bs-keyboard'] = (is_bool($params['keyboard']) ? ($params['keyboard'] ? 'true' : 'false') : 'true'); +if (isset($params['keyboard'])) { + $modalAttributes['data-bs-keyboard'] = (is_bool($params['keyboard']) ? ($params['keyboard'] ? 'true' : 'false') : 'true'); } -if (isset($params['url'])) -{ - $url = 'data-url="' . $params['url'] . '"'; - $iframeHtml = htmlspecialchars(LayoutHelper::render('libraries.html.bootstrap.modal.iframe', $displayData), ENT_COMPAT, 'UTF-8'); +if (isset($params['url'])) { + $url = 'data-url="' . $params['url'] . '"'; + $iframeHtml = htmlspecialchars(LayoutHelper::render('libraries.html.bootstrap.modal.iframe', $displayData), ENT_COMPAT, 'UTF-8'); } ?> - diff --git a/layouts/libraries/html/bootstrap/tab/addtab.php b/layouts/libraries/html/bootstrap/tab/addtab.php index 286b32bcb7214..a882acda2c95b 100644 --- a/layouts/libraries/html/bootstrap/tab/addtab.php +++ b/layouts/libraries/html/bootstrap/tab/addtab.php @@ -1,4 +1,5 @@
    + class="tab-pane" + data-active="" + data-id="" + data-title=""> diff --git a/layouts/libraries/html/bootstrap/tab/endtab.php b/layouts/libraries/html/bootstrap/tab/endtab.php index 84bdf84dac664..4b21cdf6045f5 100644 --- a/layouts/libraries/html/bootstrap/tab/endtab.php +++ b/layouts/libraries/html/bootstrap/tab/endtab.php @@ -1,4 +1,5 @@ getWebAssetManager(); $wa->registerScript('tinymce', 'media/vendor/tinymce/tinymce.min.js', [], ['defer' => true]) - ->registerScript('plg_editors_tinymce', 'plg_editors_tinymce/tinymce.min.js', [], ['defer' => true], ['core', 'tinymce']) - ->registerAndUseStyle('tinymce.skin', 'media/vendor/tinymce/skins/ui/oxide/skin.min.css') - ->registerAndUseStyle('plg_editors_tinymce.builder', 'plg_editors_tinymce/tinymce-builder.css', [], [], ['tinymce.skin', 'dragula']) - ->registerScript('plg_editors_tinymce.builder', 'plg_editors_tinymce/tinymce-builder.js', [], ['type' => 'module'], ['dragula', 'plg_editors_tinymce']) - ->useScript('plg_editors_tinymce.builder') - ->useStyle('webcomponent.joomla-tab') - ->useScript('webcomponent.joomla-tab'); + ->registerScript('plg_editors_tinymce', 'plg_editors_tinymce/tinymce.min.js', [], ['defer' => true], ['core', 'tinymce']) + ->registerAndUseStyle('tinymce.skin', 'media/vendor/tinymce/skins/ui/oxide/skin.min.css') + ->registerAndUseStyle('plg_editors_tinymce.builder', 'plg_editors_tinymce/tinymce-builder.css', [], [], ['tinymce.skin', 'dragula']) + ->registerScript('plg_editors_tinymce.builder', 'plg_editors_tinymce/tinymce-builder.js', [], ['type' => 'module'], ['dragula', 'plg_editors_tinymce']) + ->useScript('plg_editors_tinymce.builder') + ->useStyle('webcomponent.joomla-tab') + ->useScript('webcomponent.joomla-tab'); // Add TinyMCE language file to translate the buttons -if ($languageFile) -{ - $wa->registerAndUseScript('tinymce.language', $languageFile, [], ['defer' => true], []); +if ($languageFile) { + $wa->registerAndUseScript('tinymce.language', $languageFile, [], ['defer' => true], []); } // Add the builder options -$doc->addScriptOptions('plg_editors_tinymce_builder', - [ - 'menus' => $menus, - 'buttons' => $buttons, - 'toolbarPreset' => $toolbarPreset, - 'formControl' => $name . '[toolbars]', - ] +$doc->addScriptOptions( + 'plg_editors_tinymce_builder', + [ + 'menus' => $menus, + 'buttons' => $buttons, + 'toolbarPreset' => $toolbarPreset, + 'formControl' => $name . '[toolbars]', + ] ); ?>
    -

    -

    -

    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - $title) : ?> - - name=""> - - 'btn-success', - 'medium' => 'btn-info', - 'advanced' => 'btn-warning', - ]; - // Check whether the values exists, and if empty then use from preset - if (empty($value['toolbars'][$num]['menu']) - && empty($value['toolbars'][$num]['toolbar1']) - && empty($value['toolbars'][$num]['toolbar2'])) - { - // Take the preset for default value - switch ($num) { - case 0: - $preset = $toolbarPreset['advanced']; - break; - case 1: - $preset = $toolbarPreset['medium']; - break; - default: - $preset = $toolbarPreset['simple']; - } - - $value['toolbars'][$num] = $preset; - } - - // Take existing values - $valMenu = empty($value['toolbars'][$num]['menu']) ? array() : $value['toolbars'][$num]['menu']; - $valBar1 = empty($value['toolbars'][$num]['toolbar1']) ? array() : $value['toolbars'][$num]['toolbar1']; - $valBar2 = empty($value['toolbars'][$num]['toolbar2']) ? array() : $value['toolbars'][$num]['toolbar2']; - - ?> - sublayout('setaccess', array('form' => $setsForms[$num])); ?> -
    -
    - - - - - - -
    -
    - -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - sublayout('setoptions', array('form' => $setsForms[$num])); ?> -
    - -
    +

    +

    +

    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + $title) : ?> + + name=""> + + 'btn-success', + 'medium' => 'btn-info', + 'advanced' => 'btn-warning', + ]; + // Check whether the values exists, and if empty then use from preset + if ( + empty($value['toolbars'][$num]['menu']) + && empty($value['toolbars'][$num]['toolbar1']) + && empty($value['toolbars'][$num]['toolbar2']) + ) { + // Take the preset for default value + switch ($num) { + case 0: + $preset = $toolbarPreset['advanced']; + break; + case 1: + $preset = $toolbarPreset['medium']; + break; + default: + $preset = $toolbarPreset['simple']; + } + + $value['toolbars'][$num] = $preset; + } + + // Take existing values + $valMenu = empty($value['toolbars'][$num]['menu']) ? array() : $value['toolbars'][$num]['menu']; + $valBar1 = empty($value['toolbars'][$num]['toolbar1']) ? array() : $value['toolbars'][$num]['toolbar1']; + $valBar2 = empty($value['toolbars'][$num]['toolbar2']) ? array() : $value['toolbars'][$num]['toolbar2']; + + ?> + sublayout('setaccess', array('form' => $setsForms[$num])); ?> +
    +
    + + + + + + +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + sublayout('setoptions', array('form' => $setsForms[$num])); ?> +
    + +
    diff --git a/layouts/plugins/editors/tinymce/field/tinymcebuilder/setaccess.php b/layouts/plugins/editors/tinymce/field/tinymcebuilder/setaccess.php index 177300678ea48..c4be4b9eccc61 100644 --- a/layouts/plugins/editors/tinymce/field/tinymcebuilder/setaccess.php +++ b/layouts/plugins/editors/tinymce/field/tinymcebuilder/setaccess.php @@ -1,4 +1,5 @@
    - renderField('access'); ?> + renderField('access'); ?>
    diff --git a/layouts/plugins/editors/tinymce/field/tinymcebuilder/setoptions.php b/layouts/plugins/editors/tinymce/field/tinymcebuilder/setoptions.php index d3df455aea199..4b51137dcd171 100644 --- a/layouts/plugins/editors/tinymce/field/tinymcebuilder/setoptions.php +++ b/layouts/plugins/editors/tinymce/field/tinymcebuilder/setoptions.php @@ -1,4 +1,5 @@
    getFieldset('basic') as $field) : ?> - renderField(); ?> + renderField(); ?>
    diff --git a/layouts/plugins/system/privacyconsent/label.php b/layouts/plugins/system/privacyconsent/label.php index 655f11905ca5f..394f10f738b3d 100644 --- a/layouts/plugins/system/privacyconsent/label.php +++ b/layouts/plugins/system/privacyconsent/label.php @@ -1,4 +1,5 @@ 'modal', - 'data-bs-target' => '#consentModal', - 'class' => 'required', - ]; +if ($privacyLink) { + $attribs = [ + 'data-bs-toggle' => 'modal', + 'data-bs-target' => '#consentModal', + 'class' => 'required', + ]; - $link = HTMLHelper::_('link', Route::_($privacyLink . '&tmpl=component'), $text, $attribs); + $link = HTMLHelper::_('link', Route::_($privacyLink . '&tmpl=component'), $text, $attribs); - echo HTMLHelper::_( - 'bootstrap.renderModal', - 'consentModal', - [ - 'url' => Route::_($privacyLink . '&tmpl=component'), - 'title' => $text, - 'height' => '100%', - 'width' => '100%', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ] - ); -} -else -{ - $link = '' . $text . ''; + echo HTMLHelper::_( + 'bootstrap.renderModal', + 'consentModal', + [ + 'url' => Route::_($privacyLink . '&tmpl=component'), + 'title' => $text, + 'height' => '100%', + 'width' => '100%', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ] + ); +} else { + $link = '' . $text . ''; } // Add the label text and star. diff --git a/layouts/plugins/system/privacyconsent/message.php b/layouts/plugins/system/privacyconsent/message.php index a9b10704b0dfb..d9c991e472ace 100644 --- a/layouts/plugins/system/privacyconsent/message.php +++ b/layouts/plugins/system/privacyconsent/message.php @@ -1,4 +1,5 @@ ' . $privacynote . '
    '; - diff --git a/layouts/plugins/system/webauthn/manage.php b/layouts/plugins/system/webauthn/manage.php index 99c3180e6de93..e66673b5a611e 100644 --- a/layouts/plugins/system/webauthn/manage.php +++ b/layouts/plugins/system/webauthn/manage.php @@ -1,4 +1,5 @@ getIdentity(); +try { + $app = Factory::getApplication(); + $loggedInUser = $app->getIdentity(); - $app->getDocument()->getWebAssetManager() - ->registerAndUseStyle('plg_system_webauthn.backend', 'plg_system_webauthn/backend.css'); -} -catch (Exception $e) -{ - $loggedInUser = new User; + $app->getDocument()->getWebAssetManager() + ->registerAndUseStyle('plg_system_webauthn.backend', 'plg_system_webauthn/backend.css'); +} catch (Exception $e) { + $loggedInUser = new User(); } $defaultDisplayData = [ - 'user' => $loggedInUser, - 'allow_add' => false, - 'credentials' => [], - 'error' => '', + 'user' => $loggedInUser, + 'allow_add' => false, + 'credentials' => [], + 'error' => '', ]; extract(array_merge($defaultDisplayData, $displayData)); -if ($displayData['allow_add'] === false) -{ - $error = Text::_('PLG_SYSTEM_WEBAUTHN_CANNOT_ADD_FOR_A_USER'); - $allow_add = false; +if ($displayData['allow_add'] === false) { + $error = Text::_('PLG_SYSTEM_WEBAUTHN_CANNOT_ADD_FOR_A_USER'); + $allow_add = false; } // Ensure the GMP or BCmath extension is loaded in PHP - as this is required by third party library -if ($allow_add && function_exists('gmp_intval') === false && function_exists('bccomp') === false) -{ - $error = Text::_('PLG_SYSTEM_WEBAUTHN_REQUIRES_GMP'); - $allow_add = false; +if ($allow_add && function_exists('gmp_intval') === false && function_exists('bccomp') === false) { + $error = Text::_('PLG_SYSTEM_WEBAUTHN_REQUIRES_GMP'); + $allow_add = false; } /** @@ -85,67 +81,67 @@ ?>
    - + -
    - -
    - +
    + +
    + - - - - - - - - - +
    - , -
    + + + + + + + + - - - - - + + + + + - - - - - -
    + , +
    - - -
    + + +
    - -
    + + + + + + + + -

    - -

    - +

    + +

    +
    diff --git a/layouts/plugins/user/terms/label.php b/layouts/plugins/user/terms/label.php index 1b4d3278a5b58..61223ffe99097 100644 --- a/layouts/plugins/user/terms/label.php +++ b/layouts/plugins/user/terms/label.php @@ -1,4 +1,5 @@ 'modal', - 'data-bs-target' => '#tosModal', - 'class' => 'required', - ]; +if ($article) { + $attribs = [ + 'data-bs-toggle' => 'modal', + 'data-bs-target' => '#tosModal', + 'class' => 'required', + ]; - $link = HTMLHelper::_('link', Route::_($article->link . '&tmpl=component'), $text, $attribs); + $link = HTMLHelper::_('link', Route::_($article->link . '&tmpl=component'), $text, $attribs); - echo HTMLHelper::_( - 'bootstrap.renderModal', - 'tosModal', - [ - 'url' => Route::_($article->link . '&tmpl=component'), - 'title' => $text, - 'height' => '100%', - 'width' => '100%', - 'bodyHeight' => 70, - 'modalWidth' => 80, - 'footer' => '', - ] - ); -} -else -{ - $link = '' . $text . ''; + echo HTMLHelper::_( + 'bootstrap.renderModal', + 'tosModal', + [ + 'url' => Route::_($article->link . '&tmpl=component'), + 'title' => $text, + 'height' => '100%', + 'width' => '100%', + 'bodyHeight' => 70, + 'modalWidth' => 80, + 'footer' => '', + ] + ); +} else { + $link = '' . $text . ''; } // Add the label text and star. diff --git a/layouts/plugins/user/terms/message.php b/layouts/plugins/user/terms/message.php index e6284a235f632..f28f1c34491ad 100644 --- a/layouts/plugins/user/terms/message.php +++ b/layouts/plugins/user/terms/message.php @@ -1,4 +1,5 @@ getDocument()->getWebAssetManager() - ->registerAndUseScript('plg_user_token.token', 'plg_user_token/token.js', [], ['defer' => true], ['core']); + ->registerAndUseScript('plg_user_token.token', 'plg_user_token/token.js', [], ['defer' => true], ['core']); ?>
    - - + +
    diff --git a/libraries/bootstrap.php b/libraries/bootstrap.php index 54a59760a2fa1..6f9f749178f01 100644 --- a/libraries/bootstrap.php +++ b/libraries/bootstrap.php @@ -1,4 +1,5 @@ withAssertion(new \TYPO3\PharStreamWrapper\Interceptor\PharExtensionInterceptor) + $behavior->withAssertion(new \TYPO3\PharStreamWrapper\Interceptor\PharExtensionInterceptor()) ); -if (in_array('phar', stream_get_wrappers())) -{ - stream_wrapper_unregister('phar'); - stream_wrapper_register('phar', 'TYPO3\\PharStreamWrapper\\PharStreamWrapper'); +if (in_array('phar', stream_get_wrappers())) { + stream_wrapper_unregister('phar'); + stream_wrapper_register('phar', 'TYPO3\\PharStreamWrapper\\PharStreamWrapper'); } // Define the Joomla version if not already defined. -defined('JVERSION') or define('JVERSION', (new \Joomla\CMS\Version)->getShortVersion()); +defined('JVERSION') or define('JVERSION', (new \Joomla\CMS\Version())->getShortVersion()); // Set up the message queue logger for web requests -if (array_key_exists('REQUEST_METHOD', $_SERVER)) -{ - \Joomla\CMS\Log\Log::addLogger(['logger' => 'messagequeue'], \Joomla\CMS\Log\Log::ALL, ['jerror']); +if (array_key_exists('REQUEST_METHOD', $_SERVER)) { + \Joomla\CMS\Log\Log::addLogger(['logger' => 'messagequeue'], \Joomla\CMS\Log\Log::ALL, ['jerror']); } // Register the Crypto lib diff --git a/libraries/classmap.php b/libraries/classmap.php index 1b43fa4334ef0..5b203ff2ed2fc 100644 --- a/libraries/classmap.php +++ b/libraries/classmap.php @@ -1,4 +1,5 @@ withAssertion(new \TYPO3\PharStreamWrapper\Interceptor\PharExtensionInterceptor) + $behavior->withAssertion(new \TYPO3\PharStreamWrapper\Interceptor\PharExtensionInterceptor()) ); -if (in_array('phar', stream_get_wrappers())) -{ - stream_wrapper_unregister('phar'); - stream_wrapper_register('phar', 'TYPO3\\PharStreamWrapper\\PharStreamWrapper'); +if (in_array('phar', stream_get_wrappers())) { + stream_wrapper_unregister('phar'); + stream_wrapper_register('phar', 'TYPO3\\PharStreamWrapper\\PharStreamWrapper'); } // Define the Joomla version if not already defined -if (!defined('JVERSION')) -{ - define('JVERSION', (new \Joomla\CMS\Version)->getShortVersion()); +if (!defined('JVERSION')) { + define('JVERSION', (new \Joomla\CMS\Version())->getShortVersion()); } // Register a handler for uncaught exceptions that shows a pretty error page when possible set_exception_handler(array('Joomla\CMS\Exception\ExceptionHandler', 'handleException')); // Set up the message queue logger for web requests -if (array_key_exists('REQUEST_METHOD', $_SERVER)) -{ - \Joomla\CMS\Log\Log::addLogger(array('logger' => 'messagequeue'), \Joomla\CMS\Log\Log::ALL, ['jerror']); +if (array_key_exists('REQUEST_METHOD', $_SERVER)) { + \Joomla\CMS\Log\Log::addLogger(array('logger' => 'messagequeue'), \Joomla\CMS\Log\Log::ALL, ['jerror']); } // Register the Crypto lib diff --git a/libraries/extensions.classmap.php b/libraries/extensions.classmap.php index fd57dc934970b..efbd76a2526d8 100644 --- a/libraries/extensions.classmap.php +++ b/libraries/extensions.classmap.php @@ -1,4 +1,5 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt + + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace */ defined('JPATH_PLATFORM') or die; @@ -15,761 +18,688 @@ */ abstract class JLoader { - /** - * Container for already imported library paths. - * - * @var array - * @since 1.7.0 - */ - protected static $classes = array(); - - /** - * Container for already imported library paths. - * - * @var array - * @since 1.7.0 - */ - protected static $imported = array(); - - /** - * Container for registered library class prefixes and path lookups. - * - * @var array - * @since 3.0.0 - */ - protected static $prefixes = array(); - - /** - * Holds proxy classes and the class names the proxy. - * - * @var array - * @since 3.2 - */ - protected static $classAliases = array(); - - /** - * Holds the inverse lookup for proxy classes and the class names the proxy. - * - * @var array - * @since 3.4 - */ - protected static $classAliasesInverse = array(); - - /** - * Container for namespace => path map. - * - * @var array - * @since 3.1.4 - */ - protected static $namespaces = array(); - - /** - * Holds a reference for all deprecated aliases (mainly for use by a logging platform). - * - * @var array - * @since 3.6.3 - */ - protected static $deprecatedAliases = array(); - - /** - * The root folders where extensions can be found. - * - * @var array - * @since 4.0.0 - */ - protected static $extensionRootFolders = array(); - - /** - * Method to discover classes of a given type in a given path. - * - * @param string $classPrefix The class name prefix to use for discovery. - * @param string $parentPath Full path to the parent folder for the classes to discover. - * @param boolean $force True to overwrite the autoload path value for the class if it already exists. - * @param boolean $recurse Recurse through all child directories as well as the parent path. - * - * @return void - * - * @since 1.7.0 - * @deprecated 5.0 Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to register an autoloader for - * your files. - */ - public static function discover($classPrefix, $parentPath, $force = true, $recurse = false) - { - try - { - if ($recurse) - { - $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($parentPath), - RecursiveIteratorIterator::SELF_FIRST - ); - } - else - { - $iterator = new DirectoryIterator($parentPath); - } - - /** @type $file DirectoryIterator */ - foreach ($iterator as $file) - { - $fileName = $file->getFilename(); - - // Only load for php files. - if ($file->isFile() && $file->getExtension() === 'php') - { - // Get the class name and full path for each file. - $class = strtolower($classPrefix . preg_replace('#\.php$#', '', $fileName)); - - // Register the class with the autoloader if not already registered or the force flag is set. - if ($force || empty(self::$classes[$class])) - { - self::register($class, $file->getPath() . '/' . $fileName); - } - } - } - } - catch (UnexpectedValueException $e) - { - // Exception will be thrown if the path is not a directory. Ignore it. - } - } - - /** - * Method to get the list of registered classes and their respective file paths for the autoloader. - * - * @return array The array of class => path values for the autoloader. - * - * @since 1.7.0 - */ - public static function getClassList() - { - return self::$classes; - } - - /** - * Method to get the list of deprecated class aliases. - * - * @return array An associative array with deprecated class alias data. - * - * @since 3.6.3 - */ - public static function getDeprecatedAliases() - { - return self::$deprecatedAliases; - } - - /** - * Method to get the list of registered namespaces. - * - * @return array The array of namespace => path values for the autoloader. - * - * @since 3.1.4 - */ - public static function getNamespaces() - { - return self::$namespaces; - } - - /** - * Loads a class from specified directories. - * - * @param string $key The class name to look for (dot notation). - * @param string $base Search this directory for the class. - * - * @return boolean True on success. - * - * @since 1.7.0 - * @deprecated 5.0 Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to register an autoloader for - * your files. - */ - public static function import($key, $base = null) - { - // Only import the library if not already attempted. - if (!isset(self::$imported[$key])) - { - // Setup some variables. - $success = false; - $parts = explode('.', $key); - $class = array_pop($parts); - $base = (!empty($base)) ? $base : __DIR__; - $path = str_replace('.', DIRECTORY_SEPARATOR, $key); - - // Handle special case for helper classes. - if ($class === 'helper') - { - $class = ucfirst(array_pop($parts)) . ucfirst($class); - } - // Standard class. - else - { - $class = ucfirst($class); - } - - // If we are importing a library from the Joomla namespace set the class to autoload. - if (strpos($path, 'joomla') === 0) - { - // Since we are in the Joomla namespace prepend the classname with J. - $class = 'J' . $class; - - // Only register the class for autoloading if the file exists. - if (is_file($base . '/' . $path . '.php')) - { - self::$classes[strtolower($class)] = $base . '/' . $path . '.php'; - $success = true; - } - } - /* - * If we are not importing a library from the Joomla namespace directly include the - * file since we cannot assert the file/folder naming conventions. - */ - else - { - // If the file exists attempt to include it. - if (is_file($base . '/' . $path . '.php')) - { - $success = (bool) include_once $base . '/' . $path . '.php'; - } - } - - // Add the import key to the memory cache container. - self::$imported[$key] = $success; - } - - return self::$imported[$key]; - } - - /** - * Load the file for a class. - * - * @param string $class The class to be loaded. - * - * @return boolean True on success - * - * @since 1.7.0 - */ - public static function load($class) - { - // Sanitize class name. - $key = strtolower($class); - - // If the class already exists do nothing. - if (class_exists($class, false)) - { - return true; - } - - // If the class is registered include the file. - if (isset(self::$classes[$key])) - { - $found = (bool) include_once self::$classes[$key]; - - if ($found) - { - self::loadAliasFor($class); - } - - // If the class doesn't exists, we probably have a class alias available - if (!class_exists($class, false)) - { - // Search the alias class, first none namespaced and then namespaced - $original = array_search($class, self::$classAliases) ? : array_search('\\' . $class, self::$classAliases); - - // When we have an original and the class exists an alias should be created - if ($original && class_exists($original, false)) - { - class_alias($original, $class); - } - } - - return true; - } - - return false; - } - - /** - * Directly register a class to the autoload list. - * - * @param string $class The class name to register. - * @param string $path Full path to the file that holds the class to register. - * @param boolean $force True to overwrite the autoload path value for the class if it already exists. - * - * @return void - * - * @since 1.7.0 - * @deprecated 5.0 Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to register an autoloader for - * your files. - */ - public static function register($class, $path, $force = true) - { - // When an alias exists, register it as well - if (array_key_exists(strtolower($class), self::$classAliases)) - { - self::register(self::stripFirstBackslash(self::$classAliases[strtolower($class)]), $path, $force); - } - - // Sanitize class name. - $class = strtolower($class); - - // Only attempt to register the class if the name and file exist. - if (!empty($class) && is_file($path)) - { - // Register the class with the autoloader if not already registered or the force flag is set. - if ($force || empty(self::$classes[$class])) - { - self::$classes[$class] = $path; - } - } - } - - /** - * Register a class prefix with lookup path. This will allow developers to register library - * packages with different class prefixes to the system autoloader. More than one lookup path - * may be registered for the same class prefix, but if this method is called with the reset flag - * set to true then any registered lookups for the given prefix will be overwritten with the current - * lookup path. When loaded, prefix paths are searched in a "last in, first out" order. - * - * @param string $prefix The class prefix to register. - * @param string $path Absolute file path to the library root where classes with the given prefix can be found. - * @param boolean $reset True to reset the prefix with only the given lookup path. - * @param boolean $prepend If true, push the path to the beginning of the prefix lookup paths array. - * - * @return void - * - * @throws RuntimeException - * - * @since 3.0.0 - */ - public static function registerPrefix($prefix, $path, $reset = false, $prepend = false) - { - // Verify the library path exists. - if (!is_dir($path)) - { - $path = (str_replace(JPATH_ROOT, '', $path) == $path) ? basename($path) : str_replace(JPATH_ROOT, '', $path); - - throw new RuntimeException('Library path ' . $path . ' cannot be found.', 500); - } - - // If the prefix is not yet registered or we have an explicit reset flag then set set the path. - if ($reset || !isset(self::$prefixes[$prefix])) - { - self::$prefixes[$prefix] = array($path); - } - // Otherwise we want to simply add the path to the prefix. - else - { - if ($prepend) - { - array_unshift(self::$prefixes[$prefix], $path); - } - else - { - self::$prefixes[$prefix][] = $path; - } - } - } - - /** - * Offers the ability for "just in time" usage of `class_alias()`. - * You cannot overwrite an existing alias. - * - * @param string $alias The alias name to register. - * @param string $original The original class to alias. - * @param string|boolean $version The version in which the alias will no longer be present. - * - * @return boolean True if registration was successful. False if the alias already exists. - * - * @since 3.2 - */ - public static function registerAlias($alias, $original, $version = false) - { - // PHP is case insensitive so support all kind of alias combination - $lowercasedAlias = strtolower($alias); - - if (!isset(self::$classAliases[$lowercasedAlias])) - { - self::$classAliases[$lowercasedAlias] = $original; - - $original = self::stripFirstBackslash($original); - - if (!isset(self::$classAliasesInverse[$original])) - { - self::$classAliasesInverse[$original] = array($lowercasedAlias); - } - else - { - self::$classAliasesInverse[$original][] = $lowercasedAlias; - } - - // If given a version, log this alias as deprecated - if ($version) - { - self::$deprecatedAliases[] = array('old' => $alias, 'new' => $original, 'version' => $version); - } - - return true; - } - - return false; - } - - /** - * Register a namespace to the autoloader. When loaded, namespace paths are searched in a "last in, first out" order. - * - * @param string $namespace A case sensitive Namespace to register. - * @param string $path A case sensitive absolute file path to the library root where classes of the given namespace can be found. - * @param boolean $reset True to reset the namespace with only the given lookup path. - * @param boolean $prepend If true, push the path to the beginning of the namespace lookup paths array. - * - * @return void - * - * @throws RuntimeException - * - * @since 3.1.4 - */ - public static function registerNamespace($namespace, $path, $reset = false, $prepend = false) - { - // Verify the library path exists. - if (!is_dir($path)) - { - $path = (str_replace(JPATH_ROOT, '', $path) == $path) ? basename($path) : str_replace(JPATH_ROOT, '', $path); - - throw new RuntimeException('Library path ' . $path . ' cannot be found.', 500); - } - - // Trim leading and trailing backslashes from namespace, allowing "\Parent\Child", "Parent\Child\" and "\Parent\Child\" to be treated the same way. - $namespace = trim($namespace, '\\'); - - // If the namespace is not yet registered or we have an explicit reset flag then set the path. - if ($reset || !isset(self::$namespaces[$namespace])) - { - self::$namespaces[$namespace] = array($path); - } - - // Otherwise we want to simply add the path to the namespace. - else - { - if ($prepend) - { - array_unshift(self::$namespaces[$namespace], $path); - } - else - { - self::$namespaces[$namespace][] = $path; - } - } - } - - /** - * Method to setup the autoloaders for the Joomla Platform. - * Since the SPL autoloaders are called in a queue we will add our explicit - * class-registration based loader first, then fall back on the autoloader based on conventions. - * This will allow people to register a class in a specific location and override platform libraries - * as was previously possible. - * - * @param boolean $enablePsr True to enable autoloading based on PSR-0. - * @param boolean $enablePrefixes True to enable prefix based class loading (needed to auto load the Joomla core). - * @param boolean $enableClasses True to enable class map based class loading (needed to auto load the Joomla core). - * - * @return void - * - * @since 3.1.4 - */ - public static function setup($enablePsr = true, $enablePrefixes = true, $enableClasses = true) - { - if ($enableClasses) - { - // Register the class map based autoloader. - spl_autoload_register(array('JLoader', 'load')); - } - - if ($enablePrefixes) - { - // Register the prefix autoloader. - spl_autoload_register(array('JLoader', '_autoload')); - } - - if ($enablePsr) - { - // Register the PSR based autoloader. - spl_autoload_register(array('JLoader', 'loadByPsr')); - spl_autoload_register(array('JLoader', 'loadByAlias')); - } - } - - /** - * Method to autoload classes that are namespaced to the PSR-4 standard. - * - * @param string $class The fully qualified class name to autoload. - * - * @return boolean True on success, false otherwise. - * - * @since 3.7.0 - * @deprecated 5.0 Use JLoader::loadByPsr instead - */ - public static function loadByPsr4($class) - { - return self::loadByPsr($class); - } - - /** - * Method to autoload classes that are namespaced to the PSR-4 standard. - * - * @param string $class The fully qualified class name to autoload. - * - * @return boolean True on success, false otherwise. - * - * @since 4.0.0 - */ - public static function loadByPsr($class) - { - $class = self::stripFirstBackslash($class); - - // Find the location of the last NS separator. - $pos = strrpos($class, '\\'); - - // If one is found, we're dealing with a NS'd class. - if ($pos !== false) - { - $classPath = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, 0, $pos)) . DIRECTORY_SEPARATOR; - $className = substr($class, $pos + 1); - } - // If not, no need to parse path. - else - { - $classPath = null; - $className = $class; - } - - $classPath .= $className . '.php'; - - // Loop through registered namespaces until we find a match. - foreach (self::$namespaces as $ns => $paths) - { - if (strpos($class, "{$ns}\\") === 0) - { - $nsPath = trim(str_replace('\\', DIRECTORY_SEPARATOR, $ns), DIRECTORY_SEPARATOR); - - // Loop through paths registered to this namespace until we find a match. - foreach ($paths as $path) - { - $classFilePath = realpath($path . DIRECTORY_SEPARATOR . substr_replace($classPath, '', 0, strlen($nsPath) + 1)); - - // We do not allow files outside the namespace root to be loaded - if (strpos($classFilePath, realpath($path)) !== 0) - { - continue; - } - - // We check for class_exists to handle case-sensitive file systems - if (is_file($classFilePath) && !class_exists($class, false)) - { - $found = (bool) include_once $classFilePath; - - if ($found) - { - self::loadAliasFor($class); - } - - return $found; - } - } - } - } - - return false; - } - - /** - * Method to autoload classes that have been aliased using the registerAlias method. - * - * @param string $class The fully qualified class name to autoload. - * - * @return boolean True on success, false otherwise. - * - * @since 3.2 - */ - public static function loadByAlias($class) - { - $class = strtolower(self::stripFirstBackslash($class)); - - if (isset(self::$classAliases[$class])) - { - // Force auto-load of the regular class - class_exists(self::$classAliases[$class], true); - - // Normally this shouldn't execute as the autoloader will execute applyAliasFor when the regular class is - // auto-loaded above. - if (!class_exists($class, false) && !interface_exists($class, false)) - { - class_alias(self::$classAliases[$class], $class); - } - } - } - - /** - * Applies a class alias for an already loaded class, if a class alias was created for it. - * - * @param string $class We'll look for and register aliases for this (real) class name - * - * @return void - * - * @since 3.4 - */ - public static function applyAliasFor($class) - { - $class = self::stripFirstBackslash($class); - - if (isset(self::$classAliasesInverse[$class])) - { - foreach (self::$classAliasesInverse[$class] as $alias) - { - class_alias($class, $alias); - } - } - } - - /** - * Autoload a class based on name. - * - * @param string $class The class to be loaded. - * - * @return boolean True if the class was loaded, false otherwise. - * - * @since 1.7.3 - */ - public static function _autoload($class) - { - foreach (self::$prefixes as $prefix => $lookup) - { - $chr = strlen($prefix) < strlen($class) ? $class[strlen($prefix)] : 0; - - if (strpos($class, $prefix) === 0 && ($chr === strtoupper($chr))) - { - return self::_load(substr($class, strlen($prefix)), $lookup); - } - } - - return false; - } - - /** - * Load a class based on name and lookup array. - * - * @param string $class The class to be loaded (without prefix). - * @param array $lookup The array of base paths to use for finding the class file. - * - * @return boolean True if the class was loaded, false otherwise. - * - * @since 3.0.0 - */ - private static function _load($class, $lookup) - { - // Split the class name into parts separated by camelCase. - $parts = preg_split('/(?<=[a-z0-9])(?=[A-Z])/x', $class); - $partsCount = count($parts); - - foreach ($lookup as $base) - { - // Generate the path based on the class name parts. - $path = realpath($base . '/' . implode('/', array_map('strtolower', $parts)) . '.php'); - - // Load the file if it exists and is in the lookup path. - if (strpos($path, realpath($base)) === 0 && is_file($path)) - { - $found = (bool) include_once $path; - - if ($found) - { - self::loadAliasFor($class); - } - - return $found; - } - - // Backwards compatibility patch - - // If there is only one part we want to duplicate that part for generating the path. - if ($partsCount === 1) - { - // Generate the path based on the class name parts. - $path = realpath($base . '/' . implode('/', array_map('strtolower', array($parts[0], $parts[0]))) . '.php'); - - // Load the file if it exists and is in the lookup path. - if (strpos($path, realpath($base)) === 0 && is_file($path)) - { - $found = (bool) include_once $path; - - if ($found) - { - self::loadAliasFor($class); - } - - return $found; - } - } - } - - return false; - } - - /** - * Loads the aliases for the given class. - * - * @param string $class The class. - * - * @return void - * - * @since 3.8.0 - */ - private static function loadAliasFor($class) - { - if (!array_key_exists($class, self::$classAliasesInverse)) - { - return; - } - - foreach (self::$classAliasesInverse[$class] as $alias) - { - // Force auto-load of the alias class - class_exists($alias, true); - } - } - - /** - * Strips the first backslash from the given class if present. - * - * @param string $class The class to strip the first prefix from. - * - * @return string The striped class name. - * - * @since 3.8.0 - */ - private static function stripFirstBackslash($class) - { - return $class && $class[0] === '\\' ? substr($class, 1) : $class; - } + /** + * Container for already imported library paths. + * + * @var array + * @since 1.7.0 + */ + protected static $classes = array(); + + /** + * Container for already imported library paths. + * + * @var array + * @since 1.7.0 + */ + protected static $imported = array(); + + /** + * Container for registered library class prefixes and path lookups. + * + * @var array + * @since 3.0.0 + */ + protected static $prefixes = array(); + + /** + * Holds proxy classes and the class names the proxy. + * + * @var array + * @since 3.2 + */ + protected static $classAliases = array(); + + /** + * Holds the inverse lookup for proxy classes and the class names the proxy. + * + * @var array + * @since 3.4 + */ + protected static $classAliasesInverse = array(); + + /** + * Container for namespace => path map. + * + * @var array + * @since 3.1.4 + */ + protected static $namespaces = array(); + + /** + * Holds a reference for all deprecated aliases (mainly for use by a logging platform). + * + * @var array + * @since 3.6.3 + */ + protected static $deprecatedAliases = array(); + + /** + * The root folders where extensions can be found. + * + * @var array + * @since 4.0.0 + */ + protected static $extensionRootFolders = array(); + + /** + * Method to discover classes of a given type in a given path. + * + * @param string $classPrefix The class name prefix to use for discovery. + * @param string $parentPath Full path to the parent folder for the classes to discover. + * @param boolean $force True to overwrite the autoload path value for the class if it already exists. + * @param boolean $recurse Recurse through all child directories as well as the parent path. + * + * @return void + * + * @since 1.7.0 + * @deprecated 5.0 Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to register an autoloader for + * your files. + */ + public static function discover($classPrefix, $parentPath, $force = true, $recurse = false) + { + try { + if ($recurse) { + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($parentPath), + RecursiveIteratorIterator::SELF_FIRST + ); + } else { + $iterator = new DirectoryIterator($parentPath); + } + + /** @type $file DirectoryIterator */ + foreach ($iterator as $file) { + $fileName = $file->getFilename(); + + // Only load for php files. + if ($file->isFile() && $file->getExtension() === 'php') { + // Get the class name and full path for each file. + $class = strtolower($classPrefix . preg_replace('#\.php$#', '', $fileName)); + + // Register the class with the autoloader if not already registered or the force flag is set. + if ($force || empty(self::$classes[$class])) { + self::register($class, $file->getPath() . '/' . $fileName); + } + } + } + } catch (UnexpectedValueException $e) { + // Exception will be thrown if the path is not a directory. Ignore it. + } + } + + /** + * Method to get the list of registered classes and their respective file paths for the autoloader. + * + * @return array The array of class => path values for the autoloader. + * + * @since 1.7.0 + */ + public static function getClassList() + { + return self::$classes; + } + + /** + * Method to get the list of deprecated class aliases. + * + * @return array An associative array with deprecated class alias data. + * + * @since 3.6.3 + */ + public static function getDeprecatedAliases() + { + return self::$deprecatedAliases; + } + + /** + * Method to get the list of registered namespaces. + * + * @return array The array of namespace => path values for the autoloader. + * + * @since 3.1.4 + */ + public static function getNamespaces() + { + return self::$namespaces; + } + + /** + * Loads a class from specified directories. + * + * @param string $key The class name to look for (dot notation). + * @param string $base Search this directory for the class. + * + * @return boolean True on success. + * + * @since 1.7.0 + * @deprecated 5.0 Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to register an autoloader for + * your files. + */ + public static function import($key, $base = null) + { + // Only import the library if not already attempted. + if (!isset(self::$imported[$key])) { + // Setup some variables. + $success = false; + $parts = explode('.', $key); + $class = array_pop($parts); + $base = (!empty($base)) ? $base : __DIR__; + $path = str_replace('.', DIRECTORY_SEPARATOR, $key); + + // Handle special case for helper classes. + if ($class === 'helper') { + $class = ucfirst(array_pop($parts)) . ucfirst($class); + } else { + // Standard class. + $class = ucfirst($class); + } + + // If we are importing a library from the Joomla namespace set the class to autoload. + if (strpos($path, 'joomla') === 0) { + // Since we are in the Joomla namespace prepend the classname with J. + $class = 'J' . $class; + + // Only register the class for autoloading if the file exists. + if (is_file($base . '/' . $path . '.php')) { + self::$classes[strtolower($class)] = $base . '/' . $path . '.php'; + $success = true; + } + } else { + /** + * If we are not importing a library from the Joomla namespace directly include the + * file since we cannot assert the file/folder naming conventions. + */ + // If the file exists attempt to include it. + if (is_file($base . '/' . $path . '.php')) { + $success = (bool) include_once $base . '/' . $path . '.php'; + } + } + + // Add the import key to the memory cache container. + self::$imported[$key] = $success; + } + + return self::$imported[$key]; + } + + /** + * Load the file for a class. + * + * @param string $class The class to be loaded. + * + * @return boolean True on success + * + * @since 1.7.0 + */ + public static function load($class) + { + // Sanitize class name. + $key = strtolower($class); + + // If the class already exists do nothing. + if (class_exists($class, false)) { + return true; + } + + // If the class is registered include the file. + if (isset(self::$classes[$key])) { + $found = (bool) include_once self::$classes[$key]; + + if ($found) { + self::loadAliasFor($class); + } + + // If the class doesn't exists, we probably have a class alias available + if (!class_exists($class, false)) { + // Search the alias class, first none namespaced and then namespaced + $original = array_search($class, self::$classAliases) ? : array_search('\\' . $class, self::$classAliases); + + // When we have an original and the class exists an alias should be created + if ($original && class_exists($original, false)) { + class_alias($original, $class); + } + } + + return true; + } + + return false; + } + + /** + * Directly register a class to the autoload list. + * + * @param string $class The class name to register. + * @param string $path Full path to the file that holds the class to register. + * @param boolean $force True to overwrite the autoload path value for the class if it already exists. + * + * @return void + * + * @since 1.7.0 + * @deprecated 5.0 Classes should be autoloaded. Use JLoader::registerPrefix() or JLoader::registerNamespace() to register an autoloader for + * your files. + */ + public static function register($class, $path, $force = true) + { + // When an alias exists, register it as well + if (array_key_exists(strtolower($class), self::$classAliases)) { + self::register(self::stripFirstBackslash(self::$classAliases[strtolower($class)]), $path, $force); + } + + // Sanitize class name. + $class = strtolower($class); + + // Only attempt to register the class if the name and file exist. + if (!empty($class) && is_file($path)) { + // Register the class with the autoloader if not already registered or the force flag is set. + if ($force || empty(self::$classes[$class])) { + self::$classes[$class] = $path; + } + } + } + + /** + * Register a class prefix with lookup path. This will allow developers to register library + * packages with different class prefixes to the system autoloader. More than one lookup path + * may be registered for the same class prefix, but if this method is called with the reset flag + * set to true then any registered lookups for the given prefix will be overwritten with the current + * lookup path. When loaded, prefix paths are searched in a "last in, first out" order. + * + * @param string $prefix The class prefix to register. + * @param string $path Absolute file path to the library root where classes with the given prefix can be found. + * @param boolean $reset True to reset the prefix with only the given lookup path. + * @param boolean $prepend If true, push the path to the beginning of the prefix lookup paths array. + * + * @return void + * + * @throws RuntimeException + * + * @since 3.0.0 + */ + public static function registerPrefix($prefix, $path, $reset = false, $prepend = false) + { + // Verify the library path exists. + if (!is_dir($path)) { + $path = (str_replace(JPATH_ROOT, '', $path) == $path) ? basename($path) : str_replace(JPATH_ROOT, '', $path); + + throw new RuntimeException('Library path ' . $path . ' cannot be found.', 500); + } + + // If the prefix is not yet registered or we have an explicit reset flag then set set the path. + if ($reset || !isset(self::$prefixes[$prefix])) { + self::$prefixes[$prefix] = array($path); + } else { + // Otherwise we want to simply add the path to the prefix. + if ($prepend) { + array_unshift(self::$prefixes[$prefix], $path); + } else { + self::$prefixes[$prefix][] = $path; + } + } + } + + /** + * Offers the ability for "just in time" usage of `class_alias()`. + * You cannot overwrite an existing alias. + * + * @param string $alias The alias name to register. + * @param string $original The original class to alias. + * @param string|boolean $version The version in which the alias will no longer be present. + * + * @return boolean True if registration was successful. False if the alias already exists. + * + * @since 3.2 + */ + public static function registerAlias($alias, $original, $version = false) + { + // PHP is case insensitive so support all kind of alias combination + $lowercasedAlias = strtolower($alias); + + if (!isset(self::$classAliases[$lowercasedAlias])) { + self::$classAliases[$lowercasedAlias] = $original; + + $original = self::stripFirstBackslash($original); + + if (!isset(self::$classAliasesInverse[$original])) { + self::$classAliasesInverse[$original] = array($lowercasedAlias); + } else { + self::$classAliasesInverse[$original][] = $lowercasedAlias; + } + + // If given a version, log this alias as deprecated + if ($version) { + self::$deprecatedAliases[] = array('old' => $alias, 'new' => $original, 'version' => $version); + } + + return true; + } + + return false; + } + + /** + * Register a namespace to the autoloader. When loaded, namespace paths are searched in a "last in, first out" order. + * + * @param string $namespace A case sensitive Namespace to register. + * @param string $path A case sensitive absolute file path to the library root where classes of the given namespace can be found. + * @param boolean $reset True to reset the namespace with only the given lookup path. + * @param boolean $prepend If true, push the path to the beginning of the namespace lookup paths array. + * + * @return void + * + * @throws RuntimeException + * + * @since 3.1.4 + */ + public static function registerNamespace($namespace, $path, $reset = false, $prepend = false) + { + // Verify the library path exists. + if (!is_dir($path)) { + $path = (str_replace(JPATH_ROOT, '', $path) == $path) ? basename($path) : str_replace(JPATH_ROOT, '', $path); + + throw new RuntimeException('Library path ' . $path . ' cannot be found.', 500); + } + + // Trim leading and trailing backslashes from namespace, allowing "\Parent\Child", "Parent\Child\" and "\Parent\Child\" to be treated the same way. + $namespace = trim($namespace, '\\'); + + // If the namespace is not yet registered or we have an explicit reset flag then set the path. + if ($reset || !isset(self::$namespaces[$namespace])) { + self::$namespaces[$namespace] = array($path); + } else { + // Otherwise we want to simply add the path to the namespace. + if ($prepend) { + array_unshift(self::$namespaces[$namespace], $path); + } else { + self::$namespaces[$namespace][] = $path; + } + } + } + + /** + * Method to setup the autoloaders for the Joomla Platform. + * Since the SPL autoloaders are called in a queue we will add our explicit + * class-registration based loader first, then fall back on the autoloader based on conventions. + * This will allow people to register a class in a specific location and override platform libraries + * as was previously possible. + * + * @param boolean $enablePsr True to enable autoloading based on PSR-0. + * @param boolean $enablePrefixes True to enable prefix based class loading (needed to auto load the Joomla core). + * @param boolean $enableClasses True to enable class map based class loading (needed to auto load the Joomla core). + * + * @return void + * + * @since 3.1.4 + */ + public static function setup($enablePsr = true, $enablePrefixes = true, $enableClasses = true) + { + if ($enableClasses) { + // Register the class map based autoloader. + spl_autoload_register(array('JLoader', 'load')); + } + + if ($enablePrefixes) { + // Register the prefix autoloader. + spl_autoload_register(array('JLoader', '_autoload')); + } + + if ($enablePsr) { + // Register the PSR based autoloader. + spl_autoload_register(array('JLoader', 'loadByPsr')); + spl_autoload_register(array('JLoader', 'loadByAlias')); + } + } + + /** + * Method to autoload classes that are namespaced to the PSR-4 standard. + * + * @param string $class The fully qualified class name to autoload. + * + * @return boolean True on success, false otherwise. + * + * @since 3.7.0 + * @deprecated 5.0 Use JLoader::loadByPsr instead + */ + public static function loadByPsr4($class) + { + return self::loadByPsr($class); + } + + /** + * Method to autoload classes that are namespaced to the PSR-4 standard. + * + * @param string $class The fully qualified class name to autoload. + * + * @return boolean True on success, false otherwise. + * + * @since 4.0.0 + */ + public static function loadByPsr($class) + { + $class = self::stripFirstBackslash($class); + + // Find the location of the last NS separator. + $pos = strrpos($class, '\\'); + + // If one is found, we're dealing with a NS'd class. + if ($pos !== false) { + $classPath = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, 0, $pos)) . DIRECTORY_SEPARATOR; + $className = substr($class, $pos + 1); + } else { + // If not, no need to parse path. + $classPath = null; + $className = $class; + } + + $classPath .= $className . '.php'; + + // Loop through registered namespaces until we find a match. + foreach (self::$namespaces as $ns => $paths) { + if (strpos($class, "{$ns}\\") === 0) { + $nsPath = trim(str_replace('\\', DIRECTORY_SEPARATOR, $ns), DIRECTORY_SEPARATOR); + + // Loop through paths registered to this namespace until we find a match. + foreach ($paths as $path) { + $classFilePath = realpath($path . DIRECTORY_SEPARATOR . substr_replace($classPath, '', 0, strlen($nsPath) + 1)); + + // We do not allow files outside the namespace root to be loaded + if (strpos($classFilePath, realpath($path)) !== 0) { + continue; + } + + // We check for class_exists to handle case-sensitive file systems + if (is_file($classFilePath) && !class_exists($class, false)) { + $found = (bool) include_once $classFilePath; + + if ($found) { + self::loadAliasFor($class); + } + + return $found; + } + } + } + } + + return false; + } + + /** + * Method to autoload classes that have been aliased using the registerAlias method. + * + * @param string $class The fully qualified class name to autoload. + * + * @return boolean True on success, false otherwise. + * + * @since 3.2 + */ + public static function loadByAlias($class) + { + $class = strtolower(self::stripFirstBackslash($class)); + + if (isset(self::$classAliases[$class])) { + // Force auto-load of the regular class + class_exists(self::$classAliases[$class], true); + + // Normally this shouldn't execute as the autoloader will execute applyAliasFor when the regular class is + // auto-loaded above. + if (!class_exists($class, false) && !interface_exists($class, false)) { + class_alias(self::$classAliases[$class], $class); + } + } + } + + /** + * Applies a class alias for an already loaded class, if a class alias was created for it. + * + * @param string $class We'll look for and register aliases for this (real) class name + * + * @return void + * + * @since 3.4 + */ + public static function applyAliasFor($class) + { + $class = self::stripFirstBackslash($class); + + if (isset(self::$classAliasesInverse[$class])) { + foreach (self::$classAliasesInverse[$class] as $alias) { + class_alias($class, $alias); + } + } + } + + /** + * Autoload a class based on name. + * + * @param string $class The class to be loaded. + * + * @return boolean True if the class was loaded, false otherwise. + * + * @since 1.7.3 + */ + public static function _autoload($class) + { + foreach (self::$prefixes as $prefix => $lookup) { + $chr = strlen($prefix) < strlen($class) ? $class[strlen($prefix)] : 0; + + if (strpos($class, $prefix) === 0 && ($chr === strtoupper($chr))) { + return self::_load(substr($class, strlen($prefix)), $lookup); + } + } + + return false; + } + + /** + * Load a class based on name and lookup array. + * + * @param string $class The class to be loaded (without prefix). + * @param array $lookup The array of base paths to use for finding the class file. + * + * @return boolean True if the class was loaded, false otherwise. + * + * @since 3.0.0 + */ + private static function _load($class, $lookup) + { + // Split the class name into parts separated by camelCase. + $parts = preg_split('/(?<=[a-z0-9])(?=[A-Z])/x', $class); + $partsCount = count($parts); + + foreach ($lookup as $base) { + // Generate the path based on the class name parts. + $path = realpath($base . '/' . implode('/', array_map('strtolower', $parts)) . '.php'); + + // Load the file if it exists and is in the lookup path. + if (strpos($path, realpath($base)) === 0 && is_file($path)) { + $found = (bool) include_once $path; + + if ($found) { + self::loadAliasFor($class); + } + + return $found; + } + + // Backwards compatibility patch + + // If there is only one part we want to duplicate that part for generating the path. + if ($partsCount === 1) { + // Generate the path based on the class name parts. + $path = realpath($base . '/' . implode('/', array_map('strtolower', array($parts[0], $parts[0]))) . '.php'); + + // Load the file if it exists and is in the lookup path. + if (strpos($path, realpath($base)) === 0 && is_file($path)) { + $found = (bool) include_once $path; + + if ($found) { + self::loadAliasFor($class); + } + + return $found; + } + } + } + + return false; + } + + /** + * Loads the aliases for the given class. + * + * @param string $class The class. + * + * @return void + * + * @since 3.8.0 + */ + private static function loadAliasFor($class) + { + if (!array_key_exists($class, self::$classAliasesInverse)) { + return; + } + + foreach (self::$classAliasesInverse[$class] as $alias) { + // Force auto-load of the alias class + class_exists($alias, true); + } + } + + /** + * Strips the first backslash from the given class if present. + * + * @param string $class The class to strip the first prefix from. + * + * @return string The striped class name. + * + * @since 3.8.0 + */ + private static function stripFirstBackslash($class) + { + return $class && $class[0] === '\\' ? substr($class, 1) : $class; + } } // Check if jexit is defined first (our unit tests mock this) -if (!function_exists('jexit')) -{ - /** - * Global application exit. - * - * This function provides a single exit point for the platform. - * - * @param mixed $message Exit code or string. Defaults to zero. - * - * @return void - * - * @codeCoverageIgnore - * @since 1.7.0 - */ - function jexit($message = 0) - { - exit($message); - } +if (!function_exists('jexit')) { + /** + * Global application exit. + * + * This function provides a single exit point for the platform. + * + * @param mixed $message Exit code or string. Defaults to zero. + * + * @return void + * + * @codeCoverageIgnore + * @since 1.7.0 + */ + function jexit($message = 0) + { + exit($message); + } } /** @@ -786,5 +716,5 @@ function jexit($message = 0) */ function jimport($path, $base = null) { - return JLoader::import($path, $base); + return JLoader::import($path, $base); } diff --git a/libraries/namespacemap.php b/libraries/namespacemap.php index 8c1dba0a101c4..04cc702caf48e 100644 --- a/libraries/namespacemap.php +++ b/libraries/namespacemap.php @@ -1,12 +1,13 @@ * @license GNU General Public License version 2 or later; see LICENSE.txt - */ -defined('_JEXEC') or die; + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + */ use Joomla\CMS\Log\Log; use Joomla\Filesystem\File; @@ -19,301 +20,267 @@ */ class JNamespacePsr4Map { - /** - * Path to the autoloader - * - * @var string - * @since 4.0.0 - */ - protected $file = JPATH_CACHE . '/autoload_psr4.php'; - - /** - * @var array|null - * @since 4.0.0 - */ - private $cachedMap = null; - - /** - * Check if the file exists - * - * @return boolean - * - * @since 4.0.0 - */ - public function exists() - { - return is_file($this->file); - } - - /** - * Check if the namespace mapping file exists, if not create it - * - * @return void - * - * @since 4.0.0 - */ - public function ensureMapFileExists() - { - if (!$this->exists()) - { - $this->create(); - } - } - - /** - * Create the namespace file - * - * @return boolean - * - * @since 4.0.0 - */ - public function create() - { - $extensions = array_merge( - $this->getNamespaces('component'), - $this->getNamespaces('module'), - $this->getNamespaces('plugin'), - $this->getNamespaces('library') - ); - - ksort($extensions); - - $this->writeNamespaceFile($extensions); - - return true; - } - - /** - * Load the PSR4 file - * - * @return boolean - * - * @since 4.0.0 - */ - public function load() - { - if (!$this->exists()) - { - $this->create(); - } - - $map = $this->cachedMap ?: require $this->file; - - $loader = include JPATH_LIBRARIES . '/vendor/autoload.php'; - - foreach ($map as $namespace => $path) - { - $loader->setPsr4($namespace, $path); - } - - return true; - } - - /** - * Write the Namespace mapping file - * - * @param array $elements Array of elements - * - * @return void - * - * @since 4.0.0 - */ - protected function writeNamespaceFile($elements) - { - $content = array(); - $content[] = " $path) - { - $content[] = "\t'" . $namespace . "'" . ' => [' . $path . '],'; - } - - $content[] = '];'; - - /** - * Backup the current error_reporting level and set a new level - * - * We do this because file_put_contents can raise a Warning if it cannot write the autoload_psr4.php file - * and this will output to the response BEFORE the session has started, causing the session start to fail - * and ultimately leading us to a 500 Internal Server Error page just because of the output warning, which - * we can safely ignore as we can use an in-memory autoload_psr4 map temporarily, and display real errors later. - */ - $error_reporting = error_reporting(0); - - try - { - File::write($this->file, implode("\n", $content)); - } - catch (Exception $e) - { - Log::add('Could not save ' . $this->file, Log::WARNING); - - $map = []; - $constants = ['JPATH_ADMINISTRATOR', 'JPATH_API', 'JPATH_SITE', 'JPATH_PLUGINS']; - - foreach ($elements as $namespace => $path) - { - foreach ($constants as $constant) - { - $path = preg_replace(['/^(' . $constant . ")\s\.\s\'/", '/\'$/'], [constant($constant), ''], $path); - } - - $namespace = str_replace('\\\\', '\\', $namespace); - $map[$namespace] = [ $path ]; - } - - $this->cachedMap = $map; - } - - // Restore previous value of error_reporting - error_reporting($error_reporting); - } - - /** - * Get an array of namespaces with their respective path for the given extension type. - * - * @param string $type The extension type - * - * @return array - * - * @since 4.0.0 - */ - private function getNamespaces(string $type): array - { - if (!in_array($type, ['component', 'module', 'plugin', 'library'], true)) - { - return []; - } - - // Select directories containing extension manifest files. - if ($type === 'component') - { - $directories = [JPATH_ADMINISTRATOR . '/components']; - } - elseif ($type === 'module') - { - $directories = [JPATH_SITE . '/modules', JPATH_ADMINISTRATOR . '/modules']; - } - elseif ($type === 'plugin') - { - try - { - $directories = Folder::folders(JPATH_PLUGINS, '.', false, true); - } - catch (Exception $e) - { - $directories = []; - } - } - else - { - $directories = [JPATH_LIBRARIES]; - } - - $extensions = []; - - foreach ($directories as $directory) - { - try - { - $extensionFolders = Folder::folders($directory); - } - catch (Exception $e) - { - continue; - } - - foreach ($extensionFolders as $extension) - { - // Compile the extension path - $extensionPath = $directory . '/' . $extension . '/'; - - // Strip the com_ from the extension name for components - $name = str_replace('com_', '', $extension, $count); - $file = $extensionPath . $name . '.xml'; - - // If there is no manifest file, ignore. If it was a component check if the xml was named with the com_ prefix. - if (!is_file($file)) - { - if (!$count) - { - continue; - } - - $file = $extensionPath . $extension . '.xml'; - - if (!is_file($file)) - { - continue; - } - } - - // Load the manifest file - $xml = simplexml_load_file($file); - - // When invalid, ignore - if (!$xml) - { - continue; - } - - // The namespace node - $namespaceNode = $xml->namespace; - - // The namespace string - $namespace = (string) $namespaceNode; - - // Ignore when the string is empty - if (!$namespace) - { - continue; - } - - // Normalize the namespace string - $namespace = str_replace('\\', '\\\\', $namespace) . '\\\\'; - $namespacePath = rtrim($extensionPath . $namespaceNode->attributes()->path, '/'); - - if ($type === 'plugin' || $type === 'library') - { - $baseDir = $type === 'plugin' ? 'JPATH_PLUGINS . \'' : 'JPATH_LIBRARIES . \''; - $path = str_replace($type === 'plugin' ? JPATH_PLUGINS : JPATH_LIBRARIES, '', $namespacePath); - - // Set the namespace - $extensions[$namespace] = $baseDir . $path . '\''; - - continue; - } - - // Check if we need to use administrator path - $isAdministrator = strpos($namespacePath, JPATH_ADMINISTRATOR) === 0; - $path = str_replace($isAdministrator ? JPATH_ADMINISTRATOR : JPATH_SITE, '', $namespacePath); - - // Add the site path when a component - if ($type === 'component') - { - if (is_dir(JPATH_SITE . $path)) - { - $extensions[$namespace . 'Site\\\\'] = 'JPATH_SITE . \'' . $path . '\''; - } - - if (is_dir(JPATH_API . $path)) - { - $extensions[$namespace . 'Api\\\\'] = 'JPATH_API . \'' . $path . '\''; - } - } - - // Add the application specific segment when a component or module - $baseDir = $isAdministrator ? 'JPATH_ADMINISTRATOR . \'' : 'JPATH_SITE . \''; - $namespace .= $isAdministrator ? 'Administrator\\\\' : 'Site\\\\'; - - // Set the namespace - $extensions[$namespace] = $baseDir . $path . '\''; - } - } - - // Return the namespaces - return $extensions; - } + /** + * Path to the autoloader + * + * @var string + * @since 4.0.0 + */ + protected $file = JPATH_CACHE . '/autoload_psr4.php'; + + /** + * @var array|null + * @since 4.0.0 + */ + private $cachedMap = null; + + /** + * Check if the file exists + * + * @return boolean + * + * @since 4.0.0 + */ + public function exists() + { + return is_file($this->file); + } + + /** + * Check if the namespace mapping file exists, if not create it + * + * @return void + * + * @since 4.0.0 + */ + public function ensureMapFileExists() + { + if (!$this->exists()) { + $this->create(); + } + } + + /** + * Create the namespace file + * + * @return boolean + * + * @since 4.0.0 + */ + public function create() + { + $extensions = array_merge( + $this->getNamespaces('component'), + $this->getNamespaces('module'), + $this->getNamespaces('plugin'), + $this->getNamespaces('library') + ); + + ksort($extensions); + + $this->writeNamespaceFile($extensions); + + return true; + } + + /** + * Load the PSR4 file + * + * @return boolean + * + * @since 4.0.0 + */ + public function load() + { + if (!$this->exists()) { + $this->create(); + } + + $map = $this->cachedMap ?: require $this->file; + + $loader = include JPATH_LIBRARIES . '/vendor/autoload.php'; + + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + return true; + } + + /** + * Write the Namespace mapping file + * + * @param array $elements Array of elements + * + * @return void + * + * @since 4.0.0 + */ + protected function writeNamespaceFile($elements) + { + $content = array(); + $content[] = " $path) { + $content[] = "\t'" . $namespace . "'" . ' => [' . $path . '],'; + } + + $content[] = '];'; + + /** + * Backup the current error_reporting level and set a new level + * + * We do this because file_put_contents can raise a Warning if it cannot write the autoload_psr4.php file + * and this will output to the response BEFORE the session has started, causing the session start to fail + * and ultimately leading us to a 500 Internal Server Error page just because of the output warning, which + * we can safely ignore as we can use an in-memory autoload_psr4 map temporarily, and display real errors later. + */ + $error_reporting = error_reporting(0); + + try { + File::write($this->file, implode("\n", $content)); + } catch (Exception $e) { + Log::add('Could not save ' . $this->file, Log::WARNING); + + $map = []; + $constants = ['JPATH_ADMINISTRATOR', 'JPATH_API', 'JPATH_SITE', 'JPATH_PLUGINS']; + + foreach ($elements as $namespace => $path) { + foreach ($constants as $constant) { + $path = preg_replace(['/^(' . $constant . ")\s\.\s\'/", '/\'$/'], [constant($constant), ''], $path); + } + + $namespace = str_replace('\\\\', '\\', $namespace); + $map[$namespace] = [ $path ]; + } + + $this->cachedMap = $map; + } + + // Restore previous value of error_reporting + error_reporting($error_reporting); + } + + /** + * Get an array of namespaces with their respective path for the given extension type. + * + * @param string $type The extension type + * + * @return array + * + * @since 4.0.0 + */ + private function getNamespaces(string $type): array + { + if (!in_array($type, ['component', 'module', 'plugin', 'library'], true)) { + return []; + } + + // Select directories containing extension manifest files. + if ($type === 'component') { + $directories = [JPATH_ADMINISTRATOR . '/components']; + } elseif ($type === 'module') { + $directories = [JPATH_SITE . '/modules', JPATH_ADMINISTRATOR . '/modules']; + } elseif ($type === 'plugin') { + try { + $directories = Folder::folders(JPATH_PLUGINS, '.', false, true); + } catch (Exception $e) { + $directories = []; + } + } else { + $directories = [JPATH_LIBRARIES]; + } + + $extensions = []; + + foreach ($directories as $directory) { + try { + $extensionFolders = Folder::folders($directory); + } catch (Exception $e) { + continue; + } + + foreach ($extensionFolders as $extension) { + // Compile the extension path + $extensionPath = $directory . '/' . $extension . '/'; + + // Strip the com_ from the extension name for components + $name = str_replace('com_', '', $extension, $count); + $file = $extensionPath . $name . '.xml'; + + // If there is no manifest file, ignore. If it was a component check if the xml was named with the com_ prefix. + if (!is_file($file)) { + if (!$count) { + continue; + } + + $file = $extensionPath . $extension . '.xml'; + + if (!is_file($file)) { + continue; + } + } + + // Load the manifest file + $xml = simplexml_load_file($file); + + // When invalid, ignore + if (!$xml) { + continue; + } + + // The namespace node + $namespaceNode = $xml->namespace; + + // The namespace string + $namespace = (string) $namespaceNode; + + // Ignore when the string is empty + if (!$namespace) { + continue; + } + + // Normalize the namespace string + $namespace = str_replace('\\', '\\\\', $namespace) . '\\\\'; + $namespacePath = rtrim($extensionPath . $namespaceNode->attributes()->path, '/'); + + if ($type === 'plugin' || $type === 'library') { + $baseDir = $type === 'plugin' ? 'JPATH_PLUGINS . \'' : 'JPATH_LIBRARIES . \''; + $path = str_replace($type === 'plugin' ? JPATH_PLUGINS : JPATH_LIBRARIES, '', $namespacePath); + + // Set the namespace + $extensions[$namespace] = $baseDir . $path . '\''; + + continue; + } + + // Check if we need to use administrator path + $isAdministrator = strpos($namespacePath, JPATH_ADMINISTRATOR) === 0; + $path = str_replace($isAdministrator ? JPATH_ADMINISTRATOR : JPATH_SITE, '', $namespacePath); + + // Add the site path when a component + if ($type === 'component') { + if (is_dir(JPATH_SITE . $path)) { + $extensions[$namespace . 'Site\\\\'] = 'JPATH_SITE . \'' . $path . '\''; + } + + if (is_dir(JPATH_API . $path)) { + $extensions[$namespace . 'Api\\\\'] = 'JPATH_API . \'' . $path . '\''; + } + } + + // Add the application specific segment when a component or module + $baseDir = $isAdministrator ? 'JPATH_ADMINISTRATOR . \'' : 'JPATH_SITE . \''; + $namespace .= $isAdministrator ? 'Administrator\\\\' : 'Site\\\\'; + + // Set the namespace + $extensions[$namespace] = $baseDir . $path . '\''; + } + } + + // Return the namespaces + return $extensions; + } } diff --git a/libraries/src/Access/Access.php b/libraries/src/Access/Access.php index 2452c86673710..926d7b1e9336d 100644 --- a/libraries/src/Access/Access.php +++ b/libraries/src/Access/Access.php @@ -1,4 +1,5 @@ allow($action, self::$identities[$userId]); - } - - /** - * Method to preload the Rules object for the given asset type. - * - * @param integer|string|array $assetTypes The type or name of the asset (e.g. 'com_content.article', 'com_menus.menu.2'). - * Also accepts the asset id. An array of asset type or a special - * 'components' string to load all component assets. - * @param boolean $reload Set to true to reload from database. - * - * @return boolean True on success. - * - * @since 1.6 - * @note This method will return void in 4.0. - */ - public static function preload($assetTypes = 'components', $reload = false) - { - // If sent an asset id, we first get the asset type for that asset id. - if (is_numeric($assetTypes)) - { - $assetTypes = self::getAssetType($assetTypes); - } - - // Check for default case: - $isDefault = \is_string($assetTypes) && \in_array($assetTypes, array('components', 'component')); - - // Preload the rules for all of the components. - if ($isDefault) - { - self::preloadComponents(); - - return true; - } - - // If we get to this point, this is a regular asset type and we'll proceed with the preloading process. - if (!\is_array($assetTypes)) - { - $assetTypes = (array) $assetTypes; - } - - foreach ($assetTypes as $assetType) - { - self::preloadPermissions($assetType, $reload); - } - - return true; - } - - /** - * Method to recursively retrieve the list of parent Asset IDs - * for a particular Asset. - * - * @param string $assetType The asset type, or the asset name, or the extension of the asset - * (e.g. 'com_content.article', 'com_menus.menu.2', 'com_contact'). - * @param integer $assetId The numeric asset id. - * - * @return array List of ancestor ids (includes original $assetId). - * - * @since 1.6 - */ - protected static function getAssetAncestors($assetType, $assetId) - { - // Get the extension name from the $assetType provided - $extensionName = self::getExtensionNameFromAsset($assetType); - - // Holds the list of ancestors for the Asset ID: - $ancestors = array(); - - // Add in our starting Asset ID: - $ancestors[] = (int) $assetId; - - // Initialize the variable we'll use in the loop: - $id = (int) $assetId; - - while ($id !== 0) - { - if (isset(self::$assetPermissionsParentIdMapping[$extensionName][$id])) - { - $id = (int) self::$assetPermissionsParentIdMapping[$extensionName][$id]->parent_id; - - if ($id !== 0) - { - $ancestors[] = $id; - } - } - else - { - // Add additional case to break out of the while loop automatically in - // the case that the ID is non-existent in our mapping variable above. - break; - } - } - - return $ancestors; - } - - /** - * Method to retrieve the Asset Rule strings for this particular - * Asset Type and stores them for later usage in getAssetRules(). - * Stores 2 arrays: one where the list has the Asset ID as the key - * and a second one where the Asset Name is the key. - * - * @param string $assetType The asset type, or the asset name, or the extension of the asset - * (e.g. 'com_content.article', 'com_menus.menu.2', 'com_contact'). - * @param boolean $reload Reload the preloaded assets. - * - * @return void - * - * @since 1.6 - */ - protected static function preloadPermissions($assetType, $reload = false) - { - // Get the extension name from the $assetType provided - $extensionName = self::getExtensionNameFromAsset($assetType); - - // If asset is a component, make sure that all the component assets are preloaded. - if ((isset(self::$preloadedAssetTypes[$extensionName]) || isset(self::$preloadedAssetTypes[$assetType])) && !$reload) - { - return; - } - - !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::preloadPermissions (' . $extensionName . ')'); - - // Get the database connection object. - $db = Factory::getDbo(); - $assetKey = $extensionName . '.%'; - - // Get a fresh query object. - $query = $db->getQuery(true) - ->select($db->quoteName(array('id', 'name', 'rules', 'parent_id'))) - ->from($db->quoteName('#__assets')) - ->where( - [ - $db->quoteName('name') . ' LIKE :asset', - $db->quoteName('name') . ' = :extension', - $db->quoteName('parent_id') . ' = 0', - ], - 'OR' - ) - ->bind(':extension', $extensionName) - ->bind(':asset', $assetKey); - - // Get the permission map for all assets in the asset extension. - $assets = $db->setQuery($query)->loadObjectList(); - - self::$assetPermissionsParentIdMapping[$extensionName] = array(); - - foreach ($assets as $asset) - { - self::$assetPermissionsParentIdMapping[$extensionName][$asset->id] = $asset; - self::$preloadedAssets[$asset->id] = $asset->name; - } - - // Mark asset type and it's extension name as preloaded. - self::$preloadedAssetTypes[$assetType] = true; - self::$preloadedAssetTypes[$extensionName] = true; - - !JDEBUG ?: Profiler::getInstance('Application')->mark('After Access::preloadPermissions (' . $extensionName . ')'); - } - - /** - * Method to preload the Rules objects for all components. - * - * Note: This will only get the base permissions for the component. - * e.g. it will get 'com_content', but not 'com_content.article.1' or - * any more specific asset type rules. - * - * @return array Array of component names that were preloaded. - * - * @since 1.6 - */ - protected static function preloadComponents() - { - // If the components already been preloaded do nothing. - if (isset(self::$preloadedAssetTypes['components'])) - { - return array(); - } - - !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::preloadComponents (all components)'); - - // Add root to asset names list. - $components = array('root.1'); - - // Add enabled components to asset names list. - foreach (ComponentHelper::getComponents() as $component) - { - if ($component->enabled) - { - $components[] = $component->option; - } - } - - // Get the database connection object. - $db = Factory::getDbo(); - - // Get the asset info for all assets in asset names list. - $query = $db->getQuery(true) - ->select($db->quoteName(array('id', 'name', 'rules', 'parent_id'))) - ->from($db->quoteName('#__assets')) - ->whereIn($db->quoteName('name'), $components, ParameterType::STRING); - - // Get the Name Permission Map List - $assets = $db->setQuery($query)->loadObjectList(); - - $rootAsset = null; - - // First add the root asset and save it to preload memory and mark it as preloaded. - foreach ($assets as &$asset) - { - if ((int) $asset->parent_id === 0) - { - $rootAsset = $asset; - self::$rootAssetId = $asset->id; - self::$preloadedAssetTypes[$asset->name] = true; - self::$preloadedAssets[$asset->id] = $asset->name; - self::$assetPermissionsParentIdMapping[$asset->name][$asset->id] = $asset; - - unset($asset); - break; - } - } - - // Now create save the components asset tree to preload memory. - foreach ($assets as $asset) - { - if (!isset(self::$assetPermissionsParentIdMapping[$asset->name])) - { - self::$assetPermissionsParentIdMapping[$asset->name] = array($rootAsset->id => $rootAsset, $asset->id => $asset); - self::$preloadedAssets[$asset->id] = $asset->name; - } - } - - // Mark all components asset type as preloaded. - self::$preloadedAssetTypes['components'] = true; - - !JDEBUG ?: Profiler::getInstance('Application')->mark('After Access::preloadComponents (all components)'); - - return $components; - } - - /** - * Method to check if a group is authorised to perform an action, optionally on an asset. - * - * @param integer $groupId The path to the group for which to check authorisation. - * @param string $action The name of the action to authorise. - * @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset. - * @param boolean $preload Indicates whether preloading should be used. - * - * @return boolean True if authorised. - * - * @since 1.7.0 - */ - public static function checkGroup($groupId, $action, $assetKey = null, $preload = true) - { - // Sanitize input. - $groupId = (int) $groupId; - $action = strtolower(preg_replace('#[\s\-]+#', '.', trim($action))); - - return self::getAssetRules($assetKey, true, true, $preload)->allow($action, self::getGroupPath($groupId)); - } - - /** - * Gets the parent groups that a leaf group belongs to in its branch back to the root of the tree - * (including the leaf group id). - * - * @param mixed $groupId An integer or array of integers representing the identities to check. - * - * @return mixed True if allowed, false for an explicit deny, null for an implicit deny. - * - * @since 1.7.0 - */ - protected static function getGroupPath($groupId) - { - // Load all the groups to improve performance on intensive groups checks - $groups = UserGroupsHelper::getInstance()->getAll(); - - if (!isset($groups[$groupId])) - { - return array(); - } - - return $groups[$groupId]->path; - } - - /** - * Method to return the Rules object for an asset. The returned object can optionally hold - * only the rules explicitly set for the asset or the summation of all inherited rules from - * parent assets and explicit rules. - * - * @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset. - * @param boolean $recursive True to return the rules object with inherited rules. - * @param boolean $recursiveParentAsset True to calculate the rule also based on inherited component/extension rules. - * @param boolean $preload Indicates whether preloading should be used. - * - * @return Rules Rules object for the asset. - * - * @since 1.7.0 - * @note The non preloading code will be removed in 4.0. All asset rules should use asset preloading. - */ - public static function getAssetRules($assetKey, $recursive = false, $recursiveParentAsset = true, $preload = true) - { - // Auto preloads the components assets and root asset (if chosen). - if ($preload) - { - self::preload('components'); - } - - // When asset key is null fallback to root asset. - $assetKey = self::cleanAssetKey($assetKey); - - // Auto preloads assets for the asset type (if chosen). - if ($preload) - { - self::preload(self::getAssetType($assetKey)); - } - - // Get the asset id and name. - $assetId = self::getAssetId($assetKey); - - // If asset rules already cached em memory return it (only in full recursive mode). - if ($recursive && $recursiveParentAsset && $assetId && isset(self::$assetRules[$assetId])) - { - return self::$assetRules[$assetId]; - } - - // Get the asset name and the extension name. - $assetName = self::getAssetName($assetKey); - $extensionName = self::getExtensionNameFromAsset($assetName); - - // If asset id does not exist fallback to extension asset, then root asset. - if (!$assetId) - { - if ($extensionName && $assetName !== $extensionName) - { - Log::add('No asset found for ' . $assetName . ', falling back to ' . $extensionName, Log::WARNING, 'assets'); - - return self::getAssetRules($extensionName, $recursive, $recursiveParentAsset, $preload); - } - - if (self::$rootAssetId !== null && $assetName !== self::$preloadedAssets[self::$rootAssetId]) - { - Log::add('No asset found for ' . $assetName . ', falling back to ' . self::$preloadedAssets[self::$rootAssetId], Log::WARNING, 'assets'); - - return self::getAssetRules(self::$preloadedAssets[self::$rootAssetId], $recursive, $recursiveParentAsset, $preload); - } - } - - // Almost all calls can take advantage of preloading. - if ($assetId && isset(self::$preloadedAssets[$assetId])) - { - !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::getAssetRules (id:' . $assetId . ' name:' . $assetName . ')'); - - // Collects permissions for each asset - $collected = array(); - - // If not in any recursive mode. We only want the asset rules. - if (!$recursive && !$recursiveParentAsset) - { - $collected = array(self::$assetPermissionsParentIdMapping[$extensionName][$assetId]->rules); - } - // If there is any type of recursive mode. - else - { - $ancestors = array_reverse(self::getAssetAncestors($extensionName, $assetId)); - - foreach ($ancestors as $id) - { - // There are no rules for this ancestor - if (!isset(self::$assetPermissionsParentIdMapping[$extensionName][$id])) - { - continue; - } - - // If full recursive mode, but not recursive parent mode, do not add the extension asset rules. - if ($recursive && !$recursiveParentAsset && self::$assetPermissionsParentIdMapping[$extensionName][$id]->name === $extensionName) - { - continue; - } - - // If not full recursive mode, but recursive parent mode, do not add other recursion rules. - if (!$recursive && $recursiveParentAsset && self::$assetPermissionsParentIdMapping[$extensionName][$id]->name !== $extensionName - && (int) self::$assetPermissionsParentIdMapping[$extensionName][$id]->id !== $assetId) - { - continue; - } - - // If empty asset to not add to rules. - if (self::$assetPermissionsParentIdMapping[$extensionName][$id]->rules === '{}') - { - continue; - } - - $collected[] = self::$assetPermissionsParentIdMapping[$extensionName][$id]->rules; - } - } - - /** - * Hashing the collected rules allows us to store - * only one instance of the Rules object for - * Assets that have the same exact permissions... - * it's a great way to save some memory. - */ - $hash = md5(implode(',', $collected)); - - if (!isset(self::$assetRulesIdentities[$hash])) - { - $rules = new Rules; - $rules->mergeCollection($collected); - - self::$assetRulesIdentities[$hash] = $rules; - } - - // Save asset rules to memory cache(only in full recursive mode). - if ($recursive && $recursiveParentAsset) - { - self::$assetRules[$assetId] = self::$assetRulesIdentities[$hash]; - } - - !JDEBUG ?: Profiler::getInstance('Application')->mark('After Access::getAssetRules (id:' . $assetId . ' name:' . $assetName . ')'); - - return self::$assetRulesIdentities[$hash]; - } - - // Non preloading code. Use old slower method, slower. Only used in rare cases (if any) or without preloading chosen. - Log::add('Asset ' . $assetKey . ' permissions fetch without preloading (slower method).', Log::INFO, 'assets'); - - !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::getAssetRules (assetKey:' . $assetKey . ')'); - - // There's no need to process it with the recursive method for the Root Asset ID. - if ((int) $assetKey === 1) - { - $recursive = false; - } - - // Get the database connection object. - $db = Factory::getDbo(); - - // Build the database query to get the rules for the asset. - $query = $db->getQuery(true) - ->select($db->quoteName($recursive ? 'b.rules' : 'a.rules', 'rules')) - ->from($db->quoteName('#__assets', 'a')); - - // If the asset identifier is numeric assume it is a primary key, else lookup by name. - if (is_numeric($assetKey)) - { - $query->where($db->quoteName('a.id') . ' = :asset', 'OR') - ->bind(':asset', $assetKey, ParameterType::INTEGER); - } - else - { - $query->where($db->quoteName('a.name') . ' = :asset', 'OR') - ->bind(':asset', $assetKey); - } - - if ($recursiveParentAsset && ($extensionName !== $assetKey || is_numeric($assetKey))) - { - $query->where($db->quoteName('a.name') . ' = :extension') - ->bind(':extension', $extensionName); - } - - // If we want the rules cascading up to the global asset node we need a self-join. - if ($recursive) - { - $query->where($db->quoteName('a.parent_id') . ' = 0') - ->join( - 'LEFT', - $db->quoteName('#__assets', 'b'), - $db->quoteName('b.lft') . ' <= ' . $db->quoteName('a.lft') . ' AND ' . $db->quoteName('b.rgt') . ' >= ' . $db->quoteName('a.rgt') - ) - ->order($db->quoteName('b.lft')); - } - - // Execute the query and load the rules from the result. - $result = $db->setQuery($query)->loadColumn(); - - // Get the root even if the asset is not found and in recursive mode - if (empty($result)) - { - $rootId = (new Asset($db))->getRootId(); - - $query->clear() - ->select($db->quoteName('rules')) - ->from($db->quoteName('#__assets')) - ->where($db->quoteName('id') . ' = :rootId') - ->bind(':rootId', $rootId, ParameterType::INTEGER); - - $result = $db->setQuery($query)->loadColumn(); - } - - // Instantiate and return the Rules object for the asset rules. - $rules = new Rules; - $rules->mergeCollection($result); - - !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::getAssetRules Slower (assetKey:' . $assetKey . ')'); - - return $rules; - } - - /** - * Method to clean the asset key to make sure we always have something. - * - * @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset. - * - * @return integer|string Asset id or asset name. - * - * @since 3.7.0 - */ - protected static function cleanAssetKey($assetKey = null) - { - // If it's a valid asset key, clean it and return it. - if ($assetKey) - { - return strtolower(preg_replace('#[\s\-]+#', '.', trim($assetKey))); - } - - // Return root asset id if already preloaded. - if (self::$rootAssetId !== null) - { - return self::$rootAssetId; - } - - // No preload. Return root asset id from Assets. - $assets = new Asset(Factory::getDbo()); - - return $assets->getRootId(); - } - - /** - * Method to get the asset id from the asset key. - * - * @param integer|string $assetKey The asset key (asset id or asset name). - * - * @return integer The asset id. - * - * @since 3.7.0 - */ - protected static function getAssetId($assetKey) - { - static $loaded = array(); - - // If the asset is already an id return it. - if (is_numeric($assetKey)) - { - return (int) $assetKey; - } - - if (!isset($loaded[$assetKey])) - { - // It's the root asset. - if (self::$rootAssetId !== null && $assetKey === self::$preloadedAssets[self::$rootAssetId]) - { - $loaded[$assetKey] = self::$rootAssetId; - } - else - { - $preloadedAssetsByName = array_flip(self::$preloadedAssets); - - // If we already have the asset name stored in preloading, example, a component, no need to fetch it from table. - if (isset($preloadedAssetsByName[$assetKey])) - { - $loaded[$assetKey] = $preloadedAssetsByName[$assetKey]; - } - // Else we have to do an extra db query to fetch it from the table fetch it from table. - else - { - $table = new Asset(Factory::getDbo()); - $table->load(array('name' => $assetKey)); - $loaded[$assetKey] = $table->id; - } - } - } - - return (int) $loaded[$assetKey]; - } - - /** - * Method to get the asset name from the asset key. - * - * @param integer|string $assetKey The asset key (asset id or asset name). - * - * @return string The asset name (ex: com_content.article.8). - * - * @since 3.7.0 - */ - protected static function getAssetName($assetKey) - { - static $loaded = array(); - - // If the asset is already a string return it. - if (!is_numeric($assetKey)) - { - return $assetKey; - } - - if (!isset($loaded[$assetKey])) - { - // It's the root asset. - if (self::$rootAssetId !== null && $assetKey === self::$rootAssetId) - { - $loaded[$assetKey] = self::$preloadedAssets[self::$rootAssetId]; - } - // If we already have the asset name stored in preloading, example, a component, no need to fetch it from table. - elseif (isset(self::$preloadedAssets[$assetKey])) - { - $loaded[$assetKey] = self::$preloadedAssets[$assetKey]; - } - // Else we have to do an extra db query to fetch it from the table fetch it from table. - else - { - $table = new Asset(Factory::getDbo()); - $table->load($assetKey); - $loaded[$assetKey] = $table->name; - } - } - - return $loaded[$assetKey]; - } - - /** - * Method to get the extension name from the asset name. - * - * @param integer|string $assetKey The asset key (asset id or asset name). - * - * @return string The extension name (ex: com_content). - * - * @since 1.6 - */ - public static function getExtensionNameFromAsset($assetKey) - { - static $loaded = array(); - - if (!isset($loaded[$assetKey])) - { - $assetName = self::getAssetName($assetKey); - $firstDot = strpos($assetName, '.'); - - if ($assetName !== 'root.1' && $firstDot !== false) - { - $assetName = substr($assetName, 0, $firstDot); - } - - $loaded[$assetKey] = $assetName; - } - - return $loaded[$assetKey]; - } - - /** - * Method to get the asset type from the asset name. - * - * For top level components this returns "components": - * 'com_content' returns 'components' - * - * For other types: - * 'com_content.article.1' returns 'com_content.article' - * 'com_content.category.1' returns 'com_content.category' - * - * @param integer|string $assetKey The asset key (asset id or asset name). - * - * @return string The asset type (ex: com_content.article). - * - * @since 1.6 - */ - public static function getAssetType($assetKey) - { - // If the asset is already a string return it. - $assetName = self::getAssetName($assetKey); - $lastDot = strrpos($assetName, '.'); - - if ($assetName !== 'root.1' && $lastDot !== false) - { - return substr($assetName, 0, $lastDot); - } - - return 'components'; - } - - /** - * Method to return the title of a user group - * - * @param integer $groupId Id of the group for which to get the title of. - * - * @return string The title of the group - * - * @since 3.5 - */ - public static function getGroupTitle($groupId) - { - // Cast as integer until method is typehinted. - $groupId = (int) $groupId; - - // Fetch the group title from the database - $db = Factory::getDbo(); - $query = $db->getQuery(true); - $query->select($db->quoteName('title')) - ->from($db->quoteName('#__usergroups')) - ->where($db->quoteName('id') . ' = :groupId') - ->bind(':groupId', $groupId, ParameterType::INTEGER); - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Method to return a list of user groups mapped to a user. The returned list can optionally hold - * only the groups explicitly mapped to the user or all groups both explicitly mapped and inherited - * by the user. - * - * @param integer $userId Id of the user for which to get the list of groups. - * @param boolean $recursive True to include inherited user groups. - * - * @return array List of user group ids to which the user is mapped. - * - * @since 1.7.0 - */ - public static function getGroupsByUser($userId, $recursive = true) - { - // Cast as integer until method is typehinted. - $userId = (int) $userId; - - // Creates a simple unique string for each parameter combination: - $storeId = $userId . ':' . (int) $recursive; - - if (!isset(self::$groupsByUser[$storeId])) - { - // @todo: Uncouple this from ComponentHelper and allow for a configuration setting or value injection. - $guestUsergroup = (int) ComponentHelper::getParams('com_users')->get('guest_usergroup', 1); - - // Guest user (if only the actually assigned group is requested) - if (empty($userId) && !$recursive) - { - $result = array($guestUsergroup); - } - // Registered user and guest if all groups are requested - else - { - $db = Factory::getDbo(); - - // Build the database query to get the rules for the asset. - $query = $db->getQuery(true) - ->select($db->quoteName($recursive ? 'b.id' : 'a.id')); - - if (empty($userId)) - { - $query->from($db->quoteName('#__usergroups', 'a')) - ->where($db->quoteName('a.id') . ' = :guest') - ->bind(':guest', $guestUsergroup, ParameterType::INTEGER); - } - else - { - $query->from($db->quoteName('#__user_usergroup_map', 'map')) - ->where($db->quoteName('map.user_id') . ' = :userId') - ->join('LEFT', $db->quoteName('#__usergroups', 'a'), $db->quoteName('a.id') . ' = ' . $db->quoteName('map.group_id')) - ->bind(':userId', $userId, ParameterType::INTEGER); - } - - // If we want the rules cascading up to the global asset node we need a self-join. - if ($recursive) - { - $query->join( - 'LEFT', - $db->quoteName('#__usergroups', 'b'), - $db->quoteName('b.lft') . ' <= ' . $db->quoteName('a.lft') . ' AND ' . $db->quoteName('b.rgt') . ' >= ' . $db->quoteName('a.rgt') - ); - } - - // Execute the query and load the rules from the result. - $db->setQuery($query); - $result = $db->loadColumn(); - - // Clean up any NULL or duplicate values, just in case - $result = ArrayHelper::toInteger($result); - - if (empty($result)) - { - $result = array(1); - } - else - { - $result = array_unique($result); - } - } - - self::$groupsByUser[$storeId] = $result; - } - - return self::$groupsByUser[$storeId]; - } - - /** - * Method to return a list of user Ids contained in a Group - * - * @param integer $groupId The group Id - * @param boolean $recursive Recursively include all child groups (optional) - * - * @return array - * - * @since 1.7.0 - * @todo This method should move somewhere else - */ - public static function getUsersByGroup($groupId, $recursive = false) - { - // Cast as integer until method is typehinted. - $groupId = (int) $groupId; - - // Get a database object. - $db = Factory::getDbo(); - - $test = $recursive ? ' >= ' : ' = '; - - // First find the users contained in the group - $query = $db->getQuery(true) - ->select('DISTINCT(' . $db->quoteName('user_id') . ')') - ->from($db->quoteName('#__usergroups', 'ug1')) - ->join( - 'INNER', - $db->quoteName('#__usergroups', 'ug2'), - $db->quoteName('ug2.lft') . $test . $db->quoteName('ug1.lft') . ' AND ' . $db->quoteName('ug1.rgt') . $test . $db->quoteName('ug2.rgt') - ) - ->join('INNER', $db->quoteName('#__user_usergroup_map', 'm'), $db->quoteName('ug2.id') . ' = ' . $db->quoteName('m.group_id')) - ->where($db->quoteName('ug1.id') . ' = :groupId') - ->bind(':groupId', $groupId, ParameterType::INTEGER); - - $db->setQuery($query); - - $result = $db->loadColumn(); - - // Clean up any NULL values, just in case - $result = ArrayHelper::toInteger($result); - - return $result; - } - - /** - * Method to return a list of view levels for which the user is authorised. - * - * @param integer $userId Id of the user for which to get the list of authorised view levels. - * - * @return array List of view levels for which the user is authorised. - * - * @since 1.7.0 - */ - public static function getAuthorisedViewLevels($userId) - { - // Only load the view levels once. - if (empty(self::$viewLevels)) - { - // Get a database object. - $db = Factory::getDbo(); - - // Build the base query. - $query = $db->getQuery(true) - ->select($db->quoteName(['id', 'rules'])) - ->from($db->quoteName('#__viewlevels')); - - // Set the query for execution. - $db->setQuery($query); - - // Build the view levels array. - foreach ($db->loadAssocList() as $level) - { - self::$viewLevels[$level['id']] = (array) json_decode($level['rules']); - } - } - - // Initialise the authorised array. - $authorised = array(1); - - // Check for the recovery mode setting and return early. - $user = User::getInstance($userId); - $root_user = Factory::getApplication()->get('root_user'); - - if (($user->username && $user->username == $root_user) || (is_numeric($root_user) && $user->id > 0 && $user->id == $root_user)) - { - // Find the super user levels. - foreach (self::$viewLevels as $level => $rule) - { - foreach ($rule as $id) - { - if ($id > 0 && self::checkGroup($id, 'core.admin')) - { - $authorised[] = $level; - break; - } - } - } - - return array_values(array_unique($authorised)); - } - - // Get all groups that the user is mapped to recursively. - $groups = self::getGroupsByUser($userId); - - // Find the authorised levels. - foreach (self::$viewLevels as $level => $rule) - { - foreach ($rule as $id) - { - if (($id < 0) && (($id * -1) == $userId)) - { - $authorised[] = $level; - break; - } - // Check to see if the group is mapped to the level. - elseif (($id >= 0) && \in_array($id, $groups)) - { - $authorised[] = $level; - break; - } - } - } - - return array_values(array_unique($authorised)); - } - - /** - * Method to return a list of actions from a file for which permissions can be set. - * - * @param string $file The path to the XML file. - * @param string $xpath An optional xpath to search for the fields. - * - * @return boolean|array False if case of error or the list of actions available. - * - * @since 3.0.0 - */ - public static function getActionsFromFile($file, $xpath = "/access/section[@name='component']/") - { - if (!is_file($file) || !is_readable($file)) - { - // If unable to find the file return false. - return false; - } - else - { - // Else return the actions from the xml. - $xml = simplexml_load_file($file); - - return self::getActionsFromData($xml, $xpath); - } - } - - /** - * Method to return a list of actions from a string or from an xml for which permissions can be set. - * - * @param string|\SimpleXMLElement $data The XML string or an XML element. - * @param string $xpath An optional xpath to search for the fields. - * - * @return boolean|array False if case of error or the list of actions available. - * - * @since 3.0.0 - */ - public static function getActionsFromData($data, $xpath = "/access/section[@name='component']/") - { - // If the data to load isn't already an XML element or string return false. - if ((!($data instanceof \SimpleXMLElement)) && (!\is_string($data))) - { - return false; - } - - // Attempt to load the XML if a string. - if (\is_string($data)) - { - try - { - $data = new \SimpleXMLElement($data); - } - catch (\Exception $e) - { - return false; - } - - // Make sure the XML loaded correctly. - if (!$data) - { - return false; - } - } - - // Initialise the actions array - $actions = array(); - - // Get the elements from the xpath - $elements = $data->xpath($xpath . 'action[@name][@title]'); - - // If there some elements, analyse them - if (!empty($elements)) - { - foreach ($elements as $element) - { - // Add the action to the actions array - $action = array( - 'name' => (string) $element['name'], - 'title' => (string) $element['title'], - ); - - if (isset($element['description'])) - { - $action['description'] = (string) $element['description']; - } - - $actions[] = (object) $action; - } - } - - // Finally return the actions array - return $actions; - } + /** + * Array of view levels + * + * @var array + * @since 1.7.0 + */ + protected static $viewLevels = array(); + + /** + * Array of rules for the asset + * + * @var array + * @since 1.7.0 + */ + protected static $assetRules = array(); + + /** + * Array of identities for asset rules + * + * @var array + * @since 1.7.0 + */ + protected static $assetRulesIdentities = array(); + + /** + * Array of the permission parent ID mappings + * + * @var array + * @since 1.7.0 + */ + protected static $assetPermissionsParentIdMapping = array(); + + /** + * Array of asset types that have been preloaded + * + * @var array + * @since 1.7.0 + */ + protected static $preloadedAssetTypes = array(); + + /** + * Array of loaded user identities + * + * @var array + * @since 1.7.0 + */ + protected static $identities = array(); + + /** + * Array of user groups. + * + * @var array + * @since 1.7.0 + */ + protected static $userGroups = array(); + + /** + * Array of user group paths. + * + * @var array + * @since 1.7.0 + */ + protected static $userGroupPaths = array(); + + /** + * Array of cached groups by user. + * + * @var array + * @since 1.7.0 + */ + protected static $groupsByUser = array(); + + /** + * Array of preloaded asset names and ids (key is the asset id). + * + * @var array + * @since 3.7.0 + */ + protected static $preloadedAssets = array(); + + /** + * The root asset id. + * + * @var integer + * @since 3.7.0 + */ + protected static $rootAssetId = null; + + /** + * Method for clearing static caches. + * + * @return void + * + * @since 1.7.3 + */ + public static function clearStatics() + { + self::$viewLevels = array(); + self::$assetRules = array(); + self::$assetRulesIdentities = array(); + self::$assetPermissionsParentIdMapping = array(); + self::$preloadedAssetTypes = array(); + self::$identities = array(); + self::$userGroups = array(); + self::$userGroupPaths = array(); + self::$groupsByUser = array(); + self::$preloadedAssets = array(); + self::$rootAssetId = null; + } + + /** + * Method to check if a user is authorised to perform an action, optionally on an asset. + * + * @param integer $userId Id of the user for which to check authorisation. + * @param string $action The name of the action to authorise. + * @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset. + * @param boolean $preload Indicates whether preloading should be used. + * + * @return boolean|null True if allowed, false for an explicit deny, null for an implicit deny. + * + * @since 1.7.0 + */ + public static function check($userId, $action, $assetKey = null, $preload = true) + { + // Sanitise inputs. + $userId = (int) $userId; + $action = strtolower(preg_replace('#[\s\-]+#', '.', trim($action))); + + if (!isset(self::$identities[$userId])) { + // Get all groups against which the user is mapped. + self::$identities[$userId] = self::getGroupsByUser($userId); + array_unshift(self::$identities[$userId], $userId * -1); + } + + return self::getAssetRules($assetKey, true, true, $preload)->allow($action, self::$identities[$userId]); + } + + /** + * Method to preload the Rules object for the given asset type. + * + * @param integer|string|array $assetTypes The type or name of the asset (e.g. 'com_content.article', 'com_menus.menu.2'). + * Also accepts the asset id. An array of asset type or a special + * 'components' string to load all component assets. + * @param boolean $reload Set to true to reload from database. + * + * @return boolean True on success. + * + * @since 1.6 + * @note This method will return void in 4.0. + */ + public static function preload($assetTypes = 'components', $reload = false) + { + // If sent an asset id, we first get the asset type for that asset id. + if (is_numeric($assetTypes)) { + $assetTypes = self::getAssetType($assetTypes); + } + + // Check for default case: + $isDefault = \is_string($assetTypes) && \in_array($assetTypes, array('components', 'component')); + + // Preload the rules for all of the components. + if ($isDefault) { + self::preloadComponents(); + + return true; + } + + // If we get to this point, this is a regular asset type and we'll proceed with the preloading process. + if (!\is_array($assetTypes)) { + $assetTypes = (array) $assetTypes; + } + + foreach ($assetTypes as $assetType) { + self::preloadPermissions($assetType, $reload); + } + + return true; + } + + /** + * Method to recursively retrieve the list of parent Asset IDs + * for a particular Asset. + * + * @param string $assetType The asset type, or the asset name, or the extension of the asset + * (e.g. 'com_content.article', 'com_menus.menu.2', 'com_contact'). + * @param integer $assetId The numeric asset id. + * + * @return array List of ancestor ids (includes original $assetId). + * + * @since 1.6 + */ + protected static function getAssetAncestors($assetType, $assetId) + { + // Get the extension name from the $assetType provided + $extensionName = self::getExtensionNameFromAsset($assetType); + + // Holds the list of ancestors for the Asset ID: + $ancestors = array(); + + // Add in our starting Asset ID: + $ancestors[] = (int) $assetId; + + // Initialize the variable we'll use in the loop: + $id = (int) $assetId; + + while ($id !== 0) { + if (isset(self::$assetPermissionsParentIdMapping[$extensionName][$id])) { + $id = (int) self::$assetPermissionsParentIdMapping[$extensionName][$id]->parent_id; + + if ($id !== 0) { + $ancestors[] = $id; + } + } else { + // Add additional case to break out of the while loop automatically in + // the case that the ID is non-existent in our mapping variable above. + break; + } + } + + return $ancestors; + } + + /** + * Method to retrieve the Asset Rule strings for this particular + * Asset Type and stores them for later usage in getAssetRules(). + * Stores 2 arrays: one where the list has the Asset ID as the key + * and a second one where the Asset Name is the key. + * + * @param string $assetType The asset type, or the asset name, or the extension of the asset + * (e.g. 'com_content.article', 'com_menus.menu.2', 'com_contact'). + * @param boolean $reload Reload the preloaded assets. + * + * @return void + * + * @since 1.6 + */ + protected static function preloadPermissions($assetType, $reload = false) + { + // Get the extension name from the $assetType provided + $extensionName = self::getExtensionNameFromAsset($assetType); + + // If asset is a component, make sure that all the component assets are preloaded. + if ((isset(self::$preloadedAssetTypes[$extensionName]) || isset(self::$preloadedAssetTypes[$assetType])) && !$reload) { + return; + } + + !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::preloadPermissions (' . $extensionName . ')'); + + // Get the database connection object. + $db = Factory::getDbo(); + $assetKey = $extensionName . '.%'; + + // Get a fresh query object. + $query = $db->getQuery(true) + ->select($db->quoteName(array('id', 'name', 'rules', 'parent_id'))) + ->from($db->quoteName('#__assets')) + ->where( + [ + $db->quoteName('name') . ' LIKE :asset', + $db->quoteName('name') . ' = :extension', + $db->quoteName('parent_id') . ' = 0', + ], + 'OR' + ) + ->bind(':extension', $extensionName) + ->bind(':asset', $assetKey); + + // Get the permission map for all assets in the asset extension. + $assets = $db->setQuery($query)->loadObjectList(); + + self::$assetPermissionsParentIdMapping[$extensionName] = array(); + + foreach ($assets as $asset) { + self::$assetPermissionsParentIdMapping[$extensionName][$asset->id] = $asset; + self::$preloadedAssets[$asset->id] = $asset->name; + } + + // Mark asset type and it's extension name as preloaded. + self::$preloadedAssetTypes[$assetType] = true; + self::$preloadedAssetTypes[$extensionName] = true; + + !JDEBUG ?: Profiler::getInstance('Application')->mark('After Access::preloadPermissions (' . $extensionName . ')'); + } + + /** + * Method to preload the Rules objects for all components. + * + * Note: This will only get the base permissions for the component. + * e.g. it will get 'com_content', but not 'com_content.article.1' or + * any more specific asset type rules. + * + * @return array Array of component names that were preloaded. + * + * @since 1.6 + */ + protected static function preloadComponents() + { + // If the components already been preloaded do nothing. + if (isset(self::$preloadedAssetTypes['components'])) { + return array(); + } + + !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::preloadComponents (all components)'); + + // Add root to asset names list. + $components = array('root.1'); + + // Add enabled components to asset names list. + foreach (ComponentHelper::getComponents() as $component) { + if ($component->enabled) { + $components[] = $component->option; + } + } + + // Get the database connection object. + $db = Factory::getDbo(); + + // Get the asset info for all assets in asset names list. + $query = $db->getQuery(true) + ->select($db->quoteName(array('id', 'name', 'rules', 'parent_id'))) + ->from($db->quoteName('#__assets')) + ->whereIn($db->quoteName('name'), $components, ParameterType::STRING); + + // Get the Name Permission Map List + $assets = $db->setQuery($query)->loadObjectList(); + + $rootAsset = null; + + // First add the root asset and save it to preload memory and mark it as preloaded. + foreach ($assets as &$asset) { + if ((int) $asset->parent_id === 0) { + $rootAsset = $asset; + self::$rootAssetId = $asset->id; + self::$preloadedAssetTypes[$asset->name] = true; + self::$preloadedAssets[$asset->id] = $asset->name; + self::$assetPermissionsParentIdMapping[$asset->name][$asset->id] = $asset; + + unset($asset); + break; + } + } + + // Now create save the components asset tree to preload memory. + foreach ($assets as $asset) { + if (!isset(self::$assetPermissionsParentIdMapping[$asset->name])) { + self::$assetPermissionsParentIdMapping[$asset->name] = array($rootAsset->id => $rootAsset, $asset->id => $asset); + self::$preloadedAssets[$asset->id] = $asset->name; + } + } + + // Mark all components asset type as preloaded. + self::$preloadedAssetTypes['components'] = true; + + !JDEBUG ?: Profiler::getInstance('Application')->mark('After Access::preloadComponents (all components)'); + + return $components; + } + + /** + * Method to check if a group is authorised to perform an action, optionally on an asset. + * + * @param integer $groupId The path to the group for which to check authorisation. + * @param string $action The name of the action to authorise. + * @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset. + * @param boolean $preload Indicates whether preloading should be used. + * + * @return boolean True if authorised. + * + * @since 1.7.0 + */ + public static function checkGroup($groupId, $action, $assetKey = null, $preload = true) + { + // Sanitize input. + $groupId = (int) $groupId; + $action = strtolower(preg_replace('#[\s\-]+#', '.', trim($action))); + + return self::getAssetRules($assetKey, true, true, $preload)->allow($action, self::getGroupPath($groupId)); + } + + /** + * Gets the parent groups that a leaf group belongs to in its branch back to the root of the tree + * (including the leaf group id). + * + * @param mixed $groupId An integer or array of integers representing the identities to check. + * + * @return mixed True if allowed, false for an explicit deny, null for an implicit deny. + * + * @since 1.7.0 + */ + protected static function getGroupPath($groupId) + { + // Load all the groups to improve performance on intensive groups checks + $groups = UserGroupsHelper::getInstance()->getAll(); + + if (!isset($groups[$groupId])) { + return array(); + } + + return $groups[$groupId]->path; + } + + /** + * Method to return the Rules object for an asset. The returned object can optionally hold + * only the rules explicitly set for the asset or the summation of all inherited rules from + * parent assets and explicit rules. + * + * @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset. + * @param boolean $recursive True to return the rules object with inherited rules. + * @param boolean $recursiveParentAsset True to calculate the rule also based on inherited component/extension rules. + * @param boolean $preload Indicates whether preloading should be used. + * + * @return Rules Rules object for the asset. + * + * @since 1.7.0 + * @note The non preloading code will be removed in 4.0. All asset rules should use asset preloading. + */ + public static function getAssetRules($assetKey, $recursive = false, $recursiveParentAsset = true, $preload = true) + { + // Auto preloads the components assets and root asset (if chosen). + if ($preload) { + self::preload('components'); + } + + // When asset key is null fallback to root asset. + $assetKey = self::cleanAssetKey($assetKey); + + // Auto preloads assets for the asset type (if chosen). + if ($preload) { + self::preload(self::getAssetType($assetKey)); + } + + // Get the asset id and name. + $assetId = self::getAssetId($assetKey); + + // If asset rules already cached em memory return it (only in full recursive mode). + if ($recursive && $recursiveParentAsset && $assetId && isset(self::$assetRules[$assetId])) { + return self::$assetRules[$assetId]; + } + + // Get the asset name and the extension name. + $assetName = self::getAssetName($assetKey); + $extensionName = self::getExtensionNameFromAsset($assetName); + + // If asset id does not exist fallback to extension asset, then root asset. + if (!$assetId) { + if ($extensionName && $assetName !== $extensionName) { + Log::add('No asset found for ' . $assetName . ', falling back to ' . $extensionName, Log::WARNING, 'assets'); + + return self::getAssetRules($extensionName, $recursive, $recursiveParentAsset, $preload); + } + + if (self::$rootAssetId !== null && $assetName !== self::$preloadedAssets[self::$rootAssetId]) { + Log::add('No asset found for ' . $assetName . ', falling back to ' . self::$preloadedAssets[self::$rootAssetId], Log::WARNING, 'assets'); + + return self::getAssetRules(self::$preloadedAssets[self::$rootAssetId], $recursive, $recursiveParentAsset, $preload); + } + } + + // Almost all calls can take advantage of preloading. + if ($assetId && isset(self::$preloadedAssets[$assetId])) { + !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::getAssetRules (id:' . $assetId . ' name:' . $assetName . ')'); + + // Collects permissions for each asset + $collected = array(); + + // If not in any recursive mode. We only want the asset rules. + if (!$recursive && !$recursiveParentAsset) { + $collected = array(self::$assetPermissionsParentIdMapping[$extensionName][$assetId]->rules); + } else { + // If there is any type of recursive mode. + $ancestors = array_reverse(self::getAssetAncestors($extensionName, $assetId)); + + foreach ($ancestors as $id) { + // There are no rules for this ancestor + if (!isset(self::$assetPermissionsParentIdMapping[$extensionName][$id])) { + continue; + } + + // If full recursive mode, but not recursive parent mode, do not add the extension asset rules. + if ($recursive && !$recursiveParentAsset && self::$assetPermissionsParentIdMapping[$extensionName][$id]->name === $extensionName) { + continue; + } + + // If not full recursive mode, but recursive parent mode, do not add other recursion rules. + if ( + !$recursive && $recursiveParentAsset && self::$assetPermissionsParentIdMapping[$extensionName][$id]->name !== $extensionName + && (int) self::$assetPermissionsParentIdMapping[$extensionName][$id]->id !== $assetId + ) { + continue; + } + + // If empty asset to not add to rules. + if (self::$assetPermissionsParentIdMapping[$extensionName][$id]->rules === '{}') { + continue; + } + + $collected[] = self::$assetPermissionsParentIdMapping[$extensionName][$id]->rules; + } + } + + /** + * Hashing the collected rules allows us to store + * only one instance of the Rules object for + * Assets that have the same exact permissions... + * it's a great way to save some memory. + */ + $hash = md5(implode(',', $collected)); + + if (!isset(self::$assetRulesIdentities[$hash])) { + $rules = new Rules(); + $rules->mergeCollection($collected); + + self::$assetRulesIdentities[$hash] = $rules; + } + + // Save asset rules to memory cache(only in full recursive mode). + if ($recursive && $recursiveParentAsset) { + self::$assetRules[$assetId] = self::$assetRulesIdentities[$hash]; + } + + !JDEBUG ?: Profiler::getInstance('Application')->mark('After Access::getAssetRules (id:' . $assetId . ' name:' . $assetName . ')'); + + return self::$assetRulesIdentities[$hash]; + } + + // Non preloading code. Use old slower method, slower. Only used in rare cases (if any) or without preloading chosen. + Log::add('Asset ' . $assetKey . ' permissions fetch without preloading (slower method).', Log::INFO, 'assets'); + + !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::getAssetRules (assetKey:' . $assetKey . ')'); + + // There's no need to process it with the recursive method for the Root Asset ID. + if ((int) $assetKey === 1) { + $recursive = false; + } + + // Get the database connection object. + $db = Factory::getDbo(); + + // Build the database query to get the rules for the asset. + $query = $db->getQuery(true) + ->select($db->quoteName($recursive ? 'b.rules' : 'a.rules', 'rules')) + ->from($db->quoteName('#__assets', 'a')); + + // If the asset identifier is numeric assume it is a primary key, else lookup by name. + if (is_numeric($assetKey)) { + $query->where($db->quoteName('a.id') . ' = :asset', 'OR') + ->bind(':asset', $assetKey, ParameterType::INTEGER); + } else { + $query->where($db->quoteName('a.name') . ' = :asset', 'OR') + ->bind(':asset', $assetKey); + } + + if ($recursiveParentAsset && ($extensionName !== $assetKey || is_numeric($assetKey))) { + $query->where($db->quoteName('a.name') . ' = :extension') + ->bind(':extension', $extensionName); + } + + // If we want the rules cascading up to the global asset node we need a self-join. + if ($recursive) { + $query->where($db->quoteName('a.parent_id') . ' = 0') + ->join( + 'LEFT', + $db->quoteName('#__assets', 'b'), + $db->quoteName('b.lft') . ' <= ' . $db->quoteName('a.lft') . ' AND ' . $db->quoteName('b.rgt') . ' >= ' . $db->quoteName('a.rgt') + ) + ->order($db->quoteName('b.lft')); + } + + // Execute the query and load the rules from the result. + $result = $db->setQuery($query)->loadColumn(); + + // Get the root even if the asset is not found and in recursive mode + if (empty($result)) { + $rootId = (new Asset($db))->getRootId(); + + $query->clear() + ->select($db->quoteName('rules')) + ->from($db->quoteName('#__assets')) + ->where($db->quoteName('id') . ' = :rootId') + ->bind(':rootId', $rootId, ParameterType::INTEGER); + + $result = $db->setQuery($query)->loadColumn(); + } + + // Instantiate and return the Rules object for the asset rules. + $rules = new Rules(); + $rules->mergeCollection($result); + + !JDEBUG ?: Profiler::getInstance('Application')->mark('Before Access::getAssetRules Slower (assetKey:' . $assetKey . ')'); + + return $rules; + } + + /** + * Method to clean the asset key to make sure we always have something. + * + * @param integer|string $assetKey The asset key (asset id or asset name). null fallback to root asset. + * + * @return integer|string Asset id or asset name. + * + * @since 3.7.0 + */ + protected static function cleanAssetKey($assetKey = null) + { + // If it's a valid asset key, clean it and return it. + if ($assetKey) { + return strtolower(preg_replace('#[\s\-]+#', '.', trim($assetKey))); + } + + // Return root asset id if already preloaded. + if (self::$rootAssetId !== null) { + return self::$rootAssetId; + } + + // No preload. Return root asset id from Assets. + $assets = new Asset(Factory::getDbo()); + + return $assets->getRootId(); + } + + /** + * Method to get the asset id from the asset key. + * + * @param integer|string $assetKey The asset key (asset id or asset name). + * + * @return integer The asset id. + * + * @since 3.7.0 + */ + protected static function getAssetId($assetKey) + { + static $loaded = array(); + + // If the asset is already an id return it. + if (is_numeric($assetKey)) { + return (int) $assetKey; + } + + if (!isset($loaded[$assetKey])) { + // It's the root asset. + if (self::$rootAssetId !== null && $assetKey === self::$preloadedAssets[self::$rootAssetId]) { + $loaded[$assetKey] = self::$rootAssetId; + } else { + $preloadedAssetsByName = array_flip(self::$preloadedAssets); + + // If we already have the asset name stored in preloading, example, a component, no need to fetch it from table. + if (isset($preloadedAssetsByName[$assetKey])) { + $loaded[$assetKey] = $preloadedAssetsByName[$assetKey]; + } else { + // Else we have to do an extra db query to fetch it from the table fetch it from table. + $table = new Asset(Factory::getDbo()); + $table->load(array('name' => $assetKey)); + $loaded[$assetKey] = $table->id; + } + } + } + + return (int) $loaded[$assetKey]; + } + + /** + * Method to get the asset name from the asset key. + * + * @param integer|string $assetKey The asset key (asset id or asset name). + * + * @return string The asset name (ex: com_content.article.8). + * + * @since 3.7.0 + */ + protected static function getAssetName($assetKey) + { + static $loaded = array(); + + // If the asset is already a string return it. + if (!is_numeric($assetKey)) { + return $assetKey; + } + + if (!isset($loaded[$assetKey])) { + // It's the root asset. + if (self::$rootAssetId !== null && $assetKey === self::$rootAssetId) { + $loaded[$assetKey] = self::$preloadedAssets[self::$rootAssetId]; + } elseif (isset(self::$preloadedAssets[$assetKey])) { + // If we already have the asset name stored in preloading, example, a component, no need to fetch it from table. + $loaded[$assetKey] = self::$preloadedAssets[$assetKey]; + } else { + // Else we have to do an extra db query to fetch it from the table fetch it from table. + $table = new Asset(Factory::getDbo()); + $table->load($assetKey); + $loaded[$assetKey] = $table->name; + } + } + + return $loaded[$assetKey]; + } + + /** + * Method to get the extension name from the asset name. + * + * @param integer|string $assetKey The asset key (asset id or asset name). + * + * @return string The extension name (ex: com_content). + * + * @since 1.6 + */ + public static function getExtensionNameFromAsset($assetKey) + { + static $loaded = array(); + + if (!isset($loaded[$assetKey])) { + $assetName = self::getAssetName($assetKey); + $firstDot = strpos($assetName, '.'); + + if ($assetName !== 'root.1' && $firstDot !== false) { + $assetName = substr($assetName, 0, $firstDot); + } + + $loaded[$assetKey] = $assetName; + } + + return $loaded[$assetKey]; + } + + /** + * Method to get the asset type from the asset name. + * + * For top level components this returns "components": + * 'com_content' returns 'components' + * + * For other types: + * 'com_content.article.1' returns 'com_content.article' + * 'com_content.category.1' returns 'com_content.category' + * + * @param integer|string $assetKey The asset key (asset id or asset name). + * + * @return string The asset type (ex: com_content.article). + * + * @since 1.6 + */ + public static function getAssetType($assetKey) + { + // If the asset is already a string return it. + $assetName = self::getAssetName($assetKey); + $lastDot = strrpos($assetName, '.'); + + if ($assetName !== 'root.1' && $lastDot !== false) { + return substr($assetName, 0, $lastDot); + } + + return 'components'; + } + + /** + * Method to return the title of a user group + * + * @param integer $groupId Id of the group for which to get the title of. + * + * @return string The title of the group + * + * @since 3.5 + */ + public static function getGroupTitle($groupId) + { + // Cast as integer until method is typehinted. + $groupId = (int) $groupId; + + // Fetch the group title from the database + $db = Factory::getDbo(); + $query = $db->getQuery(true); + $query->select($db->quoteName('title')) + ->from($db->quoteName('#__usergroups')) + ->where($db->quoteName('id') . ' = :groupId') + ->bind(':groupId', $groupId, ParameterType::INTEGER); + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Method to return a list of user groups mapped to a user. The returned list can optionally hold + * only the groups explicitly mapped to the user or all groups both explicitly mapped and inherited + * by the user. + * + * @param integer $userId Id of the user for which to get the list of groups. + * @param boolean $recursive True to include inherited user groups. + * + * @return array List of user group ids to which the user is mapped. + * + * @since 1.7.0 + */ + public static function getGroupsByUser($userId, $recursive = true) + { + // Cast as integer until method is typehinted. + $userId = (int) $userId; + + // Creates a simple unique string for each parameter combination: + $storeId = $userId . ':' . (int) $recursive; + + if (!isset(self::$groupsByUser[$storeId])) { + // @todo: Uncouple this from ComponentHelper and allow for a configuration setting or value injection. + $guestUsergroup = (int) ComponentHelper::getParams('com_users')->get('guest_usergroup', 1); + + // Guest user (if only the actually assigned group is requested) + if (empty($userId) && !$recursive) { + $result = array($guestUsergroup); + } else { + // Registered user and guest if all groups are requested + $db = Factory::getDbo(); + + // Build the database query to get the rules for the asset. + $query = $db->getQuery(true) + ->select($db->quoteName($recursive ? 'b.id' : 'a.id')); + + if (empty($userId)) { + $query->from($db->quoteName('#__usergroups', 'a')) + ->where($db->quoteName('a.id') . ' = :guest') + ->bind(':guest', $guestUsergroup, ParameterType::INTEGER); + } else { + $query->from($db->quoteName('#__user_usergroup_map', 'map')) + ->where($db->quoteName('map.user_id') . ' = :userId') + ->join('LEFT', $db->quoteName('#__usergroups', 'a'), $db->quoteName('a.id') . ' = ' . $db->quoteName('map.group_id')) + ->bind(':userId', $userId, ParameterType::INTEGER); + } + + // If we want the rules cascading up to the global asset node we need a self-join. + if ($recursive) { + $query->join( + 'LEFT', + $db->quoteName('#__usergroups', 'b'), + $db->quoteName('b.lft') . ' <= ' . $db->quoteName('a.lft') . ' AND ' . $db->quoteName('b.rgt') . ' >= ' . $db->quoteName('a.rgt') + ); + } + + // Execute the query and load the rules from the result. + $db->setQuery($query); + $result = $db->loadColumn(); + + // Clean up any NULL or duplicate values, just in case + $result = ArrayHelper::toInteger($result); + + if (empty($result)) { + $result = array(1); + } else { + $result = array_unique($result); + } + } + + self::$groupsByUser[$storeId] = $result; + } + + return self::$groupsByUser[$storeId]; + } + + /** + * Method to return a list of user Ids contained in a Group + * + * @param integer $groupId The group Id + * @param boolean $recursive Recursively include all child groups (optional) + * + * @return array + * + * @since 1.7.0 + * @todo This method should move somewhere else + */ + public static function getUsersByGroup($groupId, $recursive = false) + { + // Cast as integer until method is typehinted. + $groupId = (int) $groupId; + + // Get a database object. + $db = Factory::getDbo(); + + $test = $recursive ? ' >= ' : ' = '; + + // First find the users contained in the group + $query = $db->getQuery(true) + ->select('DISTINCT(' . $db->quoteName('user_id') . ')') + ->from($db->quoteName('#__usergroups', 'ug1')) + ->join( + 'INNER', + $db->quoteName('#__usergroups', 'ug2'), + $db->quoteName('ug2.lft') . $test . $db->quoteName('ug1.lft') . ' AND ' . $db->quoteName('ug1.rgt') . $test . $db->quoteName('ug2.rgt') + ) + ->join('INNER', $db->quoteName('#__user_usergroup_map', 'm'), $db->quoteName('ug2.id') . ' = ' . $db->quoteName('m.group_id')) + ->where($db->quoteName('ug1.id') . ' = :groupId') + ->bind(':groupId', $groupId, ParameterType::INTEGER); + + $db->setQuery($query); + + $result = $db->loadColumn(); + + // Clean up any NULL values, just in case + $result = ArrayHelper::toInteger($result); + + return $result; + } + + /** + * Method to return a list of view levels for which the user is authorised. + * + * @param integer $userId Id of the user for which to get the list of authorised view levels. + * + * @return array List of view levels for which the user is authorised. + * + * @since 1.7.0 + */ + public static function getAuthorisedViewLevels($userId) + { + // Only load the view levels once. + if (empty(self::$viewLevels)) { + // Get a database object. + $db = Factory::getDbo(); + + // Build the base query. + $query = $db->getQuery(true) + ->select($db->quoteName(['id', 'rules'])) + ->from($db->quoteName('#__viewlevels')); + + // Set the query for execution. + $db->setQuery($query); + + // Build the view levels array. + foreach ($db->loadAssocList() as $level) { + self::$viewLevels[$level['id']] = (array) json_decode($level['rules']); + } + } + + // Initialise the authorised array. + $authorised = array(1); + + // Check for the recovery mode setting and return early. + $user = User::getInstance($userId); + $root_user = Factory::getApplication()->get('root_user'); + + if (($user->username && $user->username == $root_user) || (is_numeric($root_user) && $user->id > 0 && $user->id == $root_user)) { + // Find the super user levels. + foreach (self::$viewLevels as $level => $rule) { + foreach ($rule as $id) { + if ($id > 0 && self::checkGroup($id, 'core.admin')) { + $authorised[] = $level; + break; + } + } + } + + return array_values(array_unique($authorised)); + } + + // Get all groups that the user is mapped to recursively. + $groups = self::getGroupsByUser($userId); + + // Find the authorised levels. + foreach (self::$viewLevels as $level => $rule) { + foreach ($rule as $id) { + if (($id < 0) && (($id * -1) == $userId)) { + $authorised[] = $level; + break; + } elseif (($id >= 0) && \in_array($id, $groups)) { + // Check to see if the group is mapped to the level. + $authorised[] = $level; + break; + } + } + } + + return array_values(array_unique($authorised)); + } + + /** + * Method to return a list of actions from a file for which permissions can be set. + * + * @param string $file The path to the XML file. + * @param string $xpath An optional xpath to search for the fields. + * + * @return boolean|array False if case of error or the list of actions available. + * + * @since 3.0.0 + */ + public static function getActionsFromFile($file, $xpath = "/access/section[@name='component']/") + { + if (!is_file($file) || !is_readable($file)) { + // If unable to find the file return false. + return false; + } else { + // Else return the actions from the xml. + $xml = simplexml_load_file($file); + + return self::getActionsFromData($xml, $xpath); + } + } + + /** + * Method to return a list of actions from a string or from an xml for which permissions can be set. + * + * @param string|\SimpleXMLElement $data The XML string or an XML element. + * @param string $xpath An optional xpath to search for the fields. + * + * @return boolean|array False if case of error or the list of actions available. + * + * @since 3.0.0 + */ + public static function getActionsFromData($data, $xpath = "/access/section[@name='component']/") + { + // If the data to load isn't already an XML element or string return false. + if ((!($data instanceof \SimpleXMLElement)) && (!\is_string($data))) { + return false; + } + + // Attempt to load the XML if a string. + if (\is_string($data)) { + try { + $data = new \SimpleXMLElement($data); + } catch (\Exception $e) { + return false; + } + + // Make sure the XML loaded correctly. + if (!$data) { + return false; + } + } + + // Initialise the actions array + $actions = array(); + + // Get the elements from the xpath + $elements = $data->xpath($xpath . 'action[@name][@title]'); + + // If there some elements, analyse them + if (!empty($elements)) { + foreach ($elements as $element) { + // Add the action to the actions array + $action = array( + 'name' => (string) $element['name'], + 'title' => (string) $element['title'], + ); + + if (isset($element['description'])) { + $action['description'] = (string) $element['description']; + } + + $actions[] = (object) $action; + } + } + + // Finally return the actions array + return $actions; + } } diff --git a/libraries/src/Access/Exception/AuthenticationFailed.php b/libraries/src/Access/Exception/AuthenticationFailed.php index 5703a129c5ae1..c64a0ba5c44f9 100644 --- a/libraries/src/Access/Exception/AuthenticationFailed.php +++ b/libraries/src/Access/Exception/AuthenticationFailed.php @@ -1,4 +1,5 @@ true, 3 => true, 4 => false) - * or an equivalent JSON encoded string. - * - * @param mixed $identities A JSON format string (probably from the database) or a named array. - * - * @since 1.7.0 - */ - public function __construct($identities) - { - // Convert string input to an array. - if (\is_string($identities)) - { - $identities = json_decode($identities, true); - } - - $this->mergeIdentities($identities); - } - - /** - * Get the data for the action. - * - * @return array A named array - * - * @since 1.7.0 - */ - public function getData() - { - return $this->data; - } - - /** - * Merges the identities - * - * @param mixed $identities An integer or array of integers representing the identities to check. - * - * @return void - * - * @since 1.7.0 - */ - public function mergeIdentities($identities) - { - if ($identities instanceof Rule) - { - $identities = $identities->getData(); - } - - if (\is_array($identities)) - { - foreach ($identities as $identity => $allow) - { - $this->mergeIdentity($identity, $allow); - } - } - } - - /** - * Merges the values for an identity. - * - * @param integer $identity The identity. - * @param boolean $allow The value for the identity (true == allow, false == deny). - * - * @return void - * - * @since 1.7.0 - */ - public function mergeIdentity($identity, $allow) - { - $identity = (int) $identity; - $allow = (int) ((boolean) $allow); - - // Check that the identity exists. - if (isset($this->data[$identity])) - { - // Explicit deny always wins a merge. - if ($this->data[$identity] !== 0) - { - $this->data[$identity] = $allow; - } - } - else - { - $this->data[$identity] = $allow; - } - } - - /** - * Checks that this action can be performed by an identity. - * - * The identity is an integer where +ve represents a user group, - * and -ve represents a user. - * - * @param mixed $identities An integer or array of integers representing the identities to check. - * - * @return mixed True if allowed, false for an explicit deny, null for an implicit deny. - * - * @since 1.7.0 - */ - public function allow($identities) - { - // Implicit deny by default. - $result = null; - - // Check that the inputs are valid. - if (!empty($identities)) - { - if (!\is_array($identities)) - { - $identities = array($identities); - } - - foreach ($identities as $identity) - { - // Technically the identity just needs to be unique. - $identity = (int) $identity; - - // Check if the identity is known. - if (isset($this->data[$identity])) - { - $result = (boolean) $this->data[$identity]; - - // An explicit deny wins. - if ($result === false) - { - break; - } - } - } - } - - return $result; - } - - /** - * Convert this object into a JSON encoded string. - * - * @return string JSON encoded string - * - * @since 1.7.0 - */ - public function __toString() - { - return json_encode($this->data); - } + /** + * A named array + * + * @var array + * @since 1.7.0 + */ + protected $data = array(); + + /** + * Constructor. + * + * The input array must be in the form: array(-42 => true, 3 => true, 4 => false) + * or an equivalent JSON encoded string. + * + * @param mixed $identities A JSON format string (probably from the database) or a named array. + * + * @since 1.7.0 + */ + public function __construct($identities) + { + // Convert string input to an array. + if (\is_string($identities)) { + $identities = json_decode($identities, true); + } + + $this->mergeIdentities($identities); + } + + /** + * Get the data for the action. + * + * @return array A named array + * + * @since 1.7.0 + */ + public function getData() + { + return $this->data; + } + + /** + * Merges the identities + * + * @param mixed $identities An integer or array of integers representing the identities to check. + * + * @return void + * + * @since 1.7.0 + */ + public function mergeIdentities($identities) + { + if ($identities instanceof Rule) { + $identities = $identities->getData(); + } + + if (\is_array($identities)) { + foreach ($identities as $identity => $allow) { + $this->mergeIdentity($identity, $allow); + } + } + } + + /** + * Merges the values for an identity. + * + * @param integer $identity The identity. + * @param boolean $allow The value for the identity (true == allow, false == deny). + * + * @return void + * + * @since 1.7.0 + */ + public function mergeIdentity($identity, $allow) + { + $identity = (int) $identity; + $allow = (int) ((bool) $allow); + + // Check that the identity exists. + if (isset($this->data[$identity])) { + // Explicit deny always wins a merge. + if ($this->data[$identity] !== 0) { + $this->data[$identity] = $allow; + } + } else { + $this->data[$identity] = $allow; + } + } + + /** + * Checks that this action can be performed by an identity. + * + * The identity is an integer where +ve represents a user group, + * and -ve represents a user. + * + * @param mixed $identities An integer or array of integers representing the identities to check. + * + * @return mixed True if allowed, false for an explicit deny, null for an implicit deny. + * + * @since 1.7.0 + */ + public function allow($identities) + { + // Implicit deny by default. + $result = null; + + // Check that the inputs are valid. + if (!empty($identities)) { + if (!\is_array($identities)) { + $identities = array($identities); + } + + foreach ($identities as $identity) { + // Technically the identity just needs to be unique. + $identity = (int) $identity; + + // Check if the identity is known. + if (isset($this->data[$identity])) { + $result = (bool) $this->data[$identity]; + + // An explicit deny wins. + if ($result === false) { + break; + } + } + } + } + + return $result; + } + + /** + * Convert this object into a JSON encoded string. + * + * @return string JSON encoded string + * + * @since 1.7.0 + */ + public function __toString() + { + return json_encode($this->data); + } } diff --git a/libraries/src/Access/Rules.php b/libraries/src/Access/Rules.php index 3f605ca6e1531..a831443032e9f 100644 --- a/libraries/src/Access/Rules.php +++ b/libraries/src/Access/Rules.php @@ -1,4 +1,5 @@ array(-42 => true, 3 => true, 4 => false)) - * or an equivalent JSON encoded string, or an object where properties are arrays. - * - * @param mixed $input A JSON format string (probably from the database) or a nested array. - * - * @since 1.7.0 - */ - public function __construct($input = '') - { - // Convert in input to an array. - if (\is_string($input)) - { - $input = json_decode($input, true); - } - elseif (\is_object($input)) - { - $input = (array) $input; - } - - if (\is_array($input)) - { - // Top level keys represent the actions. - foreach ($input as $action => $identities) - { - $this->mergeAction($action, $identities); - } - } - } - - /** - * Get the data for the action. - * - * @return array A named array of Rule objects. - * - * @since 1.7.0 - */ - public function getData() - { - return $this->data; - } - - /** - * Method to merge a collection of Rules. - * - * @param mixed $input Rule or array of Rules - * - * @return void - * - * @since 1.7.0 - */ - public function mergeCollection($input) - { - // Check if the input is an array. - if (\is_array($input)) - { - foreach ($input as $actions) - { - $this->merge($actions); - } - } - } - - /** - * Method to merge actions with this object. - * - * @param mixed $actions Rule object, an array of actions or a JSON string array of actions. - * - * @return void - * - * @since 1.7.0 - */ - public function merge($actions) - { - if (\is_string($actions)) - { - $actions = json_decode($actions, true); - } - - if (\is_array($actions)) - { - foreach ($actions as $action => $identities) - { - $this->mergeAction($action, $identities); - } - } - elseif ($actions instanceof Rules) - { - $data = $actions->getData(); - - foreach ($data as $name => $identities) - { - $this->mergeAction($name, $identities); - } - } - } - - /** - * Merges an array of identities for an action. - * - * @param string $action The name of the action. - * @param array $identities An array of identities - * - * @return void - * - * @since 1.7.0 - */ - public function mergeAction($action, $identities) - { - if (isset($this->data[$action])) - { - // If exists, merge the action. - $this->data[$action]->mergeIdentities($identities); - } - else - { - // If new, add the action. - $this->data[$action] = new Rule($identities); - } - } - - /** - * Checks that an action can be performed by an identity. - * - * The identity is an integer where +ve represents a user group, - * and -ve represents a user. - * - * @param string $action The name of the action. - * @param mixed $identity An integer representing the identity, or an array of identities - * - * @return mixed Object or null if there is no information about the action. - * - * @since 1.7.0 - */ - public function allow($action, $identity) - { - // Check we have information about this action. - if (isset($this->data[$action])) - { - return $this->data[$action]->allow($identity); - } - } - - /** - * Get the allowed actions for an identity. - * - * @param mixed $identity An integer representing the identity or an array of identities - * - * @return CMSObject Allowed actions for the identity or identities - * - * @since 1.7.0 - */ - public function getAllowed($identity) - { - // Sweep for the allowed actions. - $allowed = new CMSObject; - - foreach ($this->data as $name => &$action) - { - if ($action->allow($identity)) - { - $allowed->set($name, true); - } - } - - return $allowed; - } - - /** - * Magic method to convert the object to JSON string representation. - * - * @return string JSON representation of the actions array - * - * @since 1.7.0 - */ - public function __toString() - { - $temp = array(); - - foreach ($this->data as $name => $rule) - { - if ($data = $rule->getData()) - { - $temp[$name] = $data; - } - } - - return json_encode($temp, JSON_FORCE_OBJECT); - } + /** + * A named array. + * + * @var array + * @since 1.7.0 + */ + protected $data = array(); + + /** + * Constructor. + * + * The input array must be in the form: array('action' => array(-42 => true, 3 => true, 4 => false)) + * or an equivalent JSON encoded string, or an object where properties are arrays. + * + * @param mixed $input A JSON format string (probably from the database) or a nested array. + * + * @since 1.7.0 + */ + public function __construct($input = '') + { + // Convert in input to an array. + if (\is_string($input)) { + $input = json_decode($input, true); + } elseif (\is_object($input)) { + $input = (array) $input; + } + + if (\is_array($input)) { + // Top level keys represent the actions. + foreach ($input as $action => $identities) { + $this->mergeAction($action, $identities); + } + } + } + + /** + * Get the data for the action. + * + * @return array A named array of Rule objects. + * + * @since 1.7.0 + */ + public function getData() + { + return $this->data; + } + + /** + * Method to merge a collection of Rules. + * + * @param mixed $input Rule or array of Rules + * + * @return void + * + * @since 1.7.0 + */ + public function mergeCollection($input) + { + // Check if the input is an array. + if (\is_array($input)) { + foreach ($input as $actions) { + $this->merge($actions); + } + } + } + + /** + * Method to merge actions with this object. + * + * @param mixed $actions Rule object, an array of actions or a JSON string array of actions. + * + * @return void + * + * @since 1.7.0 + */ + public function merge($actions) + { + if (\is_string($actions)) { + $actions = json_decode($actions, true); + } + + if (\is_array($actions)) { + foreach ($actions as $action => $identities) { + $this->mergeAction($action, $identities); + } + } elseif ($actions instanceof Rules) { + $data = $actions->getData(); + + foreach ($data as $name => $identities) { + $this->mergeAction($name, $identities); + } + } + } + + /** + * Merges an array of identities for an action. + * + * @param string $action The name of the action. + * @param array $identities An array of identities + * + * @return void + * + * @since 1.7.0 + */ + public function mergeAction($action, $identities) + { + if (isset($this->data[$action])) { + // If exists, merge the action. + $this->data[$action]->mergeIdentities($identities); + } else { + // If new, add the action. + $this->data[$action] = new Rule($identities); + } + } + + /** + * Checks that an action can be performed by an identity. + * + * The identity is an integer where +ve represents a user group, + * and -ve represents a user. + * + * @param string $action The name of the action. + * @param mixed $identity An integer representing the identity, or an array of identities + * + * @return mixed Object or null if there is no information about the action. + * + * @since 1.7.0 + */ + public function allow($action, $identity) + { + // Check we have information about this action. + if (isset($this->data[$action])) { + return $this->data[$action]->allow($identity); + } + } + + /** + * Get the allowed actions for an identity. + * + * @param mixed $identity An integer representing the identity or an array of identities + * + * @return CMSObject Allowed actions for the identity or identities + * + * @since 1.7.0 + */ + public function getAllowed($identity) + { + // Sweep for the allowed actions. + $allowed = new CMSObject(); + + foreach ($this->data as $name => &$action) { + if ($action->allow($identity)) { + $allowed->set($name, true); + } + } + + return $allowed; + } + + /** + * Magic method to convert the object to JSON string representation. + * + * @return string JSON representation of the actions array + * + * @since 1.7.0 + */ + public function __toString() + { + $temp = array(); + + foreach ($this->data as $name => $rule) { + if ($data = $rule->getData()) { + $temp[$name] = $data; + } + } + + return json_encode($temp, JSON_FORCE_OBJECT); + } } diff --git a/libraries/src/Adapter/Adapter.php b/libraries/src/Adapter/Adapter.php index e2161c130f401..b4a6727546eb2 100644 --- a/libraries/src/Adapter/Adapter.php +++ b/libraries/src/Adapter/Adapter.php @@ -1,4 +1,5 @@ _basepath = $basepath; - $this->_classprefix = $classprefix ?: 'J'; - $this->_adapterfolder = $adapterfolder ?: 'adapters'; - - $this->_db = Factory::getDbo(); - - // Ensure BC, when removed in 5, then the db must be set with setDatabase explicitly - if ($this instanceof DatabaseAwareInterface) - { - $this->setDatabase($this->_db); - } - } - - /** - * Get the database connector object - * - * @return \Joomla\Database\DatabaseDriver Database connector object - * - * @since 1.6 - */ - public function getDbo() - { - return $this->_db; - } - - /** - * Return an adapter. - * - * @param string $name Name of adapter to return - * @param array $options Adapter options - * - * @return static|boolean Adapter of type 'name' or false - * - * @since 1.6 - */ - public function getAdapter($name, $options = array()) - { - if (array_key_exists($name, $this->_adapters)) - { - return $this->_adapters[$name]; - } - - if ($this->setAdapter($name, $options)) - { - return $this->_adapters[$name]; - } - - return false; - } - - /** - * Set an adapter by name - * - * @param string $name Adapter name - * @param object $adapter Adapter object - * @param array $options Adapter options - * - * @return boolean True if successful - * - * @since 1.6 - */ - public function setAdapter($name, &$adapter = null, $options = array()) - { - if (is_object($adapter)) - { - $this->_adapters[$name] = &$adapter; - - return true; - } - - $class = rtrim($this->_classprefix, '\\') . '\\' . ucfirst($name); - - if (class_exists($class)) - { - $this->_adapters[$name] = new $class($this, $this->_db, $options); - - return true; - } - - $class = rtrim($this->_classprefix, '\\') . '\\' . ucfirst($name) . 'Adapter'; - - if (class_exists($class)) - { - $this->_adapters[$name] = new $class($this, $this->_db, $options); - - return true; - } - - $fullpath = $this->_basepath . '/' . $this->_adapterfolder . '/' . strtolower($name) . '.php'; - - if (!is_file($fullpath)) - { - return false; - } - - // Try to load the adapter object - $class = $this->_classprefix . ucfirst($name); - - \JLoader::register($class, $fullpath); - - if (!class_exists($class)) - { - return false; - } - - $this->_adapters[$name] = new $class($this, $this->_db, $options); - - return true; - } - - /** - * Loads all adapters. - * - * @param array $options Adapter options - * - * @return void - * - * @since 1.6 - */ - public function loadAllAdapters($options = array()) - { - $files = new \DirectoryIterator($this->_basepath . '/' . $this->_adapterfolder); - - /** @type $file \DirectoryIterator */ - foreach ($files as $file) - { - $fileName = $file->getFilename(); - - // Only load for php files. - if (!$file->isFile() || $file->getExtension() != 'php') - { - continue; - } - - // Try to load the adapter object - require_once $this->_basepath . '/' . $this->_adapterfolder . '/' . $fileName; - - // Derive the class name from the filename. - $name = str_ireplace('.php', '', ucfirst(trim($fileName))); - $class = $this->_classprefix . ucfirst($name); - - if (!class_exists($class)) - { - // Skip to next one - continue; - } - - $adapter = new $class($this, $this->_db, $options); - $this->_adapters[$name] = clone $adapter; - } - } + /** + * Associative array of adapters + * + * @var static[] + * @since 1.6 + */ + protected $_adapters = array(); + + /** + * Adapter Folder + * + * @var string + * @since 1.6 + */ + protected $_adapterfolder = 'adapters'; + + /** + * Adapter Class Prefix + * + * @var string + * @since 1.6 + */ + protected $_classprefix = 'J'; + + /** + * Base Path for the adapter instance + * + * @var string + * @since 1.6 + */ + protected $_basepath = null; + + /** + * Database Connector Object + * + * @var \Joomla\Database\DatabaseDriver + * @since 1.6 + */ + protected $_db; + + /** + * Constructor + * + * @param string $basepath Base Path of the adapters + * @param string $classprefix Class prefix of adapters + * @param string $adapterfolder Name of folder to append to base path + * + * @since 1.6 + */ + public function __construct($basepath, $classprefix = null, $adapterfolder = null) + { + $this->_basepath = $basepath; + $this->_classprefix = $classprefix ?: 'J'; + $this->_adapterfolder = $adapterfolder ?: 'adapters'; + + $this->_db = Factory::getDbo(); + + // Ensure BC, when removed in 5, then the db must be set with setDatabase explicitly + if ($this instanceof DatabaseAwareInterface) { + $this->setDatabase($this->_db); + } + } + + /** + * Get the database connector object + * + * @return \Joomla\Database\DatabaseDriver Database connector object + * + * @since 1.6 + */ + public function getDbo() + { + return $this->_db; + } + + /** + * Return an adapter. + * + * @param string $name Name of adapter to return + * @param array $options Adapter options + * + * @return static|boolean Adapter of type 'name' or false + * + * @since 1.6 + */ + public function getAdapter($name, $options = array()) + { + if (array_key_exists($name, $this->_adapters)) { + return $this->_adapters[$name]; + } + + if ($this->setAdapter($name, $options)) { + return $this->_adapters[$name]; + } + + return false; + } + + /** + * Set an adapter by name + * + * @param string $name Adapter name + * @param object $adapter Adapter object + * @param array $options Adapter options + * + * @return boolean True if successful + * + * @since 1.6 + */ + public function setAdapter($name, &$adapter = null, $options = array()) + { + if (is_object($adapter)) { + $this->_adapters[$name] = &$adapter; + + return true; + } + + $class = rtrim($this->_classprefix, '\\') . '\\' . ucfirst($name); + + if (class_exists($class)) { + $this->_adapters[$name] = new $class($this, $this->_db, $options); + + return true; + } + + $class = rtrim($this->_classprefix, '\\') . '\\' . ucfirst($name) . 'Adapter'; + + if (class_exists($class)) { + $this->_adapters[$name] = new $class($this, $this->_db, $options); + + return true; + } + + $fullpath = $this->_basepath . '/' . $this->_adapterfolder . '/' . strtolower($name) . '.php'; + + if (!is_file($fullpath)) { + return false; + } + + // Try to load the adapter object + $class = $this->_classprefix . ucfirst($name); + + \JLoader::register($class, $fullpath); + + if (!class_exists($class)) { + return false; + } + + $this->_adapters[$name] = new $class($this, $this->_db, $options); + + return true; + } + + /** + * Loads all adapters. + * + * @param array $options Adapter options + * + * @return void + * + * @since 1.6 + */ + public function loadAllAdapters($options = array()) + { + $files = new \DirectoryIterator($this->_basepath . '/' . $this->_adapterfolder); + + /** @type $file \DirectoryIterator */ + foreach ($files as $file) { + $fileName = $file->getFilename(); + + // Only load for php files. + if (!$file->isFile() || $file->getExtension() != 'php') { + continue; + } + + // Try to load the adapter object + require_once $this->_basepath . '/' . $this->_adapterfolder . '/' . $fileName; + + // Derive the class name from the filename. + $name = str_ireplace('.php', '', ucfirst(trim($fileName))); + $class = $this->_classprefix . ucfirst($name); + + if (!class_exists($class)) { + // Skip to next one + continue; + } + + $adapter = new $class($this, $this->_db, $options); + $this->_adapters[$name] = clone $adapter; + } + } } diff --git a/libraries/src/Adapter/AdapterInstance.php b/libraries/src/Adapter/AdapterInstance.php index 297ba584a09b5..d4cc20742140e 100644 --- a/libraries/src/Adapter/AdapterInstance.php +++ b/libraries/src/Adapter/AdapterInstance.php @@ -1,4 +1,5 @@ setProperties($options); + /** + * Constructor + * + * @param Adapter $parent Parent object + * @param DatabaseDriver $db Database object + * @param array $options Configuration Options + * + * @since 1.6 + */ + public function __construct(Adapter $parent, DatabaseDriver $db, array $options = array()) + { + // Set the properties from the options array that is passed in + $this->setProperties($options); - // Set the parent and db in case $options for some reason overrides it. - $this->parent = $parent; + // Set the parent and db in case $options for some reason overrides it. + $this->parent = $parent; - // Pull in the global dbo in case something happened to it. - $this->db = $db ?: Factory::getDbo(); - } + // Pull in the global dbo in case something happened to it. + $this->db = $db ?: Factory::getDbo(); + } - /** - * Retrieves the parent object - * - * @return Adapter - * - * @since 1.6 - */ - public function getParent() - { - return $this->parent; - } + /** + * Retrieves the parent object + * + * @return Adapter + * + * @since 1.6 + */ + public function getParent() + { + return $this->parent; + } } diff --git a/libraries/src/Application/AdministratorApplication.php b/libraries/src/Application/AdministratorApplication.php index bd6287f27703e..3cc2d46e964ef 100644 --- a/libraries/src/Application/AdministratorApplication.php +++ b/libraries/src/Application/AdministratorApplication.php @@ -1,4 +1,5 @@ name = 'administrator'; - - // Register the client ID - $this->clientId = 1; - - // Execute the parent constructor - parent::__construct($input, $config, $client, $container); - - // Set the root in the URI based on the application name - Uri::root(null, rtrim(\dirname(Uri::base(true)), '/\\')); - } - - /** - * Dispatch the application - * - * @param string $component The component which is being rendered. - * - * @return void - * - * @since 3.2 - */ - public function dispatch($component = null) - { - if ($component === null) - { - $component = $this->findOption(); - } - - // Load the document to the API - $this->loadDocument(); - - // Set up the params - $document = Factory::getDocument(); - - // Register the document object with Factory - Factory::$document = $document; - - switch ($document->getType()) - { - case 'html': - // Get the template - $template = $this->getTemplate(true); - $clientId = $this->getClientId(); - - // Store the template and its params to the config - $this->set('theme', $template->template); - $this->set('themeParams', $template->params); - - // Add Asset registry files - $wr = $document->getWebAssetManager()->getRegistry(); - - if ($component) - { - $wr->addExtensionRegistryFile($component); - } - - if (!empty($template->parent)) - { - $wr->addTemplateRegistryFile($template->parent, $clientId); - } - - $wr->addTemplateRegistryFile($template->template, $clientId); - - break; - - default: - break; - } - - $document->setTitle($this->get('sitename') . ' - ' . Text::_('JADMINISTRATION')); - $document->setDescription($this->get('MetaDesc')); - $document->setGenerator('Joomla! - Open Source Content Management'); - - $contents = ComponentHelper::renderComponent($component); - $document->setBuffer($contents, 'component'); - - // Trigger the onAfterDispatch event. - PluginHelper::importPlugin('system'); - $this->triggerEvent('onAfterDispatch'); - } - - /** - * Method to run the Web application routines. - * - * @return void - * - * @since 3.2 - */ - protected function doExecute() - { - // Get the language from the (login) form or user state - $login_lang = ($this->input->get('option') === 'com_login') ? $this->input->get('lang') : ''; - $options = array('language' => $login_lang ?: $this->getUserState('application.lang')); - - // Initialise the application - $this->initialiseApp($options); - - // Mark afterInitialise in the profiler. - JDEBUG ? $this->profiler->mark('afterInitialise') : null; - - // Route the application - $this->route(); - - // Mark afterRoute in the profiler. - JDEBUG ? $this->profiler->mark('afterRoute') : null; - - /* - * Check if the user is required to reset their password - * - * Before $this->route(); "option" and "view" can't be safely read using: - * $this->input->getCmd('option'); or $this->input->getCmd('view'); - * ex: due of the sef urls - */ - $this->checkUserRequireReset('com_users', 'user', 'edit', 'com_users/user.edit,com_users/user.save,com_users/user.apply,com_login/logout'); - - // Dispatch the application - $this->dispatch(); - - // Mark afterDispatch in the profiler. - JDEBUG ? $this->profiler->mark('afterDispatch') : null; - } - - /** - * Return a reference to the Router object. - * - * @param string $name The name of the application. - * @param array $options An optional associative array of configuration settings. - * - * @return Router - * - * @since 3.2 - * @deprecated 5.0 Inject the router or load it from the dependency injection container - */ - public static function getRouter($name = 'administrator', array $options = array()) - { - return parent::getRouter($name, $options); - } - - /** - * Gets the name of the current template. - * - * @param boolean $params True to return the template parameters - * - * @return string The name of the template. - * - * @since 3.2 - * @throws \InvalidArgumentException - */ - public function getTemplate($params = false) - { - if (\is_object($this->template)) - { - if ($params) - { - return $this->template; - } - - return $this->template->template; - } - - $adminStyle = $this->getIdentity() ? (int) $this->getIdentity()->getParam('admin_style') : 0; - $template = $this->bootComponent('templates')->getMVCFactory() - ->createModel('Style', 'Administrator')->getAdminTemplate($adminStyle); - - $template->template = InputFilter::getInstance()->clean($template->template, 'cmd'); - $template->params = new Registry($template->params); - - // Fallback template - if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php') - && !is_file(JPATH_THEMES . '/' . $template->parent . '/index.php')) - { - $this->getLogger()->error(Text::_('JERROR_ALERTNOTEMPLATE'), ['category' => 'system']); - $template->params = new Registry; - $template->template = 'atum'; - - // Check, the data were found and if template really exists - if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) - { - throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $template->template)); - } - } - - // Cache the result - $this->template = $template; - - // Pass the parent template to the state - $this->set('themeInherits', $template->parent); - - if ($params) - { - return $template; - } - - return $template->template; - } - - /** - * Initialise the application. - * - * @param array $options An optional associative array of configuration settings. - * - * @return void - * - * @since 3.2 - */ - protected function initialiseApp($options = array()) - { - $user = Factory::getUser(); - - // If the user is a guest we populate it with the guest user group. - if ($user->guest) - { - $guestUsergroup = ComponentHelper::getParams('com_users')->get('guest_usergroup', 1); - $user->groups = array($guestUsergroup); - } - - // If a language was specified it has priority, otherwise use user or default language settings - if (empty($options['language'])) - { - $lang = $user->getParam('admin_language'); - - // Make sure that the user's language exists - if ($lang && LanguageHelper::exists($lang)) - { - $options['language'] = $lang; - } - else - { - $params = ComponentHelper::getParams('com_languages'); - $options['language'] = $params->get('administrator', $this->get('language', 'en-GB')); - } - } - - // One last check to make sure we have something - if (!LanguageHelper::exists($options['language'])) - { - $lang = $this->get('language', 'en-GB'); - - if (LanguageHelper::exists($lang)) - { - $options['language'] = $lang; - } - else - { - // As a last ditch fail to english - $options['language'] = 'en-GB'; - } - } - - // Finish initialisation - parent::initialiseApp($options); - } - - /** - * Login authentication function - * - * @param array $credentials Array('username' => string, 'password' => string) - * @param array $options Array('remember' => boolean) - * - * @return boolean True on success. - * - * @since 3.2 - */ - public function login($credentials, $options = array()) - { - // The minimum group - $options['group'] = 'Public Backend'; - - // Make sure users are not auto-registered - $options['autoregister'] = false; - - // Set the application login entry point - if (!\array_key_exists('entry_url', $options)) - { - $options['entry_url'] = Uri::base() . 'index.php?option=com_users&task=login'; - } - - // Set the access control action to check. - $options['action'] = 'core.login.admin'; - - $result = parent::login($credentials, $options); - - if (!($result instanceof \Exception)) - { - $lang = $this->input->getCmd('lang', ''); - $lang = preg_replace('/[^A-Z-]/i', '', $lang); - - if ($lang) - { - $this->setUserState('application.lang', $lang); - } - - $this->bootComponent('messages')->getMVCFactory() - ->createModel('Messages', 'Administrator')->purge($this->getIdentity() ? $this->getIdentity()->id : 0); - } - - return $result; - } - - /** - * Purge the jos_messages table of old messages - * - * @return void - * - * @since 3.2 - * - * @deprecated 5.0 Purge the messages through the model - */ - public static function purgeMessages() - { - Factory::getApplication()->bootComponent('messages')->getMVCFactory() - ->createModel('Messages', 'Administrator')->purge(Factory::getUser()->id); - } - - /** - * Rendering is the process of pushing the document buffers into the template - * placeholders, retrieving data from the document and pushing it into - * the application response buffer. - * - * @return void - * - * @since 3.2 - */ - protected function render() - { - // Get the \JInput object - $input = $this->input; - - $component = $input->getCmd('option', 'com_login'); - $file = $input->getCmd('tmpl', 'index'); - - if ($component === 'com_login') - { - $file = 'login'; - } - - $this->set('themeFile', $file . '.php'); - - // Safety check for when configuration.php root_user is in use. - $rootUser = $this->get('root_user'); - - if (property_exists('\JConfig', 'root_user')) - { - if (Factory::getUser()->get('username') === $rootUser || Factory::getUser()->id === (string) $rootUser) - { - $this->enqueueMessage( - Text::sprintf( - 'JWARNING_REMOVE_ROOT_USER', - 'index.php?option=com_config&task=application.removeroot&' . Session::getFormToken() . '=1' - ), - 'warning' - ); - } - // Show this message to superusers too - elseif (Factory::getUser()->authorise('core.admin')) - { - $this->enqueueMessage( - Text::sprintf( - 'JWARNING_REMOVE_ROOT_USER_ADMIN', - $rootUser, - 'index.php?option=com_config&task=application.removeroot&' . Session::getFormToken() . '=1' - ), - 'warning' - ); - } - } - - parent::render(); - } - - /** - * Route the application. - * - * Routing is the process of examining the request environment to determine which - * component should receive the request. The component optional parameters - * are then set in the request object to be processed when the application is being - * dispatched. - * - * @return void - * - * @since 3.2 - */ - protected function route() - { - $uri = Uri::getInstance(); - - if ($this->get('force_ssl') >= 1 && strtolower($uri->getScheme()) !== 'https') - { - // Forward to https - $uri->setScheme('https'); - $this->redirect((string) $uri, 301); - } - - $this->isHandlingMultiFactorAuthentication(); - - // Trigger the onAfterRoute event. - PluginHelper::importPlugin('system'); - $this->triggerEvent('onAfterRoute'); - } - - /** - * Return the application option string [main component]. - * - * @return string The component to access. - * - * @since 4.0.0 - */ - public function findOption(): string - { - /** @var self $app */ - $app = Factory::getApplication(); - $option = strtolower($app->input->get('option', '')); - $user = $app->getIdentity(); - - /** - * Special handling for guest users and authenticated users without the Backend Login privilege. - * - * If the component they are trying to access is in the $this->allowedUnprivilegedOptions array we allow the - * request to go through. Otherwise we force com_login to be loaded, letting the user (re)try authenticating - * with a user account that has the Backend Login privilege. - */ - if ($user->get('guest') || !$user->authorise('core.login.admin')) - { - $option = in_array($option, $this->allowedUnprivilegedOptions) ? $option : 'com_login'; - } - - /** - * If no component is defined in the request we will try to load com_cpanel, the administrator Control Panel - * component. This allows the /administrator URL to display something meaningful after logging in instead of an - * error. - */ - if (empty($option)) - { - $option = 'com_cpanel'; - } - - /** - * Force the option to the input object. This is necessary because we might have force-changed the component in - * the two if-blocks above. - */ - $app->input->set('option', $option); - - return $option; - } + use MultiFactorAuthenticationHandler; + + /** + * List of allowed components for guests and users which do not have the core.login.admin privilege. + * + * By default we allow two core components: + * + * - com_login Absolutely necessary to let users log into the backend of the site. Do NOT remove! + * - com_ajax Handle AJAX requests or other administrative callbacks without logging in. Required for + * passwordless authentication using WebAuthn. + * + * @var array + */ + protected $allowedUnprivilegedOptions = [ + 'com_login', + 'com_ajax', + ]; + + /** + * Class constructor. + * + * @param Input $input An optional argument to provide dependency injection for the application's input + * object. If the argument is a JInput object that object will become the + * application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's config + * object. If the argument is a Registry object that object will become the + * application's config object, otherwise a default config object is created. + * @param WebClient $client An optional argument to provide dependency injection for the application's + * client object. If the argument is a WebClient object that object will become the + * application's client object, otherwise a default client object is created. + * @param Container $container Dependency injection container. + * + * @since 3.2 + */ + public function __construct(Input $input = null, Registry $config = null, WebClient $client = null, Container $container = null) + { + // Register the application name + $this->name = 'administrator'; + + // Register the client ID + $this->clientId = 1; + + // Execute the parent constructor + parent::__construct($input, $config, $client, $container); + + // Set the root in the URI based on the application name + Uri::root(null, rtrim(\dirname(Uri::base(true)), '/\\')); + } + + /** + * Dispatch the application + * + * @param string $component The component which is being rendered. + * + * @return void + * + * @since 3.2 + */ + public function dispatch($component = null) + { + if ($component === null) { + $component = $this->findOption(); + } + + // Load the document to the API + $this->loadDocument(); + + // Set up the params + $document = Factory::getDocument(); + + // Register the document object with Factory + Factory::$document = $document; + + switch ($document->getType()) { + case 'html': + // Get the template + $template = $this->getTemplate(true); + $clientId = $this->getClientId(); + + // Store the template and its params to the config + $this->set('theme', $template->template); + $this->set('themeParams', $template->params); + + // Add Asset registry files + $wr = $document->getWebAssetManager()->getRegistry(); + + if ($component) { + $wr->addExtensionRegistryFile($component); + } + + if (!empty($template->parent)) { + $wr->addTemplateRegistryFile($template->parent, $clientId); + } + + $wr->addTemplateRegistryFile($template->template, $clientId); + + break; + + default: + break; + } + + $document->setTitle($this->get('sitename') . ' - ' . Text::_('JADMINISTRATION')); + $document->setDescription($this->get('MetaDesc')); + $document->setGenerator('Joomla! - Open Source Content Management'); + + $contents = ComponentHelper::renderComponent($component); + $document->setBuffer($contents, 'component'); + + // Trigger the onAfterDispatch event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onAfterDispatch'); + } + + /** + * Method to run the Web application routines. + * + * @return void + * + * @since 3.2 + */ + protected function doExecute() + { + // Get the language from the (login) form or user state + $login_lang = ($this->input->get('option') === 'com_login') ? $this->input->get('lang') : ''; + $options = array('language' => $login_lang ?: $this->getUserState('application.lang')); + + // Initialise the application + $this->initialiseApp($options); + + // Mark afterInitialise in the profiler. + JDEBUG ? $this->profiler->mark('afterInitialise') : null; + + // Route the application + $this->route(); + + // Mark afterRoute in the profiler. + JDEBUG ? $this->profiler->mark('afterRoute') : null; + + /* + * Check if the user is required to reset their password + * + * Before $this->route(); "option" and "view" can't be safely read using: + * $this->input->getCmd('option'); or $this->input->getCmd('view'); + * ex: due of the sef urls + */ + $this->checkUserRequireReset('com_users', 'user', 'edit', 'com_users/user.edit,com_users/user.save,com_users/user.apply,com_login/logout'); + + // Dispatch the application + $this->dispatch(); + + // Mark afterDispatch in the profiler. + JDEBUG ? $this->profiler->mark('afterDispatch') : null; + } + + /** + * Return a reference to the Router object. + * + * @param string $name The name of the application. + * @param array $options An optional associative array of configuration settings. + * + * @return Router + * + * @since 3.2 + * @deprecated 5.0 Inject the router or load it from the dependency injection container + */ + public static function getRouter($name = 'administrator', array $options = array()) + { + return parent::getRouter($name, $options); + } + + /** + * Gets the name of the current template. + * + * @param boolean $params True to return the template parameters + * + * @return string The name of the template. + * + * @since 3.2 + * @throws \InvalidArgumentException + */ + public function getTemplate($params = false) + { + if (\is_object($this->template)) { + if ($params) { + return $this->template; + } + + return $this->template->template; + } + + $adminStyle = $this->getIdentity() ? (int) $this->getIdentity()->getParam('admin_style') : 0; + $template = $this->bootComponent('templates')->getMVCFactory() + ->createModel('Style', 'Administrator')->getAdminTemplate($adminStyle); + + $template->template = InputFilter::getInstance()->clean($template->template, 'cmd'); + $template->params = new Registry($template->params); + + // Fallback template + if ( + !is_file(JPATH_THEMES . '/' . $template->template . '/index.php') + && !is_file(JPATH_THEMES . '/' . $template->parent . '/index.php') + ) { + $this->getLogger()->error(Text::_('JERROR_ALERTNOTEMPLATE'), ['category' => 'system']); + $template->params = new Registry(); + $template->template = 'atum'; + + // Check, the data were found and if template really exists + if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) { + throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $template->template)); + } + } + + // Cache the result + $this->template = $template; + + // Pass the parent template to the state + $this->set('themeInherits', $template->parent); + + if ($params) { + return $template; + } + + return $template->template; + } + + /** + * Initialise the application. + * + * @param array $options An optional associative array of configuration settings. + * + * @return void + * + * @since 3.2 + */ + protected function initialiseApp($options = array()) + { + $user = Factory::getUser(); + + // If the user is a guest we populate it with the guest user group. + if ($user->guest) { + $guestUsergroup = ComponentHelper::getParams('com_users')->get('guest_usergroup', 1); + $user->groups = array($guestUsergroup); + } + + // If a language was specified it has priority, otherwise use user or default language settings + if (empty($options['language'])) { + $lang = $user->getParam('admin_language'); + + // Make sure that the user's language exists + if ($lang && LanguageHelper::exists($lang)) { + $options['language'] = $lang; + } else { + $params = ComponentHelper::getParams('com_languages'); + $options['language'] = $params->get('administrator', $this->get('language', 'en-GB')); + } + } + + // One last check to make sure we have something + if (!LanguageHelper::exists($options['language'])) { + $lang = $this->get('language', 'en-GB'); + + if (LanguageHelper::exists($lang)) { + $options['language'] = $lang; + } else { + // As a last ditch fail to english + $options['language'] = 'en-GB'; + } + } + + // Finish initialisation + parent::initialiseApp($options); + } + + /** + * Login authentication function + * + * @param array $credentials Array('username' => string, 'password' => string) + * @param array $options Array('remember' => boolean) + * + * @return boolean True on success. + * + * @since 3.2 + */ + public function login($credentials, $options = array()) + { + // The minimum group + $options['group'] = 'Public Backend'; + + // Make sure users are not auto-registered + $options['autoregister'] = false; + + // Set the application login entry point + if (!\array_key_exists('entry_url', $options)) { + $options['entry_url'] = Uri::base() . 'index.php?option=com_users&task=login'; + } + + // Set the access control action to check. + $options['action'] = 'core.login.admin'; + + $result = parent::login($credentials, $options); + + if (!($result instanceof \Exception)) { + $lang = $this->input->getCmd('lang', ''); + $lang = preg_replace('/[^A-Z-]/i', '', $lang); + + if ($lang) { + $this->setUserState('application.lang', $lang); + } + + $this->bootComponent('messages')->getMVCFactory() + ->createModel('Messages', 'Administrator')->purge($this->getIdentity() ? $this->getIdentity()->id : 0); + } + + return $result; + } + + /** + * Purge the jos_messages table of old messages + * + * @return void + * + * @since 3.2 + * + * @deprecated 5.0 Purge the messages through the model + */ + public static function purgeMessages() + { + Factory::getApplication()->bootComponent('messages')->getMVCFactory() + ->createModel('Messages', 'Administrator')->purge(Factory::getUser()->id); + } + + /** + * Rendering is the process of pushing the document buffers into the template + * placeholders, retrieving data from the document and pushing it into + * the application response buffer. + * + * @return void + * + * @since 3.2 + */ + protected function render() + { + // Get the \JInput object + $input = $this->input; + + $component = $input->getCmd('option', 'com_login'); + $file = $input->getCmd('tmpl', 'index'); + + if ($component === 'com_login') { + $file = 'login'; + } + + $this->set('themeFile', $file . '.php'); + + // Safety check for when configuration.php root_user is in use. + $rootUser = $this->get('root_user'); + + if (property_exists('\JConfig', 'root_user')) { + if (Factory::getUser()->get('username') === $rootUser || Factory::getUser()->id === (string) $rootUser) { + $this->enqueueMessage( + Text::sprintf( + 'JWARNING_REMOVE_ROOT_USER', + 'index.php?option=com_config&task=application.removeroot&' . Session::getFormToken() . '=1' + ), + 'warning' + ); + } elseif (Factory::getUser()->authorise('core.admin')) { + // Show this message to superusers too + $this->enqueueMessage( + Text::sprintf( + 'JWARNING_REMOVE_ROOT_USER_ADMIN', + $rootUser, + 'index.php?option=com_config&task=application.removeroot&' . Session::getFormToken() . '=1' + ), + 'warning' + ); + } + } + + parent::render(); + } + + /** + * Route the application. + * + * Routing is the process of examining the request environment to determine which + * component should receive the request. The component optional parameters + * are then set in the request object to be processed when the application is being + * dispatched. + * + * @return void + * + * @since 3.2 + */ + protected function route() + { + $uri = Uri::getInstance(); + + if ($this->get('force_ssl') >= 1 && strtolower($uri->getScheme()) !== 'https') { + // Forward to https + $uri->setScheme('https'); + $this->redirect((string) $uri, 301); + } + + $this->isHandlingMultiFactorAuthentication(); + + // Trigger the onAfterRoute event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onAfterRoute'); + } + + /** + * Return the application option string [main component]. + * + * @return string The component to access. + * + * @since 4.0.0 + */ + public function findOption(): string + { + /** @var self $app */ + $app = Factory::getApplication(); + $option = strtolower($app->input->get('option', '')); + $user = $app->getIdentity(); + + /** + * Special handling for guest users and authenticated users without the Backend Login privilege. + * + * If the component they are trying to access is in the $this->allowedUnprivilegedOptions array we allow the + * request to go through. Otherwise we force com_login to be loaded, letting the user (re)try authenticating + * with a user account that has the Backend Login privilege. + */ + if ($user->get('guest') || !$user->authorise('core.login.admin')) { + $option = in_array($option, $this->allowedUnprivilegedOptions) ? $option : 'com_login'; + } + + /** + * If no component is defined in the request we will try to load com_cpanel, the administrator Control Panel + * component. This allows the /administrator URL to display something meaningful after logging in instead of an + * error. + */ + if (empty($option)) { + $option = 'com_cpanel'; + } + + /** + * Force the option to the input object. This is necessary because we might have force-changed the component in + * the two if-blocks above. + */ + $app->input->set('option', $option); + + return $option; + } } diff --git a/libraries/src/Application/ApiApplication.php b/libraries/src/Application/ApiApplication.php index 50947d831f3e4..bc64a9cfd7a2f 100644 --- a/libraries/src/Application/ApiApplication.php +++ b/libraries/src/Application/ApiApplication.php @@ -1,4 +1,5 @@ name = 'api'; - - // Register the client ID - $this->clientId = 3; - - // Execute the parent constructor - parent::__construct($input, $config, $client, $container); - - $this->addFormatMap('application/json', 'json'); - $this->addFormatMap('application/vnd.api+json', 'jsonapi'); - - // Set the root in the URI based on the application name - Uri::root(null, str_ireplace('/' . $this->getName(), '', Uri::base(true))); - } - - - /** - * Method to run the application routines. - * - * Most likely you will want to instantiate a controller and execute it, or perform some sort of task directly. - * - * @return void - * - * @since 4.0.0 - */ - protected function doExecute() - { - // Initialise the application - $this->initialiseApp(); - - // Mark afterInitialise in the profiler. - JDEBUG ? $this->profiler->mark('afterInitialise') : null; - - // Route the application - $this->route(); - - // Mark afterApiRoute in the profiler. - JDEBUG ? $this->profiler->mark('afterApiRoute') : null; - - // Dispatch the application - $this->dispatch(); - - // Mark afterDispatch in the profiler. - JDEBUG ? $this->profiler->mark('afterDispatch') : null; - } - - /** - * Adds a mapping from a content type to the format stored. Note the format type cannot be overwritten. - * - * @param string $contentHeader The content header - * @param string $format The content type format - * - * @return void - * - * @since 4.0.0 - */ - public function addFormatMap($contentHeader, $format) - { - if (!\array_key_exists($contentHeader, $this->formatMapper)) - { - $this->formatMapper[$contentHeader] = $format; - } - } - - /** - * Rendering is the process of pushing the document buffers into the template - * placeholders, retrieving data from the document and pushing it into - * the application response buffer. - * - * @return void - * - * @since 4.0.0 - * - * @note Rendering should be overridden to get rid of the theme files. - */ - protected function render() - { - // Render the document - $this->setBody($this->document->render($this->allowCache())); - } - - /** - * Method to send the application response to the client. All headers will be sent prior to the main application output data. - * - * @param array $options An optional argument to enable CORS. (Temporary) - * - * @return void - * - * @since 4.0.0 - */ - protected function respond($options = array()) - { - // Set the Joomla! API signature - $this->setHeader('X-Powered-By', 'JoomlaAPI/1.0', true); - - $forceCORS = (int) $this->get('cors'); - - if ($forceCORS) - { - /** - * Enable CORS (Cross-origin resource sharing) - * Obtain allowed CORS origin from Global Settings. - * Set to * (=all) if not set. - */ - $allowedOrigin = $this->get('cors_allow_origin', '*'); - $this->setHeader('Access-Control-Allow-Origin', $allowedOrigin, true); - $this->setHeader('Access-Control-Allow-Headers', 'Authorization'); - - if ($this->input->server->getString('HTTP_ORIGIN', null) !== null) - { - $this->setHeader('Access-Control-Allow-Origin', $this->input->server->getString('HTTP_ORIGIN'), true); - $this->setHeader('Access-Control-Allow-Credentials', 'true', true); - } - } - - // Parent function can be overridden later on for debugging. - parent::respond(); - } - - /** - * Gets the name of the current template. - * - * @param boolean $params True to return the template parameters - * - * @return string|\stdClass - * - * @since 4.0.0 - */ - public function getTemplate($params = false) - { - // The API application should not need to use a template - if ($params) - { - $template = new \stdClass; - $template->template = 'system'; - $template->params = new Registry; - $template->inheritable = 0; - $template->parent = ''; - - return $template; - } - - return 'system'; - } - - /** - * Route the application. - * - * Routing is the process of examining the request environment to determine which - * component should receive the request. The component optional parameters - * are then set in the request object to be processed when the application is being - * dispatched. - * - * @return void - * - * @since 4.0.0 - */ - protected function route() - { - $router = $this->getContainer()->get(ApiRouter::class); - - // Trigger the onBeforeApiRoute event. - PluginHelper::importPlugin('webservices'); - $this->triggerEvent('onBeforeApiRoute', array(&$router, $this)); - $caught404 = false; - $method = $this->input->getMethod(); - - try - { - $this->handlePreflight($method, $router); - - $route = $router->parseApiRoute($method); - } - catch (RouteNotFoundException $e) - { - $caught404 = true; - } - - /** - * Now we have an API perform content negotiation to ensure we have a valid header. Assume if the route doesn't - * tell us otherwise it uses the plain JSON API - */ - $priorities = array('application/vnd.api+json'); - - if (!$caught404 && \array_key_exists('format', $route['vars'])) - { - $priorities = $route['vars']['format']; - } - - $negotiator = new Negotiator; - - try - { - $mediaType = $negotiator->getBest($this->input->server->getString('HTTP_ACCEPT'), $priorities); - } - catch (InvalidArgument $e) - { - $mediaType = null; - } - - // If we can't find a match bail with a 406 - Not Acceptable - if ($mediaType === null) - { - throw new Exception\NotAcceptable('Could not match accept header', 406); - } - - /** @var $mediaType Accept */ - $format = $mediaType->getValue(); - - if (\array_key_exists($mediaType->getValue(), $this->formatMapper)) - { - $format = $this->formatMapper[$mediaType->getValue()]; - } - - $this->input->set('format', $format); - - if ($caught404) - { - throw $e; - } - - $this->input->set('option', $route['vars']['component']); - $this->input->set('controller', $route['controller']); - $this->input->set('task', $route['task']); - - foreach ($route['vars'] as $key => $value) - { - if ($key !== 'component') - { - if ($this->input->getMethod() === 'POST') - { - $this->input->post->set($key, $value); - } - else - { - $this->input->set($key, $value); - } - } - } - - $this->triggerEvent('onAfterApiRoute', array($this)); - - if (!isset($route['vars']['public']) || $route['vars']['public'] === false) - { - if (!$this->login(array('username' => ''), array('silent' => true, 'action' => 'core.login.api'))) - { - throw new AuthenticationFailed; - } - } - } - - /** - * Handles preflight requests. - * - * @param String $method The REST verb - * - * @param ApiRouter $router The API Routing object - * - * @return void - * - * @since 4.0.0 - */ - protected function handlePreflight($method, $router) - { - /** - * If not an OPTIONS request or CORS is not enabled, - * there's nothing useful to do here. - */ - if ($method !== 'OPTIONS' || !(int) $this->get('cors')) - { - return; - } - - // Extract routes matching current route from all known routes. - $matchingRoutes = $router->getMatchingRoutes(); - - // Extract exposed methods from matching routes. - $matchingRoutesMethods = array_unique( - array_reduce($matchingRoutes, - function ($carry, $route) { - return array_merge($carry, $route->getMethods()); - }, - [] - ) - ); - - /** - * Obtain allowed CORS origin from Global Settings. - * Set to * (=all) if not set. - */ - $allowedOrigin = $this->get('cors_allow_origin', '*'); - - /** - * Obtain allowed CORS headers from Global Settings. - * Set to sensible default if not set. - */ - $allowedHeaders = $this->get('cors_allow_headers', 'Content-Type,X-Joomla-Token'); - - /** - * Obtain allowed CORS methods from Global Settings. - * Set to methods exposed by current route if not set. - */ - $allowedMethods = $this->get('cors_allow_methods', implode(',', $matchingRoutesMethods)); - - // No use to go through the regular route handling hassle, - // so let's simply output the headers and exit. - $this->setHeader('status', '204'); - $this->setHeader('Access-Control-Allow-Origin', $allowedOrigin); - $this->setHeader('Access-Control-Allow-Headers', $allowedHeaders); - $this->setHeader('Access-Control-Allow-Methods', $allowedMethods); - $this->sendHeaders(); - - $this->close(); - } - - /** - * Returns the application Router object. - * - * @return ApiRouter - * - * @since 4.0.0 - * @deprecated 5.0 Inject the router or load it from the dependency injection container - */ - public function getApiRouter() - { - return $this->getContainer()->get(ApiRouter::class); - } - - /** - * Dispatch the application - * - * @param string $component The component which is being rendered. - * - * @return void - * - * @since 4.0.0 - */ - public function dispatch($component = null) - { - // Get the component if not set. - if (!$component) - { - $component = $this->input->get('option', null); - } - - // Load the document to the API - $this->loadDocument(); - - // Set up the params - $document = Factory::getDocument(); - - // Register the document object with Factory - Factory::$document = $document; - - $contents = ComponentHelper::renderComponent($component); - $document->setBuffer($contents, 'component'); - - // Trigger the onAfterDispatch event. - PluginHelper::importPlugin('system'); - $this->triggerEvent('onAfterDispatch'); - } + /** + * Maps extension types to their + * + * @var array + * @since 4.0.0 + */ + protected $formatMapper = array(); + + /** + * The authentication plugin type + * + * @var string + * @since 4.0.0 + */ + protected $authenticationPluginType = 'api-authentication'; + + /** + * Class constructor. + * + * @param JInputJson $input An optional argument to provide dependency injection for the application's input + * object. If the argument is a JInput object that object will become the + * application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's config + * object. If the argument is a Registry object that object will become the + * application's config object, otherwise a default config object is created. + * @param WebClient $client An optional argument to provide dependency injection for the application's client + * object. If the argument is a WebClient object that object will become the + * application's client object, otherwise a default client object is created. + * @param Container $container Dependency injection container. + * + * @since 4.0.0 + */ + public function __construct(JInputJson $input = null, Registry $config = null, WebClient $client = null, Container $container = null) + { + // Register the application name + $this->name = 'api'; + + // Register the client ID + $this->clientId = 3; + + // Execute the parent constructor + parent::__construct($input, $config, $client, $container); + + $this->addFormatMap('application/json', 'json'); + $this->addFormatMap('application/vnd.api+json', 'jsonapi'); + + // Set the root in the URI based on the application name + Uri::root(null, str_ireplace('/' . $this->getName(), '', Uri::base(true))); + } + + + /** + * Method to run the application routines. + * + * Most likely you will want to instantiate a controller and execute it, or perform some sort of task directly. + * + * @return void + * + * @since 4.0.0 + */ + protected function doExecute() + { + // Initialise the application + $this->initialiseApp(); + + // Mark afterInitialise in the profiler. + JDEBUG ? $this->profiler->mark('afterInitialise') : null; + + // Route the application + $this->route(); + + // Mark afterApiRoute in the profiler. + JDEBUG ? $this->profiler->mark('afterApiRoute') : null; + + // Dispatch the application + $this->dispatch(); + + // Mark afterDispatch in the profiler. + JDEBUG ? $this->profiler->mark('afterDispatch') : null; + } + + /** + * Adds a mapping from a content type to the format stored. Note the format type cannot be overwritten. + * + * @param string $contentHeader The content header + * @param string $format The content type format + * + * @return void + * + * @since 4.0.0 + */ + public function addFormatMap($contentHeader, $format) + { + if (!\array_key_exists($contentHeader, $this->formatMapper)) { + $this->formatMapper[$contentHeader] = $format; + } + } + + /** + * Rendering is the process of pushing the document buffers into the template + * placeholders, retrieving data from the document and pushing it into + * the application response buffer. + * + * @return void + * + * @since 4.0.0 + * + * @note Rendering should be overridden to get rid of the theme files. + */ + protected function render() + { + // Render the document + $this->setBody($this->document->render($this->allowCache())); + } + + /** + * Method to send the application response to the client. All headers will be sent prior to the main application output data. + * + * @param array $options An optional argument to enable CORS. (Temporary) + * + * @return void + * + * @since 4.0.0 + */ + protected function respond($options = array()) + { + // Set the Joomla! API signature + $this->setHeader('X-Powered-By', 'JoomlaAPI/1.0', true); + + $forceCORS = (int) $this->get('cors'); + + if ($forceCORS) { + /** + * Enable CORS (Cross-origin resource sharing) + * Obtain allowed CORS origin from Global Settings. + * Set to * (=all) if not set. + */ + $allowedOrigin = $this->get('cors_allow_origin', '*'); + $this->setHeader('Access-Control-Allow-Origin', $allowedOrigin, true); + $this->setHeader('Access-Control-Allow-Headers', 'Authorization'); + + if ($this->input->server->getString('HTTP_ORIGIN', null) !== null) { + $this->setHeader('Access-Control-Allow-Origin', $this->input->server->getString('HTTP_ORIGIN'), true); + $this->setHeader('Access-Control-Allow-Credentials', 'true', true); + } + } + + // Parent function can be overridden later on for debugging. + parent::respond(); + } + + /** + * Gets the name of the current template. + * + * @param boolean $params True to return the template parameters + * + * @return string|\stdClass + * + * @since 4.0.0 + */ + public function getTemplate($params = false) + { + // The API application should not need to use a template + if ($params) { + $template = new \stdClass(); + $template->template = 'system'; + $template->params = new Registry(); + $template->inheritable = 0; + $template->parent = ''; + + return $template; + } + + return 'system'; + } + + /** + * Route the application. + * + * Routing is the process of examining the request environment to determine which + * component should receive the request. The component optional parameters + * are then set in the request object to be processed when the application is being + * dispatched. + * + * @return void + * + * @since 4.0.0 + */ + protected function route() + { + $router = $this->getContainer()->get(ApiRouter::class); + + // Trigger the onBeforeApiRoute event. + PluginHelper::importPlugin('webservices'); + $this->triggerEvent('onBeforeApiRoute', array(&$router, $this)); + $caught404 = false; + $method = $this->input->getMethod(); + + try { + $this->handlePreflight($method, $router); + + $route = $router->parseApiRoute($method); + } catch (RouteNotFoundException $e) { + $caught404 = true; + } + + /** + * Now we have an API perform content negotiation to ensure we have a valid header. Assume if the route doesn't + * tell us otherwise it uses the plain JSON API + */ + $priorities = array('application/vnd.api+json'); + + if (!$caught404 && \array_key_exists('format', $route['vars'])) { + $priorities = $route['vars']['format']; + } + + $negotiator = new Negotiator(); + + try { + $mediaType = $negotiator->getBest($this->input->server->getString('HTTP_ACCEPT'), $priorities); + } catch (InvalidArgument $e) { + $mediaType = null; + } + + // If we can't find a match bail with a 406 - Not Acceptable + if ($mediaType === null) { + throw new Exception\NotAcceptable('Could not match accept header', 406); + } + + /** @var $mediaType Accept */ + $format = $mediaType->getValue(); + + if (\array_key_exists($mediaType->getValue(), $this->formatMapper)) { + $format = $this->formatMapper[$mediaType->getValue()]; + } + + $this->input->set('format', $format); + + if ($caught404) { + throw $e; + } + + $this->input->set('option', $route['vars']['component']); + $this->input->set('controller', $route['controller']); + $this->input->set('task', $route['task']); + + foreach ($route['vars'] as $key => $value) { + if ($key !== 'component') { + if ($this->input->getMethod() === 'POST') { + $this->input->post->set($key, $value); + } else { + $this->input->set($key, $value); + } + } + } + + $this->triggerEvent('onAfterApiRoute', array($this)); + + if (!isset($route['vars']['public']) || $route['vars']['public'] === false) { + if (!$this->login(array('username' => ''), array('silent' => true, 'action' => 'core.login.api'))) { + throw new AuthenticationFailed(); + } + } + } + + /** + * Handles preflight requests. + * + * @param String $method The REST verb + * + * @param ApiRouter $router The API Routing object + * + * @return void + * + * @since 4.0.0 + */ + protected function handlePreflight($method, $router) + { + /** + * If not an OPTIONS request or CORS is not enabled, + * there's nothing useful to do here. + */ + if ($method !== 'OPTIONS' || !(int) $this->get('cors')) { + return; + } + + // Extract routes matching current route from all known routes. + $matchingRoutes = $router->getMatchingRoutes(); + + // Extract exposed methods from matching routes. + $matchingRoutesMethods = array_unique( + array_reduce( + $matchingRoutes, + function ($carry, $route) { + return array_merge($carry, $route->getMethods()); + }, + [] + ) + ); + + /** + * Obtain allowed CORS origin from Global Settings. + * Set to * (=all) if not set. + */ + $allowedOrigin = $this->get('cors_allow_origin', '*'); + + /** + * Obtain allowed CORS headers from Global Settings. + * Set to sensible default if not set. + */ + $allowedHeaders = $this->get('cors_allow_headers', 'Content-Type,X-Joomla-Token'); + + /** + * Obtain allowed CORS methods from Global Settings. + * Set to methods exposed by current route if not set. + */ + $allowedMethods = $this->get('cors_allow_methods', implode(',', $matchingRoutesMethods)); + + // No use to go through the regular route handling hassle, + // so let's simply output the headers and exit. + $this->setHeader('status', '204'); + $this->setHeader('Access-Control-Allow-Origin', $allowedOrigin); + $this->setHeader('Access-Control-Allow-Headers', $allowedHeaders); + $this->setHeader('Access-Control-Allow-Methods', $allowedMethods); + $this->sendHeaders(); + + $this->close(); + } + + /** + * Returns the application Router object. + * + * @return ApiRouter + * + * @since 4.0.0 + * @deprecated 5.0 Inject the router or load it from the dependency injection container + */ + public function getApiRouter() + { + return $this->getContainer()->get(ApiRouter::class); + } + + /** + * Dispatch the application + * + * @param string $component The component which is being rendered. + * + * @return void + * + * @since 4.0.0 + */ + public function dispatch($component = null) + { + // Get the component if not set. + if (!$component) { + $component = $this->input->get('option', null); + } + + // Load the document to the API + $this->loadDocument(); + + // Set up the params + $document = Factory::getDocument(); + + // Register the document object with Factory + Factory::$document = $document; + + $contents = ComponentHelper::renderComponent($component); + $document->setBuffer($contents, 'component'); + + // Trigger the onAfterDispatch event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onAfterDispatch'); + } } diff --git a/libraries/src/Application/ApplicationHelper.php b/libraries/src/Application/ApplicationHelper.php index 454e5978659d7..bda68f3c9f986 100644 --- a/libraries/src/Application/ApplicationHelper.php +++ b/libraries/src/Application/ApplicationHelper.php @@ -1,4 +1,5 @@ input; - $option = strtolower($input->get('option', '')); - - if (empty($option)) - { - $option = $default; - } - - $input->set('option', $option); - - return $option; - } - - /** - * Provides a secure hash based on a seed - * - * @param string $seed Seed string. - * - * @return string A secure hash - * - * @since 3.2 - */ - public static function getHash($seed) - { - return md5(Factory::getApplication()->get('secret') . $seed); - } - - /** - * This method transliterates a string into a URL - * safe string or returns a URL safe UTF-8 string - * based on the global configuration - * - * @param string $string String to process - * @param string $language Language to transliterate to if unicode slugs are disabled - * - * @return string Processed string - * - * @since 3.2 - */ - public static function stringURLSafe($string, $language = '') - { - if (Factory::getApplication()->get('unicodeslugs') == 1) - { - $output = OutputFilter::stringUrlUnicodeSlug($string); - } - else - { - if ($language === '*' || $language === '') - { - $languageParams = ComponentHelper::getParams('com_languages'); - $language = $languageParams->get('site'); - } - - $output = OutputFilter::stringURLSafe($string, $language); - } - - return $output; - } - - /** - * Gets information on a specific client id. This method will be useful in - * future versions when we start mapping applications in the database. - * - * This method will return a client information array if called - * with no arguments which can be used to add custom application information. - * - * @param integer|string|null $id A client identifier - * @param boolean $byName If true, find the client by its name - * - * @return \stdClass|array|void Object describing the client, array containing all the clients or void if $id not known - * - * @since 1.5 - */ - public static function getClientInfo($id = null, $byName = false) - { - // Only create the array if it is empty - if (empty(self::$_clients)) - { - $obj = new \stdClass; - - // Site Client - $obj->id = 0; - $obj->name = 'site'; - $obj->path = JPATH_SITE; - self::$_clients[0] = clone $obj; - - // Administrator Client - $obj->id = 1; - $obj->name = 'administrator'; - $obj->path = JPATH_ADMINISTRATOR; - self::$_clients[1] = clone $obj; - - // Installation Client - $obj->id = 2; - $obj->name = 'installation'; - $obj->path = JPATH_INSTALLATION; - self::$_clients[2] = clone $obj; - - // API Client - $obj->id = 3; - $obj->name = 'api'; - $obj->path = JPATH_API; - self::$_clients[3] = clone $obj; - - // CLI Client - $obj->id = 4; - $obj->name = 'cli'; - $obj->path = JPATH_CLI; - self::$_clients[4] = clone $obj; - } - - // If no client id has been passed return the whole array - if ($id === null) - { - return self::$_clients; - } - - // Are we looking for client information by id or by name? - if (!$byName) - { - if (isset(self::$_clients[$id])) - { - return self::$_clients[$id]; - } - } - else - { - foreach (self::$_clients as $client) - { - if ($client->name == strtolower($id)) - { - return $client; - } - } - } - } - - /** - * Adds information for a client. - * - * @param mixed $client A client identifier either an array or object - * - * @return boolean True if the information is added. False on error - * - * @since 1.6 - */ - public static function addClientInfo($client) - { - if (\is_array($client)) - { - $client = (object) $client; - } - - if (!\is_object($client)) - { - return false; - } - - $info = self::getClientInfo(); - - if (!isset($client->id)) - { - $client->id = \count($info); - } - - self::$_clients[$client->id] = clone $client; - - return true; - } + /** + * Client information array + * + * @var array + * @since 1.6 + */ + protected static $_clients = array(); + + /** + * Return the name of the request component [main component] + * + * @param string $default The default option + * + * @return string Option (e.g. com_something) + * + * @since 1.6 + */ + public static function getComponentName($default = null) + { + static $option; + + if ($option) { + return $option; + } + + $input = Factory::getApplication()->input; + $option = strtolower($input->get('option', '')); + + if (empty($option)) { + $option = $default; + } + + $input->set('option', $option); + + return $option; + } + + /** + * Provides a secure hash based on a seed + * + * @param string $seed Seed string. + * + * @return string A secure hash + * + * @since 3.2 + */ + public static function getHash($seed) + { + return md5(Factory::getApplication()->get('secret') . $seed); + } + + /** + * This method transliterates a string into a URL + * safe string or returns a URL safe UTF-8 string + * based on the global configuration + * + * @param string $string String to process + * @param string $language Language to transliterate to if unicode slugs are disabled + * + * @return string Processed string + * + * @since 3.2 + */ + public static function stringURLSafe($string, $language = '') + { + if (Factory::getApplication()->get('unicodeslugs') == 1) { + $output = OutputFilter::stringUrlUnicodeSlug($string); + } else { + if ($language === '*' || $language === '') { + $languageParams = ComponentHelper::getParams('com_languages'); + $language = $languageParams->get('site'); + } + + $output = OutputFilter::stringURLSafe($string, $language); + } + + return $output; + } + + /** + * Gets information on a specific client id. This method will be useful in + * future versions when we start mapping applications in the database. + * + * This method will return a client information array if called + * with no arguments which can be used to add custom application information. + * + * @param integer|string|null $id A client identifier + * @param boolean $byName If true, find the client by its name + * + * @return \stdClass|array|void Object describing the client, array containing all the clients or void if $id not known + * + * @since 1.5 + */ + public static function getClientInfo($id = null, $byName = false) + { + // Only create the array if it is empty + if (empty(self::$_clients)) { + $obj = new \stdClass(); + + // Site Client + $obj->id = 0; + $obj->name = 'site'; + $obj->path = JPATH_SITE; + self::$_clients[0] = clone $obj; + + // Administrator Client + $obj->id = 1; + $obj->name = 'administrator'; + $obj->path = JPATH_ADMINISTRATOR; + self::$_clients[1] = clone $obj; + + // Installation Client + $obj->id = 2; + $obj->name = 'installation'; + $obj->path = JPATH_INSTALLATION; + self::$_clients[2] = clone $obj; + + // API Client + $obj->id = 3; + $obj->name = 'api'; + $obj->path = JPATH_API; + self::$_clients[3] = clone $obj; + + // CLI Client + $obj->id = 4; + $obj->name = 'cli'; + $obj->path = JPATH_CLI; + self::$_clients[4] = clone $obj; + } + + // If no client id has been passed return the whole array + if ($id === null) { + return self::$_clients; + } + + // Are we looking for client information by id or by name? + if (!$byName) { + if (isset(self::$_clients[$id])) { + return self::$_clients[$id]; + } + } else { + foreach (self::$_clients as $client) { + if ($client->name == strtolower($id)) { + return $client; + } + } + } + } + + /** + * Adds information for a client. + * + * @param mixed $client A client identifier either an array or object + * + * @return boolean True if the information is added. False on error + * + * @since 1.6 + */ + public static function addClientInfo($client) + { + if (\is_array($client)) { + $client = (object) $client; + } + + if (!\is_object($client)) { + return false; + } + + $info = self::getClientInfo(); + + if (!isset($client->id)) { + $client->id = \count($info); + } + + self::$_clients[$client->id] = clone $client; + + return true; + } } diff --git a/libraries/src/Application/BaseApplication.php b/libraries/src/Application/BaseApplication.php index f4e63fa739e6f..6016313411513 100644 --- a/libraries/src/Application/BaseApplication.php +++ b/libraries/src/Application/BaseApplication.php @@ -1,4 +1,5 @@ input = $input instanceof Input ? $input : new Input; - $this->config = $config instanceof Registry ? $config : new Registry; + /** + * Class constructor. + * + * @param Input $input An optional argument to provide dependency injection for the application's + * input object. If the argument is a \JInput object that object will become + * the application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's + * config object. If the argument is a Registry object that object will become + * the application's config object, otherwise a default config object is created. + * + * @since 3.0.0 + */ + public function __construct(Input $input = null, Registry $config = null) + { + $this->input = $input instanceof Input ? $input : new Input(); + $this->config = $config instanceof Registry ? $config : new Registry(); - $this->initialise(); - } + $this->initialise(); + } } diff --git a/libraries/src/Application/CLI/CliInput.php b/libraries/src/Application/CLI/CliInput.php index 0f214306f7b6d..a53418870932b 100644 --- a/libraries/src/Application/CLI/CliInput.php +++ b/libraries/src/Application/CLI/CliInput.php @@ -1,4 +1,5 @@ setProcessor($processor ?: new Output\Processor\ColorProcessor); - } + /** + * Constructor + * + * @param ProcessorInterface $processor The output processor. + * + * @since 4.0.0 + */ + public function __construct(ProcessorInterface $processor = null) + { + $this->setProcessor($processor ?: new Output\Processor\ColorProcessor()); + } - /** - * Set a processor - * - * @param ProcessorInterface $processor The output processor. - * - * @return $this - * - * @since 4.0.0 - */ - public function setProcessor(ProcessorInterface $processor) - { - $this->processor = $processor; + /** + * Set a processor + * + * @param ProcessorInterface $processor The output processor. + * + * @return $this + * + * @since 4.0.0 + */ + public function setProcessor(ProcessorInterface $processor) + { + $this->processor = $processor; - return $this; - } + return $this; + } - /** - * Get a processor - * - * @return ProcessorInterface - * - * @since 4.0.0 - * @throws \RuntimeException - */ - public function getProcessor() - { - if ($this->processor) - { - return $this->processor; - } + /** + * Get a processor + * + * @return ProcessorInterface + * + * @since 4.0.0 + * @throws \RuntimeException + */ + public function getProcessor() + { + if ($this->processor) { + return $this->processor; + } - throw new \RuntimeException('A ProcessorInterface object has not been set.'); - } + throw new \RuntimeException('A ProcessorInterface object has not been set.'); + } - /** - * Write a string to an output handler. - * - * @param string $text The text to display. - * @param boolean $nl True (default) to append a new line at the end of the output string. - * - * @return $this - * - * @since 4.0.0 - * @codeCoverageIgnore - */ - abstract public function out($text = '', $nl = true); + /** + * Write a string to an output handler. + * + * @param string $text The text to display. + * @param boolean $nl True (default) to append a new line at the end of the output string. + * + * @return $this + * + * @since 4.0.0 + * @codeCoverageIgnore + */ + abstract public function out($text = '', $nl = true); } diff --git a/libraries/src/Application/CLI/ColorStyle.php b/libraries/src/Application/CLI/ColorStyle.php index 0b1050af826ef..3debed5f17896 100644 --- a/libraries/src/Application/CLI/ColorStyle.php +++ b/libraries/src/Application/CLI/ColorStyle.php @@ -1,4 +1,5 @@ 0, - 'red' => 1, - 'green' => 2, - 'yellow' => 3, - 'blue' => 4, - 'magenta' => 5, - 'cyan' => 6, - 'white' => 7, - ]; - - /** - * Known styles - * - * @var array - * @since 4.0.0 - */ - private static $knownOptions = [ - 'bold' => 1, - 'underscore' => 4, - 'blink' => 5, - 'reverse' => 7, - ]; - - /** - * Foreground base value - * - * @var integer - * @since 4.0.0 - */ - private static $fgBase = 30; - - /** - * Background base value - * - * @var integer - * @since 4.0.0 - */ - private static $bgBase = 40; - - /** - * Foreground color - * - * @var integer - * @since 4.0.0 - */ - private $fgColor = 0; - - /** - * Background color - * - * @var integer - * @since 4.0.0 - */ - private $bgColor = 0; - - /** - * Array of style options - * - * @var array - * @since 4.0.0 - */ - private $options = []; - - /** - * Constructor - * - * @param string $fg Foreground color. - * @param string $bg Background color. - * @param array $options Style options. - * - * @since 4.0.0 - * @throws \InvalidArgumentException - */ - public function __construct(string $fg = '', string $bg = '', array $options = []) - { - if ($fg) - { - if (\array_key_exists($fg, static::$knownColors) == false) - { - throw new \InvalidArgumentException( - sprintf( - 'Invalid foreground color "%1$s" [%2$s]', - $fg, - implode(', ', $this->getKnownColors()) - ) - ); - } - - $this->fgColor = static::$fgBase + static::$knownColors[$fg]; - } - - if ($bg) - { - if (\array_key_exists($bg, static::$knownColors) == false) - { - throw new \InvalidArgumentException( - sprintf( - 'Invalid background color "%1$s" [%2$s]', - $bg, - implode(', ', $this->getKnownColors()) - ) - ); - } - - $this->bgColor = static::$bgBase + static::$knownColors[$bg]; - } - - foreach ($options as $option) - { - if (\array_key_exists($option, static::$knownOptions) == false) - { - throw new \InvalidArgumentException( - sprintf( - 'Invalid option "%1$s" [%2$s]', - $option, - implode(', ', $this->getKnownOptions()) - ) - ); - } - - $this->options[] = $option; - } - } - - /** - * Convert to a string. - * - * @return string - * - * @since 4.0.0 - */ - public function __toString() - { - return $this->getStyle(); - } - - /** - * Create a color style from a parameter string. - * - * Example: fg=red;bg=blue;options=bold,blink - * - * @param string $string The parameter string. - * - * @return $this - * - * @since 4.0.0 - * @throws \RuntimeException - */ - public static function fromString(string $string): self - { - $fg = ''; - $bg = ''; - $options = []; - - $parts = explode(';', $string); - - foreach ($parts as $part) - { - $subParts = explode('=', $part); - - if (\count($subParts) < 2) - { - continue; - } - - switch ($subParts[0]) - { - case 'fg': - $fg = $subParts[1]; - - break; - - case 'bg': - $bg = $subParts[1]; - - break; - - case 'options': - $options = explode(',', $subParts[1]); - - break; - - default: - throw new \RuntimeException('Invalid option: ' . $subParts[0]); - } - } - - return new self($fg, $bg, $options); - } - - /** - * Get the translated color code. - * - * @return string - * - * @since 4.0.0 - */ - public function getStyle(): string - { - $values = []; - - if ($this->fgColor) - { - $values[] = $this->fgColor; - } - - if ($this->bgColor) - { - $values[] = $this->bgColor; - } - - foreach ($this->options as $option) - { - $values[] = static::$knownOptions[$option]; - } - - return implode(';', $values); - } - - /** - * Get the known colors. - * - * @return string[] - * - * @since 4.0.0 - */ - public function getKnownColors(): array - { - return array_keys(static::$knownColors); - } - - /** - * Get the known options. - * - * @return string[] - * - * @since 4.0.0 - */ - public function getKnownOptions(): array - { - return array_keys(static::$knownOptions); - } + /** + * Known colors + * + * @var array + * @since 4.0.0 + */ + private static $knownColors = [ + 'black' => 0, + 'red' => 1, + 'green' => 2, + 'yellow' => 3, + 'blue' => 4, + 'magenta' => 5, + 'cyan' => 6, + 'white' => 7, + ]; + + /** + * Known styles + * + * @var array + * @since 4.0.0 + */ + private static $knownOptions = [ + 'bold' => 1, + 'underscore' => 4, + 'blink' => 5, + 'reverse' => 7, + ]; + + /** + * Foreground base value + * + * @var integer + * @since 4.0.0 + */ + private static $fgBase = 30; + + /** + * Background base value + * + * @var integer + * @since 4.0.0 + */ + private static $bgBase = 40; + + /** + * Foreground color + * + * @var integer + * @since 4.0.0 + */ + private $fgColor = 0; + + /** + * Background color + * + * @var integer + * @since 4.0.0 + */ + private $bgColor = 0; + + /** + * Array of style options + * + * @var array + * @since 4.0.0 + */ + private $options = []; + + /** + * Constructor + * + * @param string $fg Foreground color. + * @param string $bg Background color. + * @param array $options Style options. + * + * @since 4.0.0 + * @throws \InvalidArgumentException + */ + public function __construct(string $fg = '', string $bg = '', array $options = []) + { + if ($fg) { + if (\array_key_exists($fg, static::$knownColors) == false) { + throw new \InvalidArgumentException( + sprintf( + 'Invalid foreground color "%1$s" [%2$s]', + $fg, + implode(', ', $this->getKnownColors()) + ) + ); + } + + $this->fgColor = static::$fgBase + static::$knownColors[$fg]; + } + + if ($bg) { + if (\array_key_exists($bg, static::$knownColors) == false) { + throw new \InvalidArgumentException( + sprintf( + 'Invalid background color "%1$s" [%2$s]', + $bg, + implode(', ', $this->getKnownColors()) + ) + ); + } + + $this->bgColor = static::$bgBase + static::$knownColors[$bg]; + } + + foreach ($options as $option) { + if (\array_key_exists($option, static::$knownOptions) == false) { + throw new \InvalidArgumentException( + sprintf( + 'Invalid option "%1$s" [%2$s]', + $option, + implode(', ', $this->getKnownOptions()) + ) + ); + } + + $this->options[] = $option; + } + } + + /** + * Convert to a string. + * + * @return string + * + * @since 4.0.0 + */ + public function __toString() + { + return $this->getStyle(); + } + + /** + * Create a color style from a parameter string. + * + * Example: fg=red;bg=blue;options=bold,blink + * + * @param string $string The parameter string. + * + * @return $this + * + * @since 4.0.0 + * @throws \RuntimeException + */ + public static function fromString(string $string): self + { + $fg = ''; + $bg = ''; + $options = []; + + $parts = explode(';', $string); + + foreach ($parts as $part) { + $subParts = explode('=', $part); + + if (\count($subParts) < 2) { + continue; + } + + switch ($subParts[0]) { + case 'fg': + $fg = $subParts[1]; + + break; + + case 'bg': + $bg = $subParts[1]; + + break; + + case 'options': + $options = explode(',', $subParts[1]); + + break; + + default: + throw new \RuntimeException('Invalid option: ' . $subParts[0]); + } + } + + return new self($fg, $bg, $options); + } + + /** + * Get the translated color code. + * + * @return string + * + * @since 4.0.0 + */ + public function getStyle(): string + { + $values = []; + + if ($this->fgColor) { + $values[] = $this->fgColor; + } + + if ($this->bgColor) { + $values[] = $this->bgColor; + } + + foreach ($this->options as $option) { + $values[] = static::$knownOptions[$option]; + } + + return implode(';', $values); + } + + /** + * Get the known colors. + * + * @return string[] + * + * @since 4.0.0 + */ + public function getKnownColors(): array + { + return array_keys(static::$knownColors); + } + + /** + * Get the known options. + * + * @return string[] + * + * @since 4.0.0 + */ + public function getKnownOptions(): array + { + return array_keys(static::$knownOptions); + } } diff --git a/libraries/src/Application/CLI/Output/Processor/ColorProcessor.php b/libraries/src/Application/CLI/Output/Processor/ColorProcessor.php index 894d96f6ec987..c9c479f88649a 100644 --- a/libraries/src/Application/CLI/Output/Processor/ColorProcessor.php +++ b/libraries/src/Application/CLI/Output/Processor/ColorProcessor.php @@ -1,4 +1,5 @@ (.*?)<\/\\1>/s'; - - /** - * Regex used for removing color codes - * - * @var string - * @since 4.0.0 - */ - protected static $stripFilter = '/<[\/]?[a-z=;]+>/'; - - /** - * Array of ColorStyle objects - * - * @var ColorStyle[] - * @since 4.0.0 - */ - protected $styles = []; - - /** - * Class constructor - * - * @param boolean $noColors Defines non-colored mode on construct - * - * @since 4.0.0 - */ - public function __construct($noColors = null) - { - if ($noColors === null) - { - /* - * By default windows cmd.exe and PowerShell does not support ANSI-colored output - * if the variable is not set explicitly colors should be disabled on Windows - */ - $noColors = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'); - } - - $this->noColors = $noColors; - - $this->addPredefinedStyles(); - } - - /** - * Add a style. - * - * @param string $name The style name. - * @param ColorStyle $style The color style. - * - * @return $this - * - * @since 4.0.0 - */ - public function addStyle($name, ColorStyle $style) - { - $this->styles[$name] = $style; - - return $this; - } - - /** - * Strip color tags from a string. - * - * @param string $string The string. - * - * @return string - * - * @since 4.0.0 - */ - public static function stripColors($string) - { - return preg_replace(static::$stripFilter, '', $string); - } - - /** - * Process a string. - * - * @param string $string The string to process. - * - * @return string - * - * @since 4.0.0 - */ - public function process($string) - { - preg_match_all($this->tagFilter, $string, $matches); - - if (!$matches) - { - return $string; - } - - foreach ($matches[0] as $i => $m) - { - if (\array_key_exists($matches[1][$i], $this->styles)) - { - $string = $this->replaceColors($string, $matches[1][$i], $matches[2][$i], $this->styles[$matches[1][$i]]); - } - // Custom format - elseif (strpos($matches[1][$i], '=')) - { - $string = $this->replaceColors($string, $matches[1][$i], $matches[2][$i], ColorStyle::fromString($matches[1][$i])); - } - } - - return $string; - } - - /** - * Replace color tags in a string. - * - * @param string $text The original text. - * @param string $tag The matched tag. - * @param string $match The match. - * @param ColorStyle $style The color style to apply. - * - * @return mixed - * - * @since 4.0.0 - */ - private function replaceColors($text, $tag, $match, ColorStyle $style) - { - $replace = $this->noColors - ? $match - : "\033[" . $style . 'm' . $match . "\033[0m"; - - return str_replace('<' . $tag . '>' . $match . '', $replace, $text); - } - - /** - * Adds predefined color styles to the ColorProcessor object - * - * @return $this - * - * @since 4.0.0 - */ - private function addPredefinedStyles() - { - $this->addStyle( - 'info', - new ColorStyle('green', '', ['bold']) - ); - - $this->addStyle( - 'comment', - new ColorStyle('yellow', '', ['bold']) - ); - - $this->addStyle( - 'question', - new ColorStyle('black', 'cyan') - ); - - $this->addStyle( - 'error', - new ColorStyle('white', 'red') - ); - - return $this; - } + /** + * Flag to remove color codes from the output + * + * @var boolean + * @since 4.0.0 + */ + public $noColors = false; + + /** + * Regex to match tags + * + * @var string + * @since 4.0.0 + */ + protected $tagFilter = '/<([a-z=;]+)>(.*?)<\/\\1>/s'; + + /** + * Regex used for removing color codes + * + * @var string + * @since 4.0.0 + */ + protected static $stripFilter = '/<[\/]?[a-z=;]+>/'; + + /** + * Array of ColorStyle objects + * + * @var ColorStyle[] + * @since 4.0.0 + */ + protected $styles = []; + + /** + * Class constructor + * + * @param boolean $noColors Defines non-colored mode on construct + * + * @since 4.0.0 + */ + public function __construct($noColors = null) + { + if ($noColors === null) { + /* + * By default windows cmd.exe and PowerShell does not support ANSI-colored output + * if the variable is not set explicitly colors should be disabled on Windows + */ + $noColors = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'); + } + + $this->noColors = $noColors; + + $this->addPredefinedStyles(); + } + + /** + * Add a style. + * + * @param string $name The style name. + * @param ColorStyle $style The color style. + * + * @return $this + * + * @since 4.0.0 + */ + public function addStyle($name, ColorStyle $style) + { + $this->styles[$name] = $style; + + return $this; + } + + /** + * Strip color tags from a string. + * + * @param string $string The string. + * + * @return string + * + * @since 4.0.0 + */ + public static function stripColors($string) + { + return preg_replace(static::$stripFilter, '', $string); + } + + /** + * Process a string. + * + * @param string $string The string to process. + * + * @return string + * + * @since 4.0.0 + */ + public function process($string) + { + preg_match_all($this->tagFilter, $string, $matches); + + if (!$matches) { + return $string; + } + + foreach ($matches[0] as $i => $m) { + if (\array_key_exists($matches[1][$i], $this->styles)) { + $string = $this->replaceColors($string, $matches[1][$i], $matches[2][$i], $this->styles[$matches[1][$i]]); + } elseif (strpos($matches[1][$i], '=')) { + // Custom format + $string = $this->replaceColors($string, $matches[1][$i], $matches[2][$i], ColorStyle::fromString($matches[1][$i])); + } + } + + return $string; + } + + /** + * Replace color tags in a string. + * + * @param string $text The original text. + * @param string $tag The matched tag. + * @param string $match The match. + * @param ColorStyle $style The color style to apply. + * + * @return mixed + * + * @since 4.0.0 + */ + private function replaceColors($text, $tag, $match, ColorStyle $style) + { + $replace = $this->noColors + ? $match + : "\033[" . $style . 'm' . $match . "\033[0m"; + + return str_replace('<' . $tag . '>' . $match . '', $replace, $text); + } + + /** + * Adds predefined color styles to the ColorProcessor object + * + * @return $this + * + * @since 4.0.0 + */ + private function addPredefinedStyles() + { + $this->addStyle( + 'info', + new ColorStyle('green', '', ['bold']) + ); + + $this->addStyle( + 'comment', + new ColorStyle('yellow', '', ['bold']) + ); + + $this->addStyle( + 'question', + new ColorStyle('black', 'cyan') + ); + + $this->addStyle( + 'error', + new ColorStyle('white', 'red') + ); + + return $this; + } } diff --git a/libraries/src/Application/CLI/Output/Processor/ProcessorInterface.php b/libraries/src/Application/CLI/Output/Processor/ProcessorInterface.php index 0e54f3e277226..f2b5701fc3046 100644 --- a/libraries/src/Application/CLI/Output/Processor/ProcessorInterface.php +++ b/libraries/src/Application/CLI/Output/Processor/ProcessorInterface.php @@ -1,4 +1,5 @@ getProcessor()->process($text) . ($nl ? "\n" : null)); + /** + * Write a string to standard output + * + * @param string $text The text to display. + * @param boolean $nl True (default) to append a new line at the end of the output string. + * + * @return $this + * + * @codeCoverageIgnore + * @since 4.0.0 + */ + public function out($text = '', $nl = true) + { + fwrite(STDOUT, $this->getProcessor()->process($text) . ($nl ? "\n" : null)); - return $this; - } + return $this; + } } diff --git a/libraries/src/Application/CLI/Output/Xml.php b/libraries/src/Application/CLI/Output/Xml.php index a5d09c1916ffb..9c0926396f692 100644 --- a/libraries/src/Application/CLI/Output/Xml.php +++ b/libraries/src/Application/CLI/Output/Xml.php @@ -1,4 +1,5 @@ setContainer($container); - - parent::__construct($input, $config, $client); - - // If JDEBUG is defined, load the profiler instance - if (\defined('JDEBUG') && JDEBUG) - { - $this->profiler = Profiler::getInstance('Application'); - } - - // Enable sessions by default. - if ($this->config->get('session') === null) - { - $this->config->set('session', true); - } - - // Set the session default name. - if ($this->config->get('session_name') === null) - { - $this->config->set('session_name', $this->getName()); - } - } - - /** - * Checks the user session. - * - * If the session record doesn't exist, initialise it. - * If session is new, create session variables - * - * @return void - * - * @since 3.2 - * @throws \RuntimeException - */ - public function checkSession() - { - $this->getContainer()->get(MetadataManager::class)->createOrUpdateRecord($this->getSession(), $this->getIdentity()); - } - - /** - * Enqueue a system message. - * - * @param string $msg The message to enqueue. - * @param string $type The message type. Default is message. - * - * @return void - * - * @since 3.2 - */ - public function enqueueMessage($msg, $type = self::MSG_INFO) - { - // Don't add empty messages. - if ($msg === null || trim($msg) === '') - { - return; - } - - $inputFilter = InputFilter::getInstance( - [], - [], - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ); - - // Build the message array and apply the HTML InputFilter with the default blacklist to the message - $message = array( - 'message' => $inputFilter->clean($msg, 'html'), - 'type' => $inputFilter->clean(strtolower($type), 'cmd'), - ); - - // For empty queue, if messages exists in the session, enqueue them first. - $messages = $this->getMessageQueue(); - - if (!\in_array($message, $this->messageQueue)) - { - // Enqueue the message. - $this->messageQueue[] = $message; - } - } - - /** - * Ensure several core system input variables are not arrays. - * - * @return void - * - * @since 3.9 - */ - private function sanityCheckSystemVariables() - { - $input = $this->input; - - // Get invalid input variables - $invalidInputVariables = array_filter( - array('option', 'view', 'format', 'lang', 'Itemid', 'template', 'templateStyle', 'task'), - function ($systemVariable) use ($input) { - return $input->exists($systemVariable) && is_array($input->getRaw($systemVariable)); - } - ); - - // Unset invalid system variables - foreach ($invalidInputVariables as $systemVariable) - { - $input->set($systemVariable, null); - } - - // Abort when there are invalid variables - if ($invalidInputVariables) - { - throw new \RuntimeException('Invalid input, aborting application.'); - } - } - - /** - * Execute the application. - * - * @return void - * - * @since 3.2 - */ - public function execute() - { - try - { - $this->sanityCheckSystemVariables(); - $this->setupLogging(); - $this->createExtensionNamespaceMap(); - - // Perform application routines. - $this->doExecute(); - - // If we have an application document object, render it. - if ($this->document instanceof \Joomla\CMS\Document\Document) - { - // Render the application output. - $this->render(); - } - - // If gzip compression is enabled in configuration and the server is compliant, compress the output. - if ($this->get('gzip') && !ini_get('zlib.output_compression') && ini_get('output_handler') !== 'ob_gzhandler') - { - $this->compress(); - - // Trigger the onAfterCompress event. - $this->triggerEvent('onAfterCompress'); - } - } - catch (\Throwable $throwable) - { - /** @var ErrorEvent $event */ - $event = AbstractEvent::create( - 'onError', - [ - 'subject' => $throwable, - 'eventClass' => ErrorEvent::class, - 'application' => $this, - ] - ); - - // Trigger the onError event. - $this->triggerEvent('onError', $event); - - ExceptionHandler::handleException($event->getError()); - } - - // Trigger the onBeforeRespond event. - $this->getDispatcher()->dispatch('onBeforeRespond'); - - // Send the application response. - $this->respond(); - - // Trigger the onAfterRespond event. - $this->getDispatcher()->dispatch('onAfterRespond'); - } - - /** - * Check if the user is required to reset their password. - * - * If the user is required to reset their password will be redirected to the page that manage the password reset. - * - * @param string $option The option that manage the password reset - * @param string $view The view that manage the password reset - * @param string $layout The layout of the view that manage the password reset - * @param string $tasks Permitted tasks - * - * @return void - * - * @throws \Exception - */ - protected function checkUserRequireReset($option, $view, $layout, $tasks) - { - if (Factory::getUser()->get('requireReset', 0)) - { - $redirect = false; - - /* - * By default user profile edit page is used. - * That page allows you to change more than just the password and might not be the desired behavior. - * This allows a developer to override the page that manage the password reset. - * (can be configured using the file: configuration.php, or if extended, through the global configuration form) - */ - $name = $this->getName(); - - if ($this->get($name . '_reset_password_override', 0)) - { - $option = $this->get($name . '_reset_password_option', ''); - $view = $this->get($name . '_reset_password_view', ''); - $layout = $this->get($name . '_reset_password_layout', ''); - $tasks = $this->get($name . '_reset_password_tasks', ''); - } - - $task = $this->input->getCmd('task', ''); - - // Check task or option/view/layout - if (!empty($task)) - { - $tasks = explode(',', $tasks); - - // Check full task version "option/task" - if (array_search($this->input->getCmd('option', '') . '/' . $task, $tasks) === false) - { - // Check short task version, must be on the same option of the view - if ($this->input->getCmd('option', '') !== $option || array_search($task, $tasks) === false) - { - // Not permitted task - $redirect = true; - } - } - } - else - { - if ($this->input->getCmd('option', '') !== $option || $this->input->getCmd('view', '') !== $view - || $this->input->getCmd('layout', '') !== $layout) - { - // Requested a different option/view/layout - $redirect = true; - } - } - - if ($redirect) - { - // Redirect to the profile edit page - $this->enqueueMessage(Text::_('JGLOBAL_PASSWORD_RESET_REQUIRED'), 'notice'); - - $url = Route::_('index.php?option=' . $option . '&view=' . $view . '&layout=' . $layout, false); - - // In the administrator we need a different URL - if (strtolower($name) === 'administrator') - { - $user = Factory::getApplication()->getIdentity(); - $url = Route::_('index.php?option=' . $option . '&task=' . $view . '.' . $layout . '&id=' . $user->id, false); - } - - $this->redirect($url); - } - } - } - - /** - * Gets a configuration value. - * - * @param string $varname The name of the value to get. - * @param string $default Default value to return - * - * @return mixed The user state. - * - * @since 3.2 - * @deprecated 5.0 Use get() instead - */ - public function getCfg($varname, $default = null) - { - try - { - Log::add( - sprintf('%s() is deprecated and will be removed in 5.0. Use JFactory->getApplication()->get() instead.', __METHOD__), - Log::WARNING, - 'deprecated' - ); - } - catch (\RuntimeException $exception) - { - // Informational log only - } - - return $this->get($varname, $default); - } - - /** - * Gets the client id of the current running application. - * - * @return integer A client identifier. - * - * @since 3.2 - */ - public function getClientId() - { - return $this->clientId; - } - - /** - * Returns a reference to the global CmsApplication object, only creating it if it doesn't already exist. - * - * This method must be invoked as: $web = CmsApplication::getInstance(); - * - * @param string $name The name (optional) of the CmsApplication class to instantiate. - * @param string $prefix The class name prefix of the object. - * @param Container $container An optional dependency injection container to inject into the application. - * - * @return CmsApplication - * - * @since 3.2 - * @throws \RuntimeException - * @deprecated 5.0 Use \Joomla\CMS\Factory::getContainer()->get($name) instead - */ - public static function getInstance($name = null, $prefix = '\JApplication', Container $container = null) - { - if (empty(static::$instances[$name])) - { - // Create a CmsApplication object. - $classname = $prefix . ucfirst($name); - - if (!$container) - { - $container = Factory::getContainer(); - } - - if ($container->has($classname)) - { - static::$instances[$name] = $container->get($classname); - } - elseif (class_exists($classname)) - { - // @todo This creates an implicit hard requirement on the ApplicationCms constructor - static::$instances[$name] = new $classname(null, null, null, $container); - } - else - { - throw new \RuntimeException(Text::sprintf('JLIB_APPLICATION_ERROR_APPLICATION_LOAD', $name), 500); - } - - static::$instances[$name]->loadIdentity(Factory::getUser()); - } - - return static::$instances[$name]; - } - - /** - * Returns the application \JMenu object. - * - * @param string $name The name of the application/client. - * @param array $options An optional associative array of configuration settings. - * - * @return AbstractMenu - * - * @since 3.2 - */ - public function getMenu($name = null, $options = array()) - { - if (!isset($name)) - { - $name = $this->getName(); - } - - // Inject this application object into the \JMenu tree if one isn't already specified - if (!isset($options['app'])) - { - $options['app'] = $this; - } - - if (array_key_exists($name, $this->menus)) - { - return $this->menus[$name]; - } - - if ($this->menuFactory === null) - { - @trigger_error('Menu factory must be set in 5.0', E_USER_DEPRECATED); - $this->menuFactory = $this->getContainer()->get(MenuFactoryInterface::class); - } - - $this->menus[$name] = $this->menuFactory->createMenu($name, $options); - - // Make sure the abstract menu has the instance too, is needed for BC and will be removed with version 5 - AbstractMenu::$instances[$name] = $this->menus[$name]; - - return $this->menus[$name]; - } - - /** - * Get the system message queue. - * - * @param boolean $clear Clear the messages currently attached to the application object - * - * @return array The system message queue. - * - * @since 3.2 - */ - public function getMessageQueue($clear = false) - { - // For empty queue, if messages exists in the session, enqueue them. - if (!\count($this->messageQueue)) - { - $sessionQueue = $this->getSession()->get('application.queue', []); - - if ($sessionQueue) - { - $this->messageQueue = $sessionQueue; - $this->getSession()->set('application.queue', []); - } - } - - $messageQueue = $this->messageQueue; - - if ($clear) - { - $this->messageQueue = array(); - } - - return $messageQueue; - } - - /** - * Gets the name of the current running application. - * - * @return string The name of the application. - * - * @since 3.2 - */ - public function getName() - { - return $this->name; - } - - /** - * Returns the application Pathway object. - * - * @return Pathway - * - * @since 3.2 - */ - public function getPathway() - { - if (!$this->pathway) - { - $resourceName = ucfirst($this->getName()) . 'Pathway'; - - if (!$this->getContainer()->has($resourceName)) - { - throw new \RuntimeException( - Text::sprintf('JLIB_APPLICATION_ERROR_PATHWAY_LOAD', $this->getName()), - 500 - ); - } - - $this->pathway = $this->getContainer()->get($resourceName); - } - - return $this->pathway; - } - - /** - * Returns the application Router object. - * - * @param string $name The name of the application. - * @param array $options An optional associative array of configuration settings. - * - * @return Router - * - * @since 3.2 - * - * @deprecated 5.0 Inject the router or load it from the dependency injection container - */ - public static function getRouter($name = null, array $options = array()) - { - $app = Factory::getApplication(); - - if (!isset($name)) - { - $name = $app->getName(); - } - - $options['mode'] = $app->get('sef'); - - return Router::getInstance($name, $options); - } - - /** - * Gets the name of the current template. - * - * @param boolean $params An optional associative array of configuration settings - * - * @return mixed System is the fallback. - * - * @since 3.2 - */ - public function getTemplate($params = false) - { - if ($params) - { - $template = new \stdClass; - - $template->template = 'system'; - $template->params = new Registry; - $template->inheritable = 0; - $template->parent = ''; - - return $template; - } - - return 'system'; - } - - /** - * Gets a user state. - * - * @param string $key The path of the state. - * @param mixed $default Optional default value, returned if the internal value is null. - * - * @return mixed The user state or null. - * - * @since 3.2 - */ - public function getUserState($key, $default = null) - { - $registry = $this->getSession()->get('registry'); - - if ($registry !== null) - { - return $registry->get($key, $default); - } - - return $default; - } - - /** - * Gets the value of a user state variable. - * - * @param string $key The key of the user state variable. - * @param string $request The name of the variable passed in a request. - * @param string $default The default value for the variable if not found. Optional. - * @param string $type Filter for the variable, for valid values see {@link InputFilter::clean()}. Optional. - * - * @return mixed The request user state. - * - * @since 3.2 - */ - public function getUserStateFromRequest($key, $request, $default = null, $type = 'none') - { - $cur_state = $this->getUserState($key, $default); - $new_state = $this->input->get($request, null, $type); - - if ($new_state === null) - { - return $cur_state; - } - - // Save the new value only if it was set in this request. - $this->setUserState($key, $new_state); - - return $new_state; - } - - /** - * Initialise the application. - * - * @param array $options An optional associative array of configuration settings. - * - * @return void - * - * @since 3.2 - */ - protected function initialiseApp($options = array()) - { - // Check that we were given a language in the array (since by default may be blank). - if (isset($options['language'])) - { - $this->set('language', $options['language']); - } - - // Build our language object - $lang = Language::getInstance($this->get('language'), $this->get('debug_lang')); - - // Load the language to the API - $this->loadLanguage($lang); - - // Register the language object with Factory - Factory::$language = $this->getLanguage(); - - // Load the library language files - $this->loadLibraryLanguage(); - - // Set user specific editor. - $user = Factory::getUser(); - $editor = $user->getParam('editor', $this->get('editor')); - - if (!PluginHelper::isEnabled('editors', $editor)) - { - $editor = $this->get('editor'); - - if (!PluginHelper::isEnabled('editors', $editor)) - { - $editor = 'none'; - } - } - - $this->set('editor', $editor); - - // Load the behaviour plugins - PluginHelper::importPlugin('behaviour'); - - // Trigger the onAfterInitialise event. - PluginHelper::importPlugin('system'); - $this->triggerEvent('onAfterInitialise'); - } - - /** - * Checks if HTTPS is forced in the client configuration. - * - * @param integer $clientId An optional client id (defaults to current application client). - * - * @return boolean True if is forced for the client, false otherwise. - * - * @since 3.7.3 - */ - public function isHttpsForced($clientId = null) - { - $clientId = (int) ($clientId !== null ? $clientId : $this->getClientId()); - $forceSsl = (int) $this->get('force_ssl'); - - if ($clientId === 0 && $forceSsl === 2) - { - return true; - } - - if ($clientId === 1 && $forceSsl >= 1) - { - return true; - } - - return false; - } - - /** - * Check the client interface by name. - * - * @param string $identifier String identifier for the application interface - * - * @return boolean True if this application is of the given type client interface. - * - * @since 3.7.0 - */ - public function isClient($identifier) - { - return $this->getName() === $identifier; - } - - /** - * Load the library language files for the application - * - * @return void - * - * @since 3.6.3 - */ - protected function loadLibraryLanguage() - { - $this->getLanguage()->load('lib_joomla', JPATH_ADMINISTRATOR); - } - - /** - * Login authentication function. - * - * Username and encoded password are passed the onUserLogin event which - * is responsible for the user validation. A successful validation updates - * the current session record with the user's details. - * - * Username and encoded password are sent as credentials (along with other - * possibilities) to each observer (authentication plugin) for user - * validation. Successful validation will update the current session with - * the user details. - * - * @param array $credentials Array('username' => string, 'password' => string) - * @param array $options Array('remember' => boolean) - * - * @return boolean|\Exception True on success, false if failed or silent handling is configured, or a \Exception object on authentication error. - * - * @since 3.2 - */ - public function login($credentials, $options = array()) - { - // Get the global Authentication object. - $authenticate = Authentication::getInstance($this->authenticationPluginType); - $response = $authenticate->authenticate($credentials, $options); - - // Import the user plugin group. - PluginHelper::importPlugin('user'); - - if ($response->status === Authentication::STATUS_SUCCESS) - { - /* - * Validate that the user should be able to login (different to being authenticated). - * This permits authentication plugins blocking the user. - */ - $authorisations = $authenticate->authorise($response, $options); - $denied_states = Authentication::STATUS_EXPIRED | Authentication::STATUS_DENIED; - - foreach ($authorisations as $authorisation) - { - if ((int) $authorisation->status & $denied_states) - { - // Trigger onUserAuthorisationFailure Event. - $this->triggerEvent('onUserAuthorisationFailure', array((array) $authorisation)); - - // If silent is set, just return false. - if (isset($options['silent']) && $options['silent']) - { - return false; - } - - // Return the error. - switch ($authorisation->status) - { - case Authentication::STATUS_EXPIRED: - Factory::getApplication()->enqueueMessage(Text::_('JLIB_LOGIN_EXPIRED'), 'error'); - - return false; - - case Authentication::STATUS_DENIED: - Factory::getApplication()->enqueueMessage(Text::_('JLIB_LOGIN_DENIED'), 'error'); - - return false; - - default: - Factory::getApplication()->enqueueMessage(Text::_('JLIB_LOGIN_AUTHORISATION'), 'error'); - - return false; - } - } - } - - // OK, the credentials are authenticated and user is authorised. Let's fire the onLogin event. - $results = $this->triggerEvent('onUserLogin', array((array) $response, $options)); - - /* - * If any of the user plugins did not successfully complete the login routine - * then the whole method fails. - * - * Any errors raised should be done in the plugin as this provides the ability - * to provide much more information about why the routine may have failed. - */ - $user = Factory::getUser(); - - if ($response->type === 'Cookie') - { - $user->set('cookieLogin', true); - } - - if (\in_array(false, $results, true) == false) - { - $options['user'] = $user; - $options['responseType'] = $response->type; - - // The user is successfully logged in. Run the after login events - $this->triggerEvent('onUserAfterLogin', array($options)); - - return true; - } - } - - // Trigger onUserLoginFailure Event. - $this->triggerEvent('onUserLoginFailure', array((array) $response)); - - // If silent is set, just return false. - if (isset($options['silent']) && $options['silent']) - { - return false; - } - - // If status is success, any error will have been raised by the user plugin - if ($response->status !== Authentication::STATUS_SUCCESS) - { - $this->getLogger()->warning($response->error_message, array('category' => 'jerror')); - } - - return false; - } - - /** - * Logout authentication function. - * - * Passed the current user information to the onUserLogout event and reverts the current - * session record back to 'anonymous' parameters. - * If any of the authentication plugins did not successfully complete - * the logout routine then the whole method fails. Any errors raised - * should be done in the plugin as this provides the ability to give - * much more information about why the routine may have failed. - * - * @param integer $userid The user to load - Can be an integer or string - If string, it is converted to ID automatically - * @param array $options Array('clientid' => array of client id's) - * - * @return boolean True on success - * - * @since 3.2 - */ - public function logout($userid = null, $options = array()) - { - // Get a user object from the Application. - $user = Factory::getUser($userid); - - // Build the credentials array. - $parameters['username'] = $user->get('username'); - $parameters['id'] = $user->get('id'); - - // Set clientid in the options array if it hasn't been set already and shared sessions are not enabled. - if (!$this->get('shared_session', '0') && !isset($options['clientid'])) - { - $options['clientid'] = $this->getClientId(); - } - - // Import the user plugin group. - PluginHelper::importPlugin('user'); - - // OK, the credentials are built. Lets fire the onLogout event. - $results = $this->triggerEvent('onUserLogout', array($parameters, $options)); - - // Check if any of the plugins failed. If none did, success. - if (!\in_array(false, $results, true)) - { - $options['username'] = $user->get('username'); - $this->triggerEvent('onUserAfterLogout', array($options)); - - return true; - } - - // Trigger onUserLogoutFailure Event. - $this->triggerEvent('onUserLogoutFailure', array($parameters)); - - return false; - } - - /** - * Redirect to another URL. - * - * If the headers have not been sent the redirect will be accomplished using a "301 Moved Permanently" - * or "303 See Other" code in the header pointing to the new location. If the headers have already been - * sent this will be accomplished using a JavaScript statement. - * - * @param string $url The URL to redirect to. Can only be http/https URL - * @param integer $status The HTTP 1.1 status code to be provided. 303 is assumed by default. - * - * @return void - * - * @since 3.2 - */ - public function redirect($url, $status = 303) - { - // Persist messages if they exist. - if (\count($this->messageQueue)) - { - $this->getSession()->set('application.queue', $this->messageQueue); - } - - // Hand over processing to the parent now - parent::redirect($url, $status); - } - - /** - * Rendering is the process of pushing the document buffers into the template - * placeholders, retrieving data from the document and pushing it into - * the application response buffer. - * - * @return void - * - * @since 3.2 - */ - protected function render() - { - // Setup the document options. - $this->docOptions['template'] = $this->get('theme'); - $this->docOptions['file'] = $this->get('themeFile', 'index.php'); - $this->docOptions['params'] = $this->get('themeParams'); - $this->docOptions['csp_nonce'] = $this->get('csp_nonce'); - $this->docOptions['templateInherits'] = $this->get('themeInherits'); - - if ($this->get('themes.base')) - { - $this->docOptions['directory'] = $this->get('themes.base'); - } - // Fall back to constants. - else - { - $this->docOptions['directory'] = \defined('JPATH_THEMES') ? JPATH_THEMES : (\defined('JPATH_BASE') ? JPATH_BASE : __DIR__) . '/themes'; - } - - // Parse the document. - $this->document->parse($this->docOptions); - - // Trigger the onBeforeRender event. - PluginHelper::importPlugin('system'); - $this->triggerEvent('onBeforeRender'); - - $caching = false; - - if ($this->isClient('site') && $this->get('caching') && $this->get('caching', 2) == 2 && !Factory::getUser()->get('id')) - { - $caching = true; - } - - // Render the document. - $data = $this->document->render($caching, $this->docOptions); - - // Set the application output data. - $this->setBody($data); - - // Trigger the onAfterRender event. - $this->triggerEvent('onAfterRender'); - - // Mark afterRender in the profiler. - JDEBUG ? $this->profiler->mark('afterRender') : null; - } - - /** - * Route the application. - * - * Routing is the process of examining the request environment to determine which - * component should receive the request. The component optional parameters - * are then set in the request object to be processed when the application is being - * dispatched. - * - * @return void - * - * @since 3.2 - * - * @deprecated 5.0 Implement the route functionality in the extending class, this here will be removed without replacement - */ - protected function route() - { - // Get the full request URI. - $uri = clone Uri::getInstance(); - - $router = static::getRouter(); - $result = $router->parse($uri, true); - - $active = $this->getMenu()->getActive(); - - if ($active !== null - && $active->type === 'alias' - && $active->getParams()->get('alias_redirect') - && \in_array($this->input->getMethod(), array('GET', 'HEAD'), true)) - { - $item = $this->getMenu()->getItem($active->getParams()->get('aliasoptions')); - - if ($item !== null) - { - $oldUri = clone Uri::getInstance(); - - if ($oldUri->getVar('Itemid') == $active->id) - { - $oldUri->setVar('Itemid', $item->id); - } - - $base = Uri::base(true); - $oldPath = StringHelper::strtolower(substr($oldUri->getPath(), \strlen($base) + 1)); - $activePathPrefix = StringHelper::strtolower($active->route); - - $position = strpos($oldPath, $activePathPrefix); - - if ($position !== false) - { - $oldUri->setPath($base . '/' . substr_replace($oldPath, $item->route, $position, \strlen($activePathPrefix))); - - $this->setHeader('Expires', 'Wed, 17 Aug 2005 00:00:00 GMT', true); - $this->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT', true); - $this->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate', false); - $this->sendHeaders(); - - $this->redirect((string) $oldUri, 301); - } - } - } - - foreach ($result as $key => $value) - { - $this->input->def($key, $value); - } - - // Trigger the onAfterRoute event. - PluginHelper::importPlugin('system'); - $this->triggerEvent('onAfterRoute'); - } - - /** - * Sets the value of a user state variable. - * - * @param string $key The path of the state. - * @param mixed $value The value of the variable. - * - * @return mixed|void The previous state, if one existed. - * - * @since 3.2 - */ - public function setUserState($key, $value) - { - $session = $this->getSession(); - $registry = $session->get('registry'); - - if ($registry !== null) - { - return $registry->set($key, $value); - } - } - - /** - * Sends all headers prior to returning the string - * - * @param boolean $compress If true, compress the data - * - * @return string - * - * @since 3.2 - */ - public function toString($compress = false) - { - // Don't compress something if the server is going to do it anyway. Waste of time. - if ($compress && !ini_get('zlib.output_compression') && ini_get('output_handler') !== 'ob_gzhandler') - { - $this->compress(); - } - - if ($this->allowCache() === false) - { - $this->setHeader('Cache-Control', 'no-cache', false); - } - - $this->sendHeaders(); - - return $this->getBody(); - } - - /** - * Method to determine a hash for anti-spoofing variable names - * - * @param boolean $forceNew If true, force a new token to be created - * - * @return string Hashed var name - * - * @since 4.0.0 - */ - public function getFormToken($forceNew = false) - { - /** @var Session $session */ - $session = $this->getSession(); - - return $session->getFormToken($forceNew); - } - - /** - * Checks for a form token in the request. - * - * Use in conjunction with getFormToken. - * - * @param string $method The request method in which to look for the token key. - * - * @return boolean True if found and valid, false otherwise. - * - * @since 4.0.0 - */ - public function checkToken($method = 'post') - { - /** @var Session $session */ - $session = $this->getSession(); - - return $session->checkToken($method); - } - - /** - * Flag if the application instance is a CLI or web based application. - * - * Helper function, you should use the native PHP functions to detect if it is a CLI application. - * - * @return boolean - * - * @since 4.0.0 - * @deprecated 5.0 Will be removed without replacements - */ - public function isCli() - { - return false; - } - - /** - * No longer used - * - * @return boolean - * - * @since 4.0.0 - * - * @throws \Exception - * @deprecated 4.2.0 Will be removed in 5.0 without replacement. - */ - protected function isTwoFactorAuthenticationRequired(): bool - { - return false; - } - - /** - * No longer used - * - * @return boolean - * - * @since 4.0.0 - * - * @throws \Exception - * @deprecated 4.2.0 Will be removed in 5.0 without replacement. - */ - private function hasUserConfiguredTwoFactorAuthentication(): bool - { - return false; - } - - /** - * Setup logging functionality. - * - * @return void - * - * @since 4.0.0 - */ - private function setupLogging(): void - { - // Add InMemory logger that will collect all log entries to allow to display them later by extensions - if ($this->get('debug')) - { - Log::addLogger(['logger' => 'inmemory']); - } - - // Log the deprecated API. - if ($this->get('log_deprecated')) - { - Log::addLogger(['text_file' => 'deprecated.php'], Log::ALL, ['deprecated']); - } - - // We only log errors unless Site Debug is enabled - $logLevels = Log::ERROR | Log::CRITICAL | Log::ALERT | Log::EMERGENCY; - - if ($this->get('debug')) - { - $logLevels = Log::ALL; - } - - Log::addLogger(['text_file' => 'joomla_core_errors.php'], $logLevels, ['system']); - - // Log everything (except deprecated APIs, these are logged separately with the option above). - if ($this->get('log_everything')) - { - Log::addLogger(['text_file' => 'everything.php'], Log::ALL, ['deprecated', 'deprecation-notes', 'databasequery'], true); - } - - if ($this->get('log_categories')) - { - $priority = 0; - - foreach ($this->get('log_priorities', ['all']) as $p) - { - $const = '\\Joomla\\CMS\\Log\\Log::' . strtoupper($p); - - if (defined($const)) - { - $priority |= constant($const); - } - } - - // Split into an array at any character other than alphabet, numbers, _, ., or - - $categories = preg_split('/[^\w.-]+/', $this->get('log_categories', ''), -1, PREG_SPLIT_NO_EMPTY); - $mode = (bool) $this->get('log_category_mode', false); - - if (!$categories) - { - return; - } - - Log::addLogger(['text_file' => 'custom-logging.php'], $priority, $categories, $mode); - } - } - - /** - * Sets the internal menu factory. - * - * @param MenuFactoryInterface $menuFactory The menu factory - * - * @return void - * - * @since 4.2.0 - */ - public function setMenuFactory(MenuFactoryInterface $menuFactory): void - { - $this->menuFactory = $menuFactory; - } + use ContainerAwareTrait; + use ExtensionManagerTrait; + use ExtensionNamespaceMapper; + use SessionAwareWebApplicationTrait; + + /** + * Array of options for the \JDocument object + * + * @var array + * @since 3.2 + */ + protected $docOptions = array(); + + /** + * Application instances container. + * + * @var CmsApplication[] + * @since 3.2 + */ + protected static $instances = array(); + + /** + * The scope of the application. + * + * @var string + * @since 3.2 + */ + public $scope = null; + + /** + * The client identifier. + * + * @var integer + * @since 4.0.0 + */ + protected $clientId = null; + + /** + * The application message queue. + * + * @var array + * @since 4.0.0 + */ + protected $messageQueue = array(); + + /** + * The name of the application. + * + * @var string + * @since 4.0.0 + */ + protected $name = null; + + /** + * The profiler instance + * + * @var Profiler + * @since 3.2 + */ + protected $profiler = null; + + /** + * Currently active template + * + * @var object + * @since 3.2 + */ + protected $template = null; + + /** + * The pathway object + * + * @var Pathway + * @since 4.0.0 + */ + protected $pathway = null; + + /** + * The authentication plugin type + * + * @var string + * @since 4.0.0 + */ + protected $authenticationPluginType = 'authentication'; + + /** + * Menu instances container. + * + * @var AbstractMenu[] + * @since 4.2.0 + */ + protected $menus = []; + + /** + * The menu factory + * + * @var MenuFactoryInterface + * + * @since 4.2.0 + */ + private $menuFactory; + + /** + * Class constructor. + * + * @param Input $input An optional argument to provide dependency injection for the application's input + * object. If the argument is a JInput object that object will become the + * application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's config + * object. If the argument is a Registry object that object will become the + * application's config object, otherwise a default config object is created. + * @param WebClient $client An optional argument to provide dependency injection for the application's client + * object. If the argument is a WebClient object that object will become the + * application's client object, otherwise a default client object is created. + * @param Container $container Dependency injection container. + * + * @since 3.2 + */ + public function __construct(Input $input = null, Registry $config = null, WebClient $client = null, Container $container = null) + { + $container = $container ?: new Container(); + $this->setContainer($container); + + parent::__construct($input, $config, $client); + + // If JDEBUG is defined, load the profiler instance + if (\defined('JDEBUG') && JDEBUG) { + $this->profiler = Profiler::getInstance('Application'); + } + + // Enable sessions by default. + if ($this->config->get('session') === null) { + $this->config->set('session', true); + } + + // Set the session default name. + if ($this->config->get('session_name') === null) { + $this->config->set('session_name', $this->getName()); + } + } + + /** + * Checks the user session. + * + * If the session record doesn't exist, initialise it. + * If session is new, create session variables + * + * @return void + * + * @since 3.2 + * @throws \RuntimeException + */ + public function checkSession() + { + $this->getContainer()->get(MetadataManager::class)->createOrUpdateRecord($this->getSession(), $this->getIdentity()); + } + + /** + * Enqueue a system message. + * + * @param string $msg The message to enqueue. + * @param string $type The message type. Default is message. + * + * @return void + * + * @since 3.2 + */ + public function enqueueMessage($msg, $type = self::MSG_INFO) + { + // Don't add empty messages. + if ($msg === null || trim($msg) === '') { + return; + } + + $inputFilter = InputFilter::getInstance( + [], + [], + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ); + + // Build the message array and apply the HTML InputFilter with the default blacklist to the message + $message = array( + 'message' => $inputFilter->clean($msg, 'html'), + 'type' => $inputFilter->clean(strtolower($type), 'cmd'), + ); + + // For empty queue, if messages exists in the session, enqueue them first. + $messages = $this->getMessageQueue(); + + if (!\in_array($message, $this->messageQueue)) { + // Enqueue the message. + $this->messageQueue[] = $message; + } + } + + /** + * Ensure several core system input variables are not arrays. + * + * @return void + * + * @since 3.9 + */ + private function sanityCheckSystemVariables() + { + $input = $this->input; + + // Get invalid input variables + $invalidInputVariables = array_filter( + array('option', 'view', 'format', 'lang', 'Itemid', 'template', 'templateStyle', 'task'), + function ($systemVariable) use ($input) { + return $input->exists($systemVariable) && is_array($input->getRaw($systemVariable)); + } + ); + + // Unset invalid system variables + foreach ($invalidInputVariables as $systemVariable) { + $input->set($systemVariable, null); + } + + // Abort when there are invalid variables + if ($invalidInputVariables) { + throw new \RuntimeException('Invalid input, aborting application.'); + } + } + + /** + * Execute the application. + * + * @return void + * + * @since 3.2 + */ + public function execute() + { + try { + $this->sanityCheckSystemVariables(); + $this->setupLogging(); + $this->createExtensionNamespaceMap(); + + // Perform application routines. + $this->doExecute(); + + // If we have an application document object, render it. + if ($this->document instanceof \Joomla\CMS\Document\Document) { + // Render the application output. + $this->render(); + } + + // If gzip compression is enabled in configuration and the server is compliant, compress the output. + if ($this->get('gzip') && !ini_get('zlib.output_compression') && ini_get('output_handler') !== 'ob_gzhandler') { + $this->compress(); + + // Trigger the onAfterCompress event. + $this->triggerEvent('onAfterCompress'); + } + } catch (\Throwable $throwable) { + /** @var ErrorEvent $event */ + $event = AbstractEvent::create( + 'onError', + [ + 'subject' => $throwable, + 'eventClass' => ErrorEvent::class, + 'application' => $this, + ] + ); + + // Trigger the onError event. + $this->triggerEvent('onError', $event); + + ExceptionHandler::handleException($event->getError()); + } + + // Trigger the onBeforeRespond event. + $this->getDispatcher()->dispatch('onBeforeRespond'); + + // Send the application response. + $this->respond(); + + // Trigger the onAfterRespond event. + $this->getDispatcher()->dispatch('onAfterRespond'); + } + + /** + * Check if the user is required to reset their password. + * + * If the user is required to reset their password will be redirected to the page that manage the password reset. + * + * @param string $option The option that manage the password reset + * @param string $view The view that manage the password reset + * @param string $layout The layout of the view that manage the password reset + * @param string $tasks Permitted tasks + * + * @return void + * + * @throws \Exception + */ + protected function checkUserRequireReset($option, $view, $layout, $tasks) + { + if (Factory::getUser()->get('requireReset', 0)) { + $redirect = false; + + /* + * By default user profile edit page is used. + * That page allows you to change more than just the password and might not be the desired behavior. + * This allows a developer to override the page that manage the password reset. + * (can be configured using the file: configuration.php, or if extended, through the global configuration form) + */ + $name = $this->getName(); + + if ($this->get($name . '_reset_password_override', 0)) { + $option = $this->get($name . '_reset_password_option', ''); + $view = $this->get($name . '_reset_password_view', ''); + $layout = $this->get($name . '_reset_password_layout', ''); + $tasks = $this->get($name . '_reset_password_tasks', ''); + } + + $task = $this->input->getCmd('task', ''); + + // Check task or option/view/layout + if (!empty($task)) { + $tasks = explode(',', $tasks); + + // Check full task version "option/task" + if (array_search($this->input->getCmd('option', '') . '/' . $task, $tasks) === false) { + // Check short task version, must be on the same option of the view + if ($this->input->getCmd('option', '') !== $option || array_search($task, $tasks) === false) { + // Not permitted task + $redirect = true; + } + } + } else { + if ( + $this->input->getCmd('option', '') !== $option || $this->input->getCmd('view', '') !== $view + || $this->input->getCmd('layout', '') !== $layout + ) { + // Requested a different option/view/layout + $redirect = true; + } + } + + if ($redirect) { + // Redirect to the profile edit page + $this->enqueueMessage(Text::_('JGLOBAL_PASSWORD_RESET_REQUIRED'), 'notice'); + + $url = Route::_('index.php?option=' . $option . '&view=' . $view . '&layout=' . $layout, false); + + // In the administrator we need a different URL + if (strtolower($name) === 'administrator') { + $user = Factory::getApplication()->getIdentity(); + $url = Route::_('index.php?option=' . $option . '&task=' . $view . '.' . $layout . '&id=' . $user->id, false); + } + + $this->redirect($url); + } + } + } + + /** + * Gets a configuration value. + * + * @param string $varname The name of the value to get. + * @param string $default Default value to return + * + * @return mixed The user state. + * + * @since 3.2 + * @deprecated 5.0 Use get() instead + */ + public function getCfg($varname, $default = null) + { + try { + Log::add( + sprintf('%s() is deprecated and will be removed in 5.0. Use JFactory->getApplication()->get() instead.', __METHOD__), + Log::WARNING, + 'deprecated' + ); + } catch (\RuntimeException $exception) { + // Informational log only + } + + return $this->get($varname, $default); + } + + /** + * Gets the client id of the current running application. + * + * @return integer A client identifier. + * + * @since 3.2 + */ + public function getClientId() + { + return $this->clientId; + } + + /** + * Returns a reference to the global CmsApplication object, only creating it if it doesn't already exist. + * + * This method must be invoked as: $web = CmsApplication::getInstance(); + * + * @param string $name The name (optional) of the CmsApplication class to instantiate. + * @param string $prefix The class name prefix of the object. + * @param Container $container An optional dependency injection container to inject into the application. + * + * @return CmsApplication + * + * @since 3.2 + * @throws \RuntimeException + * @deprecated 5.0 Use \Joomla\CMS\Factory::getContainer()->get($name) instead + */ + public static function getInstance($name = null, $prefix = '\JApplication', Container $container = null) + { + if (empty(static::$instances[$name])) { + // Create a CmsApplication object. + $classname = $prefix . ucfirst($name); + + if (!$container) { + $container = Factory::getContainer(); + } + + if ($container->has($classname)) { + static::$instances[$name] = $container->get($classname); + } elseif (class_exists($classname)) { + // @todo This creates an implicit hard requirement on the ApplicationCms constructor + static::$instances[$name] = new $classname(null, null, null, $container); + } else { + throw new \RuntimeException(Text::sprintf('JLIB_APPLICATION_ERROR_APPLICATION_LOAD', $name), 500); + } + + static::$instances[$name]->loadIdentity(Factory::getUser()); + } + + return static::$instances[$name]; + } + + /** + * Returns the application \JMenu object. + * + * @param string $name The name of the application/client. + * @param array $options An optional associative array of configuration settings. + * + * @return AbstractMenu + * + * @since 3.2 + */ + public function getMenu($name = null, $options = array()) + { + if (!isset($name)) { + $name = $this->getName(); + } + + // Inject this application object into the \JMenu tree if one isn't already specified + if (!isset($options['app'])) { + $options['app'] = $this; + } + + if (array_key_exists($name, $this->menus)) { + return $this->menus[$name]; + } + + if ($this->menuFactory === null) { + @trigger_error('Menu factory must be set in 5.0', E_USER_DEPRECATED); + $this->menuFactory = $this->getContainer()->get(MenuFactoryInterface::class); + } + + $this->menus[$name] = $this->menuFactory->createMenu($name, $options); + + // Make sure the abstract menu has the instance too, is needed for BC and will be removed with version 5 + AbstractMenu::$instances[$name] = $this->menus[$name]; + + return $this->menus[$name]; + } + + /** + * Get the system message queue. + * + * @param boolean $clear Clear the messages currently attached to the application object + * + * @return array The system message queue. + * + * @since 3.2 + */ + public function getMessageQueue($clear = false) + { + // For empty queue, if messages exists in the session, enqueue them. + if (!\count($this->messageQueue)) { + $sessionQueue = $this->getSession()->get('application.queue', []); + + if ($sessionQueue) { + $this->messageQueue = $sessionQueue; + $this->getSession()->set('application.queue', []); + } + } + + $messageQueue = $this->messageQueue; + + if ($clear) { + $this->messageQueue = array(); + } + + return $messageQueue; + } + + /** + * Gets the name of the current running application. + * + * @return string The name of the application. + * + * @since 3.2 + */ + public function getName() + { + return $this->name; + } + + /** + * Returns the application Pathway object. + * + * @return Pathway + * + * @since 3.2 + */ + public function getPathway() + { + if (!$this->pathway) { + $resourceName = ucfirst($this->getName()) . 'Pathway'; + + if (!$this->getContainer()->has($resourceName)) { + throw new \RuntimeException( + Text::sprintf('JLIB_APPLICATION_ERROR_PATHWAY_LOAD', $this->getName()), + 500 + ); + } + + $this->pathway = $this->getContainer()->get($resourceName); + } + + return $this->pathway; + } + + /** + * Returns the application Router object. + * + * @param string $name The name of the application. + * @param array $options An optional associative array of configuration settings. + * + * @return Router + * + * @since 3.2 + * + * @deprecated 5.0 Inject the router or load it from the dependency injection container + */ + public static function getRouter($name = null, array $options = array()) + { + $app = Factory::getApplication(); + + if (!isset($name)) { + $name = $app->getName(); + } + + $options['mode'] = $app->get('sef'); + + return Router::getInstance($name, $options); + } + + /** + * Gets the name of the current template. + * + * @param boolean $params An optional associative array of configuration settings + * + * @return mixed System is the fallback. + * + * @since 3.2 + */ + public function getTemplate($params = false) + { + if ($params) { + $template = new \stdClass(); + + $template->template = 'system'; + $template->params = new Registry(); + $template->inheritable = 0; + $template->parent = ''; + + return $template; + } + + return 'system'; + } + + /** + * Gets a user state. + * + * @param string $key The path of the state. + * @param mixed $default Optional default value, returned if the internal value is null. + * + * @return mixed The user state or null. + * + * @since 3.2 + */ + public function getUserState($key, $default = null) + { + $registry = $this->getSession()->get('registry'); + + if ($registry !== null) { + return $registry->get($key, $default); + } + + return $default; + } + + /** + * Gets the value of a user state variable. + * + * @param string $key The key of the user state variable. + * @param string $request The name of the variable passed in a request. + * @param string $default The default value for the variable if not found. Optional. + * @param string $type Filter for the variable, for valid values see {@link InputFilter::clean()}. Optional. + * + * @return mixed The request user state. + * + * @since 3.2 + */ + public function getUserStateFromRequest($key, $request, $default = null, $type = 'none') + { + $cur_state = $this->getUserState($key, $default); + $new_state = $this->input->get($request, null, $type); + + if ($new_state === null) { + return $cur_state; + } + + // Save the new value only if it was set in this request. + $this->setUserState($key, $new_state); + + return $new_state; + } + + /** + * Initialise the application. + * + * @param array $options An optional associative array of configuration settings. + * + * @return void + * + * @since 3.2 + */ + protected function initialiseApp($options = array()) + { + // Check that we were given a language in the array (since by default may be blank). + if (isset($options['language'])) { + $this->set('language', $options['language']); + } + + // Build our language object + $lang = Language::getInstance($this->get('language'), $this->get('debug_lang')); + + // Load the language to the API + $this->loadLanguage($lang); + + // Register the language object with Factory + Factory::$language = $this->getLanguage(); + + // Load the library language files + $this->loadLibraryLanguage(); + + // Set user specific editor. + $user = Factory::getUser(); + $editor = $user->getParam('editor', $this->get('editor')); + + if (!PluginHelper::isEnabled('editors', $editor)) { + $editor = $this->get('editor'); + + if (!PluginHelper::isEnabled('editors', $editor)) { + $editor = 'none'; + } + } + + $this->set('editor', $editor); + + // Load the behaviour plugins + PluginHelper::importPlugin('behaviour'); + + // Trigger the onAfterInitialise event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onAfterInitialise'); + } + + /** + * Checks if HTTPS is forced in the client configuration. + * + * @param integer $clientId An optional client id (defaults to current application client). + * + * @return boolean True if is forced for the client, false otherwise. + * + * @since 3.7.3 + */ + public function isHttpsForced($clientId = null) + { + $clientId = (int) ($clientId !== null ? $clientId : $this->getClientId()); + $forceSsl = (int) $this->get('force_ssl'); + + if ($clientId === 0 && $forceSsl === 2) { + return true; + } + + if ($clientId === 1 && $forceSsl >= 1) { + return true; + } + + return false; + } + + /** + * Check the client interface by name. + * + * @param string $identifier String identifier for the application interface + * + * @return boolean True if this application is of the given type client interface. + * + * @since 3.7.0 + */ + public function isClient($identifier) + { + return $this->getName() === $identifier; + } + + /** + * Load the library language files for the application + * + * @return void + * + * @since 3.6.3 + */ + protected function loadLibraryLanguage() + { + $this->getLanguage()->load('lib_joomla', JPATH_ADMINISTRATOR); + } + + /** + * Login authentication function. + * + * Username and encoded password are passed the onUserLogin event which + * is responsible for the user validation. A successful validation updates + * the current session record with the user's details. + * + * Username and encoded password are sent as credentials (along with other + * possibilities) to each observer (authentication plugin) for user + * validation. Successful validation will update the current session with + * the user details. + * + * @param array $credentials Array('username' => string, 'password' => string) + * @param array $options Array('remember' => boolean) + * + * @return boolean|\Exception True on success, false if failed or silent handling is configured, or a \Exception object on authentication error. + * + * @since 3.2 + */ + public function login($credentials, $options = array()) + { + // Get the global Authentication object. + $authenticate = Authentication::getInstance($this->authenticationPluginType); + $response = $authenticate->authenticate($credentials, $options); + + // Import the user plugin group. + PluginHelper::importPlugin('user'); + + if ($response->status === Authentication::STATUS_SUCCESS) { + /* + * Validate that the user should be able to login (different to being authenticated). + * This permits authentication plugins blocking the user. + */ + $authorisations = $authenticate->authorise($response, $options); + $denied_states = Authentication::STATUS_EXPIRED | Authentication::STATUS_DENIED; + + foreach ($authorisations as $authorisation) { + if ((int) $authorisation->status & $denied_states) { + // Trigger onUserAuthorisationFailure Event. + $this->triggerEvent('onUserAuthorisationFailure', array((array) $authorisation)); + + // If silent is set, just return false. + if (isset($options['silent']) && $options['silent']) { + return false; + } + + // Return the error. + switch ($authorisation->status) { + case Authentication::STATUS_EXPIRED: + Factory::getApplication()->enqueueMessage(Text::_('JLIB_LOGIN_EXPIRED'), 'error'); + + return false; + + case Authentication::STATUS_DENIED: + Factory::getApplication()->enqueueMessage(Text::_('JLIB_LOGIN_DENIED'), 'error'); + + return false; + + default: + Factory::getApplication()->enqueueMessage(Text::_('JLIB_LOGIN_AUTHORISATION'), 'error'); + + return false; + } + } + } + + // OK, the credentials are authenticated and user is authorised. Let's fire the onLogin event. + $results = $this->triggerEvent('onUserLogin', array((array) $response, $options)); + + /* + * If any of the user plugins did not successfully complete the login routine + * then the whole method fails. + * + * Any errors raised should be done in the plugin as this provides the ability + * to provide much more information about why the routine may have failed. + */ + $user = Factory::getUser(); + + if ($response->type === 'Cookie') { + $user->set('cookieLogin', true); + } + + if (\in_array(false, $results, true) == false) { + $options['user'] = $user; + $options['responseType'] = $response->type; + + // The user is successfully logged in. Run the after login events + $this->triggerEvent('onUserAfterLogin', array($options)); + + return true; + } + } + + // Trigger onUserLoginFailure Event. + $this->triggerEvent('onUserLoginFailure', array((array) $response)); + + // If silent is set, just return false. + if (isset($options['silent']) && $options['silent']) { + return false; + } + + // If status is success, any error will have been raised by the user plugin + if ($response->status !== Authentication::STATUS_SUCCESS) { + $this->getLogger()->warning($response->error_message, array('category' => 'jerror')); + } + + return false; + } + + /** + * Logout authentication function. + * + * Passed the current user information to the onUserLogout event and reverts the current + * session record back to 'anonymous' parameters. + * If any of the authentication plugins did not successfully complete + * the logout routine then the whole method fails. Any errors raised + * should be done in the plugin as this provides the ability to give + * much more information about why the routine may have failed. + * + * @param integer $userid The user to load - Can be an integer or string - If string, it is converted to ID automatically + * @param array $options Array('clientid' => array of client id's) + * + * @return boolean True on success + * + * @since 3.2 + */ + public function logout($userid = null, $options = array()) + { + // Get a user object from the Application. + $user = Factory::getUser($userid); + + // Build the credentials array. + $parameters['username'] = $user->get('username'); + $parameters['id'] = $user->get('id'); + + // Set clientid in the options array if it hasn't been set already and shared sessions are not enabled. + if (!$this->get('shared_session', '0') && !isset($options['clientid'])) { + $options['clientid'] = $this->getClientId(); + } + + // Import the user plugin group. + PluginHelper::importPlugin('user'); + + // OK, the credentials are built. Lets fire the onLogout event. + $results = $this->triggerEvent('onUserLogout', array($parameters, $options)); + + // Check if any of the plugins failed. If none did, success. + if (!\in_array(false, $results, true)) { + $options['username'] = $user->get('username'); + $this->triggerEvent('onUserAfterLogout', array($options)); + + return true; + } + + // Trigger onUserLogoutFailure Event. + $this->triggerEvent('onUserLogoutFailure', array($parameters)); + + return false; + } + + /** + * Redirect to another URL. + * + * If the headers have not been sent the redirect will be accomplished using a "301 Moved Permanently" + * or "303 See Other" code in the header pointing to the new location. If the headers have already been + * sent this will be accomplished using a JavaScript statement. + * + * @param string $url The URL to redirect to. Can only be http/https URL + * @param integer $status The HTTP 1.1 status code to be provided. 303 is assumed by default. + * + * @return void + * + * @since 3.2 + */ + public function redirect($url, $status = 303) + { + // Persist messages if they exist. + if (\count($this->messageQueue)) { + $this->getSession()->set('application.queue', $this->messageQueue); + } + + // Hand over processing to the parent now + parent::redirect($url, $status); + } + + /** + * Rendering is the process of pushing the document buffers into the template + * placeholders, retrieving data from the document and pushing it into + * the application response buffer. + * + * @return void + * + * @since 3.2 + */ + protected function render() + { + // Setup the document options. + $this->docOptions['template'] = $this->get('theme'); + $this->docOptions['file'] = $this->get('themeFile', 'index.php'); + $this->docOptions['params'] = $this->get('themeParams'); + $this->docOptions['csp_nonce'] = $this->get('csp_nonce'); + $this->docOptions['templateInherits'] = $this->get('themeInherits'); + + if ($this->get('themes.base')) { + $this->docOptions['directory'] = $this->get('themes.base'); + } else { + // Fall back to constants. + $this->docOptions['directory'] = \defined('JPATH_THEMES') ? JPATH_THEMES : (\defined('JPATH_BASE') ? JPATH_BASE : __DIR__) . '/themes'; + } + + // Parse the document. + $this->document->parse($this->docOptions); + + // Trigger the onBeforeRender event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onBeforeRender'); + + $caching = false; + + if ($this->isClient('site') && $this->get('caching') && $this->get('caching', 2) == 2 && !Factory::getUser()->get('id')) { + $caching = true; + } + + // Render the document. + $data = $this->document->render($caching, $this->docOptions); + + // Set the application output data. + $this->setBody($data); + + // Trigger the onAfterRender event. + $this->triggerEvent('onAfterRender'); + + // Mark afterRender in the profiler. + JDEBUG ? $this->profiler->mark('afterRender') : null; + } + + /** + * Route the application. + * + * Routing is the process of examining the request environment to determine which + * component should receive the request. The component optional parameters + * are then set in the request object to be processed when the application is being + * dispatched. + * + * @return void + * + * @since 3.2 + * + * @deprecated 5.0 Implement the route functionality in the extending class, this here will be removed without replacement + */ + protected function route() + { + // Get the full request URI. + $uri = clone Uri::getInstance(); + + $router = static::getRouter(); + $result = $router->parse($uri, true); + + $active = $this->getMenu()->getActive(); + + if ( + $active !== null + && $active->type === 'alias' + && $active->getParams()->get('alias_redirect') + && \in_array($this->input->getMethod(), array('GET', 'HEAD'), true) + ) { + $item = $this->getMenu()->getItem($active->getParams()->get('aliasoptions')); + + if ($item !== null) { + $oldUri = clone Uri::getInstance(); + + if ($oldUri->getVar('Itemid') == $active->id) { + $oldUri->setVar('Itemid', $item->id); + } + + $base = Uri::base(true); + $oldPath = StringHelper::strtolower(substr($oldUri->getPath(), \strlen($base) + 1)); + $activePathPrefix = StringHelper::strtolower($active->route); + + $position = strpos($oldPath, $activePathPrefix); + + if ($position !== false) { + $oldUri->setPath($base . '/' . substr_replace($oldPath, $item->route, $position, \strlen($activePathPrefix))); + + $this->setHeader('Expires', 'Wed, 17 Aug 2005 00:00:00 GMT', true); + $this->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT', true); + $this->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate', false); + $this->sendHeaders(); + + $this->redirect((string) $oldUri, 301); + } + } + } + + foreach ($result as $key => $value) { + $this->input->def($key, $value); + } + + // Trigger the onAfterRoute event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onAfterRoute'); + } + + /** + * Sets the value of a user state variable. + * + * @param string $key The path of the state. + * @param mixed $value The value of the variable. + * + * @return mixed|void The previous state, if one existed. + * + * @since 3.2 + */ + public function setUserState($key, $value) + { + $session = $this->getSession(); + $registry = $session->get('registry'); + + if ($registry !== null) { + return $registry->set($key, $value); + } + } + + /** + * Sends all headers prior to returning the string + * + * @param boolean $compress If true, compress the data + * + * @return string + * + * @since 3.2 + */ + public function toString($compress = false) + { + // Don't compress something if the server is going to do it anyway. Waste of time. + if ($compress && !ini_get('zlib.output_compression') && ini_get('output_handler') !== 'ob_gzhandler') { + $this->compress(); + } + + if ($this->allowCache() === false) { + $this->setHeader('Cache-Control', 'no-cache', false); + } + + $this->sendHeaders(); + + return $this->getBody(); + } + + /** + * Method to determine a hash for anti-spoofing variable names + * + * @param boolean $forceNew If true, force a new token to be created + * + * @return string Hashed var name + * + * @since 4.0.0 + */ + public function getFormToken($forceNew = false) + { + /** @var Session $session */ + $session = $this->getSession(); + + return $session->getFormToken($forceNew); + } + + /** + * Checks for a form token in the request. + * + * Use in conjunction with getFormToken. + * + * @param string $method The request method in which to look for the token key. + * + * @return boolean True if found and valid, false otherwise. + * + * @since 4.0.0 + */ + public function checkToken($method = 'post') + { + /** @var Session $session */ + $session = $this->getSession(); + + return $session->checkToken($method); + } + + /** + * Flag if the application instance is a CLI or web based application. + * + * Helper function, you should use the native PHP functions to detect if it is a CLI application. + * + * @return boolean + * + * @since 4.0.0 + * @deprecated 5.0 Will be removed without replacements + */ + public function isCli() + { + return false; + } + + /** + * No longer used + * + * @return boolean + * + * @since 4.0.0 + * + * @throws \Exception + * @deprecated 4.2.0 Will be removed in 5.0 without replacement. + */ + protected function isTwoFactorAuthenticationRequired(): bool + { + return false; + } + + /** + * No longer used + * + * @return boolean + * + * @since 4.0.0 + * + * @throws \Exception + * @deprecated 4.2.0 Will be removed in 5.0 without replacement. + */ + private function hasUserConfiguredTwoFactorAuthentication(): bool + { + return false; + } + + /** + * Setup logging functionality. + * + * @return void + * + * @since 4.0.0 + */ + private function setupLogging(): void + { + // Add InMemory logger that will collect all log entries to allow to display them later by extensions + if ($this->get('debug')) { + Log::addLogger(['logger' => 'inmemory']); + } + + // Log the deprecated API. + if ($this->get('log_deprecated')) { + Log::addLogger(['text_file' => 'deprecated.php'], Log::ALL, ['deprecated']); + } + + // We only log errors unless Site Debug is enabled + $logLevels = Log::ERROR | Log::CRITICAL | Log::ALERT | Log::EMERGENCY; + + if ($this->get('debug')) { + $logLevels = Log::ALL; + } + + Log::addLogger(['text_file' => 'joomla_core_errors.php'], $logLevels, ['system']); + + // Log everything (except deprecated APIs, these are logged separately with the option above). + if ($this->get('log_everything')) { + Log::addLogger(['text_file' => 'everything.php'], Log::ALL, ['deprecated', 'deprecation-notes', 'databasequery'], true); + } + + if ($this->get('log_categories')) { + $priority = 0; + + foreach ($this->get('log_priorities', ['all']) as $p) { + $const = '\\Joomla\\CMS\\Log\\Log::' . strtoupper($p); + + if (defined($const)) { + $priority |= constant($const); + } + } + + // Split into an array at any character other than alphabet, numbers, _, ., or - + $categories = preg_split('/[^\w.-]+/', $this->get('log_categories', ''), -1, PREG_SPLIT_NO_EMPTY); + $mode = (bool) $this->get('log_category_mode', false); + + if (!$categories) { + return; + } + + Log::addLogger(['text_file' => 'custom-logging.php'], $priority, $categories, $mode); + } + } + + /** + * Sets the internal menu factory. + * + * @param MenuFactoryInterface $menuFactory The menu factory + * + * @return void + * + * @since 4.2.0 + */ + public function setMenuFactory(MenuFactoryInterface $menuFactory): void + { + $this->menuFactory = $menuFactory; + } } diff --git a/libraries/src/Application/CMSApplicationInterface.php b/libraries/src/Application/CMSApplicationInterface.php index ebab368d8596e..aac6a45f83468 100644 --- a/libraries/src/Application/CMSApplicationInterface.php +++ b/libraries/src/Application/CMSApplicationInterface.php @@ -1,4 +1,5 @@ close(); - } - - $container = $container ?: Factory::getContainer(); - $this->setContainer($container); - $this->setDispatcher($dispatcher ?: $container->get(\Joomla\Event\DispatcherInterface::class)); - - if (!$container->has('session')) - { - $container->alias('session', 'session.cli') - ->alias('JSession', 'session.cli') - ->alias(\Joomla\CMS\Session\Session::class, 'session.cli') - ->alias(\Joomla\Session\Session::class, 'session.cli') - ->alias(\Joomla\Session\SessionInterface::class, 'session.cli'); - } - - $this->input = new \Joomla\CMS\Input\Cli; - $this->language = Factory::getLanguage(); - $this->output = $output ?: new Stdout; - $this->cliInput = $cliInput ?: new CliInput; - - parent::__construct($config); - - // Set the current directory. - $this->set('cwd', getcwd()); - - // Set up the environment - $this->input->set('format', 'cli'); - } - - /** - * Magic method to access properties of the application. - * - * @param string $name The name of the property. - * - * @return mixed A value if the property name is valid, null otherwise. - * - * @since 4.0.0 - * @deprecated 5.0 This is a B/C proxy for deprecated read accesses - */ - public function __get($name) - { - switch ($name) - { - case 'input': - @trigger_error( - 'Accessing the input property of the application is deprecated, use the getInput() method instead.', - E_USER_DEPRECATED - ); - - return $this->getInput(); - - default: - $trace = debug_backtrace(); - trigger_error( - sprintf( - 'Undefined property via __get(): %1$s in %2$s on line %3$s', - $name, - $trace[0]['file'], - $trace[0]['line'] - ), - E_USER_NOTICE - ); - } - } - - /** - * Method to get the application input object. - * - * @return Input - * - * @since 4.0.0 - */ - public function getInput(): Input - { - return $this->input; - } - - /** - * Method to get the application language object. - * - * @return Language The language object - * - * @since 4.0.0 - */ - public function getLanguage() - { - return $this->language; - } - - /** - * Returns a reference to the global CliApplication object, only creating it if it doesn't already exist. - * - * This method must be invoked as: $cli = CliApplication::getInstance(); - * - * @param string $name The name (optional) of the Application Cli class to instantiate. - * - * @return CliApplication - * - * @since 1.7.0 - * @deprecated 5.0 Load the app through the container - * @throws \RuntimeException - */ - public static function getInstance($name = null) - { - // Only create the object if it doesn't exist. - if (empty(static::$instance)) - { - if (!class_exists($name)) - { - throw new \RuntimeException(sprintf('Unable to load application: %s', $name), 500); - } - - static::$instance = new $name; - } - - return static::$instance; - } - - /** - * Execute the application. - * - * @return void - * - * @since 1.7.0 - */ - public function execute() - { - $this->createExtensionNamespaceMap(); - - // Trigger the onBeforeExecute event - $this->triggerEvent('onBeforeExecute'); - - // Perform application routines. - $this->doExecute(); - - // Trigger the onAfterExecute event. - $this->triggerEvent('onAfterExecute'); - } - - /** - * Get an output object. - * - * @return CliOutput - * - * @since 4.0.0 - */ - public function getOutput() - { - return $this->output; - } - - /** - * Get a CLI input object. - * - * @return CliInput - * - * @since 4.0.0 - */ - public function getCliInput() - { - return $this->cliInput; - } - - /** - * Write a string to standard output. - * - * @param string $text The text to display. - * @param boolean $nl True (default) to append a new line at the end of the output string. - * - * @return $this - * - * @since 4.0.0 - */ - public function out($text = '', $nl = true) - { - $this->getOutput()->out($text, $nl); - - return $this; - } - - /** - * Get a value from standard input. - * - * @return string The input string from standard input. - * - * @codeCoverageIgnore - * @since 4.0.0 - */ - public function in() - { - return $this->getCliInput()->in(); - } - - /** - * Set an output object. - * - * @param CliOutput $output CliOutput object - * - * @return $this - * - * @since 3.3 - */ - public function setOutput(CliOutput $output) - { - $this->output = $output; - - return $this; - } - - /** - * Enqueue a system message. - * - * @param string $msg The message to enqueue. - * @param string $type The message type. - * - * @return void - * - * @since 4.0.0 - */ - public function enqueueMessage($msg, $type = self::MSG_INFO) - { - if (!\array_key_exists($type, $this->messages)) - { - $this->messages[$type] = []; - } - - $this->messages[$type][] = $msg; - } - - /** - * Get the system message queue. - * - * @return array The system message queue. - * - * @since 4.0.0 - */ - public function getMessageQueue() - { - return $this->messages; - } - - /** - * Check the client interface by name. - * - * @param string $identifier String identifier for the application interface - * - * @return boolean True if this application is of the given type client interface. - * - * @since 4.0.0 - */ - public function isClient($identifier) - { - return $identifier === 'cli'; - } - - /** - * Method to get the application session object. - * - * @return SessionInterface The session object - * - * @since 4.0.0 - */ - public function getSession() - { - return $this->container->get(SessionInterface::class); - } - - /** - * Retrieve the application configuration object. - * - * @return Registry - * - * @since 4.0.0 - */ - public function getConfig() - { - return $this->config; - } - - /** - * Flag if the application instance is a CLI or web based application. - * - * Helper function, you should use the native PHP functions to detect if it is a CLI application. - * - * @return boolean - * - * @since 4.0.0 - * @deprecated 5.0 Will be removed without replacements - */ - public function isCli() - { - return true; - } + use DispatcherAwareTrait; + use EventAware; + use IdentityAware; + use ContainerAwareTrait; + use ExtensionManagerTrait; + use ExtensionNamespaceMapper; + + /** + * Output object + * + * @var CliOutput + * @since 4.0.0 + */ + protected $output; + + /** + * The input. + * + * @var \Joomla\Input\Input + * @since 4.0.0 + */ + protected $input = null; + + /** + * CLI Input object + * + * @var CliInput + * @since 4.0.0 + */ + protected $cliInput; + + /** + * The application language object. + * + * @var Language + * @since 4.0.0 + */ + protected $language; + + /** + * The application message queue. + * + * @var array + * @since 4.0.0 + */ + protected $messages = []; + + /** + * The application instance. + * + * @var CliApplication + * @since 1.7.0 + */ + protected static $instance; + + /** + * Class constructor. + * + * @param Input $input An optional argument to provide dependency injection for the application's + * input object. If the argument is a JInputCli object that object will become + * the application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's + * config object. If the argument is a Registry object that object will become + * the application's config object, otherwise a default config object is created. + * @param CliOutput $output The output handler. + * @param CliInput $cliInput The CLI input handler. + * @param DispatcherInterface $dispatcher An optional argument to provide dependency injection for the application's + * event dispatcher. If the argument is a DispatcherInterface object that object will become + * the application's event dispatcher, if it is null then the default event dispatcher + * will be created based on the application's loadDispatcher() method. + * @param Container $container Dependency injection container. + * + * @since 1.7.0 + */ + public function __construct( + Input $input = null, + Registry $config = null, + CliOutput $output = null, + CliInput $cliInput = null, + DispatcherInterface $dispatcher = null, + Container $container = null + ) { + // Close the application if we are not executed from the command line. + if (!\defined('STDOUT') || !\defined('STDIN') || !isset($_SERVER['argv'])) { + $this->close(); + } + + $container = $container ?: Factory::getContainer(); + $this->setContainer($container); + $this->setDispatcher($dispatcher ?: $container->get(\Joomla\Event\DispatcherInterface::class)); + + if (!$container->has('session')) { + $container->alias('session', 'session.cli') + ->alias('JSession', 'session.cli') + ->alias(\Joomla\CMS\Session\Session::class, 'session.cli') + ->alias(\Joomla\Session\Session::class, 'session.cli') + ->alias(\Joomla\Session\SessionInterface::class, 'session.cli'); + } + + $this->input = new \Joomla\CMS\Input\Cli(); + $this->language = Factory::getLanguage(); + $this->output = $output ?: new Stdout(); + $this->cliInput = $cliInput ?: new CliInput(); + + parent::__construct($config); + + // Set the current directory. + $this->set('cwd', getcwd()); + + // Set up the environment + $this->input->set('format', 'cli'); + } + + /** + * Magic method to access properties of the application. + * + * @param string $name The name of the property. + * + * @return mixed A value if the property name is valid, null otherwise. + * + * @since 4.0.0 + * @deprecated 5.0 This is a B/C proxy for deprecated read accesses + */ + public function __get($name) + { + switch ($name) { + case 'input': + @trigger_error( + 'Accessing the input property of the application is deprecated, use the getInput() method instead.', + E_USER_DEPRECATED + ); + + return $this->getInput(); + + default: + $trace = debug_backtrace(); + trigger_error( + sprintf( + 'Undefined property via __get(): %1$s in %2$s on line %3$s', + $name, + $trace[0]['file'], + $trace[0]['line'] + ), + E_USER_NOTICE + ); + } + } + + /** + * Method to get the application input object. + * + * @return Input + * + * @since 4.0.0 + */ + public function getInput(): Input + { + return $this->input; + } + + /** + * Method to get the application language object. + * + * @return Language The language object + * + * @since 4.0.0 + */ + public function getLanguage() + { + return $this->language; + } + + /** + * Returns a reference to the global CliApplication object, only creating it if it doesn't already exist. + * + * This method must be invoked as: $cli = CliApplication::getInstance(); + * + * @param string $name The name (optional) of the Application Cli class to instantiate. + * + * @return CliApplication + * + * @since 1.7.0 + * @deprecated 5.0 Load the app through the container + * @throws \RuntimeException + */ + public static function getInstance($name = null) + { + // Only create the object if it doesn't exist. + if (empty(static::$instance)) { + if (!class_exists($name)) { + throw new \RuntimeException(sprintf('Unable to load application: %s', $name), 500); + } + + static::$instance = new $name(); + } + + return static::$instance; + } + + /** + * Execute the application. + * + * @return void + * + * @since 1.7.0 + */ + public function execute() + { + $this->createExtensionNamespaceMap(); + + // Trigger the onBeforeExecute event + $this->triggerEvent('onBeforeExecute'); + + // Perform application routines. + $this->doExecute(); + + // Trigger the onAfterExecute event. + $this->triggerEvent('onAfterExecute'); + } + + /** + * Get an output object. + * + * @return CliOutput + * + * @since 4.0.0 + */ + public function getOutput() + { + return $this->output; + } + + /** + * Get a CLI input object. + * + * @return CliInput + * + * @since 4.0.0 + */ + public function getCliInput() + { + return $this->cliInput; + } + + /** + * Write a string to standard output. + * + * @param string $text The text to display. + * @param boolean $nl True (default) to append a new line at the end of the output string. + * + * @return $this + * + * @since 4.0.0 + */ + public function out($text = '', $nl = true) + { + $this->getOutput()->out($text, $nl); + + return $this; + } + + /** + * Get a value from standard input. + * + * @return string The input string from standard input. + * + * @codeCoverageIgnore + * @since 4.0.0 + */ + public function in() + { + return $this->getCliInput()->in(); + } + + /** + * Set an output object. + * + * @param CliOutput $output CliOutput object + * + * @return $this + * + * @since 3.3 + */ + public function setOutput(CliOutput $output) + { + $this->output = $output; + + return $this; + } + + /** + * Enqueue a system message. + * + * @param string $msg The message to enqueue. + * @param string $type The message type. + * + * @return void + * + * @since 4.0.0 + */ + public function enqueueMessage($msg, $type = self::MSG_INFO) + { + if (!\array_key_exists($type, $this->messages)) { + $this->messages[$type] = []; + } + + $this->messages[$type][] = $msg; + } + + /** + * Get the system message queue. + * + * @return array The system message queue. + * + * @since 4.0.0 + */ + public function getMessageQueue() + { + return $this->messages; + } + + /** + * Check the client interface by name. + * + * @param string $identifier String identifier for the application interface + * + * @return boolean True if this application is of the given type client interface. + * + * @since 4.0.0 + */ + public function isClient($identifier) + { + return $identifier === 'cli'; + } + + /** + * Method to get the application session object. + * + * @return SessionInterface The session object + * + * @since 4.0.0 + */ + public function getSession() + { + return $this->container->get(SessionInterface::class); + } + + /** + * Retrieve the application configuration object. + * + * @return Registry + * + * @since 4.0.0 + */ + public function getConfig() + { + return $this->config; + } + + /** + * Flag if the application instance is a CLI or web based application. + * + * Helper function, you should use the native PHP functions to detect if it is a CLI application. + * + * @return boolean + * + * @since 4.0.0 + * @deprecated 5.0 Will be removed without replacements + */ + public function isCli() + { + return true; + } } diff --git a/libraries/src/Application/ConsoleApplication.php b/libraries/src/Application/ConsoleApplication.php index 9961710d3a275..a2a14c93fdc19 100644 --- a/libraries/src/Application/ConsoleApplication.php +++ b/libraries/src/Application/ConsoleApplication.php @@ -1,4 +1,5 @@ close(); - } - - // Set up a Input object for Controllers etc to use - $this->input = new \Joomla\CMS\Input\Cli; - $this->language = $language; - - parent::__construct($input, $output, $config); - - $this->setVersion(JVERSION); - - // Register the client name as cli - $this->name = 'cli'; - - $this->setContainer($container); - $this->setDispatcher($dispatcher); - - // Set the execution datetime and timestamp; - $this->set('execution.datetime', gmdate('Y-m-d H:i:s')); - $this->set('execution.timestamp', time()); - $this->set('execution.microtimestamp', microtime(true)); - - // Set the current directory. - $this->set('cwd', getcwd()); - - // Set up the environment - $this->input->set('format', 'cli'); - } - - /** - * Magic method to access properties of the application. - * - * @param string $name The name of the property. - * - * @return mixed A value if the property name is valid, null otherwise. - * - * @since 4.0.0 - * @deprecated 5.0 This is a B/C proxy for deprecated read accesses - */ - public function __get($name) - { - switch ($name) - { - case 'input': - @trigger_error( - 'Accessing the input property of the application is deprecated, use the getInput() method instead.', - E_USER_DEPRECATED - ); - - return $this->getInput(); - - default: - $trace = debug_backtrace(); - trigger_error( - sprintf( - 'Undefined property via __get(): %1$s in %2$s on line %3$s', - $name, - $trace[0]['file'], - $trace[0]['line'] - ), - E_USER_NOTICE - ); - } - } - - /** - * Method to run the application routines. - * - * @return integer The exit code for the application - * - * @since 4.0.0 - * @throws \Throwable - */ - protected function doExecute(): int - { - $exitCode = parent::doExecute(); - - $style = new SymfonyStyle($this->getConsoleInput(), $this->getConsoleOutput()); - - $methodMap = [ - self::MSG_ALERT => 'error', - self::MSG_CRITICAL => 'caution', - self::MSG_DEBUG => 'comment', - self::MSG_EMERGENCY => 'caution', - self::MSG_ERROR => 'error', - self::MSG_INFO => 'note', - self::MSG_NOTICE => 'note', - self::MSG_WARNING => 'warning', - ]; - - // Output any enqueued messages before the app exits - foreach ($this->getMessageQueue() as $type => $messages) - { - $method = $methodMap[$type] ?? 'comment'; - - $style->$method($messages); - } - - return $exitCode; - } - - /** - * Execute the application. - * - * @return void - * - * @since 4.0.0 - * @throws \Throwable - */ - public function execute() - { - // Load extension namespaces - $this->createExtensionNamespaceMap(); - - // Import CMS plugin groups to be able to subscribe to events - PluginHelper::importPlugin('system'); - PluginHelper::importPlugin('console'); - - parent::execute(); - } - - /** - * Enqueue a system message. - * - * @param string $msg The message to enqueue. - * @param string $type The message type. - * - * @return void - * - * @since 4.0.0 - */ - public function enqueueMessage($msg, $type = self::MSG_INFO) - { - if (!array_key_exists($type, $this->messages)) - { - $this->messages[$type] = []; - } - - $this->messages[$type][] = $msg; - } - - /** - * Gets the name of the current running application. - * - * @return string The name of the application. - * - * @since 4.0.0 - */ - public function getName(): string - { - return $this->name; - } - - /** - * Get the commands which should be registered by default to the application. - * - * @return \Joomla\Console\Command\AbstractCommand[] - * - * @since 4.0.0 - */ - protected function getDefaultCommands(): array - { - return array_merge( - parent::getDefaultCommands(), - [ - new Console\CleanCacheCommand, - new Console\CheckUpdatesCommand, - new Console\RemoveOldFilesCommand, - new Console\AddUserCommand($this->getDatabase()), - new Console\AddUserToGroupCommand($this->getDatabase()), - new Console\RemoveUserFromGroupCommand($this->getDatabase()), - new Console\DeleteUserCommand($this->getDatabase()), - new Console\ChangeUserPasswordCommand, - new Console\ListUserCommand($this->getDatabase()), - ] - ); - } - - /** - * Retrieve the application configuration object. - * - * @return Registry - * - * @since 4.0.0 - */ - public function getConfig() - { - return $this->config; - } - - /** - * Method to get the application input object. - * - * @return Input - * - * @since 4.0.0 - */ - public function getInput(): Input - { - return $this->input; - } - - /** - * Method to get the application language object. - * - * @return Language The language object - * - * @since 4.0.0 - */ - public function getLanguage() - { - return $this->language; - } - - /** - * Get the system message queue. - * - * @return array The system message queue. - * - * @since 4.0.0 - */ - public function getMessageQueue() - { - return $this->messages; - } - - /** - * Method to get the application session object. - * - * @return SessionInterface The session object - * - * @since 4.0.0 - */ - public function getSession() - { - return $this->session; - } - - /** - * Check the client interface by name. - * - * @param string $identifier String identifier for the application interface - * - * @return boolean True if this application is of the given type client interface. - * - * @since 4.0.0 - */ - public function isClient($identifier) - { - return $this->getName() === $identifier; - } - - /** - * Flag if the application instance is a CLI or web based application. - * - * Helper function, you should use the native PHP functions to detect if it is a CLI application. - * - * @return boolean - * - * @since 4.0.0 - * @deprecated 5.0 Will be removed without replacements - */ - public function isCli() - { - return true; - } - - /** - * Sets the session for the application to use, if required. - * - * @param SessionInterface $session A session object. - * - * @return $this - * - * @since 4.0.0 - */ - public function setSession(SessionInterface $session): self - { - $this->session = $session; - - return $this; - } - - /** - * Flush the media version to refresh versionable assets - * - * @return void - * - * @since 4.0.0 - */ - public function flushAssets() - { - (new Version)->refreshMediaVersion(); - } - - /** - * Get the long version string for the application. - * - * Overrides the parent method due to conflicting use of the getName method between the console application and - * the CMS application interface. - * - * @return string - * - * @since 4.0.0 - */ - public function getLongVersion(): string - { - return sprintf('Joomla! %s (debug: %s)', (new Version)->getShortVersion(), (\defined('JDEBUG') && JDEBUG ? 'Yes' : 'No')); - } - - /** - * Set the name of the application. - * - * @param string $name The new application name. - * - * @return void - * - * @since 4.0.0 - * @throws \RuntimeException because the application name cannot be changed - */ - public function setName(string $name): void - { - throw new \RuntimeException('The console application name cannot be changed'); - } - - /** - * Returns the application Router object. - * - * @param string $name The name of the application. - * @param array $options An optional associative array of configuration settings. - * - * @return Router - * - * @since 4.0.6 - * - * @throws \InvalidArgumentException - * - * @deprecated 5.0 Inject the router or load it from the dependency injection container - */ - public static function getRouter($name = null, array $options = array()) - { - if (empty($name)) - { - throw new InvalidArgumentException('A router name must be set in console application.'); - } - - $options['mode'] = Factory::getApplication()->get('sef'); - - return Router::getInstance($name, $options); - } + use DispatcherAwareTrait; + use EventAware; + use IdentityAware; + use ContainerAwareTrait; + use ExtensionManagerTrait; + use ExtensionNamespaceMapper; + use DatabaseAwareTrait; + + /** + * The input. + * + * @var Input + * @since 4.0.0 + */ + protected $input = null; + + /** + * The name of the application. + * + * @var string + * @since 4.0.0 + */ + protected $name = null; + + /** + * The application language object. + * + * @var Language + * @since 4.0.0 + */ + protected $language; + + /** + * The application message queue. + * + * @var array + * @since 4.0.0 + */ + private $messages = []; + + /** + * The application session object. + * + * @var SessionInterface + * @since 4.0.0 + */ + private $session; + + /** + * Class constructor. + * + * @param Registry $config An optional argument to provide dependency injection for the application's config object. If the + * argument is a Registry object that object will become the application's config object, + * otherwise a default config object is created. + * @param DispatcherInterface $dispatcher An optional argument to provide dependency injection for the application's event dispatcher. If the + * argument is a DispatcherInterface object that object will become the application's event dispatcher, + * if it is null then the default event dispatcher will be created based on the application's + * loadDispatcher() method. + * @param Container $container Dependency injection container. + * @param Language $language The language object provisioned for the application. + * @param InputInterface|null $input An optional argument to provide dependency injection for the application's input object. If the + * argument is an InputInterface object that object will become the application's input object, + * otherwise a default input object is created. + * @param OutputInterface|null $output An optional argument to provide dependency injection for the application's output object. If the + * argument is an OutputInterface object that object will become the application's output object, + * otherwise a default output object is created. + * + * @since 4.0.0 + */ + public function __construct( + Registry $config, + DispatcherInterface $dispatcher, + Container $container, + Language $language, + ?InputInterface $input = null, + ?OutputInterface $output = null + ) { + // Close the application if it is not executed from the command line. + if (!\defined('STDOUT') || !\defined('STDIN') || !isset($_SERVER['argv'])) { + $this->close(); + } + + // Set up a Input object for Controllers etc to use + $this->input = new \Joomla\CMS\Input\Cli(); + $this->language = $language; + + parent::__construct($input, $output, $config); + + $this->setVersion(JVERSION); + + // Register the client name as cli + $this->name = 'cli'; + + $this->setContainer($container); + $this->setDispatcher($dispatcher); + + // Set the execution datetime and timestamp; + $this->set('execution.datetime', gmdate('Y-m-d H:i:s')); + $this->set('execution.timestamp', time()); + $this->set('execution.microtimestamp', microtime(true)); + + // Set the current directory. + $this->set('cwd', getcwd()); + + // Set up the environment + $this->input->set('format', 'cli'); + } + + /** + * Magic method to access properties of the application. + * + * @param string $name The name of the property. + * + * @return mixed A value if the property name is valid, null otherwise. + * + * @since 4.0.0 + * @deprecated 5.0 This is a B/C proxy for deprecated read accesses + */ + public function __get($name) + { + switch ($name) { + case 'input': + @trigger_error( + 'Accessing the input property of the application is deprecated, use the getInput() method instead.', + E_USER_DEPRECATED + ); + + return $this->getInput(); + + default: + $trace = debug_backtrace(); + trigger_error( + sprintf( + 'Undefined property via __get(): %1$s in %2$s on line %3$s', + $name, + $trace[0]['file'], + $trace[0]['line'] + ), + E_USER_NOTICE + ); + } + } + + /** + * Method to run the application routines. + * + * @return integer The exit code for the application + * + * @since 4.0.0 + * @throws \Throwable + */ + protected function doExecute(): int + { + $exitCode = parent::doExecute(); + + $style = new SymfonyStyle($this->getConsoleInput(), $this->getConsoleOutput()); + + $methodMap = [ + self::MSG_ALERT => 'error', + self::MSG_CRITICAL => 'caution', + self::MSG_DEBUG => 'comment', + self::MSG_EMERGENCY => 'caution', + self::MSG_ERROR => 'error', + self::MSG_INFO => 'note', + self::MSG_NOTICE => 'note', + self::MSG_WARNING => 'warning', + ]; + + // Output any enqueued messages before the app exits + foreach ($this->getMessageQueue() as $type => $messages) { + $method = $methodMap[$type] ?? 'comment'; + + $style->$method($messages); + } + + return $exitCode; + } + + /** + * Execute the application. + * + * @return void + * + * @since 4.0.0 + * @throws \Throwable + */ + public function execute() + { + // Load extension namespaces + $this->createExtensionNamespaceMap(); + + // Import CMS plugin groups to be able to subscribe to events + PluginHelper::importPlugin('system'); + PluginHelper::importPlugin('console'); + + parent::execute(); + } + + /** + * Enqueue a system message. + * + * @param string $msg The message to enqueue. + * @param string $type The message type. + * + * @return void + * + * @since 4.0.0 + */ + public function enqueueMessage($msg, $type = self::MSG_INFO) + { + if (!array_key_exists($type, $this->messages)) { + $this->messages[$type] = []; + } + + $this->messages[$type][] = $msg; + } + + /** + * Gets the name of the current running application. + * + * @return string The name of the application. + * + * @since 4.0.0 + */ + public function getName(): string + { + return $this->name; + } + + /** + * Get the commands which should be registered by default to the application. + * + * @return \Joomla\Console\Command\AbstractCommand[] + * + * @since 4.0.0 + */ + protected function getDefaultCommands(): array + { + return array_merge( + parent::getDefaultCommands(), + [ + new Console\CleanCacheCommand(), + new Console\CheckUpdatesCommand(), + new Console\RemoveOldFilesCommand(), + new Console\AddUserCommand($this->getDatabase()), + new Console\AddUserToGroupCommand($this->getDatabase()), + new Console\RemoveUserFromGroupCommand($this->getDatabase()), + new Console\DeleteUserCommand($this->getDatabase()), + new Console\ChangeUserPasswordCommand(), + new Console\ListUserCommand($this->getDatabase()), + ] + ); + } + + /** + * Retrieve the application configuration object. + * + * @return Registry + * + * @since 4.0.0 + */ + public function getConfig() + { + return $this->config; + } + + /** + * Method to get the application input object. + * + * @return Input + * + * @since 4.0.0 + */ + public function getInput(): Input + { + return $this->input; + } + + /** + * Method to get the application language object. + * + * @return Language The language object + * + * @since 4.0.0 + */ + public function getLanguage() + { + return $this->language; + } + + /** + * Get the system message queue. + * + * @return array The system message queue. + * + * @since 4.0.0 + */ + public function getMessageQueue() + { + return $this->messages; + } + + /** + * Method to get the application session object. + * + * @return SessionInterface The session object + * + * @since 4.0.0 + */ + public function getSession() + { + return $this->session; + } + + /** + * Check the client interface by name. + * + * @param string $identifier String identifier for the application interface + * + * @return boolean True if this application is of the given type client interface. + * + * @since 4.0.0 + */ + public function isClient($identifier) + { + return $this->getName() === $identifier; + } + + /** + * Flag if the application instance is a CLI or web based application. + * + * Helper function, you should use the native PHP functions to detect if it is a CLI application. + * + * @return boolean + * + * @since 4.0.0 + * @deprecated 5.0 Will be removed without replacements + */ + public function isCli() + { + return true; + } + + /** + * Sets the session for the application to use, if required. + * + * @param SessionInterface $session A session object. + * + * @return $this + * + * @since 4.0.0 + */ + public function setSession(SessionInterface $session): self + { + $this->session = $session; + + return $this; + } + + /** + * Flush the media version to refresh versionable assets + * + * @return void + * + * @since 4.0.0 + */ + public function flushAssets() + { + (new Version())->refreshMediaVersion(); + } + + /** + * Get the long version string for the application. + * + * Overrides the parent method due to conflicting use of the getName method between the console application and + * the CMS application interface. + * + * @return string + * + * @since 4.0.0 + */ + public function getLongVersion(): string + { + return sprintf('Joomla! %s (debug: %s)', (new Version())->getShortVersion(), (\defined('JDEBUG') && JDEBUG ? 'Yes' : 'No')); + } + + /** + * Set the name of the application. + * + * @param string $name The new application name. + * + * @return void + * + * @since 4.0.0 + * @throws \RuntimeException because the application name cannot be changed + */ + public function setName(string $name): void + { + throw new \RuntimeException('The console application name cannot be changed'); + } + + /** + * Returns the application Router object. + * + * @param string $name The name of the application. + * @param array $options An optional associative array of configuration settings. + * + * @return Router + * + * @since 4.0.6 + * + * @throws \InvalidArgumentException + * + * @deprecated 5.0 Inject the router or load it from the dependency injection container + */ + public static function getRouter($name = null, array $options = array()) + { + if (empty($name)) { + throw new InvalidArgumentException('A router name must be set in console application.'); + } + + $options['mode'] = Factory::getApplication()->get('sef'); + + return Router::getInstance($name, $options); + } } diff --git a/libraries/src/Application/DaemonApplication.php b/libraries/src/Application/DaemonApplication.php index f56ff88a07504..5c405354bd3dd 100644 --- a/libraries/src/Application/DaemonApplication.php +++ b/libraries/src/Application/DaemonApplication.php @@ -1,4 +1,5 @@ config->get('max_execution_time', 0)); - - if ($this->config->get('max_memory_limit') !== null) - { - ini_set('memory_limit', $this->config->get('max_memory_limit', '256M')); - } - - // Flush content immediately. - ob_implicit_flush(); - } - - /** - * Method to handle POSIX signals. - * - * @param integer $signal The received POSIX signal. - * - * @return void - * - * @since 1.7.0 - * @see pcntl_signal() - * @throws \RuntimeException - */ - public static function signal($signal) - { - // Log all signals sent to the daemon. - Log::add('Received signal: ' . $signal, Log::DEBUG); - - // Let's make sure we have an application instance. - if (!is_subclass_of(static::$instance, CliApplication::class)) - { - Log::add('Cannot find the application instance.', Log::EMERGENCY); - throw new \RuntimeException('Cannot find the application instance.'); - } - - // Fire the onReceiveSignal event. - static::$instance->triggerEvent('onReceiveSignal', array($signal)); - - switch ($signal) - { - case SIGINT: - case SIGTERM: - // Handle shutdown tasks - if (static::$instance->running && static::$instance->isActive()) - { - static::$instance->shutdown(); - } - else - { - static::$instance->close(); - } - break; - case SIGHUP: - // Handle restart tasks - if (static::$instance->running && static::$instance->isActive()) - { - static::$instance->shutdown(true); - } - else - { - static::$instance->close(); - } - break; - case SIGCHLD: - // A child process has died - while (static::$instance->pcntlWait($signal, WNOHANG || WUNTRACED) > 0) - { - usleep(1000); - } - break; - case SIGCLD: - while (static::$instance->pcntlWait($signal, WNOHANG) > 0) - { - $signal = static::$instance->pcntlChildExitStatus($signal); - } - break; - default: - break; - } - } - - /** - * Check to see if the daemon is active. This does not assume that $this daemon is active, but - * only if an instance of the application is active as a daemon. - * - * @return boolean True if daemon is active. - * - * @since 1.7.0 - */ - public function isActive() - { - // Get the process id file location for the application. - $pidFile = $this->config->get('application_pid_file'); - - // If the process id file doesn't exist then the daemon is obviously not running. - if (!is_file($pidFile)) - { - return false; - } - - // Read the contents of the process id file as an integer. - $fp = fopen($pidFile, 'r'); - $pid = fread($fp, filesize($pidFile)); - $pid = (int) $pid; - fclose($fp); - - // Check to make sure that the process id exists as a positive integer. - if (!$pid) - { - return false; - } - - // Check to make sure the process is active by pinging it and ensure it responds. - if (!posix_kill($pid, 0)) - { - // No response so remove the process id file and log the situation. - @ unlink($pidFile); - Log::add('The process found based on PID file was unresponsive.', Log::WARNING); - - return false; - } - - return true; - } - - /** - * Load an object or array into the application configuration object. - * - * @param mixed $data Either an array or object to be loaded into the configuration object. - * - * @return DaemonApplication Instance of $this to allow chaining. - * - * @since 1.7.0 - */ - public function loadConfiguration($data) - { - /* - * Setup some application metadata options. This is useful if we ever want to write out startup scripts - * or just have some sort of information available to share about things. - */ - - // The application author name. This string is used in generating startup scripts and has - // a maximum of 50 characters. - $tmp = (string) $this->config->get('author_name', 'Joomla Platform'); - $this->config->set('author_name', (\strlen($tmp) > 50) ? substr($tmp, 0, 50) : $tmp); - - // The application author email. This string is used in generating startup scripts. - $tmp = (string) $this->config->get('author_email', 'admin@joomla.org'); - $this->config->set('author_email', filter_var($tmp, FILTER_VALIDATE_EMAIL)); - - // The application name. This string is used in generating startup scripts. - $tmp = (string) $this->config->get('application_name', 'DaemonApplication'); - $this->config->set('application_name', (string) preg_replace('/[^A-Z0-9_-]/i', '', $tmp)); - - // The application description. This string is used in generating startup scripts. - $tmp = (string) $this->config->get('application_description', 'A generic Joomla Platform application.'); - $this->config->set('application_description', filter_var($tmp, FILTER_SANITIZE_STRING)); - - /* - * Setup the application path options. This defines the default executable name, executable directory, - * and also the path to the daemon process id file. - */ - - // The application executable daemon. This string is used in generating startup scripts. - $tmp = (string) $this->config->get('application_executable', basename($this->input->executable)); - $this->config->set('application_executable', $tmp); - - // The home directory of the daemon. - $tmp = (string) $this->config->get('application_directory', \dirname($this->input->executable)); - $this->config->set('application_directory', $tmp); - - // The pid file location. This defaults to a path inside the /tmp directory. - $name = $this->config->get('application_name'); - $tmp = (string) $this->config->get('application_pid_file', strtolower('/tmp/' . $name . '/' . $name . '.pid')); - $this->config->set('application_pid_file', $tmp); - - /* - * Setup the application identity options. It is important to remember if the default of 0 is set for - * either UID or GID then changing that setting will not be attempted as there is no real way to "change" - * the identity of a process from some user to root. - */ - - // The user id under which to run the daemon. - $tmp = (int) $this->config->get('application_uid', 0); - $options = array('options' => array('min_range' => 0, 'max_range' => 65000)); - $this->config->set('application_uid', filter_var($tmp, FILTER_VALIDATE_INT, $options)); - - // The group id under which to run the daemon. - $tmp = (int) $this->config->get('application_gid', 0); - $options = array('options' => array('min_range' => 0, 'max_range' => 65000)); - $this->config->set('application_gid', filter_var($tmp, FILTER_VALIDATE_INT, $options)); - - // Option to kill the daemon if it cannot switch to the chosen identity. - $tmp = (bool) $this->config->get('application_require_identity', 1); - $this->config->set('application_require_identity', $tmp); - - /* - * Setup the application runtime options. By default our execution time limit is infinite obviously - * because a daemon should be constantly running unless told otherwise. The default limit for memory - * usage is 256M, which admittedly is a little high, but remember it is a "limit" and PHP's memory - * management leaves a bit to be desired :-) - */ - - // The maximum execution time of the application in seconds. Zero is infinite. - $tmp = $this->config->get('max_execution_time'); - - if ($tmp !== null) - { - $this->config->set('max_execution_time', (int) $tmp); - } - - // The maximum amount of memory the application can use. - $tmp = $this->config->get('max_memory_limit', '256M'); - - if ($tmp !== null) - { - $this->config->set('max_memory_limit', (string) $tmp); - } - - return $this; - } - - /** - * Execute the daemon. - * - * @return void - * - * @since 1.7.0 - */ - public function execute() - { - // Trigger the onBeforeExecute event - $this->triggerEvent('onBeforeExecute'); - - // Enable basic garbage collection. - gc_enable(); - - Log::add('Starting ' . $this->name, Log::INFO); - - // Set off the process for becoming a daemon. - if ($this->daemonize()) - { - // Declare ticks to start signal monitoring. When you declare ticks, PCNTL will monitor - // incoming signals after each tick and call the relevant signal handler automatically. - declare (ticks = 1); - - // Start the main execution loop. - while (true) - { - // Perform basic garbage collection. - $this->gc(); - - // Don't completely overload the CPU. - usleep(1000); - - // Execute the main application logic. - $this->doExecute(); - } - } - // We were not able to daemonize the application so log the failure and die gracefully. - else - { - Log::add('Starting ' . $this->name . ' failed', Log::INFO); - } - - // Trigger the onAfterExecute event. - $this->triggerEvent('onAfterExecute'); - } - - /** - * Restart daemon process. - * - * @return void - * - * @since 1.7.0 - */ - public function restart() - { - Log::add('Stopping ' . $this->name, Log::INFO); - $this->shutdown(true); - } - - /** - * Stop daemon process. - * - * @return void - * - * @since 1.7.0 - */ - public function stop() - { - Log::add('Stopping ' . $this->name, Log::INFO); - $this->shutdown(); - } - - /** - * Method to change the identity of the daemon process and resources. - * - * @return boolean True if identity successfully changed - * - * @since 1.7.0 - * @see posix_setuid() - */ - protected function changeIdentity() - { - // Get the group and user ids to set for the daemon. - $uid = (int) $this->config->get('application_uid', 0); - $gid = (int) $this->config->get('application_gid', 0); - - // Get the application process id file path. - $file = $this->config->get('application_pid_file'); - - // Change the user id for the process id file if necessary. - if ($uid && (fileowner($file) != $uid) && (!@ chown($file, $uid))) - { - Log::add('Unable to change user ownership of the process id file.', Log::ERROR); - - return false; - } - - // Change the group id for the process id file if necessary. - if ($gid && (filegroup($file) != $gid) && (!@ chgrp($file, $gid))) - { - Log::add('Unable to change group ownership of the process id file.', Log::ERROR); - - return false; - } - - // Set the correct home directory for the process. - if ($uid && ($info = posix_getpwuid($uid)) && is_dir($info['dir'])) - { - system('export HOME="' . $info['dir'] . '"'); - } - - // Change the user id for the process necessary. - if ($uid && (posix_getuid() != $uid) && (!@ posix_setuid($uid))) - { - Log::add('Unable to change user ownership of the process.', Log::ERROR); - - return false; - } - - // Change the group id for the process necessary. - if ($gid && (posix_getgid() != $gid) && (!@ posix_setgid($gid))) - { - Log::add('Unable to change group ownership of the process.', Log::ERROR); - - return false; - } - - // Get the user and group information based on uid and gid. - $user = posix_getpwuid($uid); - $group = posix_getgrgid($gid); - - Log::add('Changed daemon identity to ' . $user['name'] . ':' . $group['name'], Log::INFO); - - return true; - } - - /** - * Method to put the application into the background. - * - * @return boolean - * - * @since 1.7.0 - * @throws \RuntimeException - */ - protected function daemonize() - { - // Is there already an active daemon running? - if ($this->isActive()) - { - Log::add($this->name . ' daemon is still running. Exiting the application.', Log::EMERGENCY); - - return false; - } - - // Reset Process Information - $this->safeMode = !!@ ini_get('safe_mode'); - $this->processId = 0; - $this->running = false; - - // Detach process! - try - { - // Check if we should run in the foreground. - if (!$this->input->get('f')) - { - // Detach from the terminal. - $this->detach(); - } - else - { - // Setup running values. - $this->exiting = false; - $this->running = true; - - // Set the process id. - $this->processId = (int) posix_getpid(); - $this->parentId = $this->processId; - } - } - catch (\RuntimeException $e) - { - Log::add('Unable to fork.', Log::EMERGENCY); - - return false; - } - - // Verify the process id is valid. - if ($this->processId < 1) - { - Log::add('The process id is invalid; the fork failed.', Log::EMERGENCY); - - return false; - } - - // Clear the umask. - @ umask(0); - - // Write out the process id file for concurrency management. - if (!$this->writeProcessIdFile()) - { - Log::add('Unable to write the pid file at: ' . $this->config->get('application_pid_file'), Log::EMERGENCY); - - return false; - } - - // Attempt to change the identity of user running the process. - if (!$this->changeIdentity()) - { - // If the identity change was required then we need to return false. - if ($this->config->get('application_require_identity')) - { - Log::add('Unable to change process owner.', Log::CRITICAL); - - return false; - } - else - { - Log::add('Unable to change process owner.', Log::WARNING); - } - } - - // Setup the signal handlers for the daemon. - if (!$this->setupSignalHandlers()) - { - return false; - } - - // Change the current working directory to the application working directory. - @ chdir($this->config->get('application_directory')); - - return true; - } - - /** - * This is truly where the magic happens. This is where we fork the process and kill the parent - * process, which is essentially what turns the application into a daemon. - * - * @return void - * - * @since 3.0.0 - * @throws \RuntimeException - */ - protected function detach() - { - Log::add('Detaching the ' . $this->name . ' daemon.', Log::DEBUG); - - // Attempt to fork the process. - $pid = $this->fork(); - - // If the pid is positive then we successfully forked, and can close this application. - if ($pid) - { - // Add the log entry for debugging purposes and exit gracefully. - Log::add('Ending ' . $this->name . ' parent process', Log::DEBUG); - $this->close(); - } - // We are in the forked child process. - else - { - // Setup some protected values. - $this->exiting = false; - $this->running = true; - - // Set the parent to self. - $this->parentId = $this->processId; - } - } - - /** - * Method to fork the process. - * - * @return integer The child process id to the parent process, zero to the child process. - * - * @since 1.7.0 - * @throws \RuntimeException - */ - protected function fork() - { - // Attempt to fork the process. - $pid = $this->pcntlFork(); - - // If the fork failed, throw an exception. - if ($pid === -1) - { - throw new \RuntimeException('The process could not be forked.'); - } - // Update the process id for the child. - elseif ($pid === 0) - { - $this->processId = (int) posix_getpid(); - } - // Log the fork in the parent. - else - { - // Log the fork. - Log::add('Process forked ' . $pid, Log::DEBUG); - } - - // Trigger the onFork event. - $this->postFork(); - - return $pid; - } - - /** - * Method to perform basic garbage collection and memory management in the sense of clearing the - * stat cache. We will probably call this method pretty regularly in our main loop. - * - * @return void - * - * @since 1.7.0 - */ - protected function gc() - { - // Perform generic garbage collection. - gc_collect_cycles(); - - // Clear the stat cache so it doesn't blow up memory. - clearstatcache(); - } - - /** - * Method to attach the DaemonApplication signal handler to the known signals. Applications - * can override these handlers by using the pcntl_signal() function and attaching a different - * callback method. - * - * @return boolean - * - * @since 1.7.0 - * @see pcntl_signal() - */ - protected function setupSignalHandlers() - { - // We add the error suppression for the loop because on some platforms some constants are not defined. - foreach (self::$signals as $signal) - { - // Ignore signals that are not defined. - if (!\defined($signal) || !\is_int(\constant($signal)) || (\constant($signal) === 0)) - { - // Define the signal to avoid notices. - Log::add('Signal "' . $signal . '" not defined. Defining it as null.', Log::DEBUG); - \define($signal, null); - - // Don't listen for signal. - continue; - } - - // Attach the signal handler for the signal. - if (!$this->pcntlSignal(\constant($signal), array('DaemonApplication', 'signal'))) - { - Log::add(sprintf('Unable to reroute signal handler: %s', $signal), Log::EMERGENCY); - - return false; - } - } - - return true; - } - - /** - * Method to shut down the daemon and optionally restart it. - * - * @param boolean $restart True to restart the daemon on exit. - * - * @return void - * - * @since 1.7.0 - */ - protected function shutdown($restart = false) - { - // If we are already exiting, chill. - if ($this->exiting) - { - return; - } - // If not, now we are. - else - { - $this->exiting = true; - } - - // If we aren't already daemonized then just kill the application. - if (!$this->running && !$this->isActive()) - { - Log::add('Process was not daemonized yet, just halting current process', Log::INFO); - $this->close(); - } - - // Only read the pid for the parent file. - if ($this->parentId == $this->processId) - { - // Read the contents of the process id file as an integer. - $fp = fopen($this->config->get('application_pid_file'), 'r'); - $pid = fread($fp, filesize($this->config->get('application_pid_file'))); - $pid = (int) $pid; - fclose($fp); - - // Remove the process id file. - @ unlink($this->config->get('application_pid_file')); - - // If we are supposed to restart the daemon we need to execute the same command. - if ($restart) - { - $this->close(exec(implode(' ', $GLOBALS['argv']) . ' > /dev/null &')); - } - // If we are not supposed to restart the daemon let's just kill -9. - else - { - passthru('kill -9 ' . $pid); - $this->close(); - } - } - } - - /** - * Method to write the process id file out to disk. - * - * @return boolean - * - * @since 1.7.0 - */ - protected function writeProcessIdFile() - { - // Verify the process id is valid. - if ($this->processId < 1) - { - Log::add('The process id is invalid.', Log::EMERGENCY); - - return false; - } - - // Get the application process id file path. - $file = $this->config->get('application_pid_file'); - - if (empty($file)) - { - Log::add('The process id file path is empty.', Log::ERROR); - - return false; - } - - // Make sure that the folder where we are writing the process id file exists. - $folder = \dirname($file); - - if (!is_dir($folder) && !Folder::create($folder)) - { - Log::add('Unable to create directory: ' . $folder, Log::ERROR); - - return false; - } - - // Write the process id file out to disk. - if (!file_put_contents($file, $this->processId)) - { - Log::add('Unable to write process id file: ' . $file, Log::ERROR); - - return false; - } - - // Make sure the permissions for the process id file are accurate. - if (!chmod($file, 0644)) - { - Log::add('Unable to adjust permissions for the process id file: ' . $file, Log::ERROR); - - return false; - } - - return true; - } - - /** - * Method to handle post-fork triggering of the onFork event. - * - * @return void - * - * @since 3.0.0 - */ - protected function postFork() - { - // Trigger the onFork event. - $this->triggerEvent('onFork'); - } - - /** - * Method to return the exit code of a terminated child process. - * - * @param integer $status The status parameter is the status parameter supplied to a successful call to pcntl_waitpid(). - * - * @return integer The child process exit code. - * - * @see pcntl_wexitstatus() - * @since 1.7.3 - */ - protected function pcntlChildExitStatus($status) - { - return pcntl_wexitstatus($status); - } - - /** - * Method to return the exit code of a terminated child process. - * - * @return integer On success, the PID of the child process is returned in the parent's thread - * of execution, and a 0 is returned in the child's thread of execution. On - * failure, a -1 will be returned in the parent's context, no child process - * will be created, and a PHP error is raised. - * - * @see pcntl_fork() - * @since 1.7.3 - */ - protected function pcntlFork() - { - return pcntl_fork(); - } - - /** - * Method to install a signal handler. - * - * @param integer $signal The signal number. - * @param callable $handler The signal handler which may be the name of a user created function, - * or method, or either of the two global constants SIG_IGN or SIG_DFL. - * @param boolean $restart Specifies whether system call restarting should be used when this - * signal arrives. - * - * @return boolean True on success. - * - * @see pcntl_signal() - * @since 1.7.3 - */ - protected function pcntlSignal($signal, $handler, $restart = true) - { - return pcntl_signal($signal, $handler, $restart); - } - - /** - * Method to wait on or return the status of a forked child. - * - * @param integer &$status Status information. - * @param integer $options If wait3 is available on your system (mostly BSD-style systems), - * you can provide the optional options parameter. - * - * @return integer The process ID of the child which exited, -1 on error or zero if WNOHANG - * was provided as an option (on wait3-available systems) and no child was available. - * - * @see pcntl_wait() - * @since 1.7.3 - */ - protected function pcntlWait(&$status, $options = 0) - { - return pcntl_wait($status, $options); - } + /** + * @var array The available POSIX signals to be caught by default. + * @link https://www.php.net/manual/pcntl.constants.php + * @since 1.7.0 + */ + protected static $signals = array( + 'SIGHUP', + 'SIGINT', + 'SIGQUIT', + 'SIGILL', + 'SIGTRAP', + 'SIGABRT', + 'SIGIOT', + 'SIGBUS', + 'SIGFPE', + 'SIGUSR1', + 'SIGSEGV', + 'SIGUSR2', + 'SIGPIPE', + 'SIGALRM', + 'SIGTERM', + 'SIGSTKFLT', + 'SIGCLD', + 'SIGCHLD', + 'SIGCONT', + 'SIGTSTP', + 'SIGTTIN', + 'SIGTTOU', + 'SIGURG', + 'SIGXCPU', + 'SIGXFSZ', + 'SIGVTALRM', + 'SIGPROF', + 'SIGWINCH', + 'SIGPOLL', + 'SIGIO', + 'SIGPWR', + 'SIGSYS', + 'SIGBABY', + 'SIG_BLOCK', + 'SIG_UNBLOCK', + 'SIG_SETMASK', + ); + + /** + * @var boolean True if the daemon is in the process of exiting. + * @since 1.7.0 + */ + protected $exiting = false; + + /** + * @var integer The parent process id. + * @since 3.0.0 + */ + protected $parentId = 0; + + /** + * @var integer The process id of the daemon. + * @since 1.7.0 + */ + protected $processId = 0; + + /** + * @var boolean True if the daemon is currently running. + * @since 1.7.0 + */ + protected $running = false; + + /** + * Class constructor. + * + * @param Cli $input An optional argument to provide dependency injection for the application's + * input object. If the argument is a JInputCli object that object will become + * the application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's + * config object. If the argument is a Registry object that object will become + * the application's config object, otherwise a default config object is created. + * @param DispatcherInterface $dispatcher An optional argument to provide dependency injection for the application's + * event dispatcher. If the argument is a DispatcherInterface object that object will become + * the application's event dispatcher, if it is null then the default event dispatcher + * will be created based on the application's loadDispatcher() method. + * + * @since 1.7.0 + */ + public function __construct(Cli $input = null, Registry $config = null, DispatcherInterface $dispatcher = null) + { + // Verify that the process control extension for PHP is available. + if (!\defined('SIGHUP')) { + Log::add('The PCNTL extension for PHP is not available.', Log::ERROR); + throw new \RuntimeException('The PCNTL extension for PHP is not available.'); + } + + // Verify that POSIX support for PHP is available. + if (!\function_exists('posix_getpid')) { + Log::add('The POSIX extension for PHP is not available.', Log::ERROR); + throw new \RuntimeException('The POSIX extension for PHP is not available.'); + } + + // Call the parent constructor. + parent::__construct($input, $config, null, null, $dispatcher); + + // Set some system limits. + @set_time_limit($this->config->get('max_execution_time', 0)); + + if ($this->config->get('max_memory_limit') !== null) { + ini_set('memory_limit', $this->config->get('max_memory_limit', '256M')); + } + + // Flush content immediately. + ob_implicit_flush(); + } + + /** + * Method to handle POSIX signals. + * + * @param integer $signal The received POSIX signal. + * + * @return void + * + * @since 1.7.0 + * @see pcntl_signal() + * @throws \RuntimeException + */ + public static function signal($signal) + { + // Log all signals sent to the daemon. + Log::add('Received signal: ' . $signal, Log::DEBUG); + + // Let's make sure we have an application instance. + if (!is_subclass_of(static::$instance, CliApplication::class)) { + Log::add('Cannot find the application instance.', Log::EMERGENCY); + throw new \RuntimeException('Cannot find the application instance.'); + } + + // Fire the onReceiveSignal event. + static::$instance->triggerEvent('onReceiveSignal', array($signal)); + + switch ($signal) { + case SIGINT: + case SIGTERM: + // Handle shutdown tasks + if (static::$instance->running && static::$instance->isActive()) { + static::$instance->shutdown(); + } else { + static::$instance->close(); + } + break; + case SIGHUP: + // Handle restart tasks + if (static::$instance->running && static::$instance->isActive()) { + static::$instance->shutdown(true); + } else { + static::$instance->close(); + } + break; + case SIGCHLD: + // A child process has died + while (static::$instance->pcntlWait($signal, WNOHANG || WUNTRACED) > 0) { + usleep(1000); + } + break; + case SIGCLD: + while (static::$instance->pcntlWait($signal, WNOHANG) > 0) { + $signal = static::$instance->pcntlChildExitStatus($signal); + } + break; + default: + break; + } + } + + /** + * Check to see if the daemon is active. This does not assume that $this daemon is active, but + * only if an instance of the application is active as a daemon. + * + * @return boolean True if daemon is active. + * + * @since 1.7.0 + */ + public function isActive() + { + // Get the process id file location for the application. + $pidFile = $this->config->get('application_pid_file'); + + // If the process id file doesn't exist then the daemon is obviously not running. + if (!is_file($pidFile)) { + return false; + } + + // Read the contents of the process id file as an integer. + $fp = fopen($pidFile, 'r'); + $pid = fread($fp, filesize($pidFile)); + $pid = (int) $pid; + fclose($fp); + + // Check to make sure that the process id exists as a positive integer. + if (!$pid) { + return false; + } + + // Check to make sure the process is active by pinging it and ensure it responds. + if (!posix_kill($pid, 0)) { + // No response so remove the process id file and log the situation. + @ unlink($pidFile); + Log::add('The process found based on PID file was unresponsive.', Log::WARNING); + + return false; + } + + return true; + } + + /** + * Load an object or array into the application configuration object. + * + * @param mixed $data Either an array or object to be loaded into the configuration object. + * + * @return DaemonApplication Instance of $this to allow chaining. + * + * @since 1.7.0 + */ + public function loadConfiguration($data) + { + /* + * Setup some application metadata options. This is useful if we ever want to write out startup scripts + * or just have some sort of information available to share about things. + */ + + // The application author name. This string is used in generating startup scripts and has + // a maximum of 50 characters. + $tmp = (string) $this->config->get('author_name', 'Joomla Platform'); + $this->config->set('author_name', (\strlen($tmp) > 50) ? substr($tmp, 0, 50) : $tmp); + + // The application author email. This string is used in generating startup scripts. + $tmp = (string) $this->config->get('author_email', 'admin@joomla.org'); + $this->config->set('author_email', filter_var($tmp, FILTER_VALIDATE_EMAIL)); + + // The application name. This string is used in generating startup scripts. + $tmp = (string) $this->config->get('application_name', 'DaemonApplication'); + $this->config->set('application_name', (string) preg_replace('/[^A-Z0-9_-]/i', '', $tmp)); + + // The application description. This string is used in generating startup scripts. + $tmp = (string) $this->config->get('application_description', 'A generic Joomla Platform application.'); + $this->config->set('application_description', filter_var($tmp, FILTER_SANITIZE_STRING)); + + /* + * Setup the application path options. This defines the default executable name, executable directory, + * and also the path to the daemon process id file. + */ + + // The application executable daemon. This string is used in generating startup scripts. + $tmp = (string) $this->config->get('application_executable', basename($this->input->executable)); + $this->config->set('application_executable', $tmp); + + // The home directory of the daemon. + $tmp = (string) $this->config->get('application_directory', \dirname($this->input->executable)); + $this->config->set('application_directory', $tmp); + + // The pid file location. This defaults to a path inside the /tmp directory. + $name = $this->config->get('application_name'); + $tmp = (string) $this->config->get('application_pid_file', strtolower('/tmp/' . $name . '/' . $name . '.pid')); + $this->config->set('application_pid_file', $tmp); + + /* + * Setup the application identity options. It is important to remember if the default of 0 is set for + * either UID or GID then changing that setting will not be attempted as there is no real way to "change" + * the identity of a process from some user to root. + */ + + // The user id under which to run the daemon. + $tmp = (int) $this->config->get('application_uid', 0); + $options = array('options' => array('min_range' => 0, 'max_range' => 65000)); + $this->config->set('application_uid', filter_var($tmp, FILTER_VALIDATE_INT, $options)); + + // The group id under which to run the daemon. + $tmp = (int) $this->config->get('application_gid', 0); + $options = array('options' => array('min_range' => 0, 'max_range' => 65000)); + $this->config->set('application_gid', filter_var($tmp, FILTER_VALIDATE_INT, $options)); + + // Option to kill the daemon if it cannot switch to the chosen identity. + $tmp = (bool) $this->config->get('application_require_identity', 1); + $this->config->set('application_require_identity', $tmp); + + /* + * Setup the application runtime options. By default our execution time limit is infinite obviously + * because a daemon should be constantly running unless told otherwise. The default limit for memory + * usage is 256M, which admittedly is a little high, but remember it is a "limit" and PHP's memory + * management leaves a bit to be desired :-) + */ + + // The maximum execution time of the application in seconds. Zero is infinite. + $tmp = $this->config->get('max_execution_time'); + + if ($tmp !== null) { + $this->config->set('max_execution_time', (int) $tmp); + } + + // The maximum amount of memory the application can use. + $tmp = $this->config->get('max_memory_limit', '256M'); + + if ($tmp !== null) { + $this->config->set('max_memory_limit', (string) $tmp); + } + + return $this; + } + + /** + * Execute the daemon. + * + * @return void + * + * @since 1.7.0 + */ + public function execute() + { + // Trigger the onBeforeExecute event + $this->triggerEvent('onBeforeExecute'); + + // Enable basic garbage collection. + gc_enable(); + + Log::add('Starting ' . $this->name, Log::INFO); + + // Set off the process for becoming a daemon. + if ($this->daemonize()) { + // Declare ticks to start signal monitoring. When you declare ticks, PCNTL will monitor + // incoming signals after each tick and call the relevant signal handler automatically. + declare(ticks=1); + + // Start the main execution loop. + while (true) { + // Perform basic garbage collection. + $this->gc(); + + // Don't completely overload the CPU. + usleep(1000); + + // Execute the main application logic. + $this->doExecute(); + } + } else { + // We were not able to daemonize the application so log the failure and die gracefully. + Log::add('Starting ' . $this->name . ' failed', Log::INFO); + } + + // Trigger the onAfterExecute event. + $this->triggerEvent('onAfterExecute'); + } + + /** + * Restart daemon process. + * + * @return void + * + * @since 1.7.0 + */ + public function restart() + { + Log::add('Stopping ' . $this->name, Log::INFO); + $this->shutdown(true); + } + + /** + * Stop daemon process. + * + * @return void + * + * @since 1.7.0 + */ + public function stop() + { + Log::add('Stopping ' . $this->name, Log::INFO); + $this->shutdown(); + } + + /** + * Method to change the identity of the daemon process and resources. + * + * @return boolean True if identity successfully changed + * + * @since 1.7.0 + * @see posix_setuid() + */ + protected function changeIdentity() + { + // Get the group and user ids to set for the daemon. + $uid = (int) $this->config->get('application_uid', 0); + $gid = (int) $this->config->get('application_gid', 0); + + // Get the application process id file path. + $file = $this->config->get('application_pid_file'); + + // Change the user id for the process id file if necessary. + if ($uid && (fileowner($file) != $uid) && (!@ chown($file, $uid))) { + Log::add('Unable to change user ownership of the process id file.', Log::ERROR); + + return false; + } + + // Change the group id for the process id file if necessary. + if ($gid && (filegroup($file) != $gid) && (!@ chgrp($file, $gid))) { + Log::add('Unable to change group ownership of the process id file.', Log::ERROR); + + return false; + } + + // Set the correct home directory for the process. + if ($uid && ($info = posix_getpwuid($uid)) && is_dir($info['dir'])) { + system('export HOME="' . $info['dir'] . '"'); + } + + // Change the user id for the process necessary. + if ($uid && (posix_getuid() != $uid) && (!@ posix_setuid($uid))) { + Log::add('Unable to change user ownership of the process.', Log::ERROR); + + return false; + } + + // Change the group id for the process necessary. + if ($gid && (posix_getgid() != $gid) && (!@ posix_setgid($gid))) { + Log::add('Unable to change group ownership of the process.', Log::ERROR); + + return false; + } + + // Get the user and group information based on uid and gid. + $user = posix_getpwuid($uid); + $group = posix_getgrgid($gid); + + Log::add('Changed daemon identity to ' . $user['name'] . ':' . $group['name'], Log::INFO); + + return true; + } + + /** + * Method to put the application into the background. + * + * @return boolean + * + * @since 1.7.0 + * @throws \RuntimeException + */ + protected function daemonize() + { + // Is there already an active daemon running? + if ($this->isActive()) { + Log::add($this->name . ' daemon is still running. Exiting the application.', Log::EMERGENCY); + + return false; + } + + // Reset Process Information + $this->safeMode = !!@ ini_get('safe_mode'); + $this->processId = 0; + $this->running = false; + + // Detach process! + try { + // Check if we should run in the foreground. + if (!$this->input->get('f')) { + // Detach from the terminal. + $this->detach(); + } else { + // Setup running values. + $this->exiting = false; + $this->running = true; + + // Set the process id. + $this->processId = (int) posix_getpid(); + $this->parentId = $this->processId; + } + } catch (\RuntimeException $e) { + Log::add('Unable to fork.', Log::EMERGENCY); + + return false; + } + + // Verify the process id is valid. + if ($this->processId < 1) { + Log::add('The process id is invalid; the fork failed.', Log::EMERGENCY); + + return false; + } + + // Clear the umask. + @ umask(0); + + // Write out the process id file for concurrency management. + if (!$this->writeProcessIdFile()) { + Log::add('Unable to write the pid file at: ' . $this->config->get('application_pid_file'), Log::EMERGENCY); + + return false; + } + + // Attempt to change the identity of user running the process. + if (!$this->changeIdentity()) { + // If the identity change was required then we need to return false. + if ($this->config->get('application_require_identity')) { + Log::add('Unable to change process owner.', Log::CRITICAL); + + return false; + } else { + Log::add('Unable to change process owner.', Log::WARNING); + } + } + + // Setup the signal handlers for the daemon. + if (!$this->setupSignalHandlers()) { + return false; + } + + // Change the current working directory to the application working directory. + @ chdir($this->config->get('application_directory')); + + return true; + } + + /** + * This is truly where the magic happens. This is where we fork the process and kill the parent + * process, which is essentially what turns the application into a daemon. + * + * @return void + * + * @since 3.0.0 + * @throws \RuntimeException + */ + protected function detach() + { + Log::add('Detaching the ' . $this->name . ' daemon.', Log::DEBUG); + + // Attempt to fork the process. + $pid = $this->fork(); + + // If the pid is positive then we successfully forked, and can close this application. + if ($pid) { + // Add the log entry for debugging purposes and exit gracefully. + Log::add('Ending ' . $this->name . ' parent process', Log::DEBUG); + $this->close(); + } else { + // We are in the forked child process. + // Setup some protected values. + $this->exiting = false; + $this->running = true; + + // Set the parent to self. + $this->parentId = $this->processId; + } + } + + /** + * Method to fork the process. + * + * @return integer The child process id to the parent process, zero to the child process. + * + * @since 1.7.0 + * @throws \RuntimeException + */ + protected function fork() + { + // Attempt to fork the process. + $pid = $this->pcntlFork(); + + // If the fork failed, throw an exception. + if ($pid === -1) { + throw new \RuntimeException('The process could not be forked.'); + } elseif ($pid === 0) { + // Update the process id for the child. + $this->processId = (int) posix_getpid(); + } else { + // Log the fork in the parent. + // Log the fork. + Log::add('Process forked ' . $pid, Log::DEBUG); + } + + // Trigger the onFork event. + $this->postFork(); + + return $pid; + } + + /** + * Method to perform basic garbage collection and memory management in the sense of clearing the + * stat cache. We will probably call this method pretty regularly in our main loop. + * + * @return void + * + * @since 1.7.0 + */ + protected function gc() + { + // Perform generic garbage collection. + gc_collect_cycles(); + + // Clear the stat cache so it doesn't blow up memory. + clearstatcache(); + } + + /** + * Method to attach the DaemonApplication signal handler to the known signals. Applications + * can override these handlers by using the pcntl_signal() function and attaching a different + * callback method. + * + * @return boolean + * + * @since 1.7.0 + * @see pcntl_signal() + */ + protected function setupSignalHandlers() + { + // We add the error suppression for the loop because on some platforms some constants are not defined. + foreach (self::$signals as $signal) { + // Ignore signals that are not defined. + if (!\defined($signal) || !\is_int(\constant($signal)) || (\constant($signal) === 0)) { + // Define the signal to avoid notices. + Log::add('Signal "' . $signal . '" not defined. Defining it as null.', Log::DEBUG); + \define($signal, null); + + // Don't listen for signal. + continue; + } + + // Attach the signal handler for the signal. + if (!$this->pcntlSignal(\constant($signal), array('DaemonApplication', 'signal'))) { + Log::add(sprintf('Unable to reroute signal handler: %s', $signal), Log::EMERGENCY); + + return false; + } + } + + return true; + } + + /** + * Method to shut down the daemon and optionally restart it. + * + * @param boolean $restart True to restart the daemon on exit. + * + * @return void + * + * @since 1.7.0 + */ + protected function shutdown($restart = false) + { + // If we are already exiting, chill. + if ($this->exiting) { + return; + } else { + // If not, now we are. + $this->exiting = true; + } + + // If we aren't already daemonized then just kill the application. + if (!$this->running && !$this->isActive()) { + Log::add('Process was not daemonized yet, just halting current process', Log::INFO); + $this->close(); + } + + // Only read the pid for the parent file. + if ($this->parentId == $this->processId) { + // Read the contents of the process id file as an integer. + $fp = fopen($this->config->get('application_pid_file'), 'r'); + $pid = fread($fp, filesize($this->config->get('application_pid_file'))); + $pid = (int) $pid; + fclose($fp); + + // Remove the process id file. + @ unlink($this->config->get('application_pid_file')); + + // If we are supposed to restart the daemon we need to execute the same command. + if ($restart) { + $this->close(exec(implode(' ', $GLOBALS['argv']) . ' > /dev/null &')); + } else { + // If we are not supposed to restart the daemon let's just kill -9. + passthru('kill -9 ' . $pid); + $this->close(); + } + } + } + + /** + * Method to write the process id file out to disk. + * + * @return boolean + * + * @since 1.7.0 + */ + protected function writeProcessIdFile() + { + // Verify the process id is valid. + if ($this->processId < 1) { + Log::add('The process id is invalid.', Log::EMERGENCY); + + return false; + } + + // Get the application process id file path. + $file = $this->config->get('application_pid_file'); + + if (empty($file)) { + Log::add('The process id file path is empty.', Log::ERROR); + + return false; + } + + // Make sure that the folder where we are writing the process id file exists. + $folder = \dirname($file); + + if (!is_dir($folder) && !Folder::create($folder)) { + Log::add('Unable to create directory: ' . $folder, Log::ERROR); + + return false; + } + + // Write the process id file out to disk. + if (!file_put_contents($file, $this->processId)) { + Log::add('Unable to write process id file: ' . $file, Log::ERROR); + + return false; + } + + // Make sure the permissions for the process id file are accurate. + if (!chmod($file, 0644)) { + Log::add('Unable to adjust permissions for the process id file: ' . $file, Log::ERROR); + + return false; + } + + return true; + } + + /** + * Method to handle post-fork triggering of the onFork event. + * + * @return void + * + * @since 3.0.0 + */ + protected function postFork() + { + // Trigger the onFork event. + $this->triggerEvent('onFork'); + } + + /** + * Method to return the exit code of a terminated child process. + * + * @param integer $status The status parameter is the status parameter supplied to a successful call to pcntl_waitpid(). + * + * @return integer The child process exit code. + * + * @see pcntl_wexitstatus() + * @since 1.7.3 + */ + protected function pcntlChildExitStatus($status) + { + return pcntl_wexitstatus($status); + } + + /** + * Method to return the exit code of a terminated child process. + * + * @return integer On success, the PID of the child process is returned in the parent's thread + * of execution, and a 0 is returned in the child's thread of execution. On + * failure, a -1 will be returned in the parent's context, no child process + * will be created, and a PHP error is raised. + * + * @see pcntl_fork() + * @since 1.7.3 + */ + protected function pcntlFork() + { + return pcntl_fork(); + } + + /** + * Method to install a signal handler. + * + * @param integer $signal The signal number. + * @param callable $handler The signal handler which may be the name of a user created function, + * or method, or either of the two global constants SIG_IGN or SIG_DFL. + * @param boolean $restart Specifies whether system call restarting should be used when this + * signal arrives. + * + * @return boolean True on success. + * + * @see pcntl_signal() + * @since 1.7.3 + */ + protected function pcntlSignal($signal, $handler, $restart = true) + { + return pcntl_signal($signal, $handler, $restart); + } + + /** + * Method to wait on or return the status of a forked child. + * + * @param integer &$status Status information. + * @param integer $options If wait3 is available on your system (mostly BSD-style systems), + * you can provide the optional options parameter. + * + * @return integer The process ID of the child which exited, -1 on error or zero if WNOHANG + * was provided as an option (on wait3-available systems) and no child was available. + * + * @see pcntl_wait() + * @since 1.7.3 + */ + protected function pcntlWait(&$status, $options = 0) + { + return pcntl_wait($status, $options); + } } diff --git a/libraries/src/Application/EventAware.php b/libraries/src/Application/EventAware.php index d1020858129a5..062e6fa2427a2 100644 --- a/libraries/src/Application/EventAware.php +++ b/libraries/src/Application/EventAware.php @@ -1,4 +1,5 @@ getDispatcher()->addListener($event, $handler); - } - catch (\UnexpectedValueException $e) - { - // No dispatcher is registered, don't throw an error (mimics old behavior) - } + /** + * Registers a handler to a particular event group. + * + * @param string $event The event name. + * @param callable $handler The handler, a function or an instance of an event object. + * + * @return $this + * + * @since 4.0.0 + */ + public function registerEvent($event, callable $handler) + { + try { + $this->getDispatcher()->addListener($event, $handler); + } catch (\UnexpectedValueException $e) { + // No dispatcher is registered, don't throw an error (mimics old behavior) + } - return $this; - } + return $this; + } - /** - * Calls all handlers associated with an event group. - * - * This is a legacy method, implementing old-style (Joomla! 3.x) plugin calls. It's best to go directly through the - * Dispatcher and handle the returned EventInterface object instead of going through this method. This method is - * deprecated and will be removed in Joomla! 5.x. - * - * This method will only return the 'result' argument of the event - * - * @param string $eventName The event name. - * @param array|Event $args An array of arguments or an Event object (optional). - * - * @return array An array of results from each function call. Note this will be an empty array if no dispatcher is set. - * - * @since 4.0.0 - * @throws \InvalidArgumentException - * @deprecated 5.0 - */ - public function triggerEvent($eventName, $args = []) - { - try - { - $dispatcher = $this->getDispatcher(); - } - catch (\UnexpectedValueException $exception) - { - $this->getLogger()->error(sprintf('Dispatcher not set in %s, cannot trigger events.', \get_class($this))); + /** + * Calls all handlers associated with an event group. + * + * This is a legacy method, implementing old-style (Joomla! 3.x) plugin calls. It's best to go directly through the + * Dispatcher and handle the returned EventInterface object instead of going through this method. This method is + * deprecated and will be removed in Joomla! 5.x. + * + * This method will only return the 'result' argument of the event + * + * @param string $eventName The event name. + * @param array|Event $args An array of arguments or an Event object (optional). + * + * @return array An array of results from each function call. Note this will be an empty array if no dispatcher is set. + * + * @since 4.0.0 + * @throws \InvalidArgumentException + * @deprecated 5.0 + */ + public function triggerEvent($eventName, $args = []) + { + try { + $dispatcher = $this->getDispatcher(); + } catch (\UnexpectedValueException $exception) { + $this->getLogger()->error(sprintf('Dispatcher not set in %s, cannot trigger events.', \get_class($this))); - return []; - } + return []; + } - if ($args instanceof Event) - { - $event = $args; - } - elseif (\is_array($args)) - { - $className = self::getEventClassByEventName($eventName); - $event = new $className($eventName, $args); - } - else - { - throw new \InvalidArgumentException('The arguments must either be an event or an array'); - } + if ($args instanceof Event) { + $event = $args; + } elseif (\is_array($args)) { + $className = self::getEventClassByEventName($eventName); + $event = new $className($eventName, $args); + } else { + throw new \InvalidArgumentException('The arguments must either be an event or an array'); + } - $result = $dispatcher->dispatch($eventName, $event); + $result = $dispatcher->dispatch($eventName, $event); - // @todo - There are still test cases where the result isn't defined, temporarily leave the isset check in place - return !isset($result['result']) || \is_null($result['result']) ? [] : $result['result']; - } + // @todo - There are still test cases where the result isn't defined, temporarily leave the isset check in place + return !isset($result['result']) || \is_null($result['result']) ? [] : $result['result']; + } } diff --git a/libraries/src/Application/EventAwareInterface.php b/libraries/src/Application/EventAwareInterface.php index f8bb168176af4..e650467bdc7b2 100644 --- a/libraries/src/Application/EventAwareInterface.php +++ b/libraries/src/Application/EventAwareInterface.php @@ -1,4 +1,5 @@ load(); - } + /** + * Allows the application to load a custom or default identity. + * + * @return void + * + * @since 4.0.0 + */ + public function createExtensionNamespaceMap() + { + JLoader::register('JNamespacePsr4Map', JPATH_LIBRARIES . '/namespacemap.php'); + $extensionPsr4Loader = new \JNamespacePsr4Map(); + $extensionPsr4Loader->load(); + } } diff --git a/libraries/src/Application/IdentityAware.php b/libraries/src/Application/IdentityAware.php index 03152ac51f31b..f79cc720828e1 100644 --- a/libraries/src/Application/IdentityAware.php +++ b/libraries/src/Application/IdentityAware.php @@ -1,4 +1,5 @@ identity; - } + /** + * Get the application identity. + * + * @return User + * + * @since 4.0.0 + */ + public function getIdentity() + { + return $this->identity; + } - /** - * Allows the application to load a custom or default identity. - * - * @param User $identity An optional identity object. If omitted, a null user object is created. - * - * @return $this - * - * @since 4.0.0 - */ - public function loadIdentity(User $identity = null) - { - $this->identity = $identity ?: $this->userFactory->loadUserById(0); + /** + * Allows the application to load a custom or default identity. + * + * @param User $identity An optional identity object. If omitted, a null user object is created. + * + * @return $this + * + * @since 4.0.0 + */ + public function loadIdentity(User $identity = null) + { + $this->identity = $identity ?: $this->userFactory->loadUserById(0); - return $this; - } + return $this; + } - /** - * Set the user factory to use. - * - * @param UserFactoryInterface $userFactory The user factory to use - * - * @return void - * - * @since 4.0.0 - */ - public function setUserFactory(UserFactoryInterface $userFactory) - { - $this->userFactory = $userFactory; - } + /** + * Set the user factory to use. + * + * @param UserFactoryInterface $userFactory The user factory to use + * + * @return void + * + * @since 4.0.0 + */ + public function setUserFactory(UserFactoryInterface $userFactory) + { + $this->userFactory = $userFactory; + } } diff --git a/libraries/src/Application/MultiFactorAuthenticationHandler.php b/libraries/src/Application/MultiFactorAuthenticationHandler.php index c991cb960a86d..1e3173442f0bb 100644 --- a/libraries/src/Application/MultiFactorAuthenticationHandler.php +++ b/libraries/src/Application/MultiFactorAuthenticationHandler.php @@ -1,4 +1,5 @@ getIdentity() ?? null; - } - catch (Exception $e) - { - return false; - } - - if (!($user instanceof User) || $user->guest) - { - return false; - } - - // If there is no need for a redirection I must not proceed - if (!$this->needsMultiFactorAuthenticationRedirection()) - { - return false; - } - - /** - * Automatically migrate from legacy MFA, if needed. - * - * We prefer to do a user-by-user migration instead of migrating everybody on Joomla update - * for practical reasons. On a site with hundreds or thousands of users the migration could - * take several minutes, causing Joomla Update to time out. - * - * Instead, every time we are in a captive Multi-factor Authentication page (captive MFA login - * or captive forced MFA setup) we spend a few milliseconds to check if a migration is - * necessary. If it's necessary, we perform it. - * - * The captive pages don't load any content or modules, therefore the few extra milliseconds - * we spend here are not a big deal. A failed all-users migration which would stop Joomla - * Update dead in its tracks would, however, be a big deal (broken sites). Moreover, a - * migration that has to be initiated by the site owner would also be a big deal — if they - * did not know they need to do it none of their users who had previously enabled MFA would - * now have it enabled! - * - * To paraphrase Otto von Bismarck: programming, like politics, is the art of the possible, - * the attainable -- the art of the next best. - */ - $this->migrateFromLegacyMFA(); - - // We only kick in when the user has actually set up MFA or must definitely enable MFA. - $userOptions = ComponentHelper::getParams('com_users'); - $neverMFAUserGroups = $userOptions->get('neverMFAUserGroups', []); - $forceMFAUserGroups = $userOptions->get('forceMFAUserGroups', []); - $isMFADisallowed = count( - array_intersect( - is_array($neverMFAUserGroups) ? $neverMFAUserGroups : [], - $user->getAuthorisedGroups() - ) - ) >= 1; - $isMFAMandatory = count( - array_intersect( - is_array($forceMFAUserGroups) ? $forceMFAUserGroups : [], - $user->getAuthorisedGroups() - ) - ) >= 1; - $isMFADisallowed = $isMFADisallowed && !$isMFAMandatory; - $isMFAPending = $this->isMultiFactorAuthenticationPending(); - $session = $this->getSession(); - $isNonHtml = $this->input->getCmd('format', 'html') !== 'html'; - - // Prevent non-interactive (non-HTML) content from being loaded until MFA is validated. - if ($isMFAPending && $isNonHtml) - { - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - if ($isMFAPending && !$isMFADisallowed) - { - /** - * Saves the current URL as the return URL if all of the following conditions apply - * - It is not a URL to com_users' MFA feature itself - * - A return URL does not already exist, is imperfect or external to the site - * - * If no return URL has been set up and the current URL is com_users' MFA feature - * we will save the home page as the redirect target. - */ - $returnUrl = $session->get('com_users.return_url', ''); - - if (empty($returnUrl) || !Uri::isInternal($returnUrl)) - { - $returnUrl = $this->isMultiFactorAuthenticationPage() - ? Uri::base() - : Uri::getInstance()->toString(['scheme', 'user', 'pass', 'host', 'port', 'path', 'query', 'fragment']); - $session->set('com_users.return_url', $returnUrl); - } - - // Redirect - $this->redirect(Route::_('index.php?option=com_users&view=captive', false), 307); - } - - // If we're here someone just logged in but does not have MFA set up. Just flag him as logged in and continue. - $session->set('com_users.mfa_checked', 1); - - // If the user is in a group that requires MFA we will redirect them to the setup page. - if (!$isMFAPending && $isMFAMandatory) - { - // First unset the flag to make sure the redirection will apply until they conform to the mandatory MFA - $session->set('com_users.mfa_checked', 0); - - // Now set a flag which forces rechecking MFA for this user - $session->set('com_users.mandatory_mfa_setup', 1); - - // Then redirect them to the setup page - if (!$this->isMultiFactorAuthenticationPage()) - { - $url = Route::_('index.php?option=com_users&view=methods', false); - $this->redirect($url, 307); - } - } - - // Do I need to redirect the user to the MFA setup page after they have fully logged in? - $hasRejectedMultiFactorAuthenticationSetup = $this->hasRejectedMultiFactorAuthenticationSetup() && !$isMFAMandatory; - - if (!$isMFAPending && !$isMFADisallowed && ($userOptions->get('mfaredirectonlogin', 0) == 1) - && !$user->guest && !$hasRejectedMultiFactorAuthenticationSetup && !empty(MfaHelper::getMfaMethods())) - { - $this->redirect( - $userOptions->get('mfaredirecturl', '') ?: - Route::_('index.php?option=com_users&view=methods&layout=firsttime', false) - ); - } - - return true; - } - - /** - * Does the current user need to complete MFA authentication before being allowed to access the site? - * - * @return boolean - * @throws Exception - * @since 4.2.0 - */ - private function isMultiFactorAuthenticationPending(): bool - { - $user = $this->getIdentity(); - - if (empty($user) || $user->guest) - { - return false; - } - - // Get the user's MFA records - $records = MfaHelper::getUserMfaRecords($user->id); - - // No MFA Methods? Then we obviously don't need to display a Captive login page. - if (count($records) < 1) - { - return false; - } - - // Let's get a list of all currently active MFA Methods - $mfaMethods = MfaHelper::getMfaMethods(); - - // If no MFA Method is active we can't really display a Captive login page. - if (empty($mfaMethods)) - { - return false; - } - - // Get a list of just the Method names - $methodNames = []; - - foreach ($mfaMethods as $mfaMethod) - { - $methodNames[] = $mfaMethod['name']; - } - - // Filter the records based on currently active MFA Methods - foreach ($records as $record) - { - if (in_array($record->method, $methodNames)) - { - // We found an active Method. Show the Captive page. - return true; - } - } - - // No viable MFA Method found. We won't show the Captive page. - return false; - } - - /** - * Check whether we'll need to do a redirection to the Multi-factor Authentication captive page. - * - * @return boolean - * @since 4.2.0 - */ - private function needsMultiFactorAuthenticationRedirection(): bool - { - $isAdmin = $this->isClient('administrator'); - - /** - * We only kick in if the session flag is not set AND the user is not flagged for monitoring of their MFA status - * - * In case a user belongs to a group which requires MFA to be always enabled and they logged in without having - * MFA enabled we have the recheck flag. This prevents the user from enabling and immediately disabling MFA, - * circumventing the requirement for MFA. - */ - $session = $this->getSession(); - $isMFAComplete = $session->get('com_users.mfa_checked', 0) != 0; - $isMFASetupMandatory = $session->get('com_users.mandatory_mfa_setup', 0) != 0; - - if ($isMFAComplete && !$isMFASetupMandatory) - { - return false; - } - - // Make sure we are logged in - try - { - $user = $this->getIdentity(); - } - catch (Exception $e) - { - // This would happen if we are in CLI or under an old Joomla! version. Either case is not supported. - return false; - } - - // The plugin only needs to kick in when you have logged in - if (empty($user) || $user->guest) - { - return false; - } - - // If we are in the administrator section we only kick in when the user has backend access privileges - if ($isAdmin && !$user->authorise('core.login.admin')) - { - // @todo How exactly did you end up here if you didn't have the core.login.admin privilege to begin with?! - return false; - } - - // Do not redirect if we are already in a MFA management or captive page - if ($this->isMultiFactorAuthenticationPage()) - { - return false; - } - - $option = strtolower($this->input->getCmd('option', '')); - $task = strtolower($this->input->getCmd('task', '')); - - // Allow the frontend user to log out (in case they forgot their MFA code or something) - if (!$isAdmin && ($option == 'com_users') && in_array($task, ['user.logout', 'user.menulogout'])) - { - return false; - } - - // Allow the backend user to log out (in case they forgot their MFA code or something) - if ($isAdmin && ($option == 'com_login') && ($task == 'logout')) - { - return false; - } - - // Allow the Joomla update finalisation to run - if ($isAdmin && $option === 'com_joomlaupdate' && in_array($task, ['update.finalise', 'update.cleanup', 'update.finaliseconfirm'])) - { - return false; - } - - return true; - } - - /** - * Is this a page concerning the Multi-factor Authentication feature? - * - * @return boolean - * @since 4.2.0 - */ - private function isMultiFactorAuthenticationPage(): bool - { - $option = $this->input->get('option'); - $task = $this->input->get('task'); - $view = $this->input->get('view'); - - if ($option !== 'com_users') - { - return false; - } - - $allowedViews = ['captive', 'method', 'methods', 'callback']; - $allowedTasks = [ - 'captive.display', 'captive.captive', 'captive.validate', - 'method.display', 'method.add', 'method.edit', 'method.regenerateBackupCodes', 'method.delete', 'method.save', - 'methods.display', 'methods.disable', 'methods.doNotShowThisAgain', - ]; - - return in_array($view, $allowedViews) || in_array($task, $allowedTasks); - } - - /** - * Does the user have a "don't show this again" flag? - * - * @return boolean - * @since 4.2.0 - */ - private function hasRejectedMultiFactorAuthenticationSetup(): bool - { - $user = $this->getIdentity(); - $profileKey = 'mfa.dontshow'; - /** @var DatabaseDriver $db */ - $db = Factory::getContainer()->get('DatabaseDriver'); - $query = $db->getQuery(true) - ->select($db->quoteName('profile_value')) - ->from($db->quoteName('#__user_profiles')) - ->where($db->quoteName('user_id') . ' = :userId') - ->where($db->quoteName('profile_key') . ' = :profileKey') - ->bind(':userId', $user->id, ParameterType::INTEGER) - ->bind(':profileKey', $profileKey); - - try - { - $result = $db->setQuery($query)->loadResult(); - } - catch (Exception $e) - { - $result = 1; - } - - return $result == 1; - } - - /** - * Automatically migrates a user's legacy MFA records into the new Captive MFA format. - * - * @return void - * @since 4.2.0 - */ - private function migrateFromLegacyMFA(): void - { - $user = $this->getIdentity(); - - if (!($user instanceof User) || $user->guest || $user->id <= 0) - { - return; - } - - /** @var DatabaseDriver $db */ - $db = Factory::getContainer()->get('DatabaseDriver'); - - $userTable = new UserTable($db); - - if (!$userTable->load($user->id) || empty($userTable->otpKey)) - { - return; - } - - [$otpMethod, $otpKey] = explode(':', $userTable->otpKey, 2); - $secret = $this->get('secret'); - $otpKey = $this->decryptLegacyTFAString($secret, $otpKey); - $otep = $this->decryptLegacyTFAString($secret, $userTable->otep); - $config = @json_decode($otpKey, true); - $hasConverted = true; - - if (!empty($config)) - { - switch ($otpMethod) - { - case 'totp': - $this->getLanguage()->load('plg_multifactorauth_totp', JPATH_ADMINISTRATOR); - - (new MfaTable($db))->save( - [ - 'user_id' => $user->id, - 'title' => Text::_('PLG_MULTIFACTORAUTH_TOTP_METHOD_TITLE'), - 'method' => 'totp', - 'default' => 0, - 'created_on' => Date::getInstance()->toSql(), - 'last_used' => null, - 'options' => ['key' => $config['code']], - ] - ); - break; - - case 'yubikey': - $this->getLanguage()->load('plg_multifactorauth_yubikey', JPATH_ADMINISTRATOR); - - (new MfaTable($db))->save( - [ - 'user_id' => $user->id, - 'title' => sprintf("%s %s", Text::_('PLG_MULTIFACTORAUTH_YUBIKEY_METHOD_TITLE'), $config['yubikey']), - 'method' => 'yubikey', - 'default' => 0, - 'created_on' => Date::getInstance()->toSql(), - 'last_used' => null, - 'options' => ['id' => $config['yubikey']], - ] - ); - break; - - default: - $hasConverted = false; - break; - } - } - - // Convert the emergency codes - if ($hasConverted && !empty(@json_decode($otep, true))) - { - // Delete any other record with the same user_id and Method. - $method = 'emergencycodes'; - $userId = $user->id; - $query = $db->getQuery(true) - ->delete($db->qn('#__user_mfa')) - ->where($db->qn('user_id') . ' = :user_id') - ->where($db->qn('method') . ' = :method') - ->bind(':user_id', $userId, ParameterType::INTEGER) - ->bind(':method', $method); - $db->setQuery($query)->execute(); - - // Migrate data - (new MfaTable($db))->save( - [ - 'user_id' => $user->id, - 'title' => Text::_('COM_USERS_USER_BACKUPCODES'), - 'method' => 'backupcodes', - 'default' => 0, - 'created_on' => Date::getInstance()->toSql(), - 'last_used' => null, - 'options' => @json_decode($otep, true), - ] - ); - } - - // Remove the legacy MFA - $update = (object) [ - 'id' => $user->id, - 'otpKey' => '', - 'otep' => '', - ]; - $db->updateObject('#__users', $update, ['id']); - } - - /** - * Tries to decrypt the legacy MFA configuration. - * - * @param string $secret Site's secret key - * @param string $stringToDecrypt Base64-encoded and encrypted, JSON-encoded information - * - * @return string Decrypted, but JSON-encoded, information - * - * @see https://github.com/joomla/joomla-cms/pull/12497 - * @since 4.2.0 - */ - private function decryptLegacyTFAString(string $secret, string $stringToDecrypt): string - { - // Is this already decrypted? - try - { - $decrypted = @json_decode($stringToDecrypt, true); - } - catch (Exception $e) - { - $decrypted = null; - } - - if (!empty($decrypted)) - { - return $stringToDecrypt; - } - - // No, we need to decrypt the string - $aes = new Aes($secret, 256); - $decrypted = $aes->decryptString($stringToDecrypt); - - if (!is_string($decrypted) || empty($decrypted)) - { - $aes->setPassword($secret, true); - - $decrypted = $aes->decryptString($stringToDecrypt); - } - - if (!is_string($decrypted) || empty($decrypted)) - { - return ''; - } - - // Remove the null padding added during encryption - return rtrim($decrypted, "\0"); - } + /** + * Handle the redirection to the Multi-factor Authentication captive login or setup page. + * + * @return boolean True if we are currently handling a Multi-factor Authentication captive page. + * @throws Exception + * @since 4.2.0 + */ + protected function isHandlingMultiFactorAuthentication(): bool + { + // Multi-factor Authentication checks take place only for logged in users. + try { + $user = $this->getIdentity() ?? null; + } catch (Exception $e) { + return false; + } + + if (!($user instanceof User) || $user->guest) { + return false; + } + + // If there is no need for a redirection I must not proceed + if (!$this->needsMultiFactorAuthenticationRedirection()) { + return false; + } + + /** + * Automatically migrate from legacy MFA, if needed. + * + * We prefer to do a user-by-user migration instead of migrating everybody on Joomla update + * for practical reasons. On a site with hundreds or thousands of users the migration could + * take several minutes, causing Joomla Update to time out. + * + * Instead, every time we are in a captive Multi-factor Authentication page (captive MFA login + * or captive forced MFA setup) we spend a few milliseconds to check if a migration is + * necessary. If it's necessary, we perform it. + * + * The captive pages don't load any content or modules, therefore the few extra milliseconds + * we spend here are not a big deal. A failed all-users migration which would stop Joomla + * Update dead in its tracks would, however, be a big deal (broken sites). Moreover, a + * migration that has to be initiated by the site owner would also be a big deal — if they + * did not know they need to do it none of their users who had previously enabled MFA would + * now have it enabled! + * + * To paraphrase Otto von Bismarck: programming, like politics, is the art of the possible, + * the attainable -- the art of the next best. + */ + $this->migrateFromLegacyMFA(); + + // We only kick in when the user has actually set up MFA or must definitely enable MFA. + $userOptions = ComponentHelper::getParams('com_users'); + $neverMFAUserGroups = $userOptions->get('neverMFAUserGroups', []); + $forceMFAUserGroups = $userOptions->get('forceMFAUserGroups', []); + $isMFADisallowed = count( + array_intersect( + is_array($neverMFAUserGroups) ? $neverMFAUserGroups : [], + $user->getAuthorisedGroups() + ) + ) >= 1; + $isMFAMandatory = count( + array_intersect( + is_array($forceMFAUserGroups) ? $forceMFAUserGroups : [], + $user->getAuthorisedGroups() + ) + ) >= 1; + $isMFADisallowed = $isMFADisallowed && !$isMFAMandatory; + $isMFAPending = $this->isMultiFactorAuthenticationPending(); + $session = $this->getSession(); + $isNonHtml = $this->input->getCmd('format', 'html') !== 'html'; + + // Prevent non-interactive (non-HTML) content from being loaded until MFA is validated. + if ($isMFAPending && $isNonHtml) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + if ($isMFAPending && !$isMFADisallowed) { + /** + * Saves the current URL as the return URL if all of the following conditions apply + * - It is not a URL to com_users' MFA feature itself + * - A return URL does not already exist, is imperfect or external to the site + * + * If no return URL has been set up and the current URL is com_users' MFA feature + * we will save the home page as the redirect target. + */ + $returnUrl = $session->get('com_users.return_url', ''); + + if (empty($returnUrl) || !Uri::isInternal($returnUrl)) { + $returnUrl = $this->isMultiFactorAuthenticationPage() + ? Uri::base() + : Uri::getInstance()->toString(['scheme', 'user', 'pass', 'host', 'port', 'path', 'query', 'fragment']); + $session->set('com_users.return_url', $returnUrl); + } + + // Redirect + $this->redirect(Route::_('index.php?option=com_users&view=captive', false), 307); + } + + // If we're here someone just logged in but does not have MFA set up. Just flag him as logged in and continue. + $session->set('com_users.mfa_checked', 1); + + // If the user is in a group that requires MFA we will redirect them to the setup page. + if (!$isMFAPending && $isMFAMandatory) { + // First unset the flag to make sure the redirection will apply until they conform to the mandatory MFA + $session->set('com_users.mfa_checked', 0); + + // Now set a flag which forces rechecking MFA for this user + $session->set('com_users.mandatory_mfa_setup', 1); + + // Then redirect them to the setup page + if (!$this->isMultiFactorAuthenticationPage()) { + $url = Route::_('index.php?option=com_users&view=methods', false); + $this->redirect($url, 307); + } + } + + // Do I need to redirect the user to the MFA setup page after they have fully logged in? + $hasRejectedMultiFactorAuthenticationSetup = $this->hasRejectedMultiFactorAuthenticationSetup() && !$isMFAMandatory; + + if ( + !$isMFAPending && !$isMFADisallowed && ($userOptions->get('mfaredirectonlogin', 0) == 1) + && !$user->guest && !$hasRejectedMultiFactorAuthenticationSetup && !empty(MfaHelper::getMfaMethods()) + ) { + $this->redirect( + $userOptions->get('mfaredirecturl', '') ?: + Route::_('index.php?option=com_users&view=methods&layout=firsttime', false) + ); + } + + return true; + } + + /** + * Does the current user need to complete MFA authentication before being allowed to access the site? + * + * @return boolean + * @throws Exception + * @since 4.2.0 + */ + private function isMultiFactorAuthenticationPending(): bool + { + $user = $this->getIdentity(); + + if (empty($user) || $user->guest) { + return false; + } + + // Get the user's MFA records + $records = MfaHelper::getUserMfaRecords($user->id); + + // No MFA Methods? Then we obviously don't need to display a Captive login page. + if (count($records) < 1) { + return false; + } + + // Let's get a list of all currently active MFA Methods + $mfaMethods = MfaHelper::getMfaMethods(); + + // If no MFA Method is active we can't really display a Captive login page. + if (empty($mfaMethods)) { + return false; + } + + // Get a list of just the Method names + $methodNames = []; + + foreach ($mfaMethods as $mfaMethod) { + $methodNames[] = $mfaMethod['name']; + } + + // Filter the records based on currently active MFA Methods + foreach ($records as $record) { + if (in_array($record->method, $methodNames)) { + // We found an active Method. Show the Captive page. + return true; + } + } + + // No viable MFA Method found. We won't show the Captive page. + return false; + } + + /** + * Check whether we'll need to do a redirection to the Multi-factor Authentication captive page. + * + * @return boolean + * @since 4.2.0 + */ + private function needsMultiFactorAuthenticationRedirection(): bool + { + $isAdmin = $this->isClient('administrator'); + + /** + * We only kick in if the session flag is not set AND the user is not flagged for monitoring of their MFA status + * + * In case a user belongs to a group which requires MFA to be always enabled and they logged in without having + * MFA enabled we have the recheck flag. This prevents the user from enabling and immediately disabling MFA, + * circumventing the requirement for MFA. + */ + $session = $this->getSession(); + $isMFAComplete = $session->get('com_users.mfa_checked', 0) != 0; + $isMFASetupMandatory = $session->get('com_users.mandatory_mfa_setup', 0) != 0; + + if ($isMFAComplete && !$isMFASetupMandatory) { + return false; + } + + // Make sure we are logged in + try { + $user = $this->getIdentity(); + } catch (Exception $e) { + // This would happen if we are in CLI or under an old Joomla! version. Either case is not supported. + return false; + } + + // The plugin only needs to kick in when you have logged in + if (empty($user) || $user->guest) { + return false; + } + + // If we are in the administrator section we only kick in when the user has backend access privileges + if ($isAdmin && !$user->authorise('core.login.admin')) { + // @todo How exactly did you end up here if you didn't have the core.login.admin privilege to begin with?! + return false; + } + + // Do not redirect if we are already in a MFA management or captive page + if ($this->isMultiFactorAuthenticationPage()) { + return false; + } + + $option = strtolower($this->input->getCmd('option', '')); + $task = strtolower($this->input->getCmd('task', '')); + + // Allow the frontend user to log out (in case they forgot their MFA code or something) + if (!$isAdmin && ($option == 'com_users') && in_array($task, ['user.logout', 'user.menulogout'])) { + return false; + } + + // Allow the backend user to log out (in case they forgot their MFA code or something) + if ($isAdmin && ($option == 'com_login') && ($task == 'logout')) { + return false; + } + + // Allow the Joomla update finalisation to run + if ($isAdmin && $option === 'com_joomlaupdate' && in_array($task, ['update.finalise', 'update.cleanup', 'update.finaliseconfirm'])) { + return false; + } + + return true; + } + + /** + * Is this a page concerning the Multi-factor Authentication feature? + * + * @return boolean + * @since 4.2.0 + */ + private function isMultiFactorAuthenticationPage(): bool + { + $option = $this->input->get('option'); + $task = $this->input->get('task'); + $view = $this->input->get('view'); + + if ($option !== 'com_users') { + return false; + } + + $allowedViews = ['captive', 'method', 'methods', 'callback']; + $allowedTasks = [ + 'captive.display', 'captive.captive', 'captive.validate', + 'method.display', 'method.add', 'method.edit', 'method.regenerateBackupCodes', 'method.delete', 'method.save', + 'methods.display', 'methods.disable', 'methods.doNotShowThisAgain', + ]; + + return in_array($view, $allowedViews) || in_array($task, $allowedTasks); + } + + /** + * Does the user have a "don't show this again" flag? + * + * @return boolean + * @since 4.2.0 + */ + private function hasRejectedMultiFactorAuthenticationSetup(): bool + { + $user = $this->getIdentity(); + $profileKey = 'mfa.dontshow'; + /** @var DatabaseDriver $db */ + $db = Factory::getContainer()->get('DatabaseDriver'); + $query = $db->getQuery(true) + ->select($db->quoteName('profile_value')) + ->from($db->quoteName('#__user_profiles')) + ->where($db->quoteName('user_id') . ' = :userId') + ->where($db->quoteName('profile_key') . ' = :profileKey') + ->bind(':userId', $user->id, ParameterType::INTEGER) + ->bind(':profileKey', $profileKey); + + try { + $result = $db->setQuery($query)->loadResult(); + } catch (Exception $e) { + $result = 1; + } + + return $result == 1; + } + + /** + * Automatically migrates a user's legacy MFA records into the new Captive MFA format. + * + * @return void + * @since 4.2.0 + */ + private function migrateFromLegacyMFA(): void + { + $user = $this->getIdentity(); + + if (!($user instanceof User) || $user->guest || $user->id <= 0) { + return; + } + + /** @var DatabaseDriver $db */ + $db = Factory::getContainer()->get('DatabaseDriver'); + + $userTable = new UserTable($db); + + if (!$userTable->load($user->id) || empty($userTable->otpKey)) { + return; + } + + [$otpMethod, $otpKey] = explode(':', $userTable->otpKey, 2); + $secret = $this->get('secret'); + $otpKey = $this->decryptLegacyTFAString($secret, $otpKey); + $otep = $this->decryptLegacyTFAString($secret, $userTable->otep); + $config = @json_decode($otpKey, true); + $hasConverted = true; + + if (!empty($config)) { + switch ($otpMethod) { + case 'totp': + $this->getLanguage()->load('plg_multifactorauth_totp', JPATH_ADMINISTRATOR); + + (new MfaTable($db))->save( + [ + 'user_id' => $user->id, + 'title' => Text::_('PLG_MULTIFACTORAUTH_TOTP_METHOD_TITLE'), + 'method' => 'totp', + 'default' => 0, + 'created_on' => Date::getInstance()->toSql(), + 'last_used' => null, + 'options' => ['key' => $config['code']], + ] + ); + break; + + case 'yubikey': + $this->getLanguage()->load('plg_multifactorauth_yubikey', JPATH_ADMINISTRATOR); + + (new MfaTable($db))->save( + [ + 'user_id' => $user->id, + 'title' => sprintf("%s %s", Text::_('PLG_MULTIFACTORAUTH_YUBIKEY_METHOD_TITLE'), $config['yubikey']), + 'method' => 'yubikey', + 'default' => 0, + 'created_on' => Date::getInstance()->toSql(), + 'last_used' => null, + 'options' => ['id' => $config['yubikey']], + ] + ); + break; + + default: + $hasConverted = false; + break; + } + } + + // Convert the emergency codes + if ($hasConverted && !empty(@json_decode($otep, true))) { + // Delete any other record with the same user_id and Method. + $method = 'emergencycodes'; + $userId = $user->id; + $query = $db->getQuery(true) + ->delete($db->qn('#__user_mfa')) + ->where($db->qn('user_id') . ' = :user_id') + ->where($db->qn('method') . ' = :method') + ->bind(':user_id', $userId, ParameterType::INTEGER) + ->bind(':method', $method); + $db->setQuery($query)->execute(); + + // Migrate data + (new MfaTable($db))->save( + [ + 'user_id' => $user->id, + 'title' => Text::_('COM_USERS_USER_BACKUPCODES'), + 'method' => 'backupcodes', + 'default' => 0, + 'created_on' => Date::getInstance()->toSql(), + 'last_used' => null, + 'options' => @json_decode($otep, true), + ] + ); + } + + // Remove the legacy MFA + $update = (object) [ + 'id' => $user->id, + 'otpKey' => '', + 'otep' => '', + ]; + $db->updateObject('#__users', $update, ['id']); + } + + /** + * Tries to decrypt the legacy MFA configuration. + * + * @param string $secret Site's secret key + * @param string $stringToDecrypt Base64-encoded and encrypted, JSON-encoded information + * + * @return string Decrypted, but JSON-encoded, information + * + * @see https://github.com/joomla/joomla-cms/pull/12497 + * @since 4.2.0 + */ + private function decryptLegacyTFAString(string $secret, string $stringToDecrypt): string + { + // Is this already decrypted? + try { + $decrypted = @json_decode($stringToDecrypt, true); + } catch (Exception $e) { + $decrypted = null; + } + + if (!empty($decrypted)) { + return $stringToDecrypt; + } + + // No, we need to decrypt the string + $aes = new Aes($secret, 256); + $decrypted = $aes->decryptString($stringToDecrypt); + + if (!is_string($decrypted) || empty($decrypted)) { + $aes->setPassword($secret, true); + + $decrypted = $aes->decryptString($stringToDecrypt); + } + + if (!is_string($decrypted) || empty($decrypted)) { + return ''; + } + + // Remove the null padding added during encryption + return rtrim($decrypted, "\0"); + } } diff --git a/libraries/src/Application/SiteApplication.php b/libraries/src/Application/SiteApplication.php index c4b9f8c3b4b87..2af6964b01e2b 100644 --- a/libraries/src/Application/SiteApplication.php +++ b/libraries/src/Application/SiteApplication.php @@ -1,4 +1,5 @@ name = 'site'; - - // Register the client ID - $this->clientId = 0; - - // Execute the parent constructor - parent::__construct($input, $config, $client, $container); - } - - /** - * Check if the user can access the application - * - * @param integer $itemid The item ID to check authorisation for - * - * @return void - * - * @since 3.2 - * - * @throws \Exception When you are not authorised to view the home page menu item - */ - protected function authorise($itemid) - { - $menus = $this->getMenu(); - $user = Factory::getUser(); - - if (!$menus->authorise($itemid)) - { - if ($user->get('id') == 0) - { - // Set the data - $this->setUserState('users.login.form.data', array('return' => Uri::getInstance()->toString())); - - $url = Route::_('index.php?option=com_users&view=login', false); - - $this->enqueueMessage(Text::_('JGLOBAL_YOU_MUST_LOGIN_FIRST'), 'error'); - $this->redirect($url); - } - else - { - // Get the home page menu item - $home_item = $menus->getDefault($this->getLanguage()->getTag()); - - // If we are already in the homepage raise an exception - if ($menus->getActive()->id == $home_item->id) - { - throw new \Exception(Text::_('JERROR_ALERTNOAUTHOR'), 403); - } - - // Otherwise redirect to the homepage and show an error - $this->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); - $this->redirect(Route::_('index.php?Itemid=' . $home_item->id, false)); - } - } - } - - /** - * Dispatch the application - * - * @param string $component The component which is being rendered. - * - * @return void - * - * @since 3.2 - */ - public function dispatch($component = null) - { - // Get the component if not set. - if (!$component) - { - $component = $this->input->getCmd('option', null); - } - - // Load the document to the API - $this->loadDocument(); - - // Set up the params - $document = $this->getDocument(); - $params = $this->getParams(); - - // Register the document object with Factory - Factory::$document = $document; - - switch ($document->getType()) - { - case 'html': - // Set up the language - LanguageHelper::getLanguages('lang_code'); - - // Set metadata - $document->setMetaData('rights', $this->get('MetaRights')); - - // Get the template - $template = $this->getTemplate(true); - - // Store the template and its params to the config - $this->set('theme', $template->template); - $this->set('themeParams', $template->params); - - // Add Asset registry files - $wr = $document->getWebAssetManager()->getRegistry(); - - if ($component) - { - $wr->addExtensionRegistryFile($component); - } - - if ($template->parent) - { - $wr->addTemplateRegistryFile($template->parent, $this->getClientId()); - } - - $wr->addTemplateRegistryFile($template->template, $this->getClientId()); - - break; - - case 'feed': - $document->setBase(htmlspecialchars(Uri::current())); - break; - } - - $document->setTitle($params->get('page_title')); - $document->setDescription($params->get('page_description')); - - // Add version number or not based on global configuration - if ($this->get('MetaVersion', 0)) - { - $document->setGenerator('Joomla! - Open Source Content Management - Version ' . JVERSION); - } - else - { - $document->setGenerator('Joomla! - Open Source Content Management'); - } - - $contents = ComponentHelper::renderComponent($component); - $document->setBuffer($contents, 'component'); - - // Trigger the onAfterDispatch event. - PluginHelper::importPlugin('system'); - $this->triggerEvent('onAfterDispatch'); - } - - /** - * Method to run the Web application routines. - * - * @return void - * - * @since 3.2 - */ - protected function doExecute() - { - // Initialise the application - $this->initialiseApp(); - - // Mark afterInitialise in the profiler. - JDEBUG ? $this->profiler->mark('afterInitialise') : null; - - // Route the application - $this->route(); - - // Mark afterRoute in the profiler. - JDEBUG ? $this->profiler->mark('afterRoute') : null; - - if (!$this->isHandlingMultiFactorAuthentication()) - { - /* - * Check if the user is required to reset their password - * - * Before $this->route(); "option" and "view" can't be safely read using: - * $this->input->getCmd('option'); or $this->input->getCmd('view'); - * ex: due of the sef urls - */ - $this->checkUserRequireReset('com_users', 'profile', 'edit', 'com_users/profile.save,com_users/profile.apply,com_users/user.logout'); - } - - // Dispatch the application - $this->dispatch(); - - // Mark afterDispatch in the profiler. - JDEBUG ? $this->profiler->mark('afterDispatch') : null; - } - - /** - * Return the current state of the detect browser option. - * - * @return boolean - * - * @since 3.2 - */ - public function getDetectBrowser() - { - return $this->detect_browser; - } - - /** - * Return the current state of the language filter. - * - * @return boolean - * - * @since 3.2 - */ - public function getLanguageFilter() - { - return $this->language_filter; - } - - /** - * Get the application parameters - * - * @param string $option The component option - * - * @return Registry The parameters object - * - * @since 3.2 - */ - public function getParams($option = null) - { - static $params = array(); - - $hash = '__default'; - - if (!empty($option)) - { - $hash = $option; - } - - if (!isset($params[$hash])) - { - // Get component parameters - if (!$option) - { - $option = $this->input->getCmd('option', null); - } - - // Get new instance of component global parameters - $params[$hash] = clone ComponentHelper::getParams($option); - - // Get menu parameters - $menus = $this->getMenu(); - $menu = $menus->getActive(); - - // Get language - $lang_code = $this->getLanguage()->getTag(); - $languages = LanguageHelper::getLanguages('lang_code'); - - $title = $this->get('sitename'); - - if (isset($languages[$lang_code]) && $languages[$lang_code]->metadesc) - { - $description = $languages[$lang_code]->metadesc; - } - else - { - $description = $this->get('MetaDesc'); - } - - $rights = $this->get('MetaRights'); - $robots = $this->get('robots'); - - // Retrieve com_menu global settings - $temp = clone ComponentHelper::getParams('com_menus'); - - // Lets cascade the parameters if we have menu item parameters - if (\is_object($menu)) - { - // Get show_page_heading from com_menu global settings - $params[$hash]->def('show_page_heading', $temp->get('show_page_heading')); - - $params[$hash]->merge($menu->getParams()); - $title = $menu->title; - } - else - { - // Merge com_menu global settings - $params[$hash]->merge($temp); - - // If supplied, use page title - $title = $temp->get('page_title', $title); - } - - $params[$hash]->def('page_title', $title); - $params[$hash]->def('page_description', $description); - $params[$hash]->def('page_rights', $rights); - $params[$hash]->def('robots', $robots); - } - - return $params[$hash]; - } - - /** - * Return a reference to the Pathway object. - * - * @param string $name The name of the application. - * @param array $options An optional associative array of configuration settings. - * - * @return Pathway A Pathway object - * - * @since 3.2 - */ - public function getPathway($name = 'site', $options = array()) - { - return parent::getPathway($name, $options); - } - - /** - * Return a reference to the Router object. - * - * @param string $name The name of the application. - * @param array $options An optional associative array of configuration settings. - * - * @return \Joomla\CMS\Router\Router - * - * @since 3.2 - * - * @deprecated 5.0 Inject the router or load it from the dependency injection container - */ - public static function getRouter($name = 'site', array $options = array()) - { - return parent::getRouter($name, $options); - } - - /** - * Gets the name of the current template. - * - * @param boolean $params True to return the template parameters - * - * @return string The name of the template. - * - * @since 3.2 - * @throws \InvalidArgumentException - */ - public function getTemplate($params = false) - { - if (\is_object($this->template)) - { - if ($this->template->parent) - { - if (!is_file(JPATH_THEMES . '/' . $this->template->template . '/index.php')) - { - if (!is_file(JPATH_THEMES . '/' . $this->template->parent . '/index.php')) - { - throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $this->template->template)); - } - } - } - elseif (!is_file(JPATH_THEMES . '/' . $this->template->template . '/index.php')) - { - throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $this->template->template)); - } - - if ($params) - { - return $this->template; - } - - return $this->template->template; - } - - // Get the id of the active menu item - $menu = $this->getMenu(); - $item = $menu->getActive(); - - if (!$item) - { - $item = $menu->getItem($this->input->getInt('Itemid', null)); - } - - $id = 0; - - if (\is_object($item)) - { - // Valid item retrieved - $id = $item->template_style_id; - } - - $tid = $this->input->getUint('templateStyle', 0); - - if (is_numeric($tid) && (int) $tid > 0) - { - $id = (int) $tid; - } - - /** @var OutputController $cache */ - $cache = $this->getCacheControllerFactory()->createCacheController('output', ['defaultgroup' => 'com_templates']); - - if ($this->getLanguageFilter()) - { - $tag = $this->getLanguage()->getTag(); - } - else - { - $tag = ''; - } - - $cacheId = 'templates0' . $tag; - - if ($cache->contains($cacheId)) - { - $templates = $cache->get($cacheId); - } - else - { - $templates = $this->bootComponent('templates')->getMVCFactory() - ->createModel('Style', 'Administrator')->getSiteTemplates(); - - foreach ($templates as &$template) - { - // Create home element - if ($template->home == 1 && !isset($template_home) || $this->getLanguageFilter() && $template->home == $tag) - { - $template_home = clone $template; - } - - $template->params = new Registry($template->params); - } - - // Unset the $template reference to the last $templates[n] item cycled in the foreach above to avoid editing it later - unset($template); - - // Add home element, after loop to avoid double execution - if (isset($template_home)) - { - $template_home->params = new Registry($template_home->params); - $templates[0] = $template_home; - } - - $cache->store($templates, $cacheId); - } - - if (isset($templates[$id])) - { - $template = $templates[$id]; - } - else - { - $template = $templates[0]; - } - - // Allows for overriding the active template from the request - $template_override = $this->input->getCmd('template', ''); - - // Only set template override if it is a valid template (= it exists and is enabled) - if (!empty($template_override)) - { - if (is_file(JPATH_THEMES . '/' . $template_override . '/index.php')) - { - foreach ($templates as $tmpl) - { - if ($tmpl->template === $template_override) - { - $template = $tmpl; - break; - } - } - } - } - - // Need to filter the default value as well - $template->template = InputFilter::getInstance()->clean($template->template, 'cmd'); - - // Fallback template - if (!empty($template->parent)) - { - if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) - { - if (!is_file(JPATH_THEMES . '/' . $template->parent . '/index.php')) - { - $this->enqueueMessage(Text::_('JERROR_ALERTNOTEMPLATE'), 'error'); - - // Try to find data for 'cassiopeia' template - $original_tmpl = $template->template; - - foreach ($templates as $tmpl) - { - if ($tmpl->template === 'cassiopeia') - { - $template = $tmpl; - break; - } - } - - // Check, the data were found and if template really exists - if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) - { - throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $original_tmpl)); - } - } - } - } - elseif (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) - { - $this->enqueueMessage(Text::_('JERROR_ALERTNOTEMPLATE'), 'error'); - - // Try to find data for 'cassiopeia' template - $original_tmpl = $template->template; - - foreach ($templates as $tmpl) - { - if ($tmpl->template === 'cassiopeia') - { - $template = $tmpl; - break; - } - } - - // Check, the data were found and if template really exists - if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) - { - throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $original_tmpl)); - } - } - - // Cache the result - $this->template = $template; - - if ($params) - { - return $template; - } - - return $template->template; - } - - /** - * Initialise the application. - * - * @param array $options An optional associative array of configuration settings. - * - * @return void - * - * @since 3.2 - */ - protected function initialiseApp($options = array()) - { - $user = Factory::getUser(); - - // If the user is a guest we populate it with the guest user group. - if ($user->guest) - { - $guestUsergroup = ComponentHelper::getParams('com_users')->get('guest_usergroup', 1); - $user->groups = array($guestUsergroup); - } - - if ($plugin = PluginHelper::getPlugin('system', 'languagefilter')) - { - $pluginParams = new Registry($plugin->params); - $this->setLanguageFilter(true); - $this->setDetectBrowser($pluginParams->get('detect_browser', 1) == 1); - } - - if (empty($options['language'])) - { - // Detect the specified language - $lang = $this->input->getString('language', null); - - // Make sure that the user's language exists - if ($lang && LanguageHelper::exists($lang)) - { - $options['language'] = $lang; - } - } - - if (empty($options['language']) && $this->getLanguageFilter()) - { - // Detect cookie language - $lang = $this->input->cookie->get(md5($this->get('secret') . 'language'), null, 'string'); - - // Make sure that the user's language exists - if ($lang && LanguageHelper::exists($lang)) - { - $options['language'] = $lang; - } - } - - if (empty($options['language'])) - { - // Detect user language - $lang = $user->getParam('language'); - - // Make sure that the user's language exists - if ($lang && LanguageHelper::exists($lang)) - { - $options['language'] = $lang; - } - } - - if (empty($options['language']) && $this->getDetectBrowser()) - { - // Detect browser language - $lang = LanguageHelper::detectLanguage(); - - // Make sure that the user's language exists - if ($lang && LanguageHelper::exists($lang)) - { - $options['language'] = $lang; - } - } - - if (empty($options['language'])) - { - // Detect default language - $params = ComponentHelper::getParams('com_languages'); - $options['language'] = $params->get('site', $this->get('language', 'en-GB')); - } - - // One last check to make sure we have something - if (!LanguageHelper::exists($options['language'])) - { - $lang = $this->config->get('language', 'en-GB'); - - if (LanguageHelper::exists($lang)) - { - $options['language'] = $lang; - } - else - { - // As a last ditch fail to english - $options['language'] = 'en-GB'; - } - } - - // Finish initialisation - parent::initialiseApp($options); - } - - /** - * Load the library language files for the application - * - * @return void - * - * @since 3.6.3 - */ - protected function loadLibraryLanguage() - { - /* - * Try the lib_joomla file in the current language (without allowing the loading of the file in the default language) - * Fallback to the default language if necessary - */ - $this->getLanguage()->load('lib_joomla', JPATH_SITE) - || $this->getLanguage()->load('lib_joomla', JPATH_ADMINISTRATOR); - } - - /** - * Login authentication function - * - * @param array $credentials Array('username' => string, 'password' => string) - * @param array $options Array('remember' => boolean) - * - * @return boolean True on success. - * - * @since 3.2 - */ - public function login($credentials, $options = array()) - { - // Set the application login entry point - if (!\array_key_exists('entry_url', $options)) - { - $options['entry_url'] = Uri::base() . 'index.php?option=com_users&task=user.login'; - } - - // Set the access control action to check. - $options['action'] = 'core.login.site'; - - return parent::login($credentials, $options); - } - - /** - * Rendering is the process of pushing the document buffers into the template - * placeholders, retrieving data from the document and pushing it into - * the application response buffer. - * - * @return void - * - * @since 3.2 - */ - protected function render() - { - switch ($this->document->getType()) - { - case 'feed': - // No special processing for feeds - break; - - case 'html': - default: - $template = $this->getTemplate(true); - $file = $this->input->get('tmpl', 'index'); - - if ($file === 'offline' && !$this->get('offline')) - { - $this->set('themeFile', 'index.php'); - } - - if ($this->get('offline') && !Factory::getUser()->authorise('core.login.offline')) - { - $this->setUserState('users.login.form.data', array('return' => Uri::getInstance()->toString())); - $this->set('themeFile', 'offline.php'); - $this->setHeader('Status', '503 Service Temporarily Unavailable', 'true'); - } - - if (!is_dir(JPATH_THEMES . '/' . $template->template) && !$this->get('offline')) - { - $this->set('themeFile', 'component.php'); - } - - // Ensure themeFile is set by now - if ($this->get('themeFile') == '') - { - $this->set('themeFile', $file . '.php'); - } - - // Pass the parent template to the state - $this->set('themeInherits', $template->parent); - - break; - } - - parent::render(); - } - - /** - * Route the application. - * - * Routing is the process of examining the request environment to determine which - * component should receive the request. The component optional parameters - * are then set in the request object to be processed when the application is being - * dispatched. - * - * @return void - * - * @since 3.2 - */ - protected function route() - { - // Get the full request URI. - $uri = clone Uri::getInstance(); - - // It is not possible to inject the SiteRouter as it requires a SiteApplication - // and we would end in an infinite loop - $result = $this->getContainer()->get(SiteRouter::class)->parse($uri, true); - - $active = $this->getMenu()->getActive(); - - if ($active !== null - && $active->type === 'alias' - && $active->getParams()->get('alias_redirect') - && \in_array($this->input->getMethod(), ['GET', 'HEAD'], true)) - { - $item = $this->getMenu()->getItem($active->getParams()->get('aliasoptions')); - - if ($item !== null) - { - $oldUri = clone Uri::getInstance(); - - if ($oldUri->getVar('Itemid') == $active->id) - { - $oldUri->setVar('Itemid', $item->id); - } - - $base = Uri::base(true); - $oldPath = StringHelper::strtolower(substr($oldUri->getPath(), \strlen($base) + 1)); - $activePathPrefix = StringHelper::strtolower($active->route); - - $position = strpos($oldPath, $activePathPrefix); - - if ($position !== false) - { - $oldUri->setPath($base . '/' . substr_replace($oldPath, $item->route, $position, \strlen($activePathPrefix))); - - $this->setHeader('Expires', 'Wed, 17 Aug 2005 00:00:00 GMT', true); - $this->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT', true); - $this->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate', false); - $this->sendHeaders(); - - $this->redirect((string) $oldUri, 301); - } - } - } - - foreach ($result as $key => $value) - { - $this->input->def($key, $value); - } - - // Trigger the onAfterRoute event. - PluginHelper::importPlugin('system'); - $this->triggerEvent('onAfterRoute'); - - $Itemid = $this->input->getInt('Itemid', null); - $this->authorise($Itemid); - } - - /** - * Set the current state of the detect browser option. - * - * @param boolean $state The new state of the detect browser option - * - * @return boolean The previous state - * - * @since 3.2 - */ - public function setDetectBrowser($state = false) - { - $old = $this->getDetectBrowser(); - $this->detect_browser = $state; - - return $old; - } - - /** - * Set the current state of the language filter. - * - * @param boolean $state The new state of the language filter - * - * @return boolean The previous state - * - * @since 3.2 - */ - public function setLanguageFilter($state = false) - { - $old = $this->getLanguageFilter(); - $this->language_filter = $state; - - return $old; - } - - /** - * Overrides the default template that would be used - * - * @param \stdClass|string $template The template name or definition - * @param mixed $styleParams The template style parameters - * - * @return void - * - * @since 3.2 - */ - public function setTemplate($template, $styleParams = null) - { - if (is_object($template)) - { - $templateName = empty($template->template) - ? '' - : $template->template; - $templateInheritable = empty($template->inheritable) - ? 0 - : $template->inheritable; - $templateParent = empty($template->parent) - ? '' - : $template->parent; - $templateParams = empty($template->params) - ? $styleParams - : $template->params; - } - else - { - $templateName = $template; - $templateInheritable = 0; - $templateParent = ''; - $templateParams = $styleParams; - } - - if (is_dir(JPATH_THEMES . '/' . $templateName)) - { - $this->template = new \stdClass; - $this->template->template = $templateName; - - if ($templateParams instanceof Registry) - { - $this->template->params = $templateParams; - } - else - { - $this->template->params = new Registry($templateParams); - } - - $this->template->inheritable = $templateInheritable; - $this->template->parent = $templateParent; - - // Store the template and its params to the config - $this->set('theme', $this->template->template); - $this->set('themeParams', $this->template->params); - } - } + use CacheControllerFactoryAwareTrait; + use MultiFactorAuthenticationHandler; + + /** + * Option to filter by language + * + * @var boolean + * @since 4.0.0 + */ + protected $language_filter = false; + + /** + * Option to detect language by the browser + * + * @var boolean + * @since 4.0.0 + */ + protected $detect_browser = false; + + /** + * Class constructor. + * + * @param Input $input An optional argument to provide dependency injection for the application's input + * object. If the argument is a JInput object that object will become the + * application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's config + * object. If the argument is a Registry object that object will become the + * application's config object, otherwise a default config object is created. + * @param WebClient $client An optional argument to provide dependency injection for the application's client + * object. If the argument is a WebClient object that object will become the + * application's client object, otherwise a default client object is created. + * @param Container $container Dependency injection container. + * + * @since 3.2 + */ + public function __construct(Input $input = null, Registry $config = null, WebClient $client = null, Container $container = null) + { + // Register the application name + $this->name = 'site'; + + // Register the client ID + $this->clientId = 0; + + // Execute the parent constructor + parent::__construct($input, $config, $client, $container); + } + + /** + * Check if the user can access the application + * + * @param integer $itemid The item ID to check authorisation for + * + * @return void + * + * @since 3.2 + * + * @throws \Exception When you are not authorised to view the home page menu item + */ + protected function authorise($itemid) + { + $menus = $this->getMenu(); + $user = Factory::getUser(); + + if (!$menus->authorise($itemid)) { + if ($user->get('id') == 0) { + // Set the data + $this->setUserState('users.login.form.data', array('return' => Uri::getInstance()->toString())); + + $url = Route::_('index.php?option=com_users&view=login', false); + + $this->enqueueMessage(Text::_('JGLOBAL_YOU_MUST_LOGIN_FIRST'), 'error'); + $this->redirect($url); + } else { + // Get the home page menu item + $home_item = $menus->getDefault($this->getLanguage()->getTag()); + + // If we are already in the homepage raise an exception + if ($menus->getActive()->id == $home_item->id) { + throw new \Exception(Text::_('JERROR_ALERTNOAUTHOR'), 403); + } + + // Otherwise redirect to the homepage and show an error + $this->enqueueMessage(Text::_('JERROR_ALERTNOAUTHOR'), 'error'); + $this->redirect(Route::_('index.php?Itemid=' . $home_item->id, false)); + } + } + } + + /** + * Dispatch the application + * + * @param string $component The component which is being rendered. + * + * @return void + * + * @since 3.2 + */ + public function dispatch($component = null) + { + // Get the component if not set. + if (!$component) { + $component = $this->input->getCmd('option', null); + } + + // Load the document to the API + $this->loadDocument(); + + // Set up the params + $document = $this->getDocument(); + $params = $this->getParams(); + + // Register the document object with Factory + Factory::$document = $document; + + switch ($document->getType()) { + case 'html': + // Set up the language + LanguageHelper::getLanguages('lang_code'); + + // Set metadata + $document->setMetaData('rights', $this->get('MetaRights')); + + // Get the template + $template = $this->getTemplate(true); + + // Store the template and its params to the config + $this->set('theme', $template->template); + $this->set('themeParams', $template->params); + + // Add Asset registry files + $wr = $document->getWebAssetManager()->getRegistry(); + + if ($component) { + $wr->addExtensionRegistryFile($component); + } + + if ($template->parent) { + $wr->addTemplateRegistryFile($template->parent, $this->getClientId()); + } + + $wr->addTemplateRegistryFile($template->template, $this->getClientId()); + + break; + + case 'feed': + $document->setBase(htmlspecialchars(Uri::current())); + break; + } + + $document->setTitle($params->get('page_title')); + $document->setDescription($params->get('page_description')); + + // Add version number or not based on global configuration + if ($this->get('MetaVersion', 0)) { + $document->setGenerator('Joomla! - Open Source Content Management - Version ' . JVERSION); + } else { + $document->setGenerator('Joomla! - Open Source Content Management'); + } + + $contents = ComponentHelper::renderComponent($component); + $document->setBuffer($contents, 'component'); + + // Trigger the onAfterDispatch event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onAfterDispatch'); + } + + /** + * Method to run the Web application routines. + * + * @return void + * + * @since 3.2 + */ + protected function doExecute() + { + // Initialise the application + $this->initialiseApp(); + + // Mark afterInitialise in the profiler. + JDEBUG ? $this->profiler->mark('afterInitialise') : null; + + // Route the application + $this->route(); + + // Mark afterRoute in the profiler. + JDEBUG ? $this->profiler->mark('afterRoute') : null; + + if (!$this->isHandlingMultiFactorAuthentication()) { + /* + * Check if the user is required to reset their password + * + * Before $this->route(); "option" and "view" can't be safely read using: + * $this->input->getCmd('option'); or $this->input->getCmd('view'); + * ex: due of the sef urls + */ + $this->checkUserRequireReset('com_users', 'profile', 'edit', 'com_users/profile.save,com_users/profile.apply,com_users/user.logout'); + } + + // Dispatch the application + $this->dispatch(); + + // Mark afterDispatch in the profiler. + JDEBUG ? $this->profiler->mark('afterDispatch') : null; + } + + /** + * Return the current state of the detect browser option. + * + * @return boolean + * + * @since 3.2 + */ + public function getDetectBrowser() + { + return $this->detect_browser; + } + + /** + * Return the current state of the language filter. + * + * @return boolean + * + * @since 3.2 + */ + public function getLanguageFilter() + { + return $this->language_filter; + } + + /** + * Get the application parameters + * + * @param string $option The component option + * + * @return Registry The parameters object + * + * @since 3.2 + */ + public function getParams($option = null) + { + static $params = array(); + + $hash = '__default'; + + if (!empty($option)) { + $hash = $option; + } + + if (!isset($params[$hash])) { + // Get component parameters + if (!$option) { + $option = $this->input->getCmd('option', null); + } + + // Get new instance of component global parameters + $params[$hash] = clone ComponentHelper::getParams($option); + + // Get menu parameters + $menus = $this->getMenu(); + $menu = $menus->getActive(); + + // Get language + $lang_code = $this->getLanguage()->getTag(); + $languages = LanguageHelper::getLanguages('lang_code'); + + $title = $this->get('sitename'); + + if (isset($languages[$lang_code]) && $languages[$lang_code]->metadesc) { + $description = $languages[$lang_code]->metadesc; + } else { + $description = $this->get('MetaDesc'); + } + + $rights = $this->get('MetaRights'); + $robots = $this->get('robots'); + + // Retrieve com_menu global settings + $temp = clone ComponentHelper::getParams('com_menus'); + + // Lets cascade the parameters if we have menu item parameters + if (\is_object($menu)) { + // Get show_page_heading from com_menu global settings + $params[$hash]->def('show_page_heading', $temp->get('show_page_heading')); + + $params[$hash]->merge($menu->getParams()); + $title = $menu->title; + } else { + // Merge com_menu global settings + $params[$hash]->merge($temp); + + // If supplied, use page title + $title = $temp->get('page_title', $title); + } + + $params[$hash]->def('page_title', $title); + $params[$hash]->def('page_description', $description); + $params[$hash]->def('page_rights', $rights); + $params[$hash]->def('robots', $robots); + } + + return $params[$hash]; + } + + /** + * Return a reference to the Pathway object. + * + * @param string $name The name of the application. + * @param array $options An optional associative array of configuration settings. + * + * @return Pathway A Pathway object + * + * @since 3.2 + */ + public function getPathway($name = 'site', $options = array()) + { + return parent::getPathway($name, $options); + } + + /** + * Return a reference to the Router object. + * + * @param string $name The name of the application. + * @param array $options An optional associative array of configuration settings. + * + * @return \Joomla\CMS\Router\Router + * + * @since 3.2 + * + * @deprecated 5.0 Inject the router or load it from the dependency injection container + */ + public static function getRouter($name = 'site', array $options = array()) + { + return parent::getRouter($name, $options); + } + + /** + * Gets the name of the current template. + * + * @param boolean $params True to return the template parameters + * + * @return string The name of the template. + * + * @since 3.2 + * @throws \InvalidArgumentException + */ + public function getTemplate($params = false) + { + if (\is_object($this->template)) { + if ($this->template->parent) { + if (!is_file(JPATH_THEMES . '/' . $this->template->template . '/index.php')) { + if (!is_file(JPATH_THEMES . '/' . $this->template->parent . '/index.php')) { + throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $this->template->template)); + } + } + } elseif (!is_file(JPATH_THEMES . '/' . $this->template->template . '/index.php')) { + throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $this->template->template)); + } + + if ($params) { + return $this->template; + } + + return $this->template->template; + } + + // Get the id of the active menu item + $menu = $this->getMenu(); + $item = $menu->getActive(); + + if (!$item) { + $item = $menu->getItem($this->input->getInt('Itemid', null)); + } + + $id = 0; + + if (\is_object($item)) { + // Valid item retrieved + $id = $item->template_style_id; + } + + $tid = $this->input->getUint('templateStyle', 0); + + if (is_numeric($tid) && (int) $tid > 0) { + $id = (int) $tid; + } + + /** @var OutputController $cache */ + $cache = $this->getCacheControllerFactory()->createCacheController('output', ['defaultgroup' => 'com_templates']); + + if ($this->getLanguageFilter()) { + $tag = $this->getLanguage()->getTag(); + } else { + $tag = ''; + } + + $cacheId = 'templates0' . $tag; + + if ($cache->contains($cacheId)) { + $templates = $cache->get($cacheId); + } else { + $templates = $this->bootComponent('templates')->getMVCFactory() + ->createModel('Style', 'Administrator')->getSiteTemplates(); + + foreach ($templates as &$template) { + // Create home element + if ($template->home == 1 && !isset($template_home) || $this->getLanguageFilter() && $template->home == $tag) { + $template_home = clone $template; + } + + $template->params = new Registry($template->params); + } + + // Unset the $template reference to the last $templates[n] item cycled in the foreach above to avoid editing it later + unset($template); + + // Add home element, after loop to avoid double execution + if (isset($template_home)) { + $template_home->params = new Registry($template_home->params); + $templates[0] = $template_home; + } + + $cache->store($templates, $cacheId); + } + + if (isset($templates[$id])) { + $template = $templates[$id]; + } else { + $template = $templates[0]; + } + + // Allows for overriding the active template from the request + $template_override = $this->input->getCmd('template', ''); + + // Only set template override if it is a valid template (= it exists and is enabled) + if (!empty($template_override)) { + if (is_file(JPATH_THEMES . '/' . $template_override . '/index.php')) { + foreach ($templates as $tmpl) { + if ($tmpl->template === $template_override) { + $template = $tmpl; + break; + } + } + } + } + + // Need to filter the default value as well + $template->template = InputFilter::getInstance()->clean($template->template, 'cmd'); + + // Fallback template + if (!empty($template->parent)) { + if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) { + if (!is_file(JPATH_THEMES . '/' . $template->parent . '/index.php')) { + $this->enqueueMessage(Text::_('JERROR_ALERTNOTEMPLATE'), 'error'); + + // Try to find data for 'cassiopeia' template + $original_tmpl = $template->template; + + foreach ($templates as $tmpl) { + if ($tmpl->template === 'cassiopeia') { + $template = $tmpl; + break; + } + } + + // Check, the data were found and if template really exists + if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) { + throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $original_tmpl)); + } + } + } + } elseif (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) { + $this->enqueueMessage(Text::_('JERROR_ALERTNOTEMPLATE'), 'error'); + + // Try to find data for 'cassiopeia' template + $original_tmpl = $template->template; + + foreach ($templates as $tmpl) { + if ($tmpl->template === 'cassiopeia') { + $template = $tmpl; + break; + } + } + + // Check, the data were found and if template really exists + if (!is_file(JPATH_THEMES . '/' . $template->template . '/index.php')) { + throw new \InvalidArgumentException(Text::sprintf('JERROR_COULD_NOT_FIND_TEMPLATE', $original_tmpl)); + } + } + + // Cache the result + $this->template = $template; + + if ($params) { + return $template; + } + + return $template->template; + } + + /** + * Initialise the application. + * + * @param array $options An optional associative array of configuration settings. + * + * @return void + * + * @since 3.2 + */ + protected function initialiseApp($options = array()) + { + $user = Factory::getUser(); + + // If the user is a guest we populate it with the guest user group. + if ($user->guest) { + $guestUsergroup = ComponentHelper::getParams('com_users')->get('guest_usergroup', 1); + $user->groups = array($guestUsergroup); + } + + if ($plugin = PluginHelper::getPlugin('system', 'languagefilter')) { + $pluginParams = new Registry($plugin->params); + $this->setLanguageFilter(true); + $this->setDetectBrowser($pluginParams->get('detect_browser', 1) == 1); + } + + if (empty($options['language'])) { + // Detect the specified language + $lang = $this->input->getString('language', null); + + // Make sure that the user's language exists + if ($lang && LanguageHelper::exists($lang)) { + $options['language'] = $lang; + } + } + + if (empty($options['language']) && $this->getLanguageFilter()) { + // Detect cookie language + $lang = $this->input->cookie->get(md5($this->get('secret') . 'language'), null, 'string'); + + // Make sure that the user's language exists + if ($lang && LanguageHelper::exists($lang)) { + $options['language'] = $lang; + } + } + + if (empty($options['language'])) { + // Detect user language + $lang = $user->getParam('language'); + + // Make sure that the user's language exists + if ($lang && LanguageHelper::exists($lang)) { + $options['language'] = $lang; + } + } + + if (empty($options['language']) && $this->getDetectBrowser()) { + // Detect browser language + $lang = LanguageHelper::detectLanguage(); + + // Make sure that the user's language exists + if ($lang && LanguageHelper::exists($lang)) { + $options['language'] = $lang; + } + } + + if (empty($options['language'])) { + // Detect default language + $params = ComponentHelper::getParams('com_languages'); + $options['language'] = $params->get('site', $this->get('language', 'en-GB')); + } + + // One last check to make sure we have something + if (!LanguageHelper::exists($options['language'])) { + $lang = $this->config->get('language', 'en-GB'); + + if (LanguageHelper::exists($lang)) { + $options['language'] = $lang; + } else { + // As a last ditch fail to english + $options['language'] = 'en-GB'; + } + } + + // Finish initialisation + parent::initialiseApp($options); + } + + /** + * Load the library language files for the application + * + * @return void + * + * @since 3.6.3 + */ + protected function loadLibraryLanguage() + { + /* + * Try the lib_joomla file in the current language (without allowing the loading of the file in the default language) + * Fallback to the default language if necessary + */ + $this->getLanguage()->load('lib_joomla', JPATH_SITE) + || $this->getLanguage()->load('lib_joomla', JPATH_ADMINISTRATOR); + } + + /** + * Login authentication function + * + * @param array $credentials Array('username' => string, 'password' => string) + * @param array $options Array('remember' => boolean) + * + * @return boolean True on success. + * + * @since 3.2 + */ + public function login($credentials, $options = array()) + { + // Set the application login entry point + if (!\array_key_exists('entry_url', $options)) { + $options['entry_url'] = Uri::base() . 'index.php?option=com_users&task=user.login'; + } + + // Set the access control action to check. + $options['action'] = 'core.login.site'; + + return parent::login($credentials, $options); + } + + /** + * Rendering is the process of pushing the document buffers into the template + * placeholders, retrieving data from the document and pushing it into + * the application response buffer. + * + * @return void + * + * @since 3.2 + */ + protected function render() + { + switch ($this->document->getType()) { + case 'feed': + // No special processing for feeds + break; + + case 'html': + default: + $template = $this->getTemplate(true); + $file = $this->input->get('tmpl', 'index'); + + if ($file === 'offline' && !$this->get('offline')) { + $this->set('themeFile', 'index.php'); + } + + if ($this->get('offline') && !Factory::getUser()->authorise('core.login.offline')) { + $this->setUserState('users.login.form.data', array('return' => Uri::getInstance()->toString())); + $this->set('themeFile', 'offline.php'); + $this->setHeader('Status', '503 Service Temporarily Unavailable', 'true'); + } + + if (!is_dir(JPATH_THEMES . '/' . $template->template) && !$this->get('offline')) { + $this->set('themeFile', 'component.php'); + } + + // Ensure themeFile is set by now + if ($this->get('themeFile') == '') { + $this->set('themeFile', $file . '.php'); + } + + // Pass the parent template to the state + $this->set('themeInherits', $template->parent); + + break; + } + + parent::render(); + } + + /** + * Route the application. + * + * Routing is the process of examining the request environment to determine which + * component should receive the request. The component optional parameters + * are then set in the request object to be processed when the application is being + * dispatched. + * + * @return void + * + * @since 3.2 + */ + protected function route() + { + // Get the full request URI. + $uri = clone Uri::getInstance(); + + // It is not possible to inject the SiteRouter as it requires a SiteApplication + // and we would end in an infinite loop + $result = $this->getContainer()->get(SiteRouter::class)->parse($uri, true); + + $active = $this->getMenu()->getActive(); + + if ( + $active !== null + && $active->type === 'alias' + && $active->getParams()->get('alias_redirect') + && \in_array($this->input->getMethod(), ['GET', 'HEAD'], true) + ) { + $item = $this->getMenu()->getItem($active->getParams()->get('aliasoptions')); + + if ($item !== null) { + $oldUri = clone Uri::getInstance(); + + if ($oldUri->getVar('Itemid') == $active->id) { + $oldUri->setVar('Itemid', $item->id); + } + + $base = Uri::base(true); + $oldPath = StringHelper::strtolower(substr($oldUri->getPath(), \strlen($base) + 1)); + $activePathPrefix = StringHelper::strtolower($active->route); + + $position = strpos($oldPath, $activePathPrefix); + + if ($position !== false) { + $oldUri->setPath($base . '/' . substr_replace($oldPath, $item->route, $position, \strlen($activePathPrefix))); + + $this->setHeader('Expires', 'Wed, 17 Aug 2005 00:00:00 GMT', true); + $this->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT', true); + $this->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate', false); + $this->sendHeaders(); + + $this->redirect((string) $oldUri, 301); + } + } + } + + foreach ($result as $key => $value) { + $this->input->def($key, $value); + } + + // Trigger the onAfterRoute event. + PluginHelper::importPlugin('system'); + $this->triggerEvent('onAfterRoute'); + + $Itemid = $this->input->getInt('Itemid', null); + $this->authorise($Itemid); + } + + /** + * Set the current state of the detect browser option. + * + * @param boolean $state The new state of the detect browser option + * + * @return boolean The previous state + * + * @since 3.2 + */ + public function setDetectBrowser($state = false) + { + $old = $this->getDetectBrowser(); + $this->detect_browser = $state; + + return $old; + } + + /** + * Set the current state of the language filter. + * + * @param boolean $state The new state of the language filter + * + * @return boolean The previous state + * + * @since 3.2 + */ + public function setLanguageFilter($state = false) + { + $old = $this->getLanguageFilter(); + $this->language_filter = $state; + + return $old; + } + + /** + * Overrides the default template that would be used + * + * @param \stdClass|string $template The template name or definition + * @param mixed $styleParams The template style parameters + * + * @return void + * + * @since 3.2 + */ + public function setTemplate($template, $styleParams = null) + { + if (is_object($template)) { + $templateName = empty($template->template) + ? '' + : $template->template; + $templateInheritable = empty($template->inheritable) + ? 0 + : $template->inheritable; + $templateParent = empty($template->parent) + ? '' + : $template->parent; + $templateParams = empty($template->params) + ? $styleParams + : $template->params; + } else { + $templateName = $template; + $templateInheritable = 0; + $templateParent = ''; + $templateParams = $styleParams; + } + + if (is_dir(JPATH_THEMES . '/' . $templateName)) { + $this->template = new \stdClass(); + $this->template->template = $templateName; + + if ($templateParams instanceof Registry) { + $this->template->params = $templateParams; + } else { + $this->template->params = new Registry($templateParams); + } + + $this->template->inheritable = $templateInheritable; + $this->template->parent = $templateParent; + + // Store the template and its params to the config + $this->set('theme', $this->template->template); + $this->set('themeParams', $this->template->params); + } + } } diff --git a/libraries/src/Application/WebApplication.php b/libraries/src/Application/WebApplication.php index b2b9e5881bd35..08b905bb3542e 100644 --- a/libraries/src/Application/WebApplication.php +++ b/libraries/src/Application/WebApplication.php @@ -1,4 +1,5 @@ set('execution.datetime', gmdate('Y-m-d H:i:s')); - $this->set('execution.timestamp', time()); - - // Set the system URIs. - $this->loadSystemUris(); - } - - /** - * Returns a reference to the global WebApplication object, only creating it if it doesn't already exist. - * - * This method must be invoked as: $web = WebApplication::getInstance(); - * - * @param string $name The name (optional) of the WebApplication class to instantiate. - * - * @return WebApplication - * - * @since 1.7.3 - * @throws \RuntimeException - * @deprecated 5.0 Use \Joomla\CMS\Factory::getContainer()->get($name) instead - */ - public static function getInstance($name = null) - { - // Only create the object if it doesn't exist. - if (empty(static::$instance)) - { - if (!is_subclass_of($name, '\\Joomla\\CMS\\Application\\WebApplication')) - { - throw new \RuntimeException(sprintf('Unable to load application: %s', $name), 500); - } - - static::$instance = new $name; - } - - return static::$instance; - } - - /** - * Execute the application. - * - * @return void - * - * @since 1.7.3 - */ - public function execute() - { - // Trigger the onBeforeExecute event. - $this->triggerEvent('onBeforeExecute'); - - // Perform application routines. - $this->doExecute(); - - // Trigger the onAfterExecute event. - $this->triggerEvent('onAfterExecute'); - - // If we have an application document object, render it. - if ($this->document instanceof Document) - { - // Trigger the onBeforeRender event. - $this->triggerEvent('onBeforeRender'); - - // Render the application output. - $this->render(); - - // Trigger the onAfterRender event. - $this->triggerEvent('onAfterRender'); - } - - // If gzip compression is enabled in configuration and the server is compliant, compress the output. - if ($this->get('gzip') && !ini_get('zlib.output_compression') && (ini_get('output_handler') !== 'ob_gzhandler')) - { - $this->compress(); - } - - // Trigger the onBeforeRespond event. - $this->triggerEvent('onBeforeRespond'); - - // Send the application response. - $this->respond(); - - // Trigger the onAfterRespond event. - $this->triggerEvent('onAfterRespond'); - } - - /** - * Rendering is the process of pushing the document buffers into the template - * placeholders, retrieving data from the document and pushing it into - * the application response buffer. - * - * @return void - * - * @since 1.7.3 - */ - protected function render() - { - // Setup the document options. - $options = array( - 'template' => $this->get('theme'), - 'file' => $this->get('themeFile', 'index.php'), - 'params' => $this->get('themeParams'), - 'templateInherits' => $this->get('themeInherits'), - ); - - if ($this->get('themes.base')) - { - $options['directory'] = $this->get('themes.base'); - } - // Fall back to constants. - else - { - $options['directory'] = \defined('JPATH_THEMES') ? JPATH_THEMES : (\defined('JPATH_BASE') ? JPATH_BASE : __DIR__) . '/themes'; - } - - // Parse the document. - $this->document->parse($options); - - // Render the document. - $data = $this->document->render($this->get('cache_enabled'), $options); - - // Set the application output data. - $this->setBody($data); - } - - /** - * Method to get the application document object. - * - * @return Document The document object - * - * @since 1.7.3 - */ - public function getDocument() - { - return $this->document; - } - - /** - * Method to get the application language object. - * - * @return Language The language object - * - * @since 1.7.3 - */ - public function getLanguage() - { - return $this->language; - } - - /** - * Flush the media version to refresh versionable assets - * - * @return void - * - * @since 3.2 - */ - public function flushAssets() - { - (new Version)->refreshMediaVersion(); - } - - /** - * Allows the application to load a custom or default document. - * - * The logic and options for creating this object are adequately generic for default cases - * but for many applications it will make sense to override this method and create a document, - * if required, based on more specific needs. - * - * @param Document $document An optional document object. If omitted, the factory document is created. - * - * @return WebApplication This method is chainable. - * - * @since 1.7.3 - */ - public function loadDocument(Document $document = null) - { - $this->document = $document ?? Factory::getDocument(); - - return $this; - } - - /** - * Allows the application to load a custom or default language. - * - * The logic and options for creating this object are adequately generic for default cases - * but for many applications it will make sense to override this method and create a language, - * if required, based on more specific needs. - * - * @param Language $language An optional language object. If omitted, the factory language is created. - * - * @return WebApplication This method is chainable. - * - * @since 1.7.3 - */ - public function loadLanguage(Language $language = null) - { - $this->language = $language ?? Factory::getLanguage(); - - return $this; - } - - /** - * Allows the application to load a custom or default session. - * - * The logic and options for creating this object are adequately generic for default cases - * but for many applications it will make sense to override this method and create a session, - * if required, based on more specific needs. - * - * @param Session $session An optional session object. If omitted, the session is created. - * - * @return WebApplication This method is chainable. - * - * @since 1.7.3 - * @deprecated 5.0 The session should be injected as a service. - */ - public function loadSession(Session $session = null) - { - $this->getLogger()->warning(__METHOD__ . '() is deprecated. Inject the session as a service instead.', array('category' => 'deprecated')); - - return $this; - } - - /** - * After the session has been started we need to populate it with some default values. - * - * @param SessionEvent $event Session event being triggered - * - * @return void - * - * @since 3.0.1 - */ - public function afterSessionStart(SessionEvent $event) - { - $session = $event->getSession(); - - if ($session->isNew()) - { - $session->set('registry', new Registry); - $session->set('user', new User); - } - - // Ensure the identity is loaded - if (!$this->getIdentity()) - { - $this->loadIdentity($session->get('user')); - } - } - - /** - * Method to load the system URI strings for the application. - * - * @param string $requestUri An optional request URI to use instead of detecting one from the - * server environment variables. - * - * @return void - * - * @since 1.7.3 - */ - protected function loadSystemUris($requestUri = null) - { - // Set the request URI. - if (!empty($requestUri)) - { - $this->set('uri.request', $requestUri); - } - else - { - $this->set('uri.request', $this->detectRequestUri()); - } - - // Check to see if an explicit base URI has been set. - $siteUri = trim($this->get('site_uri', '')); - - if ($siteUri !== '') - { - $uri = Uri::getInstance($siteUri); - $path = $uri->toString(array('path')); - } - // No explicit base URI was set so we need to detect it. - else - { - // Start with the requested URI. - $uri = Uri::getInstance($this->get('uri.request')); - - // If we are working from a CGI SAPI with the 'cgi.fix_pathinfo' directive disabled we use PHP_SELF. - if (strpos(PHP_SAPI, 'cgi') !== false && !ini_get('cgi.fix_pathinfo') && !empty($_SERVER['REQUEST_URI'])) - { - // We aren't expecting PATH_INFO within PHP_SELF so this should work. - $path = \dirname($_SERVER['PHP_SELF']); - } - // Pretty much everything else should be handled with SCRIPT_NAME. - else - { - $path = \dirname($_SERVER['SCRIPT_NAME']); - } - } - - $host = $uri->toString(array('scheme', 'user', 'pass', 'host', 'port')); - - // Check if the path includes "index.php". - if (strpos($path, 'index.php') !== false) - { - // Remove the index.php portion of the path. - $path = substr_replace($path, '', strpos($path, 'index.php'), 9); - } - - $path = rtrim($path, '/\\'); - - // Set the base URI both as just a path and as the full URI. - $this->set('uri.base.full', $host . $path . '/'); - $this->set('uri.base.host', $host); - $this->set('uri.base.path', $path . '/'); - - // Set the extended (non-base) part of the request URI as the route. - if (stripos($this->get('uri.request'), $this->get('uri.base.full')) === 0) - { - $this->set('uri.route', substr_replace($this->get('uri.request'), '', 0, \strlen($this->get('uri.base.full')))); - } - - // Get an explicitly set media URI is present. - $mediaURI = trim($this->get('media_uri', '')); - - if ($mediaURI) - { - if (strpos($mediaURI, '://') !== false) - { - $this->set('uri.media.full', $mediaURI); - $this->set('uri.media.path', $mediaURI); - } - else - { - // Normalise slashes. - $mediaURI = trim($mediaURI, '/\\'); - $mediaURI = !empty($mediaURI) ? '/' . $mediaURI . '/' : '/'; - $this->set('uri.media.full', $this->get('uri.base.host') . $mediaURI); - $this->set('uri.media.path', $mediaURI); - } - } - // No explicit media URI was set, build it dynamically from the base uri. - else - { - $this->set('uri.media.full', $this->get('uri.base.full') . 'media/'); - $this->set('uri.media.path', $this->get('uri.base.path') . 'media/'); - } - } - - /** - * Retrieve the application configuration object. - * - * @return Registry - * - * @since 4.0.0 - */ - public function getConfig() - { - return $this->config; - } + use EventAware; + use IdentityAware; + + /** + * The application document object. + * + * @var Document + * @since 1.7.3 + */ + protected $document; + + /** + * The application language object. + * + * @var Language + * @since 1.7.3 + */ + protected $language; + + /** + * The application instance. + * + * @var static + * @since 1.7.3 + */ + protected static $instance; + + /** + * Class constructor. + * + * @param Input $input An optional argument to provide dependency injection for the application's + * input object. If the argument is a JInput object that object will become + * the application's input object, otherwise a default input object is created. + * @param Registry $config An optional argument to provide dependency injection for the application's + * config object. If the argument is a Registry object that object will become + * the application's config object, otherwise a default config object is created. + * @param WebClient $client An optional argument to provide dependency injection for the application's + * client object. If the argument is a WebClient object that object will become + * the application's client object, otherwise a default client object is created. + * @param ResponseInterface $response An optional argument to provide dependency injection for the application's + * response object. If the argument is a ResponseInterface object that object + * will become the application's response object, otherwise a default response + * object is created. + * + * @since 1.7.3 + */ + public function __construct(Input $input = null, Registry $config = null, WebClient $client = null, ResponseInterface $response = null) + { + // Ensure we have a CMS Input object otherwise the DI for \Joomla\CMS\Session\Storage\JoomlaStorage fails + $input = $input ?: new Input(); + + parent::__construct($input, $config, $client, $response); + + // Set the execution datetime and timestamp; + $this->set('execution.datetime', gmdate('Y-m-d H:i:s')); + $this->set('execution.timestamp', time()); + + // Set the system URIs. + $this->loadSystemUris(); + } + + /** + * Returns a reference to the global WebApplication object, only creating it if it doesn't already exist. + * + * This method must be invoked as: $web = WebApplication::getInstance(); + * + * @param string $name The name (optional) of the WebApplication class to instantiate. + * + * @return WebApplication + * + * @since 1.7.3 + * @throws \RuntimeException + * @deprecated 5.0 Use \Joomla\CMS\Factory::getContainer()->get($name) instead + */ + public static function getInstance($name = null) + { + // Only create the object if it doesn't exist. + if (empty(static::$instance)) { + if (!is_subclass_of($name, '\\Joomla\\CMS\\Application\\WebApplication')) { + throw new \RuntimeException(sprintf('Unable to load application: %s', $name), 500); + } + + static::$instance = new $name(); + } + + return static::$instance; + } + + /** + * Execute the application. + * + * @return void + * + * @since 1.7.3 + */ + public function execute() + { + // Trigger the onBeforeExecute event. + $this->triggerEvent('onBeforeExecute'); + + // Perform application routines. + $this->doExecute(); + + // Trigger the onAfterExecute event. + $this->triggerEvent('onAfterExecute'); + + // If we have an application document object, render it. + if ($this->document instanceof Document) { + // Trigger the onBeforeRender event. + $this->triggerEvent('onBeforeRender'); + + // Render the application output. + $this->render(); + + // Trigger the onAfterRender event. + $this->triggerEvent('onAfterRender'); + } + + // If gzip compression is enabled in configuration and the server is compliant, compress the output. + if ($this->get('gzip') && !ini_get('zlib.output_compression') && (ini_get('output_handler') !== 'ob_gzhandler')) { + $this->compress(); + } + + // Trigger the onBeforeRespond event. + $this->triggerEvent('onBeforeRespond'); + + // Send the application response. + $this->respond(); + + // Trigger the onAfterRespond event. + $this->triggerEvent('onAfterRespond'); + } + + /** + * Rendering is the process of pushing the document buffers into the template + * placeholders, retrieving data from the document and pushing it into + * the application response buffer. + * + * @return void + * + * @since 1.7.3 + */ + protected function render() + { + // Setup the document options. + $options = array( + 'template' => $this->get('theme'), + 'file' => $this->get('themeFile', 'index.php'), + 'params' => $this->get('themeParams'), + 'templateInherits' => $this->get('themeInherits'), + ); + + if ($this->get('themes.base')) { + $options['directory'] = $this->get('themes.base'); + } else { + // Fall back to constants. + $options['directory'] = \defined('JPATH_THEMES') ? JPATH_THEMES : (\defined('JPATH_BASE') ? JPATH_BASE : __DIR__) . '/themes'; + } + + // Parse the document. + $this->document->parse($options); + + // Render the document. + $data = $this->document->render($this->get('cache_enabled'), $options); + + // Set the application output data. + $this->setBody($data); + } + + /** + * Method to get the application document object. + * + * @return Document The document object + * + * @since 1.7.3 + */ + public function getDocument() + { + return $this->document; + } + + /** + * Method to get the application language object. + * + * @return Language The language object + * + * @since 1.7.3 + */ + public function getLanguage() + { + return $this->language; + } + + /** + * Flush the media version to refresh versionable assets + * + * @return void + * + * @since 3.2 + */ + public function flushAssets() + { + (new Version())->refreshMediaVersion(); + } + + /** + * Allows the application to load a custom or default document. + * + * The logic and options for creating this object are adequately generic for default cases + * but for many applications it will make sense to override this method and create a document, + * if required, based on more specific needs. + * + * @param Document $document An optional document object. If omitted, the factory document is created. + * + * @return WebApplication This method is chainable. + * + * @since 1.7.3 + */ + public function loadDocument(Document $document = null) + { + $this->document = $document ?? Factory::getDocument(); + + return $this; + } + + /** + * Allows the application to load a custom or default language. + * + * The logic and options for creating this object are adequately generic for default cases + * but for many applications it will make sense to override this method and create a language, + * if required, based on more specific needs. + * + * @param Language $language An optional language object. If omitted, the factory language is created. + * + * @return WebApplication This method is chainable. + * + * @since 1.7.3 + */ + public function loadLanguage(Language $language = null) + { + $this->language = $language ?? Factory::getLanguage(); + + return $this; + } + + /** + * Allows the application to load a custom or default session. + * + * The logic and options for creating this object are adequately generic for default cases + * but for many applications it will make sense to override this method and create a session, + * if required, based on more specific needs. + * + * @param Session $session An optional session object. If omitted, the session is created. + * + * @return WebApplication This method is chainable. + * + * @since 1.7.3 + * @deprecated 5.0 The session should be injected as a service. + */ + public function loadSession(Session $session = null) + { + $this->getLogger()->warning(__METHOD__ . '() is deprecated. Inject the session as a service instead.', array('category' => 'deprecated')); + + return $this; + } + + /** + * After the session has been started we need to populate it with some default values. + * + * @param SessionEvent $event Session event being triggered + * + * @return void + * + * @since 3.0.1 + */ + public function afterSessionStart(SessionEvent $event) + { + $session = $event->getSession(); + + if ($session->isNew()) { + $session->set('registry', new Registry()); + $session->set('user', new User()); + } + + // Ensure the identity is loaded + if (!$this->getIdentity()) { + $this->loadIdentity($session->get('user')); + } + } + + /** + * Method to load the system URI strings for the application. + * + * @param string $requestUri An optional request URI to use instead of detecting one from the + * server environment variables. + * + * @return void + * + * @since 1.7.3 + */ + protected function loadSystemUris($requestUri = null) + { + // Set the request URI. + if (!empty($requestUri)) { + $this->set('uri.request', $requestUri); + } else { + $this->set('uri.request', $this->detectRequestUri()); + } + + // Check to see if an explicit base URI has been set. + $siteUri = trim($this->get('site_uri', '')); + + if ($siteUri !== '') { + $uri = Uri::getInstance($siteUri); + $path = $uri->toString(array('path')); + } else { + // No explicit base URI was set so we need to detect it. + // Start with the requested URI. + $uri = Uri::getInstance($this->get('uri.request')); + + // If we are working from a CGI SAPI with the 'cgi.fix_pathinfo' directive disabled we use PHP_SELF. + if (strpos(PHP_SAPI, 'cgi') !== false && !ini_get('cgi.fix_pathinfo') && !empty($_SERVER['REQUEST_URI'])) { + // We aren't expecting PATH_INFO within PHP_SELF so this should work. + $path = \dirname($_SERVER['PHP_SELF']); + } else { + // Pretty much everything else should be handled with SCRIPT_NAME. + $path = \dirname($_SERVER['SCRIPT_NAME']); + } + } + + $host = $uri->toString(array('scheme', 'user', 'pass', 'host', 'port')); + + // Check if the path includes "index.php". + if (strpos($path, 'index.php') !== false) { + // Remove the index.php portion of the path. + $path = substr_replace($path, '', strpos($path, 'index.php'), 9); + } + + $path = rtrim($path, '/\\'); + + // Set the base URI both as just a path and as the full URI. + $this->set('uri.base.full', $host . $path . '/'); + $this->set('uri.base.host', $host); + $this->set('uri.base.path', $path . '/'); + + // Set the extended (non-base) part of the request URI as the route. + if (stripos($this->get('uri.request'), $this->get('uri.base.full')) === 0) { + $this->set('uri.route', substr_replace($this->get('uri.request'), '', 0, \strlen($this->get('uri.base.full')))); + } + + // Get an explicitly set media URI is present. + $mediaURI = trim($this->get('media_uri', '')); + + if ($mediaURI) { + if (strpos($mediaURI, '://') !== false) { + $this->set('uri.media.full', $mediaURI); + $this->set('uri.media.path', $mediaURI); + } else { + // Normalise slashes. + $mediaURI = trim($mediaURI, '/\\'); + $mediaURI = !empty($mediaURI) ? '/' . $mediaURI . '/' : '/'; + $this->set('uri.media.full', $this->get('uri.base.host') . $mediaURI); + $this->set('uri.media.path', $mediaURI); + } + } else { + // No explicit media URI was set, build it dynamically from the base uri. + $this->set('uri.media.full', $this->get('uri.base.full') . 'media/'); + $this->set('uri.media.path', $this->get('uri.base.path') . 'media/'); + } + } + + /** + * Retrieve the application configuration object. + * + * @return Registry + * + * @since 4.0.0 + */ + public function getConfig() + { + return $this->config; + } } diff --git a/libraries/src/Association/AssociationExtensionHelper.php b/libraries/src/Association/AssociationExtensionHelper.php index 0815984d30bb4..cdbf4f7b853f3 100644 --- a/libraries/src/Association/AssociationExtensionHelper.php +++ b/libraries/src/Association/AssociationExtensionHelper.php @@ -1,4 +1,5 @@ associationsSupport; - } + /** + * Checks if the extension supports associations + * + * @return boolean Supports the extension associations + * + * @since 3.7.0 + */ + public function hasAssociationsSupport() + { + return $this->associationsSupport; + } - /** - * Get the item types - * - * @return array Array of item types - * - * @since 3.7.0 - */ - public function getItemTypes() - { - return $this->itemTypes; - } + /** + * Get the item types + * + * @return array Array of item types + * + * @since 3.7.0 + */ + public function getItemTypes() + { + return $this->itemTypes; + } - /** - * Get the associated items for an item - * - * @param string $typeName The item type - * @param int $itemId The id of item for which we need the associated items - * - * @return array - * - * @since 3.7.0 - */ - public function getAssociationList($typeName, $itemId) - { - $items = array(); + /** + * Get the associated items for an item + * + * @param string $typeName The item type + * @param int $itemId The id of item for which we need the associated items + * + * @return array + * + * @since 3.7.0 + */ + public function getAssociationList($typeName, $itemId) + { + $items = array(); - $associations = $this->getAssociations($typeName, $itemId); + $associations = $this->getAssociations($typeName, $itemId); - foreach ($associations as $key => $association) - { - $items[$key] = ArrayHelper::fromObject($this->getItem($typeName, (int) $association->id), false); - } + foreach ($associations as $key => $association) { + $items[$key] = ArrayHelper::fromObject($this->getItem($typeName, (int) $association->id), false); + } - return $items; - } + return $items; + } - /** - * Get information about the type - * - * @param string $typeName The item type - * - * @return array Array of item types - * - * @since 3.7.0 - */ - public function getType($typeName = '') - { - $fields = $this->getFieldsTemplate(); - $tables = array(); - $joins = array(); - $support = $this->getSupportTemplate(); - $title = ''; + /** + * Get information about the type + * + * @param string $typeName The item type + * + * @return array Array of item types + * + * @since 3.7.0 + */ + public function getType($typeName = '') + { + $fields = $this->getFieldsTemplate(); + $tables = array(); + $joins = array(); + $support = $this->getSupportTemplate(); + $title = ''; - return array( - 'fields' => $fields, - 'support' => $support, - 'tables' => $tables, - 'joins' => $joins, - 'title' => $title - ); - } + return array( + 'fields' => $fields, + 'support' => $support, + 'tables' => $tables, + 'joins' => $joins, + 'title' => $title + ); + } - /** - * Get information about the fields the type provides - * - * @param string $typeName The item type - * - * @return array Array of support information - * - * @since 3.7.0 - */ - public function getTypeFields($typeName) - { - return $this->getTypeInformation($typeName, 'fields'); - } + /** + * Get information about the fields the type provides + * + * @param string $typeName The item type + * + * @return array Array of support information + * + * @since 3.7.0 + */ + public function getTypeFields($typeName) + { + return $this->getTypeInformation($typeName, 'fields'); + } - /** - * Get information about the fields the type provides - * - * @param string $typeName The item type - * - * @return array Array of support information - * - * @since 3.7.0 - */ - public function getTypeSupport($typeName) - { - return $this->getTypeInformation($typeName, 'support'); - } + /** + * Get information about the fields the type provides + * + * @param string $typeName The item type + * + * @return array Array of support information + * + * @since 3.7.0 + */ + public function getTypeSupport($typeName) + { + return $this->getTypeInformation($typeName, 'support'); + } - /** - * Get information about the tables the type use - * - * @param string $typeName The item type - * - * @return array Array of support information - * - * @since 3.7.0 - */ - public function getTypeTables($typeName) - { - return $this->getTypeInformation($typeName, 'tables'); - } + /** + * Get information about the tables the type use + * + * @param string $typeName The item type + * + * @return array Array of support information + * + * @since 3.7.0 + */ + public function getTypeTables($typeName) + { + return $this->getTypeInformation($typeName, 'tables'); + } - /** - * Get information about the table joins for the type - * - * @param string $typeName The item type - * - * @return array Array of support information - * - * @since 3.7.0 - */ - public function getTypeJoins($typeName) - { - return $this->getTypeInformation($typeName, 'joins'); - } + /** + * Get information about the table joins for the type + * + * @param string $typeName The item type + * + * @return array Array of support information + * + * @since 3.7.0 + */ + public function getTypeJoins($typeName) + { + return $this->getTypeInformation($typeName, 'joins'); + } - /** - * Get the type title - * - * @param string $typeName The item type - * - * @return string The type title - * - * @since 3.7.0 - */ - public function getTypeTitle($typeName) - { - $type = $this->getType($typeName); + /** + * Get the type title + * + * @param string $typeName The item type + * + * @return string The type title + * + * @since 3.7.0 + */ + public function getTypeTitle($typeName) + { + $type = $this->getType($typeName); - if (!\array_key_exists('title', $type)) - { - return ''; - } + if (!\array_key_exists('title', $type)) { + return ''; + } - return $type['title']; - } + return $type['title']; + } - /** - * Get information about the type - * - * @param string $typeName The item type - * @param string $part part of the information - * - * @return array Array of support information - * - * @since 3.7.0 - */ - private function getTypeInformation($typeName, $part = 'support') - { - $type = $this->getType($typeName); + /** + * Get information about the type + * + * @param string $typeName The item type + * @param string $part part of the information + * + * @return array Array of support information + * + * @since 3.7.0 + */ + private function getTypeInformation($typeName, $part = 'support') + { + $type = $this->getType($typeName); - if (!\array_key_exists($part, $type)) - { - return array(); - } + if (!\array_key_exists($part, $type)) { + return array(); + } - return $type[$part]; - } + return $type[$part]; + } - /** - * Get a table field name for a type - * - * @param string $typeName The item type - * @param string $fieldName The item type - * - * @return string - * - * @since 3.7.0 - */ - public function getTypeFieldName($typeName, $fieldName) - { - $fields = $this->getTypeFields($typeName); + /** + * Get a table field name for a type + * + * @param string $typeName The item type + * @param string $fieldName The item type + * + * @return string + * + * @since 3.7.0 + */ + public function getTypeFieldName($typeName, $fieldName) + { + $fields = $this->getTypeFields($typeName); - if (!\array_key_exists($fieldName, $fields)) - { - return ''; - } + if (!\array_key_exists($fieldName, $fields)) { + return ''; + } - $tmp = $fields[$fieldName]; - $pos = strpos($tmp, '.'); + $tmp = $fields[$fieldName]; + $pos = strpos($tmp, '.'); - if ($pos === false) - { - return $tmp; - } + if ($pos === false) { + return $tmp; + } - return substr($tmp, $pos + 1); - } + return substr($tmp, $pos + 1); + } - /** - * Get default values for support array - * - * @return array - * - * @since 3.7.0 - */ - protected function getSupportTemplate() - { - return array( - 'state' => false, - 'acl' => false, - 'checkout' => false - ); - } + /** + * Get default values for support array + * + * @return array + * + * @since 3.7.0 + */ + protected function getSupportTemplate() + { + return array( + 'state' => false, + 'acl' => false, + 'checkout' => false + ); + } - /** - * Get default values for fields array - * - * @return array - * - * @since 3.7.0 - */ - protected function getFieldsTemplate() - { - return array( - 'id' => 'a.id', - 'title' => 'a.title', - 'alias' => 'a.alias', - 'ordering' => 'a.ordering', - 'menutype' => '', - 'level' => '', - 'catid' => 'a.catid', - 'language' => 'a.language', - 'access' => 'a.access', - 'state' => 'a.state', - 'created_user_id' => 'a.created_by', - 'checked_out' => 'a.checked_out', - 'checked_out_time' => 'a.checked_out_time' - ); - } + /** + * Get default values for fields array + * + * @return array + * + * @since 3.7.0 + */ + protected function getFieldsTemplate() + { + return array( + 'id' => 'a.id', + 'title' => 'a.title', + 'alias' => 'a.alias', + 'ordering' => 'a.ordering', + 'menutype' => '', + 'level' => '', + 'catid' => 'a.catid', + 'language' => 'a.language', + 'access' => 'a.access', + 'state' => 'a.state', + 'created_user_id' => 'a.created_by', + 'checked_out' => 'a.checked_out', + 'checked_out_time' => 'a.checked_out_time' + ); + } } diff --git a/libraries/src/Association/AssociationExtensionInterface.php b/libraries/src/Association/AssociationExtensionInterface.php index 8d9d612126462..6399dfad2a5d0 100644 --- a/libraries/src/Association/AssociationExtensionInterface.php +++ b/libraries/src/Association/AssociationExtensionInterface.php @@ -1,4 +1,5 @@ associationExtension; - } + /** + * Returns the associations extension helper class. + * + * @return AssociationExtensionInterface + * + * @since 4.0.0 + */ + public function getAssociationsExtension(): AssociationExtensionInterface + { + return $this->associationExtension; + } - /** - * The association extension. - * - * @param AssociationExtensionInterface $associationExtension The extension - * - * @return void - * - * @since 4.0.0 - */ - public function setAssociationExtension(AssociationExtensionInterface $associationExtension) - { - $this->associationExtension = $associationExtension; - } + /** + * The association extension. + * + * @param AssociationExtensionInterface $associationExtension The extension + * + * @return void + * + * @since 4.0.0 + */ + public function setAssociationExtension(AssociationExtensionInterface $associationExtension) + { + $this->associationExtension = $associationExtension; + } } diff --git a/libraries/src/Authentication/Authentication.php b/libraries/src/Authentication/Authentication.php index 05f2b629372c2..c9068aac33cbf 100644 --- a/libraries/src/Authentication/Authentication.php +++ b/libraries/src/Authentication/Authentication.php @@ -1,4 +1,5 @@ get('dispatcher'); - } - - $this->setDispatcher($dispatcher); - $this->pluginType = $pluginType; - - $isLoaded = PluginHelper::importPlugin($this->pluginType); - - if (!$isLoaded) - { - Log::add(Text::_('JLIB_USER_ERROR_AUTHENTICATION_LIBRARIES'), Log::WARNING, 'jerror'); - } - } - - /** - * Returns the global authentication object, only creating it - * if it doesn't already exist. - * - * @param string $pluginType The plugin type to run authorisation and authentication on - * - * @return Authentication The global Authentication object - * - * @since 1.7.0 - */ - public static function getInstance(string $pluginType = 'authentication') - { - if (empty(self::$instance[$pluginType])) - { - self::$instance[$pluginType] = new static($pluginType); - } - - return self::$instance[$pluginType]; - } - - /** - * Finds out if a set of login credentials are valid by asking all observing - * objects to run their respective authentication routines. - * - * @param array $credentials Array holding the user credentials. - * @param array $options Array holding user options. - * - * @return AuthenticationResponse Response object with status variable filled in for last plugin or first successful plugin. - * - * @see AuthenticationResponse - * @since 1.7.0 - */ - public function authenticate($credentials, $options = array()) - { - // Get plugins - $plugins = PluginHelper::getPlugin($this->pluginType); - - // Create authentication response - $response = new AuthenticationResponse; - - /* - * Loop through the plugins and check if the credentials can be used to authenticate - * the user - * - * Any errors raised in the plugin should be returned via the AuthenticationResponse - * and handled appropriately. - */ - foreach ($plugins as $plugin) - { - $plugin = Factory::getApplication()->bootPlugin($plugin->name, $plugin->type); - - if (!method_exists($plugin, 'onUserAuthenticate')) - { - // Bail here if the plugin can't be created - Log::add(Text::sprintf('JLIB_USER_ERROR_AUTHENTICATION_FAILED_LOAD_PLUGIN', $plugin->name), Log::WARNING, 'jerror'); - continue; - } - - // Try to authenticate - $plugin->onUserAuthenticate($credentials, $options, $response); - - // If authentication is successful break out of the loop - if ($response->status === self::STATUS_SUCCESS) - { - if (empty($response->type)) - { - $response->type = $plugin->_name ?? $plugin->name; - } - - break; - } - } - - if (empty($response->username)) - { - $response->username = $credentials['username']; - } - - if (empty($response->fullname)) - { - $response->fullname = $credentials['username']; - } - - if (empty($response->password) && isset($credentials['password'])) - { - $response->password = $credentials['password']; - } - - return $response; - } - - /** - * Authorises that a particular user should be able to login - * - * @param AuthenticationResponse $response response including username of the user to authorise - * @param array $options list of options - * - * @return AuthenticationResponse[] Array of authentication response objects - * - * @since 1.7.0 - * @throws \Exception - */ - public function authorise($response, $options = array()) - { - // Get plugins in case they haven't been imported already - PluginHelper::importPlugin('user'); - $results = Factory::getApplication()->triggerEvent('onUserAuthorisation', array($response, $options)); - - return $results; - } + use DispatcherAwareTrait; + + /** + * This is the status code returned when the authentication is success (permit login) + * + * @var integer + * @since 1.7.0 + */ + public const STATUS_SUCCESS = 1; + + /** + * Status to indicate cancellation of authentication (unused) + * + * @var integer + * @since 1.7.0 + */ + public const STATUS_CANCEL = 2; + + /** + * This is the status code returned when the authentication failed (prevent login if no success) + * + * @var integer + * @since 1.7.0 + */ + public const STATUS_FAILURE = 4; + + /** + * This is the status code returned when the account has expired (prevent login) + * + * @var integer + * @since 1.7.0 + */ + public const STATUS_EXPIRED = 8; + + /** + * This is the status code returned when the account has been denied (prevent login) + * + * @var integer + * @since 1.7.0 + */ + public const STATUS_DENIED = 16; + + /** + * This is the status code returned when the account doesn't exist (not an error) + * + * @var integer + * @since 1.7.0 + */ + public const STATUS_UNKNOWN = 32; + + /** + * @var Authentication[] JAuthentication instances container. + * @since 1.7.3 + */ + protected static $instance = []; + + /** + * Plugin Type to run + * + * @var string + * @since 4.0.0 + */ + protected $pluginType; + + /** + * Constructor + * + * @param string $pluginType The plugin type to run authorisation and authentication on + * @param DispatcherInterface $dispatcher The event dispatcher we're going to use + * + * @since 1.7.0 + */ + public function __construct(string $pluginType = 'authentication', DispatcherInterface $dispatcher = null) + { + // Set the dispatcher + if (!\is_object($dispatcher)) { + $dispatcher = Factory::getContainer()->get('dispatcher'); + } + + $this->setDispatcher($dispatcher); + $this->pluginType = $pluginType; + + $isLoaded = PluginHelper::importPlugin($this->pluginType); + + if (!$isLoaded) { + Log::add(Text::_('JLIB_USER_ERROR_AUTHENTICATION_LIBRARIES'), Log::WARNING, 'jerror'); + } + } + + /** + * Returns the global authentication object, only creating it + * if it doesn't already exist. + * + * @param string $pluginType The plugin type to run authorisation and authentication on + * + * @return Authentication The global Authentication object + * + * @since 1.7.0 + */ + public static function getInstance(string $pluginType = 'authentication') + { + if (empty(self::$instance[$pluginType])) { + self::$instance[$pluginType] = new static($pluginType); + } + + return self::$instance[$pluginType]; + } + + /** + * Finds out if a set of login credentials are valid by asking all observing + * objects to run their respective authentication routines. + * + * @param array $credentials Array holding the user credentials. + * @param array $options Array holding user options. + * + * @return AuthenticationResponse Response object with status variable filled in for last plugin or first successful plugin. + * + * @see AuthenticationResponse + * @since 1.7.0 + */ + public function authenticate($credentials, $options = array()) + { + // Get plugins + $plugins = PluginHelper::getPlugin($this->pluginType); + + // Create authentication response + $response = new AuthenticationResponse(); + + /* + * Loop through the plugins and check if the credentials can be used to authenticate + * the user + * + * Any errors raised in the plugin should be returned via the AuthenticationResponse + * and handled appropriately. + */ + foreach ($plugins as $plugin) { + $plugin = Factory::getApplication()->bootPlugin($plugin->name, $plugin->type); + + if (!method_exists($plugin, 'onUserAuthenticate')) { + // Bail here if the plugin can't be created + Log::add(Text::sprintf('JLIB_USER_ERROR_AUTHENTICATION_FAILED_LOAD_PLUGIN', $plugin->name), Log::WARNING, 'jerror'); + continue; + } + + // Try to authenticate + $plugin->onUserAuthenticate($credentials, $options, $response); + + // If authentication is successful break out of the loop + if ($response->status === self::STATUS_SUCCESS) { + if (empty($response->type)) { + $response->type = $plugin->_name ?? $plugin->name; + } + + break; + } + } + + if (empty($response->username)) { + $response->username = $credentials['username']; + } + + if (empty($response->fullname)) { + $response->fullname = $credentials['username']; + } + + if (empty($response->password) && isset($credentials['password'])) { + $response->password = $credentials['password']; + } + + return $response; + } + + /** + * Authorises that a particular user should be able to login + * + * @param AuthenticationResponse $response response including username of the user to authorise + * @param array $options list of options + * + * @return AuthenticationResponse[] Array of authentication response objects + * + * @since 1.7.0 + * @throws \Exception + */ + public function authorise($response, $options = array()) + { + // Get plugins in case they haven't been imported already + PluginHelper::importPlugin('user'); + $results = Factory::getApplication()->triggerEvent('onUserAuthorisation', array($response, $options)); + + return $results; + } } diff --git a/libraries/src/Authentication/AuthenticationResponse.php b/libraries/src/Authentication/AuthenticationResponse.php index 8d4b7dba63fc7..f29f5cd51f5bb 100644 --- a/libraries/src/Authentication/AuthenticationResponse.php +++ b/libraries/src/Authentication/AuthenticationResponse.php @@ -1,4 +1,5 @@ handlers[] = $handler; - } + /** + * Add a handler to the chain + * + * @param HandlerInterface $handler The password handler to add + * + * @return void + * + * @since 4.0.0 + */ + public function addHandler(HandlerInterface $handler) + { + $this->handlers[] = $handler; + } - /** - * Check if the password requires rehashing - * - * @param string $hash The password hash to check - * - * @return boolean - * - * @since 4.0.0 - */ - public function checkIfRehashNeeded(string $hash): bool - { - foreach ($this->handlers as $handler) - { - if ($handler instanceof CheckIfRehashNeededHandlerInterface && $handler->isSupported() && $handler->checkIfRehashNeeded($hash)) - { - return true; - } - } + /** + * Check if the password requires rehashing + * + * @param string $hash The password hash to check + * + * @return boolean + * + * @since 4.0.0 + */ + public function checkIfRehashNeeded(string $hash): bool + { + foreach ($this->handlers as $handler) { + if ($handler instanceof CheckIfRehashNeededHandlerInterface && $handler->isSupported() && $handler->checkIfRehashNeeded($hash)) { + return true; + } + } - return false; - } + return false; + } - /** - * Generate a hash for a plaintext password - * - * @param string $plaintext The plaintext password to validate - * @param array $options Options for the hashing operation - * - * @return void - * - * @since 4.0.0 - * @throws \RuntimeException - */ - public function hashPassword($plaintext, array $options = []) - { - throw new \RuntimeException('The chained password handler cannot be used to hash a password'); - } + /** + * Generate a hash for a plaintext password + * + * @param string $plaintext The plaintext password to validate + * @param array $options Options for the hashing operation + * + * @return void + * + * @since 4.0.0 + * @throws \RuntimeException + */ + public function hashPassword($plaintext, array $options = []) + { + throw new \RuntimeException('The chained password handler cannot be used to hash a password'); + } - /** - * Check that the password handler is supported in this environment - * - * @return boolean - * - * @since 4.0.0 - */ - public static function isSupported() - { - return true; - } + /** + * Check that the password handler is supported in this environment + * + * @return boolean + * + * @since 4.0.0 + */ + public static function isSupported() + { + return true; + } - /** - * Validate a password - * - * @param string $plaintext The plain text password to validate - * @param string $hashed The password hash to validate against - * - * @return boolean - * - * @since 4.0.0 - */ - public function validatePassword($plaintext, $hashed) - { - foreach ($this->handlers as $handler) - { - if ($handler->isSupported() && $handler->validatePassword($plaintext, $hashed)) - { - return true; - } - } + /** + * Validate a password + * + * @param string $plaintext The plain text password to validate + * @param string $hashed The password hash to validate against + * + * @return boolean + * + * @since 4.0.0 + */ + public function validatePassword($plaintext, $hashed) + { + foreach ($this->handlers as $handler) { + if ($handler->isSupported() && $handler->validatePassword($plaintext, $hashed)) { + return true; + } + } - return false; - } + return false; + } } diff --git a/libraries/src/Authentication/Password/CheckIfRehashNeededHandlerInterface.php b/libraries/src/Authentication/Password/CheckIfRehashNeededHandlerInterface.php index 9974a0b07ecd3..ba7735a5727bf 100644 --- a/libraries/src/Authentication/Password/CheckIfRehashNeededHandlerInterface.php +++ b/libraries/src/Authentication/Password/CheckIfRehashNeededHandlerInterface.php @@ -1,4 +1,5 @@ getPasswordHash()->HashPassword($plaintext); - } + /** + * Generate a hash for a plaintext password + * + * @param string $plaintext The plaintext password to validate + * @param array $options Options for the hashing operation + * + * @return string + * + * @since 4.0.0 + */ + public function hashPassword($plaintext, array $options = []) + { + return $this->getPasswordHash()->HashPassword($plaintext); + } - /** - * Check that the password handler is supported in this environment - * - * @return boolean - * - * @since 4.0.0 - */ - public static function isSupported() - { - return class_exists(\PasswordHash::class); - } + /** + * Check that the password handler is supported in this environment + * + * @return boolean + * + * @since 4.0.0 + */ + public static function isSupported() + { + return class_exists(\PasswordHash::class); + } - /** - * Validate a password - * - * @param string $plaintext The plain text password to validate - * @param string $hashed The password hash to validate against - * - * @return boolean - * - * @since 4.0.0 - */ - public function validatePassword($plaintext, $hashed) - { - return $this->getPasswordHash()->CheckPassword($plaintext, $hashed); - } + /** + * Validate a password + * + * @param string $plaintext The plain text password to validate + * @param string $hashed The password hash to validate against + * + * @return boolean + * + * @since 4.0.0 + */ + public function validatePassword($plaintext, $hashed) + { + return $this->getPasswordHash()->CheckPassword($plaintext, $hashed); + } - /** - * Get an instance of the PasswordHash class - * - * @return \PasswordHash - * - * @since 4.0.0 - */ - private function getPasswordHash(): \PasswordHash - { - return new \PasswordHash(10, true); - } + /** + * Get an instance of the PasswordHash class + * + * @return \PasswordHash + * + * @since 4.0.0 + */ + private function getPasswordHash(): \PasswordHash + { + return new \PasswordHash(10, true); + } } diff --git a/libraries/src/Authentication/ProviderAwareAuthenticationPluginInterface.php b/libraries/src/Authentication/ProviderAwareAuthenticationPluginInterface.php index 759eaa46a5bb3..3a2e9b1dc6d09 100644 --- a/libraries/src/Authentication/ProviderAwareAuthenticationPluginInterface.php +++ b/libraries/src/Authentication/ProviderAwareAuthenticationPluginInterface.php @@ -1,4 +1,5 @@ loader = $loader; - } + /** + * Constructor + * + * @param ComposerClassLoader $loader Composer autoloader + * + * @since 3.4 + */ + public function __construct(ComposerClassLoader $loader) + { + $this->loader = $loader; + } - /** - * Loads the given class or interface. - * - * @param string $class The name of the class - * - * @return boolean|null True if loaded, null otherwise - * - * @since 3.4 - */ - public function loadClass($class) - { - if ($result = $this->loader->loadClass($class)) - { - \JLoader::applyAliasFor($class); - } + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * + * @return boolean|null True if loaded, null otherwise + * + * @since 3.4 + */ + public function loadClass($class) + { + if ($result = $this->loader->loadClass($class)) { + \JLoader::applyAliasFor($class); + } - return $result; - } + return $result; + } } diff --git a/libraries/src/Button/ActionButton.php b/libraries/src/Button/ActionButton.php index d4da31ef276e7..9253815bddcdb 100644 --- a/libraries/src/Button/ActionButton.php +++ b/libraries/src/Button/ActionButton.php @@ -1,4 +1,5 @@ null, - 'task' => '', - 'icon' => 'question', - 'title' => 'Unknown state', - 'options' => [ - 'disabled' => false, - 'only_icon' => false, - 'tip' => true, - 'tip_title' => '', - 'task_prefix' => '', - 'checkbox_name' => 'cb', - ], - ]; - - /** - * Options of this button set. - * - * @var Registry - * - * @since 4.0.0 - */ - protected $options; - - /** - * The layout path to render. - * - * @var string - * - * @since 4.0.0 - */ - protected $layout = 'joomla.button.action-button'; - - /** - * ActionButton constructor. - * - * @param array $options The options for all buttons in this group. - * - * @since 4.0.0 - */ - public function __construct(array $options = []) - { - $this->options = new Registry($options); - - // Replace some dynamic values - $this->unknownState['title'] = Text::_('JLIB_HTML_UNKNOWN_STATE'); - - $this->preprocess(); - } - - /** - * Configure this object. - * - * @return void - * - * @since 4.0.0 - */ - protected function preprocess() - { - // Implement this method. - } - - /** - * Add a state profile. - * - * @param integer $value The value of this state. - * @param string $task The task you want to execute after click this button. - * @param string $icon The icon to display for user. - * @param string $title Title text will show if we enable tooltips. - * @param array $options The button options, will override group options. - * - * @return static Return self to support chaining. - * - * @since 4.0.0 - */ - public function addState(int $value, string $task, string $icon = 'ok', string $title = '', array $options = []): self - { - // Force type to prevent null data - $this->states[$value] = [ - 'value' => $value, - 'task' => $task, - 'icon' => $icon, - 'title' => $title, - 'options' => $options - ]; - - return $this; - } - - /** - * Get state profile by value name. - * - * @param integer $value The value name we want to get. - * - * @return array|null Return state profile or NULL. - * - * @since 4.0.0 - */ - public function getState(int $value): ?array - { - return $this->states[$value] ?? null; - } - - /** - * Remove a state by value name. - * - * @param integer $value Remove state by this value. - * - * @return static Return to support chaining. - * - * @since 4.0.0 - */ - public function removeState(int $value): self - { - if (isset($this->states[$value])) - { - unset($this->states[$value]); - } - - return $this; - } - - /** - * Render action button by item value. - * - * @param integer|null $value Current value of this item. - * @param integer|null $row The row number of this item. - * @param array $options The options to override group options. - * - * @return string Rendered HTML. - * - * @since 4.0.0 - * - * @throws \InvalidArgumentException - */ - public function render(?int $value = null, ?int $row = null, array $options = []): string - { - $data = $this->getState($value) ?? $this->unknownState; - - $data = ArrayHelper::mergeRecursive( - $this->unknownState, - $data, - [ - 'options' => $this->options->toArray() - ], - [ - 'options' => $options - ] - ); - - $data['row'] = $row; - $data['icon'] = $this->fetchIconClass($data['icon']); - - return LayoutHelper::render($this->layout, $data); - } - - /** - * Render to string. - * - * @return string - * - * @since 4.0.0 - */ - public function __toString(): string - { - try - { - return $this->render(); - } - catch (\Throwable $e) - { - return (string) $e; - } - } - - /** - * Method to get property layout. - * - * @return string - * - * @since 4.0.0 - */ - public function getLayout(): string - { - return $this->layout; - } - - /** - * Method to set property template. - * - * @param string $layout The layout path. - * - * @return static Return self to support chaining. - * - * @since 4.0.0 - */ - public function setLayout(string $layout): self - { - $this->layout = $layout; - - return $this; - } - - /** - * Method to get property options. - * - * @return array - * - * @since 4.0.0 - */ - public function getOptions(): array - { - return (array) $this->options->toArray(); - } - - /** - * Method to set property options. - * - * @param array $options The options of this button group. - * - * @return static Return self to support chaining. - * - * @since 4.0.0 - */ - public function setOptions(array $options): self - { - $this->options = new Registry($options); - - return $this; - } - - /** - * Get an option value. - * - * @param string $name The option name. - * @param mixed $default Default value if not exists. - * - * @return mixed Return option value or default value. - * - * @since 4.0.0 - */ - public function getOption(string $name, $default = null) - { - return $this->options->get($name, $default); - } - - /** - * Set option value. - * - * @param string $name The option name. - * @param mixed $value The option value. - * - * @return static Return self to support chaining. - * - * @since 4.0.0 - */ - public function setOption(string $name, $value): self - { - $this->options->set($name, $value); - - return $this; - } - - /** - * Method to get the CSS class name for an icon identifier. - * - * Can be redefined in the final class. - * - * @param string $identifier Icon identification string. - * - * @return string CSS class name. - * - * @since 4.0.0 - */ - public function fetchIconClass(string $identifier): string - { - // It's an ugly hack, but this allows templates to define the icon classes for the toolbar - $layout = new FileLayout('joomla.button.iconclass'); - - return $layout->render(array('icon' => $identifier)); - } + /** + * The button states profiles. + * + * @var array + * + * @since 4.0.0 + */ + protected $states = []; + + /** + * Default options for unknown state. + * + * @var array + * + * @since 4.0.0 + */ + protected $unknownState = [ + 'value' => null, + 'task' => '', + 'icon' => 'question', + 'title' => 'Unknown state', + 'options' => [ + 'disabled' => false, + 'only_icon' => false, + 'tip' => true, + 'tip_title' => '', + 'task_prefix' => '', + 'checkbox_name' => 'cb', + ], + ]; + + /** + * Options of this button set. + * + * @var Registry + * + * @since 4.0.0 + */ + protected $options; + + /** + * The layout path to render. + * + * @var string + * + * @since 4.0.0 + */ + protected $layout = 'joomla.button.action-button'; + + /** + * ActionButton constructor. + * + * @param array $options The options for all buttons in this group. + * + * @since 4.0.0 + */ + public function __construct(array $options = []) + { + $this->options = new Registry($options); + + // Replace some dynamic values + $this->unknownState['title'] = Text::_('JLIB_HTML_UNKNOWN_STATE'); + + $this->preprocess(); + } + + /** + * Configure this object. + * + * @return void + * + * @since 4.0.0 + */ + protected function preprocess() + { + // Implement this method. + } + + /** + * Add a state profile. + * + * @param integer $value The value of this state. + * @param string $task The task you want to execute after click this button. + * @param string $icon The icon to display for user. + * @param string $title Title text will show if we enable tooltips. + * @param array $options The button options, will override group options. + * + * @return static Return self to support chaining. + * + * @since 4.0.0 + */ + public function addState(int $value, string $task, string $icon = 'ok', string $title = '', array $options = []): self + { + // Force type to prevent null data + $this->states[$value] = [ + 'value' => $value, + 'task' => $task, + 'icon' => $icon, + 'title' => $title, + 'options' => $options + ]; + + return $this; + } + + /** + * Get state profile by value name. + * + * @param integer $value The value name we want to get. + * + * @return array|null Return state profile or NULL. + * + * @since 4.0.0 + */ + public function getState(int $value): ?array + { + return $this->states[$value] ?? null; + } + + /** + * Remove a state by value name. + * + * @param integer $value Remove state by this value. + * + * @return static Return to support chaining. + * + * @since 4.0.0 + */ + public function removeState(int $value): self + { + if (isset($this->states[$value])) { + unset($this->states[$value]); + } + + return $this; + } + + /** + * Render action button by item value. + * + * @param integer|null $value Current value of this item. + * @param integer|null $row The row number of this item. + * @param array $options The options to override group options. + * + * @return string Rendered HTML. + * + * @since 4.0.0 + * + * @throws \InvalidArgumentException + */ + public function render(?int $value = null, ?int $row = null, array $options = []): string + { + $data = $this->getState($value) ?? $this->unknownState; + + $data = ArrayHelper::mergeRecursive( + $this->unknownState, + $data, + [ + 'options' => $this->options->toArray() + ], + [ + 'options' => $options + ] + ); + + $data['row'] = $row; + $data['icon'] = $this->fetchIconClass($data['icon']); + + return LayoutHelper::render($this->layout, $data); + } + + /** + * Render to string. + * + * @return string + * + * @since 4.0.0 + */ + public function __toString(): string + { + try { + return $this->render(); + } catch (\Throwable $e) { + return (string) $e; + } + } + + /** + * Method to get property layout. + * + * @return string + * + * @since 4.0.0 + */ + public function getLayout(): string + { + return $this->layout; + } + + /** + * Method to set property template. + * + * @param string $layout The layout path. + * + * @return static Return self to support chaining. + * + * @since 4.0.0 + */ + public function setLayout(string $layout): self + { + $this->layout = $layout; + + return $this; + } + + /** + * Method to get property options. + * + * @return array + * + * @since 4.0.0 + */ + public function getOptions(): array + { + return (array) $this->options->toArray(); + } + + /** + * Method to set property options. + * + * @param array $options The options of this button group. + * + * @return static Return self to support chaining. + * + * @since 4.0.0 + */ + public function setOptions(array $options): self + { + $this->options = new Registry($options); + + return $this; + } + + /** + * Get an option value. + * + * @param string $name The option name. + * @param mixed $default Default value if not exists. + * + * @return mixed Return option value or default value. + * + * @since 4.0.0 + */ + public function getOption(string $name, $default = null) + { + return $this->options->get($name, $default); + } + + /** + * Set option value. + * + * @param string $name The option name. + * @param mixed $value The option value. + * + * @return static Return self to support chaining. + * + * @since 4.0.0 + */ + public function setOption(string $name, $value): self + { + $this->options->set($name, $value); + + return $this; + } + + /** + * Method to get the CSS class name for an icon identifier. + * + * Can be redefined in the final class. + * + * @param string $identifier Icon identification string. + * + * @return string CSS class name. + * + * @since 4.0.0 + */ + public function fetchIconClass(string $identifier): string + { + // It's an ugly hack, but this allows templates to define the icon classes for the toolbar + $layout = new FileLayout('joomla.button.iconclass'); + + return $layout->render(array('icon' => $identifier)); + } } diff --git a/libraries/src/Button/FeaturedButton.php b/libraries/src/Button/FeaturedButton.php index 818f2b05f3b63..fac1b09cd9b0d 100644 --- a/libraries/src/Button/FeaturedButton.php +++ b/libraries/src/Button/FeaturedButton.php @@ -1,4 +1,5 @@ addState(0, 'featured', 'icon-unfeatured', - Text::_('JGLOBAL_TOGGLE_FEATURED'), ['tip_title' => Text::_('JUNFEATURED')] - ); - $this->addState(1, 'unfeatured', 'icon-color-featured icon-star', - Text::_('JGLOBAL_TOGGLE_FEATURED'), ['tip_title' => Text::_('JFEATURED')] - ); - } - - /** - * Render action button by item value. - * - * @param integer|null $value Current value of this item. - * @param integer|null $row The row number of this item. - * @param array $options The options to override group options. - * @param string|Date $featuredUp The date which item featured up. - * @param string|Date $featuredDown The date which item featured down. - * - * @return string Rendered HTML. - * - * @since 4.0.0 - */ - public function render(?int $value = null, ?int $row = null, array $options = [], $featuredUp = null, $featuredDown = null): string - { - if ($featuredUp || $featuredDown) - { - $bakState = $this->getState($value); - $default = $this->getState($value) ?? $this->unknownState; - - $nowDate = Factory::getDate()->toUnix(); - - $tz = Factory::getUser()->getTimezone(); - - if (!is_null($featuredUp)) - { - $featuredUp = Factory::getDate($featuredUp, 'UTC')->setTimezone($tz); - } - - if (!is_null($featuredDown)) - { - $featuredDown = Factory::getDate($featuredDown, 'UTC')->setTimezone($tz); - } - - // Add tips and special titles - // Create special titles for featured items - if ($value === 1) - { - // Create tip text, only we have featured up or down settings - $tips = []; - - if ($featuredUp) - { - $tips[] = Text::sprintf('JLIB_HTML_FEATURED_STARTED', HTMLHelper::_('date', $featuredUp, Text::_('DATE_FORMAT_LC5'), 'UTC')); - } - - if ($featuredDown) - { - $tips[] = Text::sprintf('JLIB_HTML_FEATURED_FINISHED', HTMLHelper::_('date', $featuredDown, Text::_('DATE_FORMAT_LC5'), 'UTC')); - } - - $tip = empty($tips) ? false : implode('
    ', $tips); - - $default['title'] = $tip; - - $options['tip_title'] = Text::_('JLIB_HTML_FEATURED_ITEM'); - - if ($featuredUp && $nowDate < $featuredUp->toUnix()) - { - $options['tip_title'] = Text::_('JLIB_HTML_FEATURED_PENDING_ITEM'); - $default['icon'] = 'pending'; - } - - if ($featuredDown && $nowDate > $featuredDown->toUnix()) - { - $options['tip_title'] = Text::_('JLIB_HTML_FEATURED_EXPIRED_ITEM'); - $default['icon'] = 'expired'; - } - } - - $this->states[$value] = $default; - - $html = parent::render($value, $row, $options); - - $this->states[$value] = $bakState; - - return $html; - } - - return parent::render($value, $row, $options); - } + /** + * Configure this object. + * + * @return void + * + * @since 4.0.0 + */ + protected function preprocess() + { + $this->addState( + 0, + 'featured', + 'icon-unfeatured', + Text::_('JGLOBAL_TOGGLE_FEATURED'), + ['tip_title' => Text::_('JUNFEATURED')] + ); + $this->addState( + 1, + 'unfeatured', + 'icon-color-featured icon-star', + Text::_('JGLOBAL_TOGGLE_FEATURED'), + ['tip_title' => Text::_('JFEATURED')] + ); + } + + /** + * Render action button by item value. + * + * @param integer|null $value Current value of this item. + * @param integer|null $row The row number of this item. + * @param array $options The options to override group options. + * @param string|Date $featuredUp The date which item featured up. + * @param string|Date $featuredDown The date which item featured down. + * + * @return string Rendered HTML. + * + * @since 4.0.0 + */ + public function render(?int $value = null, ?int $row = null, array $options = [], $featuredUp = null, $featuredDown = null): string + { + if ($featuredUp || $featuredDown) { + $bakState = $this->getState($value); + $default = $this->getState($value) ?? $this->unknownState; + + $nowDate = Factory::getDate()->toUnix(); + + $tz = Factory::getUser()->getTimezone(); + + if (!is_null($featuredUp)) { + $featuredUp = Factory::getDate($featuredUp, 'UTC')->setTimezone($tz); + } + + if (!is_null($featuredDown)) { + $featuredDown = Factory::getDate($featuredDown, 'UTC')->setTimezone($tz); + } + + // Add tips and special titles + // Create special titles for featured items + if ($value === 1) { + // Create tip text, only we have featured up or down settings + $tips = []; + + if ($featuredUp) { + $tips[] = Text::sprintf('JLIB_HTML_FEATURED_STARTED', HTMLHelper::_('date', $featuredUp, Text::_('DATE_FORMAT_LC5'), 'UTC')); + } + + if ($featuredDown) { + $tips[] = Text::sprintf('JLIB_HTML_FEATURED_FINISHED', HTMLHelper::_('date', $featuredDown, Text::_('DATE_FORMAT_LC5'), 'UTC')); + } + + $tip = empty($tips) ? false : implode('
    ', $tips); + + $default['title'] = $tip; + + $options['tip_title'] = Text::_('JLIB_HTML_FEATURED_ITEM'); + + if ($featuredUp && $nowDate < $featuredUp->toUnix()) { + $options['tip_title'] = Text::_('JLIB_HTML_FEATURED_PENDING_ITEM'); + $default['icon'] = 'pending'; + } + + if ($featuredDown && $nowDate > $featuredDown->toUnix()) { + $options['tip_title'] = Text::_('JLIB_HTML_FEATURED_EXPIRED_ITEM'); + $default['icon'] = 'expired'; + } + } + + $this->states[$value] = $default; + + $html = parent::render($value, $row, $options); + + $this->states[$value] = $bakState; + + return $html; + } + + return parent::render($value, $row, $options); + } } diff --git a/libraries/src/Button/PublishedButton.php b/libraries/src/Button/PublishedButton.php index 064e34b0154e4..7fff3a9d70ab9 100644 --- a/libraries/src/Button/PublishedButton.php +++ b/libraries/src/Button/PublishedButton.php @@ -1,4 +1,5 @@ addState(1, 'unpublish', 'publish', Text::_('JLIB_HTML_UNPUBLISH_ITEM'), ['tip_title' => Text::_('JPUBLISHED')]); - $this->addState(0, 'publish', 'unpublish', Text::_('JLIB_HTML_PUBLISH_ITEM'), ['tip_title' => Text::_('JUNPUBLISHED')]); - $this->addState(2, 'unpublish', 'archive', Text::_('JLIB_HTML_UNPUBLISH_ITEM'), ['tip_title' => Text::_('JARCHIVED')]); - $this->addState(-2, 'publish', 'trash', Text::_('JLIB_HTML_PUBLISH_ITEM'), ['tip_title' => Text::_('JTRASHED')]); - } - - /** - * Render action button by item value. - * - * @param integer|null $value Current value of this item. - * @param integer|null $row The row number of this item. - * @param array $options The options to override group options. - * @param string|Date $publishUp The date which item publish up. - * @param string|Date $publishDown The date which item publish down. - * - * @return string Rendered HTML. - * - * @since 4.0.0 - */ - public function render(?int $value = null, ?int $row = null, array $options = [], $publishUp = null, $publishDown = null): string - { - if ($publishUp || $publishDown) - { - $bakState = $this->getState($value); - $default = $this->getState($value) ?? $this->unknownState; - - $nullDate = Factory::getDbo()->getNullDate(); - $nowDate = Factory::getDate()->toUnix(); - - $tz = Factory::getUser()->getTimezone(); - - $publishUp = ($publishUp !== null && $publishUp !== $nullDate) ? Factory::getDate($publishUp, 'UTC')->setTimezone($tz) : false; - $publishDown = ($publishDown !== null && $publishDown !== $nullDate) ? Factory::getDate($publishDown, 'UTC')->setTimezone($tz) : false; - - // Add tips and special titles - // Create special titles for published items - if ($value === 1) - { - // Create tip text, only we have publish up or down settings - $tips = array(); - - if ($publishUp) - { - $tips[] = Text::sprintf('JLIB_HTML_PUBLISHED_START', HTMLHelper::_('date', $publishUp, Text::_('DATE_FORMAT_LC5'), 'UTC')); - $tips[] = Text::_('JLIB_HTML_PUBLISHED_UNPUBLISH'); - } - - if ($publishDown) - { - $tips[] = Text::sprintf('JLIB_HTML_PUBLISHED_FINISHED', HTMLHelper::_('date', $publishDown, Text::_('DATE_FORMAT_LC5'), 'UTC')); - } - - $tip = empty($tips) ? false : implode('
    ', $tips); - - $default['title'] = $tip; - - $options['tip_title'] = Text::_('JLIB_HTML_PUBLISHED_ITEM'); - - if ($publishUp && $nowDate < $publishUp->toUnix()) - { - $options['tip_title'] = Text::_('JLIB_HTML_PUBLISHED_PENDING_ITEM'); - $default['icon'] = 'pending'; - } - - if ($publishDown && $nowDate > $publishDown->toUnix()) - { - $options['tip_title'] = Text::_('JLIB_HTML_PUBLISHED_EXPIRED_ITEM'); - $default['icon'] = 'expired'; - } - - if (array_key_exists('category_published', $options)) - { - $categoryPublished = $options['category_published']; - - if ($categoryPublished === 0) - { - $options['tip_title'] = Text::_('JLIB_HTML_ITEM_PUBLISHED_BUT_CATEGORY_UNPUBLISHED'); - $default['icon'] = 'expired'; - } - - if ($categoryPublished === -2) - { - $options['tip_title'] = Text::_('JLIB_HTML_ITEM_PUBLISHED_BUT_CATEGORY_TRASHED'); - $default['icon'] = 'expired'; - } - } - } - - $this->states[$value] = $default; - - $html = parent::render($value, $row, $options); - - $this->states[$value] = $bakState; - - return $html; - } - - return parent::render($value, $row, $options); - } + /** + * Configure this object. + * + * @return void + * + * @since 4.0.0 + */ + protected function preprocess() + { + $this->addState(1, 'unpublish', 'publish', Text::_('JLIB_HTML_UNPUBLISH_ITEM'), ['tip_title' => Text::_('JPUBLISHED')]); + $this->addState(0, 'publish', 'unpublish', Text::_('JLIB_HTML_PUBLISH_ITEM'), ['tip_title' => Text::_('JUNPUBLISHED')]); + $this->addState(2, 'unpublish', 'archive', Text::_('JLIB_HTML_UNPUBLISH_ITEM'), ['tip_title' => Text::_('JARCHIVED')]); + $this->addState(-2, 'publish', 'trash', Text::_('JLIB_HTML_PUBLISH_ITEM'), ['tip_title' => Text::_('JTRASHED')]); + } + + /** + * Render action button by item value. + * + * @param integer|null $value Current value of this item. + * @param integer|null $row The row number of this item. + * @param array $options The options to override group options. + * @param string|Date $publishUp The date which item publish up. + * @param string|Date $publishDown The date which item publish down. + * + * @return string Rendered HTML. + * + * @since 4.0.0 + */ + public function render(?int $value = null, ?int $row = null, array $options = [], $publishUp = null, $publishDown = null): string + { + if ($publishUp || $publishDown) { + $bakState = $this->getState($value); + $default = $this->getState($value) ?? $this->unknownState; + + $nullDate = Factory::getDbo()->getNullDate(); + $nowDate = Factory::getDate()->toUnix(); + + $tz = Factory::getUser()->getTimezone(); + + $publishUp = ($publishUp !== null && $publishUp !== $nullDate) ? Factory::getDate($publishUp, 'UTC')->setTimezone($tz) : false; + $publishDown = ($publishDown !== null && $publishDown !== $nullDate) ? Factory::getDate($publishDown, 'UTC')->setTimezone($tz) : false; + + // Add tips and special titles + // Create special titles for published items + if ($value === 1) { + // Create tip text, only we have publish up or down settings + $tips = array(); + + if ($publishUp) { + $tips[] = Text::sprintf('JLIB_HTML_PUBLISHED_START', HTMLHelper::_('date', $publishUp, Text::_('DATE_FORMAT_LC5'), 'UTC')); + $tips[] = Text::_('JLIB_HTML_PUBLISHED_UNPUBLISH'); + } + + if ($publishDown) { + $tips[] = Text::sprintf('JLIB_HTML_PUBLISHED_FINISHED', HTMLHelper::_('date', $publishDown, Text::_('DATE_FORMAT_LC5'), 'UTC')); + } + + $tip = empty($tips) ? false : implode('
    ', $tips); + + $default['title'] = $tip; + + $options['tip_title'] = Text::_('JLIB_HTML_PUBLISHED_ITEM'); + + if ($publishUp && $nowDate < $publishUp->toUnix()) { + $options['tip_title'] = Text::_('JLIB_HTML_PUBLISHED_PENDING_ITEM'); + $default['icon'] = 'pending'; + } + + if ($publishDown && $nowDate > $publishDown->toUnix()) { + $options['tip_title'] = Text::_('JLIB_HTML_PUBLISHED_EXPIRED_ITEM'); + $default['icon'] = 'expired'; + } + + if (array_key_exists('category_published', $options)) { + $categoryPublished = $options['category_published']; + + if ($categoryPublished === 0) { + $options['tip_title'] = Text::_('JLIB_HTML_ITEM_PUBLISHED_BUT_CATEGORY_UNPUBLISHED'); + $default['icon'] = 'expired'; + } + + if ($categoryPublished === -2) { + $options['tip_title'] = Text::_('JLIB_HTML_ITEM_PUBLISHED_BUT_CATEGORY_TRASHED'); + $default['icon'] = 'expired'; + } + } + } + + $this->states[$value] = $default; + + $html = parent::render($value, $row, $options); + + $this->states[$value] = $bakState; + + return $html; + } + + return parent::render($value, $row, $options); + } } diff --git a/libraries/src/Button/TransitionButton.php b/libraries/src/Button/TransitionButton.php index 01dbbc3f648ff..3ef1087814522 100644 --- a/libraries/src/Button/TransitionButton.php +++ b/libraries/src/Button/TransitionButton.php @@ -1,4 +1,5 @@ unknownState['icon'] = 'shuffle'; - $this->unknownState['title'] = $options['title'] ?? Text::_('JLIB_HTML_UNKNOWN_STATE'); - $this->unknownState['tip_content'] = $options['tip_content'] ?? $this->unknownState['title']; - } + $this->unknownState['icon'] = 'shuffle'; + $this->unknownState['title'] = $options['title'] ?? Text::_('JLIB_HTML_UNKNOWN_STATE'); + $this->unknownState['tip_content'] = $options['tip_content'] ?? $this->unknownState['title']; + } - /** - * Render action button by item value. - * - * @param integer|null $value Current value of this item. - * @param integer|null $row The row number of this item. - * @param array $options The options to override group options. - * - * @return string Rendered HTML. - * - * @since 4.0.0 - */ - public function render(?int $value = null, ?int $row = null, array $options = []): string - { - $default = $this->unknownState; + /** + * Render action button by item value. + * + * @param integer|null $value Current value of this item. + * @param integer|null $row The row number of this item. + * @param array $options The options to override group options. + * + * @return string Rendered HTML. + * + * @since 4.0.0 + */ + public function render(?int $value = null, ?int $row = null, array $options = []): string + { + $default = $this->unknownState; - $options['tip_title'] = $options['tip_title'] ?? ($options['title'] ?? $default['title']); + $options['tip_title'] = $options['tip_title'] ?? ($options['title'] ?? $default['title']); - return parent::render($value, $row, $options); - } + return parent::render($value, $row, $options); + } } diff --git a/libraries/src/Captcha/Captcha.php b/libraries/src/Captcha/Captcha.php index 651afcebd15ab..45a01413fe94c 100644 --- a/libraries/src/Captcha/Captcha.php +++ b/libraries/src/Captcha/Captcha.php @@ -1,4 +1,5 @@ name = $captcha; - - if (!empty($options['dispatcher']) && $options['dispatcher'] instanceof DispatcherInterface) - { - $this->setDispatcher($options['dispatcher']); - } - else - { - $this->setDispatcher(Factory::getApplication()->getDispatcher()); - } - - $this->_load($options); - } - - /** - * Returns the global Captcha object, only creating it - * if it doesn't already exist. - * - * @param string $captcha The plugin to use. - * @param array $options Associative array of options. - * - * @return Captcha|null Instance of this class. - * - * @since 2.5 - * @throws \RuntimeException - */ - public static function getInstance($captcha, array $options = array()) - { - $signature = md5(serialize(array($captcha, $options))); - - if (empty(self::$instances[$signature])) - { - self::$instances[$signature] = new Captcha($captcha, $options); - } - - return self::$instances[$signature]; - } - - /** - * Fire the onInit event to initialise the captcha plugin. - * - * @param string $id The id of the field. - * - * @return boolean True on success - * - * @since 2.5 - * @throws \RuntimeException - */ - public function initialise($id) - { - $arg = ['id' => $id]; - - $this->update('onInit', $arg); - - return true; - } - - /** - * Get the HTML for the captcha. - * - * @param string $name The control name. - * @param string $id The id for the control. - * @param string $class Value for the HTML class attribute - * - * @return string The return value of the function "onDisplay" of the selected Plugin. - * - * @since 2.5 - * @throws \RuntimeException - */ - public function display($name, $id, $class = '') - { - // Check if captcha is already loaded. - if ($this->captcha === null) - { - return ''; - } - - // Initialise the Captcha. - if (!$this->initialise($id)) - { - return ''; - } - - $arg = [ - 'name' => $name, - 'id' => $id ?: $name, - 'class' => $class, - ]; - - $result = $this->update('onDisplay', $arg); - - return $result; - } - - /** - * Checks if the answer is correct. - * - * @param string $code The answer. - * - * @return bool Whether the provided answer was correct - * - * @since 2.5 - * @throws \RuntimeException - */ - public function checkAnswer($code) - { - // Check if captcha is already loaded - if ($this->captcha === null) - { - return false; - } - - $arg = ['code' => $code]; - - $result = $this->update('onCheckAnswer', $arg); - - return $result; - } - - /** - * Method to react on the setup of a captcha field. Gives the possibility - * to change the field and/or the XML element for the field. - * - * @param \Joomla\CMS\Form\Field\CaptchaField $field Captcha field instance - * @param \SimpleXMLElement $element XML form definition - * - * @return void - */ - public function setupField(\Joomla\CMS\Form\Field\CaptchaField $field, \SimpleXMLElement $element) - { - if ($this->captcha === null) - { - return; - } - - $arg = [ - 'field' => $field, - 'element' => $element, - ]; - - $result = $this->update('onSetupField', $arg); - - return $result; - } - - /** - * Method to call the captcha callback if it exist. - * - * @param string $name Callback name - * @param array &$args Arguments - * - * @return mixed - * - * @since 4.0.0 - */ - private function update($name, &$args) - { - if (method_exists($this->captcha, $name)) - { - return call_user_func_array(array($this->captcha, $name), array_values($args)); - } - - return null; - } - - /** - * Load the Captcha plugin. - * - * @param array $options Associative array of options. - * - * @return void - * - * @since 2.5 - * @throws \RuntimeException - */ - private function _load(array $options = array()) - { - // Build the path to the needed captcha plugin - $name = InputFilter::getInstance()->clean($this->name, 'cmd'); - $path = JPATH_PLUGINS . '/captcha/' . $name . '/' . $name . '.php'; - - if (!is_file($path)) - { - throw new \RuntimeException(Text::sprintf('JLIB_CAPTCHA_ERROR_PLUGIN_NOT_FOUND', $name)); - } - - // Require plugin file - require_once $path; - - // Get the plugin - $plugin = PluginHelper::getPlugin('captcha', $this->name); - - if (!$plugin) - { - throw new \RuntimeException(Text::sprintf('JLIB_CAPTCHA_ERROR_PLUGIN_NOT_FOUND', $name)); - } - - // Check for already loaded params - if (!($plugin->params instanceof Registry)) - { - $params = new Registry($plugin->params); - $plugin->params = $params; - } - - // Build captcha plugin classname - $name = 'PlgCaptcha' . $this->name; - $dispatcher = $this->getDispatcher(); - $this->captcha = new $name($dispatcher, (array) $plugin, $options); - } + use DispatcherAwareTrait; + + /** + * Captcha Plugin object + * + * @var CMSPlugin + * @since 2.5 + */ + private $captcha; + + /** + * Editor Plugin name + * + * @var string + * @since 2.5 + */ + private $name; + + /** + * Array of instances of this class. + * + * @var Captcha[] + * @since 2.5 + */ + private static $instances = array(); + + /** + * Class constructor. + * + * @param string $captcha The plugin to use. + * @param array $options Associative array of options. + * + * @since 2.5 + * @throws \RuntimeException + */ + public function __construct($captcha, $options) + { + $this->name = $captcha; + + if (!empty($options['dispatcher']) && $options['dispatcher'] instanceof DispatcherInterface) { + $this->setDispatcher($options['dispatcher']); + } else { + $this->setDispatcher(Factory::getApplication()->getDispatcher()); + } + + $this->_load($options); + } + + /** + * Returns the global Captcha object, only creating it + * if it doesn't already exist. + * + * @param string $captcha The plugin to use. + * @param array $options Associative array of options. + * + * @return Captcha|null Instance of this class. + * + * @since 2.5 + * @throws \RuntimeException + */ + public static function getInstance($captcha, array $options = array()) + { + $signature = md5(serialize(array($captcha, $options))); + + if (empty(self::$instances[$signature])) { + self::$instances[$signature] = new Captcha($captcha, $options); + } + + return self::$instances[$signature]; + } + + /** + * Fire the onInit event to initialise the captcha plugin. + * + * @param string $id The id of the field. + * + * @return boolean True on success + * + * @since 2.5 + * @throws \RuntimeException + */ + public function initialise($id) + { + $arg = ['id' => $id]; + + $this->update('onInit', $arg); + + return true; + } + + /** + * Get the HTML for the captcha. + * + * @param string $name The control name. + * @param string $id The id for the control. + * @param string $class Value for the HTML class attribute + * + * @return string The return value of the function "onDisplay" of the selected Plugin. + * + * @since 2.5 + * @throws \RuntimeException + */ + public function display($name, $id, $class = '') + { + // Check if captcha is already loaded. + if ($this->captcha === null) { + return ''; + } + + // Initialise the Captcha. + if (!$this->initialise($id)) { + return ''; + } + + $arg = [ + 'name' => $name, + 'id' => $id ?: $name, + 'class' => $class, + ]; + + $result = $this->update('onDisplay', $arg); + + return $result; + } + + /** + * Checks if the answer is correct. + * + * @param string $code The answer. + * + * @return bool Whether the provided answer was correct + * + * @since 2.5 + * @throws \RuntimeException + */ + public function checkAnswer($code) + { + // Check if captcha is already loaded + if ($this->captcha === null) { + return false; + } + + $arg = ['code' => $code]; + + $result = $this->update('onCheckAnswer', $arg); + + return $result; + } + + /** + * Method to react on the setup of a captcha field. Gives the possibility + * to change the field and/or the XML element for the field. + * + * @param \Joomla\CMS\Form\Field\CaptchaField $field Captcha field instance + * @param \SimpleXMLElement $element XML form definition + * + * @return void + */ + public function setupField(\Joomla\CMS\Form\Field\CaptchaField $field, \SimpleXMLElement $element) + { + if ($this->captcha === null) { + return; + } + + $arg = [ + 'field' => $field, + 'element' => $element, + ]; + + $result = $this->update('onSetupField', $arg); + + return $result; + } + + /** + * Method to call the captcha callback if it exist. + * + * @param string $name Callback name + * @param array &$args Arguments + * + * @return mixed + * + * @since 4.0.0 + */ + private function update($name, &$args) + { + if (method_exists($this->captcha, $name)) { + return call_user_func_array(array($this->captcha, $name), array_values($args)); + } + + return null; + } + + /** + * Load the Captcha plugin. + * + * @param array $options Associative array of options. + * + * @return void + * + * @since 2.5 + * @throws \RuntimeException + */ + private function _load(array $options = array()) + { + // Build the path to the needed captcha plugin + $name = InputFilter::getInstance()->clean($this->name, 'cmd'); + $path = JPATH_PLUGINS . '/captcha/' . $name . '/' . $name . '.php'; + + if (!is_file($path)) { + throw new \RuntimeException(Text::sprintf('JLIB_CAPTCHA_ERROR_PLUGIN_NOT_FOUND', $name)); + } + + // Require plugin file + require_once $path; + + // Get the plugin + $plugin = PluginHelper::getPlugin('captcha', $this->name); + + if (!$plugin) { + throw new \RuntimeException(Text::sprintf('JLIB_CAPTCHA_ERROR_PLUGIN_NOT_FOUND', $name)); + } + + // Check for already loaded params + if (!($plugin->params instanceof Registry)) { + $params = new Registry($plugin->params); + $plugin->params = $params; + } + + // Build captcha plugin classname + $name = 'PlgCaptcha' . $this->name; + $dispatcher = $this->getDispatcher(); + $this->captcha = new $name($dispatcher, (array) $plugin, $options); + } } diff --git a/libraries/src/Captcha/Google/HttpBridgePostRequestMethod.php b/libraries/src/Captcha/Google/HttpBridgePostRequestMethod.php index ff4a8f9c91440..c93cd90f446fd 100644 --- a/libraries/src/Captcha/Google/HttpBridgePostRequestMethod.php +++ b/libraries/src/Captcha/Google/HttpBridgePostRequestMethod.php @@ -1,4 +1,5 @@ http = $http ?: HttpFactory::getHttp(); - } + /** + * Class constructor. + * + * @param Http|null $http The HTTP adapter + * + * @since 3.9.0 + */ + public function __construct(Http $http = null) + { + $this->http = $http ?: HttpFactory::getHttp(); + } - /** - * Submit the request with the specified parameters. - * - * @param RequestParameters $params Request parameters - * - * @return string Body of the reCAPTCHA response - * - * @since 3.9.0 - */ - public function submit(RequestParameters $params) - { - try - { - $response = $this->http->post(self::SITE_VERIFY_URL, $params->toArray()); + /** + * Submit the request with the specified parameters. + * + * @param RequestParameters $params Request parameters + * + * @return string Body of the reCAPTCHA response + * + * @since 3.9.0 + */ + public function submit(RequestParameters $params) + { + try { + $response = $this->http->post(self::SITE_VERIFY_URL, $params->toArray()); - return (string) $response->getBody(); - } - catch (InvalidResponseCodeException $exception) - { - return ''; - } - } + return (string) $response->getBody(); + } catch (InvalidResponseCodeException $exception) { + return ''; + } + } } diff --git a/libraries/src/Categories/Categories.php b/libraries/src/Categories/Categories.php index 5194a33d017f1..15abb9e37fe00 100644 --- a/libraries/src/Categories/Categories.php +++ b/libraries/src/Categories/Categories.php @@ -1,4 +1,5 @@ _extension = $options['extension']; - $this->_table = $options['table']; - $this->_field = isset($options['field']) && $options['field'] ? $options['field'] : 'catid'; - $this->_key = isset($options['key']) && $options['key'] ? $options['key'] : 'id'; - $this->_statefield = isset($options['statefield']) ? $options['statefield'] : 'state'; - - $options['access'] = isset($options['access']) ? $options['access'] : 'true'; - $options['published'] = isset($options['published']) ? $options['published'] : 1; - $options['countItems'] = isset($options['countItems']) ? $options['countItems'] : 0; - $options['currentlang'] = Multilanguage::isEnabled() ? Factory::getLanguage()->getTag() : 0; - - $this->_options = $options; - } - - /** - * Returns a reference to a Categories object - * - * @param string $extension Name of the categories extension - * @param array $options An array of options - * - * @return Categories|boolean Categories object on success, boolean false if an object does not exist - * - * @since 1.6 - * @deprecated 5.0 Use the ComponentInterface to get the categories - */ - public static function getInstance($extension, $options = array()) - { - $hash = md5(strtolower($extension) . serialize($options)); - - if (isset(self::$instances[$hash])) - { - return self::$instances[$hash]; - } - - $categories = null; - - try - { - $parts = explode('.', $extension, 2); - - $component = Factory::getApplication()->bootComponent($parts[0]); - - if ($component instanceof CategoryServiceInterface) - { - $categories = $component->getCategory($options, \count($parts) > 1 ? $parts[1] : ''); - } - } - catch (SectionNotFoundException $e) - { - $categories = null; - } - - self::$instances[$hash] = $categories; - - return self::$instances[$hash]; - } - - /** - * Loads a specific category and all its children in a CategoryNode object. - * - * @param mixed $id an optional id integer or equal to 'root' - * @param boolean $forceload True to force the _load method to execute - * - * @return CategoryNode|null CategoryNode object or null if $id is not valid - * - * @since 1.6 - */ - public function get($id = 'root', $forceload = false) - { - if ($id !== 'root') - { - $id = (int) $id; - - if ($id == 0) - { - $id = 'root'; - } - } - - // If this $id has not been processed yet, execute the _load method - if ((!isset($this->_nodes[$id]) && !isset($this->_checkedCategories[$id])) || $forceload) - { - $this->_load($id); - } - - // If we already have a value in _nodes for this $id, then use it. - if (isset($this->_nodes[$id])) - { - return $this->_nodes[$id]; - } - - return null; - } - - /** - * Returns the extension of the category. - * - * @return string The extension - * - * @since 3.9.0 - */ - public function getExtension() - { - return $this->_extension; - } - - /** - * Load method - * - * @param integer $id Id of category to load - * - * @return void - * - * @since 1.6 - */ - protected function _load($id) - { - try - { - $db = $this->getDatabase(); - } - catch (DatabaseNotFoundException $e) - { - @trigger_error(sprintf('Database must be set, this will not be caught anymore in 5.0.'), E_USER_DEPRECATED); - $db = Factory::getContainer()->get(DatabaseInterface::class); - } - - $app = Factory::getApplication(); - $user = Factory::getUser(); - $extension = $this->_extension; - - if ($id !== 'root') - { - $id = (int) $id; - - if ($id === 0) - { - $id = 'root'; - } - } - - // Record that has this $id has been checked - $this->_checkedCategories[$id] = true; - - $query = $db->getQuery(true) - ->select( - [ - $db->quoteName('c.id'), - $db->quoteName('c.asset_id'), - $db->quoteName('c.access'), - $db->quoteName('c.alias'), - $db->quoteName('c.checked_out'), - $db->quoteName('c.checked_out_time'), - $db->quoteName('c.created_time'), - $db->quoteName('c.created_user_id'), - $db->quoteName('c.description'), - $db->quoteName('c.extension'), - $db->quoteName('c.hits'), - $db->quoteName('c.language'), - $db->quoteName('c.level'), - $db->quoteName('c.lft'), - $db->quoteName('c.metadata'), - $db->quoteName('c.metadesc'), - $db->quoteName('c.metakey'), - $db->quoteName('c.modified_time'), - $db->quoteName('c.note'), - $db->quoteName('c.params'), - $db->quoteName('c.parent_id'), - $db->quoteName('c.path'), - $db->quoteName('c.published'), - $db->quoteName('c.rgt'), - $db->quoteName('c.title'), - $db->quoteName('c.modified_user_id'), - $db->quoteName('c.version'), - ] - ); - - $case_when = ' CASE WHEN '; - $case_when .= $query->charLength($db->quoteName('c.alias'), '!=', '0'); - $case_when .= ' THEN '; - $c_id = $query->castAsChar($db->quoteName('c.id')); - $case_when .= $query->concatenate(array($c_id, $db->quoteName('c.alias')), ':'); - $case_when .= ' ELSE '; - $case_when .= $c_id . ' END as ' . $db->quoteName('slug'); - - $query->select($case_when) - ->where('(' . $db->quoteName('c.extension') . ' = :extension OR ' . $db->quoteName('c.extension') . ' = ' . $db->quote('system') . ')') - ->bind(':extension', $extension); - - if ($this->_options['access']) - { - $groups = $user->getAuthorisedViewLevels(); - $query->whereIn($db->quoteName('c.access'), $groups); - } - - if ($this->_options['published'] == 1) - { - $query->where($db->quoteName('c.published') . ' = 1'); - } - - $query->order($db->quoteName('c.lft')); - - // Note: s for selected id - if ($id !== 'root') - { - // Get the selected category - $query->from($db->quoteName('#__categories', 's')) - ->where($db->quoteName('s.id') . ' = :id') - ->bind(':id', $id, ParameterType::INTEGER); - - if ($app->isClient('site') && Multilanguage::isEnabled()) - { - // For the most part, we use c.lft column, which index is properly used instead of c.rgt - $query->join( - 'INNER', - $db->quoteName('#__categories', 'c'), - '(' . $db->quoteName('s.lft') . ' < ' . $db->quoteName('c.lft') - . ' AND ' . $db->quoteName('c.lft') . ' < ' . $db->quoteName('s.rgt') - . ' AND ' . $db->quoteName('c.language') - . ' IN (' . implode(',', $query->bindArray([Factory::getLanguage()->getTag(), '*'], ParameterType::STRING)) . '))' - . ' OR (' . $db->quoteName('c.lft') . ' <= ' . $db->quoteName('s.lft') - . ' AND ' . $db->quoteName('s.rgt') . ' <= ' . $db->quoteName('c.rgt') . ')' - ); - } - else - { - $query->join( - 'INNER', - $db->quoteName('#__categories', 'c'), - '(' . $db->quoteName('s.lft') . ' <= ' . $db->quoteName('c.lft') - . ' AND ' . $db->quoteName('c.lft') . ' < ' . $db->quoteName('s.rgt') . ')' - . ' OR (' . $db->quoteName('c.lft') . ' < ' . $db->quoteName('s.lft') - . ' AND ' . $db->quoteName('s.rgt') . ' < ' . $db->quoteName('c.rgt') . ')' - ); - } - } - else - { - $query->from($db->quoteName('#__categories', 'c')); - - if ($app->isClient('site') && Multilanguage::isEnabled()) - { - $query->whereIn($db->quoteName('c.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); - } - } - - // Note: i for item - if ($this->_options['countItems'] == 1) - { - $subQuery = $db->getQuery(true) - ->select('COUNT(' . $db->quoteName($db->escape('i.' . $this->_key)) . ')') - ->from($db->quoteName($db->escape($this->_table), 'i')) - ->where($db->quoteName($db->escape('i.' . $this->_field)) . ' = ' . $db->quoteName('c.id')); - - if ($this->_options['published'] == 1) - { - $subQuery->where($db->quoteName($db->escape('i.' . $this->_statefield)) . ' = 1'); - } - - if ($this->_options['currentlang'] !== 0) - { - $subQuery->where( - $db->quoteName('i.language') - . ' IN (' . implode(',', $query->bindArray([$this->_options['currentlang'], '*'], ParameterType::STRING)) . ')' - ); - } - - $query->select('(' . $subQuery . ') AS ' . $db->quoteName('numitems')); - } - - // Get the results - $db->setQuery($query); - $results = $db->loadObjectList('id'); - $childrenLoaded = false; - - if (\count($results)) - { - // Foreach categories - foreach ($results as $result) - { - // Deal with root category - if ($result->id == 1) - { - $result->id = 'root'; - } - - // Deal with parent_id - if ($result->parent_id == 1) - { - $result->parent_id = 'root'; - } - - // Create the node - if (!isset($this->_nodes[$result->id])) - { - // Create the CategoryNode and add to _nodes - $this->_nodes[$result->id] = new CategoryNode($result, $this); - - // If this is not root and if the current node's parent is in the list or the current node parent is 0 - if ($result->id !== 'root' && (isset($this->_nodes[$result->parent_id]) || $result->parent_id == 1)) - { - // Compute relationship between node and its parent - set the parent in the _nodes field - $this->_nodes[$result->id]->setParent($this->_nodes[$result->parent_id]); - } - - // If the node's parent id is not in the _nodes list and the node is not root (doesn't have parent_id == 0), - // then remove the node from the list - if (!(isset($this->_nodes[$result->parent_id]) || $result->parent_id == 0)) - { - unset($this->_nodes[$result->id]); - continue; - } - - if ($result->id == $id || $childrenLoaded) - { - $this->_nodes[$result->id]->setAllLoaded(); - $childrenLoaded = true; - } - } - elseif ($result->id == $id || $childrenLoaded) - { - // Create the CategoryNode - $this->_nodes[$result->id] = new CategoryNode($result, $this); - - if ($result->id !== 'root' && (isset($this->_nodes[$result->parent_id]) || $result->parent_id)) - { - // Compute relationship between node and its parent - $this->_nodes[$result->id]->setParent($this->_nodes[$result->parent_id]); - } - - // If the node's parent id is not in the _nodes list and the node is not root (doesn't have parent_id == 0), - // then remove the node from the list - if (!(isset($this->_nodes[$result->parent_id]) || $result->parent_id == 0)) - { - unset($this->_nodes[$result->id]); - continue; - } - - if ($result->id == $id || $childrenLoaded) - { - $this->_nodes[$result->id]->setAllLoaded(); - $childrenLoaded = true; - } - } - } - } - else - { - $this->_nodes[$id] = null; - } - } + use DatabaseAwareTrait; + + /** + * Array to hold the object instances + * + * @var Categories[] + * @since 1.6 + */ + public static $instances = array(); + + /** + * Array of category nodes + * + * @var CategoryNode[] + * @since 1.6 + */ + protected $_nodes; + + /** + * Array of checked categories -- used to save values when _nodes are null + * + * @var boolean[] + * @since 1.6 + */ + protected $_checkedCategories; + + /** + * Name of the extension the categories belong to + * + * @var string + * @since 1.6 + */ + protected $_extension = null; + + /** + * Name of the linked content table to get category content count + * + * @var string + * @since 1.6 + */ + protected $_table = null; + + /** + * Name of the category field + * + * @var string + * @since 1.6 + */ + protected $_field = null; + + /** + * Name of the key field + * + * @var string + * @since 1.6 + */ + protected $_key = null; + + /** + * Name of the items state field + * + * @var string + * @since 1.6 + */ + protected $_statefield = null; + + /** + * Array of options + * + * @var array + * @since 1.6 + */ + protected $_options = []; + + /** + * Class constructor + * + * @param array $options Array of options + * + * @since 1.6 + */ + public function __construct($options) + { + $this->_extension = $options['extension']; + $this->_table = $options['table']; + $this->_field = isset($options['field']) && $options['field'] ? $options['field'] : 'catid'; + $this->_key = isset($options['key']) && $options['key'] ? $options['key'] : 'id'; + $this->_statefield = isset($options['statefield']) ? $options['statefield'] : 'state'; + + $options['access'] = isset($options['access']) ? $options['access'] : 'true'; + $options['published'] = isset($options['published']) ? $options['published'] : 1; + $options['countItems'] = isset($options['countItems']) ? $options['countItems'] : 0; + $options['currentlang'] = Multilanguage::isEnabled() ? Factory::getLanguage()->getTag() : 0; + + $this->_options = $options; + } + + /** + * Returns a reference to a Categories object + * + * @param string $extension Name of the categories extension + * @param array $options An array of options + * + * @return Categories|boolean Categories object on success, boolean false if an object does not exist + * + * @since 1.6 + * @deprecated 5.0 Use the ComponentInterface to get the categories + */ + public static function getInstance($extension, $options = array()) + { + $hash = md5(strtolower($extension) . serialize($options)); + + if (isset(self::$instances[$hash])) { + return self::$instances[$hash]; + } + + $categories = null; + + try { + $parts = explode('.', $extension, 2); + + $component = Factory::getApplication()->bootComponent($parts[0]); + + if ($component instanceof CategoryServiceInterface) { + $categories = $component->getCategory($options, \count($parts) > 1 ? $parts[1] : ''); + } + } catch (SectionNotFoundException $e) { + $categories = null; + } + + self::$instances[$hash] = $categories; + + return self::$instances[$hash]; + } + + /** + * Loads a specific category and all its children in a CategoryNode object. + * + * @param mixed $id an optional id integer or equal to 'root' + * @param boolean $forceload True to force the _load method to execute + * + * @return CategoryNode|null CategoryNode object or null if $id is not valid + * + * @since 1.6 + */ + public function get($id = 'root', $forceload = false) + { + if ($id !== 'root') { + $id = (int) $id; + + if ($id == 0) { + $id = 'root'; + } + } + + // If this $id has not been processed yet, execute the _load method + if ((!isset($this->_nodes[$id]) && !isset($this->_checkedCategories[$id])) || $forceload) { + $this->_load($id); + } + + // If we already have a value in _nodes for this $id, then use it. + if (isset($this->_nodes[$id])) { + return $this->_nodes[$id]; + } + + return null; + } + + /** + * Returns the extension of the category. + * + * @return string The extension + * + * @since 3.9.0 + */ + public function getExtension() + { + return $this->_extension; + } + + /** + * Load method + * + * @param integer $id Id of category to load + * + * @return void + * + * @since 1.6 + */ + protected function _load($id) + { + try { + $db = $this->getDatabase(); + } catch (DatabaseNotFoundException $e) { + @trigger_error(sprintf('Database must be set, this will not be caught anymore in 5.0.'), E_USER_DEPRECATED); + $db = Factory::getContainer()->get(DatabaseInterface::class); + } + + $app = Factory::getApplication(); + $user = Factory::getUser(); + $extension = $this->_extension; + + if ($id !== 'root') { + $id = (int) $id; + + if ($id === 0) { + $id = 'root'; + } + } + + // Record that has this $id has been checked + $this->_checkedCategories[$id] = true; + + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('c.id'), + $db->quoteName('c.asset_id'), + $db->quoteName('c.access'), + $db->quoteName('c.alias'), + $db->quoteName('c.checked_out'), + $db->quoteName('c.checked_out_time'), + $db->quoteName('c.created_time'), + $db->quoteName('c.created_user_id'), + $db->quoteName('c.description'), + $db->quoteName('c.extension'), + $db->quoteName('c.hits'), + $db->quoteName('c.language'), + $db->quoteName('c.level'), + $db->quoteName('c.lft'), + $db->quoteName('c.metadata'), + $db->quoteName('c.metadesc'), + $db->quoteName('c.metakey'), + $db->quoteName('c.modified_time'), + $db->quoteName('c.note'), + $db->quoteName('c.params'), + $db->quoteName('c.parent_id'), + $db->quoteName('c.path'), + $db->quoteName('c.published'), + $db->quoteName('c.rgt'), + $db->quoteName('c.title'), + $db->quoteName('c.modified_user_id'), + $db->quoteName('c.version'), + ] + ); + + $case_when = ' CASE WHEN '; + $case_when .= $query->charLength($db->quoteName('c.alias'), '!=', '0'); + $case_when .= ' THEN '; + $c_id = $query->castAsChar($db->quoteName('c.id')); + $case_when .= $query->concatenate(array($c_id, $db->quoteName('c.alias')), ':'); + $case_when .= ' ELSE '; + $case_when .= $c_id . ' END as ' . $db->quoteName('slug'); + + $query->select($case_when) + ->where('(' . $db->quoteName('c.extension') . ' = :extension OR ' . $db->quoteName('c.extension') . ' = ' . $db->quote('system') . ')') + ->bind(':extension', $extension); + + if ($this->_options['access']) { + $groups = $user->getAuthorisedViewLevels(); + $query->whereIn($db->quoteName('c.access'), $groups); + } + + if ($this->_options['published'] == 1) { + $query->where($db->quoteName('c.published') . ' = 1'); + } + + $query->order($db->quoteName('c.lft')); + + // Note: s for selected id + if ($id !== 'root') { + // Get the selected category + $query->from($db->quoteName('#__categories', 's')) + ->where($db->quoteName('s.id') . ' = :id') + ->bind(':id', $id, ParameterType::INTEGER); + + if ($app->isClient('site') && Multilanguage::isEnabled()) { + // For the most part, we use c.lft column, which index is properly used instead of c.rgt + $query->join( + 'INNER', + $db->quoteName('#__categories', 'c'), + '(' . $db->quoteName('s.lft') . ' < ' . $db->quoteName('c.lft') + . ' AND ' . $db->quoteName('c.lft') . ' < ' . $db->quoteName('s.rgt') + . ' AND ' . $db->quoteName('c.language') + . ' IN (' . implode(',', $query->bindArray([Factory::getLanguage()->getTag(), '*'], ParameterType::STRING)) . '))' + . ' OR (' . $db->quoteName('c.lft') . ' <= ' . $db->quoteName('s.lft') + . ' AND ' . $db->quoteName('s.rgt') . ' <= ' . $db->quoteName('c.rgt') . ')' + ); + } else { + $query->join( + 'INNER', + $db->quoteName('#__categories', 'c'), + '(' . $db->quoteName('s.lft') . ' <= ' . $db->quoteName('c.lft') + . ' AND ' . $db->quoteName('c.lft') . ' < ' . $db->quoteName('s.rgt') . ')' + . ' OR (' . $db->quoteName('c.lft') . ' < ' . $db->quoteName('s.lft') + . ' AND ' . $db->quoteName('s.rgt') . ' < ' . $db->quoteName('c.rgt') . ')' + ); + } + } else { + $query->from($db->quoteName('#__categories', 'c')); + + if ($app->isClient('site') && Multilanguage::isEnabled()) { + $query->whereIn($db->quoteName('c.language'), [Factory::getLanguage()->getTag(), '*'], ParameterType::STRING); + } + } + + // Note: i for item + if ($this->_options['countItems'] == 1) { + $subQuery = $db->getQuery(true) + ->select('COUNT(' . $db->quoteName($db->escape('i.' . $this->_key)) . ')') + ->from($db->quoteName($db->escape($this->_table), 'i')) + ->where($db->quoteName($db->escape('i.' . $this->_field)) . ' = ' . $db->quoteName('c.id')); + + if ($this->_options['published'] == 1) { + $subQuery->where($db->quoteName($db->escape('i.' . $this->_statefield)) . ' = 1'); + } + + if ($this->_options['currentlang'] !== 0) { + $subQuery->where( + $db->quoteName('i.language') + . ' IN (' . implode(',', $query->bindArray([$this->_options['currentlang'], '*'], ParameterType::STRING)) . ')' + ); + } + + $query->select('(' . $subQuery . ') AS ' . $db->quoteName('numitems')); + } + + // Get the results + $db->setQuery($query); + $results = $db->loadObjectList('id'); + $childrenLoaded = false; + + if (\count($results)) { + // Foreach categories + foreach ($results as $result) { + // Deal with root category + if ($result->id == 1) { + $result->id = 'root'; + } + + // Deal with parent_id + if ($result->parent_id == 1) { + $result->parent_id = 'root'; + } + + // Create the node + if (!isset($this->_nodes[$result->id])) { + // Create the CategoryNode and add to _nodes + $this->_nodes[$result->id] = new CategoryNode($result, $this); + + // If this is not root and if the current node's parent is in the list or the current node parent is 0 + if ($result->id !== 'root' && (isset($this->_nodes[$result->parent_id]) || $result->parent_id == 1)) { + // Compute relationship between node and its parent - set the parent in the _nodes field + $this->_nodes[$result->id]->setParent($this->_nodes[$result->parent_id]); + } + + // If the node's parent id is not in the _nodes list and the node is not root (doesn't have parent_id == 0), + // then remove the node from the list + if (!(isset($this->_nodes[$result->parent_id]) || $result->parent_id == 0)) { + unset($this->_nodes[$result->id]); + continue; + } + + if ($result->id == $id || $childrenLoaded) { + $this->_nodes[$result->id]->setAllLoaded(); + $childrenLoaded = true; + } + } elseif ($result->id == $id || $childrenLoaded) { + // Create the CategoryNode + $this->_nodes[$result->id] = new CategoryNode($result, $this); + + if ($result->id !== 'root' && (isset($this->_nodes[$result->parent_id]) || $result->parent_id)) { + // Compute relationship between node and its parent + $this->_nodes[$result->id]->setParent($this->_nodes[$result->parent_id]); + } + + // If the node's parent id is not in the _nodes list and the node is not root (doesn't have parent_id == 0), + // then remove the node from the list + if (!(isset($this->_nodes[$result->parent_id]) || $result->parent_id == 0)) { + unset($this->_nodes[$result->id]); + continue; + } + + if ($result->id == $id || $childrenLoaded) { + $this->_nodes[$result->id]->setAllLoaded(); + $childrenLoaded = true; + } + } + } + } else { + $this->_nodes[$id] = null; + } + } } diff --git a/libraries/src/Categories/CategoryFactory.php b/libraries/src/Categories/CategoryFactory.php index eb6228184acc1..3e5811e6b8e3a 100644 --- a/libraries/src/Categories/CategoryFactory.php +++ b/libraries/src/Categories/CategoryFactory.php @@ -1,4 +1,5 @@ namespace = $namespace; - } + /** + * The namespace must be like: + * Joomla\Component\Content + * + * @param string $namespace The namespace + * + * @since 4.0.0 + */ + public function __construct($namespace) + { + $this->namespace = $namespace; + } - /** - * Creates a category. - * - * @param array $options The options - * @param string $section The section - * - * @return CategoryInterface - * - * @since 3.10.0 - * - * @throws SectionNotFoundException - */ - public function createCategory(array $options = [], string $section = ''): CategoryInterface - { - $className = trim($this->namespace, '\\') . '\\Site\\Service\\' . ucfirst($section) . 'Category'; + /** + * Creates a category. + * + * @param array $options The options + * @param string $section The section + * + * @return CategoryInterface + * + * @since 3.10.0 + * + * @throws SectionNotFoundException + */ + public function createCategory(array $options = [], string $section = ''): CategoryInterface + { + $className = trim($this->namespace, '\\') . '\\Site\\Service\\' . ucfirst($section) . 'Category'; - if (!class_exists($className)) - { - throw new SectionNotFoundException; - } + if (!class_exists($className)) { + throw new SectionNotFoundException(); + } - $category = new $className($options); + $category = new $className($options); - if ($category instanceof DatabaseAwareInterface) - { - $category->setDatabase($this->getDatabase()); - } + if ($category instanceof DatabaseAwareInterface) { + $category->setDatabase($this->getDatabase()); + } - return $category; - } + return $category; + } } diff --git a/libraries/src/Categories/CategoryFactoryInterface.php b/libraries/src/Categories/CategoryFactoryInterface.php index c3b0dca0bf466..627a241900baf 100644 --- a/libraries/src/Categories/CategoryFactoryInterface.php +++ b/libraries/src/Categories/CategoryFactoryInterface.php @@ -1,4 +1,5 @@ setProperties($category); - - if ($constructor) - { - $this->_constructor = $constructor; - } - - return true; - } - - return false; - } - - /** - * Set the parent of this category - * - * If the category already has a parent, the link is unset - * - * @param CategoryNode|null $parent CategoryNode for the parent to be set or null - * - * @return void - * - * @since 1.6 - */ - public function setParent(NodeInterface $parent) - { - if (!\is_null($this->_parent)) - { - $key = array_search($this, $this->_parent->_children); - unset($this->_parent->_children[$key]); - } - - $this->_parent = $parent; - - $this->_parent->_children[] = & $this; - - if (\count($this->_parent->_children) > 1) - { - end($this->_parent->_children); - $this->_leftSibling = prev($this->_parent->_children); - $this->_leftSibling->_rightsibling = & $this; - } - - if ($this->parent_id != 1) - { - $this->_path = $parent->getPath(); - } - - $this->_path[$this->id] = $this->id . ':' . $this->alias; - } - - /** - * Get the children of this node - * - * @param boolean $recursive False by default - * - * @return CategoryNode[] The children - * - * @since 1.6 - */ - public function &getChildren($recursive = false) - { - if (!$this->_allChildrenloaded) - { - $temp = $this->_constructor->get($this->id, true); - - if ($temp) - { - $this->_children = $temp->getChildren(); - $this->_leftSibling = $temp->getSibling(false); - $this->_rightSibling = $temp->getSibling(true); - $this->setAllLoaded(); - } - } - - if ($recursive) - { - $items = array(); - - foreach ($this->_children as $child) - { - $items[] = $child; - $items = array_merge($items, $child->getChildren(true)); - } - - return $items; - } - - return $this->_children; - } - - /** - * Returns the right or left sibling of a category - * - * @param boolean $right If set to false, returns the left sibling - * - * @return CategoryNode|null CategoryNode object with the sibling information or null if there is no sibling on that side. - * - * @since 1.6 - */ - public function getSibling($right = true) - { - if (!$this->_allChildrenloaded) - { - $temp = $this->_constructor->get($this->id, true); - $this->_children = $temp->getChildren(); - $this->_leftSibling = $temp->getSibling(false); - $this->_rightSibling = $temp->getSibling(true); - $this->setAllLoaded(); - } - - if ($right) - { - return $this->_rightSibling; - } - else - { - return $this->_leftSibling; - } - } - - /** - * Returns the category parameters - * - * @return Registry - * - * @since 1.6 - */ - public function getParams() - { - if (!($this->params instanceof Registry)) - { - $this->params = new Registry($this->params); - } - - return $this->params; - } - - /** - * Returns the category metadata - * - * @return Registry A Registry object containing the metadata - * - * @since 1.6 - */ - public function getMetadata() - { - if (!($this->metadata instanceof Registry)) - { - $this->metadata = new Registry($this->metadata); - } - - return $this->metadata; - } - - /** - * Returns the category path to the root category - * - * @return array - * - * @since 1.6 - */ - public function getPath() - { - return $this->_path; - } - - /** - * Returns the user that created the category - * - * @param boolean $modifiedUser Returns the modified_user when set to true - * - * @return \Joomla\CMS\User\User A User object containing a userid - * - * @since 1.6 - */ - public function getAuthor($modifiedUser = false) - { - if ($modifiedUser) - { - return Factory::getUser($this->modified_user_id); - } - - return Factory::getUser($this->created_user_id); - } - - /** - * Set to load all children - * - * @return void - * - * @since 1.6 - */ - public function setAllLoaded() - { - $this->_allChildrenloaded = true; - - foreach ($this->_children as $child) - { - $child->setAllLoaded(); - } - } - - /** - * Returns the number of items. - * - * @param boolean $recursive If false number of children, if true number of descendants - * - * @return integer Number of children or descendants - * - * @since 1.6 - */ - public function getNumItems($recursive = false) - { - if ($recursive) - { - $count = $this->numitems; - - foreach ($this->getChildren() as $child) - { - $count = $count + $child->getNumItems(true); - } - - return $count; - } - - return $this->numitems; - } + use NodeTrait; + + /** + * Primary key + * + * @var integer + * @since 1.6 + */ + public $id = null; + + /** + * The id of the category in the asset table + * + * @var integer + * @since 1.6 + */ + public $asset_id = null; + + /** + * The id of the parent of category in the asset table, 0 for category root + * + * @var integer + * @since 1.6 + */ + public $parent_id = null; + + /** + * The lft value for this category in the category tree + * + * @var integer + * @since 1.6 + */ + public $lft = null; + + /** + * The rgt value for this category in the category tree + * + * @var integer + * @since 1.6 + */ + public $rgt = null; + + /** + * The depth of this category's position in the category tree + * + * @var integer + * @since 1.6 + */ + public $level = null; + + /** + * The extension this category is associated with + * + * @var integer + * @since 1.6 + */ + public $extension = null; + + /** + * The menu title for the category (a short name) + * + * @var string + * @since 1.6 + */ + public $title = null; + + /** + * The the alias for the category + * + * @var string + * @since 1.6 + */ + public $alias = null; + + /** + * Description of the category. + * + * @var string + * @since 1.6 + */ + public $description = null; + + /** + * The publication status of the category + * + * @var boolean + * @since 1.6 + */ + public $published = null; + + /** + * Whether the category is or is not checked out + * + * @var boolean + * @since 1.6 + */ + public $checked_out = null; + + /** + * The time at which the category was checked out + * + * @var string + * @since 1.6 + */ + public $checked_out_time = null; + + /** + * Access level for the category + * + * @var integer + * @since 1.6 + */ + public $access = null; + + /** + * JSON string of parameters + * + * @var string + * @since 1.6 + */ + public $params = null; + + /** + * Metadata description + * + * @var string + * @since 1.6 + */ + public $metadesc = null; + + /** + * Key words for metadata + * + * @var string + * @since 1.6 + */ + public $metakey = null; + + /** + * JSON string of other metadata + * + * @var string + * @since 1.6 + */ + public $metadata = null; + + /** + * The ID of the user who created the category + * + * @var integer + * @since 1.6 + */ + public $created_user_id = null; + + /** + * The time at which the category was created + * + * @var string + * @since 1.6 + */ + public $created_time = null; + + /** + * The ID of the user who last modified the category + * + * @var integer + * @since 1.6 + */ + public $modified_user_id = null; + + /** + * The time at which the category was modified + * + * @var string + * @since 1.6 + */ + public $modified_time = null; + + /** + * Number of times the category has been viewed + * + * @var integer + * @since 1.6 + */ + public $hits = null; + + /** + * The language for the category in xx-XX format + * + * @var string + * @since 1.6 + */ + public $language = null; + + /** + * Number of items in this category or descendants of this category + * + * @var integer + * @since 1.6 + */ + public $numitems = null; + + /** + * Number of children items + * + * @var integer + * @since 1.6 + */ + public $childrennumitems = null; + + /** + * Slug for the category (used in URL) + * + * @var string + * @since 1.6 + */ + public $slug = null; + + /** + * Array of assets + * + * @var array + * @since 1.6 + */ + public $assets = null; + + /** + * Path from root to this category + * + * @var array + * @since 1.6 + */ + protected $_path = array(); + + /** + * Flag if all children have been loaded + * + * @var boolean + * @since 1.6 + */ + protected $_allChildrenloaded = false; + + /** + * Constructor of this tree + * + * @var Categories + * @since 1.6 + */ + protected $_constructor = null; + + /** + * Class constructor + * + * @param array $category The category data. + * @param Categories $constructor The tree constructor. + * + * @since 1.6 + */ + public function __construct($category = null, $constructor = null) + { + if ($category) { + $this->setProperties($category); + + if ($constructor) { + $this->_constructor = $constructor; + } + + return true; + } + + return false; + } + + /** + * Set the parent of this category + * + * If the category already has a parent, the link is unset + * + * @param CategoryNode|null $parent CategoryNode for the parent to be set or null + * + * @return void + * + * @since 1.6 + */ + public function setParent(NodeInterface $parent) + { + if (!\is_null($this->_parent)) { + $key = array_search($this, $this->_parent->_children); + unset($this->_parent->_children[$key]); + } + + $this->_parent = $parent; + + $this->_parent->_children[] = & $this; + + if (\count($this->_parent->_children) > 1) { + end($this->_parent->_children); + $this->_leftSibling = prev($this->_parent->_children); + $this->_leftSibling->_rightsibling = & $this; + } + + if ($this->parent_id != 1) { + $this->_path = $parent->getPath(); + } + + $this->_path[$this->id] = $this->id . ':' . $this->alias; + } + + /** + * Get the children of this node + * + * @param boolean $recursive False by default + * + * @return CategoryNode[] The children + * + * @since 1.6 + */ + public function &getChildren($recursive = false) + { + if (!$this->_allChildrenloaded) { + $temp = $this->_constructor->get($this->id, true); + + if ($temp) { + $this->_children = $temp->getChildren(); + $this->_leftSibling = $temp->getSibling(false); + $this->_rightSibling = $temp->getSibling(true); + $this->setAllLoaded(); + } + } + + if ($recursive) { + $items = array(); + + foreach ($this->_children as $child) { + $items[] = $child; + $items = array_merge($items, $child->getChildren(true)); + } + + return $items; + } + + return $this->_children; + } + + /** + * Returns the right or left sibling of a category + * + * @param boolean $right If set to false, returns the left sibling + * + * @return CategoryNode|null CategoryNode object with the sibling information or null if there is no sibling on that side. + * + * @since 1.6 + */ + public function getSibling($right = true) + { + if (!$this->_allChildrenloaded) { + $temp = $this->_constructor->get($this->id, true); + $this->_children = $temp->getChildren(); + $this->_leftSibling = $temp->getSibling(false); + $this->_rightSibling = $temp->getSibling(true); + $this->setAllLoaded(); + } + + if ($right) { + return $this->_rightSibling; + } else { + return $this->_leftSibling; + } + } + + /** + * Returns the category parameters + * + * @return Registry + * + * @since 1.6 + */ + public function getParams() + { + if (!($this->params instanceof Registry)) { + $this->params = new Registry($this->params); + } + + return $this->params; + } + + /** + * Returns the category metadata + * + * @return Registry A Registry object containing the metadata + * + * @since 1.6 + */ + public function getMetadata() + { + if (!($this->metadata instanceof Registry)) { + $this->metadata = new Registry($this->metadata); + } + + return $this->metadata; + } + + /** + * Returns the category path to the root category + * + * @return array + * + * @since 1.6 + */ + public function getPath() + { + return $this->_path; + } + + /** + * Returns the user that created the category + * + * @param boolean $modifiedUser Returns the modified_user when set to true + * + * @return \Joomla\CMS\User\User A User object containing a userid + * + * @since 1.6 + */ + public function getAuthor($modifiedUser = false) + { + if ($modifiedUser) { + return Factory::getUser($this->modified_user_id); + } + + return Factory::getUser($this->created_user_id); + } + + /** + * Set to load all children + * + * @return void + * + * @since 1.6 + */ + public function setAllLoaded() + { + $this->_allChildrenloaded = true; + + foreach ($this->_children as $child) { + $child->setAllLoaded(); + } + } + + /** + * Returns the number of items. + * + * @param boolean $recursive If false number of children, if true number of descendants + * + * @return integer Number of children or descendants + * + * @since 1.6 + */ + public function getNumItems($recursive = false) + { + if ($recursive) { + $count = $this->numitems; + + foreach ($this->getChildren() as $child) { + $count = $count + $child->getNumItems(true); + } + + return $count; + } + + return $this->numitems; + } } diff --git a/libraries/src/Categories/CategoryServiceInterface.php b/libraries/src/Categories/CategoryServiceInterface.php index 29b313d19b4f6..585d5de23fd76 100644 --- a/libraries/src/Categories/CategoryServiceInterface.php +++ b/libraries/src/Categories/CategoryServiceInterface.php @@ -1,4 +1,5 @@ categoryFactory->createCategory($options, $section); - } + /** + * Returns the category service. + * + * @param array $options The options + * @param string $section The section + * + * @return CategoryInterface + * + * @since 4.0.0 + * @throws SectionNotFoundException + */ + public function getCategory(array $options = [], $section = ''): CategoryInterface + { + return $this->categoryFactory->createCategory($options, $section); + } - /** - * Sets the internal category factory. - * - * @param CategoryFactoryInterface $categoryFactory The categories factory - * - * @return void - * - * @since 4.0.0 - */ - public function setCategoryFactory(CategoryFactoryInterface $categoryFactory) - { - $this->categoryFactory = $categoryFactory; - } + /** + * Sets the internal category factory. + * + * @param CategoryFactoryInterface $categoryFactory The categories factory + * + * @return void + * + * @since 4.0.0 + */ + public function setCategoryFactory(CategoryFactoryInterface $categoryFactory) + { + $this->categoryFactory = $categoryFactory; + } - /** - * Adds Count Items for Category Manager. - * - * @param \stdClass[] $items The category objects - * @param string $section The section - * - * @return void - * - * @since 4.0.0 - * @throws \Exception - */ - public function countItems(array $items, string $section) - { - $config = (object) array( - 'related_tbl' => $this->getTableNameForSection($section), - 'state_col' => $this->getStateColumnForSection($section), - 'group_col' => 'catid', - 'relation_type' => 'category_or_group', - ); + /** + * Adds Count Items for Category Manager. + * + * @param \stdClass[] $items The category objects + * @param string $section The section + * + * @return void + * + * @since 4.0.0 + * @throws \Exception + */ + public function countItems(array $items, string $section) + { + $config = (object) array( + 'related_tbl' => $this->getTableNameForSection($section), + 'state_col' => $this->getStateColumnForSection($section), + 'group_col' => 'catid', + 'relation_type' => 'category_or_group', + ); - ContentHelper::countRelations($items, $config); - } + ContentHelper::countRelations($items, $config); + } - /** - * Prepares the category form - * - * @param Form $form The form to change - * @param array|object $data The form data - * - * @return void - */ - public function prepareForm(Form $form, $data) - { - } + /** + * Prepares the category form + * + * @param Form $form The form to change + * @param array|object $data The form data + * + * @return void + */ + public function prepareForm(Form $form, $data) + { + } - /** - * Returns the table for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getTableNameForSection(string $section = null) - { - return null; - } + /** + * Returns the table for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getTableNameForSection(string $section = null) + { + return null; + } - /** - * Returns the state column for the count items functions for the given section. - * - * @param string $section The section - * - * @return string|null - * - * @since 4.0.0 - */ - protected function getStateColumnForSection(string $section = null) - { - return 'state'; - } + /** + * Returns the state column for the count items functions for the given section. + * + * @param string $section The section + * + * @return string|null + * + * @since 4.0.0 + */ + protected function getStateColumnForSection(string $section = null) + { + return 'state'; + } } diff --git a/libraries/src/Categories/SectionNotFoundException.php b/libraries/src/Categories/SectionNotFoundException.php index 439101771d9b6..3dd2635ce8768 100644 --- a/libraries/src/Categories/SectionNotFoundException.php +++ b/libraries/src/Categories/SectionNotFoundException.php @@ -1,4 +1,5 @@ ` element - * - * @var string - * @since 4.0.0 - */ - protected $element; - - /** - * Update manifest `` element - * - * @var string - * @since 4.0.0 - */ - protected $type; - - /** - * Update manifest `` element - * - * @var string - * @since 4.0.0 - */ - protected $version; - - /** - * Update manifest `` element - * - * @var array - * @since 4.0.0 - */ - protected $security = array(); - - /** - * Update manifest `` element - * - * @var array - * @since 4.0.0 - */ - protected $fix = array(); - - /** - * Update manifest `` element - * - * @var array - * @since 4.0.0 - */ - protected $language = array(); - - /** - * Update manifest `` element - * - * @var array - * @since 4.0.0 - */ - protected $addition = array(); - - /** - * Update manifest `` elements - * - * @var array - * @since 4.0.0 - */ - protected $change = array(); - - /** - * Update manifest `` element - * - * @var array - * @since 4.0.0 - */ - protected $remove = array(); - - /** - * Update manifest `` element - * - * @var array - * @since 4.0.0 - */ - protected $note = array(); - - /** - * List of node items - * - * @var array - * @since 4.0.0 - */ - private $items = array(); - - /** - * Resource handle for the XML Parser - * - * @var resource - * @since 4.0.0 - */ - protected $xmlParser; - - /** - * Element call stack - * - * @var array - * @since 4.0.0 - */ - protected $stack = array('base'); - - /** - * Object containing the current update data - * - * @var \stdClass - * @since 4.0.0 - */ - protected $currentChangelog; - - /** - * The version to match the changelog - * - * @var string - * @since 4.0.0 - */ - private $matchVersion = ''; - - /** - * Object containing the latest changelog data - * - * @var \stdClass - * @since 4.0.0 - */ - protected $latest; - - /** - * Gets the reference to the current direct parent - * - * @return string - * - * @since 4.0.0 - */ - protected function getStackLocation() - { - return implode('->', $this->stack); - } - - /** - * Get the last position in stack count - * - * @return string - * - * @since 4.0.0 - */ - protected function getLastTag() - { - return $this->stack[\count($this->stack) - 1]; - } - - /** - * Set the version to match. - * - * @param string $version The version to match - * - * @return void - * - * @since 4.0.0 - */ - public function setVersion(string $version) - { - $this->matchVersion = $version; - } - - /** - * XML Start Element callback - * - * @param object $parser Parser object - * @param string $name Name of the tag found - * @param array $attrs Attributes of the tag - * - * @return void - * - * @note This is public because it is called externally - * @since 1.7.0 - */ - public function startElement($parser, $name, $attrs = array()) - { - $this->stack[] = $name; - $tag = $this->getStackLocation(); - - // Reset the data - if (isset($this->$tag)) - { - $this->$tag->data = ''; - } - - $name = strtolower($name); - - if (!isset($this->currentChangelog->$name)) - { - $this->currentChangelog->$name = new \stdClass; - } - - $this->currentChangelog->$name->data = ''; - - foreach ($attrs as $key => $data) - { - $key = strtolower($key); - $this->currentChangelog->$name->$key = $data; - } - } - - /** - * Callback for closing the element - * - * @param object $parser Parser object - * @param string $name Name of element that was closed - * - * @return void - * - * @note This is public because it is called externally - * @since 1.7.0 - */ - public function endElement($parser, $name) - { - array_pop($this->stack); - - switch ($name) - { - case 'SECURITY': - case 'FIX': - case 'LANGUAGE': - case 'ADDITION': - case 'CHANGE': - case 'REMOVE': - case 'NOTE': - $name = strtolower($name); - $this->currentChangelog->$name->data = $this->items; - $this->items = array(); - break; - case 'CHANGELOG': - if (version_compare($this->currentChangelog->version->data, $this->matchVersion, '==') === true) - { - $this->latest = $this->currentChangelog; - } - - // No version match, empty it - $this->currentChangelog = new \stdClass; - break; - case 'CHANGELOGS': - // If the latest item is set then we transfer it to where we want to - if (isset($this->latest)) - { - foreach (get_object_vars($this->latest) as $key => $val) - { - $this->$key = $val; - } - - unset($this->latest); - unset($this->currentChangelog); - } - elseif (isset($this->currentChangelog)) - { - // The update might be for an older version of j! - unset($this->currentChangelog); - } - break; - } - } - - /** - * Character Parser Function - * - * @param object $parser Parser object. - * @param object $data The data. - * - * @return void - * - * @note This is public because its called externally. - * @since 1.7.0 - */ - public function characterData($parser, $data) - { - $tag = $this->getLastTag(); - - switch ($tag) - { - case 'ITEM': - $this->items[] = $data; - break; - case 'SECURITY': - case 'FIX': - case 'LANGUAGE': - case 'ADDITION': - case 'CHANGE': - case 'REMOVE': - case 'NOTE': - break; - default: - // Throw the data for this item together - $tag = strtolower($tag); - - if (isset($this->currentChangelog->$tag)) - { - $this->currentChangelog->$tag->data .= $data; - } - break; - } - } - - /** - * Loads an XML file from a URL. - * - * @param string $url The URL. - * - * @return boolean True on success - * - * @since 4.0.0 - */ - public function loadFromXml($url) - { - $version = new Version; - $httpOption = new Registry; - $httpOption->set('userAgent', $version->getUserAgent('Joomla', true, false)); - - try - { - $http = HttpFactory::getHttp($httpOption); - $response = $http->get($url); - } - catch (RuntimeException $e) - { - $response = null; - } - - if ($response === null || $response->code !== 200) - { - // @todo: Add a 'mark bad' setting here somehow - Log::add(Text::sprintf('JLIB_UPDATER_ERROR_EXTENSION_OPEN_URL', $url), Log::WARNING, 'jerror'); - - return false; - } - - $this->currentChangelog = new \stdClass; - - $this->xmlParser = xml_parser_create(''); - xml_set_object($this->xmlParser, $this); - xml_set_element_handler($this->xmlParser, 'startElement', 'endElement'); - xml_set_character_data_handler($this->xmlParser, 'characterData'); - - if (!xml_parse($this->xmlParser, $response->body)) - { - Log::add( - sprintf( - 'XML error: %s at line %d', xml_error_string(xml_get_error_code($this->xmlParser)), - xml_get_current_line_number($this->xmlParser) - ), - Log::WARNING, 'updater' - ); - - return false; - } - - xml_parser_free($this->xmlParser); - - return true; - } + /** + * Update manifest `` element + * + * @var string + * @since 4.0.0 + */ + protected $element; + + /** + * Update manifest `` element + * + * @var string + * @since 4.0.0 + */ + protected $type; + + /** + * Update manifest `` element + * + * @var string + * @since 4.0.0 + */ + protected $version; + + /** + * Update manifest `` element + * + * @var array + * @since 4.0.0 + */ + protected $security = array(); + + /** + * Update manifest `` element + * + * @var array + * @since 4.0.0 + */ + protected $fix = array(); + + /** + * Update manifest `` element + * + * @var array + * @since 4.0.0 + */ + protected $language = array(); + + /** + * Update manifest `` element + * + * @var array + * @since 4.0.0 + */ + protected $addition = array(); + + /** + * Update manifest `` elements + * + * @var array + * @since 4.0.0 + */ + protected $change = array(); + + /** + * Update manifest `` element + * + * @var array + * @since 4.0.0 + */ + protected $remove = array(); + + /** + * Update manifest `` element + * + * @var array + * @since 4.0.0 + */ + protected $note = array(); + + /** + * List of node items + * + * @var array + * @since 4.0.0 + */ + private $items = array(); + + /** + * Resource handle for the XML Parser + * + * @var resource + * @since 4.0.0 + */ + protected $xmlParser; + + /** + * Element call stack + * + * @var array + * @since 4.0.0 + */ + protected $stack = array('base'); + + /** + * Object containing the current update data + * + * @var \stdClass + * @since 4.0.0 + */ + protected $currentChangelog; + + /** + * The version to match the changelog + * + * @var string + * @since 4.0.0 + */ + private $matchVersion = ''; + + /** + * Object containing the latest changelog data + * + * @var \stdClass + * @since 4.0.0 + */ + protected $latest; + + /** + * Gets the reference to the current direct parent + * + * @return string + * + * @since 4.0.0 + */ + protected function getStackLocation() + { + return implode('->', $this->stack); + } + + /** + * Get the last position in stack count + * + * @return string + * + * @since 4.0.0 + */ + protected function getLastTag() + { + return $this->stack[\count($this->stack) - 1]; + } + + /** + * Set the version to match. + * + * @param string $version The version to match + * + * @return void + * + * @since 4.0.0 + */ + public function setVersion(string $version) + { + $this->matchVersion = $version; + } + + /** + * XML Start Element callback + * + * @param object $parser Parser object + * @param string $name Name of the tag found + * @param array $attrs Attributes of the tag + * + * @return void + * + * @note This is public because it is called externally + * @since 1.7.0 + */ + public function startElement($parser, $name, $attrs = array()) + { + $this->stack[] = $name; + $tag = $this->getStackLocation(); + + // Reset the data + if (isset($this->$tag)) { + $this->$tag->data = ''; + } + + $name = strtolower($name); + + if (!isset($this->currentChangelog->$name)) { + $this->currentChangelog->$name = new \stdClass(); + } + + $this->currentChangelog->$name->data = ''; + + foreach ($attrs as $key => $data) { + $key = strtolower($key); + $this->currentChangelog->$name->$key = $data; + } + } + + /** + * Callback for closing the element + * + * @param object $parser Parser object + * @param string $name Name of element that was closed + * + * @return void + * + * @note This is public because it is called externally + * @since 1.7.0 + */ + public function endElement($parser, $name) + { + array_pop($this->stack); + + switch ($name) { + case 'SECURITY': + case 'FIX': + case 'LANGUAGE': + case 'ADDITION': + case 'CHANGE': + case 'REMOVE': + case 'NOTE': + $name = strtolower($name); + $this->currentChangelog->$name->data = $this->items; + $this->items = array(); + break; + case 'CHANGELOG': + if (version_compare($this->currentChangelog->version->data, $this->matchVersion, '==') === true) { + $this->latest = $this->currentChangelog; + } + + // No version match, empty it + $this->currentChangelog = new \stdClass(); + break; + case 'CHANGELOGS': + // If the latest item is set then we transfer it to where we want to + if (isset($this->latest)) { + foreach (get_object_vars($this->latest) as $key => $val) { + $this->$key = $val; + } + + unset($this->latest); + unset($this->currentChangelog); + } elseif (isset($this->currentChangelog)) { + // The update might be for an older version of j! + unset($this->currentChangelog); + } + break; + } + } + + /** + * Character Parser Function + * + * @param object $parser Parser object. + * @param object $data The data. + * + * @return void + * + * @note This is public because its called externally. + * @since 1.7.0 + */ + public function characterData($parser, $data) + { + $tag = $this->getLastTag(); + + switch ($tag) { + case 'ITEM': + $this->items[] = $data; + break; + case 'SECURITY': + case 'FIX': + case 'LANGUAGE': + case 'ADDITION': + case 'CHANGE': + case 'REMOVE': + case 'NOTE': + break; + default: + // Throw the data for this item together + $tag = strtolower($tag); + + if (isset($this->currentChangelog->$tag)) { + $this->currentChangelog->$tag->data .= $data; + } + break; + } + } + + /** + * Loads an XML file from a URL. + * + * @param string $url The URL. + * + * @return boolean True on success + * + * @since 4.0.0 + */ + public function loadFromXml($url) + { + $version = new Version(); + $httpOption = new Registry(); + $httpOption->set('userAgent', $version->getUserAgent('Joomla', true, false)); + + try { + $http = HttpFactory::getHttp($httpOption); + $response = $http->get($url); + } catch (RuntimeException $e) { + $response = null; + } + + if ($response === null || $response->code !== 200) { + // @todo: Add a 'mark bad' setting here somehow + Log::add(Text::sprintf('JLIB_UPDATER_ERROR_EXTENSION_OPEN_URL', $url), Log::WARNING, 'jerror'); + + return false; + } + + $this->currentChangelog = new \stdClass(); + + $this->xmlParser = xml_parser_create(''); + xml_set_object($this->xmlParser, $this); + xml_set_element_handler($this->xmlParser, 'startElement', 'endElement'); + xml_set_character_data_handler($this->xmlParser, 'characterData'); + + if (!xml_parse($this->xmlParser, $response->body)) { + Log::add( + sprintf( + 'XML error: %s at line %d', + xml_error_string(xml_get_error_code($this->xmlParser)), + xml_get_current_line_number($this->xmlParser) + ), + Log::WARNING, + 'updater' + ); + + return false; + } + + xml_parser_free($this->xmlParser); + + return true; + } } diff --git a/libraries/src/Client/ClientHelper.php b/libraries/src/Client/ClientHelper.php index 26ba9afe1b0c9..f14a3a51aad6d 100644 --- a/libraries/src/Client/ClientHelper.php +++ b/libraries/src/Client/ClientHelper.php @@ -1,4 +1,5 @@ $app->get('ftp_enable'), - 'host' => $app->get('ftp_host'), - 'port' => $app->get('ftp_port'), - 'user' => $app->get('ftp_user'), - 'pass' => $app->get('ftp_pass'), - 'root' => $app->get('ftp_root'), - ); - break; - - default: - $options = array('enabled' => false, 'host' => '', 'port' => '', 'user' => '', 'pass' => '', 'root' => ''); - break; - } - - // If user and pass are not set in global config lets see if they are in the session - if ($options['enabled'] == true && ($options['user'] == '' || $options['pass'] == '')) - { - $session = Factory::getSession(); - $options['user'] = $session->get($client . '.user', null, 'JClientHelper'); - $options['pass'] = $session->get($client . '.pass', null, 'JClientHelper'); - } - - // If user or pass are missing, disable this client - if ($options['user'] == '' || $options['pass'] == '') - { - $options['enabled'] = false; - } - - // Save the credentials for later use - $credentials[$client] = $options; - } - - return $credentials[$client]; - } - - /** - * Method to set client login credentials - * - * @param string $client Client name, currently only 'ftp' is supported - * @param string $user Username - * @param string $pass Password - * - * @return boolean True if the given login credentials have been set and are valid - * - * @since 1.7.0 - */ - public static function setCredentials($client, $user, $pass) - { - $return = false; - $client = strtolower($client); - - // Test if the given credentials are valid - switch ($client) - { - case 'ftp': - $app = Factory::getApplication(); - $options = array('enabled' => $app->get('ftp_enable'), 'host' => $app->get('ftp_host'), 'port' => $app->get('ftp_port')); - - if ($options['enabled']) - { - $ftp = FtpClient::getInstance($options['host'], $options['port']); - - // Test the connection and try to log in - if ($ftp->isConnected()) - { - if ($ftp->login($user, $pass)) - { - $return = true; - } - - $ftp->quit(); - } - } - break; - - default: - break; - } - - if ($return) - { - // Save valid credentials to the session - $session = Factory::getSession(); - $session->set($client . '.user', $user, 'JClientHelper'); - $session->set($client . '.pass', $pass, 'JClientHelper'); - - // Force re-creation of the data saved within JClientHelper::getCredentials() - self::getCredentials($client, true); - } - - return $return; - } - - /** - * Method to determine if client login credentials are present - * - * @param string $client Client name, currently only 'ftp' is supported - * - * @return boolean True if login credentials are available - * - * @since 1.7.0 - */ - public static function hasCredentials($client) - { - $return = false; - $client = strtolower($client); - - // Get (unmodified) credentials for this client - switch ($client) - { - case 'ftp': - $app = Factory::getApplication(); - $options = array('enabled' => $app->get('ftp_enable'), 'user' => $app->get('ftp_user'), 'pass' => $app->get('ftp_pass')); - break; - - default: - $options = array('enabled' => false, 'user' => '', 'pass' => ''); - break; - } - - if ($options['enabled'] == false) - { - // The client is disabled in global config, so let's pretend we are OK - $return = true; - } - elseif ($options['user'] != '' && $options['pass'] != '') - { - // Login credentials are available in global config - $return = true; - } - else - { - // Check if login credentials are available in the session - $session = Factory::getSession(); - $user = $session->get($client . '.user', null, 'JClientHelper'); - $pass = $session->get($client . '.pass', null, 'JClientHelper'); - - if ($user != '' && $pass != '') - { - $return = true; - } - } - - return $return; - } - - /** - * Determine whether input fields for client settings need to be shown - * - * If valid credentials were passed along with the request, they are saved to the session. - * This functions returns an exception if invalid credentials have been given or if the - * connection to the server failed for some other reason. - * - * @param string $client The name of the client. - * - * @return boolean True if credentials are present - * - * @since 1.7.0 - * @throws \InvalidArgumentException if credentials invalid - */ - public static function setCredentialsFromRequest($client) - { - // Determine whether FTP credentials have been passed along with the current request - $input = Factory::getApplication()->input; - $user = $input->post->getString('username', null); - $pass = $input->post->getString('password', null); - - if ($user != '' && $pass != '') - { - // Add credentials to the session - if (!self::setCredentials($client, $user, $pass)) - { - throw new \InvalidArgumentException('Invalid user credentials'); - } - - $return = false; - } - else - { - // Just determine if the FTP input fields need to be shown - $return = !self::hasCredentials('ftp'); - } - - return $return; - } + /** + * Method to return the array of client layer configuration options + * + * @param string $client Client name, currently only 'ftp' is supported + * @param boolean $force Forces re-creation of the login credentials. Set this to + * true if login credentials in the session storage have changed + * + * @return array Client layer configuration options, consisting of at least + * these fields: enabled, host, port, user, pass, root + * + * @since 1.7.0 + */ + public static function getCredentials($client, $force = false) + { + static $credentials = array(); + + $client = strtolower($client); + + if (!isset($credentials[$client]) || $force) { + $app = Factory::getApplication(); + + // Fetch the client layer configuration options for the specific client + switch ($client) { + case 'ftp': + $options = array( + 'enabled' => $app->get('ftp_enable'), + 'host' => $app->get('ftp_host'), + 'port' => $app->get('ftp_port'), + 'user' => $app->get('ftp_user'), + 'pass' => $app->get('ftp_pass'), + 'root' => $app->get('ftp_root'), + ); + break; + + default: + $options = array('enabled' => false, 'host' => '', 'port' => '', 'user' => '', 'pass' => '', 'root' => ''); + break; + } + + // If user and pass are not set in global config lets see if they are in the session + if ($options['enabled'] == true && ($options['user'] == '' || $options['pass'] == '')) { + $session = Factory::getSession(); + $options['user'] = $session->get($client . '.user', null, 'JClientHelper'); + $options['pass'] = $session->get($client . '.pass', null, 'JClientHelper'); + } + + // If user or pass are missing, disable this client + if ($options['user'] == '' || $options['pass'] == '') { + $options['enabled'] = false; + } + + // Save the credentials for later use + $credentials[$client] = $options; + } + + return $credentials[$client]; + } + + /** + * Method to set client login credentials + * + * @param string $client Client name, currently only 'ftp' is supported + * @param string $user Username + * @param string $pass Password + * + * @return boolean True if the given login credentials have been set and are valid + * + * @since 1.7.0 + */ + public static function setCredentials($client, $user, $pass) + { + $return = false; + $client = strtolower($client); + + // Test if the given credentials are valid + switch ($client) { + case 'ftp': + $app = Factory::getApplication(); + $options = array('enabled' => $app->get('ftp_enable'), 'host' => $app->get('ftp_host'), 'port' => $app->get('ftp_port')); + + if ($options['enabled']) { + $ftp = FtpClient::getInstance($options['host'], $options['port']); + + // Test the connection and try to log in + if ($ftp->isConnected()) { + if ($ftp->login($user, $pass)) { + $return = true; + } + + $ftp->quit(); + } + } + break; + + default: + break; + } + + if ($return) { + // Save valid credentials to the session + $session = Factory::getSession(); + $session->set($client . '.user', $user, 'JClientHelper'); + $session->set($client . '.pass', $pass, 'JClientHelper'); + + // Force re-creation of the data saved within JClientHelper::getCredentials() + self::getCredentials($client, true); + } + + return $return; + } + + /** + * Method to determine if client login credentials are present + * + * @param string $client Client name, currently only 'ftp' is supported + * + * @return boolean True if login credentials are available + * + * @since 1.7.0 + */ + public static function hasCredentials($client) + { + $return = false; + $client = strtolower($client); + + // Get (unmodified) credentials for this client + switch ($client) { + case 'ftp': + $app = Factory::getApplication(); + $options = array('enabled' => $app->get('ftp_enable'), 'user' => $app->get('ftp_user'), 'pass' => $app->get('ftp_pass')); + break; + + default: + $options = array('enabled' => false, 'user' => '', 'pass' => ''); + break; + } + + if ($options['enabled'] == false) { + // The client is disabled in global config, so let's pretend we are OK + $return = true; + } elseif ($options['user'] != '' && $options['pass'] != '') { + // Login credentials are available in global config + $return = true; + } else { + // Check if login credentials are available in the session + $session = Factory::getSession(); + $user = $session->get($client . '.user', null, 'JClientHelper'); + $pass = $session->get($client . '.pass', null, 'JClientHelper'); + + if ($user != '' && $pass != '') { + $return = true; + } + } + + return $return; + } + + /** + * Determine whether input fields for client settings need to be shown + * + * If valid credentials were passed along with the request, they are saved to the session. + * This functions returns an exception if invalid credentials have been given or if the + * connection to the server failed for some other reason. + * + * @param string $client The name of the client. + * + * @return boolean True if credentials are present + * + * @since 1.7.0 + * @throws \InvalidArgumentException if credentials invalid + */ + public static function setCredentialsFromRequest($client) + { + // Determine whether FTP credentials have been passed along with the current request + $input = Factory::getApplication()->input; + $user = $input->post->getString('username', null); + $pass = $input->post->getString('password', null); + + if ($user != '' && $pass != '') { + // Add credentials to the session + if (!self::setCredentials($client, $user, $pass)) { + throw new \InvalidArgumentException('Invalid user credentials'); + } + + $return = false; + } else { + // Just determine if the FTP input fields need to be shown + $return = !self::hasCredentials('ftp'); + } + + return $return; + } } diff --git a/libraries/src/Client/FtpClient.php b/libraries/src/Client/FtpClient.php index f7d86fb5326e1..4d65ead463980 100644 --- a/libraries/src/Client/FtpClient.php +++ b/libraries/src/Client/FtpClient.php @@ -1,4 +1,5 @@ "\n", 'WIN' => "\r\n"); - - /** - * @var array FtpClient instances container. - * @since 2.5 - */ - protected static $instances = array(); - - /** - * FtpClient object constructor - * - * @param array $options Associative array of options to set - * - * @since 1.5 - */ - public function __construct(array $options = array()) - { - // If default transfer type is not set, set it to autoascii detect - if (!isset($options['type'])) - { - $options['type'] = FTP_BINARY; - } - - $this->setOptions($options); - - if (FTP_NATIVE) - { - BufferStreamHandler::stream_register(); - } - } - - /** - * FtpClient object destructor - * - * Closes an existing connection, if we have one - * - * @since 1.5 - */ - public function __destruct() - { - if (\is_resource($this->_conn)) - { - $this->quit(); - } - } - - /** - * Returns the global FTP connector object, only creating it - * if it doesn't already exist. - * - * You may optionally specify a username and password in the parameters. If you do so, - * you may not login() again with different credentials using the same object. - * If you do not use this option, you must quit() the current connection when you - * are done, to free it for use by others. - * - * @param string $host Host to connect to - * @param string $port Port to connect to - * @param array $options Array with any of these options: type=>[FTP_AUTOASCII|FTP_ASCII|FTP_BINARY], timeout=>(int) - * @param string $user Username to use for a connection - * @param string $pass Password to use for a connection - * - * @return FtpClient The FTP Client object. - * - * @since 1.5 - */ - public static function getInstance($host = '127.0.0.1', $port = '21', array $options = array(), $user = null, $pass = null) - { - $signature = $user . ':' . $pass . '@' . $host . ':' . $port; - - // Create a new instance, or set the options of an existing one - if (!isset(static::$instances[$signature]) || !\is_object(static::$instances[$signature])) - { - static::$instances[$signature] = new static($options); - } - else - { - static::$instances[$signature]->setOptions($options); - } - - // Connect to the server, and login, if requested - if (!static::$instances[$signature]->isConnected()) - { - $return = static::$instances[$signature]->connect($host, $port); - - if ($return && $user !== null && $pass !== null) - { - static::$instances[$signature]->login($user, $pass); - } - } - - return static::$instances[$signature]; - } - - /** - * Set client options - * - * @param array $options Associative array of options to set - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function setOptions(array $options) - { - if (isset($options['type'])) - { - $this->_type = $options['type']; - } - - if (isset($options['timeout'])) - { - $this->_timeout = $options['timeout']; - } - - return true; - } - - /** - * Method to connect to a FTP server - * - * @param string $host Host to connect to [Default: 127.0.0.1] - * @param int $port Port to connect on [Default: port 21] - * - * @return boolean True if successful - * - * @since 3.0.0 - */ - public function connect($host = '127.0.0.1', $port = 21) - { - $errno = null; - $err = null; - - // If already connected, return - if (\is_resource($this->_conn)) - { - return true; - } - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - $this->_conn = @ftp_connect($host, $port, $this->_timeout); - - if ($this->_conn === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT', __METHOD__, $host, $port), Log::WARNING, 'jerror'); - - return false; - } - - // Set the timeout for this connection - ftp_set_option($this->_conn, FTP_TIMEOUT_SEC, $this->_timeout); - - return true; - } - - // Connect to the FTP server. - $this->_conn = @ fsockopen($host, $port, $errno, $err, $this->_timeout); - - if (!$this->_conn) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT_SOCKET', __METHOD__, $host, $port, $errno, $err), Log::WARNING, 'jerror'); - - return false; - } - - // Set the timeout for this connection - socket_set_timeout($this->_conn, $this->_timeout, 0); - - // Check for welcome response code - if (!$this->_verifyResponse(220)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 220), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to determine if the object is connected to an FTP server - * - * @return boolean True if connected - * - * @since 1.5 - */ - public function isConnected() - { - return \is_resource($this->_conn); - } - - /** - * Method to login to a server once connected - * - * @param string $user Username to login to the server - * @param string $pass Password to login to the server - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function login($user = 'anonymous', $pass = 'jftp@joomla.org') - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (@ftp_login($this->_conn, $user, $pass) === false) - { - Log::add('JFtp::login: Unable to login', Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - // Send the username - if (!$this->_putCmd('USER ' . $user, array(331, 503))) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_USERNAME', __METHOD__, $this->_response, $user), Log::WARNING, 'jerror'); - - return false; - } - - // If we are already logged in, continue :) - if ($this->_responseCode == 503) - { - return true; - } - - // Send the password - if (!$this->_putCmd('PASS ' . $pass, 230)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_PASSWORD', __METHOD__, $this->_response, str_repeat('*', \strlen($pass))), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to quit and close the connection - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function quit() - { - // If native FTP support is enabled lets use it... - if (FTP_NATIVE) - { - @ftp_close($this->_conn); - - return true; - } - - // Logout and close connection - @fwrite($this->_conn, "QUIT\r\n"); - @fclose($this->_conn); - - return true; - } - - /** - * Method to retrieve the current working directory on the FTP server - * - * @return string Current working directory - * - * @since 1.5 - */ - public function pwd() - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (($ret = @ftp_pwd($this->_conn)) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return $ret; - } - - $match = array(null); - - // Send print working directory command and verify success - if (!$this->_putCmd('PWD', 257)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 257), Log::WARNING, 'jerror'); - - return false; - } - - // Match just the path - preg_match('/"[^"\r\n]*"/', $this->_response, $match); - - // Return the cleaned path - return preg_replace("/\"/", '', $match[0]); - } - - /** - * Method to system string from the FTP server - * - * @return string System identifier string - * - * @since 1.5 - */ - public function syst() - { - // If native FTP support is enabled lets use it... - if (FTP_NATIVE) - { - if (($ret = @ftp_systype($this->_conn)) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - } - else - { - // Send print working directory command and verify success - if (!$this->_putCmd('SYST', 215)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 215), Log::WARNING, 'jerror'); - - return false; - } - - $ret = $this->_response; - } - - // Match the system string to an OS - if (strpos(strtoupper($ret), 'MAC') !== false) - { - $ret = 'MAC'; - } - elseif (strpos(strtoupper($ret), 'WIN') !== false) - { - $ret = 'WIN'; - } - else - { - $ret = 'UNIX'; - } - - // Return the os type - return $ret; - } - - /** - * Method to change the current working directory on the FTP server - * - * @param string $path Path to change into on the server - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function chdir($path) - { - // If native FTP support is enabled lets use it... - if (FTP_NATIVE) - { - if (@ftp_chdir($this->_conn, $path) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - // Send change directory command and verify success - if (!$this->_putCmd('CWD ' . $path, 250)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 250, $path), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to reinitialise the server, ie. need to login again - * - * NOTE: This command not available on all servers - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function reinit() - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (@ftp_site($this->_conn, 'REIN') === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - // Send reinitialise command to the server - if (!$this->_putCmd('REIN', 220)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 220), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to rename a file/folder on the FTP server - * - * @param string $from Path to change file/folder from - * @param string $to Path to change file/folder to - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function rename($from, $to) - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (@ftp_rename($this->_conn, $from, $to) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - // Send rename from command to the server - if (!$this->_putCmd('RNFR ' . $from, 350)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_RENAME_BAD_RESPONSE_FROM', __METHOD__, $this->_response, $from), Log::WARNING, 'jerror'); - - return false; - } - - // Send rename to command to the server - if (!$this->_putCmd('RNTO ' . $to, 250)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_RENAME_BAD_RESPONSE_TO', __METHOD__, $this->_response, $to), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to change mode for a path on the FTP server - * - * @param string $path Path to change mode on - * @param mixed $mode Octal value to change mode to, e.g. '0777', 0777 or 511 (string or integer) - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function chmod($path, $mode) - { - // If no filename is given, we assume the current directory is the target - if ($path == '') - { - $path = '.'; - } - - // Convert the mode to a string - if (\is_int($mode)) - { - $mode = decoct($mode); - } - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (@ftp_site($this->_conn, 'CHMOD ' . $mode . ' ' . $path) === false) - { - if (!IS_WIN) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - } - - return false; - } - - return true; - } - - // Send change mode command and verify success [must convert mode from octal] - if (!$this->_putCmd('SITE CHMOD ' . $mode . ' ' . $path, array(200, 250))) - { - if (!IS_WIN) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_CHMOD_BAD_RESPONSE', __METHOD__, $this->_response, $path, $mode), Log::WARNING, 'jerror'); - } - - return false; - } - - return true; - } - - /** - * Method to delete a path [file/folder] on the FTP server - * - * @param string $path Path to delete - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function delete($path) - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (@ftp_delete($this->_conn, $path) === false) - { - if (@ftp_rmdir($this->_conn, $path) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - } - - return true; - } - - // Send delete file command and if that doesn't work, try to remove a directory - if (!$this->_putCmd('DELE ' . $path, 250)) - { - if (!$this->_putCmd('RMD ' . $path, 250)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 250, $path), Log::WARNING, 'jerror'); - - return false; - } - } - - return true; - } - - /** - * Method to create a directory on the FTP server - * - * @param string $path Directory to create - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function mkdir($path) - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (@ftp_mkdir($this->_conn, $path) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - // Send change directory command and verify success - if (!$this->_putCmd('MKD ' . $path, 257)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 257, $path), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to restart data transfer at a given byte - * - * @param integer $point Byte to restart transfer at - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function restart($point) - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - if (@ftp_site($this->_conn, 'REST ' . $point) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - // Send restart command and verify success - if (!$this->_putCmd('REST ' . $point, 350)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_RESTART_BAD_RESPONSE', __METHOD__, $this->_response, $point), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to create an empty file on the FTP server - * - * @param string $path Path local file to store on the FTP server - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function create($path) - { - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - $buffer = fopen('buffer://tmp', 'r'); - - if (@ftp_fput($this->_conn, $path, $buffer, FTP_ASCII) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - fclose($buffer); - - return false; - } - - fclose($buffer); - - return true; - } - - // Start passive mode - if (!$this->_passive()) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (!$this->_putCmd('STOR ' . $path, array(150, 125))) - { - @ fclose($this->_dataconn); - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); - - return false; - } - - // To create a zero byte upload close the data port connection - fclose($this->_dataconn); - - if (!$this->_verifyResponse(226)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to read a file from the FTP server's contents into a buffer - * - * @param string $remote Path to remote file to read on the FTP server - * @param string &$buffer Buffer variable to read file contents into - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function read($remote, &$buffer) - { - // Determine file type - $mode = $this->_findMode($remote); - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - $tmp = fopen('buffer://tmp', 'br+'); - - if (@ftp_fget($this->_conn, $tmp, $remote, $mode) === false) - { - fclose($tmp); - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - // Read tmp buffer contents - rewind($tmp); - $buffer = ''; - - while (!feof($tmp)) - { - $buffer .= fread($tmp, 8192); - } - - fclose($tmp); - - return true; - } - - $this->_mode($mode); - - // Start passive mode - if (!$this->_passive()) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (!$this->_putCmd('RETR ' . $remote, array(150, 125))) - { - @ fclose($this->_dataconn); - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - - return false; - } - - // Read data from data port connection and add to the buffer - $buffer = ''; - - while (!feof($this->_dataconn)) - { - $buffer .= fread($this->_dataconn, 4096); - } - - // Close the data port connection - fclose($this->_dataconn); - - // Let's try to cleanup some line endings if it is ascii - if ($mode == FTP_ASCII) - { - $os = 'UNIX'; - - if (IS_WIN) - { - $os = 'WIN'; - } - - $buffer = preg_replace('/' . CRLF . '/', $this->_lineEndings[$os], $buffer); - } - - if (!$this->_verifyResponse(226)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to get a file from the FTP server and save it to a local file - * - * @param string $local Local path to save remote file to - * @param string $remote Path to remote file to get on the FTP server - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function get($local, $remote) - { - // Determine file type - $mode = $this->_findMode($remote); - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (@ftp_get($this->_conn, $local, $remote, $mode) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - $this->_mode($mode); - - // Check to see if the local file can be opened for writing - $fp = fopen($local, 'wb'); - - if (!$fp) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_LOCAL_FILE_OPEN_WRITING', __METHOD__, $local), Log::WARNING, 'jerror'); - - return false; - } - - // Start passive mode - if (!$this->_passive()) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (!$this->_putCmd('RETR ' . $remote, array(150, 125))) - { - @ fclose($this->_dataconn); - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - - return false; - } - - // Read data from data port connection and add to the buffer - while (!feof($this->_dataconn)) - { - $buffer = fread($this->_dataconn, 4096); - fwrite($fp, $buffer, 4096); - } - - // Close the data port connection and file pointer - fclose($this->_dataconn); - fclose($fp); - - if (!$this->_verifyResponse(226)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to store a file to the FTP server - * - * @param string $local Path to local file to store on the FTP server - * @param string $remote FTP path to file to create - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function store($local, $remote = null) - { - // If remote file is not given, use the filename of the local file in the current - // working directory. - if ($remote == null) - { - $remote = basename($local); - } - - // Determine file type - $mode = $this->_findMode($remote); - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (@ftp_put($this->_conn, $remote, $local, $mode) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - $this->_mode($mode); - - // Check to see if the local file exists and if so open it for reading - if (@ file_exists($local)) - { - $fp = fopen($local, 'rb'); - - if (!$fp) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_LOCAL_FILE_OPEN_READING', __METHOD__, $local), Log::WARNING, 'jerror'); - - return false; - } - } - else - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_LOCAL_FILE_FIND', __METHOD__, $local), Log::WARNING, 'jerror'); - - return false; - } - - // Start passive mode - if (!$this->_passive()) - { - @ fclose($fp); - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - // Send store command to the FTP server - if (!$this->_putCmd('STOR ' . $remote, array(150, 125))) - { - @ fclose($fp); - @ fclose($this->_dataconn); - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - - return false; - } - - // Do actual file transfer, read local file and write to data port connection - while (!feof($fp)) - { - $line = fread($fp, 4096); - - do - { - if (($result = @ fwrite($this->_dataconn, $line)) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_DATA_PORT', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - $line = substr($line, $result); - } - while ($line != ''); - } - - fclose($fp); - fclose($this->_dataconn); - - if (!$this->_verifyResponse(226)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to write a string to the FTP server - * - * @param string $remote FTP path to file to write to - * @param string $buffer Contents to write to the FTP server - * - * @return boolean True if successful - * - * @since 1.5 - */ - public function write($remote, $buffer) - { - // Determine file type - $mode = $this->_findMode($remote); - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - $tmp = fopen('buffer://tmp', 'br+'); - fwrite($tmp, $buffer); - rewind($tmp); - - if (@ftp_fput($this->_conn, $remote, $tmp, $mode) === false) - { - fclose($tmp); - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - fclose($tmp); - - return true; - } - - // First we need to set the transfer mode - $this->_mode($mode); - - // Start passive mode - if (!$this->_passive()) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - // Send store command to the FTP server - if (!$this->_putCmd('STOR ' . $remote, array(150, 125))) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - @ fclose($this->_dataconn); - - return false; - } - - // Write buffer to the data connection port - do - { - if (($result = @ fwrite($this->_dataconn, $buffer)) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_DATA_PORT', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - $buffer = substr($buffer, $result); - } - while ($buffer != ''); - - // Close the data connection port [Data transfer complete] - fclose($this->_dataconn); - - // Verify that the server received the transfer - if (!$this->_verifyResponse(226)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); - - return false; - } - - return true; - } - - /** - * Method to append a string to the FTP server - * - * @param string $remote FTP path to file to append to - * @param string $buffer Contents to append to the FTP server - * - * @return boolean True if successful - * - * @since 3.6.0 - */ - public function append($remote, $buffer) - { - // Determine file type - $mode = $this->_findMode($remote); - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), 36); - } - - $tmp = fopen('buffer://tmp', 'bw+'); - fwrite($tmp, $buffer); - rewind($tmp); - - $size = $this->size($remote); - - if ($size === false) - { - } - - if (@ftp_fput($this->_conn, $remote, $tmp, $mode, $size) === false) - { - fclose($tmp); - - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), 35); - } - - fclose($tmp); - - return true; - } - - // First we need to set the transfer mode - $this->_mode($mode); - - // Start passive mode - if (!$this->_passive()) - { - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), 36); - } - - // Send store command to the FTP server - if (!$this->_putCmd('APPE ' . $remote, array(150, 125))) - { - @fclose($this->_dataconn); - - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), 35); - } - - // Write buffer to the data connection port - do - { - if (($result = @ fwrite($this->_dataconn, $buffer)) === false) - { - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_DATA_PORT', __METHOD__), 37); - } - - $buffer = substr($buffer, $result); - } - while ($buffer != ''); - - // Close the data connection port [Data transfer complete] - fclose($this->_dataconn); - - // Verify that the server received the transfer - if (!$this->_verifyResponse(226)) - { - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), 37); - } - - return true; - } - - /** - * Get the size of the remote file. - * - * @param string $remote FTP path to file whose size to get - * - * @return mixed number of bytes or false on error - * - * @since 3.6.0 - */ - public function size($remote) - { - if (FTP_NATIVE) - { - $size = ftp_size($this->_conn, $remote); - - // In case ftp_size fails, try the SIZE command directly. - if ($size === -1) - { - $response = ftp_raw($this->_conn, 'SIZE ' . $remote); - $responseCode = substr($response[0], 0, 3); - $responseMessage = substr($response[0], 4); - - if ($responseCode != '213') - { - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), 35); - } - - $size = (int) $responseMessage; - } - - return $size; - } - - // Start passive mode - if (!$this->_passive()) - { - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), 36); - } - - // Send size command to the FTP server - if (!$this->_putCmd('SIZE ' . $remote, array(213))) - { - @fclose($this->_dataconn); - - throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 213, $remote), 35); - } - - return (int) substr($this->_responseMsg, 4); - } - - /** - * Method to list the filenames of the contents of a directory on the FTP server - * - * Note: Some servers also return folder names. However, to be sure to list folders on all - * servers, you should use listDetails() instead if you also need to deal with folders - * - * @param string $path Path local file to store on the FTP server - * - * @return string Directory listing - * - * @since 1.5 - */ - public function listNames($path = null) - { - $data = null; - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (($list = @ftp_nlist($this->_conn, $path)) === false) - { - // Workaround for empty directories on some servers - if ($this->listDetails($path, 'files') === array()) - { - return array(); - } - - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - $list = preg_replace('#^' . preg_quote($path, '#') . '[/\\\\]?#', '', $list); - - if ($keys = array_merge(array_keys($list, '.'), array_keys($list, '..'))) - { - foreach ($keys as $key) - { - unset($list[$key]); - } - } - - return $list; - } - - // If a path exists, prepend a space - if ($path != null) - { - $path = ' ' . $path; - } - - // Start passive mode - if (!$this->_passive()) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (!$this->_putCmd('NLST' . $path, array(150, 125))) - { - @ fclose($this->_dataconn); - - // Workaround for empty directories on some servers - if ($this->listDetails($path, 'files') === array()) - { - return array(); - } - - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); - - return false; - } - - // Read in the file listing. - while (!feof($this->_dataconn)) - { - $data .= fread($this->_dataconn, 4096); - } - - fclose($this->_dataconn); - - // Everything go okay? - if (!$this->_verifyResponse(226)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); - - return false; - } - - $data = preg_split('/[' . CRLF . ']+/', $data, -1, PREG_SPLIT_NO_EMPTY); - $data = preg_replace('#^' . preg_quote(substr($path, 1), '#') . '[/\\\\]?#', '', $data); - - if ($keys = array_merge(array_keys($data, '.'), array_keys($data, '..'))) - { - foreach ($keys as $key) - { - unset($data[$key]); - } - } - - return $data; - } - - /** - * Method to list the contents of a directory on the FTP server - * - * @param string $path Path to the local file to be stored on the FTP server - * @param string $type Return type [raw|all|folders|files] - * - * @return mixed If $type is raw: string Directory listing, otherwise array of string with file-names - * - * @since 1.5 - */ - public function listDetails($path = null, $type = 'all') - { - $dir_list = array(); - $data = null; - $regs = null; - - // @todo: Deal with recurse -- nightmare - // For now we will just set it to false - $recurse = false; - - // If native FTP support is enabled let's use it... - if (FTP_NATIVE) - { - // Turn passive mode on - if (@ftp_pasv($this->_conn, true) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - if (($contents = @ftp_rawlist($this->_conn, $path)) === false) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - } - else - { - // Non Native mode - - // Start passive mode - if (!$this->_passive()) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - // If a path exists, prepend a space - if ($path != null) - { - $path = ' ' . $path; - } - - // Request the file listing - if (!$this->_putCmd(($recurse == true) ? 'LIST -R' : 'LIST' . $path, array(150, 125))) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); - @ fclose($this->_dataconn); - - return false; - } - - // Read in the file listing. - while (!feof($this->_dataconn)) - { - $data .= fread($this->_dataconn, 4096); - } - - fclose($this->_dataconn); - - // Everything go okay? - if (!$this->_verifyResponse(226)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); - - return false; - } - - $contents = explode(CRLF, $data); - } - - // If only raw output is requested we are done - if ($type === 'raw') - { - return $data; - } - - // If we received the listing of an empty directory, we are done as well - if (empty($contents[0])) - { - return $dir_list; - } - - // If the server returned the number of results in the first response, let's dump it - if (strtolower(substr($contents[0], 0, 6)) === 'total ') - { - array_shift($contents); - - if (!isset($contents[0]) || empty($contents[0])) - { - return $dir_list; - } - } - - // Regular expressions for the directory listing parsing. - $regexps = array( - 'UNIX' => '#([-dl][rwxstST-]+).* ([0-9]*) ([a-zA-Z0-9]+).* ([a-zA-Z0-9]+).* ([0-9]*)' - . ' ([a-zA-Z]+[0-9: ]*[0-9])[ ]+(([0-9]{1,2}:[0-9]{2})|[0-9]{4}) (.+)#', - 'MAC' => '#([-dl][rwxstST-]+).* ?([0-9 ]*)?([a-zA-Z0-9]+).* ([a-zA-Z0-9]+).* ([0-9]*)' - . ' ([a-zA-Z]+[0-9: ]*[0-9])[ ]+(([0-9]{2}:[0-9]{2})|[0-9]{4}) (.+)#', - 'WIN' => '#([0-9]{2})-([0-9]{2})-([0-9]{2}) +([0-9]{2}):([0-9]{2})(AM|PM) +([0-9]+|) +(.+)#', - ); - - // Find out the format of the directory listing by matching one of the regexps - $osType = null; - - foreach ($regexps as $k => $v) - { - if (@preg_match($v, $contents[0])) - { - $osType = $k; - $regexp = $v; - break; - } - } - - if (!$osType) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_UNRECOGNISED_FOLDER_LISTING_FORMATJLIB_CLIENT_ERROR_JFTP_LISTDETAILS_UNRECOGNISED', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - // Here is where it is going to get dirty.... - if ($osType === 'UNIX' || $osType === 'MAC') - { - foreach ($contents as $file) - { - $tmp_array = null; - - if (@preg_match($regexp, $file, $regs)) - { - $fType = (int) strpos('-dl', $regs[1][0]); - - // $tmp_array['line'] = $regs[0]; - $tmp_array['type'] = $fType; - $tmp_array['rights'] = $regs[1]; - - // $tmp_array['number'] = $regs[2]; - $tmp_array['user'] = $regs[3]; - $tmp_array['group'] = $regs[4]; - $tmp_array['size'] = $regs[5]; - $tmp_array['date'] = @date('m-d', strtotime($regs[6])); - $tmp_array['time'] = $regs[7]; - $tmp_array['name'] = $regs[9]; - } - - // If we just want files, do not add a folder - if ($type === 'files' && $tmp_array['type'] == 1) - { - continue; - } - - // If we just want folders, do not add a file - if ($type === 'folders' && $tmp_array['type'] == 0) - { - continue; - } - - if (\is_array($tmp_array) && $tmp_array['name'] != '.' && $tmp_array['name'] != '..') - { - $dir_list[] = $tmp_array; - } - } - } - else - { - foreach ($contents as $file) - { - $tmp_array = null; - - if (@preg_match($regexp, $file, $regs)) - { - $fType = (int) ($regs[7] === ''); - $timestamp = strtotime("$regs[3]-$regs[1]-$regs[2] $regs[4]:$regs[5]$regs[6]"); - - // $tmp_array['line'] = $regs[0]; - $tmp_array['type'] = $fType; - $tmp_array['rights'] = ''; - - // $tmp_array['number'] = 0; - $tmp_array['user'] = ''; - $tmp_array['group'] = ''; - $tmp_array['size'] = (int) $regs[7]; - $tmp_array['date'] = date('m-d', $timestamp); - $tmp_array['time'] = date('H:i', $timestamp); - $tmp_array['name'] = $regs[8]; - } - - // If we just want files, do not add a folder - if ($type === 'files' && $tmp_array['type'] == 1) - { - continue; - } - - // If we just want folders, do not add a file - if ($type === 'folders' && $tmp_array['type'] == 0) - { - continue; - } - - if (\is_array($tmp_array) && $tmp_array['name'] != '.' && $tmp_array['name'] != '..') - { - $dir_list[] = $tmp_array; - } - } - } - - return $dir_list; - } - - /** - * Send command to the FTP server and validate an expected response code - * - * @param string $cmd Command to send to the FTP server - * @param mixed $expectedResponse Integer response code or array of integer response codes - * - * @return boolean True if command executed successfully - * - * @since 1.5 - */ - protected function _putCmd($cmd, $expectedResponse) - { - // Make sure we have a connection to the server - if (!\is_resource($this->_conn)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PUTCMD_UNCONNECTED', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - // Send the command to the server - if (!fwrite($this->_conn, $cmd . "\r\n")) - { - Log::add(Text::sprintf('DDD', Text::sprintf('JLIB_CLIENT_ERROR_FTP_PUTCMD_SEND', __METHOD__, $cmd)), Log::WARNING, 'jerror'); - } - - return $this->_verifyResponse($expectedResponse); - } - - /** - * Verify the response code from the server and log response if flag is set - * - * @param mixed $expected Integer response code or array of integer response codes - * - * @return boolean True if response code from the server is expected - * - * @since 1.5 - */ - protected function _verifyResponse($expected) - { - $parts = null; - - // Wait for a response from the server, but timeout after the set time limit - $endTime = time() + $this->_timeout; - $this->_response = ''; - - do - { - $this->_response .= fgets($this->_conn, 4096); - } - while (!preg_match('/^([0-9]{3})(-(.*' . CRLF . ')+\1)? [^' . CRLF . ']+' . CRLF . "$/", $this->_response, $parts) && time() < $endTime); - - // Catch a timeout or bad response - if (!isset($parts[1])) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TIMEOUT', __METHOD__, $this->_response), Log::WARNING, 'jerror'); - - return false; - } - - // Separate the code from the message - $this->_responseCode = $parts[1]; - $this->_responseMsg = $parts[0]; - - // Did the server respond with the code we wanted? - if (\is_array($expected)) - { - if (\in_array($this->_responseCode, $expected)) - { - $retval = true; - } - else - { - $retval = false; - } - } - else - { - if ($this->_responseCode == $expected) - { - $retval = true; - } - else - { - $retval = false; - } - } - - return $retval; - } - - /** - * Set server to passive mode and open a data port connection - * - * @return boolean True if successful - * - * @since 1.5 - */ - protected function _passive() - { - $match = array(); - $parts = array(); - $errno = null; - $err = null; - - // Make sure we have a connection to the server - if (!\is_resource($this->_conn)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT', __METHOD__), Log::WARNING, 'jerror'); - - return false; - } - - // Request a passive connection - this means, we'll talk to you, you don't talk to us. - @ fwrite($this->_conn, "PASV\r\n"); - - // Wait for a response from the server, but timeout after the set time limit - $endTime = time() + $this->_timeout; - $this->_response = ''; - - do - { - $this->_response .= fgets($this->_conn, 4096); - } - while (!preg_match('/^([0-9]{3})(-(.*' . CRLF . ')+\1)? [^' . CRLF . ']+' . CRLF . "$/", $this->_response, $parts) && time() < $endTime); - - // Catch a timeout or bad response - if (!isset($parts[1])) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TIMEOUT', __METHOD__, $this->_response), Log::WARNING, 'jerror'); - - return false; - } - - // Separate the code from the message - $this->_responseCode = $parts[1]; - $this->_responseMsg = $parts[0]; - - // If it's not 227, we weren't given an IP and port, which means it failed. - if ($this->_responseCode != '227') - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE_IP_OBTAIN', __METHOD__, $this->_responseMsg), Log::WARNING, 'jerror'); - - return false; - } - - // Snatch the IP and port information, or die horribly trying... - if (preg_match('~\((\d+),\s*(\d+),\s*(\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+))\)~', $this->_responseMsg, $match) == 0) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE_IP_VALID', __METHOD__, $this->_responseMsg), Log::WARNING, 'jerror'); - - return false; - } - - // This is pretty simple - store it for later use ;). - $this->_pasv = array('ip' => $match[1] . '.' . $match[2] . '.' . $match[3] . '.' . $match[4], 'port' => $match[5] * 256 + $match[6]); - - // Connect, assuming we've got a connection. - $this->_dataconn = @fsockopen($this->_pasv['ip'], $this->_pasv['port'], $errno, $err, $this->_timeout); - - if (!$this->_dataconn) - { - Log::add( - Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT', __METHOD__, $this->_pasv['ip'], $this->_pasv['port'], $errno, $err), - Log::WARNING, - 'jerror' - ); - - return false; - } - - // Set the timeout for this connection - socket_set_timeout($this->_conn, $this->_timeout, 0); - - return true; - } - - /** - * Method to find out the correct transfer mode for a specific file - * - * @param string $fileName Name of the file - * - * @return integer Transfer-mode for this filetype [FTP_ASCII|FTP_BINARY] - * - * @since 1.5 - */ - protected function _findMode($fileName) - { - if ($this->_type == FTP_AUTOASCII) - { - $dot = strrpos($fileName, '.') + 1; - $ext = substr($fileName, $dot); - - if (\in_array($ext, $this->_autoAscii)) - { - $mode = FTP_ASCII; - } - else - { - $mode = FTP_BINARY; - } - } - elseif ($this->_type == FTP_ASCII) - { - $mode = FTP_ASCII; - } - else - { - $mode = FTP_BINARY; - } - - return $mode; - } - - /** - * Set transfer mode - * - * @param integer $mode Integer representation of data transfer mode [1:Binary|0:Ascii] - * Defined constants can also be used [FTP_BINARY|FTP_ASCII] - * - * @return boolean True if successful - * - * @since 1.5 - */ - protected function _mode($mode) - { - if ($mode == FTP_BINARY) - { - if (!$this->_putCmd('TYPE I', 200)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_MODE_BINARY', __METHOD__, $this->_response), Log::WARNING, 'jerror'); - - return false; - } - } - else - { - if (!$this->_putCmd('TYPE A', 200)) - { - Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_MODE_ASCII', __METHOD__, $this->_response), Log::WARNING, 'jerror'); - - return false; - } - } - - return true; - } + /** + * @var resource Socket resource + * @since 1.5 + */ + protected $_conn = null; + + /** + * @var resource Data port connection resource + * @since 1.5 + */ + protected $_dataconn = null; + + /** + * @var array Passive connection information + * @since 1.5 + */ + protected $_pasv = null; + + /** + * @var string Response Message + * @since 1.5 + */ + protected $_response = null; + + /** + * @var integer Timeout limit + * @since 1.5 + */ + protected $_timeout = 15; + + /** + * @var integer Transfer Type + * @since 1.5 + */ + protected $_type = null; + + /** + * @var array Array to hold ascii format file extensions + * @since 1.5 + */ + protected $_autoAscii = array( + 'asp', + 'bat', + 'c', + 'cpp', + 'csv', + 'h', + 'htm', + 'html', + 'shtml', + 'ini', + 'inc', + 'log', + 'php', + 'php3', + 'pl', + 'perl', + 'sh', + 'sql', + 'txt', + 'xhtml', + 'xml', + ); + + /** + * Array to hold native line ending characters + * + * @var array + * @since 1.5 + */ + protected $_lineEndings = array('UNIX' => "\n", 'WIN' => "\r\n"); + + /** + * @var array FtpClient instances container. + * @since 2.5 + */ + protected static $instances = array(); + + /** + * FtpClient object constructor + * + * @param array $options Associative array of options to set + * + * @since 1.5 + */ + public function __construct(array $options = array()) + { + // If default transfer type is not set, set it to autoascii detect + if (!isset($options['type'])) { + $options['type'] = FTP_BINARY; + } + + $this->setOptions($options); + + if (FTP_NATIVE) { + BufferStreamHandler::stream_register(); + } + } + + /** + * FtpClient object destructor + * + * Closes an existing connection, if we have one + * + * @since 1.5 + */ + public function __destruct() + { + if (\is_resource($this->_conn)) { + $this->quit(); + } + } + + /** + * Returns the global FTP connector object, only creating it + * if it doesn't already exist. + * + * You may optionally specify a username and password in the parameters. If you do so, + * you may not login() again with different credentials using the same object. + * If you do not use this option, you must quit() the current connection when you + * are done, to free it for use by others. + * + * @param string $host Host to connect to + * @param string $port Port to connect to + * @param array $options Array with any of these options: type=>[FTP_AUTOASCII|FTP_ASCII|FTP_BINARY], timeout=>(int) + * @param string $user Username to use for a connection + * @param string $pass Password to use for a connection + * + * @return FtpClient The FTP Client object. + * + * @since 1.5 + */ + public static function getInstance($host = '127.0.0.1', $port = '21', array $options = array(), $user = null, $pass = null) + { + $signature = $user . ':' . $pass . '@' . $host . ':' . $port; + + // Create a new instance, or set the options of an existing one + if (!isset(static::$instances[$signature]) || !\is_object(static::$instances[$signature])) { + static::$instances[$signature] = new static($options); + } else { + static::$instances[$signature]->setOptions($options); + } + + // Connect to the server, and login, if requested + if (!static::$instances[$signature]->isConnected()) { + $return = static::$instances[$signature]->connect($host, $port); + + if ($return && $user !== null && $pass !== null) { + static::$instances[$signature]->login($user, $pass); + } + } + + return static::$instances[$signature]; + } + + /** + * Set client options + * + * @param array $options Associative array of options to set + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function setOptions(array $options) + { + if (isset($options['type'])) { + $this->_type = $options['type']; + } + + if (isset($options['timeout'])) { + $this->_timeout = $options['timeout']; + } + + return true; + } + + /** + * Method to connect to a FTP server + * + * @param string $host Host to connect to [Default: 127.0.0.1] + * @param int $port Port to connect on [Default: port 21] + * + * @return boolean True if successful + * + * @since 3.0.0 + */ + public function connect($host = '127.0.0.1', $port = 21) + { + $errno = null; + $err = null; + + // If already connected, return + if (\is_resource($this->_conn)) { + return true; + } + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + $this->_conn = @ftp_connect($host, $port, $this->_timeout); + + if ($this->_conn === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT', __METHOD__, $host, $port), Log::WARNING, 'jerror'); + + return false; + } + + // Set the timeout for this connection + ftp_set_option($this->_conn, FTP_TIMEOUT_SEC, $this->_timeout); + + return true; + } + + // Connect to the FTP server. + $this->_conn = @ fsockopen($host, $port, $errno, $err, $this->_timeout); + + if (!$this->_conn) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT_SOCKET', __METHOD__, $host, $port, $errno, $err), Log::WARNING, 'jerror'); + + return false; + } + + // Set the timeout for this connection + socket_set_timeout($this->_conn, $this->_timeout, 0); + + // Check for welcome response code + if (!$this->_verifyResponse(220)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 220), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to determine if the object is connected to an FTP server + * + * @return boolean True if connected + * + * @since 1.5 + */ + public function isConnected() + { + return \is_resource($this->_conn); + } + + /** + * Method to login to a server once connected + * + * @param string $user Username to login to the server + * @param string $pass Password to login to the server + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function login($user = 'anonymous', $pass = 'jftp@joomla.org') + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (@ftp_login($this->_conn, $user, $pass) === false) { + Log::add('JFtp::login: Unable to login', Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + // Send the username + if (!$this->_putCmd('USER ' . $user, array(331, 503))) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_USERNAME', __METHOD__, $this->_response, $user), Log::WARNING, 'jerror'); + + return false; + } + + // If we are already logged in, continue :) + if ($this->_responseCode == 503) { + return true; + } + + // Send the password + if (!$this->_putCmd('PASS ' . $pass, 230)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_PASSWORD', __METHOD__, $this->_response, str_repeat('*', \strlen($pass))), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to quit and close the connection + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function quit() + { + // If native FTP support is enabled lets use it... + if (FTP_NATIVE) { + @ftp_close($this->_conn); + + return true; + } + + // Logout and close connection + @fwrite($this->_conn, "QUIT\r\n"); + @fclose($this->_conn); + + return true; + } + + /** + * Method to retrieve the current working directory on the FTP server + * + * @return string Current working directory + * + * @since 1.5 + */ + public function pwd() + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (($ret = @ftp_pwd($this->_conn)) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return $ret; + } + + $match = array(null); + + // Send print working directory command and verify success + if (!$this->_putCmd('PWD', 257)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 257), Log::WARNING, 'jerror'); + + return false; + } + + // Match just the path + preg_match('/"[^"\r\n]*"/', $this->_response, $match); + + // Return the cleaned path + return preg_replace("/\"/", '', $match[0]); + } + + /** + * Method to system string from the FTP server + * + * @return string System identifier string + * + * @since 1.5 + */ + public function syst() + { + // If native FTP support is enabled lets use it... + if (FTP_NATIVE) { + if (($ret = @ftp_systype($this->_conn)) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + } else { + // Send print working directory command and verify success + if (!$this->_putCmd('SYST', 215)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 215), Log::WARNING, 'jerror'); + + return false; + } + + $ret = $this->_response; + } + + // Match the system string to an OS + if (strpos(strtoupper($ret), 'MAC') !== false) { + $ret = 'MAC'; + } elseif (strpos(strtoupper($ret), 'WIN') !== false) { + $ret = 'WIN'; + } else { + $ret = 'UNIX'; + } + + // Return the os type + return $ret; + } + + /** + * Method to change the current working directory on the FTP server + * + * @param string $path Path to change into on the server + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function chdir($path) + { + // If native FTP support is enabled lets use it... + if (FTP_NATIVE) { + if (@ftp_chdir($this->_conn, $path) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + // Send change directory command and verify success + if (!$this->_putCmd('CWD ' . $path, 250)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 250, $path), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to reinitialise the server, ie. need to login again + * + * NOTE: This command not available on all servers + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function reinit() + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (@ftp_site($this->_conn, 'REIN') === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + // Send reinitialise command to the server + if (!$this->_putCmd('REIN', 220)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE', __METHOD__, $this->_response, 220), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to rename a file/folder on the FTP server + * + * @param string $from Path to change file/folder from + * @param string $to Path to change file/folder to + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function rename($from, $to) + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (@ftp_rename($this->_conn, $from, $to) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + // Send rename from command to the server + if (!$this->_putCmd('RNFR ' . $from, 350)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_RENAME_BAD_RESPONSE_FROM', __METHOD__, $this->_response, $from), Log::WARNING, 'jerror'); + + return false; + } + + // Send rename to command to the server + if (!$this->_putCmd('RNTO ' . $to, 250)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_RENAME_BAD_RESPONSE_TO', __METHOD__, $this->_response, $to), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to change mode for a path on the FTP server + * + * @param string $path Path to change mode on + * @param mixed $mode Octal value to change mode to, e.g. '0777', 0777 or 511 (string or integer) + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function chmod($path, $mode) + { + // If no filename is given, we assume the current directory is the target + if ($path == '') { + $path = '.'; + } + + // Convert the mode to a string + if (\is_int($mode)) { + $mode = decoct($mode); + } + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (@ftp_site($this->_conn, 'CHMOD ' . $mode . ' ' . $path) === false) { + if (!IS_WIN) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + } + + return false; + } + + return true; + } + + // Send change mode command and verify success [must convert mode from octal] + if (!$this->_putCmd('SITE CHMOD ' . $mode . ' ' . $path, array(200, 250))) { + if (!IS_WIN) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_CHMOD_BAD_RESPONSE', __METHOD__, $this->_response, $path, $mode), Log::WARNING, 'jerror'); + } + + return false; + } + + return true; + } + + /** + * Method to delete a path [file/folder] on the FTP server + * + * @param string $path Path to delete + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function delete($path) + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (@ftp_delete($this->_conn, $path) === false) { + if (@ftp_rmdir($this->_conn, $path) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + } + + return true; + } + + // Send delete file command and if that doesn't work, try to remove a directory + if (!$this->_putCmd('DELE ' . $path, 250)) { + if (!$this->_putCmd('RMD ' . $path, 250)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 250, $path), Log::WARNING, 'jerror'); + + return false; + } + } + + return true; + } + + /** + * Method to create a directory on the FTP server + * + * @param string $path Directory to create + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function mkdir($path) + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (@ftp_mkdir($this->_conn, $path) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + // Send change directory command and verify success + if (!$this->_putCmd('MKD ' . $path, 257)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 257, $path), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to restart data transfer at a given byte + * + * @param integer $point Byte to restart transfer at + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function restart($point) + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + if (@ftp_site($this->_conn, 'REST ' . $point) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + // Send restart command and verify success + if (!$this->_putCmd('REST ' . $point, 350)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_RESTART_BAD_RESPONSE', __METHOD__, $this->_response, $point), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to create an empty file on the FTP server + * + * @param string $path Path local file to store on the FTP server + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function create($path) + { + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + $buffer = fopen('buffer://tmp', 'r'); + + if (@ftp_fput($this->_conn, $path, $buffer, FTP_ASCII) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + fclose($buffer); + + return false; + } + + fclose($buffer); + + return true; + } + + // Start passive mode + if (!$this->_passive()) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (!$this->_putCmd('STOR ' . $path, array(150, 125))) { + @ fclose($this->_dataconn); + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); + + return false; + } + + // To create a zero byte upload close the data port connection + fclose($this->_dataconn); + + if (!$this->_verifyResponse(226)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to read a file from the FTP server's contents into a buffer + * + * @param string $remote Path to remote file to read on the FTP server + * @param string &$buffer Buffer variable to read file contents into + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function read($remote, &$buffer) + { + // Determine file type + $mode = $this->_findMode($remote); + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + $tmp = fopen('buffer://tmp', 'br+'); + + if (@ftp_fget($this->_conn, $tmp, $remote, $mode) === false) { + fclose($tmp); + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + // Read tmp buffer contents + rewind($tmp); + $buffer = ''; + + while (!feof($tmp)) { + $buffer .= fread($tmp, 8192); + } + + fclose($tmp); + + return true; + } + + $this->_mode($mode); + + // Start passive mode + if (!$this->_passive()) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (!$this->_putCmd('RETR ' . $remote, array(150, 125))) { + @ fclose($this->_dataconn); + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + + return false; + } + + // Read data from data port connection and add to the buffer + $buffer = ''; + + while (!feof($this->_dataconn)) { + $buffer .= fread($this->_dataconn, 4096); + } + + // Close the data port connection + fclose($this->_dataconn); + + // Let's try to cleanup some line endings if it is ascii + if ($mode == FTP_ASCII) { + $os = 'UNIX'; + + if (IS_WIN) { + $os = 'WIN'; + } + + $buffer = preg_replace('/' . CRLF . '/', $this->_lineEndings[$os], $buffer); + } + + if (!$this->_verifyResponse(226)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to get a file from the FTP server and save it to a local file + * + * @param string $local Local path to save remote file to + * @param string $remote Path to remote file to get on the FTP server + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function get($local, $remote) + { + // Determine file type + $mode = $this->_findMode($remote); + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (@ftp_get($this->_conn, $local, $remote, $mode) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + $this->_mode($mode); + + // Check to see if the local file can be opened for writing + $fp = fopen($local, 'wb'); + + if (!$fp) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_LOCAL_FILE_OPEN_WRITING', __METHOD__, $local), Log::WARNING, 'jerror'); + + return false; + } + + // Start passive mode + if (!$this->_passive()) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (!$this->_putCmd('RETR ' . $remote, array(150, 125))) { + @ fclose($this->_dataconn); + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + + return false; + } + + // Read data from data port connection and add to the buffer + while (!feof($this->_dataconn)) { + $buffer = fread($this->_dataconn, 4096); + fwrite($fp, $buffer, 4096); + } + + // Close the data port connection and file pointer + fclose($this->_dataconn); + fclose($fp); + + if (!$this->_verifyResponse(226)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to store a file to the FTP server + * + * @param string $local Path to local file to store on the FTP server + * @param string $remote FTP path to file to create + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function store($local, $remote = null) + { + // If remote file is not given, use the filename of the local file in the current + // working directory. + if ($remote == null) { + $remote = basename($local); + } + + // Determine file type + $mode = $this->_findMode($remote); + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (@ftp_put($this->_conn, $remote, $local, $mode) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + $this->_mode($mode); + + // Check to see if the local file exists and if so open it for reading + if (@ file_exists($local)) { + $fp = fopen($local, 'rb'); + + if (!$fp) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_LOCAL_FILE_OPEN_READING', __METHOD__, $local), Log::WARNING, 'jerror'); + + return false; + } + } else { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_LOCAL_FILE_FIND', __METHOD__, $local), Log::WARNING, 'jerror'); + + return false; + } + + // Start passive mode + if (!$this->_passive()) { + @ fclose($fp); + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + // Send store command to the FTP server + if (!$this->_putCmd('STOR ' . $remote, array(150, 125))) { + @ fclose($fp); + @ fclose($this->_dataconn); + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + + return false; + } + + // Do actual file transfer, read local file and write to data port connection + while (!feof($fp)) { + $line = fread($fp, 4096); + + do { + if (($result = @ fwrite($this->_dataconn, $line)) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_DATA_PORT', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + $line = substr($line, $result); + } while ($line != ''); + } + + fclose($fp); + fclose($this->_dataconn); + + if (!$this->_verifyResponse(226)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to write a string to the FTP server + * + * @param string $remote FTP path to file to write to + * @param string $buffer Contents to write to the FTP server + * + * @return boolean True if successful + * + * @since 1.5 + */ + public function write($remote, $buffer) + { + // Determine file type + $mode = $this->_findMode($remote); + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + $tmp = fopen('buffer://tmp', 'br+'); + fwrite($tmp, $buffer); + rewind($tmp); + + if (@ftp_fput($this->_conn, $remote, $tmp, $mode) === false) { + fclose($tmp); + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + fclose($tmp); + + return true; + } + + // First we need to set the transfer mode + $this->_mode($mode); + + // Start passive mode + if (!$this->_passive()) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + // Send store command to the FTP server + if (!$this->_putCmd('STOR ' . $remote, array(150, 125))) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + @ fclose($this->_dataconn); + + return false; + } + + // Write buffer to the data connection port + do { + if (($result = @ fwrite($this->_dataconn, $buffer)) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_DATA_PORT', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + $buffer = substr($buffer, $result); + } while ($buffer != ''); + + // Close the data connection port [Data transfer complete] + fclose($this->_dataconn); + + // Verify that the server received the transfer + if (!$this->_verifyResponse(226)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), Log::WARNING, 'jerror'); + + return false; + } + + return true; + } + + /** + * Method to append a string to the FTP server + * + * @param string $remote FTP path to file to append to + * @param string $buffer Contents to append to the FTP server + * + * @return boolean True if successful + * + * @since 3.6.0 + */ + public function append($remote, $buffer) + { + // Determine file type + $mode = $this->_findMode($remote); + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), 36); + } + + $tmp = fopen('buffer://tmp', 'bw+'); + fwrite($tmp, $buffer); + rewind($tmp); + + $size = $this->size($remote); + + if ($size === false) { + } + + if (@ftp_fput($this->_conn, $remote, $tmp, $mode, $size) === false) { + fclose($tmp); + + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), 35); + } + + fclose($tmp); + + return true; + } + + // First we need to set the transfer mode + $this->_mode($mode); + + // Start passive mode + if (!$this->_passive()) { + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), 36); + } + + // Send store command to the FTP server + if (!$this->_putCmd('APPE ' . $remote, array(150, 125))) { + @fclose($this->_dataconn); + + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $remote), 35); + } + + // Write buffer to the data connection port + do { + if (($result = @ fwrite($this->_dataconn, $buffer)) === false) { + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_DATA_PORT', __METHOD__), 37); + } + + $buffer = substr($buffer, $result); + } while ($buffer != ''); + + // Close the data connection port [Data transfer complete] + fclose($this->_dataconn); + + // Verify that the server received the transfer + if (!$this->_verifyResponse(226)) { + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $remote), 37); + } + + return true; + } + + /** + * Get the size of the remote file. + * + * @param string $remote FTP path to file whose size to get + * + * @return mixed number of bytes or false on error + * + * @since 3.6.0 + */ + public function size($remote) + { + if (FTP_NATIVE) { + $size = ftp_size($this->_conn, $remote); + + // In case ftp_size fails, try the SIZE command directly. + if ($size === -1) { + $response = ftp_raw($this->_conn, 'SIZE ' . $remote); + $responseCode = substr($response[0], 0, 3); + $responseMessage = substr($response[0], 4); + + if ($responseCode != '213') { + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), 35); + } + + $size = (int) $responseMessage; + } + + return $size; + } + + // Start passive mode + if (!$this->_passive()) { + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), 36); + } + + // Send size command to the FTP server + if (!$this->_putCmd('SIZE ' . $remote, array(213))) { + @fclose($this->_dataconn); + + throw new \RuntimeException(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_PATH_SENT', __METHOD__, $this->_response, 213, $remote), 35); + } + + return (int) substr($this->_responseMsg, 4); + } + + /** + * Method to list the filenames of the contents of a directory on the FTP server + * + * Note: Some servers also return folder names. However, to be sure to list folders on all + * servers, you should use listDetails() instead if you also need to deal with folders + * + * @param string $path Path local file to store on the FTP server + * + * @return string Directory listing + * + * @since 1.5 + */ + public function listNames($path = null) + { + $data = null; + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (($list = @ftp_nlist($this->_conn, $path)) === false) { + // Workaround for empty directories on some servers + if ($this->listDetails($path, 'files') === array()) { + return array(); + } + + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + $list = preg_replace('#^' . preg_quote($path, '#') . '[/\\\\]?#', '', $list); + + if ($keys = array_merge(array_keys($list, '.'), array_keys($list, '..'))) { + foreach ($keys as $key) { + unset($list[$key]); + } + } + + return $list; + } + + // If a path exists, prepend a space + if ($path != null) { + $path = ' ' . $path; + } + + // Start passive mode + if (!$this->_passive()) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (!$this->_putCmd('NLST' . $path, array(150, 125))) { + @ fclose($this->_dataconn); + + // Workaround for empty directories on some servers + if ($this->listDetails($path, 'files') === array()) { + return array(); + } + + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); + + return false; + } + + // Read in the file listing. + while (!feof($this->_dataconn)) { + $data .= fread($this->_dataconn, 4096); + } + + fclose($this->_dataconn); + + // Everything go okay? + if (!$this->_verifyResponse(226)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); + + return false; + } + + $data = preg_split('/[' . CRLF . ']+/', $data, -1, PREG_SPLIT_NO_EMPTY); + $data = preg_replace('#^' . preg_quote(substr($path, 1), '#') . '[/\\\\]?#', '', $data); + + if ($keys = array_merge(array_keys($data, '.'), array_keys($data, '..'))) { + foreach ($keys as $key) { + unset($data[$key]); + } + } + + return $data; + } + + /** + * Method to list the contents of a directory on the FTP server + * + * @param string $path Path to the local file to be stored on the FTP server + * @param string $type Return type [raw|all|folders|files] + * + * @return mixed If $type is raw: string Directory listing, otherwise array of string with file-names + * + * @since 1.5 + */ + public function listDetails($path = null, $type = 'all') + { + $dir_list = array(); + $data = null; + $regs = null; + + // @todo: Deal with recurse -- nightmare + // For now we will just set it to false + $recurse = false; + + // If native FTP support is enabled let's use it... + if (FTP_NATIVE) { + // Turn passive mode on + if (@ftp_pasv($this->_conn, true) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + if (($contents = @ftp_rawlist($this->_conn, $path)) === false) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_BAD_RESPONSE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + } else { + // Non Native mode + + // Start passive mode + if (!$this->_passive()) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + // If a path exists, prepend a space + if ($path != null) { + $path = ' ' . $path; + } + + // Request the file listing + if (!$this->_putCmd(($recurse == true) ? 'LIST -R' : 'LIST' . $path, array(150, 125))) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NOT_EXPECTED_RESPONSE_150_125', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); + @ fclose($this->_dataconn); + + return false; + } + + // Read in the file listing. + while (!feof($this->_dataconn)) { + $data .= fread($this->_dataconn, 4096); + } + + fclose($this->_dataconn); + + // Everything go okay? + if (!$this->_verifyResponse(226)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TRANSFER_FAILED', __METHOD__, $this->_response, $path), Log::WARNING, 'jerror'); + + return false; + } + + $contents = explode(CRLF, $data); + } + + // If only raw output is requested we are done + if ($type === 'raw') { + return $data; + } + + // If we received the listing of an empty directory, we are done as well + if (empty($contents[0])) { + return $dir_list; + } + + // If the server returned the number of results in the first response, let's dump it + if (strtolower(substr($contents[0], 0, 6)) === 'total ') { + array_shift($contents); + + if (!isset($contents[0]) || empty($contents[0])) { + return $dir_list; + } + } + + // Regular expressions for the directory listing parsing. + $regexps = array( + 'UNIX' => '#([-dl][rwxstST-]+).* ([0-9]*) ([a-zA-Z0-9]+).* ([a-zA-Z0-9]+).* ([0-9]*)' + . ' ([a-zA-Z]+[0-9: ]*[0-9])[ ]+(([0-9]{1,2}:[0-9]{2})|[0-9]{4}) (.+)#', + 'MAC' => '#([-dl][rwxstST-]+).* ?([0-9 ]*)?([a-zA-Z0-9]+).* ([a-zA-Z0-9]+).* ([0-9]*)' + . ' ([a-zA-Z]+[0-9: ]*[0-9])[ ]+(([0-9]{2}:[0-9]{2})|[0-9]{4}) (.+)#', + 'WIN' => '#([0-9]{2})-([0-9]{2})-([0-9]{2}) +([0-9]{2}):([0-9]{2})(AM|PM) +([0-9]+|) +(.+)#', + ); + + // Find out the format of the directory listing by matching one of the regexps + $osType = null; + + foreach ($regexps as $k => $v) { + if (@preg_match($v, $contents[0])) { + $osType = $k; + $regexp = $v; + break; + } + } + + if (!$osType) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_UNRECOGNISED_FOLDER_LISTING_FORMATJLIB_CLIENT_ERROR_JFTP_LISTDETAILS_UNRECOGNISED', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + // Here is where it is going to get dirty.... + if ($osType === 'UNIX' || $osType === 'MAC') { + foreach ($contents as $file) { + $tmp_array = null; + + if (@preg_match($regexp, $file, $regs)) { + $fType = (int) strpos('-dl', $regs[1][0]); + + // $tmp_array['line'] = $regs[0]; + $tmp_array['type'] = $fType; + $tmp_array['rights'] = $regs[1]; + + // $tmp_array['number'] = $regs[2]; + $tmp_array['user'] = $regs[3]; + $tmp_array['group'] = $regs[4]; + $tmp_array['size'] = $regs[5]; + $tmp_array['date'] = @date('m-d', strtotime($regs[6])); + $tmp_array['time'] = $regs[7]; + $tmp_array['name'] = $regs[9]; + } + + // If we just want files, do not add a folder + if ($type === 'files' && $tmp_array['type'] == 1) { + continue; + } + + // If we just want folders, do not add a file + if ($type === 'folders' && $tmp_array['type'] == 0) { + continue; + } + + if (\is_array($tmp_array) && $tmp_array['name'] != '.' && $tmp_array['name'] != '..') { + $dir_list[] = $tmp_array; + } + } + } else { + foreach ($contents as $file) { + $tmp_array = null; + + if (@preg_match($regexp, $file, $regs)) { + $fType = (int) ($regs[7] === ''); + $timestamp = strtotime("$regs[3]-$regs[1]-$regs[2] $regs[4]:$regs[5]$regs[6]"); + + // $tmp_array['line'] = $regs[0]; + $tmp_array['type'] = $fType; + $tmp_array['rights'] = ''; + + // $tmp_array['number'] = 0; + $tmp_array['user'] = ''; + $tmp_array['group'] = ''; + $tmp_array['size'] = (int) $regs[7]; + $tmp_array['date'] = date('m-d', $timestamp); + $tmp_array['time'] = date('H:i', $timestamp); + $tmp_array['name'] = $regs[8]; + } + + // If we just want files, do not add a folder + if ($type === 'files' && $tmp_array['type'] == 1) { + continue; + } + + // If we just want folders, do not add a file + if ($type === 'folders' && $tmp_array['type'] == 0) { + continue; + } + + if (\is_array($tmp_array) && $tmp_array['name'] != '.' && $tmp_array['name'] != '..') { + $dir_list[] = $tmp_array; + } + } + } + + return $dir_list; + } + + /** + * Send command to the FTP server and validate an expected response code + * + * @param string $cmd Command to send to the FTP server + * @param mixed $expectedResponse Integer response code or array of integer response codes + * + * @return boolean True if command executed successfully + * + * @since 1.5 + */ + protected function _putCmd($cmd, $expectedResponse) + { + // Make sure we have a connection to the server + if (!\is_resource($this->_conn)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PUTCMD_UNCONNECTED', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + // Send the command to the server + if (!fwrite($this->_conn, $cmd . "\r\n")) { + Log::add(Text::sprintf('DDD', Text::sprintf('JLIB_CLIENT_ERROR_FTP_PUTCMD_SEND', __METHOD__, $cmd)), Log::WARNING, 'jerror'); + } + + return $this->_verifyResponse($expectedResponse); + } + + /** + * Verify the response code from the server and log response if flag is set + * + * @param mixed $expected Integer response code or array of integer response codes + * + * @return boolean True if response code from the server is expected + * + * @since 1.5 + */ + protected function _verifyResponse($expected) + { + $parts = null; + + // Wait for a response from the server, but timeout after the set time limit + $endTime = time() + $this->_timeout; + $this->_response = ''; + + do { + $this->_response .= fgets($this->_conn, 4096); + } while (!preg_match('/^([0-9]{3})(-(.*' . CRLF . ')+\1)? [^' . CRLF . ']+' . CRLF . "$/", $this->_response, $parts) && time() < $endTime); + + // Catch a timeout or bad response + if (!isset($parts[1])) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TIMEOUT', __METHOD__, $this->_response), Log::WARNING, 'jerror'); + + return false; + } + + // Separate the code from the message + $this->_responseCode = $parts[1]; + $this->_responseMsg = $parts[0]; + + // Did the server respond with the code we wanted? + if (\is_array($expected)) { + if (\in_array($this->_responseCode, $expected)) { + $retval = true; + } else { + $retval = false; + } + } else { + if ($this->_responseCode == $expected) { + $retval = true; + } else { + $retval = false; + } + } + + return $retval; + } + + /** + * Set server to passive mode and open a data port connection + * + * @return boolean True if successful + * + * @since 1.5 + */ + protected function _passive() + { + $match = array(); + $parts = array(); + $errno = null; + $err = null; + + // Make sure we have a connection to the server + if (!\is_resource($this->_conn)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT', __METHOD__), Log::WARNING, 'jerror'); + + return false; + } + + // Request a passive connection - this means, we'll talk to you, you don't talk to us. + @ fwrite($this->_conn, "PASV\r\n"); + + // Wait for a response from the server, but timeout after the set time limit + $endTime = time() + $this->_timeout; + $this->_response = ''; + + do { + $this->_response .= fgets($this->_conn, 4096); + } while (!preg_match('/^([0-9]{3})(-(.*' . CRLF . ')+\1)? [^' . CRLF . ']+' . CRLF . "$/", $this->_response, $parts) && time() < $endTime); + + // Catch a timeout or bad response + if (!isset($parts[1])) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_TIMEOUT', __METHOD__, $this->_response), Log::WARNING, 'jerror'); + + return false; + } + + // Separate the code from the message + $this->_responseCode = $parts[1]; + $this->_responseMsg = $parts[0]; + + // If it's not 227, we weren't given an IP and port, which means it failed. + if ($this->_responseCode != '227') { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE_IP_OBTAIN', __METHOD__, $this->_responseMsg), Log::WARNING, 'jerror'); + + return false; + } + + // Snatch the IP and port information, or die horribly trying... + if (preg_match('~\((\d+),\s*(\d+),\s*(\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+))\)~', $this->_responseMsg, $match) == 0) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_PASSIVE_IP_VALID', __METHOD__, $this->_responseMsg), Log::WARNING, 'jerror'); + + return false; + } + + // This is pretty simple - store it for later use ;). + $this->_pasv = array('ip' => $match[1] . '.' . $match[2] . '.' . $match[3] . '.' . $match[4], 'port' => $match[5] * 256 + $match[6]); + + // Connect, assuming we've got a connection. + $this->_dataconn = @fsockopen($this->_pasv['ip'], $this->_pasv['port'], $errno, $err, $this->_timeout); + + if (!$this->_dataconn) { + Log::add( + Text::sprintf('JLIB_CLIENT_ERROR_FTP_NO_CONNECT', __METHOD__, $this->_pasv['ip'], $this->_pasv['port'], $errno, $err), + Log::WARNING, + 'jerror' + ); + + return false; + } + + // Set the timeout for this connection + socket_set_timeout($this->_conn, $this->_timeout, 0); + + return true; + } + + /** + * Method to find out the correct transfer mode for a specific file + * + * @param string $fileName Name of the file + * + * @return integer Transfer-mode for this filetype [FTP_ASCII|FTP_BINARY] + * + * @since 1.5 + */ + protected function _findMode($fileName) + { + if ($this->_type == FTP_AUTOASCII) { + $dot = strrpos($fileName, '.') + 1; + $ext = substr($fileName, $dot); + + if (\in_array($ext, $this->_autoAscii)) { + $mode = FTP_ASCII; + } else { + $mode = FTP_BINARY; + } + } elseif ($this->_type == FTP_ASCII) { + $mode = FTP_ASCII; + } else { + $mode = FTP_BINARY; + } + + return $mode; + } + + /** + * Set transfer mode + * + * @param integer $mode Integer representation of data transfer mode [1:Binary|0:Ascii] + * Defined constants can also be used [FTP_BINARY|FTP_ASCII] + * + * @return boolean True if successful + * + * @since 1.5 + */ + protected function _mode($mode) + { + if ($mode == FTP_BINARY) { + if (!$this->_putCmd('TYPE I', 200)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_MODE_BINARY', __METHOD__, $this->_response), Log::WARNING, 'jerror'); + + return false; + } + } else { + if (!$this->_putCmd('TYPE A', 200)) { + Log::add(Text::sprintf('JLIB_CLIENT_ERROR_FTP_MODE_ASCII', __METHOD__, $this->_response), Log::WARNING, 'jerror'); + + return false; + } + } + + return true; + } } diff --git a/libraries/src/Component/ComponentHelper.php b/libraries/src/Component/ComponentHelper.php index 9d8fcec6301b3..a00f0a2a99076 100644 --- a/libraries/src/Component/ComponentHelper.php +++ b/libraries/src/Component/ComponentHelper.php @@ -1,4 +1,5 @@ enabled = $strict ? false : true; - $result->setParams(new Registry); - - return $result; - } - - /** - * Checks if the component is enabled - * - * @param string $option The component option. - * - * @return boolean - * - * @since 1.5 - */ - public static function isEnabled($option) - { - $components = static::getComponents(); - - return isset($components[$option]) && $components[$option]->enabled; - } - - /** - * Checks if a component is installed - * - * @param string $option The component option. - * - * @return integer - * - * @since 3.4 - */ - public static function isInstalled($option) - { - $components = static::getComponents(); - - return isset($components[$option]) ? 1 : 0; - } - - /** - * Gets the parameter object for the component - * - * @param string $option The option for the component. - * @param boolean $strict If set and the component does not exist, false will be returned - * - * @return Registry A Registry object. - * - * @see Registry - * @since 1.5 - */ - public static function getParams($option, $strict = false) - { - return static::getComponent($option, $strict)->getParams(); - } - - /** - * Applies the global text filters to arbitrary text as per settings for current user groups - * - * @param string $text The string to filter - * - * @return string The filtered string - * - * @since 2.5 - */ - public static function filterText($text) - { - // Punyencoding utf8 email addresses - $text = InputFilter::getInstance()->emailToPunycode($text); - - // Filter settings - $config = static::getParams('com_config'); - $user = Factory::getUser(); - $userGroups = Access::getGroupsByUser($user->get('id')); - - $filters = $config->get('filters'); - - $forbiddenListTags = array(); - $forbiddenListAttributes = array(); - - $customListTags = array(); - $customListAttributes = array(); - - $allowedListTags = array(); - $allowedListAttributes = array(); - - $allowedList = false; - $forbiddenList = false; - $customList = false; - $unfiltered = false; - - // Cycle through each of the user groups the user is in. - // Remember they are included in the Public group as well. - foreach ($userGroups as $groupId) - { - // May have added a group by not saved the filters. - if (!isset($filters->$groupId)) - { - continue; - } - - // Each group the user is in could have different filtering properties. - $filterData = $filters->$groupId; - $filterType = strtoupper($filterData->filter_type); - - if ($filterType === 'NH') - { - // Maximum HTML filtering. - } - elseif ($filterType === 'NONE') - { - // No HTML filtering. - $unfiltered = true; - } - else - { - // Forbidden list or allowed list. - // Preprocess the tags and attributes. - $tags = explode(',', $filterData->filter_tags); - $attributes = explode(',', $filterData->filter_attributes); - $tempTags = array(); - $tempAttributes = array(); - - foreach ($tags as $tag) - { - $tag = trim($tag); - - if ($tag) - { - $tempTags[] = $tag; - } - } - - foreach ($attributes as $attribute) - { - $attribute = trim($attribute); - - if ($attribute) - { - $tempAttributes[] = $attribute; - } - } - - // Collect the forbidden list or allowed list tags and attributes. - // Each list is cumulative. - if ($filterType === 'BL') - { - $forbiddenList = true; - $forbiddenListTags = array_merge($forbiddenListTags, $tempTags); - $forbiddenListAttributes = array_merge($forbiddenListAttributes, $tempAttributes); - } - elseif ($filterType === 'CBL') - { - // Only set to true if Tags or Attributes were added - if ($tempTags || $tempAttributes) - { - $customList = true; - $customListTags = array_merge($customListTags, $tempTags); - $customListAttributes = array_merge($customListAttributes, $tempAttributes); - } - } - elseif ($filterType === 'WL') - { - $allowedList = true; - $allowedListTags = array_merge($allowedListTags, $tempTags); - $allowedListAttributes = array_merge($allowedListAttributes, $tempAttributes); - } - } - } - - // Remove duplicates before processing (because the forbidden list uses both sets of arrays). - $forbiddenListTags = array_unique($forbiddenListTags); - $forbiddenListAttributes = array_unique($forbiddenListAttributes); - $customListTags = array_unique($customListTags); - $customListAttributes = array_unique($customListAttributes); - $allowedListTags = array_unique($allowedListTags); - $allowedListAttributes = array_unique($allowedListAttributes); - - if (!$unfiltered) - { - // Custom Forbidden list precedes Default forbidden list. - if ($customList) - { - $filter = InputFilter::getInstance(array(), array(), 1, 1); - - // Override filter's default forbidden tags and attributes - if ($customListTags) - { - $filter->blockedTags = $customListTags; - } - - if ($customListAttributes) - { - $filter->blockedAttributes = $customListAttributes; - } - } - // Forbidden list takes second precedence. - elseif ($forbiddenList) - { - // Remove the allowed tags and attributes from the forbidden list. - $forbiddenListTags = array_diff($forbiddenListTags, $allowedListTags); - $forbiddenListAttributes = array_diff($forbiddenListAttributes, $allowedListAttributes); - - $filter = InputFilter::getInstance( - $forbiddenListTags, - $forbiddenListAttributes, - InputFilter::ONLY_BLOCK_DEFINED_TAGS, - InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES - ); - - // Remove the allowed tags from filter's default forbidden list. - if ($allowedListTags) - { - $filter->blockedTags = array_diff($filter->blockedTags, $allowedListTags); - } - - // Remove the allowed attributes from filter's default forbidden list. - if ($allowedListAttributes) - { - $filter->blockedAttributes = array_diff($filter->blockedAttributes, $allowedListAttributes); - } - } - // Allowed lists take third precedence. - elseif ($allowedList) - { - // Turn off XSS auto clean - $filter = InputFilter::getInstance($allowedListTags, $allowedListAttributes, 0, 0, 0); - } - // No HTML takes last place. - else - { - $filter = InputFilter::getInstance(); - } - - $text = $filter->clean($text, 'html'); - } - - return $text; - } - - /** - * Render the component. - * - * @param string $option The component option. - * @param array $params The component parameters - * - * @return string - * - * @since 1.5 - * @throws MissingComponentException - */ - public static function renderComponent($option, $params = array()) - { - $app = Factory::getApplication(); - $lang = Factory::getLanguage(); - - if (!$app->isClient('api')) - { - // Load template language files. - $template = $app->getTemplate(true)->template; - $lang->load('tpl_' . $template, JPATH_BASE) - || $lang->load('tpl_' . $template, JPATH_THEMES . "/$template"); - } - - if (empty($option)) - { - throw new MissingComponentException(Text::_('JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND'), 404); - } - - if (JDEBUG) - { - Profiler::getInstance('Application')->mark('beforeRenderComponent ' . $option); - } - - // Record the scope - $scope = $app->scope; - - // Set scope to component name - $app->scope = $option; - - // Build the component path. - $option = preg_replace('/[^A-Z0-9_\.-]/i', '', $option); - - // Define component path. - - if (!\defined('JPATH_COMPONENT')) - { - /** - * Defines the path to the active component for the request - * - * Note this constant is application aware and is different for each application (site/admin). - * - * @var string - * @since 1.5 - * @deprecated 5.0 without replacement - */ - \define('JPATH_COMPONENT', JPATH_BASE . '/components/' . $option); - } - - if (!\defined('JPATH_COMPONENT_SITE')) - { - /** - * Defines the path to the site element of the active component for the request - * - * @var string - * @since 1.5 - * @deprecated 5.0 without replacement - */ - \define('JPATH_COMPONENT_SITE', JPATH_SITE . '/components/' . $option); - } - - if (!\defined('JPATH_COMPONENT_ADMINISTRATOR')) - { - /** - * Defines the path to the admin element of the active component for the request - * - * @var string - * @since 1.5 - * @deprecated 5.0 without replacement - */ - \define('JPATH_COMPONENT_ADMINISTRATOR', JPATH_ADMINISTRATOR . '/components/' . $option); - } - - // If component is disabled throw error - if (!static::isEnabled($option)) - { - throw new MissingComponentException(Text::_('JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND'), 404); - } - - ob_start(); - $app->bootComponent($option)->getDispatcher($app)->dispatch(); - $contents = ob_get_clean(); - - // Revert the scope - $app->scope = $scope; - - if (JDEBUG) - { - Profiler::getInstance('Application')->mark('afterRenderComponent ' . $option); - } - - return $contents; - } - - /** - * Load the installed components into the components property. - * - * @return boolean True on success - * - * @since 3.2 - */ - protected static function load() - { - $loader = function () - { - $db = Factory::getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName(['extension_id', 'element', 'params', 'enabled'], ['id', 'option', null, null])) - ->from($db->quoteName('#__extensions')) - ->where( - [ - $db->quoteName('type') . ' = ' . $db->quote('component'), - $db->quoteName('state') . ' = 0', - $db->quoteName('enabled') . ' = 1', - ] - ); - - $components = []; - $db->setQuery($query); - - foreach ($db->getIterator() as $component) - { - $components[$component->option] = new ComponentRecord((array) $component); - } - - return $components; - }; - - /** @var CallbackController $cache */ - $cache = Factory::getContainer()->get(CacheControllerFactoryInterface::class)->createCacheController('callback', ['defaultgroup' => '_system']); - - try - { - static::$components = $cache->get($loader, array(), __METHOD__); - } - catch (CacheExceptionInterface $e) - { - static::$components = $loader(); - } - - return true; - } - - /** - * Get installed components - * - * @return ComponentRecord[] The components property - * - * @since 3.6.3 - */ - public static function getComponents() - { - if (empty(static::$components)) - { - static::load(); - } - - 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 4.0.0 - */ - public static function getComponentName($object, string $alternativeName): string - { - $reflect = new \ReflectionClass($object); - - if (!$reflect->getNamespaceName() || \get_class($object) === ComponentDispatcher::class || \get_class($object) === ApiDispatcher::class) - { - 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)); - } + /** + * The component list cache + * + * @var ComponentRecord[] + * @since 1.6 + */ + protected static $components = array(); + + /** + * Get the component information. + * + * @param string $option The component option. + * @param boolean $strict If set and the component does not exist, the enabled attribute will be set to false. + * + * @return ComponentRecord An object with the information for the component. + * + * @since 1.5 + */ + public static function getComponent($option, $strict = false) + { + $components = static::getComponents(); + + if (isset($components[$option])) { + return $components[$option]; + } + + $result = new ComponentRecord(); + $result->enabled = $strict ? false : true; + $result->setParams(new Registry()); + + return $result; + } + + /** + * Checks if the component is enabled + * + * @param string $option The component option. + * + * @return boolean + * + * @since 1.5 + */ + public static function isEnabled($option) + { + $components = static::getComponents(); + + return isset($components[$option]) && $components[$option]->enabled; + } + + /** + * Checks if a component is installed + * + * @param string $option The component option. + * + * @return integer + * + * @since 3.4 + */ + public static function isInstalled($option) + { + $components = static::getComponents(); + + return isset($components[$option]) ? 1 : 0; + } + + /** + * Gets the parameter object for the component + * + * @param string $option The option for the component. + * @param boolean $strict If set and the component does not exist, false will be returned + * + * @return Registry A Registry object. + * + * @see Registry + * @since 1.5 + */ + public static function getParams($option, $strict = false) + { + return static::getComponent($option, $strict)->getParams(); + } + + /** + * Applies the global text filters to arbitrary text as per settings for current user groups + * + * @param string $text The string to filter + * + * @return string The filtered string + * + * @since 2.5 + */ + public static function filterText($text) + { + // Punyencoding utf8 email addresses + $text = InputFilter::getInstance()->emailToPunycode($text); + + // Filter settings + $config = static::getParams('com_config'); + $user = Factory::getUser(); + $userGroups = Access::getGroupsByUser($user->get('id')); + + $filters = $config->get('filters'); + + $forbiddenListTags = array(); + $forbiddenListAttributes = array(); + + $customListTags = array(); + $customListAttributes = array(); + + $allowedListTags = array(); + $allowedListAttributes = array(); + + $allowedList = false; + $forbiddenList = false; + $customList = false; + $unfiltered = false; + + // Cycle through each of the user groups the user is in. + // Remember they are included in the Public group as well. + foreach ($userGroups as $groupId) { + // May have added a group by not saved the filters. + if (!isset($filters->$groupId)) { + continue; + } + + // Each group the user is in could have different filtering properties. + $filterData = $filters->$groupId; + $filterType = strtoupper($filterData->filter_type); + + if ($filterType === 'NH') { + // Maximum HTML filtering. + } elseif ($filterType === 'NONE') { + // No HTML filtering. + $unfiltered = true; + } else { + // Forbidden list or allowed list. + // Preprocess the tags and attributes. + $tags = explode(',', $filterData->filter_tags); + $attributes = explode(',', $filterData->filter_attributes); + $tempTags = array(); + $tempAttributes = array(); + + foreach ($tags as $tag) { + $tag = trim($tag); + + if ($tag) { + $tempTags[] = $tag; + } + } + + foreach ($attributes as $attribute) { + $attribute = trim($attribute); + + if ($attribute) { + $tempAttributes[] = $attribute; + } + } + + // Collect the forbidden list or allowed list tags and attributes. + // Each list is cumulative. + if ($filterType === 'BL') { + $forbiddenList = true; + $forbiddenListTags = array_merge($forbiddenListTags, $tempTags); + $forbiddenListAttributes = array_merge($forbiddenListAttributes, $tempAttributes); + } elseif ($filterType === 'CBL') { + // Only set to true if Tags or Attributes were added + if ($tempTags || $tempAttributes) { + $customList = true; + $customListTags = array_merge($customListTags, $tempTags); + $customListAttributes = array_merge($customListAttributes, $tempAttributes); + } + } elseif ($filterType === 'WL') { + $allowedList = true; + $allowedListTags = array_merge($allowedListTags, $tempTags); + $allowedListAttributes = array_merge($allowedListAttributes, $tempAttributes); + } + } + } + + // Remove duplicates before processing (because the forbidden list uses both sets of arrays). + $forbiddenListTags = array_unique($forbiddenListTags); + $forbiddenListAttributes = array_unique($forbiddenListAttributes); + $customListTags = array_unique($customListTags); + $customListAttributes = array_unique($customListAttributes); + $allowedListTags = array_unique($allowedListTags); + $allowedListAttributes = array_unique($allowedListAttributes); + + if (!$unfiltered) { + // Custom Forbidden list precedes Default forbidden list. + if ($customList) { + $filter = InputFilter::getInstance(array(), array(), 1, 1); + + // Override filter's default forbidden tags and attributes + if ($customListTags) { + $filter->blockedTags = $customListTags; + } + + if ($customListAttributes) { + $filter->blockedAttributes = $customListAttributes; + } + } elseif ($forbiddenList) { + // Forbidden list takes second precedence. + // Remove the allowed tags and attributes from the forbidden list. + $forbiddenListTags = array_diff($forbiddenListTags, $allowedListTags); + $forbiddenListAttributes = array_diff($forbiddenListAttributes, $allowedListAttributes); + + $filter = InputFilter::getInstance( + $forbiddenListTags, + $forbiddenListAttributes, + InputFilter::ONLY_BLOCK_DEFINED_TAGS, + InputFilter::ONLY_BLOCK_DEFINED_ATTRIBUTES + ); + + // Remove the allowed tags from filter's default forbidden list. + if ($allowedListTags) { + $filter->blockedTags = array_diff($filter->blockedTags, $allowedListTags); + } + + // Remove the allowed attributes from filter's default forbidden list. + if ($allowedListAttributes) { + $filter->blockedAttributes = array_diff($filter->blockedAttributes, $allowedListAttributes); + } + } elseif ($allowedList) { + // Allowed lists take third precedence. + // Turn off XSS auto clean + $filter = InputFilter::getInstance($allowedListTags, $allowedListAttributes, 0, 0, 0); + } else { + // No HTML takes last place. + $filter = InputFilter::getInstance(); + } + + $text = $filter->clean($text, 'html'); + } + + return $text; + } + + /** + * Render the component. + * + * @param string $option The component option. + * @param array $params The component parameters + * + * @return string + * + * @since 1.5 + * @throws MissingComponentException + */ + public static function renderComponent($option, $params = array()) + { + $app = Factory::getApplication(); + $lang = Factory::getLanguage(); + + if (!$app->isClient('api')) { + // Load template language files. + $template = $app->getTemplate(true)->template; + $lang->load('tpl_' . $template, JPATH_BASE) + || $lang->load('tpl_' . $template, JPATH_THEMES . "/$template"); + } + + if (empty($option)) { + throw new MissingComponentException(Text::_('JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND'), 404); + } + + if (JDEBUG) { + Profiler::getInstance('Application')->mark('beforeRenderComponent ' . $option); + } + + // Record the scope + $scope = $app->scope; + + // Set scope to component name + $app->scope = $option; + + // Build the component path. + $option = preg_replace('/[^A-Z0-9_\.-]/i', '', $option); + + // Define component path. + + if (!\defined('JPATH_COMPONENT')) { + /** + * Defines the path to the active component for the request + * + * Note this constant is application aware and is different for each application (site/admin). + * + * @var string + * @since 1.5 + * @deprecated 5.0 without replacement + */ + \define('JPATH_COMPONENT', JPATH_BASE . '/components/' . $option); + } + + if (!\defined('JPATH_COMPONENT_SITE')) { + /** + * Defines the path to the site element of the active component for the request + * + * @var string + * @since 1.5 + * @deprecated 5.0 without replacement + */ + \define('JPATH_COMPONENT_SITE', JPATH_SITE . '/components/' . $option); + } + + if (!\defined('JPATH_COMPONENT_ADMINISTRATOR')) { + /** + * Defines the path to the admin element of the active component for the request + * + * @var string + * @since 1.5 + * @deprecated 5.0 without replacement + */ + \define('JPATH_COMPONENT_ADMINISTRATOR', JPATH_ADMINISTRATOR . '/components/' . $option); + } + + // If component is disabled throw error + if (!static::isEnabled($option)) { + throw new MissingComponentException(Text::_('JLIB_APPLICATION_ERROR_COMPONENT_NOT_FOUND'), 404); + } + + ob_start(); + $app->bootComponent($option)->getDispatcher($app)->dispatch(); + $contents = ob_get_clean(); + + // Revert the scope + $app->scope = $scope; + + if (JDEBUG) { + Profiler::getInstance('Application')->mark('afterRenderComponent ' . $option); + } + + return $contents; + } + + /** + * Load the installed components into the components property. + * + * @return boolean True on success + * + * @since 3.2 + */ + protected static function load() + { + $loader = function () { + $db = Factory::getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName(['extension_id', 'element', 'params', 'enabled'], ['id', 'option', null, null])) + ->from($db->quoteName('#__extensions')) + ->where( + [ + $db->quoteName('type') . ' = ' . $db->quote('component'), + $db->quoteName('state') . ' = 0', + $db->quoteName('enabled') . ' = 1', + ] + ); + + $components = []; + $db->setQuery($query); + + foreach ($db->getIterator() as $component) { + $components[$component->option] = new ComponentRecord((array) $component); + } + + return $components; + }; + + /** @var CallbackController $cache */ + $cache = Factory::getContainer()->get(CacheControllerFactoryInterface::class)->createCacheController('callback', ['defaultgroup' => '_system']); + + try { + static::$components = $cache->get($loader, array(), __METHOD__); + } catch (CacheExceptionInterface $e) { + static::$components = $loader(); + } + + return true; + } + + /** + * Get installed components + * + * @return ComponentRecord[] The components property + * + * @since 3.6.3 + */ + public static function getComponents() + { + if (empty(static::$components)) { + static::load(); + } + + 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 4.0.0 + */ + public static function getComponentName($object, string $alternativeName): string + { + $reflect = new \ReflectionClass($object); + + if (!$reflect->getNamespaceName() || \get_class($object) === ComponentDispatcher::class || \get_class($object) === ApiDispatcher::class) { + 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/Component/ComponentRecord.php b/libraries/src/Component/ComponentRecord.php index b4389e00a015f..11cdfce354d38 100644 --- a/libraries/src/Component/ComponentRecord.php +++ b/libraries/src/Component/ComponentRecord.php @@ -1,4 +1,5 @@ $value) - { - $this->$key = $value; - } - } - - /** - * Method to get certain otherwise inaccessible properties from the form field object. - * - * @param string $name The property name for which to get the value. - * - * @return mixed The property value or null. - * - * @since 3.7.0 - * @deprecated 5.0 Access the item parameters through the `getParams()` method - */ - public function __get($name) - { - if ($name === 'params') - { - return $this->getParams(); - } - - return $this->$name; - } - - /** - * Method to set certain otherwise inaccessible properties of the form field object. - * - * @param string $name The property name for which to set the value. - * @param mixed $value The value of the property. - * - * @return void - * - * @since 3.7.0 - * @deprecated 5.0 Set the item parameters through the `setParams()` method - */ - public function __set($name, $value) - { - if ($name === 'params') - { - $this->setParams($value); - - return; - } - - $this->$name = $value; - } - - /** - * Returns the menu item parameters - * - * @return Registry - * - * @since 3.7.0 - */ - public function getParams() - { - if (!($this->params instanceof Registry)) - { - $this->params = new Registry($this->params); - } - - return $this->params; - } - - /** - * Sets the menu item parameters - * - * @param Registry|string $params The data to be stored as the parameters - * - * @return void - * - * @since 3.7.0 - */ - public function setParams($params) - { - $this->params = $params; - } + /** + * Primary key + * + * @var integer + * @since 3.7.0 + */ + public $id; + + /** + * The component name + * + * @var integer + * @since 3.7.0 + */ + public $option; + + /** + * The component parameters + * + * @var string|Registry + * @since 3.7.0 + * @note This field is protected to require reading this field to proxy through the getter to convert the params to a Registry instance + */ + protected $params; + + /** + * The extension namespace + * + * @var string + * @since 4.0.0 + */ + public $namespace; + + /** + * Indicates if this component is enabled + * + * @var integer + * @since 3.7.0 + */ + public $enabled; + + /** + * Class constructor + * + * @param array $data The component record data to load + * + * @since 3.7.0 + */ + public function __construct($data = array()) + { + foreach ((array) $data as $key => $value) { + $this->$key = $value; + } + } + + /** + * Method to get certain otherwise inaccessible properties from the form field object. + * + * @param string $name The property name for which to get the value. + * + * @return mixed The property value or null. + * + * @since 3.7.0 + * @deprecated 5.0 Access the item parameters through the `getParams()` method + */ + public function __get($name) + { + if ($name === 'params') { + return $this->getParams(); + } + + return $this->$name; + } + + /** + * Method to set certain otherwise inaccessible properties of the form field object. + * + * @param string $name The property name for which to set the value. + * @param mixed $value The value of the property. + * + * @return void + * + * @since 3.7.0 + * @deprecated 5.0 Set the item parameters through the `setParams()` method + */ + public function __set($name, $value) + { + if ($name === 'params') { + $this->setParams($value); + + return; + } + + $this->$name = $value; + } + + /** + * Returns the menu item parameters + * + * @return Registry + * + * @since 3.7.0 + */ + public function getParams() + { + if (!($this->params instanceof Registry)) { + $this->params = new Registry($this->params); + } + + return $this->params; + } + + /** + * Sets the menu item parameters + * + * @param Registry|string $params The data to be stored as the parameters + * + * @return void + * + * @since 3.7.0 + */ + public function setParams($params) + { + $this->params = $params; + } } diff --git a/libraries/src/Component/Exception/MissingComponentException.php b/libraries/src/Component/Exception/MissingComponentException.php index 67947b5b24000..b64d3edd30b55 100644 --- a/libraries/src/Component/Exception/MissingComponentException.php +++ b/libraries/src/Component/Exception/MissingComponentException.php @@ -1,4 +1,5 @@ app = $app; - } - else - { - $this->app = Factory::getApplication(); - } + /** + * Class constructor. + * + * @param \Joomla\CMS\Application\CMSApplication $app Application-object that the router should use + * @param \Joomla\CMS\Menu\AbstractMenu $menu Menu-object that the router should use + * + * @since 3.4 + */ + public function __construct($app = null, $menu = null) + { + if ($app) { + $this->app = $app; + } else { + $this->app = Factory::getApplication(); + } - if ($menu) - { - $this->menu = $menu; - } - else - { - $this->menu = $this->app->getMenu(); - } - } + if ($menu) { + $this->menu = $menu; + } else { + $this->menu = $this->app->getMenu(); + } + } - /** - * Generic method to preprocess a URL - * - * @param array $query An associative array of URL arguments - * - * @return array The URL arguments to use to assemble the subsequent URL. - * - * @since 3.3 - */ - public function preprocess($query) - { - return $query; - } + /** + * Generic method to preprocess a URL + * + * @param array $query An associative array of URL arguments + * + * @return array The URL arguments to use to assemble the subsequent URL. + * + * @since 3.3 + */ + public function preprocess($query) + { + return $query; + } } diff --git a/libraries/src/Component/Router/RouterFactory.php b/libraries/src/Component/Router/RouterFactory.php index eb82e1aa2aa66..3f88f612376b5 100644 --- a/libraries/src/Component/Router/RouterFactory.php +++ b/libraries/src/Component/Router/RouterFactory.php @@ -1,4 +1,5 @@ namespace = $namespace; - $this->categoryFactory = $categoryFactory; - $this->db = $db; - } + /** + * The namespace must be like: + * Joomla\Component\Content + * + * @param string $namespace The namespace + * @param CategoryFactoryInterface $categoryFactory The category object + * @param DatabaseInterface $db The database object + * + * @since 4.0.0 + */ + public function __construct($namespace, CategoryFactoryInterface $categoryFactory = null, DatabaseInterface $db = null) + { + $this->namespace = $namespace; + $this->categoryFactory = $categoryFactory; + $this->db = $db; + } - /** - * Creates a router. - * - * @param CMSApplicationInterface $application The application - * @param AbstractMenu $menu The menu object to work with - * - * @return RouterInterface - * - * @since 4.0.0 - */ - public function createRouter(CMSApplicationInterface $application, AbstractMenu $menu): RouterInterface - { - $className = trim($this->namespace, '\\') . '\\' . ucfirst($application->getName()) . '\\Service\\Router'; + /** + * Creates a router. + * + * @param CMSApplicationInterface $application The application + * @param AbstractMenu $menu The menu object to work with + * + * @return RouterInterface + * + * @since 4.0.0 + */ + public function createRouter(CMSApplicationInterface $application, AbstractMenu $menu): RouterInterface + { + $className = trim($this->namespace, '\\') . '\\' . ucfirst($application->getName()) . '\\Service\\Router'; - if (!class_exists($className)) - { - throw new \RuntimeException('No router available for this application.'); - } + if (!class_exists($className)) { + throw new \RuntimeException('No router available for this application.'); + } - return new $className($application, $menu, $this->categoryFactory, $this->db); - } + return new $className($application, $menu, $this->categoryFactory, $this->db); + } } diff --git a/libraries/src/Component/Router/RouterFactoryInterface.php b/libraries/src/Component/Router/RouterFactoryInterface.php index b1e6748a899ba..38c75c594528b 100644 --- a/libraries/src/Component/Router/RouterFactoryInterface.php +++ b/libraries/src/Component/Router/RouterFactoryInterface.php @@ -1,4 +1,5 @@ component = $component; - } + /** + * Constructor + * + * @param string $component Component name without the com_ prefix this router should react upon + * + * @since 3.3 + */ + public function __construct($component) + { + $this->component = $component; + } - /** - * Generic preprocess function for missing or legacy component router - * - * @param array $query An associative array of URL arguments - * - * @return array The URL arguments to use to assemble the subsequent URL. - * - * @since 3.3 - */ - public function preprocess($query) - { - return $query; - } + /** + * Generic preprocess function for missing or legacy component router + * + * @param array $query An associative array of URL arguments + * + * @return array The URL arguments to use to assemble the subsequent URL. + * + * @since 3.3 + */ + public function preprocess($query) + { + return $query; + } - /** - * Generic build function for missing or legacy component router - * - * @param array &$query An array of URL arguments - * - * @return array The URL arguments to use to assemble the subsequent URL. - * - * @since 3.3 - */ - public function build(&$query) - { - $function = $this->component . 'BuildRoute'; + /** + * Generic build function for missing or legacy component router + * + * @param array &$query An array of URL arguments + * + * @return array The URL arguments to use to assemble the subsequent URL. + * + * @since 3.3 + */ + public function build(&$query) + { + $function = $this->component . 'BuildRoute'; - if (\function_exists($function)) - { - $segments = $function($query); - $total = \count($segments); + if (\function_exists($function)) { + $segments = $function($query); + $total = \count($segments); - for ($i = 0; $i < $total; $i++) - { - $segments[$i] = str_replace(':', '-', $segments[$i]); - } + for ($i = 0; $i < $total; $i++) { + $segments[$i] = str_replace(':', '-', $segments[$i]); + } - return $segments; - } + return $segments; + } - return array(); - } + return array(); + } - /** - * Generic parse function for missing or legacy component router - * - * @param array &$segments The segments of the URL to parse. - * - * @return array The URL attributes to be used by the application. - * - * @since 3.3 - */ - public function parse(&$segments) - { - $function = $this->component . 'ParseRoute'; + /** + * Generic parse function for missing or legacy component router + * + * @param array &$segments The segments of the URL to parse. + * + * @return array The URL attributes to be used by the application. + * + * @since 3.3 + */ + public function parse(&$segments) + { + $function = $this->component . 'ParseRoute'; - if (\function_exists($function)) - { - $total = \count($segments); + if (\function_exists($function)) { + $total = \count($segments); - for ($i = 0; $i < $total; $i++) - { - $segments[$i] = preg_replace('/-/', ':', $segments[$i], 1); - } + for ($i = 0; $i < $total; $i++) { + $segments[$i] = preg_replace('/-/', ':', $segments[$i], 1); + } - return $function($segments); - } + return $function($segments); + } - return array(); - } + return array(); + } } diff --git a/libraries/src/Component/Router/RouterServiceInterface.php b/libraries/src/Component/Router/RouterServiceInterface.php index f949a2352b32b..32c13926d9324 100644 --- a/libraries/src/Component/Router/RouterServiceInterface.php +++ b/libraries/src/Component/Router/RouterServiceInterface.php @@ -1,4 +1,5 @@ routerFactory->createRouter($application, $menu); - } + /** + * Returns the router. + * + * @param CMSApplicationInterface $application The application object + * @param AbstractMenu $menu The menu object to work with + * + * @return RouterInterface + * + * @since 4.0.0 + */ + public function createRouter(CMSApplicationInterface $application, AbstractMenu $menu): RouterInterface + { + return $this->routerFactory->createRouter($application, $menu); + } - /** - * The router factory. - * - * @param RouterFactoryInterface $routerFactory The router factory - * - * @return void - * - * @since 4.0.0 - */ - public function setRouterFactory(RouterFactoryInterface $routerFactory) - { - $this->routerFactory = $routerFactory; - } + /** + * The router factory. + * + * @param RouterFactoryInterface $routerFactory The router factory + * + * @return void + * + * @since 4.0.0 + */ + public function setRouterFactory(RouterFactoryInterface $routerFactory) + { + $this->routerFactory = $routerFactory; + } } diff --git a/libraries/src/Component/Router/RouterView.php b/libraries/src/Component/Router/RouterView.php index 60a22272dc121..5c6cb2fb2ce45 100644 --- a/libraries/src/Component/Router/RouterView.php +++ b/libraries/src/Component/Router/RouterView.php @@ -1,4 +1,5 @@ views[$view->name] = $view; - } - - /** - * Return an array of registered view objects - * - * @return RouterViewConfiguration[] Array of registered view objects - * - * @since 3.5 - */ - public function getViews() - { - return $this->views; - } - - /** - * Get the path of views from target view to root view - * including content items of a nestable view - * - * @param array $query Array of query elements - * - * @return array List of views including IDs of content items - * - * @since 3.5 - */ - public function getPath($query) - { - $views = $this->getViews(); - $result = array(); - - // Get the right view object - if (isset($query['view']) && isset($views[$query['view']])) - { - $viewobj = $views[$query['view']]; - } - - // Get the path from the current item to the root view with all IDs - if (isset($viewobj)) - { - $path = array_reverse($viewobj->path); - $start = true; - $childkey = false; - - foreach ($path as $element) - { - $view = $views[$element]; - - if ($start) - { - $key = $view->key; - $start = false; - } - else - { - $key = $childkey; - } - - $childkey = $view->parent_key; - - if (($key || $view->key) && \is_callable(array($this, 'get' . ucfirst($view->name) . 'Segment'))) - { - if (isset($query[$key])) - { - $result[$view->name] = \call_user_func_array(array($this, 'get' . ucfirst($view->name) . 'Segment'), array($query[$key], $query)); - } - elseif (isset($query[$view->key])) - { - $result[$view->name] = \call_user_func_array(array($this, 'get' . ucfirst($view->name) . 'Segment'), array($query[$view->key], $query)); - } - else - { - $result[$view->name] = array(); - } - } - else - { - $result[$view->name] = true; - } - } - } - - return $result; - } - - /** - * Get all currently attached rules - * - * @return RulesInterface[] All currently attached rules in an array - * - * @since 3.5 - */ - public function getRules() - { - return $this->rules; - } - - /** - * Add a number of router rules to the object - * - * @param RulesInterface[] $rules Array of JComponentRouterRulesInterface objects - * - * @return void - * - * @since 3.5 - */ - public function attachRules($rules) - { - foreach ($rules as $rule) - { - $this->attachRule($rule); - } - } - - /** - * Attach a build rule - * - * @param RulesInterface $rule The function to be called. - * - * @return void - * - * @since 3.5 - */ - public function attachRule(RulesInterface $rule) - { - $this->rules[] = $rule; - } - - /** - * Remove a build rule - * - * @param RulesInterface $rule The rule to be removed. - * - * @return boolean Was a rule removed? - * - * @since 3.5 - */ - public function detachRule(RulesInterface $rule) - { - foreach ($this->rules as $id => $r) - { - if ($r == $rule) - { - unset($this->rules[$id]); - - return true; - } - } - - return false; - } - - /** - * Generic method to preprocess a URL - * - * @param array $query An associative array of URL arguments - * - * @return array The URL arguments to use to assemble the subsequent URL. - * - * @since 3.5 - */ - public function preprocess($query) - { - // Process the parsed variables based on custom defined rules - foreach ($this->rules as $rule) - { - $rule->preprocess($query); - } - - return $query; - } - - /** - * Build method for URLs - * - * @param array &$query Array of query elements - * - * @return array Array of URL segments - * - * @since 3.5 - */ - public function build(&$query) - { - $segments = array(); - - // Process the parsed variables based on custom defined rules - foreach ($this->rules as $rule) - { - $rule->build($query, $segments); - } - - return $segments; - } - - /** - * Parse method for URLs - * - * @param array &$segments Array of URL string-segments - * - * @return array Associative array of query values - * - * @since 3.5 - */ - public function parse(&$segments) - { - $vars = array(); - - // Process the parsed variables based on custom defined rules - foreach ($this->rules as $rule) - { - $rule->parse($segments, $vars); - } - - return $vars; - } - - /** - * Method to return the name of the router - * - * @return string Name of the router - * - * @since 3.5 - */ - public function getName() - { - if (empty($this->name)) - { - $r = null; - - if (!preg_match('/(.*)Router/i', \get_class($this), $r)) - { - throw new \Exception('JLIB_APPLICATION_ERROR_ROUTER_GET_NAME', 500); - } - - $this->name = str_replace('com_', '', ComponentHelper::getComponentName($this, strtolower($r[1]))); - } - - return $this->name; - } + /** + * Name of the router of the component + * + * @var string + * @since 3.5 + */ + protected $name; + + /** + * Array of rules + * + * @var RulesInterface[] + * @since 3.5 + */ + protected $rules = array(); + + /** + * Views of the component + * + * @var RouterViewConfiguration[] + * @since 3.5 + */ + protected $views = array(); + + /** + * Register the views of a component + * + * @param RouterViewConfiguration $view View configuration object + * + * @return void + * + * @since 3.5 + */ + public function registerView(RouterViewConfiguration $view) + { + $this->views[$view->name] = $view; + } + + /** + * Return an array of registered view objects + * + * @return RouterViewConfiguration[] Array of registered view objects + * + * @since 3.5 + */ + public function getViews() + { + return $this->views; + } + + /** + * Get the path of views from target view to root view + * including content items of a nestable view + * + * @param array $query Array of query elements + * + * @return array List of views including IDs of content items + * + * @since 3.5 + */ + public function getPath($query) + { + $views = $this->getViews(); + $result = array(); + + // Get the right view object + if (isset($query['view']) && isset($views[$query['view']])) { + $viewobj = $views[$query['view']]; + } + + // Get the path from the current item to the root view with all IDs + if (isset($viewobj)) { + $path = array_reverse($viewobj->path); + $start = true; + $childkey = false; + + foreach ($path as $element) { + $view = $views[$element]; + + if ($start) { + $key = $view->key; + $start = false; + } else { + $key = $childkey; + } + + $childkey = $view->parent_key; + + if (($key || $view->key) && \is_callable(array($this, 'get' . ucfirst($view->name) . 'Segment'))) { + if (isset($query[$key])) { + $result[$view->name] = \call_user_func_array(array($this, 'get' . ucfirst($view->name) . 'Segment'), array($query[$key], $query)); + } elseif (isset($query[$view->key])) { + $result[$view->name] = \call_user_func_array(array($this, 'get' . ucfirst($view->name) . 'Segment'), array($query[$view->key], $query)); + } else { + $result[$view->name] = array(); + } + } else { + $result[$view->name] = true; + } + } + } + + return $result; + } + + /** + * Get all currently attached rules + * + * @return RulesInterface[] All currently attached rules in an array + * + * @since 3.5 + */ + public function getRules() + { + return $this->rules; + } + + /** + * Add a number of router rules to the object + * + * @param RulesInterface[] $rules Array of JComponentRouterRulesInterface objects + * + * @return void + * + * @since 3.5 + */ + public function attachRules($rules) + { + foreach ($rules as $rule) { + $this->attachRule($rule); + } + } + + /** + * Attach a build rule + * + * @param RulesInterface $rule The function to be called. + * + * @return void + * + * @since 3.5 + */ + public function attachRule(RulesInterface $rule) + { + $this->rules[] = $rule; + } + + /** + * Remove a build rule + * + * @param RulesInterface $rule The rule to be removed. + * + * @return boolean Was a rule removed? + * + * @since 3.5 + */ + public function detachRule(RulesInterface $rule) + { + foreach ($this->rules as $id => $r) { + if ($r == $rule) { + unset($this->rules[$id]); + + return true; + } + } + + return false; + } + + /** + * Generic method to preprocess a URL + * + * @param array $query An associative array of URL arguments + * + * @return array The URL arguments to use to assemble the subsequent URL. + * + * @since 3.5 + */ + public function preprocess($query) + { + // Process the parsed variables based on custom defined rules + foreach ($this->rules as $rule) { + $rule->preprocess($query); + } + + return $query; + } + + /** + * Build method for URLs + * + * @param array &$query Array of query elements + * + * @return array Array of URL segments + * + * @since 3.5 + */ + public function build(&$query) + { + $segments = array(); + + // Process the parsed variables based on custom defined rules + foreach ($this->rules as $rule) { + $rule->build($query, $segments); + } + + return $segments; + } + + /** + * Parse method for URLs + * + * @param array &$segments Array of URL string-segments + * + * @return array Associative array of query values + * + * @since 3.5 + */ + public function parse(&$segments) + { + $vars = array(); + + // Process the parsed variables based on custom defined rules + foreach ($this->rules as $rule) { + $rule->parse($segments, $vars); + } + + return $vars; + } + + /** + * Method to return the name of the router + * + * @return string Name of the router + * + * @since 3.5 + */ + public function getName() + { + if (empty($this->name)) { + $r = null; + + if (!preg_match('/(.*)Router/i', \get_class($this), $r)) { + throw new \Exception('JLIB_APPLICATION_ERROR_ROUTER_GET_NAME', 500); + } + + $this->name = str_replace('com_', '', ComponentHelper::getComponentName($this, strtolower($r[1]))); + } + + return $this->name; + } } diff --git a/libraries/src/Component/Router/Rules/MenuRules.php b/libraries/src/Component/Router/Rules/MenuRules.php index cd5254bc4925c..43546d193530b 100644 --- a/libraries/src/Component/Router/Rules/MenuRules.php +++ b/libraries/src/Component/Router/Rules/MenuRules.php @@ -1,4 +1,5 @@ router = $router; - - $this->buildLookup(); - } - - /** - * Finds the right Itemid for this query - * - * @param array &$query The query array to process - * - * @return void - * - * @since 3.4 - */ - public function preprocess(&$query) - { - $active = $this->router->menu->getActive(); - - /** - * If the active item id is not the same as the supplied item id or we have a supplied item id and no active - * menu item then we just use the supplied menu item and continue - */ - if (isset($query['Itemid']) && ($active === null || $query['Itemid'] != $active->id)) - { - return; - } - - // Get query language - $language = isset($query['lang']) ? $query['lang'] : '*'; - - // Set the language to the current one when multilang is enabled and item is tagged to ALL - if (Multilanguage::isEnabled() && $language === '*') - { - $language = $this->router->app->get('language'); - } - - if (!isset($this->lookup[$language])) - { - $this->buildLookup($language); - } - - // Check if the active menu item matches the requested query - if ($active !== null && isset($query['Itemid'])) - { - // Check if active->query and supplied query are the same - $match = true; - - foreach ($active->query as $k => $v) - { - if (isset($query[$k]) && $v !== $query[$k]) - { - // Compare again without alias - if (\is_string($v) && $v == current(explode(':', $query[$k], 2))) - { - continue; - } - - $match = false; - break; - } - } - - if ($match) - { - // Just use the supplied menu item - return; - } - } - - $needles = $this->router->getPath($query); - - $layout = isset($query['layout']) && $query['layout'] !== 'default' ? ':' . $query['layout'] : ''; - - if ($needles) - { - foreach ($needles as $view => $ids) - { - $viewLayout = $view . $layout; - - if ($layout && isset($this->lookup[$language][$viewLayout])) - { - if (\is_bool($ids)) - { - $query['Itemid'] = $this->lookup[$language][$viewLayout]; - - return; - } - - foreach ($ids as $id => $segment) - { - if (isset($this->lookup[$language][$viewLayout][(int) $id])) - { - $query['Itemid'] = $this->lookup[$language][$viewLayout][(int) $id]; - - return; - } - } - } - - if (isset($this->lookup[$language][$view])) - { - if (\is_bool($ids)) - { - $query['Itemid'] = $this->lookup[$language][$view]; - - return; - } - - foreach ($ids as $id => $segment) - { - if (isset($this->lookup[$language][$view][(int) $id])) - { - $query['Itemid'] = $this->lookup[$language][$view][(int) $id]; - - return; - } - } - } - } - } - - // Check if the active menuitem matches the requested language - if ($active && $active->component === 'com_' . $this->router->getName() - && ($language === '*' || \in_array($active->language, array('*', $language)) || !Multilanguage::isEnabled())) - { - $query['Itemid'] = $active->id; - - return; - } - - // If not found, return language specific home link - $default = $this->router->menu->getDefault($language); - - if (!empty($default->id)) - { - $query['Itemid'] = $default->id; - } - } - - /** - * Method to build the lookup array - * - * @param string $language The language that the lookup should be built up for - * - * @return void - * - * @since 3.4 - */ - protected function buildLookup($language = '*') - { - // Prepare the reverse lookup array. - if (!isset($this->lookup[$language])) - { - $this->lookup[$language] = array(); - - $component = ComponentHelper::getComponent('com_' . $this->router->getName()); - $views = $this->router->getViews(); - - $attributes = array('component_id'); - $values = array((int) $component->id); - - $attributes[] = 'language'; - $values[] = array($language, '*'); - - $items = $this->router->menu->getItems($attributes, $values); - - foreach ($items as $item) - { - if (isset($item->query['view'], $views[$item->query['view']])) - { - $view = $item->query['view']; - - $layout = ''; - - if (isset($item->query['layout'])) - { - $layout = ':' . $item->query['layout']; - } - - if ($views[$view]->key) - { - if (!isset($this->lookup[$language][$view . $layout])) - { - $this->lookup[$language][$view . $layout] = array(); - } - - if (!isset($this->lookup[$language][$view])) - { - $this->lookup[$language][$view] = array(); - } - - // If menuitem has no key set, we assume 0. - if (!isset($item->query[$views[$view]->key])) - { - $item->query[$views[$view]->key] = 0; - } - - /** - * Here it will become a bit tricky - * language != * can override existing entries - * language == * cannot override existing entries - */ - if (!isset($this->lookup[$language][$view . $layout][$item->query[$views[$view]->key]]) || $item->language !== '*') - { - $this->lookup[$language][$view . $layout][$item->query[$views[$view]->key]] = $item->id; - $this->lookup[$language][$view][$item->query[$views[$view]->key]] = $item->id; - } - } - else - { - /** - * Here it will become a bit tricky - * language != * can override existing entries - * language == * cannot override existing entries - */ - if (!isset($this->lookup[$language][$view . $layout]) || $item->language !== '*') - { - $this->lookup[$language][$view . $layout] = $item->id; - } - } - } - } - } - } - - /** - * Dummy method to fulfil the interface requirements - * - * @param array &$segments The URL segments to parse - * @param array &$vars The vars that result from the segments - * - * @return void - * - * @since 3.4 - * @codeCoverageIgnore - */ - public function parse(&$segments, &$vars) - { - } - - /** - * Dummy method to fulfil the interface requirements - * - * @param array &$query The vars that should be converted - * @param array &$segments The URL segments to create - * - * @return void - * - * @since 3.4 - * @codeCoverageIgnore - */ - public function build(&$query, &$segments) - { - } + /** + * Router this rule belongs to + * + * @var RouterView + * @since 3.4 + */ + protected $router; + + /** + * Lookup array of the menu items + * + * @var array + * @since 3.4 + */ + protected $lookup = array(); + + /** + * Class constructor. + * + * @param RouterView $router Router this rule belongs to + * + * @since 3.4 + */ + public function __construct(RouterView $router) + { + $this->router = $router; + + $this->buildLookup(); + } + + /** + * Finds the right Itemid for this query + * + * @param array &$query The query array to process + * + * @return void + * + * @since 3.4 + */ + public function preprocess(&$query) + { + $active = $this->router->menu->getActive(); + + /** + * If the active item id is not the same as the supplied item id or we have a supplied item id and no active + * menu item then we just use the supplied menu item and continue + */ + if (isset($query['Itemid']) && ($active === null || $query['Itemid'] != $active->id)) { + return; + } + + // Get query language + $language = isset($query['lang']) ? $query['lang'] : '*'; + + // Set the language to the current one when multilang is enabled and item is tagged to ALL + if (Multilanguage::isEnabled() && $language === '*') { + $language = $this->router->app->get('language'); + } + + if (!isset($this->lookup[$language])) { + $this->buildLookup($language); + } + + // Check if the active menu item matches the requested query + if ($active !== null && isset($query['Itemid'])) { + // Check if active->query and supplied query are the same + $match = true; + + foreach ($active->query as $k => $v) { + if (isset($query[$k]) && $v !== $query[$k]) { + // Compare again without alias + if (\is_string($v) && $v == current(explode(':', $query[$k], 2))) { + continue; + } + + $match = false; + break; + } + } + + if ($match) { + // Just use the supplied menu item + return; + } + } + + $needles = $this->router->getPath($query); + + $layout = isset($query['layout']) && $query['layout'] !== 'default' ? ':' . $query['layout'] : ''; + + if ($needles) { + foreach ($needles as $view => $ids) { + $viewLayout = $view . $layout; + + if ($layout && isset($this->lookup[$language][$viewLayout])) { + if (\is_bool($ids)) { + $query['Itemid'] = $this->lookup[$language][$viewLayout]; + + return; + } + + foreach ($ids as $id => $segment) { + if (isset($this->lookup[$language][$viewLayout][(int) $id])) { + $query['Itemid'] = $this->lookup[$language][$viewLayout][(int) $id]; + + return; + } + } + } + + if (isset($this->lookup[$language][$view])) { + if (\is_bool($ids)) { + $query['Itemid'] = $this->lookup[$language][$view]; + + return; + } + + foreach ($ids as $id => $segment) { + if (isset($this->lookup[$language][$view][(int) $id])) { + $query['Itemid'] = $this->lookup[$language][$view][(int) $id]; + + return; + } + } + } + } + } + + // Check if the active menuitem matches the requested language + if ( + $active && $active->component === 'com_' . $this->router->getName() + && ($language === '*' || \in_array($active->language, array('*', $language)) || !Multilanguage::isEnabled()) + ) { + $query['Itemid'] = $active->id; + + return; + } + + // If not found, return language specific home link + $default = $this->router->menu->getDefault($language); + + if (!empty($default->id)) { + $query['Itemid'] = $default->id; + } + } + + /** + * Method to build the lookup array + * + * @param string $language The language that the lookup should be built up for + * + * @return void + * + * @since 3.4 + */ + protected function buildLookup($language = '*') + { + // Prepare the reverse lookup array. + if (!isset($this->lookup[$language])) { + $this->lookup[$language] = array(); + + $component = ComponentHelper::getComponent('com_' . $this->router->getName()); + $views = $this->router->getViews(); + + $attributes = array('component_id'); + $values = array((int) $component->id); + + $attributes[] = 'language'; + $values[] = array($language, '*'); + + $items = $this->router->menu->getItems($attributes, $values); + + foreach ($items as $item) { + if (isset($item->query['view'], $views[$item->query['view']])) { + $view = $item->query['view']; + + $layout = ''; + + if (isset($item->query['layout'])) { + $layout = ':' . $item->query['layout']; + } + + if ($views[$view]->key) { + if (!isset($this->lookup[$language][$view . $layout])) { + $this->lookup[$language][$view . $layout] = array(); + } + + if (!isset($this->lookup[$language][$view])) { + $this->lookup[$language][$view] = array(); + } + + // If menuitem has no key set, we assume 0. + if (!isset($item->query[$views[$view]->key])) { + $item->query[$views[$view]->key] = 0; + } + + /** + * Here it will become a bit tricky + * language != * can override existing entries + * language == * cannot override existing entries + */ + if (!isset($this->lookup[$language][$view . $layout][$item->query[$views[$view]->key]]) || $item->language !== '*') { + $this->lookup[$language][$view . $layout][$item->query[$views[$view]->key]] = $item->id; + $this->lookup[$language][$view][$item->query[$views[$view]->key]] = $item->id; + } + } else { + /** + * Here it will become a bit tricky + * language != * can override existing entries + * language == * cannot override existing entries + */ + if (!isset($this->lookup[$language][$view . $layout]) || $item->language !== '*') { + $this->lookup[$language][$view . $layout] = $item->id; + } + } + } + } + } + } + + /** + * Dummy method to fulfil the interface requirements + * + * @param array &$segments The URL segments to parse + * @param array &$vars The vars that result from the segments + * + * @return void + * + * @since 3.4 + * @codeCoverageIgnore + */ + public function parse(&$segments, &$vars) + { + } + + /** + * Dummy method to fulfil the interface requirements + * + * @param array &$query The vars that should be converted + * @param array &$segments The URL segments to create + * + * @return void + * + * @since 3.4 + * @codeCoverageIgnore + */ + public function build(&$query, &$segments) + { + } } diff --git a/libraries/src/Component/Router/Rules/NomenuRules.php b/libraries/src/Component/Router/Rules/NomenuRules.php index 84f08ad515d8c..1a931311602fc 100644 --- a/libraries/src/Component/Router/Rules/NomenuRules.php +++ b/libraries/src/Component/Router/Rules/NomenuRules.php @@ -1,4 +1,5 @@ router = $router; - } - - /** - * Dummy method to fulfil the interface requirements - * - * @param array &$query The query array to process - * - * @return void - * - * @since 3.4 - * @codeCoverageIgnore - */ - public function preprocess(&$query) - { - } - - /** - * Parse a menu-less URL - * - * @param array &$segments The URL segments to parse - * @param array &$vars The vars that result from the segments - * - * @return void - * - * @since 3.4 - */ - public function parse(&$segments, &$vars) - { - $active = $this->router->menu->getActive(); - - if (!\is_object($active)) - { - $views = $this->router->getViews(); - - if (isset($views[$segments[0]])) - { - $vars['view'] = array_shift($segments); - $view = $views[$vars['view']]; - - if (isset($view->key) && isset($segments[0])) - { - if (\is_callable(array($this->router, 'get' . ucfirst($view->name) . 'Id'))) - { - if ($view->parent_key && $this->router->app->input->get($view->parent_key)) - { - $vars[$view->parent->key] = $this->router->app->input->get($view->parent_key); - $vars[$view->parent_key] = $this->router->app->input->get($view->parent_key); - } - - if ($view->nestable) - { - $vars[$view->key] = 0; - - while (count($segments)) - { - $segment = array_shift($segments); - $result = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Id'), array($segment, $vars)); - - if (!$result) - { - array_unshift($segments, $segment); - break; - } - - $vars[$view->key] = preg_replace('/-/', ':', $result, 1); - } - } - else - { - $segment = array_shift($segments); - $result = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Id'), array($segment, $vars)); - - $vars[$view->key] = preg_replace('/-/', ':', $result, 1); - } - } - else - { - $vars[$view->key] = preg_replace('/-/', ':', array_shift($segments), 1); - } - } - } - } - } - - /** - * Build a menu-less URL - * - * @param array &$query The vars that should be converted - * @param array &$segments The URL segments to create - * - * @return void - * - * @since 3.4 - */ - public function build(&$query, &$segments) - { - $menu_found = false; - - if (isset($query['Itemid'])) - { - $item = $this->router->menu->getItem($query['Itemid']); - - if (!isset($query['option']) - || ($item && isset($item->query['option']) && $item->query['option'] === $query['option'])) - { - $menu_found = true; - } - } - - if (!$menu_found && isset($query['view'])) - { - $views = $this->router->getViews(); - - if (isset($views[$query['view']])) - { - $view = $views[$query['view']]; - $segments[] = $query['view']; - - if ($view->key && isset($query[$view->key])) - { - if (\is_callable(array($this->router, 'get' . ucfirst($view->name) . 'Segment'))) - { - $result = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Segment'), array($query[$view->key], $query)); - - if ($view->nestable) - { - array_pop($result); - - while (count($result)) - { - $segments[] = str_replace(':', '-', array_pop($result)); - } - } - else - { - $segments[] = str_replace(':', '-', array_pop($result)); - } - } - else - { - $segments[] = str_replace(':', '-', $query[$view->key]); - } - - unset($query[$views[$query['view']]->key]); - } - - unset($query['view']); - } - } - } + /** + * Router this rule belongs to + * + * @var RouterView + * @since 3.4 + */ + protected $router; + + /** + * Class constructor. + * + * @param RouterView $router Router this rule belongs to + * + * @since 3.4 + */ + public function __construct(RouterView $router) + { + $this->router = $router; + } + + /** + * Dummy method to fulfil the interface requirements + * + * @param array &$query The query array to process + * + * @return void + * + * @since 3.4 + * @codeCoverageIgnore + */ + public function preprocess(&$query) + { + } + + /** + * Parse a menu-less URL + * + * @param array &$segments The URL segments to parse + * @param array &$vars The vars that result from the segments + * + * @return void + * + * @since 3.4 + */ + public function parse(&$segments, &$vars) + { + $active = $this->router->menu->getActive(); + + if (!\is_object($active)) { + $views = $this->router->getViews(); + + if (isset($views[$segments[0]])) { + $vars['view'] = array_shift($segments); + $view = $views[$vars['view']]; + + if (isset($view->key) && isset($segments[0])) { + if (\is_callable(array($this->router, 'get' . ucfirst($view->name) . 'Id'))) { + if ($view->parent_key && $this->router->app->input->get($view->parent_key)) { + $vars[$view->parent->key] = $this->router->app->input->get($view->parent_key); + $vars[$view->parent_key] = $this->router->app->input->get($view->parent_key); + } + + if ($view->nestable) { + $vars[$view->key] = 0; + + while (count($segments)) { + $segment = array_shift($segments); + $result = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Id'), array($segment, $vars)); + + if (!$result) { + array_unshift($segments, $segment); + break; + } + + $vars[$view->key] = preg_replace('/-/', ':', $result, 1); + } + } else { + $segment = array_shift($segments); + $result = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Id'), array($segment, $vars)); + + $vars[$view->key] = preg_replace('/-/', ':', $result, 1); + } + } else { + $vars[$view->key] = preg_replace('/-/', ':', array_shift($segments), 1); + } + } + } + } + } + + /** + * Build a menu-less URL + * + * @param array &$query The vars that should be converted + * @param array &$segments The URL segments to create + * + * @return void + * + * @since 3.4 + */ + public function build(&$query, &$segments) + { + $menu_found = false; + + if (isset($query['Itemid'])) { + $item = $this->router->menu->getItem($query['Itemid']); + + if ( + !isset($query['option']) + || ($item && isset($item->query['option']) && $item->query['option'] === $query['option']) + ) { + $menu_found = true; + } + } + + if (!$menu_found && isset($query['view'])) { + $views = $this->router->getViews(); + + if (isset($views[$query['view']])) { + $view = $views[$query['view']]; + $segments[] = $query['view']; + + if ($view->key && isset($query[$view->key])) { + if (\is_callable(array($this->router, 'get' . ucfirst($view->name) . 'Segment'))) { + $result = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Segment'), array($query[$view->key], $query)); + + if ($view->nestable) { + array_pop($result); + + while (count($result)) { + $segments[] = str_replace(':', '-', array_pop($result)); + } + } else { + $segments[] = str_replace(':', '-', array_pop($result)); + } + } else { + $segments[] = str_replace(':', '-', $query[$view->key]); + } + + unset($query[$views[$query['view']]->key]); + } + + unset($query['view']); + } + } + } } diff --git a/libraries/src/Component/Router/Rules/RulesInterface.php b/libraries/src/Component/Router/Rules/RulesInterface.php index d56015c2dc633..4e2b703be7a47 100644 --- a/libraries/src/Component/Router/Rules/RulesInterface.php +++ b/libraries/src/Component/Router/Rules/RulesInterface.php @@ -1,4 +1,5 @@ router = $router; - } - - /** - * Dummy method to fulfil the interface requirements - * - * @param array &$query The query array to process - * - * @return void - * - * @since 3.4 - */ - public function preprocess(&$query) - { - } - - /** - * Parse the URL - * - * @param array &$segments The URL segments to parse - * @param array &$vars The vars that result from the segments - * - * @return void - * - * @since 3.4 - */ - public function parse(&$segments, &$vars) - { - // Get the views and the currently active query vars - $views = $this->router->getViews(); - $active = $this->router->menu->getActive(); - - if ($active) - { - $vars = array_merge($active->query, $vars); - } - - // We don't have a view or its not a view of this component! We stop here - if (!isset($vars['view']) || !isset($views[$vars['view']])) - { - return; - } - - // Copy the segments, so that we can iterate over all of them and at the same time modify the original segments - $tempSegments = $segments; - - // Iterate over the segments as long as a segment fits - foreach ($tempSegments as $segment) - { - // Our current view is nestable. We need to check first if the segment fits to that - if ($views[$vars['view']]->nestable) - { - if (\is_callable(array($this->router, 'get' . ucfirst($views[$vars['view']]->name) . 'Id'))) - { - $key = \call_user_func_array(array($this->router, 'get' . ucfirst($views[$vars['view']]->name) . 'Id'), array($segment, $vars)); - - // Did we get a proper key? If not, we need to look in the child-views - if ($key) - { - $vars[$views[$vars['view']]->key] = $key; - - array_shift($segments); - - continue; - } - } - else - { - // The router is not complete. The getId() method is missing. - return; - } - } - - // Lets find the right view that belongs to this segment - $found = false; - - foreach ($views[$vars['view']]->children as $view) - { - if (!$view->key) - { - if ($view->name === $segment) - { - // The segment is a view name - $parent = $views[$vars['view']]; - $vars['view'] = $view->name; - $found = true; - - if ($view->parent_key && isset($vars[$parent->key])) - { - $parent_key = $vars[$parent->key]; - $vars[$view->parent_key] = $parent_key; - - unset($vars[$parent->key]); - } - - break; - } - } - elseif (\is_callable(array($this->router, 'get' . ucfirst($view->name) . 'Id'))) - { - // Hand the data over to the router specific method and see if there is a content item that fits - $key = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Id'), array($segment, $vars)); - - if ($key) - { - // Found the right view and the right item - $parent = $views[$vars['view']]; - $vars['view'] = $view->name; - $found = true; - - if ($view->parent_key && isset($vars[$parent->key])) - { - $parent_key = $vars[$parent->key]; - $vars[$view->parent_key] = $parent_key; - - unset($vars[$parent->key]); - } - - $vars[$view->key] = $key; - - break; - } - } - } - - if (!$found) - { - return; - } - - array_shift($segments); - } - } - - /** - * Build a standard URL - * - * @param array &$query The vars that should be converted - * @param array &$segments The URL segments to create - * - * @return void - * - * @since 3.4 - */ - public function build(&$query, &$segments) - { - if (!isset($query['Itemid'], $query['view'])) - { - return; - } - - // Get the menu item belonging to the Itemid that has been found - $item = $this->router->menu->getItem($query['Itemid']); - - if ($item === null - || $item->component !== 'com_' . $this->router->getName() - || !isset($item->query['view'])) - { - return; - } - - // Get menu item layout - $mLayout = isset($item->query['layout']) ? $item->query['layout'] : null; - - // Get all views for this component - $views = $this->router->getViews(); - - // Return directly when the URL of the Itemid is identical with the URL to build - if ($item->query['view'] === $query['view']) - { - $view = $views[$query['view']]; - - if (!$view->key) - { - unset($query['view']); - - if (isset($query['layout']) && $mLayout === $query['layout']) - { - unset($query['layout']); - } - - return; - } - - if (isset($query[$view->key]) && $item->query[$view->key] == (int) $query[$view->key]) - { - unset($query[$view->key]); - - while ($view) - { - unset($query[$view->parent_key]); - - $view = $view->parent; - } - - unset($query['view']); - - if (isset($query['layout']) && $mLayout === $query['layout']) - { - unset($query['layout']); - } - - return; - } - } - - // Get the path from the view of the current URL and parse it to the menu item - $path = array_reverse($this->router->getPath($query), true); - $found = false; - - foreach ($path as $element => $ids) - { - $view = $views[$element]; - - if ($found === false && $item->query['view'] === $element) - { - if ($view->nestable) - { - $found = true; - } - elseif ($view->children) - { - $found = true; - - continue; - } - } - - if ($found === false) - { - // Jump to the next view - continue; - } - - if ($ids) - { - if ($view->nestable) - { - $found2 = false; - - foreach (array_reverse($ids, true) as $id => $segment) - { - if ($found2) - { - $segments[] = str_replace(':', '-', $segment); - } - elseif ((int) $item->query[$view->key] === (int) $id) - { - $found2 = true; - } - } - } - elseif ($ids === true) - { - $segments[] = $element; - } - else - { - $segments[] = str_replace(':', '-', current($ids)); - } - } - - if ($view->parent_key) - { - // Remove parent key from query - unset($query[$view->parent_key]); - } - } - - if ($found) - { - unset($query[$views[$query['view']]->key], $query['view']); - - if (isset($query['layout']) && $mLayout === $query['layout']) - { - unset($query['layout']); - } - } - } + /** + * Router this rule belongs to + * + * @var RouterView + * @since 3.4 + */ + protected $router; + + /** + * Class constructor. + * + * @param RouterView $router Router this rule belongs to + * + * @since 3.4 + */ + public function __construct(RouterView $router) + { + $this->router = $router; + } + + /** + * Dummy method to fulfil the interface requirements + * + * @param array &$query The query array to process + * + * @return void + * + * @since 3.4 + */ + public function preprocess(&$query) + { + } + + /** + * Parse the URL + * + * @param array &$segments The URL segments to parse + * @param array &$vars The vars that result from the segments + * + * @return void + * + * @since 3.4 + */ + public function parse(&$segments, &$vars) + { + // Get the views and the currently active query vars + $views = $this->router->getViews(); + $active = $this->router->menu->getActive(); + + if ($active) { + $vars = array_merge($active->query, $vars); + } + + // We don't have a view or its not a view of this component! We stop here + if (!isset($vars['view']) || !isset($views[$vars['view']])) { + return; + } + + // Copy the segments, so that we can iterate over all of them and at the same time modify the original segments + $tempSegments = $segments; + + // Iterate over the segments as long as a segment fits + foreach ($tempSegments as $segment) { + // Our current view is nestable. We need to check first if the segment fits to that + if ($views[$vars['view']]->nestable) { + if (\is_callable(array($this->router, 'get' . ucfirst($views[$vars['view']]->name) . 'Id'))) { + $key = \call_user_func_array(array($this->router, 'get' . ucfirst($views[$vars['view']]->name) . 'Id'), array($segment, $vars)); + + // Did we get a proper key? If not, we need to look in the child-views + if ($key) { + $vars[$views[$vars['view']]->key] = $key; + + array_shift($segments); + + continue; + } + } else { + // The router is not complete. The getId() method is missing. + return; + } + } + + // Lets find the right view that belongs to this segment + $found = false; + + foreach ($views[$vars['view']]->children as $view) { + if (!$view->key) { + if ($view->name === $segment) { + // The segment is a view name + $parent = $views[$vars['view']]; + $vars['view'] = $view->name; + $found = true; + + if ($view->parent_key && isset($vars[$parent->key])) { + $parent_key = $vars[$parent->key]; + $vars[$view->parent_key] = $parent_key; + + unset($vars[$parent->key]); + } + + break; + } + } elseif (\is_callable(array($this->router, 'get' . ucfirst($view->name) . 'Id'))) { + // Hand the data over to the router specific method and see if there is a content item that fits + $key = \call_user_func_array(array($this->router, 'get' . ucfirst($view->name) . 'Id'), array($segment, $vars)); + + if ($key) { + // Found the right view and the right item + $parent = $views[$vars['view']]; + $vars['view'] = $view->name; + $found = true; + + if ($view->parent_key && isset($vars[$parent->key])) { + $parent_key = $vars[$parent->key]; + $vars[$view->parent_key] = $parent_key; + + unset($vars[$parent->key]); + } + + $vars[$view->key] = $key; + + break; + } + } + } + + if (!$found) { + return; + } + + array_shift($segments); + } + } + + /** + * Build a standard URL + * + * @param array &$query The vars that should be converted + * @param array &$segments The URL segments to create + * + * @return void + * + * @since 3.4 + */ + public function build(&$query, &$segments) + { + if (!isset($query['Itemid'], $query['view'])) { + return; + } + + // Get the menu item belonging to the Itemid that has been found + $item = $this->router->menu->getItem($query['Itemid']); + + if ( + $item === null + || $item->component !== 'com_' . $this->router->getName() + || !isset($item->query['view']) + ) { + return; + } + + // Get menu item layout + $mLayout = isset($item->query['layout']) ? $item->query['layout'] : null; + + // Get all views for this component + $views = $this->router->getViews(); + + // Return directly when the URL of the Itemid is identical with the URL to build + if ($item->query['view'] === $query['view']) { + $view = $views[$query['view']]; + + if (!$view->key) { + unset($query['view']); + + if (isset($query['layout']) && $mLayout === $query['layout']) { + unset($query['layout']); + } + + return; + } + + if (isset($query[$view->key]) && $item->query[$view->key] == (int) $query[$view->key]) { + unset($query[$view->key]); + + while ($view) { + unset($query[$view->parent_key]); + + $view = $view->parent; + } + + unset($query['view']); + + if (isset($query['layout']) && $mLayout === $query['layout']) { + unset($query['layout']); + } + + return; + } + } + + // Get the path from the view of the current URL and parse it to the menu item + $path = array_reverse($this->router->getPath($query), true); + $found = false; + + foreach ($path as $element => $ids) { + $view = $views[$element]; + + if ($found === false && $item->query['view'] === $element) { + if ($view->nestable) { + $found = true; + } elseif ($view->children) { + $found = true; + + continue; + } + } + + if ($found === false) { + // Jump to the next view + continue; + } + + if ($ids) { + if ($view->nestable) { + $found2 = false; + + foreach (array_reverse($ids, true) as $id => $segment) { + if ($found2) { + $segments[] = str_replace(':', '-', $segment); + } elseif ((int) $item->query[$view->key] === (int) $id) { + $found2 = true; + } + } + } elseif ($ids === true) { + $segments[] = $element; + } else { + $segments[] = str_replace(':', '-', current($ids)); + } + } + + if ($view->parent_key) { + // Remove parent key from query + unset($query[$view->parent_key]); + } + } + + if ($found) { + unset($query[$views[$query['view']]->key], $query['view']); + + if (isset($query['layout']) && $mLayout === $query['layout']) { + unset($query['layout']); + } + } + } } diff --git a/libraries/src/Console/AddUserCommand.php b/libraries/src/Console/AddUserCommand.php index c44cf43f7071c..6799a76f479c0 100644 --- a/libraries/src/Console/AddUserCommand.php +++ b/libraries/src/Console/AddUserCommand.php @@ -1,4 +1,5 @@ setDatabase($db); - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - $this->ioStyle->title('Add user'); - $this->user = $this->getStringFromOption('username', 'Please enter a username'); - $this->name = $this->getStringFromOption('name', 'Please enter a name (full name of user)'); - $this->email = $this->getStringFromOption('email', 'Please enter an email address'); - $this->password = $this->getStringFromOption('password', 'Please enter a password'); - $this->userGroups = $this->getUserGroups(); - - if (\in_array("error", $this->userGroups)) - { - $this->ioStyle->error("'" . $this->userGroups[1] . "' user group doesn't exist!"); - - return Command::FAILURE; - } - - // Get filter to remove invalid characters - $filter = new InputFilter; - - $user['username'] = $filter->clean($this->user, 'USERNAME'); - $user['password'] = $this->password; - $user['name'] = $filter->clean($this->name, 'STRING'); - $user['email'] = $this->email; - $user['groups'] = $this->userGroups; - - $userObj = User::getInstance(); - $userObj->bind($user); - - if (!$userObj->save()) - { - switch ($userObj->getError()) - { - case "JLIB_DATABASE_ERROR_USERNAME_INUSE": - $this->ioStyle->error("The username already exists!"); - break; - case "JLIB_DATABASE_ERROR_EMAIL_INUSE": - $this->ioStyle->error("The email address already exists!"); - break; - case "JLIB_DATABASE_ERROR_VALID_MAIL": - $this->ioStyle->error("The email address is invalid!"); - break; - } - - return 1; - } - - $this->ioStyle->success("User created!"); - - return Command::SUCCESS; - } - - /** - * Method to get groupId by groupName - * - * @param string $groupName name of group - * - * @return integer - * - * @since 4.0.0 - */ - protected function getGroupId($groupName) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__usergroups')) - ->where($db->quoteName('title') . ' = :groupName') - ->bind(':groupName', $groupName); - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Method to get a value from option - * - * @param string $option set the option name - * @param string $question set the question if user enters no value to option - * - * @return string - * - * @since 4.0.0 - */ - public function getStringFromOption($option, $question): string - { - $answer = (string) $this->cliInput->getOption($option); - - while (!$answer) - { - if ($option === 'password') - { - $answer = (string) $this->ioStyle->askHidden($question); - } - else - { - $answer = (string) $this->ioStyle->ask($question); - } - } - - return $answer; - } - - /** - * Method to get a value from option - * - * @return array - * - * @since 4.0.0 - */ - protected function getUserGroups(): array - { - $groups = $this->getApplication()->getConsoleInput()->getOption('usergroup'); - $db = $this->getDatabase(); - - $groupList = []; - - // Group names have been supplied as input arguments - if (!\is_null($groups) && $groups[0]) - { - $groups = explode(',', $groups); - - foreach ($groups as $group) - { - $groupId = $this->getGroupId($group); - - if (empty($groupId)) - { - $this->ioStyle->error("Invalid group name '" . $group . "'"); - throw new InvalidOptionException("Invalid group name " . $group); - } - - $groupList[] = $this->getGroupId($group); - } - - return $groupList; - } - - // Generate select list for user - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__usergroups')) - ->order($db->quoteName('id') . 'ASC'); - $db->setQuery($query); - - $list = $db->loadColumn(); - - $choice = new ChoiceQuestion( - 'Please select a usergroup (separate multiple groups with a comma)', - $list - ); - $choice->setMultiselect(true); - - $answer = (array) $this->ioStyle->askQuestion($choice); - - foreach ($answer as $group) - { - $groupList[] = $this->getGroupId($group); - } - - return $groupList; - } - - /** - * Configure the IO. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return void - * - * @since 4.0.0 - */ - private function configureIO(InputInterface $input, OutputInterface $output) - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Configure the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $help = "%command.name% will add a user + use DatabaseAwareTrait; + + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'user:add'; + + /** + * SymfonyStyle Object + * @var object + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Stores the Input Object + * @var object + * @since 4.0.0 + */ + private $cliInput; + + /** + * The username + * + * @var string + * + * @since 4.0.0 + */ + private $user; + + /** + * The password + * + * @var string + * + * @since 4.0.0 + */ + private $password; + + /** + * The name + * + * @var string + * + * @since 4.0.0 + */ + private $name; + + /** + * The email address + * + * @var string + * + * @since 4.0.0 + */ + private $email; + + /** + * The usergroups + * + * @var array + * + * @since 4.0.0 + */ + private $userGroups = []; + + /** + * Command constructor. + * + * @param DatabaseInterface $db The database + * + * @since 4.2.0 + */ + public function __construct(DatabaseInterface $db) + { + parent::__construct(); + + $this->setDatabase($db); + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + $this->ioStyle->title('Add user'); + $this->user = $this->getStringFromOption('username', 'Please enter a username'); + $this->name = $this->getStringFromOption('name', 'Please enter a name (full name of user)'); + $this->email = $this->getStringFromOption('email', 'Please enter an email address'); + $this->password = $this->getStringFromOption('password', 'Please enter a password'); + $this->userGroups = $this->getUserGroups(); + + if (\in_array("error", $this->userGroups)) { + $this->ioStyle->error("'" . $this->userGroups[1] . "' user group doesn't exist!"); + + return Command::FAILURE; + } + + // Get filter to remove invalid characters + $filter = new InputFilter(); + + $user['username'] = $filter->clean($this->user, 'USERNAME'); + $user['password'] = $this->password; + $user['name'] = $filter->clean($this->name, 'STRING'); + $user['email'] = $this->email; + $user['groups'] = $this->userGroups; + + $userObj = User::getInstance(); + $userObj->bind($user); + + if (!$userObj->save()) { + switch ($userObj->getError()) { + case "JLIB_DATABASE_ERROR_USERNAME_INUSE": + $this->ioStyle->error("The username already exists!"); + break; + case "JLIB_DATABASE_ERROR_EMAIL_INUSE": + $this->ioStyle->error("The email address already exists!"); + break; + case "JLIB_DATABASE_ERROR_VALID_MAIL": + $this->ioStyle->error("The email address is invalid!"); + break; + } + + return 1; + } + + $this->ioStyle->success("User created!"); + + return Command::SUCCESS; + } + + /** + * Method to get groupId by groupName + * + * @param string $groupName name of group + * + * @return integer + * + * @since 4.0.0 + */ + protected function getGroupId($groupName) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__usergroups')) + ->where($db->quoteName('title') . ' = :groupName') + ->bind(':groupName', $groupName); + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Method to get a value from option + * + * @param string $option set the option name + * @param string $question set the question if user enters no value to option + * + * @return string + * + * @since 4.0.0 + */ + public function getStringFromOption($option, $question): string + { + $answer = (string) $this->cliInput->getOption($option); + + while (!$answer) { + if ($option === 'password') { + $answer = (string) $this->ioStyle->askHidden($question); + } else { + $answer = (string) $this->ioStyle->ask($question); + } + } + + return $answer; + } + + /** + * Method to get a value from option + * + * @return array + * + * @since 4.0.0 + */ + protected function getUserGroups(): array + { + $groups = $this->getApplication()->getConsoleInput()->getOption('usergroup'); + $db = $this->getDatabase(); + + $groupList = []; + + // Group names have been supplied as input arguments + if (!\is_null($groups) && $groups[0]) { + $groups = explode(',', $groups); + + foreach ($groups as $group) { + $groupId = $this->getGroupId($group); + + if (empty($groupId)) { + $this->ioStyle->error("Invalid group name '" . $group . "'"); + throw new InvalidOptionException("Invalid group name " . $group); + } + + $groupList[] = $this->getGroupId($group); + } + + return $groupList; + } + + // Generate select list for user + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__usergroups')) + ->order($db->quoteName('id') . 'ASC'); + $db->setQuery($query); + + $list = $db->loadColumn(); + + $choice = new ChoiceQuestion( + 'Please select a usergroup (separate multiple groups with a comma)', + $list + ); + $choice->setMultiselect(true); + + $answer = (array) $this->ioStyle->askQuestion($choice); + + foreach ($answer as $group) { + $groupList[] = $this->getGroupId($group); + } + + return $groupList; + } + + /** + * Configure the IO. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return void + * + * @since 4.0.0 + */ + private function configureIO(InputInterface $input, OutputInterface $output) + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Configure the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% will add a user \nUsage: php %command.full_name%"; - $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); - $this->addOption('name', null, InputOption::VALUE_OPTIONAL, 'full name of user'); - $this->addOption('password', null, InputOption::VALUE_OPTIONAL, 'password'); - $this->addOption('email', null, InputOption::VALUE_OPTIONAL, 'email address'); - $this->addOption('usergroup', null, InputOption::VALUE_OPTIONAL, 'usergroup (separate multiple groups with comma ",")'); - $this->setDescription('Add a user'); - $this->setHelp($help); - } + $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); + $this->addOption('name', null, InputOption::VALUE_OPTIONAL, 'full name of user'); + $this->addOption('password', null, InputOption::VALUE_OPTIONAL, 'password'); + $this->addOption('email', null, InputOption::VALUE_OPTIONAL, 'email address'); + $this->addOption('usergroup', null, InputOption::VALUE_OPTIONAL, 'usergroup (separate multiple groups with comma ",")'); + $this->setDescription('Add a user'); + $this->setHelp($help); + } } diff --git a/libraries/src/Console/AddUserToGroupCommand.php b/libraries/src/Console/AddUserToGroupCommand.php index 07204d935b033..489c7819a771a 100644 --- a/libraries/src/Console/AddUserToGroupCommand.php +++ b/libraries/src/Console/AddUserToGroupCommand.php @@ -1,4 +1,5 @@ setDatabase($db); - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - $this->username = $this->getStringFromOption('username', 'Please enter a username'); - $this->ioStyle->title('Add user to group'); - - $userId = $this->getUserId($this->username); - - if (empty($userId)) - { - $this->ioStyle->error("The user " . $this->username . " does not exist!"); - - return Command::FAILURE; - } - - // Fetch user - $user = User::getInstance($userId); - - $this->userGroups = $this->getGroups($user); - - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__usergroups')) - ->where($db->quoteName('id') . ' = :userGroup'); - - foreach ($this->userGroups as $userGroup) - { - $query->bind(':userGroup', $userGroup); - $db->setQuery($query); - - $result = $db->loadResult(); - - if (UserHelper::addUserToGroup($user->id, $userGroup)) - { - $this->ioStyle->success("Added '" . $user->username . "' to group '" . $result . "'!"); - } - else - { - $this->ioStyle->error("Can't add '" . $user->username . "' to group '" . $result . "'!"); - - return Command::FAILURE; - } - } - - return Command::SUCCESS; - } - - - /** - * Method to get a value from option - * - * @param User $user a UserInstance - * - * @return array - * - * @since 4.0.0 - */ - protected function getGroups($user): array - { - $groups = $this->getApplication()->getConsoleInput()->getOption('group'); - - $db = $this->getDatabase(); - - $groupList = []; - - // Group names have been supplied as input arguments - if ($groups) - { - $groups = explode(',', $groups); - - foreach ($groups as $group) - { - $groupId = $this->getGroupId($group); - - if (empty($groupId)) - { - $this->ioStyle->error("Invalid group name '" . $group . "'"); - throw new InvalidOptionException("Invalid group name " . $group); - } - - $groupList[] = $this->getGroupId($group); - } - - return $groupList; - } - - $userGroups = Access::getGroupsByUser($user->id, false); - - // Generate select list for user - $query = $db->getQuery(true) - ->select($db->quoteName('title')) - ->from($db->quoteName('#__usergroups')) - ->whereNotIn($db->quoteName('id'), $userGroups) - ->order($db->quoteName('id') . ' ASC'); - $db->setQuery($query); - - $list = $db->loadColumn(); - - $choice = new ChoiceQuestion( - 'Please select a usergroup (separate multiple groups with a comma)', - $list - ); - $choice->setMultiselect(true); - - $answer = (array) $this->ioStyle->askQuestion($choice); - - foreach ($answer as $group) - { - $groupList[] = $this->getGroupId($group); - } - - return $groupList; - } - - /** - * Method to get groupId by groupName - * - * @param string $groupName name of group - * - * @return integer - * - * @since 4.0.0 - */ - protected function getGroupId($groupName) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__usergroups')) - ->where($db->quoteName('title') . '= :groupName') - ->bind(':groupName', $groupName); - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Method to get a user object - * - * @param string $username username - * - * @return object - * - * @since 4.0.0 - */ - protected function getUserId($username) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName('id')) - ->from($db->quoteName('#__users')) - ->where($db->quoteName('username') . '= :username') - ->bind(':username', $username); - $db->setQuery($query); - - return $db->loadResult(); - } - - /** - * Method to get a value from option - * - * @param string $option set the option name - * - * @param string $question set the question if user enters no value to option - * - * @return string - * - * @since 4.0.0 - */ - protected function getStringFromOption($option, $question): string - { - $answer = (string) $this->getApplication()->getConsoleInput()->getOption($option); - - while (!$answer) - { - $answer = (string) $this->ioStyle->ask($question); - } - - return $answer; - } - - /** - * Configure the IO. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return void - * - * @since 4.0.0 - */ - private function configureIO(InputInterface $input, OutputInterface $output) - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Configure the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $help = "%command.name% adds a user to a group + use DatabaseAwareTrait; + + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'user:addtogroup'; + + /** + * SymfonyStyle Object + * @var object + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Stores the Input Object + * @var object + * @since 4.0.0 + */ + private $cliInput; + + /** + * The username + * + * @var string + * + * @since 4.0.0 + */ + private $username; + + /** + * The usergroups + * + * @var array + * + * @since 4.0.0 + */ + private $userGroups = []; + + /** + * Command constructor. + * + * @param DatabaseInterface $db The database + * + * @since 4.2.0 + */ + public function __construct(DatabaseInterface $db) + { + parent::__construct(); + + $this->setDatabase($db); + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + $this->username = $this->getStringFromOption('username', 'Please enter a username'); + $this->ioStyle->title('Add user to group'); + + $userId = $this->getUserId($this->username); + + if (empty($userId)) { + $this->ioStyle->error("The user " . $this->username . " does not exist!"); + + return Command::FAILURE; + } + + // Fetch user + $user = User::getInstance($userId); + + $this->userGroups = $this->getGroups($user); + + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__usergroups')) + ->where($db->quoteName('id') . ' = :userGroup'); + + foreach ($this->userGroups as $userGroup) { + $query->bind(':userGroup', $userGroup); + $db->setQuery($query); + + $result = $db->loadResult(); + + if (UserHelper::addUserToGroup($user->id, $userGroup)) { + $this->ioStyle->success("Added '" . $user->username . "' to group '" . $result . "'!"); + } else { + $this->ioStyle->error("Can't add '" . $user->username . "' to group '" . $result . "'!"); + + return Command::FAILURE; + } + } + + return Command::SUCCESS; + } + + + /** + * Method to get a value from option + * + * @param User $user a UserInstance + * + * @return array + * + * @since 4.0.0 + */ + protected function getGroups($user): array + { + $groups = $this->getApplication()->getConsoleInput()->getOption('group'); + + $db = $this->getDatabase(); + + $groupList = []; + + // Group names have been supplied as input arguments + if ($groups) { + $groups = explode(',', $groups); + + foreach ($groups as $group) { + $groupId = $this->getGroupId($group); + + if (empty($groupId)) { + $this->ioStyle->error("Invalid group name '" . $group . "'"); + throw new InvalidOptionException("Invalid group name " . $group); + } + + $groupList[] = $this->getGroupId($group); + } + + return $groupList; + } + + $userGroups = Access::getGroupsByUser($user->id, false); + + // Generate select list for user + $query = $db->getQuery(true) + ->select($db->quoteName('title')) + ->from($db->quoteName('#__usergroups')) + ->whereNotIn($db->quoteName('id'), $userGroups) + ->order($db->quoteName('id') . ' ASC'); + $db->setQuery($query); + + $list = $db->loadColumn(); + + $choice = new ChoiceQuestion( + 'Please select a usergroup (separate multiple groups with a comma)', + $list + ); + $choice->setMultiselect(true); + + $answer = (array) $this->ioStyle->askQuestion($choice); + + foreach ($answer as $group) { + $groupList[] = $this->getGroupId($group); + } + + return $groupList; + } + + /** + * Method to get groupId by groupName + * + * @param string $groupName name of group + * + * @return integer + * + * @since 4.0.0 + */ + protected function getGroupId($groupName) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__usergroups')) + ->where($db->quoteName('title') . '= :groupName') + ->bind(':groupName', $groupName); + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Method to get a user object + * + * @param string $username username + * + * @return object + * + * @since 4.0.0 + */ + protected function getUserId($username) + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName('id')) + ->from($db->quoteName('#__users')) + ->where($db->quoteName('username') . '= :username') + ->bind(':username', $username); + $db->setQuery($query); + + return $db->loadResult(); + } + + /** + * Method to get a value from option + * + * @param string $option set the option name + * + * @param string $question set the question if user enters no value to option + * + * @return string + * + * @since 4.0.0 + */ + protected function getStringFromOption($option, $question): string + { + $answer = (string) $this->getApplication()->getConsoleInput()->getOption($option); + + while (!$answer) { + $answer = (string) $this->ioStyle->ask($question); + } + + return $answer; + } + + /** + * Configure the IO. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return void + * + * @since 4.0.0 + */ + private function configureIO(InputInterface $input, OutputInterface $output) + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Configure the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% adds a user to a group \nUsage: php %command.full_name%"; - $this->setDescription('Add a user to a group'); - $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); - $this->addOption('group', null, InputOption::VALUE_OPTIONAL, 'group'); - $this->setHelp($help); - } + $this->setDescription('Add a user to a group'); + $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); + $this->addOption('group', null, InputOption::VALUE_OPTIONAL, 'group'); + $this->setHelp($help); + } } diff --git a/libraries/src/Console/ChangeUserPasswordCommand.php b/libraries/src/Console/ChangeUserPasswordCommand.php index f556932465be6..bfa87b841358d 100644 --- a/libraries/src/Console/ChangeUserPasswordCommand.php +++ b/libraries/src/Console/ChangeUserPasswordCommand.php @@ -1,4 +1,5 @@ configureIO($input, $output); - $this->username = $this->getStringFromOption('username', 'Please enter a username'); - $this->ioStyle->title('Change password'); - - $userId = UserHelper::getUserId($this->username); - - if (empty($userId)) - { - $this->ioStyle->error("The user " . $this->username . " does not exist!"); - - return Command::FAILURE; - } - - $user = User::getInstance($userId); - $this->password = $this->getStringFromOption('password', 'Please enter a new password'); - - $user->password = UserHelper::hashPassword($this->password); - - if (!$user->save(true)) - { - $this->ioStyle->error($user->getError()); - - return Command::FAILURE; - } - - $this->ioStyle->success("Password changed!"); - - return Command::SUCCESS; - } - - /** - * Method to get a value from option - * - * @param string $option set the option name - * - * @param string $question set the question if user enters no value to option - * - * @return string - * - * @since 4.0.0 - */ - protected function getStringFromOption($option, $question): string - { - $answer = (string) $this->cliInput->getOption($option); - - while (!$answer) - { - if ($option === 'password') - { - $answer = (string) $this->ioStyle->askHidden($question); - } - else - { - $answer = (string) $this->ioStyle->ask($question); - } - } - - return $answer; - } - - /** - * Configure the IO. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return void - * - * @since 4.0.0 - */ - private function configureIO(InputInterface $input, OutputInterface $output) - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Configure the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $help = "%command.name% will change a user's password + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'user:reset-password'; + + /** + * SymfonyStyle Object + * @var object + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Stores the Input Object + * @var object + * @since 4.0.0 + */ + private $cliInput; + + /** + * The username + * + * @var string + * + * @since 4.0.0 + */ + private $username; + + /** + * The password + * + * @var string + * + * @since 4.0.0 + */ + private $password; + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + $this->username = $this->getStringFromOption('username', 'Please enter a username'); + $this->ioStyle->title('Change password'); + + $userId = UserHelper::getUserId($this->username); + + if (empty($userId)) { + $this->ioStyle->error("The user " . $this->username . " does not exist!"); + + return Command::FAILURE; + } + + $user = User::getInstance($userId); + $this->password = $this->getStringFromOption('password', 'Please enter a new password'); + + $user->password = UserHelper::hashPassword($this->password); + + if (!$user->save(true)) { + $this->ioStyle->error($user->getError()); + + return Command::FAILURE; + } + + $this->ioStyle->success("Password changed!"); + + return Command::SUCCESS; + } + + /** + * Method to get a value from option + * + * @param string $option set the option name + * + * @param string $question set the question if user enters no value to option + * + * @return string + * + * @since 4.0.0 + */ + protected function getStringFromOption($option, $question): string + { + $answer = (string) $this->cliInput->getOption($option); + + while (!$answer) { + if ($option === 'password') { + $answer = (string) $this->ioStyle->askHidden($question); + } else { + $answer = (string) $this->ioStyle->ask($question); + } + } + + return $answer; + } + + /** + * Configure the IO. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return void + * + * @since 4.0.0 + */ + private function configureIO(InputInterface $input, OutputInterface $output) + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Configure the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% will change a user's password \nUsage: php %command.full_name%"; - $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); - $this->addOption('password', null, InputOption::VALUE_OPTIONAL, 'password'); - $this->setDescription("Change a user's password"); - $this->setHelp($help); - } + $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); + $this->addOption('password', null, InputOption::VALUE_OPTIONAL, 'password'); + $this->setDescription("Change a user's password"); + $this->setHelp($help); + } } diff --git a/libraries/src/Console/CheckJoomlaUpdatesCommand.php b/libraries/src/Console/CheckJoomlaUpdatesCommand.php index cc1661a9a1af0..f030ad13a9fd4 100644 --- a/libraries/src/Console/CheckJoomlaUpdatesCommand.php +++ b/libraries/src/Console/CheckJoomlaUpdatesCommand.php @@ -1,4 +1,5 @@ %command.name% will check for Joomla updates + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'core:check-updates'; + + /** + * Stores the Update Information + * + * @var UpdateModel + * @since 4.0.0 + */ + private $updateInfo; + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% will check for Joomla updates \nUsage: php %command.full_name%"; - $this->setDescription('Check for Joomla updates'); - $this->setHelp($help); - } - - /** - * Retrieves Update Information - * - * @return mixed - * - * @since 4.0.0 - */ - private function getUpdateInformationFromModel() - { - $app = $this->getApplication(); - $updatemodel = $app->bootComponent('com_joomlaupdate')->getMVCFactory($app)->createModel('Update', 'Administrator'); - $updatemodel->purge(); - $updatemodel->refreshUpdates(true); - - return $updatemodel; - } - - /** - * Gets the Update Information - * - * @return mixed - * - * @since 4.0.0 - */ - public function getUpdateInfo() - { - if (!$this->updateInfo) - { - $this->setUpdateInfo(); - } - - return $this->updateInfo; - } - - /** - * Sets the Update Information - * - * @param null $info stores update Information - * - * @return void - * - * @since 4.0.0 - */ - public function setUpdateInfo($info = null): void - { - if (!$info) - { - $this->updateInfo = $this->getUpdateInformationFromModel(); - } - else - { - $this->updateInfo = $info; - } - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $symfonyStyle = new SymfonyStyle($input, $output); - - $model = $this->getUpdateInfo(); - $data = $model->getUpdateInformation(); - $symfonyStyle->title('Joomla! Updates'); - - if (!$data['hasUpdate']) - { - $symfonyStyle->success('You already have the latest Joomla version ' . $data['latest']); - - return Command::SUCCESS; - } - - $symfonyStyle->note('New Joomla Version ' . $data['latest'] . ' is available.'); - - if (!isset($data['object']->downloadurl->_data)) - { - $symfonyStyle->warning('We cannot find an update URL'); - } - - return Command::SUCCESS; - } + $this->setDescription('Check for Joomla updates'); + $this->setHelp($help); + } + + /** + * Retrieves Update Information + * + * @return mixed + * + * @since 4.0.0 + */ + private function getUpdateInformationFromModel() + { + $app = $this->getApplication(); + $updatemodel = $app->bootComponent('com_joomlaupdate')->getMVCFactory($app)->createModel('Update', 'Administrator'); + $updatemodel->purge(); + $updatemodel->refreshUpdates(true); + + return $updatemodel; + } + + /** + * Gets the Update Information + * + * @return mixed + * + * @since 4.0.0 + */ + public function getUpdateInfo() + { + if (!$this->updateInfo) { + $this->setUpdateInfo(); + } + + return $this->updateInfo; + } + + /** + * Sets the Update Information + * + * @param null $info stores update Information + * + * @return void + * + * @since 4.0.0 + */ + public function setUpdateInfo($info = null): void + { + if (!$info) { + $this->updateInfo = $this->getUpdateInformationFromModel(); + } else { + $this->updateInfo = $info; + } + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $symfonyStyle = new SymfonyStyle($input, $output); + + $model = $this->getUpdateInfo(); + $data = $model->getUpdateInformation(); + $symfonyStyle->title('Joomla! Updates'); + + if (!$data['hasUpdate']) { + $symfonyStyle->success('You already have the latest Joomla version ' . $data['latest']); + + return Command::SUCCESS; + } + + $symfonyStyle->note('New Joomla Version ' . $data['latest'] . ' is available.'); + + if (!isset($data['object']->downloadurl->_data)) { + $symfonyStyle->warning('We cannot find an update URL'); + } + + return Command::SUCCESS; + } } diff --git a/libraries/src/Console/CheckUpdatesCommand.php b/libraries/src/Console/CheckUpdatesCommand.php index a1948642eb4d8..f68b9054f4e60 100644 --- a/libraries/src/Console/CheckUpdatesCommand.php +++ b/libraries/src/Console/CheckUpdatesCommand.php @@ -1,4 +1,5 @@ title('Fetching Extension Updates'); + $symfonyStyle->title('Fetching Extension Updates'); - // Get the update cache time - $component = ComponentHelper::getComponent('com_installer'); + // Get the update cache time + $component = ComponentHelper::getComponent('com_installer'); - $cache_timeout = 3600 * (int) $component->getParams()->get('cachetimeout', 6); + $cache_timeout = 3600 * (int) $component->getParams()->get('cachetimeout', 6); - // Find all updates - $ret = Updater::getInstance()->findUpdates(0, $cache_timeout); + // Find all updates + $ret = Updater::getInstance()->findUpdates(0, $cache_timeout); - if ($ret) - { - $symfonyStyle->note('There are available updates to apply'); - $symfonyStyle->success('Check complete.'); - } - else - { - $symfonyStyle->success('There are no available updates'); - } + if ($ret) { + $symfonyStyle->note('There are available updates to apply'); + $symfonyStyle->success('Check complete.'); + } else { + $symfonyStyle->success('There are no available updates'); + } - return Command::SUCCESS; - } + return Command::SUCCESS; + } - /** - * Configure the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $help = "%command.name% command checks for pending extension updates + /** + * Configure the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% command checks for pending extension updates \nUsage: php %command.full_name%"; - $this->setDescription('Check for pending extension updates'); - $this->setHelp($help); - } + $this->setDescription('Check for pending extension updates'); + $this->setHelp($help); + } } diff --git a/libraries/src/Console/CleanCacheCommand.php b/libraries/src/Console/CleanCacheCommand.php index 55fd1e3aac439..9777356c5a42d 100644 --- a/libraries/src/Console/CleanCacheCommand.php +++ b/libraries/src/Console/CleanCacheCommand.php @@ -1,4 +1,5 @@ title('Cleaning System Cache'); - - $cache = $this->getApplication()->bootComponent('com_cache')->getMVCFactory(); - /** @var Joomla\Component\Cache\Administrator\Model\CacheModel $model */ - $model = $cache->createModel('Cache', 'Administrator', ['ignore_request' => true]); - - if ($input->getArgument('expired')) - { - if (!$model->purge()) - { - $symfonyStyle->error('Expired Cache not cleaned'); - - return Command::FAILURE; - } - - $symfonyStyle->success('Expired Cache cleaned'); - - return Command::SUCCESS; - } - - if (!$model->clean()) - { - $symfonyStyle->error('Cache not cleaned'); - - return Command::FAILURE; - } - - $symfonyStyle->success('Cache cleaned'); - - return Command::SUCCESS; - } - - /** - * Configure the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $help = "%command.name% will clear entries from the system cache + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'cache:clean'; + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $symfonyStyle = new SymfonyStyle($input, $output); + + $symfonyStyle->title('Cleaning System Cache'); + + $cache = $this->getApplication()->bootComponent('com_cache')->getMVCFactory(); + /** @var Joomla\Component\Cache\Administrator\Model\CacheModel $model */ + $model = $cache->createModel('Cache', 'Administrator', ['ignore_request' => true]); + + if ($input->getArgument('expired')) { + if (!$model->purge()) { + $symfonyStyle->error('Expired Cache not cleaned'); + + return Command::FAILURE; + } + + $symfonyStyle->success('Expired Cache cleaned'); + + return Command::SUCCESS; + } + + if (!$model->clean()) { + $symfonyStyle->error('Cache not cleaned'); + + return Command::FAILURE; + } + + $symfonyStyle->success('Cache cleaned'); + + return Command::SUCCESS; + } + + /** + * Configure the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% will clear entries from the system cache \nUsage: php %command.full_name%"; - $this->addArgument('expired', InputArgument::OPTIONAL, 'will clear expired entries from the system cache'); - $this->setDescription('Clean cache entries'); - $this->setHelp($help); - } + $this->addArgument('expired', InputArgument::OPTIONAL, 'will clear expired entries from the system cache'); + $this->setDescription('Clean cache entries'); + $this->setHelp($help); + } } diff --git a/libraries/src/Console/DeleteUserCommand.php b/libraries/src/Console/DeleteUserCommand.php index 704a3b299e7a6..cf6c72d5891ad 100644 --- a/libraries/src/Console/DeleteUserCommand.php +++ b/libraries/src/Console/DeleteUserCommand.php @@ -1,4 +1,5 @@ setDatabase($db); - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - - $this->ioStyle->title('Delete users'); - - $this->username = $this->getStringFromOption('username', 'Please enter a username'); - - $userId = UserHelper::getUserId($this->username); - $db = $this->getDatabase(); - - if (empty($userId)) - { - $this->ioStyle->error($this->username . ' does not exist!'); - - return Command::FAILURE; - } - - if ($input->isInteractive() && !$this->ioStyle->confirm('Are you sure you want to delete this user?', false)) - { - $this->ioStyle->note('User not deleted'); - - return Command::SUCCESS; - } - - $groups = UserHelper::getUserGroups($userId); - $user = User::getInstance($userId); - - if ($user->block == 0) - { - foreach ($groups as $groupId) - { - if (Access::checkGroup($groupId, 'core.admin')) - { - $queryUser = $db->getQuery(true); - $queryUser->select('COUNT(*)') - ->from($db->quoteName('#__users', 'u')) - ->leftJoin( - $db->quoteName('#__user_usergroup_map', 'g'), - '(' . $db->quoteName('u.id') . ' = ' . $db->quoteName('g.user_id') . ')' - ) - ->where($db->quoteName('g.group_id') . " = :groupId") - ->where($db->quoteName('u.block') . " = 0") - ->bind(':groupId', $groupId, ParameterType::INTEGER); - - $db->setQuery($queryUser); - $activeSuperUser = $db->loadResult(); - - if ($activeSuperUser < 2) - { - $this->ioStyle->error("You can't delete the last active Super User"); - - return Command::FAILURE; - } - } - } - } - - // Trigger delete of user - $result = $user->delete(); - - if (!$result) - { - $this->ioStyle->error("Can't remove " . $this->username . ' from usertable'); - - return Command::FAILURE; - } - - $this->ioStyle->success('User ' . $this->username . ' deleted!'); - - return Command::SUCCESS; - } - - /** - * Method to get a value from option - * - * @param string $option set the option name - * - * @param string $question set the question if user enters no value to option - * - * @return string - * - * @since 4.0.0 - */ - protected function getStringFromOption($option, $question): string - { - $answer = (string) $this->getApplication()->getConsoleInput()->getOption($option); - - while (!$answer) - { - $answer = (string) $this->ioStyle->ask($question); - } - - return $answer; - } - - /** - * Configure the IO. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return void - * - * @since 4.0.0 - */ - private function configureIO(InputInterface $input, OutputInterface $output) - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Configure the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $help = "%command.name% deletes a user + use DatabaseAwareTrait; + + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'user:delete'; + + /** + * SymfonyStyle Object + * @var object + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Stores the Input Object + * @var object + * @since 4.0.0 + */ + private $cliInput; + + /** + * The username + * + * @var string + * + * @since 4.0.0 + */ + private $username; + + /** + * Command constructor. + * + * @param DatabaseInterface $db The database + * + * @since 4.2.0 + */ + public function __construct(DatabaseInterface $db) + { + parent::__construct(); + + $this->setDatabase($db); + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + + $this->ioStyle->title('Delete users'); + + $this->username = $this->getStringFromOption('username', 'Please enter a username'); + + $userId = UserHelper::getUserId($this->username); + $db = $this->getDatabase(); + + if (empty($userId)) { + $this->ioStyle->error($this->username . ' does not exist!'); + + return Command::FAILURE; + } + + if ($input->isInteractive() && !$this->ioStyle->confirm('Are you sure you want to delete this user?', false)) { + $this->ioStyle->note('User not deleted'); + + return Command::SUCCESS; + } + + $groups = UserHelper::getUserGroups($userId); + $user = User::getInstance($userId); + + if ($user->block == 0) { + foreach ($groups as $groupId) { + if (Access::checkGroup($groupId, 'core.admin')) { + $queryUser = $db->getQuery(true); + $queryUser->select('COUNT(*)') + ->from($db->quoteName('#__users', 'u')) + ->leftJoin( + $db->quoteName('#__user_usergroup_map', 'g'), + '(' . $db->quoteName('u.id') . ' = ' . $db->quoteName('g.user_id') . ')' + ) + ->where($db->quoteName('g.group_id') . " = :groupId") + ->where($db->quoteName('u.block') . " = 0") + ->bind(':groupId', $groupId, ParameterType::INTEGER); + + $db->setQuery($queryUser); + $activeSuperUser = $db->loadResult(); + + if ($activeSuperUser < 2) { + $this->ioStyle->error("You can't delete the last active Super User"); + + return Command::FAILURE; + } + } + } + } + + // Trigger delete of user + $result = $user->delete(); + + if (!$result) { + $this->ioStyle->error("Can't remove " . $this->username . ' from usertable'); + + return Command::FAILURE; + } + + $this->ioStyle->success('User ' . $this->username . ' deleted!'); + + return Command::SUCCESS; + } + + /** + * Method to get a value from option + * + * @param string $option set the option name + * + * @param string $question set the question if user enters no value to option + * + * @return string + * + * @since 4.0.0 + */ + protected function getStringFromOption($option, $question): string + { + $answer = (string) $this->getApplication()->getConsoleInput()->getOption($option); + + while (!$answer) { + $answer = (string) $this->ioStyle->ask($question); + } + + return $answer; + } + + /** + * Configure the IO. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return void + * + * @since 4.0.0 + */ + private function configureIO(InputInterface $input, OutputInterface $output) + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Configure the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% deletes a user \nUsage: php %command.full_name%"; - $this->setDescription('Delete a user'); - $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); - $this->setHelp($help); - } + $this->setDescription('Delete a user'); + $this->addOption('username', null, InputOption::VALUE_OPTIONAL, 'username'); + $this->setHelp($help); + } } diff --git a/libraries/src/Console/ExtensionDiscoverCommand.php b/libraries/src/Console/ExtensionDiscoverCommand.php index 61f488b4653b5..76f323074a128 100644 --- a/libraries/src/Console/ExtensionDiscoverCommand.php +++ b/libraries/src/Console/ExtensionDiscoverCommand.php @@ -1,4 +1,5 @@ cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Initialise the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $help = "%command.name% is used to discover extensions + /** + * The default command name + * + * @var string + * + * @since 4.0.0 + */ + protected static $defaultName = 'extension:discover'; + + /** + * Stores the Input Object + * + * @var InputInterface + * + * @since 4.0.0 + */ + private $cliInput; + + /** + * SymfonyStyle Object + * + * @var SymfonyStyle + * + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Configures the IO + * + * @param InputInterface $input Console Input + * @param OutputInterface $output Console Output + * + * @return void + * + * @since 4.0.0 + * + */ + private function configureIO(InputInterface $input, OutputInterface $output): void + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% is used to discover extensions \nUsage: \n php %command.full_name%"; - $this->setDescription('Discover extensions'); - $this->setHelp($help); - } - - /** - * Used for discovering extensions - * - * @return integer The count of discovered extensions - * - * @throws \Exception - * - * @since 4.0.0 - */ - public function processDiscover(): int - { - $app = $this->getApplication(); - - $mvcFactory = $app->bootComponent('com_installer')->getMVCFactory(); - - $model = $mvcFactory->createModel('Discover', 'Administrator'); - - return $model->discover(); - } - - /** - * Used for finding the text for the note - * - * @param int $count The count of installed Extensions - * - * @return string The text for the note - * - * @since 4.0.0 - */ - public function getNote(int $count): string - { - if ($count < 1) - { - return 'No extensions were discovered.'; - } - elseif ($count === 1) - { - return $count . ' extension has been discovered.'; - } - else - { - return $count . ' extensions have been discovered.'; - } - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - - $count = $this->processDiscover(); - - $this->ioStyle->note($this->getNote($count)); - - return Command::SUCCESS; - } + $this->setDescription('Discover extensions'); + $this->setHelp($help); + } + + /** + * Used for discovering extensions + * + * @return integer The count of discovered extensions + * + * @throws \Exception + * + * @since 4.0.0 + */ + public function processDiscover(): int + { + $app = $this->getApplication(); + + $mvcFactory = $app->bootComponent('com_installer')->getMVCFactory(); + + $model = $mvcFactory->createModel('Discover', 'Administrator'); + + return $model->discover(); + } + + /** + * Used for finding the text for the note + * + * @param int $count The count of installed Extensions + * + * @return string The text for the note + * + * @since 4.0.0 + */ + public function getNote(int $count): string + { + if ($count < 1) { + return 'No extensions were discovered.'; + } elseif ($count === 1) { + return $count . ' extension has been discovered.'; + } else { + return $count . ' extensions have been discovered.'; + } + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + + $count = $this->processDiscover(); + + $this->ioStyle->note($this->getNote($count)); + + return Command::SUCCESS; + } } diff --git a/libraries/src/Console/ExtensionDiscoverInstallCommand.php b/libraries/src/Console/ExtensionDiscoverInstallCommand.php index 7d03ef5944c4f..6b7551f94dc4f 100644 --- a/libraries/src/Console/ExtensionDiscoverInstallCommand.php +++ b/libraries/src/Console/ExtensionDiscoverInstallCommand.php @@ -1,4 +1,5 @@ setDatabase($db); - } - - /** - * Configures the IO - * - * @param InputInterface $input Console Input - * @param OutputInterface $output Console Output - * - * @return void - * - * @since 4.0.0 - * - */ - private function configureIO(InputInterface $input, OutputInterface $output): void - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Initialise the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $this->addOption('eid', null, InputOption::VALUE_REQUIRED, 'The ID of the extension to discover'); - - $help = "%command.name% is used to discover extensions + use DatabaseAwareTrait; + + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'extension:discover:install'; + + /** + * Stores the Input Object + * + * @var InputInterface + * @since 4.0.0 + */ + private $cliInput; + + /** + * SymfonyStyle Object + * + * @var SymfonyStyle + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Instantiate the command. + * + * @param DatabaseInterface $db Database connector + * + * @since 4.0.0 + */ + public function __construct(DatabaseInterface $db) + { + parent::__construct(); + + $this->setDatabase($db); + } + + /** + * Configures the IO + * + * @param InputInterface $input Console Input + * @param OutputInterface $output Console Output + * + * @return void + * + * @since 4.0.0 + * + */ + private function configureIO(InputInterface $input, OutputInterface $output): void + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $this->addOption('eid', null, InputOption::VALUE_REQUIRED, 'The ID of the extension to discover'); + + $help = "%command.name% is used to discover extensions \nYou can provide the following option to the command: \n --eid: The ID of the extension \n If you do not provide a ID all discovered extensions are installed. \nUsage: \n php %command.full_name% --eid="; - $this->setDescription('Install discovered extensions'); - $this->setHelp($help); - } - - /** - * Used for discovering extensions - * - * @param string $eid Id of the extension - * - * @return integer The count of installed extensions - * - * @throws \Exception - * @since 4.0.0 - */ - public function processDiscover($eid): int - { - $jInstaller = new Installer; - $jInstaller->setDatabase($this->db); - $count = 0; - - if ($eid === -1) - { - $db = $this->getDatabase(); - $query = $db->getQuery(true) - ->select($db->quoteName(['extension_id'])) - ->from($db->quoteName('#__extensions')) - ->where($db->quoteName('state') . ' = -1'); - $db->setQuery($query); - $eidsToDiscover = $db->loadObjectList(); - - foreach ($eidsToDiscover as $eidToDiscover) - { - if (!$jInstaller->discover_install($eidToDiscover->extension_id)) - { - return -1; - } - - $count++; - } - - if (empty($eidsToDiscover)) - { - return 0; - } - } - else - { - if ($jInstaller->discover_install($eid)) - { - return 1; - } - else - { - return -1; - } - } - - return $count; - } - - /** - * Used for finding the text for the note - * - * @param int $count Number of extensions to install - * @param int $eid ID of the extension or -1 if no special - * - * @return string The text for the note - * - * @since 4.0.0 - */ - public function getNote(int $count, int $eid): string - { - if ($count < 0 && $eid >= 0) - { - return 'Unable to install the extension with ID ' . $eid; - } - elseif ($count < 0 && $eid < 0) - { - return 'Unable to install discovered extensions.'; - } - elseif ($count === 0) - { - return 'There are no pending discovered extensions for install. Perhaps you need to run extension:discover first?'; - } - elseif ($count === 1 && $eid > 0) - { - return 'Extension with ID ' . $eid . ' installed successfully.'; - } - elseif ($count === 1 && $eid < 0) - { - return $count . ' discovered extension has been installed.'; - } - elseif ($count > 1 && $eid < 0) - { - return $count . ' discovered extensions have been installed.'; - } - else - { - return 'The return value is not possible and has to be checked.'; - } - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - - if ($eid = $this->cliInput->getOption('eid')) - { - $result = $this->processDiscover($eid); - - if ($result === -1) - { - $this->ioStyle->error($this->getNote($result, $eid)); - - return Command::FAILURE; - } - else - { - $this->ioStyle->success($this->getNote($result, $eid)); - - return Command::SUCCESS; - } - } - else - { - $result = $this->processDiscover(-1); - - if ($result < 0) - { - $this->ioStyle->error($this->getNote($result, -1)); - - return Command::FAILURE; - } - elseif ($result === 0) - { - $this->ioStyle->note($this->getNote($result, -1)); - - return Command::SUCCESS; - } - - else - { - $this->ioStyle->note($this->getNote($result, -1)); - - return Command::SUCCESS; - } - } - } + $this->setDescription('Install discovered extensions'); + $this->setHelp($help); + } + + /** + * Used for discovering extensions + * + * @param string $eid Id of the extension + * + * @return integer The count of installed extensions + * + * @throws \Exception + * @since 4.0.0 + */ + public function processDiscover($eid): int + { + $jInstaller = new Installer(); + $jInstaller->setDatabase($this->db); + $count = 0; + + if ($eid === -1) { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select($db->quoteName(['extension_id'])) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('state') . ' = -1'); + $db->setQuery($query); + $eidsToDiscover = $db->loadObjectList(); + + foreach ($eidsToDiscover as $eidToDiscover) { + if (!$jInstaller->discover_install($eidToDiscover->extension_id)) { + return -1; + } + + $count++; + } + + if (empty($eidsToDiscover)) { + return 0; + } + } else { + if ($jInstaller->discover_install($eid)) { + return 1; + } else { + return -1; + } + } + + return $count; + } + + /** + * Used for finding the text for the note + * + * @param int $count Number of extensions to install + * @param int $eid ID of the extension or -1 if no special + * + * @return string The text for the note + * + * @since 4.0.0 + */ + public function getNote(int $count, int $eid): string + { + if ($count < 0 && $eid >= 0) { + return 'Unable to install the extension with ID ' . $eid; + } elseif ($count < 0 && $eid < 0) { + return 'Unable to install discovered extensions.'; + } elseif ($count === 0) { + return 'There are no pending discovered extensions for install. Perhaps you need to run extension:discover first?'; + } elseif ($count === 1 && $eid > 0) { + return 'Extension with ID ' . $eid . ' installed successfully.'; + } elseif ($count === 1 && $eid < 0) { + return $count . ' discovered extension has been installed.'; + } elseif ($count > 1 && $eid < 0) { + return $count . ' discovered extensions have been installed.'; + } else { + return 'The return value is not possible and has to be checked.'; + } + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + + if ($eid = $this->cliInput->getOption('eid')) { + $result = $this->processDiscover($eid); + + if ($result === -1) { + $this->ioStyle->error($this->getNote($result, $eid)); + + return Command::FAILURE; + } else { + $this->ioStyle->success($this->getNote($result, $eid)); + + return Command::SUCCESS; + } + } else { + $result = $this->processDiscover(-1); + + if ($result < 0) { + $this->ioStyle->error($this->getNote($result, -1)); + + return Command::FAILURE; + } elseif ($result === 0) { + $this->ioStyle->note($this->getNote($result, -1)); + + return Command::SUCCESS; + } else { + $this->ioStyle->note($this->getNote($result, -1)); + + return Command::SUCCESS; + } + } + } } diff --git a/libraries/src/Console/ExtensionDiscoverListCommand.php b/libraries/src/Console/ExtensionDiscoverListCommand.php index 9c5edaecdaebe..b2974e5584107 100644 --- a/libraries/src/Console/ExtensionDiscoverListCommand.php +++ b/libraries/src/Console/ExtensionDiscoverListCommand.php @@ -1,4 +1,5 @@ %command.name% is used to list all extensions that could be installed via discoverinstall + /** + * The default command name + * + * @var string + * + * @since 4.0.0 + */ + protected static $defaultName = 'extension:discover:list'; + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $help = "%command.name% is used to list all extensions that could be installed via discoverinstall \nUsage: \n php %command.full_name%"; - $this->setDescription('List discovered extensions'); - $this->setHelp($help); - } - - /** - * Filters the extension state - * - * @param array $extensions The Extensions - * @param string $state The Extension state - * - * @return array - * - * @since 4.0.0 - */ - public function filterExtensionsBasedOnState($extensions, $state): array - { - $filteredExtensions = []; - - foreach ($extensions as $key => $extension) - { - if ($extension['state'] === $state) - { - $filteredExtensions[] = $extension; - } - } - - return $filteredExtensions; - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - - $extensions = $this->getExtensions(); - $state = -1; - - $discovered_extensions = $this->filterExtensionsBasedOnState($extensions, $state); - - if (empty($discovered_extensions)) - { - $this->ioStyle->note("There are no pending discovered extensions to install. Perhaps you need to run extension:discover first?"); - - return Command::SUCCESS; - } - - $discovered_extensions = $this->getExtensionsNameAndId($discovered_extensions); - - $this->ioStyle->title('Discovered extensions.'); - $this->ioStyle->table(['Name', 'Extension ID', 'Version', 'Type', 'Active'], $discovered_extensions); - - return Command::SUCCESS; - } + $this->setDescription('List discovered extensions'); + $this->setHelp($help); + } + + /** + * Filters the extension state + * + * @param array $extensions The Extensions + * @param string $state The Extension state + * + * @return array + * + * @since 4.0.0 + */ + public function filterExtensionsBasedOnState($extensions, $state): array + { + $filteredExtensions = []; + + foreach ($extensions as $key => $extension) { + if ($extension['state'] === $state) { + $filteredExtensions[] = $extension; + } + } + + return $filteredExtensions; + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + + $extensions = $this->getExtensions(); + $state = -1; + + $discovered_extensions = $this->filterExtensionsBasedOnState($extensions, $state); + + if (empty($discovered_extensions)) { + $this->ioStyle->note("There are no pending discovered extensions to install. Perhaps you need to run extension:discover first?"); + + return Command::SUCCESS; + } + + $discovered_extensions = $this->getExtensionsNameAndId($discovered_extensions); + + $this->ioStyle->title('Discovered extensions.'); + $this->ioStyle->table(['Name', 'Extension ID', 'Version', 'Type', 'Active'], $discovered_extensions); + + return Command::SUCCESS; + } } diff --git a/libraries/src/Console/ExtensionInstallCommand.php b/libraries/src/Console/ExtensionInstallCommand.php index cf3758ecdfc53..457185f0d4978 100644 --- a/libraries/src/Console/ExtensionInstallCommand.php +++ b/libraries/src/Console/ExtensionInstallCommand.php @@ -1,4 +1,5 @@ cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Initialise the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $this->addOption('path', null, InputOption::VALUE_REQUIRED, 'The path to the extension'); - $this->addOption('url', null, InputOption::VALUE_REQUIRED, 'The url to the extension'); - - $help = "%command.name% is used to install extensions + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'extension:install'; + + /** + * Stores the Input Object + * @var InputInterface + * @since 4.0.0 + */ + private $cliInput; + + /** + * SymfonyStyle Object + * @var SymfonyStyle + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Exit Code For installation failure + * @since 4.0.0 + */ + public const INSTALLATION_FAILED = 1; + + /** + * Exit Code For installation Success + * @since 4.0.0 + */ + public const INSTALLATION_SUCCESSFUL = 0; + + /** + * Configures the IO + * + * @param InputInterface $input Console Input + * @param OutputInterface $output Console Output + * + * @return void + * + * @since 4.0.0 + * + */ + private function configureIO(InputInterface $input, OutputInterface $output): void + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $this->addOption('path', null, InputOption::VALUE_REQUIRED, 'The path to the extension'); + $this->addOption('url', null, InputOption::VALUE_REQUIRED, 'The url to the extension'); + + $help = "%command.name% is used to install extensions \nYou must provide one of the following options to the command: \n --path: The path on your local filesystem to the install package \n --url: The URL from where the install package should be downloaded @@ -96,127 +95,119 @@ protected function configure(): void \n php %command.full_name% --path= \n php %command.full_name% --url="; - $this->setDescription('Install an extension from a URL or from a path'); - $this->setHelp($help); - } - - /** - * Used for installing extension from a path - * - * @param string $path Path to the extension zip file - * - * @return boolean - * - * @since 4.0.0 - * - * @throws \Exception - */ - public function processPathInstallation($path): bool - { - if (!file_exists($path)) - { - $this->ioStyle->warning('The file path specified does not exist.'); - - return false; - } - - $tmpPath = $this->getApplication()->get('tmp_path'); - $tmpPath = $tmpPath . '/' . basename($path); - $package = InstallerHelper::unpack($path, true); - - if ($package['type'] === false) - { - return false; - } - - $jInstaller = Installer::getInstance(); - $result = $jInstaller->install($package['extractdir']); - InstallerHelper::cleanupInstall($tmpPath, $package['extractdir']); - - return $result; - } - - - /** - * Used for installing extension from a URL - * - * @param string $url URL to the extension zip file - * - * @return boolean - * - * @since 4.0.0 - * - * @throws \Exception - */ - public function processUrlInstallation($url): bool - { - $filename = InstallerHelper::downloadPackage($url); - - $tmpPath = $this->getApplication()->get('tmp_path'); - - $path = $tmpPath . '/' . basename($filename); - $package = InstallerHelper::unpack($path, true); - - if ($package['type'] === false) - { - return false; - } - - $jInstaller = new Installer; - $result = $jInstaller->install($package['extractdir']); - InstallerHelper::cleanupInstall($path, $package['extractdir']); - - return $result; - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @throws \Exception - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - - if ($path = $this->cliInput->getOption('path')) - { - $result = $this->processPathInstallation($path); - - if (!$result) - { - $this->ioStyle->error('Unable to install extension'); - - return self::INSTALLATION_FAILED; - } - - $this->ioStyle->success('Extension installed successfully.'); - - return self::INSTALLATION_SUCCESSFUL; - } - elseif ($url = $this->cliInput->getOption('url')) - { - $result = $this->processUrlInstallation($url); - - if (!$result) - { - $this->ioStyle->error('Unable to install extension'); - - return self::INSTALLATION_FAILED; - } - - $this->ioStyle->success('Extension installed successfully.'); - - return self::INSTALLATION_SUCCESSFUL; - } - - $this->ioStyle->error('Invalid argument supplied for command.'); - - return self::INSTALLATION_FAILED; - } + $this->setDescription('Install an extension from a URL or from a path'); + $this->setHelp($help); + } + + /** + * Used for installing extension from a path + * + * @param string $path Path to the extension zip file + * + * @return boolean + * + * @since 4.0.0 + * + * @throws \Exception + */ + public function processPathInstallation($path): bool + { + if (!file_exists($path)) { + $this->ioStyle->warning('The file path specified does not exist.'); + + return false; + } + + $tmpPath = $this->getApplication()->get('tmp_path'); + $tmpPath = $tmpPath . '/' . basename($path); + $package = InstallerHelper::unpack($path, true); + + if ($package['type'] === false) { + return false; + } + + $jInstaller = Installer::getInstance(); + $result = $jInstaller->install($package['extractdir']); + InstallerHelper::cleanupInstall($tmpPath, $package['extractdir']); + + return $result; + } + + + /** + * Used for installing extension from a URL + * + * @param string $url URL to the extension zip file + * + * @return boolean + * + * @since 4.0.0 + * + * @throws \Exception + */ + public function processUrlInstallation($url): bool + { + $filename = InstallerHelper::downloadPackage($url); + + $tmpPath = $this->getApplication()->get('tmp_path'); + + $path = $tmpPath . '/' . basename($filename); + $package = InstallerHelper::unpack($path, true); + + if ($package['type'] === false) { + return false; + } + + $jInstaller = new Installer(); + $result = $jInstaller->install($package['extractdir']); + InstallerHelper::cleanupInstall($path, $package['extractdir']); + + return $result; + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @throws \Exception + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + + if ($path = $this->cliInput->getOption('path')) { + $result = $this->processPathInstallation($path); + + if (!$result) { + $this->ioStyle->error('Unable to install extension'); + + return self::INSTALLATION_FAILED; + } + + $this->ioStyle->success('Extension installed successfully.'); + + return self::INSTALLATION_SUCCESSFUL; + } elseif ($url = $this->cliInput->getOption('url')) { + $result = $this->processUrlInstallation($url); + + if (!$result) { + $this->ioStyle->error('Unable to install extension'); + + return self::INSTALLATION_FAILED; + } + + $this->ioStyle->success('Extension installed successfully.'); + + return self::INSTALLATION_SUCCESSFUL; + } + + $this->ioStyle->error('Invalid argument supplied for command.'); + + return self::INSTALLATION_FAILED; + } } diff --git a/libraries/src/Console/ExtensionRemoveCommand.php b/libraries/src/Console/ExtensionRemoveCommand.php index 3b0dfe088cb11..8244c6f2335e2 100644 --- a/libraries/src/Console/ExtensionRemoveCommand.php +++ b/libraries/src/Console/ExtensionRemoveCommand.php @@ -1,4 +1,5 @@ setDatabase($db); - } - - /** - * Configures the IO - * - * @param InputInterface $input Console Input - * @param OutputInterface $output Console Output - * - * @return void - * - * @since 4.0.0 - * - */ - private function configureIO(InputInterface $input, OutputInterface $output): void - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - $language = Factory::getLanguage(); - $language->load('', JPATH_ADMINISTRATOR, null, false, false) || - $language->load('', JPATH_ADMINISTRATOR, null, true); - $language->load('com_installer', JPATH_ADMINISTRATOR, null, false, false)|| - $language->load('com_installer', JPATH_ADMINISTRATOR, null, true); - } - - /** - * Initialise the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $this->addArgument( - 'extensionId', - InputArgument::REQUIRED, - 'ID of extension to be removed (run extension:list command to check)' - ); - - $help = "%command.name% is used to uninstall extensions. + use DatabaseAwareTrait; + + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'extension:remove'; + + /** + * @var InputInterface + * @since 4.0.0 + */ + private $cliInput; + + /** + * @var SymfonyStyle + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Exit Code for extensions remove abort + * @since 4.0.0 + */ + public const REMOVE_ABORT = 3; + + /** + * Exit Code for extensions remove failure + * @since 4.0.0 + */ + public const REMOVE_FAILED = 1; + + /** + * Exit Code for invalid response + * @since 4.0.0 + */ + public const REMOVE_INVALID_RESPONSE = 5; + + /** + * Exit Code for invalid type + * @since 4.0.0 + */ + public const REMOVE_INVALID_TYPE = 6; + + /** + * Exit Code for extensions locked remove failure + * @since 4.0.0 + */ + public const REMOVE_LOCKED = 4; + + /** + * Exit Code for extensions not found + * @since 4.0.0 + */ + public const REMOVE_NOT_FOUND = 2; + + /** + * Exit Code for extensions remove success + * @since 4.0.0 + */ + public const REMOVE_SUCCESSFUL = 0; + + /** + * Command constructor. + * + * @param DatabaseInterface $db The database + * + * @since 4.2.0 + */ + public function __construct(DatabaseInterface $db) + { + parent::__construct(); + + $this->setDatabase($db); + } + + /** + * Configures the IO + * + * @param InputInterface $input Console Input + * @param OutputInterface $output Console Output + * + * @return void + * + * @since 4.0.0 + * + */ + private function configureIO(InputInterface $input, OutputInterface $output): void + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + $language = Factory::getLanguage(); + $language->load('', JPATH_ADMINISTRATOR, null, false, false) || + $language->load('', JPATH_ADMINISTRATOR, null, true); + $language->load('com_installer', JPATH_ADMINISTRATOR, null, false, false) || + $language->load('com_installer', JPATH_ADMINISTRATOR, null, true); + } + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $this->addArgument( + 'extensionId', + InputArgument::REQUIRED, + 'ID of extension to be removed (run extension:list command to check)' + ); + + $help = "%command.name% is used to uninstall extensions. \nThe command requires one argument, the ID of the extension to uninstall. \nYou may find this ID by running the extension:list command. \nUsage: php %command.full_name% "; - $this->setDescription('Remove an extension'); - $this->setHelp($help); - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - $extensionId = $this->cliInput->getArgument('extensionId'); - - $response = $this->ioStyle->ask('Are you sure you want to remove this extension?', 'yes/no'); - - if (strtolower($response) === 'yes') - { - // Get an installer object for the extension type - $installer = Installer::getInstance(); - $row = new Extension($this->getDatabase()); - - if ((int) $extensionId === 0 || !$row->load($extensionId)) - { - $this->ioStyle->error("Extension with ID of $extensionId not found."); - - return self::REMOVE_NOT_FOUND; - } - - // Do not allow to uninstall locked extensions. - if ((int) $row->locked === 1) - { - $this->ioStyle->error(Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR_LOCKED_EXTENSION', $row->name, $extensionId)); - - return self::REMOVE_LOCKED; - } - - if ($row->type) - { - if (!$installer->uninstall($row->type, $extensionId)) - { - $this->ioStyle->error('Extension not removed.'); - - return self::REMOVE_FAILED; - } - - $this->ioStyle->success('Extension removed!'); - - return self::REMOVE_SUCCESSFUL; - } - - return self::REMOVE_INVALID_TYPE; - } - elseif (strtolower($response) === 'no') - { - $this->ioStyle->note('Extension not removed.'); - - return self::REMOVE_ABORT; - } - - $this->ioStyle->warning('Invalid response'); - - return self::REMOVE_INVALID_RESPONSE; - } + $this->setDescription('Remove an extension'); + $this->setHelp($help); + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + $extensionId = $this->cliInput->getArgument('extensionId'); + + $response = $this->ioStyle->ask('Are you sure you want to remove this extension?', 'yes/no'); + + if (strtolower($response) === 'yes') { + // Get an installer object for the extension type + $installer = Installer::getInstance(); + $row = new Extension($this->getDatabase()); + + if ((int) $extensionId === 0 || !$row->load($extensionId)) { + $this->ioStyle->error("Extension with ID of $extensionId not found."); + + return self::REMOVE_NOT_FOUND; + } + + // Do not allow to uninstall locked extensions. + if ((int) $row->locked === 1) { + $this->ioStyle->error(Text::sprintf('COM_INSTALLER_UNINSTALL_ERROR_LOCKED_EXTENSION', $row->name, $extensionId)); + + return self::REMOVE_LOCKED; + } + + if ($row->type) { + if (!$installer->uninstall($row->type, $extensionId)) { + $this->ioStyle->error('Extension not removed.'); + + return self::REMOVE_FAILED; + } + + $this->ioStyle->success('Extension removed!'); + + return self::REMOVE_SUCCESSFUL; + } + + return self::REMOVE_INVALID_TYPE; + } elseif (strtolower($response) === 'no') { + $this->ioStyle->note('Extension not removed.'); + + return self::REMOVE_ABORT; + } + + $this->ioStyle->warning('Invalid response'); + + return self::REMOVE_INVALID_RESPONSE; + } } diff --git a/libraries/src/Console/ExtensionsListCommand.php b/libraries/src/Console/ExtensionsListCommand.php index 51427c6935c59..b86cd863a0844 100644 --- a/libraries/src/Console/ExtensionsListCommand.php +++ b/libraries/src/Console/ExtensionsListCommand.php @@ -1,4 +1,5 @@ setDatabase($db); - } - - /** - * Configures the IO - * - * @param InputInterface $input Console Input - * @param OutputInterface $output Console Output - * - * @return void - * - * @since 4.0.0 - * - */ - protected function configureIO(InputInterface $input, OutputInterface $output): void - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - /** - * Initialise the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - - $this->addOption('type', null, InputOption::VALUE_REQUIRED, 'Type of the extension'); - - $help = "%command.name% lists all installed extensions + use DatabaseAwareTrait; + + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'extension:list'; + + /** + * Stores the installed Extensions + * @var array + * @since 4.0.0 + */ + protected $extensions; + + /** + * Stores the Input Object + * @var InputInterface + * @since 4.0.0 + */ + protected $cliInput; + + /** + * SymfonyStyle Object + * @var SymfonyStyle + * @since 4.0.0 + */ + protected $ioStyle; + + /** + * Instantiate the command. + * + * @param DatabaseInterface $db Database connector + * + * @since 4.0.0 + */ + public function __construct(DatabaseInterface $db) + { + parent::__construct(); + + $this->setDatabase($db); + } + + /** + * Configures the IO + * + * @param InputInterface $input Console Input + * @param OutputInterface $output Console Output + * + * @return void + * + * @since 4.0.0 + * + */ + protected function configureIO(InputInterface $input, OutputInterface $output): void + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + + $this->addOption('type', null, InputOption::VALUE_REQUIRED, 'Type of the extension'); + + $help = "%command.name% lists all installed extensions \nUsage: php %command.full_name% \nYou may filter on the type of extension (component, module, plugin, etc.) using the --type option: \n php %command.full_name% --type="; - $this->setDescription('List installed extensions'); - $this->setHelp($help); - } - - /** - * Retrieves all extensions - * - * @return mixed - * - * @since 4.0.0 - */ - public function getExtensions() - { - if (!$this->extensions) - { - $this->setExtensions(); - } - - return $this->extensions; - } - - /** - * Retrieves the extension from the model and sets the class variable - * - * @param null $extensions Array of extensions - * - * @return void - * - * @since 4.0.0 - */ - public function setExtensions($extensions = null): void - { - if (!$extensions) - { - $this->extensions = $this->getAllExtensionsFromDB(); - } - else - { - $this->extensions = $extensions; - } - } - - /** - * Retrieves extension list from DB - * - * @return array - * - * @since 4.0.0 - */ - private function getAllExtensionsFromDB(): array - { - $db = $this->getDatabase(); - $query = $db->getQuery(true); - $query->select('*') - ->from('#__extensions'); - $db->setQuery($query); - $extensions = $db->loadAssocList('extension_id'); - - return $extensions; - } - - /** - * Transforms extension arrays into required form - * - * @param array $extensions Array of extensions - * - * @return array - * - * @since 4.0.0 - */ - protected function getExtensionsNameAndId($extensions): array - { - $extInfo = []; - - foreach ($extensions as $key => $extension) - { - $manifest = json_decode($extension['manifest_cache']); - $extInfo[] = [ - $extension['name'], - $extension['extension_id'], - $manifest ? $manifest->version : '--', - $extension['type'], - $extension['enabled'] == 1 ? 'Yes' : 'No', - ]; - } - - return $extInfo; - } - - /** - * Filters the extension type - * - * @param string $type Extension type - * - * @return array - * - * @since 4.0.0 - */ - private function filterExtensionsBasedOn($type): array - { - $extensions = []; - - foreach ($this->extensions as $key => $extension) - { - if ($extension['type'] == $type) - { - $extensions[] = $extension; - } - } - - return $extensions; - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - $this->configureIO($input, $output); - $extensions = $this->getExtensions(); - $type = $this->cliInput->getOption('type'); - - if ($type) - { - $extensions = $this->filterExtensionsBasedOn($type); - } - - if (empty($extensions)) - { - $this->ioStyle->error("Cannot find extensions of the type '$type' specified."); - - return Command::SUCCESS; - } - - $extensions = $this->getExtensionsNameAndId($extensions); - - $this->ioStyle->title('Installed extensions.'); - $this->ioStyle->table(['Name', 'Extension ID', 'Version', 'Type', 'Active'], $extensions); - - return Command::SUCCESS; - } + $this->setDescription('List installed extensions'); + $this->setHelp($help); + } + + /** + * Retrieves all extensions + * + * @return mixed + * + * @since 4.0.0 + */ + public function getExtensions() + { + if (!$this->extensions) { + $this->setExtensions(); + } + + return $this->extensions; + } + + /** + * Retrieves the extension from the model and sets the class variable + * + * @param null $extensions Array of extensions + * + * @return void + * + * @since 4.0.0 + */ + public function setExtensions($extensions = null): void + { + if (!$extensions) { + $this->extensions = $this->getAllExtensionsFromDB(); + } else { + $this->extensions = $extensions; + } + } + + /** + * Retrieves extension list from DB + * + * @return array + * + * @since 4.0.0 + */ + private function getAllExtensionsFromDB(): array + { + $db = $this->getDatabase(); + $query = $db->getQuery(true); + $query->select('*') + ->from('#__extensions'); + $db->setQuery($query); + $extensions = $db->loadAssocList('extension_id'); + + return $extensions; + } + + /** + * Transforms extension arrays into required form + * + * @param array $extensions Array of extensions + * + * @return array + * + * @since 4.0.0 + */ + protected function getExtensionsNameAndId($extensions): array + { + $extInfo = []; + + foreach ($extensions as $key => $extension) { + $manifest = json_decode($extension['manifest_cache']); + $extInfo[] = [ + $extension['name'], + $extension['extension_id'], + $manifest ? $manifest->version : '--', + $extension['type'], + $extension['enabled'] == 1 ? 'Yes' : 'No', + ]; + } + + return $extInfo; + } + + /** + * Filters the extension type + * + * @param string $type Extension type + * + * @return array + * + * @since 4.0.0 + */ + private function filterExtensionsBasedOn($type): array + { + $extensions = []; + + foreach ($this->extensions as $key => $extension) { + if ($extension['type'] == $type) { + $extensions[] = $extension; + } + } + + return $extensions; + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + $this->configureIO($input, $output); + $extensions = $this->getExtensions(); + $type = $this->cliInput->getOption('type'); + + if ($type) { + $extensions = $this->filterExtensionsBasedOn($type); + } + + if (empty($extensions)) { + $this->ioStyle->error("Cannot find extensions of the type '$type' specified."); + + return Command::SUCCESS; + } + + $extensions = $this->getExtensionsNameAndId($extensions); + + $this->ioStyle->title('Installed extensions.'); + $this->ioStyle->table(['Name', 'Extension ID', 'Version', 'Type', 'Active'], $extensions); + + return Command::SUCCESS; + } } diff --git a/libraries/src/Console/FinderIndexCommand.php b/libraries/src/Console/FinderIndexCommand.php index c15103288ffc6..54ad4c4753893 100644 --- a/libraries/src/Console/FinderIndexCommand.php +++ b/libraries/src/Console/FinderIndexCommand.php @@ -1,4 +1,5 @@ db = $db; - parent::__construct(); - } - - /** - * Initialise the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $this->addArgument('purge', InputArgument::OPTIONAL, 'Purge the index and rebuilds'); - $this->addOption('minproctime', null, InputOption::VALUE_REQUIRED, 'Minimum processing time in seconds, in order to apply a pause', 1); - $this->addOption('pause', null, InputOption::VALUE_REQUIRED, 'Pausing type or defined pause time in seconds', 'division'); - $this->addOption('divisor', null, InputOption::VALUE_REQUIRED, 'The divisor of the division: batch-processing time / divisor', 5); - $help = <<<'EOF' + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'finder:index'; + + /** + * Stores the Input Object + * + * @var InputInterface + * @since 4.0.0 + */ + private $cliInput; + + /** + * SymfonyStyle Object + * + * @var SymfonyStyle + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Database connector + * + * @var DatabaseInterface + * @since 4.0.0 + */ + private $db; + + /** + * Start time for the index process + * + * @var string + * @since 2.5 + */ + private $time; + + /** + * Start time for each batch + * + * @var string + * @since 2.5 + */ + private $qtime; + + /** + * Static filters information. + * + * @var array + * @since 3.3 + */ + private $filters = array(); + + /** + * Pausing type or defined pause time in seconds. + * One pausing type is implemented: 'division' for dynamic calculation of pauses + * + * Defaults to 'division' + * + * @var string|integer + * @since 3.9.12 + */ + private $pause = 'division'; + + /** + * The divisor of the division: batch-processing time / divisor. + * This is used together with --pause=division in order to pause dynamically + * in relation to the processing time + * Defaults to 5 + * + * @var integer + * @since 3.9.12 + */ + private $divisor = 5; + + /** + * Minimum processing time in seconds, in order to apply a pause + * Defaults to 1 + * + * @var integer + * @since 3.9.12 + */ + private $minimumBatchProcessingTime = 1; + + /** + * Instantiate the command. + * + * @param DatabaseInterface $db Database connector + * + * @since 4.0.0 + */ + public function __construct(DatabaseInterface $db) + { + $this->db = $db; + parent::__construct(); + } + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $this->addArgument('purge', InputArgument::OPTIONAL, 'Purge the index and rebuilds'); + $this->addOption('minproctime', null, InputOption::VALUE_REQUIRED, 'Minimum processing time in seconds, in order to apply a pause', 1); + $this->addOption('pause', null, InputOption::VALUE_REQUIRED, 'Pausing type or defined pause time in seconds', 'division'); + $this->addOption('divisor', null, InputOption::VALUE_REQUIRED, 'The divisor of the division: batch-processing time / divisor', 5); + $help = <<<'EOF' The %command.name% Purges and rebuilds the index (search filters are preserved). php %command.full_name% EOF; - $this->setDescription('Purges and rebuild the index'); - $this->setHelp($help); - } - - /** - * Internal function to execute the command. - * - * @param InputInterface $input The input to inject into the command. - * @param OutputInterface $output The output to inject into the command. - * - * @return integer The command exit code - * - * @since 4.0.0 - */ - protected function doExecute(InputInterface $input, OutputInterface $output): int - { - - // Initialize the time value. - $this->time = microtime(true); - $this->configureIO($input, $output); - - $this->ioStyle->writeln( - [ - 'Finder Indexer', - '==========================', - '', - ] - ); - - if ($this->cliInput->getOption('minproctime')) - { - $this->minimumBatchProcessingTime = $this->cliInput->getOption('minproctime'); - } - - if ($this->cliInput->getOption('pause')) - { - $this->pause = $this->cliInput->getOption('pause'); - } - - if ($this->cliInput->getOption('divisor')) - { - $this->divisor = $this->cliInput->getOption('divisor'); - } - - if ($this->cliInput->getArgument('purge')) - { - // Taxonomy ids will change following a purge/index, so save filter information first. - $this->getFilters(); - - // Purge the index. - $this->purge(); - - // Run the indexer. - $this->index(); - - // Restore the filters again. - $this->putFilters(); - } - else - { - $this->index(); - } - - $this->ioStyle->newLine(1); - - // Total reporting. - $this->ioStyle->writeln( - [ - '' . Text::sprintf('FINDER_CLI_PROCESS_COMPLETE', round(microtime(true) - $this->time, 3)) . '', - '' . Text::sprintf('FINDER_CLI_PEAK_MEMORY_USAGE', number_format(memory_get_peak_usage(true))) . '', - ] - ); - - $this->ioStyle->newLine(1); - - return Command::SUCCESS; - } - - /** - * Configures the IO - * - * @param InputInterface $input Console Input - * @param OutputInterface $output Console Output - * - * @return void - * - * @since 4.0.0 - * - */ - private function configureIO(InputInterface $input, OutputInterface $output): void - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - $language = Factory::getLanguage(); - $language->load('', JPATH_ADMINISTRATOR, null, false, false) || - $language->load('', JPATH_ADMINISTRATOR, null, true); - $language->load('finder_cli', JPATH_SITE, null, false, false)|| - $language->load('finder_cli', JPATH_SITE, null, true); - } - - /** - * Save static filters. - * - * Since a purge/index cycle will cause all the taxonomy ids to change, - * the static filters need to be updated with the new taxonomy ids. - * The static filter information is saved prior to the purge/index - * so that it can later be used to update the filters with new ids. - * - * @return void - * - * @since 4.0.0 - */ - private function getFilters(): void - { - $this->ioStyle->text(Text::_('FINDER_CLI_SAVE_FILTERS')); - - // Get the taxonomy ids used by the filters. - $db = $this->db; - $query = $db->getQuery(true); - $query - ->select('filter_id, title, data') - ->from($db->quoteName('#__finder_filters')); - $filters = $db->setQuery($query)->loadObjectList(); - - // Get the name of each taxonomy and the name of its parent. - foreach ($filters as $filter) - { - // Skip empty filters. - if ($filter->data === '') - { - continue; - } - - // Get taxonomy records. - $query = $db->getQuery(true); - $query - ->select('t.title, p.title AS parent') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t') - ->leftJoin($db->quoteName('#__finder_taxonomy') . ' AS p ON p.id = t.parent_id') - ->where($db->quoteName('t.id') . ' IN (' . $filter->data . ')'); - $taxonomies = $db->setQuery($query)->loadObjectList(); - - // Construct a temporary data structure to hold the filter information. - foreach ($taxonomies as $taxonomy) - { - $this->filters[$filter->filter_id][] = array( - 'filter' => $filter->title, - 'title' => $taxonomy->title, - 'parent' => $taxonomy->parent, - ); - } - } - - $this->ioStyle->text(Text::sprintf('FINDER_CLI_SAVE_FILTER_COMPLETED', count($filters))); - } - - /** - * Purge the index. - * - * @return void - * - * @since 3.3 - */ - private function purge() - { - $this->ioStyle->text(Text::_('FINDER_CLI_INDEX_PURGE')); - - // Load the model. - $app = $this->getApplication(); - $model = $app->bootComponent('com_finder')->getMVCFactory($app)->createModel('Index', 'Administrator'); - - // Attempt to purge the index. - $return = $model->purge(); - - // If unsuccessful then abort. - if (!$return) - { - $message = Text::_('FINDER_CLI_INDEX_PURGE_FAILED', $model->getError()); - $this->ioStyle->error($message); - exit(); - } - - $this->ioStyle->text(Text::_('FINDER_CLI_INDEX_PURGE_SUCCESS')); - } - - /** - * Run the indexer. - * - * @return void - * - * @since 2.5 - */ - private function index() - { - - // Disable caching. - $app = $this->getApplication(); - $app->set('caching', 0); - $app->set('cache_handler', 'file'); - - // Reset the indexer state. - Indexer::resetState(); - - // Import the plugins. - PluginHelper::importPlugin('system'); - PluginHelper::importPlugin('finder'); - - // Starting Indexer. - $this->ioStyle->text(Text::_('FINDER_CLI_STARTING_INDEXER')); - - // Trigger the onStartIndex event. - $app->triggerEvent('onStartIndex'); - - // Remove the script time limit. - @set_time_limit(0); - - // Get the indexer state. - $state = Indexer::getState(); - - // Setting up plugins. - $this->ioStyle->text(Text::_('FINDER_CLI_SETTING_UP_PLUGINS')); - - // Trigger the onBeforeIndex event. - $app->triggerEvent('onBeforeIndex'); - - // Startup reporting. - $this->ioStyle->text(Text::sprintf('FINDER_CLI_SETUP_ITEMS', $state->totalItems, round(microtime(true) - $this->time, 3))); - - // Get the number of batches. - $t = (int) $state->totalItems; - $c = (int) ceil($t / $state->batchSize); - $c = $c === 0 ? 1 : $c; - - try - { - // Process the batches. - for ($i = 0; $i < $c; $i++) - { - // Set the batch start time. - $this->qtime = microtime(true); - - // Reset the batch offset. - $state->batchOffset = 0; - - // Trigger the onBuildIndex event. - Factory::getApplication()->triggerEvent('onBuildIndex'); - - // Batch reporting. - $text = Text::sprintf('FINDER_CLI_BATCH_COMPLETE', $i + 1, $processingTime = round(microtime(true) - $this->qtime, 3)); - $this->ioStyle->text($text); - - if ($this->pause !== 0) - { - // Pausing Section - $skip = !($processingTime >= $this->minimumBatchProcessingTime); - $pause = 0; - - if ($this->pause === 'division' && $this->divisor > 0) - { - if (!$skip) - { - $pause = round($processingTime / $this->divisor); - } - else - { - $pause = 1; - } - } - elseif ($this->pause > 0) - { - $pause = $this->pause; - } - - if ($pause > 0 && !$skip) - { - $this->ioStyle->text(Text::sprintf('FINDER_CLI_BATCH_PAUSING', $pause)); - sleep($pause); - $this->ioStyle->text(Text::_('FINDER_CLI_BATCH_CONTINUING')); - } - - if ($skip) - { - $this->ioStyle->text( - Text::sprintf( - 'FINDER_CLI_SKIPPING_PAUSE_LOW_BATCH_PROCESSING_TIME', - $processingTime, - $this->minimumBatchProcessingTime - ) - ); - } - - // End of Pausing Section - } - } - } - catch (Exception $e) - { - // Display the error - $this->ioStyle->error($e->getMessage()); - - // Reset the indexer state. - Indexer::resetState(); - - // Close the app - $app->close($e->getCode()); - } - - // Reset the indexer state. - Indexer::resetState(); - } - - /** - * Restore static filters. - * - * Using the saved filter information, update the filter records - * with the new taxonomy ids. - * - * @return void - * - * @since 3.3 - */ - private function putFilters() - { - $this->ioStyle->text(Text::_('FINDER_CLI_RESTORE_FILTERS')); - - $db = $this->db; - - // Use the temporary filter information to update the filter taxonomy ids. - foreach ($this->filters as $filter_id => $filter) - { - $tids = array(); - - foreach ($filter as $element) - { - // Look for the old taxonomy in the new taxonomy table. - $query = $db->getQuery(true); - $query - ->select('t.id') - ->from($db->quoteName('#__finder_taxonomy') . ' AS t') - ->leftJoin($db->quoteName('#__finder_taxonomy') . ' AS p ON p.id = t.parent_id') - ->where($db->quoteName('t.title') . ' = ' . $db->quote($element['title'])) - ->where($db->quoteName('p.title') . ' = ' . $db->quote($element['parent'])); - $taxonomy = $db->setQuery($query)->loadResult(); - - // If we found it then add it to the list. - if ($taxonomy) - { - $tids[] = $taxonomy; - } - else - { - $text = Text::sprintf('FINDER_CLI_FILTER_RESTORE_WARNING', $element['parent'], $element['title'], $element['filter']); - $this->ioStyle->text($text); - } - } - - // Construct a comma-separated string from the taxonomy ids. - $taxonomyIds = empty($tids) ? '' : implode(',', $tids); - - // Update the filter with the new taxonomy ids. - $query = $db->getQuery(true); - $query - ->update($db->quoteName('#__finder_filters')) - ->set($db->quoteName('data') . ' = ' . $db->quote($taxonomyIds)) - ->where($db->quoteName('filter_id') . ' = ' . (int) $filter_id); - $db->setQuery($query)->execute(); - } - - $this->ioStyle->text(Text::sprintf('FINDER_CLI_RESTORE_FILTER_COMPLETED', count($this->filters))); - } - + $this->setDescription('Purges and rebuild the index'); + $this->setHelp($help); + } + + /** + * Internal function to execute the command. + * + * @param InputInterface $input The input to inject into the command. + * @param OutputInterface $output The output to inject into the command. + * + * @return integer The command exit code + * + * @since 4.0.0 + */ + protected function doExecute(InputInterface $input, OutputInterface $output): int + { + + // Initialize the time value. + $this->time = microtime(true); + $this->configureIO($input, $output); + + $this->ioStyle->writeln( + [ + 'Finder Indexer', + '==========================', + '', + ] + ); + + if ($this->cliInput->getOption('minproctime')) { + $this->minimumBatchProcessingTime = $this->cliInput->getOption('minproctime'); + } + + if ($this->cliInput->getOption('pause')) { + $this->pause = $this->cliInput->getOption('pause'); + } + + if ($this->cliInput->getOption('divisor')) { + $this->divisor = $this->cliInput->getOption('divisor'); + } + + if ($this->cliInput->getArgument('purge')) { + // Taxonomy ids will change following a purge/index, so save filter information first. + $this->getFilters(); + + // Purge the index. + $this->purge(); + + // Run the indexer. + $this->index(); + + // Restore the filters again. + $this->putFilters(); + } else { + $this->index(); + } + + $this->ioStyle->newLine(1); + + // Total reporting. + $this->ioStyle->writeln( + [ + '' . Text::sprintf('FINDER_CLI_PROCESS_COMPLETE', round(microtime(true) - $this->time, 3)) . '', + '' . Text::sprintf('FINDER_CLI_PEAK_MEMORY_USAGE', number_format(memory_get_peak_usage(true))) . '', + ] + ); + + $this->ioStyle->newLine(1); + + return Command::SUCCESS; + } + + /** + * Configures the IO + * + * @param InputInterface $input Console Input + * @param OutputInterface $output Console Output + * + * @return void + * + * @since 4.0.0 + * + */ + private function configureIO(InputInterface $input, OutputInterface $output): void + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + $language = Factory::getLanguage(); + $language->load('', JPATH_ADMINISTRATOR, null, false, false) || + $language->load('', JPATH_ADMINISTRATOR, null, true); + $language->load('finder_cli', JPATH_SITE, null, false, false) || + $language->load('finder_cli', JPATH_SITE, null, true); + } + + /** + * Save static filters. + * + * Since a purge/index cycle will cause all the taxonomy ids to change, + * the static filters need to be updated with the new taxonomy ids. + * The static filter information is saved prior to the purge/index + * so that it can later be used to update the filters with new ids. + * + * @return void + * + * @since 4.0.0 + */ + private function getFilters(): void + { + $this->ioStyle->text(Text::_('FINDER_CLI_SAVE_FILTERS')); + + // Get the taxonomy ids used by the filters. + $db = $this->db; + $query = $db->getQuery(true); + $query + ->select('filter_id, title, data') + ->from($db->quoteName('#__finder_filters')); + $filters = $db->setQuery($query)->loadObjectList(); + + // Get the name of each taxonomy and the name of its parent. + foreach ($filters as $filter) { + // Skip empty filters. + if ($filter->data === '') { + continue; + } + + // Get taxonomy records. + $query = $db->getQuery(true); + $query + ->select('t.title, p.title AS parent') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t') + ->leftJoin($db->quoteName('#__finder_taxonomy') . ' AS p ON p.id = t.parent_id') + ->where($db->quoteName('t.id') . ' IN (' . $filter->data . ')'); + $taxonomies = $db->setQuery($query)->loadObjectList(); + + // Construct a temporary data structure to hold the filter information. + foreach ($taxonomies as $taxonomy) { + $this->filters[$filter->filter_id][] = array( + 'filter' => $filter->title, + 'title' => $taxonomy->title, + 'parent' => $taxonomy->parent, + ); + } + } + + $this->ioStyle->text(Text::sprintf('FINDER_CLI_SAVE_FILTER_COMPLETED', count($filters))); + } + + /** + * Purge the index. + * + * @return void + * + * @since 3.3 + */ + private function purge() + { + $this->ioStyle->text(Text::_('FINDER_CLI_INDEX_PURGE')); + + // Load the model. + $app = $this->getApplication(); + $model = $app->bootComponent('com_finder')->getMVCFactory($app)->createModel('Index', 'Administrator'); + + // Attempt to purge the index. + $return = $model->purge(); + + // If unsuccessful then abort. + if (!$return) { + $message = Text::_('FINDER_CLI_INDEX_PURGE_FAILED', $model->getError()); + $this->ioStyle->error($message); + exit(); + } + + $this->ioStyle->text(Text::_('FINDER_CLI_INDEX_PURGE_SUCCESS')); + } + + /** + * Run the indexer. + * + * @return void + * + * @since 2.5 + */ + private function index() + { + + // Disable caching. + $app = $this->getApplication(); + $app->set('caching', 0); + $app->set('cache_handler', 'file'); + + // Reset the indexer state. + Indexer::resetState(); + + // Import the plugins. + PluginHelper::importPlugin('system'); + PluginHelper::importPlugin('finder'); + + // Starting Indexer. + $this->ioStyle->text(Text::_('FINDER_CLI_STARTING_INDEXER')); + + // Trigger the onStartIndex event. + $app->triggerEvent('onStartIndex'); + + // Remove the script time limit. + @set_time_limit(0); + + // Get the indexer state. + $state = Indexer::getState(); + + // Setting up plugins. + $this->ioStyle->text(Text::_('FINDER_CLI_SETTING_UP_PLUGINS')); + + // Trigger the onBeforeIndex event. + $app->triggerEvent('onBeforeIndex'); + + // Startup reporting. + $this->ioStyle->text(Text::sprintf('FINDER_CLI_SETUP_ITEMS', $state->totalItems, round(microtime(true) - $this->time, 3))); + + // Get the number of batches. + $t = (int) $state->totalItems; + $c = (int) ceil($t / $state->batchSize); + $c = $c === 0 ? 1 : $c; + + try { + // Process the batches. + for ($i = 0; $i < $c; $i++) { + // Set the batch start time. + $this->qtime = microtime(true); + + // Reset the batch offset. + $state->batchOffset = 0; + + // Trigger the onBuildIndex event. + Factory::getApplication()->triggerEvent('onBuildIndex'); + + // Batch reporting. + $text = Text::sprintf('FINDER_CLI_BATCH_COMPLETE', $i + 1, $processingTime = round(microtime(true) - $this->qtime, 3)); + $this->ioStyle->text($text); + + if ($this->pause !== 0) { + // Pausing Section + $skip = !($processingTime >= $this->minimumBatchProcessingTime); + $pause = 0; + + if ($this->pause === 'division' && $this->divisor > 0) { + if (!$skip) { + $pause = round($processingTime / $this->divisor); + } else { + $pause = 1; + } + } elseif ($this->pause > 0) { + $pause = $this->pause; + } + + if ($pause > 0 && !$skip) { + $this->ioStyle->text(Text::sprintf('FINDER_CLI_BATCH_PAUSING', $pause)); + sleep($pause); + $this->ioStyle->text(Text::_('FINDER_CLI_BATCH_CONTINUING')); + } + + if ($skip) { + $this->ioStyle->text( + Text::sprintf( + 'FINDER_CLI_SKIPPING_PAUSE_LOW_BATCH_PROCESSING_TIME', + $processingTime, + $this->minimumBatchProcessingTime + ) + ); + } + + // End of Pausing Section + } + } + } catch (Exception $e) { + // Display the error + $this->ioStyle->error($e->getMessage()); + + // Reset the indexer state. + Indexer::resetState(); + + // Close the app + $app->close($e->getCode()); + } + + // Reset the indexer state. + Indexer::resetState(); + } + + /** + * Restore static filters. + * + * Using the saved filter information, update the filter records + * with the new taxonomy ids. + * + * @return void + * + * @since 3.3 + */ + private function putFilters() + { + $this->ioStyle->text(Text::_('FINDER_CLI_RESTORE_FILTERS')); + + $db = $this->db; + + // Use the temporary filter information to update the filter taxonomy ids. + foreach ($this->filters as $filter_id => $filter) { + $tids = array(); + + foreach ($filter as $element) { + // Look for the old taxonomy in the new taxonomy table. + $query = $db->getQuery(true); + $query + ->select('t.id') + ->from($db->quoteName('#__finder_taxonomy') . ' AS t') + ->leftJoin($db->quoteName('#__finder_taxonomy') . ' AS p ON p.id = t.parent_id') + ->where($db->quoteName('t.title') . ' = ' . $db->quote($element['title'])) + ->where($db->quoteName('p.title') . ' = ' . $db->quote($element['parent'])); + $taxonomy = $db->setQuery($query)->loadResult(); + + // If we found it then add it to the list. + if ($taxonomy) { + $tids[] = $taxonomy; + } else { + $text = Text::sprintf('FINDER_CLI_FILTER_RESTORE_WARNING', $element['parent'], $element['title'], $element['filter']); + $this->ioStyle->text($text); + } + } + + // Construct a comma-separated string from the taxonomy ids. + $taxonomyIds = empty($tids) ? '' : implode(',', $tids); + + // Update the filter with the new taxonomy ids. + $query = $db->getQuery(true); + $query + ->update($db->quoteName('#__finder_filters')) + ->set($db->quoteName('data') . ' = ' . $db->quote($taxonomyIds)) + ->where($db->quoteName('filter_id') . ' = ' . (int) $filter_id); + $db->setQuery($query)->execute(); + } + + $this->ioStyle->text(Text::sprintf('FINDER_CLI_RESTORE_FILTER_COMPLETED', count($this->filters))); + } } diff --git a/libraries/src/Console/GetConfigurationCommand.php b/libraries/src/Console/GetConfigurationCommand.php index 4b1f14befa16c..43c9f8a3a51ac 100644 --- a/libraries/src/Console/GetConfigurationCommand.php +++ b/libraries/src/Console/GetConfigurationCommand.php @@ -1,4 +1,5 @@ 'db', - 'options' => [ - 'dbtype', - 'host', - 'user', - 'password', - 'dbprefix', - 'db', - 'dbencryption', - 'dbsslverifyservercert', - 'dbsslkey', - 'dbsslcert', - 'dbsslca', - 'dbsslcipher' - ] - ]; - - /** - * Constant defining the Session option group - * @var array - * @since 4.0.0 - */ - public const SESSION_GROUP = [ - 'name' => 'session', - 'options' => [ - 'session_handler', - 'shared_session', - 'session_metadata' - ] - ]; - - /** - * Constant defining the Mail option group - * @var array - * @since 4.0.0 - */ - public const MAIL_GROUP = [ - 'name' => 'mail', - 'options' => [ - 'mailonline', - 'mailer', - 'mailfrom', - 'fromname', - 'sendmail', - 'smtpauth', - 'smtpuser', - 'smtppass', - 'smtphost', - 'smtpsecure', - 'smtpport' - ] - ]; - - /** - * Return code if configuration is get successfully - * @since 4.0.0 - */ - public const CONFIG_GET_SUCCESSFUL = 0; - - /** - * Return code if configuration group option is not found - * @since 4.0.0 - */ - public const CONFIG_GET_GROUP_NOT_FOUND = 1; - - /** - * Return code if configuration option is not found - * @since 4.0.0 - */ - public const CONFIG_GET_OPTION_NOT_FOUND = 2; - - /** - * Return code if the command has been invoked with wrong options - * @since 4.0.0 - */ - public const CONFIG_GET_OPTION_FAILED = 3; - - /** - * Configures the IO - * - * @param InputInterface $input Console Input - * @param OutputInterface $output Console Output - * - * @return void - * - * @since 4.0.0 - * - */ - private function configureIO(InputInterface $input, OutputInterface $output) - { - $this->cliInput = $input; - $this->ioStyle = new SymfonyStyle($input, $output); - } - - - /** - * Displays logically grouped options - * - * @param string $group The group to be processed - * - * @return integer - * - * @since 4.0.0 - */ - public function processGroupOptions($group): int - { - $configs = $this->getApplication()->getConfig()->toArray(); - $configs = $this->formatConfig($configs); - - $groups = $this->getGroups(); - - $foundGroup = false; - - foreach ($groups as $key => $value) - { - if ($value['name'] === $group) - { - $foundGroup = true; - $options = []; - - foreach ($value['options'] as $option) - { - $options[] = [$option, $configs[$option]]; - } - - $this->ioStyle->table(['Option', 'Value'], $options); - } - } - - if (!$foundGroup) - { - $this->ioStyle->error("Group *$group* not found"); - - return self::CONFIG_GET_GROUP_NOT_FOUND; - } - - return self::CONFIG_GET_SUCCESSFUL; - } - - /** - * Gets the defined option groups - * - * @return array - * - * @since 4.0.0 - */ - public function getGroups() - { - return [ - self::DB_GROUP, - self::MAIL_GROUP, - self::SESSION_GROUP - ]; - } - - /** - * Formats the configuration array into desired format - * - * @param array $configs Array of the configurations - * - * @return array - * - * @since 4.0.0 - */ - public function formatConfig(Array $configs): array - { - $newConfig = []; - - foreach ($configs as $key => $config) - { - $config = $config === false ? "false" : $config; - $config = $config === true ? "true" : $config; - - if (!in_array($key, ['cwd', 'execution'])) - { - $newConfig[$key] = $config; - } - } - - return $newConfig; - } - - /** - * Handles the command when a single option is requested - * - * @param string $option The option we want to get its value - * - * @return integer - * - * @since 4.0.0 - */ - public function processSingleOption($option): int - { - $configs = $this->getApplication()->getConfig()->toArray(); - - if (!array_key_exists($option, $configs)) - { - $this->ioStyle->error("Can't find option *$option* in configuration list"); - - return self::CONFIG_GET_OPTION_NOT_FOUND; - } - - $value = $this->formatConfigValue($this->getApplication()->get($option)); - - $this->ioStyle->table(['Option', 'Value'], [[$option, $value]]); - - return self::CONFIG_GET_SUCCESSFUL; - } - - /** - * Formats the Configuration value - * - * @param mixed $value Value to be formatted - * - * @return string - * - * @since 4.0.0 - */ - protected function formatConfigValue($value): string - { - if ($value === false) - { - return 'false'; - } - elseif ($value === true) - { - return 'true'; - } - elseif ($value === null) - { - return 'Not Set'; - } - elseif (\is_array($value)) - { - return \json_encode($value); - } - elseif (\is_object($value)) - { - return \json_encode(\get_object_vars($value)); - } - else - { - return $value; - } - } - - /** - * Initialise the command. - * - * @return void - * - * @since 4.0.0 - */ - protected function configure(): void - { - $groups = $this->getGroups(); - - foreach ($groups as $key => $group) - { - $groupNames[] = $group['name']; - } - - $groupNames = implode(', ', $groupNames); - - $this->addArgument('option', null, 'Name of the option'); - $this->addOption('group', 'g', InputOption::VALUE_REQUIRED, 'Name of the option'); - - $help = "%command.name% displays the current value of a configuration option + /** + * The default command name + * + * @var string + * @since 4.0.0 + */ + protected static $defaultName = 'config:get'; + + /** + * Stores the Input Object + * @var Input + * @since 4.0.0 + */ + private $cliInput; + + /** + * SymfonyStyle Object + * @var SymfonyStyle + * @since 4.0.0 + */ + private $ioStyle; + + /** + * Constant defining the Database option group + * @var array + * @since 4.0.0 + */ + public const DB_GROUP = [ + 'name' => 'db', + 'options' => [ + 'dbtype', + 'host', + 'user', + 'password', + 'dbprefix', + 'db', + 'dbencryption', + 'dbsslverifyservercert', + 'dbsslkey', + 'dbsslcert', + 'dbsslca', + 'dbsslcipher' + ] + ]; + + /** + * Constant defining the Session option group + * @var array + * @since 4.0.0 + */ + public const SESSION_GROUP = [ + 'name' => 'session', + 'options' => [ + 'session_handler', + 'shared_session', + 'session_metadata' + ] + ]; + + /** + * Constant defining the Mail option group + * @var array + * @since 4.0.0 + */ + public const MAIL_GROUP = [ + 'name' => 'mail', + 'options' => [ + 'mailonline', + 'mailer', + 'mailfrom', + 'fromname', + 'sendmail', + 'smtpauth', + 'smtpuser', + 'smtppass', + 'smtphost', + 'smtpsecure', + 'smtpport' + ] + ]; + + /** + * Return code if configuration is get successfully + * @since 4.0.0 + */ + public const CONFIG_GET_SUCCESSFUL = 0; + + /** + * Return code if configuration group option is not found + * @since 4.0.0 + */ + public const CONFIG_GET_GROUP_NOT_FOUND = 1; + + /** + * Return code if configuration option is not found + * @since 4.0.0 + */ + public const CONFIG_GET_OPTION_NOT_FOUND = 2; + + /** + * Return code if the command has been invoked with wrong options + * @since 4.0.0 + */ + public const CONFIG_GET_OPTION_FAILED = 3; + + /** + * Configures the IO + * + * @param InputInterface $input Console Input + * @param OutputInterface $output Console Output + * + * @return void + * + * @since 4.0.0 + * + */ + private function configureIO(InputInterface $input, OutputInterface $output) + { + $this->cliInput = $input; + $this->ioStyle = new SymfonyStyle($input, $output); + } + + + /** + * Displays logically grouped options + * + * @param string $group The group to be processed + * + * @return integer + * + * @since 4.0.0 + */ + public function processGroupOptions($group): int + { + $configs = $this->getApplication()->getConfig()->toArray(); + $configs = $this->formatConfig($configs); + + $groups = $this->getGroups(); + + $foundGroup = false; + + foreach ($groups as $key => $value) { + if ($value['name'] === $group) { + $foundGroup = true; + $options = []; + + foreach ($value['options'] as $option) { + $options[] = [$option, $configs[$option]]; + } + + $this->ioStyle->table(['Option', 'Value'], $options); + } + } + + if (!$foundGroup) { + $this->ioStyle->error("Group *$group* not found"); + + return self::CONFIG_GET_GROUP_NOT_FOUND; + } + + return self::CONFIG_GET_SUCCESSFUL; + } + + /** + * Gets the defined option groups + * + * @return array + * + * @since 4.0.0 + */ + public function getGroups() + { + return [ + self::DB_GROUP, + self::MAIL_GROUP, + self::SESSION_GROUP + ]; + } + + /** + * Formats the configuration array into desired format + * + * @param array $configs Array of the configurations + * + * @return array + * + * @since 4.0.0 + */ + public function formatConfig(array $configs): array + { + $newConfig = []; + + foreach ($configs as $key => $config) { + $config = $config === false ? "false" : $config; + $config = $config === true ? "true" : $config; + + if (!in_array($key, ['cwd', 'execution'])) { + $newConfig[$key] = $config; + } + } + + return $newConfig; + } + + /** + * Handles the command when a single option is requested + * + * @param string $option The option we want to get its value + * + * @return integer + * + * @since 4.0.0 + */ + public function processSingleOption($option): int + { + $configs = $this->getApplication()->getConfig()->toArray(); + + if (!array_key_exists($option, $configs)) { + $this->ioStyle->error("Can't find option *$option* in configuration list"); + + return self::CONFIG_GET_OPTION_NOT_FOUND; + } + + $value = $this->formatConfigValue($this->getApplication()->get($option)); + + $this->ioStyle->table(['Option', 'Value'], [[$option, $value]]); + + return self::CONFIG_GET_SUCCESSFUL; + } + + /** + * Formats the Configuration value + * + * @param mixed $value Value to be formatted + * + * @return string + * + * @since 4.0.0 + */ + protected function formatConfigValue($value): string + { + if ($value === false) { + return 'false'; + } elseif ($value === true) { + return 'true'; + } elseif ($value === null) { + return 'Not Set'; + } elseif (\is_array($value)) { + return \json_encode($value); + } elseif (\is_object($value)) { + return \json_encode(\get_object_vars($value)); + } else { + return $value; + } + } + + /** + * Initialise the command. + * + * @return void + * + * @since 4.0.0 + */ + protected function configure(): void + { + $groups = $this->getGroups(); + + foreach ($groups as $key => $group) { + $groupNames[] = $group['name']; + } + + $groupNames = implode(', ', $groupNames); + + $this->addArgument('option', null, 'Name of the option'); + $this->addOption('group', 'g', InputOption::VALUE_REQUIRED, 'Name of the option'); + + $help = "%command.name% displays the current value of a configuration option \nUsage: php %command.full_name%